Курс с нуля · Java 25 LTS

Программирование на Java для начинающих

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

  • 17 уроков
  • Без опыта
  • Задачи с решениями
  • Итоговый проект
Начать обучение
Кому подойдет Стажерам и всем, кто никогда не программировал
Как заниматься 60–90 минут на урок, обязательно печатая код руками
Что получится Консольный менеджер задач с сохранением в файл
Перед стартом

Как получить от курса максимум

Программирование похоже на обучение езде на велосипеде: читать полезно, но навык появляется только тогда, когда вы сами садитесь и пробуете. Поэтому здесь мало «сухой» теории и много короткой практики.

Понимать, как Java выполняет программу
Использовать условия, циклы и методы
Создавать классы и работать с коллекциями
Находить ошибки, читать файлы и тестировать код

Правило каждого урока: «прочитал → повторил → изменил → решил»

  1. Прочитайте объяснение. Не пытайтесь запомнить каждое слово. Сначала поймите общую идею.
  2. Напечатайте пример руками. Копирование экономит минуту, но отнимает тренировку.
  3. Предскажите результат. До запуска скажите себе, что программа выведет и почему.
  4. Измените пример. Поменяйте числа, текст или условие и посмотрите, что произойдет.
  5. Решите задачи без подсказки. Открывайте решение только после честной попытки.
Ошибки — нормальная часть работы Компилятор не ругает вас. Он показывает место, где не смог понять команду. У опытных разработчиков ошибки тоже возникают каждый день — они просто быстрее их читают.

Рекомендуемый ритм

Самостоятельно

3 урока в неделю по 60–90 минут. Полный курс займет примерно 6 недель.

Со стажерами

2 урока, 1 практическая встреча и короткое ревью кода каждую неделю.

Создайте папку для работ Например, java-course, а внутри — отдельную папку lesson-01, lesson-02 и так далее. Не стирайте старые решения: через месяц вы увидите собственный прогресс.
Урок 1

Что такое Java, JDK и JVM

≈ 45 минут

Перед кодом разберемся, кто превращает наши команды в работающую программу. Терминов всего три, и у каждого есть простая роль.

Понять путь программы от текста до запуска
Установить JDK 25 и проверить его

Java — это язык

Язык программирования — набор слов и правил, которыми человек описывает задачу компьютеру. В Java мы пишем обычный текст в файлах с расширением .java.

Представьте конструктор Java — это инструкция с понятными деталями. JDK — коробка инструментов для сборки. JVM — механизм, который запускает уже собранную модель.
Термин Полное название Простое объяснение
JDK Java Development Kit Набор разработчика: компилятор, JVM и другие инструменты.
JVM Java Virtual Machine Виртуальная машина, которая выполняет байт-код Java.
JRE Java Runtime Environment Среда выполнения. Отдельно новичку ее обычно устанавливать не нужно: она входит в JDK.

Как запускается программа

Благодаря JVM один и тот же байт-код можно запускать на Windows, macOS и Linux, если для системы установлена подходящая Java.

Установка

  1. Установите JDK 25 LTS для своей операционной системы. Ссылку на официальную страницу загрузки найдете в конце курса.
  2. Откройте терминал: Terminal в macOS/Linux или PowerShell в Windows.
  3. Выполните две команды:
java --version
javac --version

В обеих строках должна появиться версия 25 (после нее могут быть дополнительные цифры).

Почему Java 25? Это LTS-релиз — версия с длительным сроком поддержки. Курс не использует экспериментальные возможности, поэтому базовые примеры также понятны на Java 21.

Практика разминка

  1. Объясните своими словами разницу между JDK и JVM.
  2. Найдите на компьютере версии java и javac.
  3. Ответьте: какой файл пишет человек, а какой создает компилятор?
Проверить ответы
  1. JDK нужен, чтобы разрабатывать и собирать программы; JVM — чтобы выполнять байт-код.
  2. Обе версии показывает терминал после команд выше.
  3. Человек пишет .java, компилятор создает .class.
Урок 2

Первая программа: разбираем каждую строчку

≈ 60 минут

Сейчас создадим маленькую программу, запустим ее и поймем назначение всех символов. После этого Java перестанет выглядеть как заклинание.

Создать и запустить файл Main.java
Понять класс, метод main и вывод

Шаг 1. Создаем файл

Создайте папку lesson-02, а в ней файл Main.java. Имя файла важно: оно должно совпадать с именем публичного класса Main, включая заглавную букву.

public class Main {
    public static void main(String[] args) {
        System.out.println("Привет, Java!");
    }
}

Шаг 2. Компилируем и запускаем

Откройте терминал в этой папке и выполните:

javac Main.java
java Main
Привет, Java!

Первая команда создает Main.class. Вторая просит JVM выполнить класс Main. При запуске расширение .class не пишут.

Что означает код

Фрагмент Что говорит Java
public class Main «Создай доступный класс с именем Main».
{ ... } Фигурные скобки обозначают начало и конец блока.
main Точка старта программы. JVM ищет именно этот метод.
String[] args Место для аргументов командной строки. Пока просто оставляем его.
System.out.println(...) Вывести значение и перейти на новую строку.
; Точка с запятой завершает команду.
Java различает регистр Main, main и MAIN — три разных имени. Поэтому System нельзя написать как system.

print и println

public class Main {
    public static void main(String[] args) {
        System.out.print("Один ");
        System.out.print("два ");
        System.out.println("три");
        System.out.println("Новая строка");
    }
}
Один два три Новая строка

print оставляет курсор в той же строке, а println после текста добавляет перевод строки.

Специальные символы в тексте

System.out.println("Первая строка\nВторая строка");
System.out.println("Он сказал: \"Привет!\"");
System.out.println("Папка: C:\\java\\lesson-02");
  • \n — перейти на новую строку;
  • \" — кавычка внутри текста;
  • \\ — обычный обратный слеш.
Если программа не компилируется Читайте первую ошибку сверху. Проверьте имя файла, регистр букв, кавычки, точки с запятой и парность фигурных скобок. Одна ранняя ошибка часто создает еще десять сообщений ниже.

Практика легко

  1. Выведите на трех отдельных строках свое имя, направление стажировки и фразу Я изучаю Java.
  2. Нарисуйте символами прямоугольник размером 5 × 3 из знаков #.
  3. Сначала предположите, затем проверьте: что изменится, если заменить первый println на print?
Решение задач 1–2
public class Main {
    public static void main(String[] args) {
        System.out.println("Анна");
        System.out.println("Backend-разработка");
        System.out.println("Я изучаю Java");

        System.out.println("#####");
        System.out.println("#####");
        System.out.println("#####");
    }
}

Имя и направление замените своими. В третьей задаче следующий текст продолжится в той же строке.

Урок 3

Переменные и типы данных

≈ 75 минут

Программе нужно хранить имя пользователя, количество товаров, цену и множество других значений. Для этого существуют переменные — подписанные ячейки памяти.

Создавать и изменять переменные
Выбирать подходящий тип данных
Переменная похожа на подписанную коробку На коробке написано age, внутри лежит число 19. Тип int говорит, что в эту коробку разрешено класть только целые числа.

Объявление и присваивание

public class Main {
    public static void main(String[] args) {
        String name = "Миша";
        int age = 19;
        double height = 1.78;
        boolean intern = true;

        System.out.println("Имя: " + name);
        System.out.println("Возраст: " + age);

        age = 20;
        System.out.println("Через год: " + age);
    }
}

Слева от знака = находится переменная, справа — новое значение. Оператор + соединяет текст со значением переменной.

= — не «равно» В программировании запись age = 20 означает «положить 20 в age». Сравнение на равенство позже будем писать как ==.

Основные типы

Тип Что хранит Пример Когда использовать
int Целое число int count = 12; Счетчики, возраст, количество
long Большое целое long views = 8_000_000_000L; Очень большие значения
double Дробное число double price = 99.50; Измерения и обычные вычисления с дробями
boolean Истина или ложь boolean ready = true; Ответы «да/нет»
char Один символ char grade = 'A'; Одиночная буква или знак
String Строка текста String city = "Цюрих"; Имена, сообщения и любой текст

Для char используют одинарные кавычки, для String — двойные. String технически является классом, но используется так часто, что мы изучаем его вместе с простыми типами.

Хорошие имена

int userAge = 19;          // понятно
double orderTotal = 845.5; // понятно

int x = 19;                // непонятно без контекста
double a = 845.5;          // непонятно без контекста
  • Имена начинаются с маленькой буквы: userName.
  • Каждое следующее слово начинается с большой: itemsInCart. Это стиль camelCase.
  • Нельзя начинать имя с цифры и использовать пробелы.
  • Имя должно объяснять смысл, а не тип значения.

Константы

Если значение нельзя менять, добавьте слово final:

final int DAYS_IN_WEEK = 7;
final double TAX_RATE = 0.20;

Константы принято называть заглавными буквами через подчеркивание. Попытка присвоить им другое значение вызовет ошибку компиляции.

