Как делать11 октября 2026 г.8 мин чтения
JavaШаг 1 из 6

Java для backend: синтаксис, типы, коллекции и Maven

Коротко

Для backend на Java нужен небольшой, но плотный минимум: статические типы и var, классы и record для данных, коллекции List, Set и Map, Stream API для их обработки, Optional вместо null и понимание checked- и unchecked-исключений. Проект собирает Maven: pom.xml описывает версию Java и зависимости, а mvn package компилирует код, прогоняет тесты JUnit и упаковывает jar. Актуальная LTS-версия — Java 25.

Пошаговый урокУровень: НачальныйJavaПрактика: ~1,5–3 ч

После материала вы сможете

  • описать данные через 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 г.

Содержание
  1. 01Java и JVM: от исходника до запуска
  2. 02Типы, var, классы, enum и record
  3. 03Коллекции: List, Set и Map
  4. 04Stream API: фильтры, сортировка, группировка
  5. 05Optional вместо null
  6. 06Исключения: checked и unchecked
  7. 07Maven: pom.xml, сборка и тест на JUnit
  8. 08Частые ошибки, упражнения и что дальше

Java и JVM: от исходника до запуска

Java — компилируемый язык со статической типизацией: типы переменных и методов проверяет компилятор javac ещё до запуска. Он превращает исходник .java не в машинный код, а в байткод — файлы .class, которые исполняет виртуальная машина Java (JVM). Поэтому один и тот же jar-архив запускается на Linux-сервере, в Docker-контейнере и на ноутбуке разработчика. Набор для разработки называется JDK: в нём компилятор, JVM и утилиты; на сервере для запуска достаточно среды выполнения — например, образа eclipse-temurin:25-jre.

Hello.javajavacHello.classJVMВывод
сборка (JDK)запуск
Компилятор превращает исходник в байткод, JVM его исполняет.

Новая версия Java выходит каждые полгода, но для серверов выбирают версии с долгосрочной поддержкой — LTS. По дорожной карте Oracle актуальная LTS — Java 25 (сентябрь 2025), предыдущая — Java 21. Весь код урока проверен на Java 25 (Eclipse Temurin 25.0.4); классы проекта, кроме Hello.java, компилируются и на Java 21.

Hello.java
// Компактный исходный файл (Java 25): без объявления класса и без static
void main() {
    String name = "InterLAB";
    int year = 2026;
    var services = 3; // var — тип int выведен из значения
    IO.println("Привет, " + name + "! Год: " + year + ", услуг: " + services);
}
Компактный исходный файл (JEP 512, финальная возможность Java 25): удобно для первых шагов и скриптов.
terminal
$ java --version
openjdk 25.0.4.1 2026-08-18 LTS
…
$ java Hello.java
Привет, InterLAB! Год: 2026, услуг: 3
java 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.

src/main/java/com/example/catalog/Category.java
package com.example.catalog;

public enum Category {
    HAIR,
    NAILS,
    SPA
}
src/main/java/com/example/catalog/ServiceItem.java
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> хранит только строки, и компилятор это проверяет.

CollectionsDemo.java
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). Каталог услуг целиком построен на этих операциях.

src/main/java/com/example/catalog/ServiceCatalog.java
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; из метода, который возвращает Optional
  • Optional в полях, параметрах и элементах коллекций

Последний пункт — не вкусовщина: в документации класса сказано, что Optional задуман прежде всего как тип возвращаемого значения там, где явно нужно выразить «нет результата», а dev.java прямо называет поля, коллекции и аргументы неподходящим местом. Метод totalMinutes в каталоге показывает обе стороны: внутри потока orElseThrow превращает неизвестный код в IllegalArgumentException с понятным сообщением.

Исключения: checked и unchecked

Исключение — объект ошибки, который JVM передаёт вверх по стеку вызовов, пока его не поймает подходящий catch. В Java два вида исключений. Проверяемые (checked, наследники Exception, например IOException) подчиняются правилу «поймай или объяви»: компилятор не соберёт код, который их игнорирует. Непроверяемые (RuntimeException, Error и их наследники) компилятор не отслеживает.

src/main/java/com/example/catalog/CatalogLoader.java
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 из метода, который читает файл, компилятор остановит сборку:

terminal
$ javac Neg.java
Neg.java:6: error: unreported exception IOException; must be caught or declared to be thrown
        return Files.readString(p);
                               ^
1 error
Реальный вывод javac 25 для метода с Files.readString без throws.
src/main/java/com/example/catalog/Main.java
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, а структура папок задана соглашением, поэтому её не нужно настраивать:

  1. pom.xml

    координаты проекта (groupId, artifactId, version), версия Java, зависимости и плагины

  2. src/main/java

    код приложения; путь папок повторяет пакет: com/example/catalog

  3. src/test/java

    тесты; в итоговый jar они не попадают

  4. target

    результат сборки: .class-файлы, отчёты тестов и catalog-1.0-SNAPSHOT.jar; в Git не коммитят

pom.xml
<?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 проверяет, что код бросает нужное исключение, и возвращает его для проверки сообщения.

src/test/java/com/example/catalog/ServiceCatalogTest.java
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:

bash
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
EOF
Создаёт services.csv в корне проекта: заголовок и пять услуг, разделитель — точка с запятой.
terminal
$ 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: нет такой услуги
Визит, минут: 150
Вывод сокращён. Файл services.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.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

Нажимая кнопку, вы соглашаетесь с обработкой персональных данных.