Для backend на Java нужен небольшой, но плотный минимум: статические типы и var, классы и record для данных, коллекции List, Set и Map, Stream API для их обработки, Optional вместо null и понимание checked- и unchecked-исключений. Проект собирает Maven: pom.xml описывает версию Java и зависимости, а mvn package компилирует код, прогоняет тесты JUnit и упаковывает jar. Актуальная LTS-версия — Java 25.
После материала вы сможете
- описать данные через
recordс проверкой в компактном конструкторе иenumдля фиксированного набора значений - выбрать между
List,SetиMapи обработать коллекцию через Stream API:filter,map,sorted,groupingBy - вернуть
Optionalвместоnullи отличить checked-исключение от unchecked - собрать Maven-проект с JUnit-тестами командой
mvn packageи запустить полученныйjar
Проверено на
Java 25 LTS (Eclipse Temurin 25.0.4)Maven 3.10.0JUnit 6.1.3maven-compiler-plugin 3.16.0maven-surefire-plugin 3.6.0
Сверено с документацией: 11 октября 2026 г.
Содержание
Java и JVM: от исходника до запуска
Java — компилируемый язык со статической типизацией: типы переменных и методов проверяет компилятор javac ещё до запуска. Он превращает исходник .java не в машинный код, а в байткод — файлы .class, которые исполняет виртуальная машина Java (JVM). Поэтому один и тот же jar-архив запускается на Linux-сервере, в Docker-контейнере и на ноутбуке разработчика. Набор для разработки называется JDK: в нём компилятор, JVM и утилиты; на сервере для запуска достаточно среды выполнения — например, образа eclipse-temurin:25-jre.
Новая версия Java выходит каждые полгода, но для серверов выбирают версии с долгосрочной поддержкой — LTS. По дорожной карте Oracle актуальная LTS — Java 25 (сентябрь 2025), предыдущая — Java 21. Весь код урока проверен на Java 25 (Eclipse Temurin 25.0.4); классы проекта, кроме Hello.java, компилируются и на Java 21.
// Компактный исходный файл (Java 25): без объявления класса и без static
void main() {
String name = "InterLAB";
int year = 2026;
var services = 3; // var — тип int выведен из значения
IO.println("Привет, " + name + "! Год: " + year + ", услуг: " + services);
}$ java --version
openjdk 25.0.4.1 2026-08-18 LTS
…
$ java Hello.java
Привет, InterLAB! Год: 2026, услуг: 3java Hello.java компилирует файл в памяти и сразу запускает его — без отдельного вызова javac.Типы, var, классы, enum и record
В Java два вида типов. Примитивы (int, long, double, boolean, char) хранят само значение и не бывают null. Всё остальное — ссылочные типы: String, List, ваши классы и записи. Переменная ссылочного типа хранит ссылку на объект и может быть null — отсюда самая частая ошибка новичка, NullPointerException.
int
- Что хранит
- целое число до ~2,1 млрд
- Пример
int minutes = 60;
long
- Что хранит
- большое целое: идентификаторы, суммы в тиынах
- Пример
long id = 10_000_000_000L;
double
- Что хранит
- дробное число с плавающей точкой
- Пример
double rating = 4.8;
boolean
- Что хранит
trueилиfalse- Пример
boolean active = true;
String
- Что хранит
- ссылка на неизменяемую строку
- Пример
String title = "Маникюр";
BigDecimal
- Что хранит
- точная десятичная дробь — для денег с копейками
- Пример
new BigDecimal("19.99")
Ключевое слово var (с Java 10) просит компилятор вывести тип из значения: var services = 3 — это по-прежнему int, а не «любой тип». Как пишет dev.java, var работает только для локальных переменных с инициализатором — не для полей и не для параметров методов; строка var x; без значения не скомпилируется.
Практика урока — каталог услуг салона. Набор категорий фиксирован, поэтому это enum: компилятор не даст передать несуществующую категорию строкой с опечаткой. Сама услуга — record.
package com.example.catalog;
public enum Category {
HAIR,
NAILS,
SPA
}package com.example.catalog;
// record — неизменяемый носитель данных: поля, конструктор, методы доступа,
// equals, hashCode и toString создаёт компилятор
public record ServiceItem(String code, String title, Category category, int price, int minutes) {
// компактный конструктор: проверки выполняются до присваивания полей
public ServiceItem {
if (code == null || code.isBlank()) {
throw new IllegalArgumentException("Код услуги не может быть пустым");
}
if (price < 0) {
throw new IllegalArgumentException("Цена не может быть отрицательной: " + price);
}
if (minutes <= 0) {
throw new IllegalArgumentException("Длительность должна быть больше нуля: " + minutes);
}
}
}Одна строка record ServiceItem(...) заменяет класс на полсотни строк. По документации dev.java компилятор создаёт для каждого компонента поле private final, метод доступа с тем же именем (item.price(), а не getPrice()), канонический конструктор и методы equals, hashCode, toString. Класс записи неявно final, наследоваться от другого класса не может, а добавить поле экземпляра сверх компонентов нельзя. Компактный конструктор — место для проверок: объект с отрицательной ценой просто не будет создан.
Коллекции: List, Set и Map
Коллекции в Java — это интерфейсы и их реализации. Переменную объявляют типом интерфейса (List<String>), а конкретный класс выбирают справа от = (new ArrayList<>()): так реализацию можно заменить, не трогая остальной код. Угловые скобки — дженерики: List<String> хранит только строки, и компилятор это проверяет.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class CollectionsDemo {
public static void main(String[] args) {
// List — упорядоченный список, дубликаты разрешены
List<String> visits = new ArrayList<>();
visits.add("стрижка");
visits.add("маникюр");
visits.add("стрижка");
System.out.println(visits + ", размер " + visits.size()); // [стрижка, маникюр, стрижка], размер 3
System.out.println(visits.get(1)); // маникюр
// Set — только уникальные значения
Set<String> unique = new HashSet<>(visits);
System.out.println(unique.size() + " " + unique.contains("стрижка")); // 2 true
// Map — пары «ключ → значение», ключи уникальны
Map<String, Integer> prices = new HashMap<>();
prices.put("стрижка", 9000);
prices.put("маникюр", 8000);
prices.put("стрижка", 9500); // тот же ключ: значение перезаписано
System.out.println(prices.get("стрижка")); // 9500
System.out.println(prices.get("педикюр")); // null — ключа нет
System.out.println(prices.getOrDefault("педикюр", 0)); // 0
// List.of — неизменяемый список
List<String> fixed = List.of("HAIR", "NAILS", "SPA");
try {
fixed.add("BARBER");
} catch (UnsupportedOperationException e) {
System.out.println("List.of нельзя изменить"); // List.of нельзя изменить
}
}
}java CollectionsDemo.java. Комментарии справа — фактический вывод.List
- Частая реализация
ArrayList- Когда брать
- упорядоченный набор, доступ по индексу, дубликаты допустимы
Set
- Частая реализация
HashSet,TreeSet- Когда брать
- только уникальные значения; быстрая проверка
contains;TreeSetхранит в порядке сортировки
Map
- Частая реализация
HashMap,LinkedHashMap,TreeMap- Когда брать
- поиск по ключу;
LinkedHashMapпомнит порядок добавления,TreeMapсортирует ключи
Stream API: фильтры, сортировка, группировка
Stream — это конвейер обработки данных из коллекции. Он сам ничего не хранит: источник (values().stream()) отдаёт элементы, промежуточные операции (filter, map, sorted) лишь описывают шаги и возвращают новый поток, а работа начинается на терминальной операции (toList, collect, sum). Каталог услуг целиком построен на этих операциях.
package com.example.catalog;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;
public class ServiceCatalog {
// LinkedHashMap помнит порядок добавления; ключ — код услуги
private final Map<String, ServiceItem> byCode = new LinkedHashMap<>();
public void add(ServiceItem item) {
if (byCode.containsKey(item.code())) {
throw new IllegalArgumentException("Услуга с кодом " + item.code() + " уже есть");
}
byCode.put(item.code(), item);
}
public int size() {
return byCode.size();
}
// услуги может не оказаться — сообщаем об этом типом, а не null
public Optional<ServiceItem> findByCode(String code) {
return Optional.ofNullable(byCode.get(code));
}
// услуги категории — от дешёвых к дорогим
public List<ServiceItem> byCategory(Category category) {
return byCode.values().stream()
.filter(item -> item.category() == category)
.sorted(Comparator.comparingInt(ServiceItem::price))
.toList();
}
// названия услуг, которые укладываются в бюджет
public List<String> titlesWithin(int budget) {
return byCode.values().stream()
.filter(item -> item.price() <= budget)
.map(ServiceItem::title)
.toList();
}
// категории, в которых есть хотя бы одна услуга; TreeSet хранит их в порядке enum
public Set<Category> categories() {
return byCode.values().stream()
.map(ServiceItem::category)
.collect(Collectors.toCollection(TreeSet::new));
}
// сколько услуг в каждой категории: {HAIR=2, NAILS=1, SPA=2}
public Map<Category, Long> countByCategory() {
return byCode.values().stream()
.collect(Collectors.groupingBy(ServiceItem::category, TreeMap::new, Collectors.counting()));
}
// длительность визита из нескольких услуг; неизвестный код — ошибка вызывающего
public int totalMinutes(List<String> codes) {
return codes.stream()
.map(code -> findByCode(code)
.orElseThrow(() -> new IllegalArgumentException("Нет услуги с кодом " + code)))
.mapToInt(ServiceItem::minutes)
.sum();
}
}filter(item -> …)
- Вид
- промежуточная
- Что делает в каталоге
- оставляет услуги нужной категории или в пределах бюджета
map(ServiceItem::title)
- Вид
- промежуточная
- Что делает в каталоге
- превращает услугу в её название
sorted(Comparator.comparingInt(…))
- Вид
- промежуточная
- Что делает в каталоге
- сортирует по цене
toList()
- Вид
- терминальная
- Что делает в каталоге
- собирает результат в неизменяемый
List
collect(groupingBy(…, counting()))
- Вид
- терминальная
- Что делает в каталоге
- считает услуги по категориям в
Map
mapToInt(…).sum()
- Вид
- терминальная
- Что делает в каталоге
- суммирует минуты визита
ServiceItem::title — ссылка на метод, короткая запись лямбды item -> item.title(). Поле byCode объявлено private final: снаружи каталог меняют только через add, который защищает инвариант «код услуги уникален». Методы возвращают результат toList() — по документации Java SE это неизменяемый список, так что вызывающий код не испортит данные каталога.
Optional вместо null
Метод findByCode может не найти услугу. Вернуть null — значит переложить проверку на память вызывающего: забыл — получил NullPointerException где-то дальше. Optional<ServiceItem> говорит об отсутствии результата прямо в сигнатуре. Пустой Optional создаёт Optional.empty(), а Optional.ofNullable(map.get(code)) превращает возможный null в пустой контейнер.
Так
findByCode(code).map(ServiceItem::title).orElse("нет такой услуги")— значение по умолчаниюfindByCode(code).orElseThrow(() -> new IllegalArgumentException(...))— отсутствие считается ошибкойOptionalкак тип результата метода
Не так
optional.get()без проверки — документация называетorElseThrow()предпочтительной заменойreturn null;из метода, который возвращаетOptionalOptionalв полях, параметрах и элементах коллекций
Последний пункт — не вкусовщина: в документации класса сказано, что Optional задуман прежде всего как тип возвращаемого значения там, где явно нужно выразить «нет результата», а dev.java прямо называет поля, коллекции и аргументы неподходящим местом. Метод totalMinutes в каталоге показывает обе стороны: внутри потока orElseThrow превращает неизвестный код в IllegalArgumentException с понятным сообщением.
Исключения: checked и unchecked
Исключение — объект ошибки, который JVM передаёт вверх по стеку вызовов, пока его не поймает подходящий catch. В Java два вида исключений. Проверяемые (checked, наследники Exception, например IOException) подчиняются правилу «поймай или объяви»: компилятор не соберёт код, который их игнорирует. Непроверяемые (RuntimeException, Error и их наследники) компилятор не отслеживает.
package com.example.catalog;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public final class CatalogLoader {
private CatalogLoader() {
}
// IOException — проверяемое исключение: компилятор заставит вызывающий код
// либо поймать его, либо объявить в своём throws
public static ServiceCatalog load(Path file) throws IOException {
List<String> lines = Files.readAllLines(file);
ServiceCatalog catalog = new ServiceCatalog();
for (int i = 1; i < lines.size(); i++) { // строка 0 — заголовок
String line = lines.get(i);
if (line.isBlank()) {
continue;
}
catalog.add(parse(line, i + 1));
}
return catalog;
}
static ServiceItem parse(String line, int lineNumber) {
String[] parts = line.split(";");
if (parts.length != 5) {
throw new IllegalArgumentException("Строка " + lineNumber + ": ожидалось 5 полей, получено " + parts.length);
}
try {
return new ServiceItem(
parts[0].trim(),
parts[1].trim(),
Category.valueOf(parts[2].trim()),
Integer.parseInt(parts[3].trim()),
Integer.parseInt(parts[4].trim()));
} catch (IllegalArgumentException e) { // NumberFormatException — её подкласс
throw new IllegalArgumentException("Строка " + lineNumber + ": " + e.getMessage(), e);
}
}
}Если убрать throws IOException из метода, который читает файл, компилятор остановит сборку:
$ javac Neg.java
Neg.java:6: error: unreported exception IOException; must be caught or declared to be thrown
return Files.readString(p);
^
1 errorjavac 25 для метода с Files.readString без throws.package com.example.catalog;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.List;
public class Main {
public static void main(String[] args) {
Path file = Path.of(args.length > 0 ? args[0] : "services.csv");
ServiceCatalog catalog;
try {
catalog = CatalogLoader.load(file);
} catch (NoSuchFileException e) { // более узкий тип ловим раньше общего
System.err.println("Файл не найден: " + file);
return;
} catch (IOException e) {
System.err.println("Не удалось прочитать " + file + ": " + e.getMessage());
return;
}
System.out.println("Услуг в каталоге: " + catalog.size());
System.out.println("Категории: " + catalog.categories());
System.out.println("По категориям: " + catalog.countByCategory());
System.out.println("До 10 000 ₸: " + catalog.titlesWithin(10_000));
String spa = catalog.findByCode("SPA-01")
.map(ServiceItem::title)
.orElse("нет такой услуги");
System.out.println("SPA-01: " + spa);
String unknown = catalog.findByCode("X-99")
.map(ServiceItem::title)
.orElse("нет такой услуги");
System.out.println("X-99: " + unknown);
System.out.println("Визит, минут: " + catalog.totalMinutes(List.of("HAIR-01", "NAILS-01")));
}
}checked
- Примеры
IOException,NoSuchFileException- Что делать
- внешний сбой, от которого можно оправиться: поймать и сообщить или объявить в
throws
unchecked: RuntimeException
- Примеры
IllegalArgumentException,NullPointerException,NumberFormatException- Что делать
- ошибка в данных или в коде: проверить вход и бросить с понятным сообщением, исправить баг
Error
- Примеры
OutOfMemoryError,StackOverflowError- Что делать
- не ловить в обычном коде — это проблема среды или программы целиком
Правило из руководства dev.java: если вызывающий код может разумно восстановиться после ошибки — исключение делают проверяемым, если нет — непроверяемым. В Main блоки catch идут от узкого типа к широкому: NoSuchFileException — наследник IOException, и в обратном порядке код не скомпилируется. В CatalogLoader.parse исходная ошибка не теряется: она передаётся вторым аргументом (cause), и в стектрейсе видны обе причины.
Maven: pom.xml, сборка и тест на JUnit
Maven — стандартный инструмент сборки Java-проектов: он скачивает зависимости, компилирует код, запускает тесты и упаковывает jar. Всё описано в pom.xml, а структура папок задана соглашением, поэтому её не нужно настраивать:
pom.xml
координаты проекта (
groupId,artifactId,version), версия Java, зависимости и плагиныsrc/main/java
код приложения; путь папок повторяет пакет:
com/example/catalogsrc/test/java
тесты; в итоговый
jarони не попадаютtarget
результат сборки:
.class-файлы, отчёты тестов иcatalog-1.0-SNAPSHOT.jar; в Git не коммитят
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>catalog</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>6.1.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>maven.compiler.release задаёт версию Java; BOM junit-bom выравнивает версии всех модулей JUnit, поэтому у junit-jupiter версия не указана.Тесты пишем на JUnit 6 — текущей версии фреймворка (6.1.3 на дату урока). Аннотации и проверки лежат в том же пакете org.junit.jupiter.api, что и в JUnit 5; JUnit 6 требует Java 17+ и Maven Surefire не ниже 3.0.0. @BeforeEach готовит свежий каталог перед каждым тестом, assertEquals сравнивает ожидаемое с фактическим, assertThrows проверяет, что код бросает нужное исключение, и возвращает его для проверки сообщения.
package com.example.catalog;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class ServiceCatalogTest {
private ServiceCatalog catalog;
@BeforeEach
void setUp() { // перед каждым тестом — новый каталог
catalog = new ServiceCatalog();
catalog.add(new ServiceItem("HAIR-02", "Окрашивание", Category.HAIR, 25000, 150));
catalog.add(new ServiceItem("HAIR-01", "Женская стрижка", Category.HAIR, 9000, 60));
catalog.add(new ServiceItem("SPA-01", "Массаж спины", Category.SPA, 12000, 45));
}
@Test
void sortsCategoryByPrice() {
List<String> codes = catalog.byCategory(Category.HAIR).stream()
.map(ServiceItem::code)
.toList();
assertEquals(List.of("HAIR-01", "HAIR-02"), codes);
}
@Test
void returnsEmptyOptionalForUnknownCode() {
assertTrue(catalog.findByCode("X-99").isEmpty());
assertEquals("Массаж спины", catalog.findByCode("SPA-01").orElseThrow().title());
}
@Test
void countsByCategory() {
assertEquals(Map.of(Category.HAIR, 2L, Category.SPA, 1L), catalog.countByCategory());
}
@Test
void rejectsDuplicateCode() {
ServiceItem duplicate = new ServiceItem("SPA-01", "Другой массаж", Category.SPA, 10000, 30);
IllegalArgumentException error =
assertThrows(IllegalArgumentException.class, () -> catalog.add(duplicate));
assertEquals("Услуга с кодом SPA-01 уже есть", error.getMessage());
}
@Test
void recordRejectsNegativePrice() {
assertThrows(IllegalArgumentException.class,
() -> new ServiceItem("BAD-01", "Ошибка", Category.SPA, -1, 30));
}
@Test
void resultListIsUnmodifiable() {
List<ServiceItem> hair = catalog.byCategory(Category.HAIR);
assertThrows(UnsupportedOperationException.class, () -> hair.add(hair.get(0)));
}
@Test
void sumsMinutesOfVisit() {
assertEquals(105, catalog.totalMinutes(List.of("HAIR-01", "SPA-01")));
assertThrows(IllegalArgumentException.class, () -> catalog.totalMinutes(List.of("X-99")));
}
}Команда mvn package — это фаза жизненного цикла: Maven выполняет все фазы до неё по порядку — validate, compile, test, package. Поэтому упавший тест останавливает сборку, и сломанный jar не появится. Если Java и Maven установлены локально, достаточно mvn package в папке проекта; без них сработает Docker-образ с JDK 25 и Maven:
cat > services.csv <<'EOF'
code;title;category;price;minutes
HAIR-01;Женская стрижка;HAIR;9000;60
HAIR-02;Окрашивание;HAIR;25000;150
NAILS-01;Маникюр с покрытием;NAILS;8000;90
SPA-01;Массаж спины;SPA;12000;45
SPA-02;Обёртывание;SPA;15000;60
EOFservices.csv в корне проекта: заголовок и пять услуг, разделитель — точка с запятой.$ docker run --rm -v "$PWD:/app" -v "$HOME/.m2:/root/.m2" -w /app maven:3.10-eclipse-temurin-25 mvn -B package
…
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.example.catalog.ServiceCatalogTest
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0
…
[INFO] --- jar:3.5.1:jar (default-jar) @ catalog ---
[INFO] BUILD SUCCESS
$ docker run --rm -v "$PWD:/app" -w /app eclipse-temurin:25-jre java -cp target/catalog-1.0-SNAPSHOT.jar com.example.catalog.Main
Услуг в каталоге: 5
Категории: [HAIR, NAILS, SPA]
По категориям: {HAIR=2, NAILS=1, SPA=2}
До 10 000 ₸: [Женская стрижка, Маникюр с покрытием]
SPA-01: Массаж спины
X-99: нет такой услуги
Визит, минут: 150services.csv лежит в корне проекта рядом с pom.xml; второй запуск с аргументом missing.csv печатает «Файл не найден: missing.csv».Частые ошибки, упражнения и что дальше
- Строки сравнивают через
equals, а не==:==сравнивает ссылки на объекты, а не текст - Деньги с копейками не хранят в
double— только целые минимальные единицы (long) илиBigDecimal - Из методов не возвращают
nullвместо пустого списка илиOptional - Результат
toList()иList.of()не пытаются изменить — для изменяемой копии естьnew ArrayList<>(list) - Исключения не глушат пустым
catch; исходную причину передают в конструктор нового исключения - Версии зависимостей и плагинов фиксируют в
pom.xml, аtarget/добавляют в.gitignore
Чек-лист готовности к Spring Boot: вы читаете сигнатуру public Optional<ServiceItem> findByCode(String code) и понимаете, что вернётся; пишете record с проверкой в компактном конструкторе; собираете данные потоком с filter, map и groupingBy; отличаете checked-исключение от unchecked; запускаете mvn package и читаете отчёт тестов. Следующий шаг — REST API на Spring Boot: тот же Maven, record в роли DTO и исключения, которые превращаются в HTTP-ответы.
Как проверено5 · пройденосверено 11 октября 2026 г.
- запущено, работает
Maven-проект catalog: pom.xml, Category, ServiceItem, ServiceCatalog, CatalogLoader, Main, ServiceCatalogTest (RU и EN-копия с английскими строками)
docker maven:3.10-eclipse-temurin-25 (Maven 3.10.0, Temurin 25.0.4.1): `mvn -B clean package` — 7/7 тестов, BUILD SUCCESS в обеих локалях; `java -cp target/catalog-1.0-SNAPSHOT.jar com.example.catalog.Main` в eclipse-temurin:25-jre — 7 строк вывода совпали со статьёй; с аргументом missing.csv — «Файл не найден» / «File not found»; `javac -Xlint:all` по src/main — без предупреждений; те же классы собраны и запущены в eclipse-temurin:21 (21.0.12.1)
- запущено, работает
Hello.java (компактный исходный файл) и CollectionsDemo.java, RU и EN
eclipse-temurin:25-jdk: `java Hello.java`, `javac -Xlint:all` + `java CollectionsDemo` — вывод совпал с комментариями; `java --version` → openjdk 25.0.4.1 2026-08-18 LTS
- запущено, работает
сообщения компилятора в тексте (unreported exception IOException; порядок catch; var без инициализатора)
негативные файлы Neg.java / Order2.java скомпилированы javac 25 — получены ровно приведённые ошибки
- запущено, работает
решения упражнений: cheapest(), averagePriceByCategory(), CatalogLoaderTest с @TempDir (RU и EN)
копия проекта catalog-ex: `mvn -B clean package` — 12/12 тестов (7 + 2 ExercisesTest + 3 CatalogLoaderTest) в обеих локалях; в статье методы — фрагменты из проверенного ServiceCatalog.java
- запущено, работает
bash-блок с heredoc для services.csv и Docker-команды сборки/запуска
heredoc выполнен в sh — файл побайтно совпал с проверенным services.csv; docker-команды выполнены (на Windows через Git Bash с MSYS_NO_PATHCONV=1 и абсолютным путём вместо $PWD)
«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.