Практика легко

  1. Создайте переменные для названия товара, его цены, количества и наличия на складе. Выведите их понятными строками.
  2. Даны стороны прямоугольника 8 и 5. Посчитайте площадь и периметр, сохранив результаты в переменные.
  3. Создайте int minutes = 135. Посчитайте полные часы и оставшиеся минуты. Подсказка: операции деления и остатка будут подробно разобраны в следующем уроке.
Решение задачи о прямоугольнике
public class Main {
    public static void main(String[] args) {
        int width = 8;
        int height = 5;

        int area = width * height;
        int perimeter = 2 * (width + height);

        System.out.println("Площадь: " + area);
        System.out.println("Периметр: " + perimeter);
    }
}
Решение задачи о времени
int minutes = 135;
int fullHours = minutes / 60;
int remainingMinutes = minutes % 60;

System.out.println(fullHours + " ч " + remainingMinutes + " мин");
2 ч 15 мин
Урок 4

Операторы и ввод данных

≈ 90 минут

До сих пор значения были записаны прямо в коде. Научим программу задавать вопросы пользователю, получать ответы и выполнять вычисления.

Выполнять арифметические операции
Читать текст и числа из консоли

Арифметические операторы

Оператор Действие Пример Результат
+Сложение7 + 310
-Вычитание7 - 34
*Умножение7 * 321
/Деление7 / 32 для целых чисел
%Остаток от деления7 % 31
Целое деление отбрасывает дробную часть 7 / 2 дает 3, потому что оба числа целые. Для результата 3.5 хотя бы одно число должно быть дробным: 7.0 / 2.

Порядок вычислений

Как в математике, сначала выполняются скобки, затем умножение и деление, потом сложение и вычитание. Если выражение трудно читать — добавьте скобки.

int resultA = 2 + 3 * 4;       // 14
int resultB = (2 + 3) * 4;     // 20
double average = (8 + 9 + 7) / 3.0; // 8.0

Читаем ответы пользователя

Класс Scanner умеет читать данные из консоли. Чтобы избежать частой путаницы между nextInt() и nextLine(), в начале курса будем всегда читать целую строку, а затем превращать ее в число.

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.print("Как вас зовут? ");
        String name = scanner.nextLine();

        System.out.print("Сколько вам лет? ");
        int age = Integer.parseInt(scanner.nextLine());

        System.out.println("Привет, " + name + "!");
        System.out.println("Через 5 лет вам будет " + (age + 5));
    }
}
  • import сообщает, какой готовый класс мы хотим использовать.
  • new Scanner(System.in) создает объект для чтения клавиатуры.
  • nextLine() возвращает введенный текст.
  • Integer.parseInt(...) превращает текст вроде "19" в число 19.

Сокращенные операции

int score = 10;
score += 5;  // то же, что score = score + 5
score -= 2;  // 13
score++;     // увеличить на 1, теперь 14
score--;     // уменьшить на 1, снова 13

Практика средне

  1. Запросите цену товара и количество. Выведите общую стоимость.
  2. Пользователь вводит общее количество секунд. Выведите полные часы, минуты и оставшиеся секунды.
  3. Напишите простой конвертер температуры из градусов Цельсия в Фаренгейты: F = C × 9 / 5 + 32.
Решение задачи о стоимости
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.print("Цена: ");
        double price = Double.parseDouble(scanner.nextLine());

        System.out.print("Количество: ");
        int quantity = Integer.parseInt(scanner.nextLine());

        double total = price * quantity;
        System.out.println("Итого: " + total);
    }
}
Решение задачи о секундах
int totalSeconds = 7384;

int hours = totalSeconds / 3600;
int secondsAfterHours = totalSeconds % 3600;
int minutes = secondsAfterHours / 60;
int seconds = secondsAfterHours % 60;

System.out.println(hours + " ч " + minutes + " мин " + seconds + " с");
2 ч 3 мин 4 с
Решение конвертера температуры
double celsius = 20.0;
double fahrenheit = celsius * 9.0 / 5.0 + 32.0;
System.out.println(fahrenheit);

Используем дробные числа, чтобы деление не стало целым.

Урок 5

Условия: учим программу принимать решения

≈ 90 минут

Условие позволяет выполнять разные команды в разных ситуациях: показать скидку постоянному клиенту, запретить неверный пароль или определить победителя.

Писать ветвления if / else
Объединять несколько проверок

Простое условие

int age = 19;

if (age >= 18) {
    System.out.println("Доступ разрешен");
} else {
    System.out.println("Доступ пока закрыт");
}

Java вычисляет выражение в круглых скобках. Если результат true, выполняется первый блок; если false — блок после else.

Операторы сравнения и логики

Оператор Смысл Пример
==равноscore == 100
!=не равноstatus != 0
>, <больше, меньшеtemperature < 0
>=, <=больше/меньше или равноage >= 18
&&И: верны оба условияage >= 18 && hasTicket
||ИЛИ: верно хотя бы одноisAdmin || isOwner
!НЕ: меняет значение на обратное!isBlocked
int age = 22;
boolean hasTicket = true;

if (age >= 18 && hasTicket) {
    System.out.println("Можно войти");
}

Несколько вариантов

int score = 78;

if (score >= 90) {
    System.out.println("Отлично");
} else if (score >= 70) {
    System.out.println("Хорошо");
} else if (score >= 50) {
    System.out.println("Зачет");
} else {
    System.out.println("Нужно повторить тему");
}
Порядок условий важен Java идет сверху вниз и выполняет только первую подходящую ветку. Поэтому сначала проверяем самый высокий порог.

Как сравнивать строки

String command = "start";

if (command.equals("start")) {
    System.out.println("Запускаем");
}
Строки сравнивают через equals Для чисел подходит ==, но для содержимого строк используйте text.equals("ожидаемый текст"). Причина станет понятнее после урока об объектах.

switch: выбор по одному значению

int day = 2;

String dayName = switch (day) {
    case 1 -> "Понедельник";
    case 2 -> "Вторник";
    case 3 -> "Среда";
    case 4 -> "Четверг";
    case 5 -> "Пятница";
    case 6, 7 -> "Выходной";
    default -> "Нет такого дня";
};

System.out.println(dayName);

switch удобен, когда сравниваем одно значение с известным набором вариантов. Для диапазонов вроде «от 18 и старше» обычно понятнее if.

Практика средне

  1. Пользователь вводит число. Сообщите, положительное оно, отрицательное или равно нулю.
  2. Даны три числа. Найдите самое большое без использования готовой функции Math.max.
  3. Запросите логин и пароль. Доступ разрешен только при логине intern и пароле java25.
  4. Со звездочкой: определите, является ли год високосным. Год високосный, если делится на 400 или делится на 4, но не делится на 100.
Решение: максимум из трех чисел
int a = 8;
int b = 12;
int c = 5;

int max = a;

if (b > max) {
    max = b;
}
if (c > max) {
    max = c;
}

System.out.println("Максимум: " + max);
Решение: проверка логина и пароля
String login = "intern";
String password = "java25";

if (login.equals("intern") && password.equals("java25")) {
    System.out.println("Доступ разрешен");
} else {
    System.out.println("Неверный логин или пароль");
}

В настоящем приложении пароль нельзя хранить открытым текстом. Здесь это только упражнение на условия.

Решение: високосный год
int year = 2028;

boolean divisibleBy400 = year % 400 == 0;
boolean divisibleBy4Not100 = year % 4 == 0 && year % 100 != 0;
boolean leapYear = divisibleBy400 || divisibleBy4Not100;

System.out.println(leapYear ? "Високосный" : "Обычный");

Здесь показан тернарный оператор: условие ? значениеЕслиДа : значениеЕслиНет. Используйте его только для короткого и очевидного выбора.

Урок 6

Циклы: повторяем команды без копирования

≈ 90 минут

Если нужно вывести сто строк, не надо сто раз писать println. Цикл повторяет блок кода, пока выполняется заданное правило.

Использовать циклы for и while
Не создавать бесконечные циклы случайно

Цикл for: когда известно количество повторений

for (int i = 1; i <= 5; i++) {
    System.out.println("Шаг " + i);
}
Шаг 1 Шаг 2 Шаг 3 Шаг 4 Шаг 5

В круглых скобках три части:

  1. int i = 1 — стартовое значение счетчика;
  2. i <= 5 — условие продолжения;
  3. i++ — изменение после каждого шага.
Счетчик похож на номер круга стадиона Начинаем с первого круга, после каждого прибавляем один и останавливаемся, когда пятый круг завершен.

Накапливаем результат

int sum = 0;

for (int number = 1; number <= 100; number++) {
    sum += number;
}

System.out.println("Сумма: " + sum);
Сумма: 5050

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

Цикл while: когда количество повторений заранее неизвестно

int battery = 5;

while (battery > 0) {
    System.out.println("Работаем. Заряд: " + battery);
    battery--;
}

System.out.println("Нужна зарядка");

Перед каждым повторением while проверяет условие. Если оно сразу ложно, тело цикла не выполнится ни разу.

do-while: выполнить хотя бы один раз

String command;

do {
    System.out.println("Меню показано хотя бы один раз");
    command = "exit";
} while (!command.equals("exit"));

break и continue

