Программирование на Java для начинающих
Очень понятный путь от первой строки кода до собственного приложения. Объясняем простыми словами, показываем на примерах и сразу закрепляем задачами.
// Ваша первая программа public class Main { public static void main( String[] args) { System.out.println( "Привет, разработчик!" ); } } > Привет, разработчик!
Как получить от курса максимум
Программирование похоже на обучение езде на велосипеде: читать полезно, но навык появляется только тогда, когда вы сами садитесь и пробуете. Поэтому здесь мало «сухой» теории и много короткой практики.
Правило каждого урока: «прочитал → повторил → изменил → решил»
- Прочитайте объяснение. Не пытайтесь запомнить каждое слово. Сначала поймите общую идею.
- Напечатайте пример руками. Копирование экономит минуту, но отнимает тренировку.
- Предскажите результат. До запуска скажите себе, что программа выведет и почему.
- Измените пример. Поменяйте числа, текст или условие и посмотрите, что произойдет.
- Решите задачи без подсказки. Открывайте решение только после честной попытки.
Рекомендуемый ритм
Самостоятельно
3 урока в неделю по 60–90 минут. Полный курс займет примерно 6 недель.
Со стажерами
2 урока, 1 практическая встреча и короткое ревью кода каждую неделю.
java-course, а внутри — отдельную папку
lesson-01, lesson-02 и так далее. Не стирайте старые решения:
через месяц вы увидите собственный прогресс.
Что такое Java, JDK и JVM
Перед кодом разберемся, кто превращает наши команды в работающую программу. Терминов всего три, и у каждого есть простая роль.
Java — это язык
Язык программирования — набор слов и правил, которыми человек описывает задачу компьютеру.
В Java мы пишем обычный текст в файлах с расширением .java.
| Термин | Полное название | Простое объяснение |
|---|---|---|
| JDK | Java Development Kit | Набор разработчика: компилятор, JVM и другие инструменты. |
| JVM | Java Virtual Machine | Виртуальная машина, которая выполняет байт-код Java. |
| JRE | Java Runtime Environment | Среда выполнения. Отдельно новичку ее обычно устанавливать не нужно: она входит в JDK. |
Как запускается программа
Main.javaпроверяет и переводит
Main.class выполняет JVMБлагодаря JVM один и тот же байт-код можно запускать на Windows, macOS и Linux, если для системы установлена подходящая Java.
Установка
- Установите JDK 25 LTS для своей операционной системы. Ссылку на официальную страницу загрузки найдете в конце курса.
- Откройте терминал: Terminal в macOS/Linux или PowerShell в Windows.
- Выполните две команды:
java --version
javac --version
В обеих строках должна появиться версия 25 (после нее могут быть дополнительные цифры).
Практика разминка
- Объясните своими словами разницу между JDK и JVM.
- Найдите на компьютере версии
javaиjavac. - Ответьте: какой файл пишет человек, а какой создает компилятор?
Проверить ответы
- JDK нужен, чтобы разрабатывать и собирать программы; JVM — чтобы выполнять байт-код.
- Обе версии показывает терминал после команд выше.
- Человек пишет
.java, компилятор создает.class.
Первая программа: разбираем каждую строчку
Сейчас создадим маленькую программу, запустим ее и поймем назначение всех символов. После этого Java перестанет выглядеть как заклинание.
Main.javamain и выводШаг 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
Первая команда создает Main.class. Вторая просит JVM выполнить
класс Main. При запуске расширение .class не пишут.
Что означает код
| Фрагмент | Что говорит Java |
|---|---|
public class Main |
«Создай доступный класс с именем Main». |
{ ... } |
Фигурные скобки обозначают начало и конец блока. |
main |
Точка старта программы. JVM ищет именно этот метод. |
String[] args |
Место для аргументов командной строки. Пока просто оставляем его. |
System.out.println(...) |
Вывести значение и перейти на новую строку. |
; |
Точка с запятой завершает команду. |
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— перейти на новую строку;\"— кавычка внутри текста;\\— обычный обратный слеш.
Практика легко
-
Выведите на трех отдельных строках свое имя, направление стажировки
и фразу
Я изучаю Java. - Нарисуйте символами прямоугольник размером 5 × 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("#####");
}
}
Имя и направление замените своими. В третьей задаче следующий текст продолжится в той же строке.
Переменные и типы данных
Программе нужно хранить имя пользователя, количество товаров, цену и множество других значений. Для этого существуют переменные — подписанные ячейки памяти.
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;
Константы принято называть заглавными буквами через подчеркивание. Попытка присвоить им другое значение вызовет ошибку компиляции.
Практика легко
- Создайте переменные для названия товара, его цены, количества и наличия на складе. Выведите их понятными строками.
-
Даны стороны прямоугольника
8и5. Посчитайте площадь и периметр, сохранив результаты в переменные. -
Создайте
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 + " мин");
Операторы и ввод данных
До сих пор значения были записаны прямо в коде. Научим программу задавать вопросы пользователю, получать ответы и выполнять вычисления.
Арифметические операторы
| Оператор | Действие | Пример | Результат |
|---|---|---|---|
+ | Сложение | 7 + 3 | 10 |
- | Вычитание | 7 - 3 | 4 |
* | Умножение | 7 * 3 | 21 |
/ | Деление | 7 / 3 | 2 для целых чисел |
% | Остаток от деления | 7 % 3 | 1 |
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
Практика средне
- Запросите цену товара и количество. Выведите общую стоимость.
- Пользователь вводит общее количество секунд. Выведите полные часы, минуты и оставшиеся секунды.
-
Напишите простой конвертер температуры из градусов Цельсия в Фаренгейты:
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 + " с");
Решение конвертера температуры
double celsius = 20.0;
double fahrenheit = celsius * 9.0 / 5.0 + 32.0;
System.out.println(fahrenheit);
Используем дробные числа, чтобы деление не стало целым.
Условия: учим программу принимать решения
Условие позволяет выполнять разные команды в разных ситуациях: показать скидку постоянному клиенту, запретить неверный пароль или определить победителя.
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("Нужно повторить тему");
}
Как сравнивать строки
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.
Практика средне
- Пользователь вводит число. Сообщите, положительное оно, отрицательное или равно нулю.
-
Даны три числа. Найдите самое большое без использования готовой функции
Math.max. -
Запросите логин и пароль. Доступ разрешен только при логине
internи паролеjava25. - Со звездочкой: определите, является ли год високосным. Год високосный, если делится на 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 ? "Високосный" : "Обычный");
Здесь показан тернарный оператор: условие ? значениеЕслиДа : значениеЕслиНет.
Используйте его только для короткого и очевидного выбора.
Циклы: повторяем команды без копирования
Если нужно вывести сто строк, не надо сто раз писать println.
Цикл повторяет блок кода, пока выполняется заданное правило.
for и whileЦикл for: когда известно количество повторений
for (int i = 1; i <= 5; i++) {
System.out.println("Шаг " + i);
}
В круглых скобках три части:
int i = 1— стартовое значение счетчика;i <= 5— условие продолжения;i++— изменение после каждого шага.
Накапливаем результат
int sum = 0;
for (int number = 1; number <= 100; number++) {
sum += number;
}
System.out.println("Сумма: " + sum);
Переменная 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();
}
Практика средне
- Выведите все четные числа от 2 до 20.
- Запросите число и выведите таблицу умножения для него от 1 до 10.
- Посчитайте факториал числа
n:1 × 2 × ... × n. - Мини-проект: компьютер загадывает число от 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);
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);
}
}
Методы: делим большую задачу на маленькие
Метод — именованный кусочек программы, который выполняет одну понятную работу. Методы уменьшают повторение и превращают длинный код в читаемый план действий.
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. - Он достаточно короткий, чтобы понять его без долгой прокрутки.
- Если код повторяется два-три раза, возможно, его стоит вынести в метод.
Практика средне
- Создайте метод
isEven(int number), который проверяет четность. - Создайте метод
maxOfThree(int a, int b, int c). -
Создайте метод
countVowels(String text), который считает русские гласныеа, е, ё, и, о, у, ы, э, ю, ябез учета регистра. - Разделите игру «Угадай число» из прошлого урока минимум на три метода.
Решение: четность и максимум
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;
}
Массивы и строки
Отдельная переменная удобна для одного значения. Когда оценок, имен или цен много, их объединяют в массив и обрабатывают циклом.
Массив: несколько значений одного типа
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);
Практика средне
- Найдите минимальное и максимальное число в массиве.
- Создайте новый массив, в котором элементы исходного идут в обратном порядке.
-
Проверьте, является ли слово палиндромом: одинаково читается слева направо
и справа налево, например
топот. - Посчитайте, сколько раз каждое слово встречается в предложении. Пока можно выбрать одно искомое слово; полное решение появится в уроке о коллекциях.
Решение: минимум и максимум
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;
}
Классы и объекты: создаем собственные типы
До этого мы пользовались готовыми типами: String, Scanner,
массивами. Теперь создадим собственный тип, который объединит данные и поведение.
Описываем класс
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();
}
}
Инкапсуляция: защищаем правильное состояние
Хороший класс не просто хранит данные. Он не позволяет сделать их бессмысленными: установить отрицательный заряд, снять больше денег, чем есть, или создать товар без названия.
privatestatic, 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 удобен для простых
неизменяемых значений, но не заменяет все обычные классы.
Практика средне
-
Улучшите класс
Book: сделайте поля закрытыми, проверьте, что название не пустое, а страниц больше нуля. - Создайте
enum Priorityсо значениями LOW, MEDIUM, HIGH. - Создайте 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 сама присвоит их компонентам.
Наследование, интерфейсы и полиморфизм
Разные объекты могут выполнять одну и ту же команду по-разному. Круг и прямоугольник оба умеют считать площадь — формулы у них разные, а общий вопрос один.
Интерфейс — договор о поведении
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, но машина не является двигателем.
Практика средне
-
Создайте интерфейс
PaymentMethodс методомpay(int amountCents). -
Реализуйте
CardPaymentиCashPayment, которые выводят разные сообщения. -
Сложите их в массив
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);
}
Коллекции: удобная работа с наборами данных
Размер массива фиксируется при создании. В реальном приложении мы заранее не знаем, сколько появится задач, пользователей или заказов. Коллекции умеют расти.
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.
Практика средне
- Создайте список покупок. Добавьте пять значений, удалите одно, замените одно и выведите список с порядковыми номерами.
- Дан список городов с повторами. Получите количество уникальных городов.
-
Создайте телефонную книгу
Map<String, String>: имя — ключ, номер — значение. Реализуйте поиск по имени. - Нормализуйте регистр и посчитайте частоту всех слов в произвольной строке.
Решение: нумерованный список покупок
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("Контакт не найден");
}
Исключения: что делать, когда что-то пошло не так
Пользователь может ввести буквы вместо числа, файл может отсутствовать, а сеть — отключиться. Хорошая программа не притворяется, что ошибок не бывает: она замечает их и реагирует предсказуемо.
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)
- Первая строка: тип и причина — деление на ноль.
- Первая строка стека с вашим классом:
Calculator.java:12. - Дальше видно, кто вызвал проблемный метод:
Main.java:5.
if. Исключение подходит для ситуации, в которой нормальное выполнение
конкретной операции невозможно.
Практика средне
-
Создайте метод
divide(double a, double b), запрещающий нулевой делитель. - Запрашивайте положительное количество товара до тех пор, пока пользователь не введет корректное целое число.
- Найдите элемент массива по индексу. Обработайте неверный индекс понятным сообщением.
Решение метода 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("Введите целое число.");
}
}
}
Файлы и даты: сохраняем данные между запусками
Переменные исчезают после завершения программы. Файл позволяет сохранить список задач сегодня и снова загрузить его завтра.
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 превращайте ее только для ввода, вывода или хранения
в выбранном формате.
Практика средне
-
Запросите у пользователя три заметки, сохраните их в
notes.txt, затем прочитайте файл и выведите строки с номерами. -
Запросите дату рождения в формате
dd.MM.yyyyи выведите возраст в полных годах. Подсказка: используйтеPeriod.between. - Создайте файл журнала, где каждая строка начинается с текущей даты и времени.
Решение: заметки в файле
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);
Лямбды и 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 состоит из понятных этапов:
stream()— начать поток обработки;filter— оставить только подходящие элементы;map— превратить каждый элемент;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();
filter и map сами по себе ничего не запускают.
Обработка начинается, когда появляется завершающая операция:
toList, sum, count, forEach и другие.
Практика средне
- Из списка чисел получите новый список квадратов только четных чисел, отсортированный по убыванию.
-
Дан список товаров
Product(name, priceCents). Оставьте товары дешевле 10 000, получите только названия и отсортируйте по алфавиту. - Найдите суммарную длину всех непустых строк в списке.
- Решите одну из задач двумя способами — циклом и 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);
Решение: товары
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();
Отладка, тесты и привычки разработчика
Написать код — половина работы. Вторая половина — доказать, что он работает, найти ошибку, сделать решение понятным команде и не сломать его следующим изменением.
Алгоритм отладки
- Воспроизведите проблему. Запишите точные входные данные и ожидаемый результат.
- Сузьте область. Найдите метод или условие, где результат впервые становится неверным.
- Проверьте предположение. Посмотрите значения переменных, не меняя сразу пять строк.
- Исправьте причину. Не маскируйте симптом дополнительным случайным условием.
- Добавьте проверку. Тест должен воспроизводить старую ошибку и подтверждать исправление.
Отладчик 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++.
Полезный комментарий объясняет неочевидное правило, ограничение или причину решения.
Минимальная командная дисциплина
- Перед задачей уточните ожидаемое поведение и крайние случаи.
- Делайте небольшие логические изменения.
- Перед передачей кода запустите программу и тесты самостоятельно.
- Не смешивайте исправление ошибки с несвязанной большой переделкой.
- В ревью спокойно обсуждается код, а не человек.
Практика важно
- Напишите не менее пяти тестов для метода
isPalindrome. -
Найдите ошибку: метод среднего значения возвращает неверный ответ для
{2, 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.
Итоговый проект: консольный менеджер задач
Соберем изученные темы в одно приложение: пользователь добавляет задачи, отмечает выполненные, удаляет и после перезапуска видит сохраненные данные. Это уже не отдельное упражнение, а маленькая законченная система.
Что должно уметь приложение
- показывать меню до команды выхода;
- добавлять задачу с уникальным числовым ID;
- показывать все или только невыполненные задачи;
- отмечать задачу выполненной;
- удалять задачу по ID;
- проверять пустое название и неверный ввод;
- сохранять данные в UTF-8 файл
data/tasks.tsv; - загружать сохраненные данные при следующем запуске.
Структура проекта
task-tracker/
├── data/
│ └── tasks.tsv ← появится автоматически
└── src/
├── Main.java ← меню и общение с пользователем
├── Task.java ← модель одной задачи
├── TaskService.java ← правила и операции
└── TaskRepository.java ← чтение и запись файла
ввод и вывод
правила приложения
файл с данными
Модель Task используется всеми тремя частями. Такое разделение
делает код проще для проверки: консоль не решает, как хранить файл,
а репозиторий не спрашивает пользователя.
План разработки
-
Модель. Создайте
Task, конструктор, геттеры,markCompleted()и красивыйtoString(). -
Логика в памяти. Создайте
TaskServiceсArrayList. Добавление, завершение и удаление сначала работают без файла. -
Меню. Подключите
Scannerи цикл вMain. Проверьте неверные команды и ID. -
Хранение. Реализуйте
TaskRepositoryи сохраняйте после каждой изменяющей операции. - Проверка. Пройдите приемочные сценарии и только потом улучшайте интерфейс.
Эталон: 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 и перезапустить. | Удаленная задача не возвращается. |
Улучшения после обязательной версии
Добавляйте их по одному, проверяя старые сценарии после каждого изменения:
- приоритет через
enum Priority; - срок выполнения через
LocalDate; - поиск по части названия;
- сортировка: сначала открытые, затем по сроку;
- команда редактирования названия;
- автоматические тесты
TaskService; - замена 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; сообщения понятны, причина не скрывается. |
| Читаемость | Понятные имена, короткие методы, единое форматирование. |
| Проверка | Пройдены приемочные сценарии и есть тесты ключевой логики. |
| Защита | Стажер объясняет код и может изменить его без подсказки. |
Вопросы для короткой защиты
- Почему ID находится в
Task, а поиск — вTaskService? - Что произойдет, если файл еще не существует?
- Почему
getAll()возвращаетList.copyOf? - Где проверяется пустое название и почему именно там?
- Чем
Listв проекте удобнее массива? - Как программа отличает поврежденную строку файла от корректной?
- Какой тест вы добавили бы первым и почему?
- Как добавить приоритет, не сломав старые данные?
Уровни результата
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("Неверное число");
}
Куда двигаться после курса
- Git. Коммиты, ветки, pull request и спокойное ревью изменений.
- Maven или Gradle. Стандартная структура проекта и зависимости.
- JUnit 5. Параметризованные тесты, моки — позже и по необходимости.
- SQL и JDBC. Таблицы, запросы, транзакции и хранение данных.
- HTTP и REST. Как клиент и сервер обмениваются запросами.
- Spring Boot. Только после уверенного понимания Java Core.
Официальные материалы
- Dev.java — учебные материалы по языку, коллекциям, Stream API, JVM и инструментам
- Java SE 25 & JDK 25 API — справочник классов и методов
- Oracle Java SE Support Roadmap — список LTS-релизов и сроки поддержки
- Официальная загрузка JDK 25
Версия курса: 1.0, июль 2026 года. Учебная база — Java 25 LTS; экспериментальные preview-возможности не используются.