for (int i = 1; i <= 10; i++) {
    if (i == 3) {
        continue; // пропустить только число 3
    }
    if (i == 8) {
        break;    // полностью закончить цикл
    }
    System.out.println(i);
}
Опасность бесконечного цикла Если условие while всегда истинно, программа не остановится. Проверьте, изменяется ли внутри цикла переменная, от которой зависит условие. Принудительно остановить консольную программу обычно можно сочетанием Ctrl + C.

Вложенные циклы

for (int row = 1; row <= 3; row++) {
    for (int column = 1; column <= 5; column++) {
        System.out.print("#");
    }
    System.out.println();
}
##### ##### #####

Практика средне

  1. Выведите все четные числа от 2 до 20.
  2. Запросите число и выведите таблицу умножения для него от 1 до 10.
  3. Посчитайте факториал числа n: 1 × 2 × ... × n.
  4. Мини-проект: компьютер загадывает число от 1 до 100, а пользователь угадывает. После попытки программа пишет «больше» или «меньше».
Решение: четные числа и таблица умножения
for (int number = 2; number <= 20; number += 2) {
    System.out.println(number);
}

int value = 7;
for (int multiplier = 1; multiplier <= 10; multiplier++) {
    int result = value * multiplier;
    System.out.println(value + " × " + multiplier + " = " + result);
}
Решение: факториал
int n = 5;
long factorial = 1;

for (int number = 2; number <= n; number++) {
    factorial *= number;
}

System.out.println(factorial);
120

long выбран потому, что факториал растет очень быстро.

Решение мини-проекта «Угадай число»
import java.util.Random;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        Random random = new Random();

        int secret = random.nextInt(100) + 1;
        int attempts = 0;
        int guess;

        do {
            System.out.print("Ваше число: ");
            guess = Integer.parseInt(scanner.nextLine());
            attempts++;

            if (guess < secret) {
                System.out.println("Загаданное число больше");
            } else if (guess > secret) {
                System.out.println("Загаданное число меньше");
            }
        } while (guess != secret);

        System.out.println("Угадано! Попыток: " + attempts);
    }
}
Урок 7

Методы: делим большую задачу на маленькие

≈ 90 минут

Метод — именованный кусочек программы, который выполняет одну понятную работу. Методы уменьшают повторение и превращают длинный код в читаемый план действий.

Создавать методы с параметрами
Возвращать результат через return

Первый метод

public class Main {
    public static void main(String[] args) {
        greet("Лена");
        greet("Олег");
    }

    static void greet(String name) {
        System.out.println("Привет, " + name + "!");
    }
}
Привет, Лена! Привет, Олег!

greet — имя метода, String name — параметр, а void означает, что метод ничего не возвращает. Значения "Лена" и "Олег" при вызове называются аргументами.

Метод, который возвращает значение

public class Main {
    public static void main(String[] args) {
        int total = add(8, 5);
        System.out.println("Сумма: " + total);
    }

    static int add(int first, int second) {
        int result = first + second;
        return result;
    }
}

Перед именем add указан тип int. Значит, метод обязан вернуть целое число. Команда return отдает результат вызывающему коду и сразу завершает метод.

Метод похож на кухонный прибор В блендер передают ингредиенты — это параметры. Он выполняет одну работу и возвращает готовый результат. Внутреннее устройство не нужно повторять при каждом использовании.

Ранний выход делает код проще

static boolean canRegister(int age) {
    if (age < 14) {
        return false;
    }

    return true;
}

Еще короче тот же метод записывается так:

static boolean canRegister(int age) {
    return age >= 14;
}

Область видимости

static int doubleNumber(int number) {
    int result = number * 2;
    return result;
}

// Здесь переменных number и result уже не существует.

Переменная, созданная внутри фигурных скобок, обычно доступна только внутри этого блока. Это помогает разным методам не мешать друг другу.

Как придумать хороший метод

  • Он делает одну работу.
  • Его имя начинается с действия: calculateTotal, printMenu, isValid.
  • Он достаточно короткий, чтобы понять его без долгой прокрутки.
  • Если код повторяется два-три раза, возможно, его стоит вынести в метод.
Сначала напишите решение, потом улучшайте Новичку не нужно угадывать идеальную структуру заранее. Получите работающий вариант, заметите повторяющиеся части и спокойно вынесите их в методы. Это называется рефакторингом.

Практика средне

  1. Создайте метод isEven(int number), который проверяет четность.
  2. Создайте метод maxOfThree(int a, int b, int c).
  3. Создайте метод countVowels(String text), который считает русские гласные а, е, ё, и, о, у, ы, э, ю, я без учета регистра.
  4. Разделите игру «Угадай число» из прошлого урока минимум на три метода.
Решение: четность и максимум
static boolean isEven(int number) {
    return number % 2 == 0;
}

static int maxOfThree(int a, int b, int c) {
    int max = a;
    if (b > max) {
        max = b;
    }
    if (c > max) {
        max = c;
    }
    return max;
}
Решение: подсчет гласных
static int countVowels(String text) {
    String vowels = "аеёиоуыэюя";
    String lowerText = text.toLowerCase();
    int count = 0;

    for (int i = 0; i < lowerText.length(); i++) {
        char letter = lowerText.charAt(i);
        if (vowels.indexOf(letter) >= 0) {
            count++;
        }
    }

    return count;
}
Урок 8

Массивы и строки

≈ 105 минут

Отдельная переменная удобна для одного значения. Когда оценок, имен или цен много, их объединяют в массив и обрабатывают циклом.

Создавать и обходить массивы
Безопасно работать со строками

Массив: несколько значений одного типа

int[] scores = {85, 92, 74, 88};

System.out.println(scores[0]);      // первый элемент: 85
System.out.println(scores.length);  // количество элементов: 4

scores[2] = 80;                     // изменить третий элемент
Индексация начинается с нуля У четырех элементов индексы 0, 1, 2, 3. Обращение к scores[4] вызовет ошибку во время работы программы.

Обход массива

String[] names = {"Аня", "Борис", "Вика"};

for (int i = 0; i < names.length; i++) {
    System.out.println(i + ": " + names[i]);
}

Если индекс не нужен, используйте улучшенный цикл for-each:

for (String name : names) {
    System.out.println("Привет, " + name);
}

Среднее значение

public class Main {
    public static void main(String[] args) {
        int[] scores = {85, 92, 74, 88};
        int sum = 0;

        for (int score : scores) {
            sum += score;
        }

        double average = (double) sum / scores.length;
        System.out.println("Средний балл: " + average);
    }
}

Запись (double) sum временно превращает целое число в дробное, чтобы деление сохранило дробную часть. Это называется приведением типа.

Полезные операции со строками

String text = "  Java для начинающих  ";

System.out.println(text.length());              // длина
System.out.println(text.trim());                // убрать пробелы по краям
System.out.println(text.toUpperCase());         // верхний регистр
System.out.println(text.contains("Java"));      // true
System.out.println(text.startsWith("Java"));    // false: есть пробелы
System.out.println(text.replace("Java", "JVM"));
System.out.println(text.substring(2, 6));       // Java

Строка в Java неизменяема. Метод toUpperCase() не меняет исходную строку, а возвращает новую:

String name = "java";
name.toUpperCase();
System.out.println(name); // java

name = name.toUpperCase();
System.out.println(name); // JAVA

Собираем большой текст

Для множества последовательных добавлений используйте StringBuilder:

StringBuilder report = new StringBuilder();

for (int i = 1; i <= 3; i++) {
    report.append("Строка ").append(i).append("\n");
}

System.out.println(report);

Практика средне

  1. Найдите минимальное и максимальное число в массиве.
  2. Создайте новый массив, в котором элементы исходного идут в обратном порядке.
  3. Проверьте, является ли слово палиндромом: одинаково читается слева направо и справа налево, например топот.
  4. Посчитайте, сколько раз каждое слово встречается в предложении. Пока можно выбрать одно искомое слово; полное решение появится в уроке о коллекциях.
Решение: минимум и максимум
int[] numbers = {8, -2, 14, 5, 0};
int min = numbers[0];
int max = numbers[0];

for (int number : numbers) {
    if (number < min) {
        min = number;
    }
    if (number > max) {
        max = number;
    }
}

System.out.println("Минимум: " + min);
System.out.println("Максимум: " + max);
Решение: обратный массив
int[] source = {10, 20, 30, 40};
int[] reversed = new int[source.length];

for (int i = 0; i < source.length; i++) {
    reversed[i] = source[source.length - 1 - i];
}
Решение: палиндром
static boolean isPalindrome(String text) {
    String normalized = text.toLowerCase().replace(" ", "");

    for (int left = 0; left < normalized.length() / 2; left++) {
        int right = normalized.length() - 1 - left;
        if (normalized.charAt(left) != normalized.charAt(right)) {
            return false;
        }
    }

    return true;
}
Урок 9

Классы и объекты: создаем собственные типы

≈ 105 минут

До этого мы пользовались готовыми типами: String, Scanner, массивами. Теперь создадим собственный тип, который объединит данные и поведение.

Отличать класс от объекта
Создавать поля, конструкторы и методы
Класс — чертеж, объект — конкретная вещь Чертеж описывает, что у каждого робота есть имя и заряд. По одному чертежу можно собрать много разных роботов: Роби с зарядом 80 и Бипа с зарядом 45.

Описываем класс

public class Robot {
    String name;
    int battery;

    Robot(String name, int battery) {
        this.name = name;
        this.battery = battery;
    }

    void introduce() {
        System.out.println("Я " + name + ". Заряд: " + battery + "%");
    }

    void work() {
        if (battery >= 10) {
            battery -= 10;
            System.out.println(name + " выполнил задачу");
        } else {
            System.out.println(name + " нужно зарядить");
        }
    }
}
  • name и battery — поля, то есть состояние объекта.
  • Robot(...) — конструктор. Он задает начальное состояние.
  • this.name означает поле текущего объекта.
  • introduce() и work() — поведение объекта.

Создаем объекты

public class Main {
    public static void main(String[] args) {
        Robot first = new Robot("Роби", 80);
        Robot second = new Robot("Бип", 15);

        first.introduce();
        second.introduce();

        second.work();
        second.work();
    }
}

Слово new создает новый объект. Переменные first и second хранят ссылки на два независимых объекта. Изменение заряда одного робота не меняет другого.

Почему данные и методы живут вместе

Если данные о роботе лежат в одном месте, а работа с ними разбросана по всей программе, легко допустить несогласованность. Класс собирает связанную информацию и действия рядом. Это первая важная идея объектно-ориентированного программирования.

Называйте классы существительными Robot, Order, User, Invoice. Имя класса начинается с заглавной буквы. Имя объекта — с маленькой: Robot deliveryRobot.

Практика средне

Создайте класс Book:

  • поля: название, автор, количество страниц, прочитана ли книга;
  • конструктор для названия, автора и страниц;
  • метод printInfo();
  • метод markAsRead(), который меняет состояние;
  • в Main создайте две разные книги и вызовите методы.
Решение класса Book
public class Book {
    String title;
    String author;
    int pages;
    boolean read;

    Book(String title, String author, int pages) {
        this.title = title;
        this.author = author;
        this.pages = pages;
        this.read = false;
    }

    void markAsRead() {
        read = true;
    }

    void printInfo() {
        String status = read ? "прочитана" : "не прочитана";
        System.out.println(title + " — " + author);
        System.out.println(pages + " стр., " + status);
    }
}
public class Main {
    public static void main(String[] args) {
        Book cleanCode = new Book("Чистый код", "Роберт Мартин", 464);
        Book javaBook = new Book("Java. Руководство", "Герберт Шилдт", 1344);

        cleanCode.markAsRead();
        cleanCode.printInfo();
        javaBook.printInfo();
    }
}
Урок 10

Инкапсуляция: защищаем правильное состояние

≈ 105 минут

Хороший класс не просто хранит данные. Он не позволяет сделать их бессмысленными: установить отрицательный заряд, снять больше денег, чем есть, или создать товар без названия.

Закрывать поля через private
Понимать static, enum и record

Проблема открытых полей

Robot robot = new Robot("Роби", 80);
robot.battery = -500; // технически возможно, но бессмысленно

Инкапсуляция скрывает внутренние данные и разрешает менять их только контролируемыми способами.

public class BankAccount {
    private final String owner;
    private int balanceCents;

    public BankAccount(String owner, int initialBalanceCents) {
        if (owner == null || owner.isBlank()) {
            throw new IllegalArgumentException("Владелец обязателен");
        }
        if (initialBalanceCents < 0) {
            throw new IllegalArgumentException("Баланс не может быть отрицательным");
        }

        this.owner = owner;
        this.balanceCents = initialBalanceCents;
    }

    public void deposit(int amountCents) {
        if (amountCents <= 0) {
            throw new IllegalArgumentException("Сумма должна быть положительной");
        }
        balanceCents += amountCents;
    }

    public boolean withdraw(int amountCents) {
        if (amountCents <= 0 || amountCents > balanceCents) {
            return false;
        }
        balanceCents -= amountCents;
        return true;
    }

    public String getOwner() {
        return owner;
    }

    public int getBalanceCents() {
        return balanceCents;
    }
}

Поля видны только самому классу. Внешний код может узнать баланс через getBalanceCents(), но не может записать туда случайное значение. Отдельный setBalance здесь не нужен: баланс меняется только через осмысленные операции пополнения и снятия.

Геттеры и сеттеры — не обязательная пара Не создавайте метод изменения для каждого поля автоматически. Сначала спросите: «Какая операция предметной области действительно должна менять это значение?»

Модификаторы доступа

Слово Где доступно Практический совет новичку
private Только внутри класса Обычно начинайте поля с него.
public Из любого места Открывайте понятные операции класса.
без слова Внутри того же пакета Называется package-private; полезен позже при организации проекта.
protected Пакет и наследники Не используйте без ясной причины.

static: принадлежит классу, а не объекту

public class User {
    private static int createdCount = 0;
    private final String name;

    public User(String name) {
        this.name = name;
        createdCount++;
    }

    public static int getCreatedCount() {
        return createdCount;
    }
}
User first = new User("Аня");
User second = new User("Борис");

System.out.println(User.getCreatedCount()); // 2

У каждого пользователя свое поле name, но счетчик createdCount один на весь класс. Метод main тоже статический, потому JVM вызывает его до создания объекта Main.

enum: закрытый набор вариантов

public enum OrderStatus {
    NEW,
    PAID,
    SENT,
    DELIVERED,
    CANCELLED
}
OrderStatus status = OrderStatus.PAID;

if (status == OrderStatus.PAID) {
    System.out.println("Заказ можно отправлять");
}

enum безопаснее произвольной строки: нельзя случайно записать "оплочин" вместо ожидаемого статуса.

record: короткая модель неизменяемых данных

public record Point(int x, int y) {
}
Point point = new Point(4, 7);
System.out.println(point.x());
System.out.println(point); // Point[x=4, y=7]

Java сама создает конструктор, методы доступа, equals, hashCode и toString. Record удобен для простых неизменяемых значений, но не заменяет все обычные классы.

Практика средне

  1. Улучшите класс Book: сделайте поля закрытыми, проверьте, что название не пустое, а страниц больше нуля.
  2. Создайте enum Priority со значениями LOW, MEDIUM, HIGH.
  3. Создайте record Product(String name, int priceCents) с проверкой данных.
Решение record Product
public record Product(String name, int priceCents) {
    public Product {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("Название обязательно");
        }
        if (priceCents < 0) {
            throw new IllegalArgumentException("Цена не может быть отрицательной");
        }
    }
}

Это компактный конструктор record. Параметры уже объявлены в заголовке, а после проверки Java сама присвоит их компонентам.

Урок 11

Наследование, интерфейсы и полиморфизм

≈ 105 минут

Разные объекты могут выполнять одну и ту же команду по-разному. Круг и прямоугольник оба умеют считать площадь — формулы у них разные, а общий вопрос один.

Описывать общее поведение интерфейсом
Понимать наследование и полиморфизм

Интерфейс — договор о поведении

public interface Shape {
    double area();
    String name();
}

Интерфейс обещает: любой Shape умеет вернуть площадь и имя. Как именно — решает конкретный класс.

public class Rectangle implements Shape {
    private final double width;
    private final double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() {
        return width * height;
    }

    @Override
    public String name() {
        return "Прямоугольник";
    }
}
public class Circle implements Shape {
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }

    @Override
    public String name() {
        return "Круг";
    }
}

Полиморфизм: одна команда — разные реализации

public class Main {
    public static void main(String[] args) {
        Shape[] shapes = {
            new Rectangle(4, 5),
            new Circle(3)
        };

        for (Shape shape : shapes) {
            System.out.println(shape.name() + ": " + shape.area());
        }
    }
}

Переменная типа Shape может ссылаться на любой объект, соблюдающий договор. При вызове area() Java выбирает реализацию реального объекта. Это и есть полиморфизм.

Наследование через extends

public class Employee {
    private final String name;

    public Employee(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public String describeWork() {
        return "Выполняет рабочие задачи";
    }
}
public class Developer extends Employee {
    private final String language;

    public Developer(String name, String language) {
        super(name);
        this.language = language;
    }

    @Override
    public String describeWork() {
        return "Пишет код на " + language;
    }
}
  • extends создает отношение «является»: разработчик является сотрудником.
  • super(name) вызывает конструктор родительского класса.
  • @Override просит компилятор проверить, что метод действительно переопределен.
Не стройте глубокие деревья наследования Наследование связывает классы очень тесно. Часто безопаснее композиция: объект содержит другой объект и поручает ему работу. Используйте наследование, только когда отношение «является» действительно естественно.

Наследование

Developer является Employee.

Композиция

Car содержит Engine, но машина не является двигателем.

Практика средне

  1. Создайте интерфейс PaymentMethod с методом pay(int amountCents).
  2. Реализуйте CardPayment и CashPayment, которые выводят разные сообщения.
  3. Сложите их в массив PaymentMethod[] и вызовите один метод у каждого элемента.
Решение PaymentMethod
public interface PaymentMethod {
    void pay(int amountCents);
}
public class CardPayment implements PaymentMethod {
    @Override
    public void pay(int amountCents) {
        System.out.println("Карта: оплачено " + amountCents + " коп.");
    }
}
public class CashPayment implements PaymentMethod {
    @Override
    public void pay(int amountCents) {
        System.out.println("Наличные: принято " + amountCents + " коп.");
    }
}
PaymentMethod[] methods = {
    new CardPayment(),
    new CashPayment()
};

for (PaymentMethod method : methods) {
    method.pay(15000);
}
Урок 12

Коллекции: удобная работа с наборами данных

≈ 120 минут

Размер массива фиксируется при создании. В реальном приложении мы заранее не знаем, сколько появится задач, пользователей или заказов. Коллекции умеют расти.

Выбирать между List, Set и Map
Понимать запись с угловыми скобками
Интерфейс Простая модель Главное свойство
List<T> Список Есть порядок, элементы могут повторяться.
Set<T> Множество Каждое значение встречается не более одного раза.
Map<K, V> Словарь Значения хранятся по уникальному ключу.

List: упорядоченный список

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> tasks = new ArrayList<>();

        tasks.add("Прочитать урок");
        tasks.add("Написать код");
        tasks.add("Сделать перерыв");

        System.out.println(tasks.get(0));
        System.out.println("Количество: " + tasks.size());

        tasks.set(2, "Проверить решение");
        tasks.remove(0);

        for (String task : tasks) {
            System.out.println(task);
        }
    }
}

Запись List<String> означает «список, в котором разрешены только строки». Тип в угловых скобках — это обобщение, или generic. Компилятор не позволит случайно добавить туда число.

Set: только уникальные значения

import java.util.HashSet;
import java.util.Set;

Set<String> tags = new HashSet<>();
tags.add("java");
tags.add("backend");
tags.add("java");

System.out.println(tags.size());          // 2
System.out.println(tags.contains("java")); // true

HashSet не обещает порядок элементов. Если порядок важен, изучите позже LinkedHashSet или TreeSet.

Map: значение по ключу

import java.util.HashMap;
import java.util.Map;

Map<String, Integer> stock = new HashMap<>();
stock.put("Клавиатура", 7);
stock.put("Мышь", 12);

int keyboards = stock.getOrDefault("Клавиатура", 0);
System.out.println(keyboards);

for (Map.Entry<String, Integer> entry : stock.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

Считаем слова

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        String text = "java код java стажер код java";
        String[] words = text.split(" ");
        Map<String, Integer> frequency = new HashMap<>();

        for (String word : words) {
            int oldCount = frequency.getOrDefault(word, 0);
            frequency.put(word, oldCount + 1);
        }

        for (Map.Entry<String, Integer> entry : frequency.entrySet()) {
            System.out.println(entry.getKey() + " → " + entry.getValue());
        }
    }
}
Объявляйте интерфейс, создавайте конкретную реализацию Частая запись List<String> names = new ArrayList<>() оставляет внешнему коду простой договор List, а реализацию при необходимости можно заменить.
Нельзя удалять из списка обычным способом внутри for-each Это может вызвать ConcurrentModificationException. Для простого удаления по условию подойдет list.removeIf(...); лямбды для такой записи изучим в уроке 15.

Практика средне

  1. Создайте список покупок. Добавьте пять значений, удалите одно, замените одно и выведите список с порядковыми номерами.
  2. Дан список городов с повторами. Получите количество уникальных городов.
  3. Создайте телефонную книгу Map<String, String>: имя — ключ, номер — значение. Реализуйте поиск по имени.
  4. Нормализуйте регистр и посчитайте частоту всех слов в произвольной строке.
Решение: нумерованный список покупок
List<String> shopping = new ArrayList<>();
shopping.add("Хлеб");
shopping.add("Молоко");
shopping.add("Яблоки");
shopping.add("Чай");
shopping.add("Рис");

shopping.remove("Чай");
shopping.set(1, "Кефир");

for (int i = 0; i < shopping.size(); i++) {
    System.out.println((i + 1) + ". " + shopping.get(i));
}
Решение: уникальные города и телефонная книга
List<String> cities = List.of("Омск", "Томск", "Омск", "Пермь");
Set<String> uniqueCities = new HashSet<>(cities);
System.out.println(uniqueCities.size());

Map<String, String> phones = new HashMap<>();
phones.put("Анна", "+41 00 000 00 01");
phones.put("Борис", "+41 00 000 00 02");

String phone = phones.get("Анна");
if (phone != null) {
    System.out.println(phone);
} else {
    System.out.println("Контакт не найден");
}
Урок 13

Исключения: что делать, когда что-то пошло не так

≈ 105 минут

Пользователь может ввести буквы вместо числа, файл может отсутствовать, а сеть — отключиться. Хорошая программа не притворяется, что ошибок не бывает: она замечает их и реагирует предсказуемо.

Читать сообщение и стек вызовов
Обрабатывать ожидаемые проблемы через try-catch

Три вида проблем

Вид Когда обнаруживается Пример
Ошибка компиляции До запуска Пропущена ;, неизвестное имя.
Исключение Во время запуска Введены буквы вместо числа, индекс вне массива.
Логическая ошибка Программа работает, но неверно Скидка прибавляется вместо вычитания.

Безопасно читаем число

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int age;

        while (true) {
            System.out.print("Введите возраст: ");
            String input = scanner.nextLine();

            try {
                age = Integer.parseInt(input);
                break;
            } catch (NumberFormatException error) {
                System.out.println("Нужно ввести целое число.");
            }
        }

        System.out.println("Возраст принят: " + age);
    }
}

Java пробует выполнить код внутри try. Если возникает именно NumberFormatException, управление переходит в соответствующий catch. После сообщения цикл просит значение снова.

Не ловите все ошибки молча Пустой блок catch (Exception error) { } прячет проблему, но не исправляет ее. Ловите конкретное ожидаемое исключение и либо помогите восстановиться, либо сообщите достаточно информации.

Создаем исключение сами

static void validateAge(int age) {
    if (age < 0 || age > 130) {
        throw new IllegalArgumentException(
            "Возраст должен быть от 0 до 130"
        );
    }
}

throw останавливает обычное выполнение и сообщает вызывающему коду, что правило нарушено. Так класс защищает свое корректное состояние.

Проверяемые и непроверяемые исключения

  • Непроверяемые наследуются от RuntimeException: например, неверный индекс или аргумент. Компилятор не заставляет их обрабатывать.
  • Проверяемые, например IOException, описывают внешние проблемы. Компилятор просит обработать их или объявить через throws.

Как читать stack trace

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Calculator.divide(Calculator.java:12)
    at Main.main(Main.java:5)
  1. Первая строка: тип и причина — деление на ноль.
  2. Первая строка стека с вашим классом: Calculator.java:12.
  3. Дальше видно, кто вызвал проблемный метод: Main.java:5.
Исключение — не замена обычному условию Если отсутствие элемента является нормальной ситуацией, часто понятнее проверить if. Исключение подходит для ситуации, в которой нормальное выполнение конкретной операции невозможно.

Практика средне

  1. Создайте метод divide(double a, double b), запрещающий нулевой делитель.
  2. Запрашивайте положительное количество товара до тех пор, пока пользователь не введет корректное целое число.
  3. Найдите элемент массива по индексу. Обработайте неверный индекс понятным сообщением.
Решение метода divide
static double divide(double dividend, double divisor) {
    if (divisor == 0.0) {
        throw new IllegalArgumentException("Делитель не может быть нулем");
    }
    return dividend / divisor;
}
Решение: положительное количество
static int readPositiveInt(Scanner scanner) {
    while (true) {
        System.out.print("Количество: ");
        String input = scanner.nextLine();

        try {
            int value = Integer.parseInt(input);
            if (value > 0) {
                return value;
            }
            System.out.println("Число должно быть больше нуля.");
        } catch (NumberFormatException error) {
            System.out.println("Введите целое число.");
        }
    }
}
Урок 14

Файлы и даты: сохраняем данные между запусками

≈ 105 минут

Переменные исчезают после завершения программы. Файл позволяет сохранить список задач сегодня и снова загрузить его завтра.

Читать и записывать текстовые файлы
Работать с датой через java.time

Путь к файлу

import java.nio.file.Path;

Path file = Path.of("data", "notes.txt");
System.out.println(file.toAbsolutePath());

Относительный путь data/notes.txt считается от рабочей папки, из которой запущена программа. Path.of сам использует правильный разделитель для Windows, macOS или Linux.

Записываем и читаем текст

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class Main {
    public static void main(String[] args) {
        Path directory = Path.of("data");
        Path file = directory.resolve("message.txt");

        try {
            Files.createDirectories(directory);
            Files.writeString(
                file,
                "Привет из файла!",
                StandardCharsets.UTF_8
            );

            String text = Files.readString(file, StandardCharsets.UTF_8);
            System.out.println(text);
        } catch (IOException error) {
            System.out.println("Не удалось работать с файлом: " + error.getMessage());
        }
    }
}
  • createDirectories создает папку, если ее еще нет.
  • writeString записывает строку, заменяя старое содержимое.
  • readString читает весь небольшой текстовый файл.
  • UTF_8 явно задает кодировку и надежно хранит русский текст.
Не считывайте огромный файл целиком readString и readAllLines удобны для небольших учебных файлов. Большие файлы обрабатывают постепенно через поток строк или reader, чтобы не занять всю память.

Список строк

import java.util.List;

List<String> tasks = List.of(
    "Прочитать урок",
    "Решить задачи",
    "Отправить код наставнику"
);

Files.write(Path.of("tasks.txt"), tasks, StandardCharsets.UTF_8);

List<String> loadedTasks =
    Files.readAllLines(Path.of("tasks.txt"), StandardCharsets.UTF_8);

for (String task : loadedTasks) {
    System.out.println(task);
}

Дата без старых и сложных классов

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

LocalDate today = LocalDate.now();
LocalDate deadline = today.plusDays(10);

DateTimeFormatter formatter =
    DateTimeFormatter.ofPattern("dd.MM.yyyy");

System.out.println("Сегодня: " + today.format(formatter));
System.out.println("Срок: " + deadline.format(formatter));
System.out.println(deadline.isAfter(today)); // true

LocalDate хранит только дату без времени и часового пояса. Для времени есть LocalTime, а для даты и времени — LocalDateTime. В серверных системах с часовыми поясами позже понадобятся Instant и ZonedDateTime.

Преобразование текста в дату

DateTimeFormatter formatter =
    DateTimeFormatter.ofPattern("dd.MM.yyyy");

LocalDate date = LocalDate.parse("31.12.2026", formatter);
System.out.println(date.getYear()); // 2026
Храните данные отдельно от красивого вывода В программе дата остается объектом LocalDate. В строку 31.12.2026 превращайте ее только для ввода, вывода или хранения в выбранном формате.

Практика средне

  1. Запросите у пользователя три заметки, сохраните их в notes.txt, затем прочитайте файл и выведите строки с номерами.
  2. Запросите дату рождения в формате dd.MM.yyyy и выведите возраст в полных годах. Подсказка: используйте Period.between.
  3. Создайте файл журнала, где каждая строка начинается с текущей даты и времени.
Решение: заметки в файле
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        List<String> notes = new ArrayList<>();
        Path file = Path.of("notes.txt");

        for (int i = 1; i <= 3; i++) {
            System.out.print("Заметка " + i + ": ");
            notes.add(scanner.nextLine());
        }

        try {
            Files.write(file, notes, StandardCharsets.UTF_8);
            List<String> loaded =
                Files.readAllLines(file, StandardCharsets.UTF_8);

            for (int i = 0; i < loaded.size(); i++) {
                System.out.println((i + 1) + ". " + loaded.get(i));
            }
        } catch (IOException error) {
            System.out.println("Ошибка файла: " + error.getMessage());
        }
    }
}
Решение: возраст по дате рождения
import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeFormatter;

DateTimeFormatter formatter =
    DateTimeFormatter.ofPattern("dd.MM.yyyy");
LocalDate birthDate = LocalDate.parse("14.08.2004", formatter);
int years = Period.between(birthDate, LocalDate.now()).getYears();

System.out.println("Полных лет: " + years);
Урок 15

Лямбды и Stream API: описываем обработку данных

≈ 105 минут

Цикл объясняет компьютеру каждый шаг. Stream API позволяет описать желаемый результат: отфильтровать, преобразовать, отсортировать и собрать данные.

Читать простые лямбда-выражения
Строить понятную цепочку Stream API

Лямбда — короткое поведение

List<String> names = new ArrayList<>(
    List.of("Анна", "Борис", "Алексей", "Вика")
);

names.removeIf(name -> name.startsWith("А"));

Выражение name -> name.startsWith("А") можно прочитать так: «получи имя и ответь, начинается ли оно с А». Метод removeIf удаляет элементы, для которых ответ true.

Обычный цикл и Stream API

Цикл

List<String> result =
    new ArrayList<>();

for (String name : names) {
    if (name.length() >= 5) {
        result.add(
            name.toUpperCase()
        );
    }
}

Stream

List<String> result = names
    .stream()
    .filter(name ->
        name.length() >= 5)
    .map(String::toUpperCase)
    .toList();

Обе версии верны. Stream состоит из понятных этапов:

  1. stream() — начать поток обработки;
  2. filter — оставить только подходящие элементы;
  3. map — превратить каждый элемент;
  4. toList() — собрать результат в новый список.

Фильтрация и сортировка объектов

import java.util.Comparator;
import java.util.List;

public class Main {
    record Intern(String name, int completedTasks) {
    }

    public static void main(String[] args) {
        List<Intern> interns = List.of(
            new Intern("Анна", 12),
            new Intern("Борис", 5),
            new Intern("Вика", 18)
        );

        List<String> activeNames = interns.stream()
            .filter(intern -> intern.completedTasks() >= 10)
            .sorted(Comparator.comparingInt(Intern::completedTasks).reversed())
            .map(Intern::name)
            .toList();

        System.out.println(activeNames);
    }
}
[Вика, Анна]

Запись Intern::name — ссылка на метод. В данном случае она равнозначна intern -> intern.name().

Сумма чисел

int total = interns.stream()
    .mapToInt(Intern::completedTasks)
    .sum();
Stream не всегда лучше цикла Если цепочка требует сложного состояния, множества побочных действий или становится труднее читать, обычный цикл — хороший выбор. Цель не «использовать модную возможность», а написать ясный код.
Промежуточные операции ленивы filter и map сами по себе ничего не запускают. Обработка начинается, когда появляется завершающая операция: toList, sum, count, forEach и другие.

Практика средне

  1. Из списка чисел получите новый список квадратов только четных чисел, отсортированный по убыванию.
  2. Дан список товаров Product(name, priceCents). Оставьте товары дешевле 10 000, получите только названия и отсортируйте по алфавиту.
  3. Найдите суммарную длину всех непустых строк в списке.
  4. Решите одну из задач двумя способами — циклом и Stream API — и сравните читаемость.
Решение: квадраты четных чисел
List<Integer> numbers = List.of(3, 8, 2, 7, 4);

List<Integer> result = numbers.stream()
    .filter(number -> number % 2 == 0)
    .map(number -> number * number)
    .sorted(Comparator.reverseOrder())
    .toList();

System.out.println(result);
[64, 16, 4]
Решение: товары
record Product(String name, int priceCents) {}

List<Product> products = List.of(
    new Product("Мышь", 4500),
    new Product("Монитор", 28000),
    new Product("Клавиатура", 8900)
);

List<String> names = products.stream()
    .filter(product -> product.priceCents() < 10_000)
    .map(Product::name)
    .sorted()
    .toList();
Урок 16

Отладка, тесты и привычки разработчика

≈ 120 минут

Написать код — половина работы. Вторая половина — доказать, что он работает, найти ошибку, сделать решение понятным команде и не сломать его следующим изменением.

Искать ошибку системно, а не наугад
Проверять методы автоматическими тестами

Алгоритм отладки

  1. Воспроизведите проблему. Запишите точные входные данные и ожидаемый результат.
  2. Сузьте область. Найдите метод или условие, где результат впервые становится неверным.
  3. Проверьте предположение. Посмотрите значения переменных, не меняя сразу пять строк.
  4. Исправьте причину. Не маскируйте симптом дополнительным случайным условием.
  5. Добавьте проверку. Тест должен воспроизводить старую ошибку и подтверждать исправление.

Отладчик IDE

Поставьте breakpoint — точку остановки — на интересующей строке и запустите Debug. Основные команды почти во всех IDE одинаковы:

Команда Что делает
Step OverВыполнить текущую строку, не заходя внутрь вызванного метода.
Step IntoЗайти внутрь вызываемого метода.
Step OutЗакончить текущий метод и вернуться к вызывающему.
ResumeПродолжить до следующей точки остановки.
Сначала посмотрите значения, потом редактируйте Отладчик полезнее десятка случайных println, потому что показывает состояние программы в точный момент и позволяет идти по строкам.

Первый автоматический тест без библиотек

public class Calculator {
    public static int add(int a, int b) {
        return a + b;
    }

    public static int max(int a, int b) {
        return a > b ? a : b;
    }
}
public class CalculatorTest {
    public static void main(String[] args) {
        checkEquals(5, Calculator.add(2, 3), "2 + 3");
        checkEquals(-1, Calculator.add(2, -3), "2 + (-3)");
        checkEquals(8, Calculator.max(8, 5), "максимум");
        checkEquals(5, Calculator.max(5, 5), "равные числа");

        System.out.println("Все тесты пройдены");
    }

    static void checkEquals(int expected, int actual, String testName) {
        if (expected != actual) {
            throw new AssertionError(
                testName + ": ожидалось " + expected + ", получено " + actual
            );
        }
    }
}

Каждый тест следует схеме Arrange → Act → Assert: подготовить данные, вызвать код, сравнить фактический результат с ожидаемым. В рабочих проектах обычно используют JUnit, который автоматизирует запуск и красивый отчет, но сама логика теста остается такой же.

Какие случаи проверять

  • обычный успешный пример;
  • границы: 0, 1, минимальное и максимальное допустимое значение;
  • пустую строку или пустую коллекцию;
  • неверные данные и ожидаемое исключение;
  • случай, на котором раньше уже была ошибка.

Короткий чек-лист чистого кода

Имена объясняют смысл
Метод делает одну работу
Повторение вынесено в общее место
Нет «магических» чисел без объяснения
Ошибки не проглатываются
Внешний ввод проверяется
Код отформатирован одинаково
Сложное решение покрыто тестами
Комментарии объясняют «почему», код — «что» Не пишите // увеличить i рядом с i++. Полезный комментарий объясняет неочевидное правило, ограничение или причину решения.

Минимальная командная дисциплина

  • Перед задачей уточните ожидаемое поведение и крайние случаи.
  • Делайте небольшие логические изменения.
  • Перед передачей кода запустите программу и тесты самостоятельно.
  • Не смешивайте исправление ошибки с несвязанной большой переделкой.
  • В ревью спокойно обсуждается код, а не человек.

Практика важно

  1. Напишите не менее пяти тестов для метода isPalindrome.
  2. Найдите ошибку: метод среднего значения возвращает неверный ответ для {2, 3}.
  3. Возьмите старое решение из урока 5–8, переименуйте непонятные переменные, выделите методы и убедитесь, что результат не изменился.
Подсказка к ошибке среднего значения
static double average(int[] numbers) {
    int sum = 0;
    for (int number : numbers) {
        sum += number;
    }
    return sum / numbers.length;
}

Оба операнда деления имеют тип int, поэтому 5 / 2 превращается в 2, а уже потом — в 2.0.

return (double) sum / numbers.length;
Набор тестов для палиндрома
  • "топот"true;
  • "Java"false;
  • "А роза упала на лапу Азора"true после нормализации;
  • пустая строка → заранее определите ожидаемое правило;
  • одна буква → true;
  • разный регистр "ТоПоТ"true.
Урок 17 · Итог

Итоговый проект: консольный менеджер задач

≈ 5–8 часов

Соберем изученные темы в одно приложение: пользователь добавляет задачи, отмечает выполненные, удаляет и после перезапуска видит сохраненные данные. Это уже не отдельное упражнение, а маленькая законченная система.

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

Что должно уметь приложение

  • показывать меню до команды выхода;
  • добавлять задачу с уникальным числовым ID;
  • показывать все или только невыполненные задачи;
  • отмечать задачу выполненной;
  • удалять задачу по ID;
  • проверять пустое название и неверный ввод;
  • сохранять данные в UTF-8 файл data/tasks.tsv;
  • загружать сохраненные данные при следующем запуске.
Не начинайте с копирования готового решения Сначала создайте проект по этапам ниже и доведите каждый этап до работы. Эталон нужен для сравнения, когда своя версия уже написана или вы действительно застряли.

Структура проекта

task-tracker/
├── data/
│   └── tasks.tsv        ← появится автоматически
└── src/
    ├── Main.java        ← меню и общение с пользователем
    ├── Task.java        ← модель одной задачи
    ├── TaskService.java ← правила и операции
    └── TaskRepository.java ← чтение и запись файла

Модель Task используется всеми тремя частями. Такое разделение делает код проще для проверки: консоль не решает, как хранить файл, а репозиторий не спрашивает пользователя.

План разработки

  1. Модель. Создайте Task, конструктор, геттеры, markCompleted() и красивый toString().
  2. Логика в памяти. Создайте TaskService с ArrayList. Добавление, завершение и удаление сначала работают без файла.
  3. Меню. Подключите Scanner и цикл в Main. Проверьте неверные команды и ID.
  4. Хранение. Реализуйте TaskRepository и сохраняйте после каждой изменяющей операции.
  5. Проверка. Пройдите приемочные сценарии и только потом улучшайте интерфейс.
Эталон: Task.java
public class Task {
    private final int id;
    private final String title;
    private boolean completed;

    public Task(int id, String title, boolean completed) {
        if (id <= 0) {
            throw new IllegalArgumentException("ID должен быть больше нуля");
        }
        if (title == null || title.isBlank()) {
            throw new IllegalArgumentException("Название задачи обязательно");
        }

        this.id = id;
        this.title = title.trim();
        this.completed = completed;
    }

    public int getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    public boolean isCompleted() {
        return completed;
    }

    public void markCompleted() {
        completed = true;
    }

    public String toFileLine() {
        String safeTitle = title
            .replace('\t', ' ')
            .replace('\n', ' ')
            .replace('\r', ' ');

        return id + "\t" + completed + "\t" + safeTitle;
    }

    public static Task fromFileLine(String line) {
        String[] parts = line.split("\t", 3);
        if (parts.length != 3) {
            throw new IllegalArgumentException("Ожидалось три поля");
        }
        if (!parts[1].equals("true") && !parts[1].equals("false")) {
            throw new IllegalArgumentException("Неверный статус задачи");
        }

        int id = Integer.parseInt(parts[0]);
        boolean completed = Boolean.parseBoolean(parts[1]);
        return new Task(id, parts[2], completed);
    }

    @Override
    public String toString() {
        String marker = completed ? "[x]" : "[ ]";
        return marker + " " + id + ". " + title;
    }
}
Эталон: TaskRepository.java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

public class TaskRepository {
    private final Path file;

    public TaskRepository(Path file) {
        this.file = file;
    }

    public List<Task> load() throws IOException {
        List<Task> tasks = new ArrayList<>();

        if (Files.notExists(file)) {
            return tasks;
        }

        List<String> lines =
            Files.readAllLines(file, StandardCharsets.UTF_8);

        for (int i = 0; i < lines.size(); i++) {
            String line = lines.get(i);
            if (line.isBlank()) {
                continue;
            }

            try {
                tasks.add(Task.fromFileLine(line));
            } catch (RuntimeException error) {
                throw new IOException(
                    "Повреждена строка " + (i + 1) + " в " + file,
                    error
                );
            }
        }

        return tasks;
    }

    public void save(List<Task> tasks) throws IOException {
        Path parent = file.getParent();
        if (parent != null) {
            Files.createDirectories(parent);
        }

        List<String> lines = tasks.stream()
            .map(Task::toFileLine)
            .toList();

        Files.write(file, lines, StandardCharsets.UTF_8);
    }
}
Эталон: TaskService.java
import java.io.IOException;
import java.util.List;

public class TaskService {
    private final TaskRepository repository;
    private final List<Task> tasks;
    private int nextId;

    public TaskService(TaskRepository repository) throws IOException {
        this.repository = repository;
        this.tasks = repository.load();
        this.nextId = tasks.stream()
            .mapToInt(Task::getId)
            .max()
            .orElse(0) + 1;
    }

    public Task add(String title) throws IOException {
        Task task = new Task(nextId, title, false);
        tasks.add(task);
        nextId++;
        repository.save(tasks);
        return task;
    }

    public boolean complete(int id) throws IOException {
        Task task = findById(id);
        if (task == null) {
            return false;
        }

        task.markCompleted();
        repository.save(tasks);
        return true;
    }

    public boolean delete(int id) throws IOException {
        Task task = findById(id);
        if (task == null) {
            return false;
        }

        tasks.remove(task);
        repository.save(tasks);
        return true;
    }

    public List<Task> getAll() {
        return List.copyOf(tasks);
    }

    public List<Task> getOpen() {
        return tasks.stream()
            .filter(task -> !task.isCompleted())
            .toList();
    }

    private Task findById(int id) {
        for (Task task : tasks) {
            if (task.getId() == id) {
                return task;
            }
        }
        return null;
    }
}
Эталон: Main.java
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Scanner;

public class Main {
    private static final Scanner SCANNER = new Scanner(System.in);

    public static void main(String[] args) {
        TaskRepository repository =
            new TaskRepository(Path.of("data", "tasks.tsv"));

        try {
            TaskService service = new TaskService(repository);
            runMenu(service);
        } catch (IOException error) {
            System.out.println(
                "Не удалось запустить приложение: " + error.getMessage()
            );
        }
    }

    private static void runMenu(TaskService service) {
        while (true) {
            printMenu();
            String command = SCANNER.nextLine().trim();

            try {
                switch (command) {
                    case "1" -> addTask(service);
                    case "2" -> printTasks("Все задачи", service.getAll());
                    case "3" -> completeTask(service);
                    case "4" -> deleteTask(service);
                    case "5" -> printTasks("Открытые задачи", service.getOpen());
                    case "0" -> {
                        System.out.println("До встречи!");
                        return;
                    }
                    default -> System.out.println("Неизвестная команда.");
                }
            } catch (NumberFormatException error) {
                System.out.println("ID должен быть целым числом.");
            } catch (IllegalArgumentException error) {
                System.out.println("Ошибка: " + error.getMessage());
            } catch (IOException error) {
                System.out.println(
                    "Не удалось сохранить данные: " + error.getMessage()
                );
            }
        }
    }

    private static void printMenu() {
        System.out.println();
        System.out.println("=== МЕНЕДЖЕР ЗАДАЧ ===");
        System.out.println("1. Добавить задачу");
        System.out.println("2. Показать все");
        System.out.println("3. Отметить выполненной");
        System.out.println("4. Удалить");
        System.out.println("5. Показать открытые");
        System.out.println("0. Выход");
        System.out.print("Команда: ");
    }

    private static void addTask(TaskService service) throws IOException {
        System.out.print("Название: ");
        String title = SCANNER.nextLine();
        Task task = service.add(title);
        System.out.println("Добавлена задача с ID " + task.getId());
    }

    private static void completeTask(TaskService service) throws IOException {
        int id = readId();
        if (service.complete(id)) {
            System.out.println("Задача выполнена.");
        } else {
            System.out.println("Задача с таким ID не найдена.");
        }
    }

    private static void deleteTask(TaskService service) throws IOException {
        int id = readId();
        if (service.delete(id)) {
            System.out.println("Задача удалена.");
        } else {
            System.out.println("Задача с таким ID не найдена.");
        }
    }

    private static int readId() {
        System.out.print("ID задачи: ");
        return Integer.parseInt(SCANNER.nextLine());
    }

    private static void printTasks(String title, List<Task> tasks) {
        System.out.println();
        System.out.println("--- " + title + " ---");

        if (tasks.isEmpty()) {
            System.out.println("Список пуст.");
            return;
        }

        for (Task task : tasks) {
            System.out.println(task);
        }
    }
}

Сборка и запуск

Из корневой папки task-tracker:

javac -d out src/*.java
java -cp out Main

В PowerShell команда с *.java может вести себя иначе в зависимости от среды. Проще открыть проект в IDE и запустить Main.java, либо передать компилятору имена четырех файлов явно.

Приемочные сценарии

Сценарий Действия Ожидаемый результат
Первый запуск Запустить без папки data. Приложение показывает пустой список и не падает.
Добавление Добавить две разные задачи. Получены ID 1 и 2; обе видны в списке.
Проверка ввода Попробовать добавить пустое название. Понятное сообщение, новая задача не появилась.
Завершение Завершить ID 1. Маркер меняется с [ ] на [x].
Неверный ID Ввести буквы, затем несуществующий ID. Два понятных сообщения, приложение продолжает работу.
Сохранение Выйти и запустить снова. Задачи и их статусы сохранились.
Удаление Удалить ID 2 и перезапустить. Удаленная задача не возвращается.

Улучшения после обязательной версии

Добавляйте их по одному, проверяя старые сценарии после каждого изменения:

  1. приоритет через enum Priority;
  2. срок выполнения через LocalDate;
  3. поиск по части названия;
  4. сортировка: сначала открытые, затем по сроку;
  5. команда редактирования названия;
  6. автоматические тесты TaskService;
  7. замена TSV на JSON после подключения подходящей библиотеки.
Главный результат проекта — не количество функций Важнее, чтобы обязательные сценарии работали надежно, код был разбит на понятные части, ошибки обрабатывались, а стажер мог объяснить каждое свое решение.
Для компании

Как провести курс со стажерами

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

Учебный план

Неделя Темы Результат недели Что проверяет наставник
1 Уроки 1–4 Консольный калькулятор и конвертер Запуск, типы, имена, целое деление, ввод
2 Уроки 5–7 Игра «Угадай число», разбитая на методы Условия, остановка циклов, декомпозиция
3 Уроки 8–10 Мини-каталог книг Индексы, объекты, инкапсуляция, валидация
4 Уроки 11–13 Каталог на коллекциях с обработкой ошибок Интерфейсы, List/Set/Map, конкретные catch
5 Уроки 14–16 Сохранение каталога и набор тестов UTF-8, даты, отладка, крайние случаи
6 Урок 17 Менеджер задач и защита проекта Архитектура, сценарии, объяснение решений

Формат одной недели

Теория:  стажер читает и повторяет примеры
Практика:  решает задачи без открытого ответа
Ревью:  наставник задает вопросы по коду
Исправление:  стажер сам вносит изменения

Рубрика итогового проекта

За каждый критерий поставьте 0 — не работает, 1 — частично, 2 — выполнено уверенно. Максимум — 20 баллов.

Критерий Что означает 2 балла
ФункциональностьВсе обязательные команды работают по сценарию.
СохранениеСостояние корректно восстанавливается после перезапуска.
Проверка вводаПустые значения, неверные команды и ID не ломают приложение.
СтруктураMain, модель, логика и хранение имеют разные обязанности.
ИнкапсуляцияПоля закрыты, правила защищены методами и конструкторами.
КоллекцииКоллекция и операции с ней выбраны осознанно.
ИсключенияНет пустых catch; сообщения понятны, причина не скрывается.
ЧитаемостьПонятные имена, короткие методы, единое форматирование.
ПроверкаПройдены приемочные сценарии и есть тесты ключевой логики.
ЗащитаСтажер объясняет код и может изменить его без подсказки.

Вопросы для короткой защиты

  1. Почему ID находится в Task, а поиск — в TaskService?
  2. Что произойдет, если файл еще не существует?
  3. Почему getAll() возвращает List.copyOf?
  4. Где проверяется пустое название и почему именно там?
  5. Чем List в проекте удобнее массива?
  6. Как программа отличает поврежденную строку файла от корректной?
  7. Какой тест вы добавили бы первым и почему?
  8. Как добавить приоритет, не сломав старые данные?
Правило использования ИИ на обучении ИИ может объяснить ошибку, предложить варианты и провести ревью. Но стажер должен уметь без подсказки объяснить каждую строку принятого решения, воспроизвести проверку и внести небольшое изменение во время защиты. Непонятный код считается незавершенным.

Уровни результата

0–9: повторить основу

Вернуться к условиям, циклам, методам и проверке ввода.

10–14: базовый уровень

Приложение работает, но структура или надежность требуют помощи.

15–17: уверенный начинающий

Стажер самостоятельно реализует небольшую консольную задачу.

18–20: готов к следующему этапу

Можно переходить к Git, Maven/Gradle, JUnit, SQL и веб-разработке.

Справочник

Словарь начинающего разработчика

Не учите определения наизусть. Возвращайтесь к таблице, когда встречаете незнакомое слово в задании, IDE или сообщении об ошибке.

Термин Простое объяснение
АлгоритмПоследовательность шагов для решения задачи.
АргументКонкретное значение, переданное методу при вызове.
Байт-кодРезультат компиляции Java, который выполняет JVM.
ИнкапсуляцияСкрытие внутреннего состояния за безопасными операциями.
ИнтерфейсДоговор: какие действия должен поддерживать объект.
ИсключениеОбъект, сообщающий о проблеме во время выполнения.
КлассОписание данных и поведения объектов определенного типа.
КоллекцияГотовая структура для хранения группы элементов.
КомпиляторПроверяет исходный код и переводит его в байт-код.
КонструкторОсобый код, задающий начальное состояние объекта.
ЛямбдаКороткая запись поведения, которое можно передать методу.
МетодИменованный блок кода, выполняющий одну операцию.
ОбъектКонкретный экземпляр класса со своим состоянием.
ПараметрПеременная в объявлении метода, принимающая аргумент.
ПолиморфизмВозможность вызывать общий метод у разных реализаций.
ПолеПеременная, хранящая состояние объекта или класса.
РефакторингУлучшение структуры кода без изменения поведения.
СсылкаЗначение, указывающее на объект в памяти.
Стек вызововЦепочка методов, приведшая к текущей точке или ошибке.
ТестКод, который сравнивает фактическое поведение с ожидаемым.
ТипПравило о том, какие значения и операции допустимы.
JDKИнструменты для разработки Java-программ.
JVMВиртуальная машина, выполняющая байт-код.
LTSРелиз с длительным сроком поддержки.

Шпаргалка по синтаксису

// Переменные
int count = 5;
String name = "Анна";
boolean ready = true;

// Условие
if (count > 0) {
    System.out.println("Есть элементы");
} else {
    System.out.println("Пусто");
}

// Циклы
for (int i = 0; i < count; i++) {
    System.out.println(i);
}

while (!ready) {
    ready = true;
}

// Метод
static int square(int number) {
    return number * number;
}

// Список
List<String> names = new ArrayList<>();
names.add("Анна");

// Обработка ожидаемой ошибки
try {
    int value = Integer.parseInt("42");
} catch (NumberFormatException error) {
    System.out.println("Неверное число");
}

Куда двигаться после курса

  1. Git. Коммиты, ветки, pull request и спокойное ревью изменений.
  2. Maven или Gradle. Стандартная структура проекта и зависимости.
  3. JUnit 5. Параметризованные тесты, моки — позже и по необходимости.
  4. SQL и JDBC. Таблицы, запросы, транзакции и хранение данных.
  5. HTTP и REST. Как клиент и сервер обмениваются запросами.
  6. Spring Boot. Только после уверенного понимания Java Core.
Не спешите во фреймворк Если условия, коллекции, исключения и объекты пока требуют постоянного копирования, еще один небольшой проект на Java Core принесет больше пользы, чем ранний переход к Spring.

Официальные материалы

Версия курса: 1.0, июль 2026 года. Учебная база — Java 25 LTS; экспериментальные preview-возможности не используются.

Финальная мысль Хороший начинающий разработчик — не тот, кто помнит все методы Java. Это человек, который умеет разбить задачу, найти нужную информацию, проверить предположение, прочитать ошибку и понятно объяснить свое решение.