Java Backend Handbook
Практическое руководство по Java Backend разработке
Добро пожаловать в Java Backend Handbook - комплексное руководство, которое поможет вам освоить разработку серверных приложений на Java.
Для кого эта книга
Это руководство предназначено для:
- Начинающих разработчиков, желающих освоить Java Backend
- Опытных разработчиков, которые хотят систематизировать знания
- Специалистов, готовящихся к техническим собеседованиям
Структура книги
Книга разделена на шесть частей:
Часть I: Основы Java
Фундаментальные концепции языка Java, от базового синтаксиса до многопоточности.
Часть II: Алгоритмы и паттерны
Структуры данных, алгоритмы и паттерны проектирования.
Часть III: Spring Ecosystem
Экосистема Spring Framework: Spring Core, Spring Boot, Spring Security, Spring Cloud и WebFlux.
Часть IV: Данные и интеграции
Работа с базами данных и брокерами сообщений.
Часть V: Инструменты и DevOps
Системы сборки, контейнеризация и CI/CD.
Часть VI: Дополнительно
Kotlin и Agile методологии.
Как пользоваться книгой
Каждая тема содержит:
- Теоретическое описание
- Ссылки на документацию
- Ссылки на видеоматериалы
Удачи в изучении Java Backend разработки!
1. Java Basic
Первые шаги в мире Java
В этом разделе вы изучите основы языка Java: синтаксис, типы данных, управляющие конструкции и основы объектно-ориентированного программирования.
Содержание раздела
- Введение в Java
- Синтаксис и структура программы
- Примитивные типы данных
- Операторы и выражения
- Управляющие конструкции
- Массивы
- Методы
- ООП: Классы и объекты
- Наследование и полиморфизм
- Интерфейсы и абстрактные классы
1.1. Введение в Java
Материалы
Добро пожаловать в мир Java! Если вы слышали о Java но не знаете с чего начать, или просто хотите понять что это такое - вы в правильном месте.
Что такое Java?
Java - это одновременно язык программирования и платформа. Да, сразу две вещи в одной! Давайте разберёмся что это значит.
Java как язык программирования
Java - это высокоуровневый язык, созданный быть:
- Простым - легче чем C++, но мощным
- Объектно-ориентированным - всё вращается вокруг объектов
- Безопасным - множество проверок защищают от ошибок
- Надёжным - сборщик мусора управляет памятью за вас
- Переносимым - “напиши один раз, запускай везде”
- Многопоточным - встроенная поддержка параллелизма
- Быстрым - современные JVM очень оптимизированы
Java как платформа
Обычная платформа - это комбинация операционной системы и оборудования. Java платформа другая - это программная платформа поверх обычных платформ.
Java платформа состоит из двух компонентов:
- Java Virtual Machine (JVM) - виртуальная машина, которая запускает ваш код
- Java API - огромная библиотека готовых компонентов
Как работает Java?
В отличие от языков вроде C, Java работает по-особенному:
Процесс разработки
┌─────────────────┐
│ MyProgram.java │ ← Исходный код (текстовый файл)
└────────┬────────┘
│
[компиляция]
javac компилятор
│
▼
┌─────────────────┐
│ MyProgram.class │ ← Байт-код (не машинный код!)
└────────┬────────┘
│
[выполнение]
java launcher
│
▼
┌────────┐
│ JVM │ ← Виртуальная машина
└────┬───┘
│
▼
Ваша программа работает!
Три ключевых шага:
1. Пишете код
Создаёте файл с расширением .java и пишете код:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Привет, мир!");
}
}
2. Компилируете
Компилятор javac превращает ваш код в байт-код:
javac HelloWorld.java
Это создаёт файл HelloWorld.class с байт-кодом.
Важно: Байт-код - это НЕ машинный код вашего процессора! Это специальный промежуточный код для JVM.
3. Запускаете
JVM запускает байт-код:
java HelloWorld
И видите:
Привет, мир!
“Напиши один раз, запускай везде”
Вот в чём магия Java! Тот же самый .class файл работает на:
- Windows
- macOS
- Linux
- И многих других платформах
HelloWorld.class (один файл байт-кода)
│
├─────────────┬─────────────┬─────────────┐
│ │ │ │
▼ ▼ ▼ ▼
JVM Windows JVM macOS JVM Linux JVM Android
│ │ │ │
▼ ▼ ▼ ▼
Работает везде одинаково!
Компилируете один раз, запускаете везде. Не нужно перекомпилировать под каждую платформу!
Что такое JVM?
Java Virtual Machine - это “компьютер внутри компьютера”. Это программа, которая:
- Загружает ваш байт-код
- Проверяет его на безопасность
- Выполняет его
- Оптимизирует горячие участки кода прямо во время работы
Современные JVM (как HotSpot) очень умные:
- Находят узкие места производительности
- Компилируют часто используемый код в машинный код
- Управляют памятью (сборщик мусора)
- Делают вашу программу быстрой
Интересный факт: JVM может запускать не только Java! Kotlin, Scala, Groovy, Clojure - все они компилируются в байт-код JVM.
Java API: ваш швейцарский нож
Java поставляется с огромной стандартной библиотекой - Java API. Это тысячи готовых классов для всего:
- java.lang - базовые классы (String, Math, System)
- java.util - коллекции, дата/время
- java.io - работа с файлами
- java.net - сетевое программирование
- java.awt / javax.swing - графические интерфейсы
- javafx - современный UI
- И многое другое!
Вместо того чтобы писать всё с нуля, вы используете готовые компоненты:
import java.util.ArrayList;
import java.util.List;
List<String> names = new ArrayList<>();
names.add("Анна");
names.add("Боб");
names.add("Карл");
for (String name : names) {
System.out.println("Привет, " + name);
}
Всё уже написано за вас!
Почему Java популярна?
1. Простота изучения
Хотя Java мощная, она проще чем C++:
- Нет указателей (и утечек памяти!)
- Автоматическая сборка мусора
- Меньше сложных концепций
2. Пиши меньше кода
Java код обычно в 4 раза короче аналогичного C++ кода. Меньше кода = меньше ошибок!
// Java - кратко и ясно
String message = "Hello";
System.out.println(message.toUpperCase());
// На C++ это было бы сложнее
3. Объектно-ориентированное программирование
Всё в Java - объект (почти). Это помогает организовать код:
class Dog {
private String name;
public Dog(String name) {
this.name = name;
}
public void bark() {
System.out.println(name + " говорит: Гав!");
}
}
Dog myDog = new Dog("Шарик");
myDog.bark(); // Шарик говорит: Гав!
4. Надёжность
Java заботится о безопасности:
- Строгая типизация ловит ошибки при компиляции
- Автоматическое управление памятью
- Проверка границ массивов
- Обработка исключений встроена в язык
5. Огромное сообщество
- Миллионы разработчиков по всему миру
- Тысячи библиотек и фреймворков
- Много вакансий
- Куча обучающих материалов
Где используется Java?
Java везде! Вот несколько примеров:
Веб-приложения (Backend)
- Spring Boot - самый популярный Java фреймворк
- Amazon, Netflix, LinkedIn используют Java
Мобильные приложения
- Android - большинство приложений написано на Java/Kotlin
Корпоративные системы
- Банки, страховые компании
- Системы управления предприятием
Big Data
- Hadoop, Apache Spark написаны на Java
- Обработка терабайтов данных
Встроенные системы
- Смарт-карты
- Телевизионные приставки
- IoT устройства
Desktop приложения
- IntelliJ IDEA (лучшая IDE для Java)
- Minecraft (написан на Java!)
Ваша первая программа
Давайте создадим классическую программу “Hello World”!
1. Создайте файл HelloWorld.java:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
2. Скомпилируйте:
javac HelloWorld.java
Это создаст HelloWorld.class.
3. Запустите:
java HelloWorld
Вы увидите:
Hello, World!
Поздравляем! Вы только что написали, скомпилировали и запустили вашу первую Java программу!
Что происходит в коде?
Давайте разберём построчно:
public class HelloWorld {
public- класс доступен всемclass- объявляем классHelloWorld- имя класса (должно совпадать с именем файла!)
public static void main(String[] args) {
public- метод доступен всемstatic- метод принадлежит классу, а не объектуvoid- метод ничего не возвращаетmain- точка входа в программуString[] args- параметры командной строки
System.out.println("Hello, World!");
System.out- стандартный выводprintln- вывести строку и перейти на новую строку"Hello, World!"- строка для вывода
}
}
Закрывающие скобки для метода и класса.
Основные концепции (коротко)
Вот ключевые концепции, которые вы будете изучать:
1. Классы и объекты
class Car {
String brand;
void drive() {
System.out.println(brand + " едет!");
}
}
Car myCar = new Car();
myCar.brand = "Toyota";
myCar.drive();
2. Типы данных
int age = 25; // целое число
double price = 19.99; // число с плавающей точкой
String name = "Анна"; // строка
boolean isActive = true; // логическое значение
3. Управление потоком
if (age >= 18) {
System.out.println("Взрослый");
} else {
System.out.println("Ребёнок");
}
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
4. Методы
public int add(int a, int b) {
return a + b;
}
int result = add(5, 3); // 8
5. Коллекции
List<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Банан");
fruits.add("Апельсин");
Java экосистема
Инструменты сборки
- Maven - управление зависимостями и сборка проектов
- Gradle - современная альтернатива Maven
Популярные фреймворки
- Spring / Spring Boot - веб-приложения и микросервисы
- Hibernate - работа с базами данных
- JUnit - тестирование
- JavaFX - графические интерфейсы
Важно: Java и JavaScript - это РАЗНЫЕ языки! Они похожи только названием.
Удачи в изучении Java!
Синтаксис и структура программы
Основы синтаксиса Java
Материалы
| Тип | Ссылка |
|---|---|
| Документ | Java Language Specification |
| Видео | Java Tutorial for Beginners |
Прежде чем писать сложные программы, нужно понять базовые правила языка. В этом разделе мы изучим структуру Java-программы, соглашения об именовании и основные синтаксические конструкции.
Структура Java-программы
Каждая Java-программа состоит из одного или нескольких классов. Вот минимальная программа:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
Анатомия программы
┌─────────────────────────────────────────────────────────────┐
│ public class HelloWorld { │
│ ├── Модификатор доступа: public │
│ ├── Ключевое слово: class │
│ └── Имя класса: HelloWorld (должно совпадать с файлом!) │
├─────────────────────────────────────────────────────────────┤
│ public static void main(String[] args) { │
│ ├── public: доступен JVM для запуска │
│ ├── static: не требует создания объекта │
│ ├── void: ничего не возвращает │
│ ├── main: точка входа в программу │
│ └── String[] args: аргументы командной строки │
├─────────────────────────────────────────────────────────────┤
│ System.out.println("Hello, World!"); │
│ └── Оператор (statement), заканчивается ; │
│ } │
│ } │
└─────────────────────────────────────────────────────────────┘
Правило: один public класс = один файл
// Файл: Calculator.java
public class Calculator {
// ...
}
// В этом же файле можно добавить НЕ-public классы
class Helper {
// ...
}
Имя файла ДОЛЖНО совпадать с именем public класса:
Calculator.javaсодержитpublic class CalculatorMyProgram.javaсодержитpublic class MyProgram
Пакеты (Packages)
Пакеты организуют классы в логические группы и предотвращают конфликты имён.
Объявление пакета
package com.company.project;
public class MyClass {
// ...
}
Пакет должен быть первой строкой в файле (после комментариев).
Структура каталогов
Пакет отражается в структуре папок:
src/
└── com/
└── company/
└── project/
└── MyClass.java
Соглашения об именовании пакетов
// Обратный домен компании
package com.google.common;
package org.apache.commons;
// Для учебных проектов
package ru.example.myapp;
package edu.university.course;
Пакеты пишутся строчными буквами без camelCase.
Импорты (Imports)
Импорты позволяют использовать классы из других пакетов без полного имени.
Импорт конкретного класса
import java.util.ArrayList;
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
HashMap<String, Integer> map = new HashMap<>();
}
}
Импорт всего пакета
import java.util.*; // все классы из java.util
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
LinkedList<Integer> linked = new LinkedList<>();
HashMap<String, String> map = new HashMap<>();
}
}
Статический импорт
Импортирует static члены класса:
import static java.lang.Math.PI;
import static java.lang.Math.sqrt;
import static java.lang.System.out;
public class Main {
public static void main(String[] args) {
out.println("PI = " + PI); // вместо System.out.println
out.println("sqrt(16) = " + sqrt(16)); // вместо Math.sqrt
}
}
Автоматически импортированный пакет
Пакет java.lang импортируется автоматически:
// Не нужно писать: import java.lang.String;
// Не нужно писать: import java.lang.System;
// Не нужно писать: import java.lang.Math;
String s = "Hello"; // java.lang.String
System.out.println(s); // java.lang.System
int x = Math.abs(-5); // java.lang.Math
Комментарии
Java поддерживает три типа комментариев.
Однострочные комментарии
// Это однострочный комментарий
int age = 25; // комментарий в конце строки
Многострочные комментарии
/* Это многострочный комментарий.
Он может занимать несколько строк.
Используется для временного отключения кода. */
int result = calculate(x, y);
/*
System.out.println("Этот код закомментирован");
System.out.println("И не выполняется");
*/
Документационные комментарии (Javadoc)
/**
* Вычисляет факториал числа.
*
* <p>Факториал определён только для неотрицательных чисел.
* Для отрицательных чисел выбрасывается исключение.</p>
*
* @param n неотрицательное целое число
* @return факториал числа n
* @throws IllegalArgumentException если n отрицательное
* @see Math#abs(int)
* @since 1.0
*/
public static long factorial(int n) {
if (n < 0) {
throw new IllegalArgumentException("n должно быть >= 0");
}
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
Основные теги Javadoc:
| Тег | Описание |
|---|---|
@param | Описание параметра метода |
@return | Описание возвращаемого значения |
@throws / @exception | Описание исключения |
@see | Ссылка на связанный элемент |
@since | Версия, с которой появился элемент |
@deprecated | Помечает устаревший элемент |
@author | Автор класса |
@version | Версия класса |
Идентификаторы
Идентификатор — это имя переменной, метода, класса или другого элемента.
Правила именования
Допустимо:
- Буквы (a-z, A-Z), включая Unicode
- Цифры (0-9), но НЕ в начале
- Знак подчёркивания
_ - Знак доллара
$(не рекомендуется)
Недопустимо:
- Начинать с цифры
- Использовать пробелы
- Использовать ключевые слова Java
// Допустимые идентификаторы
int age;
String firstName;
double _value;
int число; // Unicode разрешён, но не рекомендуется
int MAX_SIZE;
// Недопустимые идентификаторы
// int 2ndPlace; // начинается с цифры
// int my-var; // содержит дефис
// int class; // ключевое слово
Соглашения об именовании (Code Conventions)
Java использует разные стили для разных элементов:
| Элемент | Стиль | Примеры |
|---|---|---|
| Классы | PascalCase | MyClass, ArrayList, StringBuilder |
| Интерфейсы | PascalCase | Runnable, Comparable, List |
| Методы | camelCase | getName(), calculateTotal(), isEmpty() |
| Переменные | camelCase | firstName, totalCount, isActive |
| Константы | UPPER_SNAKE_CASE | MAX_VALUE, DEFAULT_SIZE, PI |
| Пакеты | lowercase | java.util, com.company.app |
package com.example.shop; // пакет: lowercase
public class ShoppingCart { // класс: PascalCase
private static final int MAX_ITEMS = 100; // константа: UPPER_SNAKE_CASE
private List<Item> items; // переменная: camelCase
private int totalCount;
public void addItem(Item item) { // метод: camelCase
if (totalCount < MAX_ITEMS) {
items.add(item);
totalCount++;
}
}
public int getTotalCount() { // getter: camelCase
return totalCount;
}
public boolean isEmpty() { // boolean getter: is/has/can
return totalCount == 0;
}
}
Ключевые слова Java
Java имеет 67 зарезервированных слов (включая литералы и зарезервированные для будущего):
Модификаторы доступа
public protected private
Модификаторы классов/методов/полей
abstract static final native
synchronized transient volatile strictfp
Управление потоком
if else switch case default
for while do break continue return
Исключения
try catch finally throw throws
Типы данных
boolean byte char short int long float double
void
Классы и объекты
class interface enum record extends implements
new this super instanceof
Пакеты
package import
Другие
assert const (зарезервировано) goto (зарезервировано)
Литералы (не ключевые слова, но зарезервированы)
true false null
Контекстуальные ключевые слова (Java 9+)
var yield sealed permits non-sealed record
Операторы (Statements)
Оператор — это завершённая единица выполнения. Операторы заканчиваются точкой с запятой ;.
Типы операторов
Объявление переменной:
int count;
String name = "Java";
final double PI = 3.14159;
Выражение-оператор:
count = 10; // присваивание
count++; // инкремент
System.out.println(x); // вызов метода
new ArrayList<>(); // создание объекта
Управляющий оператор:
if (condition) { }
for (int i = 0; i < 10; i++) { }
while (running) { }
return value;
break;
continue;
Блоки кода
Блок — это группа операторов в фигурных скобках:
{
int x = 10;
int y = 20;
System.out.println(x + y);
}
Переменные, объявленные в блоке, видны только внутри него:
{
int x = 10;
System.out.println(x); // OK
}
// System.out.println(x); // ОШИБКА: x не видна здесь
Точка с запятой и фигурные скобки
Когда нужна точка с запятой
// После каждого оператора
int x = 5;
System.out.println(x);
return x;
// После объявления поля
private int count;
// После import и package
package com.example;
import java.util.List;
Когда НЕ нужна точка с запятой
// После фигурных скобок блоков
if (condition) {
// код
} // НЕТ точки с запятой
for (int i = 0; i < 10; i++) {
// код
} // НЕТ точки с запятой
public void method() {
// код
} // НЕТ точки с запятой
class MyClass {
// код
} // НЕТ точки с запятой
Исключение: инициализация массивов
int[] arr = {1, 2, 3}; // точка с запятой после }
Пробелы и форматирование
Java игнорирует лишние пробелы и переносы строк. Технически это валидный код:
public class Ugly{public static void main(String[]args){System.out.println("Работает!");}}
Но читать его невозможно. Используйте форматирование:
public class Beautiful {
public static void main(String[] args) {
System.out.println("Работает!");
}
}
Рекомендации по форматированию
Отступы: 4 пробела (не табуляция)
public class Example {
public void method() {
if (condition) {
// 4 пробела на уровень
}
}
}
Фигурные скобки: открывающая на той же строке
// Рекомендуемый стиль (K&R / Java style)
if (condition) {
doSomething();
}
// Альтернативный стиль (Allman) - менее распространён в Java
if (condition)
{
doSomething();
}
Пробелы вокруг операторов:
// Хорошо
int sum = a + b;
if (x == 5) { }
for (int i = 0; i < 10; i++) { }
// Плохо
int sum=a+b;
if(x==5){}
for(int i=0;i<10;i++){}
Пустые строки для разделения логических блоков:
public class Person {
// Поля
private String name;
private int age;
// Конструктор
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Геттеры
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
Полный пример программы
Соберём всё вместе:
package com.example.greeting;
import java.util.Scanner;
import java.time.LocalTime;
/**
* Программа приветствия пользователя.
*
* Запрашивает имя и выводит приветствие
* в зависимости от времени суток.
*
* @author Developer
* @version 1.0
*/
public class GreetingApp {
// Константы для времени суток
private static final int MORNING_START = 6;
private static final int AFTERNOON_START = 12;
private static final int EVENING_START = 18;
private static final int NIGHT_START = 22;
/**
* Точка входа в программу.
*
* @param args аргументы командной строки (не используются)
*/
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// Запрашиваем имя
System.out.print("Как вас зовут? ");
String name = scanner.nextLine();
// Получаем текущий час
int hour = LocalTime.now().getHour();
// Формируем приветствие
String greeting = getGreeting(hour);
// Выводим результат
System.out.println(greeting + ", " + name + "!");
scanner.close();
}
/**
* Возвращает приветствие в зависимости от времени суток.
*
* @param hour текущий час (0-23)
* @return строка приветствия
*/
private static String getGreeting(int hour) {
if (hour >= MORNING_START && hour < AFTERNOON_START) {
return "Доброе утро";
} else if (hour >= AFTERNOON_START && hour < EVENING_START) {
return "Добрый день";
} else if (hour >= EVENING_START && hour < NIGHT_START) {
return "Добрый вечер";
} else {
return "Доброй ночи";
}
}
}
Итоги
Вы изучили основы синтаксиса Java:
- Структура программы: класс с методом
main()как точка входа - Пакеты: организация кода в логические группы
- Импорты: использование классов из других пакетов
- Комментарии: однострочные, многострочные и Javadoc
- Идентификаторы: правила именования элементов
- Соглашения: PascalCase, camelCase, UPPER_SNAKE_CASE
- Ключевые слова: зарезервированные слова языка
- Операторы: завершённые единицы выполнения
- Форматирование: отступы, скобки, пробелы
Теперь вы готовы писать правильно структурированный Java-код.
Задания для практики
-
Создайте программу
Calculator.javaв пакетеcom.example.mathс методами для базовых операций -
Напишите класс с правильными Javadoc-комментариями для всех публичных методов
-
Отформатируйте следующий код по стандартам Java:
public class ugly{public static void main(String[]a){int x=5;int y=10;if(x<y){System.out.println("x меньше");}else{System.out.println("y меньше или равно");}}} -
Определите, какие из следующих идентификаторов допустимы:
myVariable2ndValue_countclassmy-valueMAX_SIZE
Примитивные типы данных
byte, short, int, long, float, double, boolean, char
Материалы
| Тип | Ссылка |
|---|---|
| Документ | Primitive Data Types |
| Видео | Java Data Types |
Java — строго типизированный язык. Каждая переменная должна иметь объявленный тип. В этом разделе мы изучим 8 примитивных типов данных — фундамент Java.
Обзор примитивных типов
Java имеет ровно 8 примитивных типов:
| Тип | Размер | Диапазон значений | Значение по умолчанию |
|---|---|---|---|
byte | 1 байт | -128 … 127 | 0 |
short | 2 байта | -32 768 … 32 767 | 0 |
int | 4 байта | -2 147 483 648 … 2 147 483 647 | 0 |
long | 8 байт | -9 223 372 036 854 775 808 … 9 223 372 036 854 775 807 | 0L |
float | 4 байта | ~±3.4×10^38 (6-7 значащих цифр) | 0.0f |
double | 8 байт | ~±1.7×10^308 (15-17 значащих цифр) | 0.0 |
char | 2 байта | 0 … 65 535 (Unicode) | ‘\u0000’ |
boolean | ~1 бит | true / false | false |
Целочисленные типы
byte
Самый маленький целочисленный тип. Занимает 1 байт.
byte minByte = -128;
byte maxByte = 127;
byte age = 25;
// Полезен для экономии памяти в больших массивах
byte[] buffer = new byte[1024];
short
Редко используется. Занимает 2 байта.
short minShort = -32768;
short maxShort = 32767;
short year = 2024;
int (основной тип)
Самый часто используемый целочисленный тип. Занимает 4 байта.
int count = 0;
int population = 1_000_000; // подчёркивания для читаемости
int negative = -42;
// Диапазон
int min = Integer.MIN_VALUE; // -2147483648
int max = Integer.MAX_VALUE; // 2147483647
По умолчанию все целочисленные литералы имеют тип int:
int x = 100; // 100 — это int
long y = 100; // 100 — всё ещё int, но присваивается в long
// long z = 10000000000; // ОШИБКА! 10 миллиардов не помещается в int
long z = 10000000000L; // OK с суффиксом L
long
Для очень больших чисел. Занимает 8 байт. Требует суффикс L.
long worldPopulation = 8_000_000_000L;
long fileSize = 1099511627776L; // 1 TB в байтах
long timestamp = System.currentTimeMillis();
// Диапазон
long min = Long.MIN_VALUE; // -9223372036854775808
long max = Long.MAX_VALUE; // 9223372036854775807
Важно: Всегда используйте заглавную L, не строчную l (легко спутать с единицей).
long bad = 100l; // работает, но плохо читается
long good = 100L; // чётко видно, что это long
Числа с плавающей точкой
float
Одинарная точность. Занимает 4 байта. Требует суффикс f или F.
float pi = 3.14f;
float temperature = -40.0f;
float price = 19.99F;
// Точность: ~6-7 значащих цифр
float precise = 123456.789f;
System.out.println(precise); // 123456.79 (потеря точности!)
double (основной тип)
Двойная точность. Занимает 8 байт. Используется по умолчанию.
double pi = 3.141592653589793;
double avogadro = 6.022e23; // научная нотация
double planck = 6.626e-34;
// Точность: ~15-17 значащих цифр
double precise = 123456789.123456789;
System.out.println(precise); // 1.2345678912345679E8
По умолчанию все дробные литералы имеют тип double:
double d = 3.14; // OK
// float f = 3.14; // ОШИБКА! 3.14 — это double
float f = 3.14f; // OK с суффиксом f
Специальные значения
double positiveInfinity = Double.POSITIVE_INFINITY; // бесконечность
double negativeInfinity = Double.NEGATIVE_INFINITY;
double notANumber = Double.NaN; // Not a Number
// Примеры
double inf = 1.0 / 0.0; // Infinity
double negInf = -1.0 / 0.0; // -Infinity
double nan = 0.0 / 0.0; // NaN
// Проверка на NaN
Double.isNaN(nan); // true
Double.isInfinite(inf); // true
// Внимание: NaN != NaN
System.out.println(nan == nan); // false!
Проблемы точности
Числа с плавающей точкой не могут точно представить все десятичные дроби:
double a = 0.1;
double b = 0.2;
double c = a + b;
System.out.println(c); // 0.30000000000000004
System.out.println(c == 0.3); // false!
// Правильное сравнение
double epsilon = 1e-10;
System.out.println(Math.abs(c - 0.3) < epsilon); // true
Для финансовых расчётов используйте BigDecimal:
import java.math.BigDecimal;
BigDecimal price = new BigDecimal("19.99");
BigDecimal quantity = new BigDecimal("3");
BigDecimal total = price.multiply(quantity);
System.out.println(total); // 59.97 (точно!)
Символьный тип char
char хранит один Unicode-символ. Занимает 2 байта (16 бит).
char letter = 'A';
char digit = '7';
char cyrillic = 'Я';
char newline = '\n';
Символы как числа
char — это беззнаковое 16-битное целое (0-65535):
char ch = 'A';
System.out.println((int) ch); // 65
char fromCode = 65;
System.out.println(fromCode); // A
// Арифметика с символами
char next = (char) ('A' + 1);
System.out.println(next); // B
Escape-последовательности
| Последовательность | Значение |
|---|---|
\n | Новая строка |
\t | Табуляция |
\r | Возврат каретки |
\\ | Обратный слэш |
\' | Одинарная кавычка |
\" | Двойная кавычка |
\uXXXX | Unicode символ (4 hex цифры) |
char tab = '\t';
char quote = '\'';
char backslash = '\\';
char omega = '\u03A9'; // Ω
char cyrillic = '\u0410'; // А (русская)
Unicode и суррогатные пары
char может хранить только символы из Basic Multilingual Plane (BMP): U+0000 до U+FFFF.
Для символов за пределами BMP (например, emoji) нужны суррогатные пары:
// Emoji U+1F60A (смайлик) не помещается в один char!
// char smile = 0x1F60A; // ОШИБКА! Выход за пределы char
// Используйте String для emoji
String smile = "\uD83D\uDE0A"; // суррогатная пара
System.out.println(smile); // выведет смайлик
// Или используйте code point
int codePoint = 0x1F60A;
String emoji = new String(Character.toChars(codePoint));
System.out.println(emoji);
// Проверка
System.out.println(Character.charCount(0x1F60A)); // 2 (требует 2 char)
System.out.println(Character.charCount('A')); // 1
Логический тип boolean
boolean имеет только два значения: true и false.
boolean isActive = true;
boolean hasPermission = false;
boolean isAdult = age >= 18;
boolean isValid = name != null && !name.isEmpty();
Особенности boolean
В отличие от C/C++, в Java:
booleanнельзя преобразовать в число- Числа нельзя использовать как boolean
boolean flag = true;
// В C/C++ можно, в Java — ОШИБКА:
// if (1) { } // ОШИБКА!
// boolean b = 1; // ОШИБКА!
// int x = true; // ОШИБКА!
// Правильно:
if (flag) { }
if (count > 0) { }
Naming conventions для boolean
// Используйте is/has/can/should для boolean переменных и методов
boolean isReady;
boolean hasChildren;
boolean canEdit;
boolean shouldUpdate;
public boolean isEmpty() { return size == 0; }
public boolean hasNext() { return index < length; }
Литералы
Литерал — это фиксированное значение, записанное в коде.
Целочисленные литералы
// Десятичные (по умолчанию)
int decimal = 42;
// Шестнадцатеричные (0x или 0X)
int hex = 0x2A; // 42
int color = 0xFF00FF;
// Восьмеричные (начинаются с 0)
int octal = 052; // 42
// Двоичные (0b или 0B, Java 7+)
int binary = 0b101010; // 42
// Подчёркивания для читаемости (Java 7+)
int million = 1_000_000;
long creditCard = 1234_5678_9012_3456L;
int bytes = 0b1111_0000_1111_0000;
Литералы с плавающей точкой
double d1 = 3.14;
double d2 = 3.14d; // явный суффикс
double d3 = 314e-2; // научная нотация
double d4 = 0x1.0p3; // hex float: 1.0 × 2³ = 8.0
float f1 = 3.14f; // обязательный суффикс
float f2 = 3.14F;
Символьные литералы
char c1 = 'A';
char c2 = '\n';
char c3 = '\u0041'; // 'A' в Unicode
Логические литералы
boolean t = true;
boolean f = false;
Null литерал
String s = null; // только для ссылочных типов
// int x = null; // ОШИБКА! Примитивы не могут быть null
Значения по умолчанию
Для полей класса примитивы инициализируются автоматически:
public class Defaults {
byte b; // 0
short s; // 0
int i; // 0
long l; // 0L
float f; // 0.0f
double d; // 0.0
char c; // '\u0000' (null character)
boolean z; // false
}
Локальные переменные НЕ инициализируются автоматически:
public void method() {
int x;
// System.out.println(x); // ОШИБКА! x не инициализирована
int y = 0;
System.out.println(y); // OK
}
Преобразования типов
Расширяющее преобразование (автоматическое)
Меньший тип автоматически преобразуется в больший:
byte → short → int → long → float → double
↑
char
int i = 100;
long l = i; // int → long (автоматически)
double d = l; // long → double (автоматически)
char c = 'A';
int code = c; // char → int (автоматически)
Сужающее преобразование (явное)
Требует явное приведение типа. Может привести к потере данных!
double d = 3.99;
int i = (int) d; // 3 (дробная часть отбрасывается)
long big = 1000000000000L;
int small = (int) big; // переполнение!
int value = 130;
byte b = (byte) value; // -126 (переполнение!)
Приведение типов
// Синтаксис: (тип) выражение
int i = (int) 3.14; // 3
byte b = (byte) 300; // 44 (300 % 256)
char c = (char) 65; // 'A'
double d = (double) 5 / 2; // 2.5 (не 2!)
Приведение в выражениях
byte a = 10;
byte b = 20;
// byte c = a + b; // ОШИБКА! a + b даёт int
byte c = (byte) (a + b); // OK
// Операции с byte, short, char всегда дают int
short s1 = 10;
short s2 = 20;
// short s3 = s1 + s2; // ОШИБКА!
int s3 = s1 + s2; // OK
Wrapper-классы
Каждый примитив имеет соответствующий класс-обёртку:
| Примитив | Wrapper |
|---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
Autoboxing и Unboxing
Java автоматически преобразует между примитивами и wrapper-классами:
// Autoboxing: примитив → объект
Integer obj = 42; // int → Integer
List<Integer> list = new ArrayList<>();
list.add(10); // int → Integer
// Unboxing: объект → примитив
int value = obj; // Integer → int
int first = list.get(0); // Integer → int
Осторожно с null!
Integer obj = null;
int value = obj; // NullPointerException!
// Безопасная проверка
if (obj != null) {
int safe = obj;
}
Integer cache
Java кэширует Integer объекты для значений -128…127:
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true (один объект из кэша)
Integer c = 1000;
Integer d = 1000;
System.out.println(c == d); // false (разные объекты!)
// Всегда используйте equals() для сравнения объектов
System.out.println(c.equals(d)); // true
Полезные методы
Для числовых типов
// Парсинг строк
int i = Integer.parseInt("42");
double d = Double.parseDouble("3.14");
long l = Long.parseLong("1000000000");
// Конвертация в строку
String s1 = Integer.toString(42);
String s2 = String.valueOf(42); // универсальный способ
// Системы счисления
String binary = Integer.toBinaryString(42); // "101010"
String hex = Integer.toHexString(255); // "ff"
int fromHex = Integer.parseInt("ff", 16); // 255
// Min/Max
int max = Integer.max(10, 20); // 20
int min = Integer.min(10, 20); // 10
// Сравнение
int cmp = Integer.compare(10, 20); // -1 (10 < 20)
Для Character
char c = 'A';
Character.isLetter(c); // true
Character.isDigit('5'); // true
Character.isWhitespace(' '); // true
Character.isUpperCase(c); // true
Character.toLowerCase(c); // 'a'
Character.toUpperCase('b'); // 'B'
Итоги
Вы изучили 8 примитивных типов Java:
Целочисленные:
byte(1 байт) — редко используетсяshort(2 байта) — редко используетсяint(4 байта) — основной целочисленный типlong(8 байт) — для больших чисел, суффиксL
С плавающей точкой:
float(4 байта) — суффиксf, низкая точностьdouble(8 байт) — основной дробный тип
Другие:
char(2 байта) — Unicode символboolean(~1 бит) — true/false
Ключевые моменты:
- Используйте
intиdoubleпо умолчанию - Не забывайте суффиксы
Lдля long иfдля float - Избегайте
floatдля финансовых расчётов (используйтеBigDecimal) char— это число 0-65535, не может хранить emoji напрямуюbooleanне конвертируется в число и обратно- Осторожно с Integer cache при сравнении объектов через
==
Задания для практики
-
Объявите переменные всех 8 примитивных типов и выведите их значения
-
Напишите программу, которая демонстрирует переполнение
intпри умножении больших чисел -
Сравните результат
0.1 + 0.2с0.3и объясните, почему они не равны -
Напишите метод, который определяет, является ли символ цифрой, без использования
Character.isDigit() -
Продемонстрируйте разницу между
Integer.valueOf(100) == Integer.valueOf(100)иInteger.valueOf(1000) == Integer.valueOf(1000)
Операторы и выражения
Арифметические, логические, битовые операторы
Ресурсы
Основные понятия
Выражение (expression) - это конструкция, которая вычисляется и возвращает результат. Результатом может быть:
- Переменная (lvalue)
- Значение (value)
- Ничего (void - для методов без возвращаемого значения)
При вычислении выражение может завершиться:
- Нормально (normal completion) - если все шаги выполнены без исключений
- Резко (abrupt completion) - если возникло исключение
Типы выражений
Выражения классифицируются по синтаксическим формам:
- Имена выражений
- Первичные выражения (литералы, this, создание объектов)
- Унарные операторы
- Бинарные операторы
- Тернарный оператор
? : - Лямбда-выражения
- Switch-выражения
Порядок вычисления
Java гарантирует строгий порядок вычисления выражений:
Основные правила
- Левый операнд вычисляется первым - в бинарных операторах левый операнд всегда вычисляется до правого
- Операнды вычисляются до операции - все операнды вычисляются полностью перед выполнением операции
- Соблюдение скобок и приоритета - порядок определяется скобками и приоритетом операторов
- Аргументы слева направо - аргументы методов вычисляются слева направо
int i = 2;
int j = (i=3) * i; // j = 9, не 6
Литералы
Литерал - фиксированное неизменяемое значение.
Типы литералов:
- Целочисленные:
42,0xFF,0b1010,100L - Вещественные:
3.14,2.5f,1.0e-10 - Логические:
true,false - Символьные:
'a','\n','\u0041' - Строковые:
"Hello", текстовые блоки"""...""" - Null:
null
Первичные выражения
this и super
this- ссылка на текущий объектsuper- ссылка на родительский класс- Квалифицированный this:
ClassName.this
Литералы классов
Class<String> c1 = String.class;
Class<Integer> c2 = int.class;
Class<Void> c3 = void.class;
Создание объектов
new ClassName()
new ClassName(args)
new ClassName() { /* анонимный класс */ }
Унарные операторы
Инкремент и декремент
++x- префиксный инкремент (сначала увеличение, потом использование)x++- постфиксный инкремент (сначала использование, потом увеличение)--x- префиксный декрементx--- постфиксный декремент
Знаковые операторы
+x- унарный плюс-x- унарный минус (смена знака)
Логические и битовые
!x- логическое отрицание (НЕ)~x- побитовое отрицание (инверсия битов)
Арифметические операторы
Мультипликативные
*- умножение/- деление (целочисленное для int, обычное для float/double)%- остаток от деления
int a = 7 / 2; // 3
double b = 7.0 / 2; // 3.5
int c = 7 % 2; // 1
Важно: Деление на ноль для целых чисел выбрасывает ArithmeticException, для вещественных - возвращает Infinity или NaN.
Аддитивные
+- сложение или конкатенация строк-- вычитание
int sum = 5 + 3; // 8
String s = "Hello" + "World"; // "HelloWorld"
String s2 = "Value: " + 42; // "Value: 42"
Операторы сдвига
Работают только с целочисленными типами:
<<- сдвиг влево (умножение на 2^n)>>- арифметический сдвиг вправо (деление на 2^n, сохраняет знак)>>>- логический сдвиг вправо (заполняет нулями слева)
int x = 8;
x << 2; // 32 (8 * 4)
x >> 2; // 2 (8 / 4)
int y = -8;
y >> 2; // -2 (знак сохраняется)
y >>> 2; // 1073741822 (беззнаковый сдвиг)
Операторы сравнения
Числовые операторы сравнения
<- меньше<=- меньше или равно>- больше>=- больше или равно
Результат: boolean
instanceof
Проверяет принадлежность объекта к типу:
if (obj instanceof String) {
String s = (String) obj;
}
// С pattern matching (Java 16+)
if (obj instanceof String s) {
// s доступна здесь
}
Операторы равенства
Для примитивов
==- равенство значений!=- неравенство значений
Для ссылок
==- проверка идентичности (ссылаются ли на один объект)!=- проверка неидентичности
String s1 = new String("Hello");
String s2 = new String("Hello");
s1 == s2; // false (разные объекты)
s1.equals(s2); // true (одинаковое содержимое)
Битовые и логические операторы
Целочисленные битовые
&- побитовое И (AND)|- побитовое ИЛИ (OR)^- побитовое исключающее ИЛИ (XOR)
int a = 0b1100;
int b = 0b1010;
a & b; // 0b1000 (8)
a | b; // 0b1110 (14)
a ^ b; // 0b0110 (6)
Логические для boolean
&- логическое И (вычисляет оба операнда)|- логическое ИЛИ (вычисляет оба операнда)^- логическое XOR
Условные логические (короткое замыкание)
&&- условное И (если левый false, правый не вычисляется)||- условное ИЛИ (если левый true, правый не вычисляется)
if (obj != null && obj.isValid()) { // безопасно
// obj.isValid() не вызовется если obj == null
}
Тернарный оператор
Синтаксис: условие ? значение_если_true : значение_если_false
int max = (a > b) ? a : b;
String status = (age >= 18) ? "Взрослый" : "Ребёнок";
Типы результата:
- Если оба операнда числовые - выбирается общий числовой тип
- Если оба boolean - результат boolean
- Если ссылочные типы - выбирается общий родительский тип
Операторы присваивания
Простое присваивание
=- присваивание значения
int x = 5;
String s = "Hello";
Составные операторы присваивания
Комбинируют операцию с присваиванием:
+=- сложение с присваиванием-=- вычитание с присваиванием*=- умножение с присваиванием/=- деление с присваиванием%=- остаток с присваиванием&=- побитовое И с присваиванием|=- побитовое ИЛИ с присваиванием^=- побитовое XOR с присваиванием<<=- сдвиг влево с присваиванием>>=- сдвиг вправо с присваиванием>>>=- беззнаковый сдвиг с присваиванием
int x = 10;
x += 5; // эквивалентно x = x + 5; (x = 15)
x *= 2; // эквивалентно x = x * 2; (x = 30)
Важно: Составные операторы автоматически приводят результат к типу левого операнда:
byte b = 5;
b += 10; // ОК, эквивалентно b = (byte)(b + 10)
b = b + 10; // ОШИБКА компиляции! (требуется явное приведение)
Приоритет операторов
От высшего к низшему (сверху вниз):
- Постфиксные:
expr++,expr-- - Унарные:
++expr,--expr,+,-,~,! - Приведение типов:
(type) - Мультипликативные:
*,/,% - Аддитивные:
+,- - Сдвиг:
<<,>>,>>> - Сравнение:
<,>,<=,>=,instanceof - Равенство:
==,!= - Побитовое И:
& - Побитовое XOR:
^ - Побитовое ИЛИ:
| - Логическое И:
&& - Логическое ИЛИ:
|| - Тернарный:
? : - Присваивание:
=,+=,-=,*=,/=,%=,&=,^=,|=,<<=,>>=,>>>= - Лямбда:
->
int result = 2 + 3 * 4; // 14, не 20 (* выше +)
int result2 = (2 + 3) * 4; // 20 (скобки меняют порядок)
boolean b = x > 0 && y < 10; // сначала сравнения, потом &&
Особенности вещественных чисел
Java полностью поддерживает IEEE 754:
- Специальные значения:
Infinity,-Infinity,NaN - Операции с NaN всегда возвращают NaN
- Сравнения с NaN всегда false (кроме
!=, которое true)
double inf = 1.0 / 0.0; // Infinity
double nan = 0.0 / 0.0; // NaN
nan == nan; // false
Double.isNaN(nan); // true
Политики округления:
- Round to nearest (округление к ближайшему) - для большинства операций
- Round toward zero (округление к нулю) - при приведении к целому типу
Приведение типов (Cast)
Синтаксис: (type) expression
double d = 3.14;
int i = (int) d; // 3 (отбрасывается дробная часть)
Object obj = "Hello";
String s = (String) obj; // ОК
// Небезопасное приведение вызывает ClassCastException
Integer num = (Integer) obj; // Runtime error!
Создание массивов
С указанием размера
int[] arr1 = new int[10];
int[][] arr2 = new int[5][10];
int[][] arr3 = new int[5][]; // "рваный" массив
С инициализацией
int[] arr1 = {1, 2, 3, 4, 5};
int[][] arr2 = {{1, 2}, {3, 4}, {5, 6}};
String[] names = new String[] {"Alice", "Bob"};
Доступ к массиву
Синтаксис: array[index]
int[] arr = {10, 20, 30};
int x = arr[0]; // 10
arr[1] = 25; // изменение элемента
// Проверки времени выполнения
arr[5]; // ArrayIndexOutOfBoundsException
int[] nullArr = null;
nullArr[0]; // NullPointerException
Вызов методов
object.method()
object.method(arg1, arg2)
ClassName.staticMethod()
Перегрузка разрешается во время компиляции на основе типов аргументов.
Ссылки на методы (Method References)
Синтаксис для функционального программирования (Java 8+):
// Ссылка на статический метод
Function<String, Integer> parser = Integer::parseInt;
// Ссылка на метод экземпляра
List<String> list = Arrays.asList("a", "b", "c");
list.forEach(System.out::println);
// Ссылка на конструктор
Supplier<List<String>> listSupplier = ArrayList::new;
Лямбда-выражения
Синтаксис: (parameters) -> expression или (parameters) -> { statements }
// Без параметров
Runnable r = () -> System.out.println("Hello");
// С одним параметром (скобки необязательны)
Consumer<String> printer = s -> System.out.println(s);
Consumer<String> printer2 = (String s) -> System.out.println(s);
// С несколькими параметрами
Comparator<Integer> comp = (a, b) -> a.compareTo(b);
// С телом блока
BiFunction<Integer, Integer, Integer> sum = (a, b) -> {
int result = a + b;
return result;
};
Switch-выражения (Java 14+)
// Традиционный switch-statement
switch (day) {
case MONDAY:
case FRIDAY:
System.out.println("Work");
break;
case SATURDAY:
case SUNDAY:
System.out.println("Rest");
break;
}
// Switch-выражение (возвращает значение)
String activity = switch (day) {
case MONDAY, FRIDAY -> "Work";
case SATURDAY, SUNDAY -> "Rest";
default -> "Other";
};
// С блоком и yield
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> {
System.out.println("Checking...");
yield 9;
}
};
Константные выражения
Константное выражение - это выражение, значение которого может быть вычислено на этапе компиляции.
Константными могут быть:
- Литералы примитивных типов и String
- Приведение типов к примитивам или String
- Унарные операторы:
+,-,~,! - Мультипликативные:
*,/,% - Аддитивные:
+,- - Сдвиг:
<<,>>,>>> - Сравнение:
<,<=,>,>= - Равенство:
==,!= - Побитовые и логические:
&,^,| - Условные:
&&,|| - Тернарный:
? : - Скобки:
( ) - Простые имена final переменных, инициализированных константными выражениями
final int MAX = 100;
final int MIN = 0;
final int RANGE = MAX - MIN; // константное выражение
// Используется в switch
switch (value) {
case MIN: // OK
case MAX: // OK
case RANGE: // OK
}
Исключения времени выполнения
Операторы могут вызывать исключения:
NullPointerException- операция с nullArithmeticException- деление на ноль (целые числа)ArrayIndexOutOfBoundsException- выход за границы массиваNegativeArraySizeException- отрицательный размер массиваClassCastException- недопустимое приведение типовArrayStoreException- несовместимый тип при записи в массивOutOfMemoryError- нехватка памяти
Автоупаковка и автораспаковка
Автоматическое преобразование между примитивами и обёртками:
// Автоупаковка (boxing)
Integer obj = 42; // эквивалентно Integer.valueOf(42)
// Автораспаковка (unboxing)
int x = obj; // эквивалентно obj.intValue()
// В выражениях
Integer a = 10;
Integer b = 20;
Integer sum = a + b; // автораспаковка, затем автоупаковка
// Осторожно с null!
Integer nullObj = null;
int y = nullObj; // NullPointerException!
Важные особенности и best practices
- Используйте скобки для ясности - не полагайтесь только на приоритет операторов
- Избегайте сложных выражений - разбивайте на несколько строк для читаемости
- Осторожно с автоупаковкой - может вызвать NullPointerException
- Используйте equals() для объектов - не == (если не проверяете идентичность)
- Проверяйте null перед && - используйте короткое замыкание
- Избегайте деления целых на ноль - проверяйте делитель
- Осторожно с NaN - проверяйте через Double.isNaN()
- Используйте составные операторы - они короче и автоматически приводят тип
// Хорошо
if (obj != null && obj.isValid()) { }
// Плохо
if (obj.isValid() && obj != null) { } // может быть NPE
// Хорошо
String.valueOf(obj).equals("value")
// Плохо
obj.toString().equals("value") // может быть NPE
1.5. Управляющие конструкции
if/else, switch, for, while, do-while
Материалы
Способность выполнять код в зависимости от условий и повторять код пока условие истинно - это базовые строительные блоки большинства языков программирования. Давайте разберёмся, как управлять потоком выполнения в Java.
Условный оператор if
Ваш первый if
Оператор if позволяет запускать код только если условие истинно. Давайте создадим простой пример:
public class Main {
public static void main(String[] args) {
int number = 3;
if (number < 5) {
System.out.println("число меньше 5");
}
}
}
Если вы запустите этот код, вы увидите:
число меньше 5
Условие number < 5 проверяется, и если оно истинно, код внутри фигурных скобок выполняется. Если бы мы изменили number на 7, ничего не было бы выведено.
Обработка альтернативы с else
Часто нужно выполнить один блок кода если условие истинно, и другой блок если ложно. Для этого используется else:
int age = 16;
if (age >= 18) {
System.out.println("Вы можете голосовать");
} else {
System.out.println("Вы пока не можете голосовать");
}
Вывод:
Вы пока не можете голосовать
Поскольку age равен 16, условие age >= 18 ложно, поэтому выполняется блок else.
Множественные условия с else if
Что если нужно проверить несколько условий? Используйте else if:
int score = 75;
if (score >= 90) {
System.out.println("Оценка: A");
} else if (score >= 80) {
System.out.println("Оценка: B");
} else if (score >= 70) {
System.out.println("Оценка: C");
} else if (score >= 60) {
System.out.println("Оценка: D");
} else {
System.out.println("Оценка: F");
}
Вывод:
Оценка: C
Java проверяет каждое условие по порядку и выполняет только первый подходящий блок. Даже если несколько условий истинны, выполнится только первое совпавшее.
Примечание: Использование слишком много
else ifможет усложнить код. В таких случаях рассмотрите использованиеswitch.
Оператор switch
Когда нужно сравнить одно значение с множеством вариантов, switch может быть более читаемым чем цепочка if-else.
Классический switch
Вот пример с днями недели:
int day = 3;
switch (day) {
case 1:
System.out.println("Понедельник");
break;
case 2:
System.out.println("Вторник");
break;
case 3:
System.out.println("Среда");
break;
default:
System.out.println("Другой день");
}
Вывод:
Среда
Ключевое слово break важно! Без него выполнение “провалится” в следующий case.
Проваливание (Fall-through)
Иногда проваливание полезно для группировки случаев:
String month = "Январь";
switch (month) {
case "Декабрь":
case "Январь":
case "Февраль":
System.out.println("Зима");
break;
case "Март":
case "Апрель":
case "Май":
System.out.println("Весна");
break;
default:
System.out.println("Другой сезон");
}
Все три зимних месяца приведут к выводу “Зима”.
Современный switch (Java 14+)
Java 14 добавила более удобную форму switch - switch-выражения. Они возвращают значение и не требуют break:
int dayNum = 3;
String dayName = switch (dayNum) {
case 1 -> "Понедельник";
case 2 -> "Вторник";
case 3 -> "Среда";
case 4 -> "Четверг";
case 5 -> "Пятница";
case 6 -> "Суббота";
case 7 -> "Воскресенье";
default -> "Неверный день";
};
System.out.println(dayName); // Выведет: Среда
Стрелка -> означает “вернуть это значение”. Никаких break не нужно!
Можно группировать случаи через запятую:
String dayType = switch (dayNum) {
case 1, 2, 3, 4, 5 -> "Рабочий день";
case 6, 7 -> "Выходной";
default -> "Неверный день";
};
Если нужно выполнить несколько строк кода, используйте блок с yield:
int numLetters = switch (dayName) {
case "Понедельник", "Воскресенье" -> 11;
case "Среда" -> {
System.out.println("Середина недели!");
yield 5; // yield возвращает значение
}
default -> 0;
};
Цикл while
Цикл while повторяет код пока условие истинно. Условие проверяется перед каждой итерацией.
int count = 0;
while (count < 5) {
System.out.println("count: " + count);
count++;
}
Вывод:
count: 0
count: 1
count: 2
count: 3
count: 4
Если условие сразу ложно, код внутри цикла не выполнится ни разу:
int x = 10;
while (x < 5) {
System.out.println("Не выполнится");
}
Осторожно: Убедитесь что условие когда-нибудь станет ложным, иначе получится бесконечный цикл!
Цикл do-while
Цикл do-while похож на while, но проверяет условие после выполнения тела. Это означает, что код выполнится минимум один раз.
int number = 0;
do {
System.out.println("number: " + number);
number++;
} while (number < 3);
Вывод:
number: 0
number: 1
number: 2
Даже если условие изначально ложно, код выполнится один раз:
int x = 10;
do {
System.out.println("Выполнится хотя бы раз");
} while (x < 5); // условие ложно, но код уже выполнился
Вывод:
Выполнится хотя бы раз
Цикл for
Цикл for отлично подходит когда знаете сколько раз нужно повторить код.
Базовый for
for (int i = 0; i < 5; i++) {
System.out.println("i: " + i);
}
Вывод:
i: 0
i: 1
i: 2
i: 3
i: 4
Цикл for состоит из трёх частей:
- Инициализация (
int i = 0) - выполняется один раз в начале - Условие (
i < 5) - проверяется перед каждой итерацией - Обновление (
i++) - выполняется после каждой итерации
Все три части опциональны:
// Бесконечный цикл
for (;;) {
// будет работать вечно
}
Можно использовать несколько переменных:
for (int i = 0, j = 10; i < j; i++, j--) {
System.out.println("i=" + i + ", j=" + j);
}
Вывод:
i=0, j=10
i=1, j=9
i=2, j=8
i=3, j=7
i=4, j=6
Enhanced for (for-each)
Для обхода массивов и коллекций есть упрощённая форма:
int[] numbers = {10, 20, 30, 40, 50};
for (int num : numbers) {
System.out.println(num);
}
Вывод:
10
20
30
40
50
Читается как “для каждого num в numbers”. Это намного проще чем обычный for с индексами!
Работает с любыми коллекциями:
List<String> names = Arrays.asList("Анна", "Боб", "Карл");
for (String name : names) {
System.out.println("Привет, " + name);
}
Ограничение: Enhanced for не позволяет изменять элементы массива или получать индекс. Для этого используйте обычный
for.
Прерывание потока: break и continue
break - выход из цикла
break немедленно завершает цикл:
for (int i = 0; i < 10; i++) {
if (i == 5) {
break; // выход из цикла
}
System.out.println(i);
}
Вывод:
0
1
2
3
4
Полезно для поиска элемента:
int[] numbers = {5, 10, 15, 20, 25};
int target = 15;
boolean found = false;
for (int num : numbers) {
if (num == target) {
found = true;
break; // нашли, выходим
}
}
System.out.println("Найдено: " + found);
continue - пропуск итерации
continue пропускает оставшийся код в текущей итерации и переходит к следующей:
for (int i = 0; i < 5; i++) {
if (i == 2) {
continue; // пропускаем i=2
}
System.out.println(i);
}
Вывод:
0
1
3
4
Полезно для фильтрации:
int[] numbers = {-5, 10, -3, 15, -7, 20};
int sum = 0;
for (int num : numbers) {
if (num < 0) {
continue; // пропускаем отрицательные
}
sum += num;
}
System.out.println("Сумма положительных: " + sum); // 45
Метки для вложенных циклов
Когда нужно выйти из вложенного цикла, используйте метки:
outer: for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i == 1 && j == 1) {
break outer; // выход из ОБОИХ циклов
}
System.out.println("i=" + i + ", j=" + j);
}
}
Вывод:
i=0, j=0
i=0, j=1
i=0, j=2
i=1, j=0
Это работает и с continue:
outer: for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (j == 1) {
continue outer; // к следующей итерации внешнего цикла
}
System.out.println("i=" + i + ", j=" + j);
}
}
Оператор return
return завершает метод и возвращает управление:
public static int findMax(int a, int b) {
if (a > b) {
return a;
}
return b;
}
Можно использовать несколько return:
public static String getGrade(int score) {
if (score >= 90) return "A";
if (score >= 80) return "B";
if (score >= 70) return "C";
if (score >= 60) return "D";
return "F";
}
Для void методов return просто выходит:
public static void printPositive(int num) {
if (num <= 0) {
return; // выход из метода
}
System.out.println("Положительное: " + num);
}
Обработка исключений
try-catch
Обрабатывайте ошибки чтобы программа не упала:
try {
int result = 10 / 0; // ошибка деления на ноль
} catch (ArithmeticException e) {
System.out.println("Ошибка: нельзя делить на ноль!");
}
Можно ловить несколько типов исключений:
try {
String text = null;
System.out.println(text.length());
} catch (NullPointerException e) {
System.out.println("Переменная null");
} catch (Exception e) {
System.out.println("Другая ошибка");
}
finally - код который выполнится всегда
Блок finally выполняется независимо от того, было ли исключение:
FileReader file = null;
try {
file = new FileReader("data.txt");
// читаем файл
} catch (IOException e) {
System.out.println("Ошибка чтения");
} finally {
if (file != null) {
try {
file.close(); // закрываем файл в любом случае
} catch (IOException e) {
e.printStackTrace();
}
}
}
try-with-resources
Java 7+ позволяет автоматически закрывать ресурсы:
try (FileReader file = new FileReader("data.txt")) {
// читаем файл
} catch (IOException e) {
System.out.println("Ошибка чтения");
}
// файл закроется автоматически!
Это работает с любыми AutoCloseable ресурсами.
Оператор assert
Проверяйте условия во время разработки:
int age = 15;
assert age >= 18 : "Возраст должен быть >= 18";
Важно: Assertions по умолчанию отключены! Включите их флагом
-eaпри запуске:java -ea Main
С сообщением:
double price = -10.0;
assert price > 0 : "Цена должна быть положительной, а не " + price;
Примечание: Не используйте assertions для проверки аргументов в публичных методах. Используйте исключения вместо этого.
Pattern Matching (Java 16+)
instanceof с паттернами
Старый способ:
Object obj = "Hello";
if (obj instanceof String) {
String s = (String) obj; // нужно приведение типа
System.out.println(s.toUpperCase());
}
Новый способ (Java 16+):
Object obj = "Hello";
if (obj instanceof String s) { // s уже String!
System.out.println(s.toUpperCase());
}
Switch с паттернами (Java 17+)
Object obj = 42;
String result = switch (obj) {
case Integer i -> "Число: " + i;
case String s -> "Строка: " + s;
case null -> "Это null";
default -> "Неизвестный тип";
};
С условиями (guards):
Object obj = 42;
String description = switch (obj) {
case Integer i when i > 0 -> "Положительное число";
case Integer i when i < 0 -> "Отрицательное число";
case Integer i -> "Ноль";
default -> "Не число";
};
Record Patterns (Java 21+)
record Point(int x, int y) {}
Point p = new Point(3, 4);
String location = switch (p) {
case Point(0, 0) -> "Начало координат";
case Point(int x, 0) -> "На оси X: " + x;
case Point(0, int y) -> "На оси Y: " + y;
case Point(int x, int y) -> "Точка (" + x + ", " + y + ")";
};
Полезные советы
Используйте фигурные скобки
Даже для однострочных блоков:
// Плохо
if (condition)
doSomething();
// Хорошо
if (condition) {
doSomething();
}
Это предотвращает ошибки при добавлении кода.
Извлекайте сложные условия
// Плохо
if (user.getAge() >= 18 && user.hasLicense() && !user.isBanned()) {
allowDriving();
}
// Хорошо
boolean canDrive = user.getAge() >= 18
&& user.hasLicense()
&& !user.isBanned();
if (canDrive) {
allowDriving();
}
Избегайте магических чисел
// Плохо
if (status == 1) {
// что означает 1?
}
// Хорошо
final int STATUS_ACTIVE = 1;
if (status == STATUS_ACTIVE) {
// ясно!
}
// Ещё лучше - enum
enum Status { ACTIVE, INACTIVE, PENDING }
if (status == Status.ACTIVE) {
// идеально!
}
Предпочитайте ранний выход
// Плохо
public void process(String data) {
if (data != null) {
if (data.length() > 0) {
// много кода
}
}
}
// Хорошо
public void process(String data) {
if (data == null || data.length() == 0) {
return; // ранний выход
}
// много кода
}
Итоги
Вы изучили основные управляющие конструкции Java:
if,else if,elseдля условного выполненияswitchдля множественного выбора (классический и современный)whileиdo-whileдля циклов с условиемforи enhanced for для итерацииbreak,continue,returnдля управления потокомtry-catch-finallyдля обработки ошибок- Pattern matching для современного Java
Теперь вы можете эффективно управлять потоком выполнения ваших программ!
Практика
Попробуйте написать программы для:
- Проверки является ли год високосным
- Вычисления факториала числа
- Поиска всех простых чисел до N
- Конвертации температуры между Цельсием и Фаренгейтом
Удачи в программировании!
1.6. Массивы
Материалы
Массив - это контейнер, который хранит фиксированное количество значений одного типа. Когда нужно работать с множеством связанных значений, массивы - ваш лучший друг!
Ваш первый массив
Представьте что вам нужно хранить температуры за неделю. Вместо создания семи отдельных переменных, используйте массив:
public class Main {
public static void main(String[] args) {
int[] temperatures = {18, 20, 22, 19, 21, 23, 20};
System.out.println("Понедельник: " + temperatures[0] + "°C");
System.out.println("Вторник: " + temperatures[1] + "°C");
}
}
Вывод:
Понедельник: 18°C
Вторник: 20°C
Индексация начинается с нуля! Первый элемент имеет индекс 0, второй - 1, и так далее.
Объявление массивов
Есть два способа объявить массив:
// Способ 1 (рекомендуемый)
int[] numbers;
String[] names;
// Способ 2 (не рекомендуется, но работает)
int numbers[];
String names[];
Первый способ более читаемый - квадратные скобки сразу показывают что это массив.
Примечание: Объявление массива не создаёт его, только говорит компилятору что переменная будет ссылаться на массив.
Создание массивов
Создание с помощью new
int[] numbers = new int[5]; // массив из 5 целых чисел
Это создаёт массив из 5 элементов, все инициализированы значением по умолчанию (0 для чисел):
[0, 0, 0, 0, 0]
Значения по умолчанию для разных типов:
- Числа (
int,double, и т.д.):0или0.0 boolean:false- Ссылочные типы:
null
Создание с инициализацией
Часто вы знаете значения заранее:
int[] scores = {95, 87, 92, 78, 88};
String[] colors = {"красный", "зелёный", "синий"};
Java автоматически определяет размер массива по количеству элементов.
Работа с элементами
Доступ к элементам
Используйте квадратные скобки и индекс:
int[] numbers = {10, 20, 30, 40, 50};
System.out.println(numbers[0]); // 10
System.out.println(numbers[2]); // 30
System.out.println(numbers[4]); // 50
Изменение элементов
int[] numbers = {10, 20, 30};
numbers[1] = 99; // изменяем второй элемент
System.out.println(numbers[1]); // 99
Длина массива
Каждый массив знает свою длину:
int[] numbers = {10, 20, 30, 40, 50};
System.out.println("Длина массива: " + numbers.length); // 5
Важно:
length- это свойство, а не метод! Без скобок.
Перебор массивов
С помощью обычного for
int[] numbers = {10, 20, 30, 40, 50};
for (int i = 0; i < numbers.length; i++) {
System.out.println("Элемент " + i + ": " + numbers[i]);
}
Вывод:
Элемент 0: 10
Элемент 1: 20
Элемент 2: 30
Элемент 3: 40
Элемент 4: 50
С помощью enhanced for (for-each)
Когда индекс не нужен, используйте более простую форму:
int[] numbers = {10, 20, 30, 40, 50};
for (int num : numbers) {
System.out.println(num);
}
Это читается как “для каждого num в numbers”. Намного проще!
Вычисление суммы
int[] scores = {95, 87, 92, 78, 88};
int sum = 0;
for (int score : scores) {
sum += score;
}
double average = sum / (double) scores.length;
System.out.println("Средний балл: " + average);
Вывод:
Средний балл: 88.0
Многомерные массивы
Массив может содержать другие массивы! Это полезно для матриц и таблиц.
Двумерный массив (матрица)
int[][] matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
System.out.println(matrix[0][0]); // 1
System.out.println(matrix[1][2]); // 6
System.out.println(matrix[2][1]); // 8
Визуализация:
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
Создание многомерного массива
// Прямоугольный массив 3x4
int[][] table = new int[3][4];
// "Рваный" массив - строки разной длины
int[][] ragged = {
{1, 2},
{3, 4, 5},
{6}
};
Перебор двумерного массива
int[][] matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
System.out.print(matrix[i][j] + " ");
}
System.out.println(); // новая строка
}
Вывод:
1 2 3
4 5 6
7 8 9
С enhanced for:
for (int[] row : matrix) {
for (int num : row) {
System.out.print(num + " ");
}
System.out.println();
}
Копирование массивов
Почему простое присваивание не работает
int[] original = {1, 2, 3};
int[] copy = original; // ЭТО НЕ КОПИЯ!
copy[0] = 999;
System.out.println(original[0]); // 999 - упс!
Переменные массивов хранят ссылки, а не сами данные. Обе переменные указывают на один массив!
Копирование с помощью цикла
int[] original = {1, 2, 3, 4, 5};
int[] copy = new int[original.length];
for (int i = 0; i < original.length; i++) {
copy[i] = original[i];
}
Работает, но многословно.
Использование System.arraycopy()
int[] original = {1, 2, 3, 4, 5};
int[] copy = new int[original.length];
System.arraycopy(original, 0, copy, 0, original.length);
// System.arraycopy(источник, откуда, назначение, куда, сколько)
Можно копировать часть массива:
String[] coffees = {
"Affogato", "Americano", "Cappuccino", "Corretto", "Cortado",
"Doppio", "Espresso", "Frappucino", "Freddo"
};
String[] selection = new String[4];
System.arraycopy(coffees, 2, selection, 0, 4);
for (String coffee : selection) {
System.out.print(coffee + " ");
}
Вывод:
Cappuccino Corretto Cortado Doppio
Использование Arrays.copyOf()
Самый простой способ:
import java.util.Arrays;
int[] original = {1, 2, 3, 4, 5};
int[] copy = Arrays.copyOf(original, original.length);
Можно изменить размер:
int[] bigger = Arrays.copyOf(original, 10); // новые элементы = 0
int[] smaller = Arrays.copyOf(original, 3); // только первые 3
Использование Arrays.copyOfRange()
Копирование части массива:
import java.util.Arrays;
String[] coffees = {
"Affogato", "Americano", "Cappuccino", "Corretto", "Cortado",
"Doppio", "Espresso", "Frappucino", "Freddo"
};
String[] selection = Arrays.copyOfRange(coffees, 2, 6);
// От индекса 2 (включительно) до 6 (не включая)
for (String coffee : selection) {
System.out.print(coffee + " ");
}
Вывод:
Cappuccino Corretto Cortado Doppio
Полезные методы класса Arrays
Класс java.util.Arrays содержит множество полезных методов для работы с массивами.
Сортировка
import java.util.Arrays;
int[] numbers = {5, 2, 8, 1, 9};
Arrays.sort(numbers);
System.out.println(Arrays.toString(numbers));
Вывод:
[1, 2, 5, 8, 9]
Работает и со строками:
String[] names = {"Зина", "Алиса", "Боб"};
Arrays.sort(names);
System.out.println(Arrays.toString(names));
Вывод:
[Алиса, Боб, Зина]
Бинарный поиск
Быстрый поиск в отсортированном массиве:
import java.util.Arrays;
int[] numbers = {1, 3, 5, 7, 9, 11};
int index = Arrays.binarySearch(numbers, 7);
System.out.println("Число 7 находится на индексе: " + index); // 3
Важно: Массив должен быть отсортирован! Иначе результат непредсказуем.
Если элемент не найден, возвращается отрицательное число:
int notFound = Arrays.binarySearch(numbers, 4);
System.out.println(notFound); // -3 (отрицательное)
Заполнение массива
import java.util.Arrays;
int[] numbers = new int[5];
Arrays.fill(numbers, 42);
System.out.println(Arrays.toString(numbers));
Вывод:
[42, 42, 42, 42, 42]
Можно заполнить часть массива:
int[] numbers = new int[10];
Arrays.fill(numbers, 2, 7, 99); // от индекса 2 до 7
System.out.println(Arrays.toString(numbers));
Вывод:
[0, 0, 99, 99, 99, 99, 99, 0, 0, 0]
Сравнение массивов
import java.util.Arrays;
int[] array1 = {1, 2, 3};
int[] array2 = {1, 2, 3};
int[] array3 = {1, 2, 4};
System.out.println(Arrays.equals(array1, array2)); // true
System.out.println(Arrays.equals(array1, array3)); // false
Внимание: Оператор
==сравнивает ссылки, не содержимое!
System.out.println(array1 == array2); // false (разные объекты)
System.out.println(Arrays.equals(array1, array2)); // true (одинаковое содержимое)
Преобразование в строку
import java.util.Arrays;
int[] numbers = {1, 2, 3, 4, 5};
System.out.println(Arrays.toString(numbers));
Вывод:
[1, 2, 3, 4, 5]
Для многомерных массивов используйте deepToString():
int[][] matrix = {{1, 2}, {3, 4}};
System.out.println(Arrays.deepToString(matrix));
Вывод:
[[1, 2], [3, 4]]
Создание списка из массива
import java.util.Arrays;
import java.util.List;
String[] array = {"Яблоко", "Банан", "Апельсин"};
List<String> list = Arrays.asList(array);
System.out.println(list);
Вывод:
[Яблоко, Банан, Апельсин]
Примечание: Полученный список имеет фиксированный размер - нельзя добавлять или удалять элементы!
Частые ошибки и их решение
ArrayIndexOutOfBoundsException
Самая частая ошибка при работе с массивами:
int[] numbers = {10, 20, 30};
System.out.println(numbers[5]); // Ошибка! Индекс вне границ
Всегда проверяйте границы:
int index = 5;
if (index >= 0 && index < numbers.length) {
System.out.println(numbers[index]);
} else {
System.out.println("Индекс вне границ!");
}
NullPointerException
int[] numbers = null;
System.out.println(numbers.length); // Ошибка! numbers это null
Проверяйте на null:
if (numbers != null) {
System.out.println(numbers.length);
} else {
System.out.println("Массив не инициализирован!");
}
Изменение размера массива
Размер массива фиксирован после создания! Нельзя:
int[] numbers = {1, 2, 3};
numbers.length = 5; // ОШИБКА КОМПИЛЯЦИИ!
Решение - создать новый массив:
int[] numbers = {1, 2, 3};
int[] bigger = Arrays.copyOf(numbers, 5);
Или используйте ArrayList для динамического размера:
import java.util.ArrayList;
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4); // можно добавлять сколько угодно!
Практические примеры
Поиск максимального элемента
int[] numbers = {23, 45, 12, 67, 34, 89, 15};
int max = numbers[0];
for (int num : numbers) {
if (num > max) {
max = num;
}
}
System.out.println("Максимум: " + max); // 89
Реверс массива
int[] numbers = {1, 2, 3, 4, 5};
for (int i = 0; i < numbers.length / 2; i++) {
int temp = numbers[i];
numbers[i] = numbers[numbers.length - 1 - i];
numbers[numbers.length - 1 - i] = temp;
}
System.out.println(Arrays.toString(numbers));
Вывод:
[5, 4, 3, 2, 1]
Удаление дубликатов
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
int[] numbers = {1, 2, 3, 2, 4, 3, 5};
Set<Integer> set = new HashSet<>();
for (int num : numbers) {
set.add(num);
}
int[] unique = new int[set.size()];
int i = 0;
for (int num : set) {
unique[i++] = num;
}
Arrays.sort(unique);
System.out.println(Arrays.toString(unique));
Вывод:
[1, 2, 3, 4, 5]
Сдвиг элементов
int[] numbers = {1, 2, 3, 4, 5};
// Сдвиг вправо
int last = numbers[numbers.length - 1];
for (int i = numbers.length - 1; i > 0; i--) {
numbers[i] = numbers[i - 1];
}
numbers[0] = last;
System.out.println(Arrays.toString(numbers));
Вывод:
[5, 1, 2, 3, 4]
Лучшие практики
Используйте константы для размеров
// Плохо
int[] scores = new int[100];
// Хорошо
final int MAX_STUDENTS = 100;
int[] scores = new int[MAX_STUDENTS];
Проверяйте границы
public static int getElement(int[] array, int index) {
if (array == null) {
throw new IllegalArgumentException("Массив null");
}
if (index < 0 || index >= array.length) {
throw new IndexOutOfBoundsException("Индекс: " + index);
}
return array[index];
}
Используйте enhanced for когда возможно
// Хорошо для чтения
for (int num : numbers) {
System.out.println(num);
}
// Используйте обычный for когда нужен индекс
for (int i = 0; i < numbers.length; i++) {
System.out.println("Индекс " + i + ": " + numbers[i]);
}
Предпочитайте Collections для сложной логики
Для динамических коллекций используйте ArrayList:
// Вместо
int[] numbers = new int[10];
int count = 0;
// Используйте
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(42); // проще!
Итоги
Вы изучили массивы в Java:
- Объявление и создание массивов
- Доступ и изменение элементов
- Перебор массивов циклами
- Многомерные массивы
- Копирование массивов
- Полезные методы
Arrays - Частые ошибки и как их избежать
- Практические примеры
Массивы - фундаментальная структура данных. Они быстрые и эффективные, но помните:
- Размер фиксирован после создания
- Индексация начинается с 0
- Всегда проверяйте границы
Для динамических коллекций рассмотрите использование ArrayList и других классов из пакета java.util.
Задания для практики
Попробуйте написать программы для:
- Нахождения второго по величине элемента в массиве
- Проверки является ли массив палиндромом
- Слияния двух отсортированных массивов в один отсортированный
- Подсчёта частоты каждого элемента в массиве
Удачи!
1.7. Методы
Материалы
Методы - это блоки кода, которые выполняют определённую задачу. Они позволяют разбить программу на небольшие переиспользуемые части. Давайте научимся создавать и использовать методы!
Ваш первый метод
Вот простой метод, который выводит приветствие:
public class Main {
public static void greet() {
System.out.println("Привет, мир!");
}
public static void main(String[] args) {
greet(); // вызываем метод
}
}
Вывод:
Привет, мир!
Анатомия метода
Давайте разберём что означает каждая часть:
public static void greet() {
System.out.println("Привет, мир!");
}
public- модификатор доступа (метод доступен всем)static- метод принадлежит классу, а не объектуvoid- метод ничего не возвращаетgreet- имя метода()- список параметров (пока пустой){ }- тело метода
Параметры: передача данных в метод
Методы могут принимать информацию через параметры:
public static void greet(String name) {
System.out.println("Привет, " + name + "!");
}
public static void main(String[] args) {
greet("Анна"); // Привет, Анна!
greet("Боб"); // Привет, Боб!
greet("Карл"); // Привет, Карл!
}
name - это формальный параметр (объявлен в методе), а "Анна", "Боб", "Карл" - это фактические параметры (передаются при вызове).
Несколько параметров
Методы могут принимать несколько параметров:
public static void printSum(int a, int b) {
int sum = a + b;
System.out.println(a + " + " + b + " = " + sum);
}
public static void main(String[] args) {
printSum(5, 3); // 5 + 3 = 8
printSum(10, 20); // 10 + 20 = 30
}
Важно: Порядок параметров имеет значение!
printSum(5, 3)не то же самое чтоprintSum(3, 5).
Возвращаемые значения
Методы могут возвращать результат вычислений:
public static int add(int a, int b) {
return a + b;
}
public static void main(String[] args) {
int result = add(5, 3);
System.out.println("Результат: " + result); // Результат: 8
// Можно использовать прямо в выражениях
int total = add(10, 20) + add(5, 15);
System.out.println("Всего: " + total); // Всего: 50
}
Обратите внимание:
- Тип возвращаемого значения (
int) указан перед именем метода - Используется
returnчтобы вернуть значение - После
returnметод завершается
void vs возвращаемое значение
// void - ничего не возвращает
public static void printGreeting(String name) {
System.out.println("Привет, " + name);
}
// int - возвращает целое число
public static int getAge() {
return 25;
}
// String - возвращает строку
public static String getName() {
return "Анна";
}
// boolean - возвращает логическое значение
public static boolean isAdult(int age) {
return age >= 18;
}
Ранний выход с return
return можно использовать для раннего выхода из метода:
public static void checkAge(int age) {
if (age < 0) {
System.out.println("Некорректный возраст!");
return; // выход из метода
}
if (age < 18) {
System.out.println("Несовершеннолетний");
} else {
System.out.println("Взрослый");
}
}
Перегрузка методов
Java позволяет создавать несколько методов с одинаковым именем, но разными параметрами. Это называется перегрузкой (overloading):
public class Calculator {
// Сложение двух целых чисел
public static int add(int a, int b) {
return a + b;
}
// Сложение трёх целых чисел
public static int add(int a, int b, int c) {
return a + b + c;
}
// Сложение двух дробных чисел
public static double add(double a, double b) {
return a + b;
}
public static void main(String[] args) {
System.out.println(add(5, 3)); // 8
System.out.println(add(5, 3, 2)); // 10
System.out.println(add(5.5, 3.2)); // 8.7
}
}
Java выбирает нужный метод на основе:
- Количества параметров
- Типов параметров
- Порядка параметров
Примечание: Только тип возвращаемого значения не может различать перегруженные методы!
// ЭТО НЕ РАБОТАЕТ!
public static int getValue() { return 42; }
public static double getValue() { return 42.0; } // ОШИБКА КОМПИЛЯЦИИ!
Variable Arguments (Varargs)
Когда не знаете сколько параметров будет передано, используйте varargs:
public static int sum(int... numbers) {
int total = 0;
for (int num : numbers) {
total += num;
}
return total;
}
public static void main(String[] args) {
System.out.println(sum(1, 2)); // 3
System.out.println(sum(1, 2, 3)); // 6
System.out.println(sum(1, 2, 3, 4, 5)); // 15
System.out.println(sum()); // 0 (можно вызвать без аргументов)
}
Синтаксис: тип... имя (три точки!)
Правила varargs
- Varargs должен быть последним параметром:
// Правильно
public static void print(String prefix, int... numbers) {
// ...
}
// НЕПРАВИЛЬНО - не компилируется!
public static void print(int... numbers, String prefix) {
// ...
}
- Только один varargs на метод:
// НЕПРАВИЛЬНО - не компилируется!
public static void print(int... numbers, String... words) {
// ...
}
static vs instance методы
static методы (методы класса)
static методы принадлежат классу, а не объектам:
public class MathUtils {
public static int square(int x) {
return x * x;
}
public static void main(String[] args) {
// Вызываем через имя класса
int result = MathUtils.square(5);
System.out.println(result); // 25
}
}
Instance методы (методы экземпляра)
Instance методы принадлежат объектам класса:
public class Dog {
private String name;
public Dog(String name) {
this.name = name;
}
// Instance метод - нет static!
public void bark() {
System.out.println(name + " говорит: Гав!");
}
public void eat(String food) {
System.out.println(name + " ест " + food);
}
}
public class Main {
public static void main(String[] args) {
Dog myDog = new Dog("Шарик");
Dog herDog = new Dog("Бобик");
myDog.bark(); // Шарик говорит: Гав!
herDog.bark(); // Бобик говорит: Гав!
myDog.eat("мясо"); // Шарик ест мясо
herDog.eat("кость"); // Бобик ест кость
}
}
Когда использовать static?
Используйте static когда:
- Метод не зависит от состояния объекта
- Создаёте утилитные функции
- Нужен метод до создания объектов
// Утилитные методы - static
public class StringUtils {
public static boolean isEmpty(String str) {
return str == null || str.length() == 0;
}
public static String reverse(String str) {
return new StringBuilder(str).reverse().toString();
}
}
Используйте instance методы когда:
- Метод работает с данными объекта
- Поведение зависит от состояния объекта
// Методы работают с состоянием объекта - instance
public class BankAccount {
private double balance;
public void deposit(double amount) {
balance += amount;
}
public void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
}
}
public double getBalance() {
return balance;
}
}
Модификаторы доступа
Модификаторы контролируют кто может вызывать метод:
| Модификатор | Доступ |
|---|---|
public | Везде |
protected | В пакете + подклассы |
| (default) | Только в пакете |
private | Только в классе |
public class Person {
private String ssn; // номер соц. страхования
// public - доступен всем
public String getName() {
return "Анна";
}
// private - только внутри класса
private void validateSSN() {
// проверка номера
}
// protected - для подклассов
protected void updateRecords() {
// обновление записей
}
}
Инкапсуляция с геттерами и сеттерами
Хорошая практика - делать поля private и предоставлять public методы доступа:
public class Person {
private String name;
private int age;
// Getter для name
public String getName() {
return name;
}
// Setter для name
public void setName(String name) {
if (name != null && !name.isEmpty()) {
this.name = name;
}
}
// Getter для age
public int getAge() {
return age;
}
// Setter для age с валидацией
public void setAge(int age) {
if (age >= 0 && age <= 150) {
this.age = age;
}
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person();
person.setName("Анна");
person.setAge(25);
System.out.println(person.getName() + ", " + person.getAge());
}
}
Преимущества:
- ✅ Контроль над данными
- ✅ Валидация значений
- ✅ Можно изменить реализацию без изменения интерфейса
- ✅ Логирование, отладка
Рекурсия
Метод может вызывать сам себя - это рекурсия:
public static int factorial(int n) {
// Базовый случай
if (n <= 1) {
return 1;
}
// Рекурсивный случай
return n * factorial(n - 1);
}
public static void main(String[] args) {
System.out.println(factorial(5)); // 120
// 5! = 5 * 4 * 3 * 2 * 1 = 120
}
Как это работает:
factorial(5)
= 5 * factorial(4)
= 5 * (4 * factorial(3))
= 5 * (4 * (3 * factorial(2)))
= 5 * (4 * (3 * (2 * factorial(1))))
= 5 * (4 * (3 * (2 * 1)))
= 120
Важные правила рекурсии
- Базовый случай - условие остановки
- Рекурсивный случай - вызов самого себя с изменёнными параметрами
- Движение к базовому случаю - параметры должны приближать к остановке
// Плохая рекурсия - бесконечная!
public static int badRecursion(int n) {
return badRecursion(n); // НЕТ базового случая!
}
Рекурсия vs Итерация
Рекурсия:
public static int sumRecursive(int n) {
if (n <= 0) return 0;
return n + sumRecursive(n - 1);
}
Итерация:
public static int sumIterative(int n) {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
Оба дают одинаковый результат, но:
- Рекурсия: элегантнее, нагляднее
- Итерация: быстрее, меньше памяти
Совет: Используйте итерацию для простых случаев, рекурсию когда задача естественно рекурсивна (деревья, графы).
Примеры из практики
Проверка простого числа
public static boolean isPrime(int n) {
if (n <= 1) return false;
if (n <= 3) return true;
if (n % 2 == 0 || n % 3 == 0) return false;
for (int i = 5; i * i <= n; i += 6) {
if (n % i == 0 || n % (i + 2) == 0) {
return false;
}
}
return true;
}
Поиск максимума в массиве
public static int findMax(int[] array) {
if (array == null || array.length == 0) {
throw new IllegalArgumentException("Массив пустой");
}
int max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
return max;
}
Переворот строки (рекурсивно)
public static String reverse(String str) {
if (str.isEmpty()) {
return str;
}
return reverse(str.substring(1)) + str.charAt(0);
}
public static void main(String[] args) {
System.out.println(reverse("Hello")); // olleH
}
Числа Фибоначчи
Рекурсивный вариант (простой но медленный):
public static int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
Итеративный вариант (быстрый):
public static int fibonacci(int n) {
if (n <= 1) return n;
int prev = 0, curr = 1;
for (int i = 2; i <= n; i++) {
int next = prev + curr;
prev = curr;
curr = next;
}
return curr;
}
Лучшие практики
1. Выбирайте понятные имена
// Плохо
public static int calc(int x, int y) { ... }
// Хорошо
public static int calculateTotalPrice(int quantity, int unitPrice) { ... }
2. Методы должны делать одну вещь
// Плохо - слишком много ответственности
public static void processUserAndSendEmail(User user) {
validateUser(user);
saveToDatabase(user);
sendWelcomeEmail(user);
logActivity(user);
}
// Хорошо - разбито на отдельные методы
public static void registerUser(User user) {
validateUser(user);
saveToDatabase(user);
}
public static void notifyUser(User user) {
sendWelcomeEmail(user);
}
3. Избегайте побочных эффектов
// Плохо - изменяет глобальное состояние
static int counter = 0;
public static int badIncrement() {
counter++; // побочный эффект!
return counter;
}
// Хорошо - чистая функция
public static int goodIncrement(int value) {
return value + 1;
}
4. Валидируйте параметры
public static double divide(double a, double b) {
if (b == 0) {
throw new IllegalArgumentException("Делитель не может быть нулём");
}
return a / b;
}
5. Документируйте сложные методы
/**
* Вычисляет наибольший общий делитель двух чисел.
* Использует алгоритм Евклида.
*
* @param a первое число
* @param b второе число
* @return НОД(a, b)
*/
public static int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}
6. Держите методы короткими
Если метод не помещается на экран - возможно, его нужно разбить на несколько:
// Длинный метод
public static void processOrder(Order order) {
// 100+ строк кода
}
// Лучше разбить
public static void processOrder(Order order) {
validateOrder(order);
calculateTotal(order);
applyDiscounts(order);
processPayment(order);
sendConfirmation(order);
}
Method References (Java 8+)
Java 8 добавила возможность ссылаться на методы как на значения:
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = Arrays.asList("Анна", "Боб", "Карл");
// С лямбдой
names.forEach(name -> System.out.println(name));
// С method reference (короче!)
names.forEach(System.out::println);
}
}
Типы method references:
// 1. Ссылка на static метод
Function<String, Integer> parser = Integer::parseInt;
// 2. Ссылка на instance метод
String str = "Hello";
Supplier<Integer> lengthGetter = str::length;
// 3. Ссылка на instance метод произвольного объекта
Function<String, String> upperCaser = String::toUpperCase;
// 4. Ссылка на конструктор
Supplier<List<String>> listSupplier = ArrayList::new;
Частые ошибки
1. Забыть return
public static int add(int a, int b) {
int sum = a + b;
// ОШИБКА: нет return!
}
2. Не все пути возвращают значение
public static int getSign(int number) {
if (number > 0) {
return 1;
} else if (number < 0) {
return -1;
}
// ОШИБКА: что если number == 0?
}
Правильно:
public static int getSign(int number) {
if (number > 0) return 1;
if (number < 0) return -1;
return 0; // для нуля
}
3. Изменение параметров (примитивов)
public static void increment(int x) {
x++; // изменяет ЛОКАЛЬНУЮ копию, не оригинал!
}
public static void main(String[] args) {
int num = 5;
increment(num);
System.out.println(num); // всё ещё 5!
}
Важно: Java передаёт примитивы по значению - метод получает копию!
Изменение параметров (объектов)
Объекты тоже передаются по значению, но значение — это ссылка:
public static void modifyArray(int[] arr) {
arr[0] = 999; // изменяет оригинальный массив!
}
public static void replaceArray(int[] arr) {
arr = new int[]{1, 2, 3}; // создаёт новую ссылку, оригинал не меняется
}
public static void main(String[] args) {
int[] numbers = {10, 20, 30};
modifyArray(numbers);
System.out.println(numbers[0]); // 999 (изменился!)
replaceArray(numbers);
System.out.println(numbers[0]); // 999 (не изменился)
}
Итоги
Вы изучили методы в Java:
Основы:
- Метод = блок кода с именем, параметрами и возвращаемым значением
void— метод ничего не возвращаетreturn— возврат значения и выход из метода
Параметры:
- Формальные параметры (в объявлении) и фактические (при вызове)
- Передача по значению для примитивов
- Передача ссылки по значению для объектов
Расширенные возможности:
- Перегрузка (overloading) — методы с одинаковым именем, разными параметрами
- Varargs (
тип...) — переменное число аргументов - Method references (
::) — ссылки на методы
static vs instance:
static— принадлежит классу, вызывается черезClassName.method()- instance — принадлежит объекту, вызывается через
object.method()
Модификаторы доступа:
public— вездеprotected— пакет + подклассы- (default) — только пакет
private— только класс
Рекурсия:
- Метод вызывает сам себя
- Обязателен базовый случай
- Используйте для древовидных структур
Best practices:
- Понятные имена (глагол + существительное)
- Один метод — одна задача
- Короткие методы (помещаются на экран)
- Валидация параметров
- Избегайте побочных эффектов
Задания для практики
-
Калькулятор:
- Методы add, subtract, multiply, divide
- Перегрузка для int и double
- Валидация деления на ноль
-
Палиндром:
- Метод isPalindrome(String)
- Рекурсивная и итеративная версии
- Сравните производительность
-
Поиск в массиве:
- Метод indexOf(int[] array, int value)
- Метод contains(int[] array, int value)
- Varargs версия sum(int… numbers)
-
Класс StringUtils:
- static метод reverse(String)
- static метод countVowels(String)
- static метод capitalize(String)
1.8. ООП: Классы и объекты
Основы объектно-ориентированного программирования
Материалы
Добро пожаловать в мир объектно-ориентированного программирования! В этом разделе мы изучим основы ООП: что такое классы и объекты, как их создавать и использовать, и почему инкапсуляция так важна.
Что такое ООП?
Посмотрите вокруг прямо сейчас. Всё, что вы видите - это объекты: ваша собака, велосипед, телефон, чашка кофе. Каждый объект имеет:
- Состояние (свойства, характеристики) - цвет собаки, скорость велосипеда, заряд батареи телефона
- Поведение (что он умеет делать) - собака лает, велосипед едет, телефон звонит
ООП переносит эту идею в программирование! Мы создаём программные объекты, которые имеют:
- Поля (fields) - хранят состояние
- Методы (methods) - определяют поведение
Простой пример из жизни
Объект: Собака "Шарик"
├── Состояние (поля):
│ ├── name = "Шарик"
│ ├── breed = "Лабрадор"
│ ├── age = 3
│ └── hungry = true
└── Поведение (методы):
├── bark() - лаять
├── eat() - есть
├── sleep() - спать
└── play() - играть
Преимущества ООП
1. Модульность Код организован в логические блоки (классы), которые легко понять и поддерживать.
2. Переиспользование Написали класс один раз - используем много раз. Создаём много объектов из одного класса.
3. Инкапсуляция Скрываем внутренние детали, показываем только важное.
4. Масштабируемость Легко добавлять новую функциональность без изменения существующего кода.
Классы и объекты: чертёж и дом
Класс - это чертёж, шаблон, blueprint для создания объектов. Как архитектурный план дома.
Объект - это конкретный экземпляр (instance) класса. Как настоящий дом, построенный по плану.
Аналогия
Класс "Дом" → Объекты (конкретные дома)
├── план → Мой дом на ул. Ленина
├── материалы → Дом соседа на ул. Мира
└── спецификация → Дом друга на ул. Садовой
По одному плану (классу) можно построить много домов (объектов)!
Первый класс
Давайте создадим класс Dog (Собака):
// Класс - чертёж для создания собак
public class Dog {
// Поля (fields) - состояние объекта
String name;
String breed;
int age;
boolean hungry;
// Методы (methods) - поведение объекта
void bark() {
System.out.println(name + " говорит: Гав-гав!");
}
void eat(String food) {
System.out.println(name + " ест " + food);
hungry = false;
}
void sleep() {
System.out.println(name + " спит... Zzz");
}
void play() {
System.out.println(name + " играет!");
hungry = true;
}
}
Анатомия класса
public class Dog {
// 1. Поля (переменные экземпляра)
String name;
int age;
// 2. Конструкторы (скоро изучим)
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
// 3. Методы (функции объекта)
void bark() {
System.out.println("Гав!");
}
}
Создание объектов
Чтобы создать объект, используем оператор new:
public class Main {
public static void main(String[] args) {
// Создаём первого пса (первый объект)
Dog myDog = new Dog();
myDog.name = "Шарик";
myDog.breed = "Лабрадор";
myDog.age = 3;
myDog.hungry = true;
// Создаём второго пса (второй объект)
Dog herDog = new Dog();
herDog.name = "Бобик";
herDog.breed = "Овчарка";
herDog.age = 5;
herDog.hungry = false;
// Используем объекты!
myDog.bark(); // Шарик говорит: Гав-гав!
herDog.bark(); // Бобик говорит: Гав-гав!
myDog.eat("мясо"); // Шарик ест мясо
herDog.sleep(); // Бобик спит... Zzz
myDog.play();
System.out.println(myDog.name + " голоден? " + myDog.hungry); // true
}
}
Что происходит при создании объекта?
Dog myDog = new Dog();
Dog- тип переменнойmyDog- имя переменной (ссылка на объект)new- оператор создания объектаDog()- вызов конструктора
Память:
┌──────────────┐
│ myDog (ссылка) │ ──→ ┌───────────────┐
└──────────────┘ │ Объект Dog │
├───────────────┤
│ name: "Шарик" │
│ breed: "..." │
│ age: 3 │
│ hungry: true │
└───────────────┘
Важно:
myDogиherDog- это два совершенно разных объекта в памяти! У каждого своё имя, свой возраст, своё состояние!
Конструкторы: правильная инициализация
Конструктор - это специальный метод для создания и инициализации объектов.
Особенности конструктора
- ✅ Имя совпадает с именем класса
- ✅ НЕТ типа возвращаемого значения (даже
void) - ✅ Вызывается автоматически при создании объекта через
new - ✅ Может быть несколько (перегрузка)
Простой конструктор
public class Dog {
String name;
String breed;
int age;
// Конструктор
public Dog(String name, String breed, int age) {
this.name = name;
this.breed = breed;
this.age = age;
}
void bark() {
System.out.println(name + " говорит: Гав-гав!");
}
void info() {
System.out.println(name + " - " + breed + ", " + age + " лет");
}
}
// Использование
Dog myDog = new Dog("Шарик", "Лабрадор", 3);
Dog herDog = new Dog("Бобик", "Овчарка", 5);
myDog.info(); // Шарик - Лабрадор, 3 лет
herDog.info(); // Бобик - Овчарка, 5 лет
myDog.bark(); // Шарик говорит: Гав-гав!
Почему конструкторы важны?
Без конструктора:
Dog dog = new Dog();
dog.name = "Шарик"; // можем забыть!
dog.breed = "Лабрадор"; // можем забыть!
dog.age = 3; // можем забыть!
// У собаки могут быть неинициализированные поля!
С конструктором:
Dog dog = new Dog("Шарик", "Лабрадор", 3);
// Все поля гарантированно инициализированы!
Конструктор по умолчанию
Если вы НЕ создали конструктор, Java создаёт конструктор по умолчанию автоматически:
public class Dog {
String name;
int age;
// Java автоматически создаёт:
// public Dog() {}
}
Dog dog = new Dog(); // работает!
Но если вы создали ХОТЯ БЫ ОДИН конструктор, конструктор по умолчанию НЕ создаётся:
public class Dog {
String name;
public Dog(String name) {
this.name = name;
}
}
// Dog dog = new Dog(); // ОШИБКА! Нет конструктора без параметров
Dog dog = new Dog("Шарик"); // OK
this - ссылка на текущий объект
Ключевое слово this указывает на объект, для которого вызван метод.
Зачем нужен this?
Проблема: параметр и поле имеют одинаковое имя
public class Dog {
String name;
// name параметра "затеняет" поле name
public Dog(String name) {
name = name; // ЧТО?? Это не работает!
// Оба name - это параметр!
}
}
Решение: используем this
public class Dog {
String name;
int age;
public Dog(String name, int age) {
this.name = name; // this.name - поле объекта
// name - параметр конструктора
this.age = age;
}
}
this для вызова другого конструктора
public class Dog {
String name;
String breed;
int age;
// Полный конструктор
public Dog(String name, String breed, int age) {
this.name = name;
this.breed = breed;
this.age = age;
}
// Упрощённый - делегирует полному
public Dog(String name) {
this(name, "Дворняжка", 0); // вызов другого конструктора!
}
// Конструктор по умолчанию
public Dog() {
this("Безымянный"); // вызов конструктора с одним параметром
}
}
Использование:
Dog dog1 = new Dog("Шарик", "Лабрадор", 3); // полный
Dog dog2 = new Dog("Бобик"); // упрощённый
Dog dog3 = new Dog(); // по умолчанию
dog1.info(); // Шарик - Лабрадор, 3 лет
dog2.info(); // Бобик - Дворняжка, 0 лет
dog3.info(); // Безымянный - Дворняжка, 0 лет
Правила this():
- ⚠️ Вызов
this()должен быть ПЕРВОЙ строкой в конструкторе - ⚠️ Нельзя вызвать два конструктора одновременно
public Dog(String name) {
System.out.println("Creating dog");
this(name, "Unknown", 0); // ОШИБКА! Должно быть первым
}
Инкапсуляция: прячем и контролируем
Инкапсуляция - это сокрытие внутренних деталей объекта и предоставление контролируемого доступа. Один из четырёх столпов ООП!
Зачем нужна инкапсуляция?
1. Защита данных Предотвращаем некорректное изменение состояния объекта.
2. Гибкость Можем изменить внутреннюю реализацию без изменения внешнего API.
3. Простота использования Пользователь класса видит только важные методы, а не внутренние детали.
Проблема без инкапсуляции
public class BankAccount {
public double balance; // публичное поле - ОПАСНО!
public String owner;
}
BankAccount account = new BankAccount();
account.balance = 1000000; // Кто угодно может изменить баланс!
account.balance = -500; // Даже отрицательный!
account.owner = ""; // Пустое имя!
Проблемы:
- ❌ Нет контроля над данными
- ❌ Можно установить некорректные значения
- ❌ Нельзя добавить логику (логирование, валидация)
Решение: private + методы доступа
public class BankAccount {
// Поля приватные!
private double balance;
private String owner;
public BankAccount(String owner, double initialBalance) {
this.owner = owner;
// Валидация при создании
if (initialBalance >= 0) {
this.balance = initialBalance;
} else {
this.balance = 0;
System.out.println("Начальный баланс не может быть отрицательным!");
}
}
// Контролируемые методы доступа
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
System.out.println("Внесено: " + amount + " руб.");
System.out.println("Новый баланс: " + balance + " руб.");
} else {
System.out.println("Сумма должна быть положительной!");
}
}
public boolean withdraw(double amount) {
if (amount <= 0) {
System.out.println("Сумма должна быть положительной!");
return false;
}
if (amount > balance) {
System.out.println("Недостаточно средств!");
System.out.println("Доступно: " + balance + " руб.");
return false;
}
balance -= amount;
System.out.println("Снято: " + amount + " руб.");
System.out.println("Остаток: " + balance + " руб.");
return true;
}
// Только чтение баланса (getter)
public double getBalance() {
return balance;
}
public String getOwner() {
return owner;
}
}
// Использование
BankAccount account = new BankAccount("Анна", 1000);
account.deposit(500); // OK
account.deposit(-100); // Ошибка: сумма должна быть положительной
account.withdraw(2000); // Ошибка: недостаточно средств
account.withdraw(800); // OK
System.out.println("Баланс: " + account.getBalance());
// account.balance = 1000000; // ОШИБКА КОМПИЛЯЦИИ! Поле private
Модификаторы доступа
Java имеет 4 уровня доступа:
| Модификатор | Внутри класса | В пакете | В подклассе | Везде |
|---|---|---|---|---|
public | ✅ | ✅ | ✅ | ✅ |
protected | ✅ | ✅ | ✅ | ❌ |
| (default) | ✅ | ✅ | ❌ | ❌ |
private | ✅ | ❌ | ❌ | ❌ |
Примеры
public class Example {
public int publicField; // доступно везде
protected int protectedField; // пакет + наследники
int defaultField; // только в пакете
private int privateField; // только в классе
public void publicMethod() {} // доступен везде
protected void protectedMethod() {} // пакет + наследники
void defaultMethod() {} // только пакет
private void privateMethod() {} // только класс
}
Когда что использовать?
private (по умолчанию для полей)
- ✅ Поля класса
- ✅ Вспомогательные методы
- ✅ Внутренняя реализация
public (для API)
- ✅ Методы, которые должны использовать другие классы
- ✅ Константы
- ✅ Интерфейс класса
protected (для наследников)
- ✅ Методы/поля для использования в подклассах
- ✅ Расширяемая функциональность
default (package-private)
- ✅ Классы-помощники внутри пакета
- ✅ Методы для внутреннего использования в пакете
Геттеры и сеттеры
Геттеры (getters) - методы для чтения приватных полей. Сеттеры (setters) - методы для изменения приватных полей.
Базовый пример
public class Person {
private String name;
private int age;
// Getter для name
public String getName() {
return name;
}
// Setter для name с валидацией
public void setName(String name) {
if (name != null && !name.trim().isEmpty()) {
this.name = name;
} else {
System.out.println("Имя не может быть пустым!");
}
}
// Getter для age
public int getAge() {
return age;
}
// Setter для age с валидацией
public void setAge(int age) {
if (age >= 0 && age <= 150) {
this.age = age;
} else {
System.out.println("Некорректный возраст!");
}
}
}
// Использование
Person person = new Person();
person.setName("Анна");
person.setAge(25);
System.out.println(person.getName() + ", " + person.getAge() + " лет");
person.setAge(200); // Некорректный возраст!
person.setName(""); // Имя не может быть пустым!
Зачем геттеры и сеттеры?
1. Валидация
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Некорректный возраст");
}
this.age = age;
}
2. Вычисляемые свойства
private String firstName;
private String lastName;
public String getFullName() {
return firstName + " " + lastName;
}
3. Логирование
public void setBalance(double balance) {
System.out.println("Изменение баланса: " + this.balance + " → " + balance);
this.balance = balance;
}
4. Ленивая инициализация
private List<String> items;
public List<String> getItems() {
if (items == null) {
items = new ArrayList<>();
}
return items;
}
5. Контроль записи
private String id;
public String getId() {
return id;
}
// Нет setter - поле только для чтения!
Naming conventions
// Для boolean используем is/has/can
private boolean active;
public boolean isActive() { return active; }
private boolean hasLicense;
public boolean hasLicense() { return hasLicense; }
// Для остальных типов - get/set
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
Практический пример: Класс Rectangle
Применим все концепции:
public class Rectangle {
// Инкапсуляция - поля приватные
private double width;
private double height;
private String color;
// Конструктор с валидацией
public Rectangle(double width, double height, String color) {
setWidth(width);
setHeight(height);
setColor(color);
}
// Перегрузка конструктора
public Rectangle(double side) {
this(side, side, "белый"); // квадрат
}
// Геттеры
public double getWidth() {
return width;
}
public double getHeight() {
return height;
}
public String getColor() {
return color;
}
// Сеттеры с валидацией
public void setWidth(double width) {
if (width > 0) {
this.width = width;
} else {
throw new IllegalArgumentException("Ширина должна быть положительной");
}
}
public void setHeight(double height) {
if (height > 0) {
this.height = height;
} else {
throw new IllegalArgumentException("Высота должна быть положительной");
}
}
public void setColor(String color) {
if (color != null && !color.isEmpty()) {
this.color = color;
} else {
this.color = "белый";
}
}
// Методы поведения
public double getArea() {
return width * height;
}
public double getPerimeter() {
return 2 * (width + height);
}
public boolean isSquare() {
return width == height;
}
public void scale(double factor) {
if (factor > 0) {
width *= factor;
height *= factor;
}
}
@Override
public String toString() {
return String.format("Rectangle[%.1f x %.1f, %s, area=%.2f]",
width, height, color, getArea());
}
}
// Использование
public class Main {
public static void main(String[] args) {
Rectangle rect1 = new Rectangle(10, 5, "красный");
Rectangle rect2 = new Rectangle(8); // квадрат
System.out.println(rect1); // Rectangle[10.0 x 5.0, красный, area=50.00]
System.out.println(rect2); // Rectangle[8.0 x 8.0, белый, area=64.00]
System.out.println("Площадь: " + rect1.getArea()); // 50.0
System.out.println("Периметр: " + rect1.getPerimeter()); // 30.0
System.out.println("Квадрат? " + rect1.isSquare()); // false
rect1.scale(2);
System.out.println(rect1); // Rectangle[20.0 x 10.0, красный, area=200.00]
// rect1.width = -5; // ОШИБКА! Поле private
// rect1.setWidth(-5); // IllegalArgumentException
}
}
Лучшие практики
1. Делайте поля private
// ❌ Плохо
public class Person {
public String name; // прямой доступ
public int age;
}
// ✅ Хорошо
public class Person {
private String name;
private int age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
2. Используйте конструкторы для инициализации
// ❌ Плохо
Dog dog = new Dog();
dog.name = "Шарик";
dog.age = 3;
// ✅ Хорошо
Dog dog = new Dog("Шарик", 3);
3. Валидируйте в сеттерах
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Некорректный возраст: " + age);
}
this.age = age;
}
4. Используйте final для неизменяемых полей
public class Person {
private final String id; // нельзя изменить после создания
private String name;
public Person(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id; // только getter, нет setter
}
}
5. Создавайте неизменяемые классы когда возможно
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
// Нет setters - объект неизменяемый!
// Для "изменения" создаём новый объект
public Point move(int dx, int dy) {
return new Point(x + dx, y + dy);
}
}
6. Документируйте публичные методы
/**
* Устанавливает возраст человека.
*
* @param age возраст в годах (должен быть от 0 до 150)
* @throws IllegalArgumentException если возраст вне допустимого диапазона
*/
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Некорректный возраст: " + age);
}
this.age = age;
}
Итоги
Вы изучили основы ООП в Java!
Ключевые концепции:
- Классы - чертежи для создания объектов
- Объекты - экземпляры классов со своим состоянием
- Поля - хранят состояние объекта
- Методы - определяют поведение объекта
- Конструкторы - инициализируют новые объекты
- this - ссылка на текущий объект
- Инкапсуляция - скрытие деталей реализации
- Модификаторы доступа - контроль видимости
- Геттеры/сеттеры - контролируемый доступ к полям
Почему это важно:
- Организация кода в логические блоки
- Защита данных от некорректного использования
- Переиспользование через создание множества объектов
- Модульность и простота поддержки
Задания для практики
-
Класс Book:
- Поля: title, author, pages, price
- Конструкторы: полный и упрощённый
- Геттеры/сеттеры с валидацией
- Метод displayInfo()
-
Класс BankAccount:
- Поля: accountNumber, balance, owner
- Методы: deposit(), withdraw(), transfer()
- Валидация всех операций
- История транзакций
-
Класс Circle:
- Поле: radius
- Методы: getArea(), getCircumference(), getDiameter()
- Сравнение двух окружностей
- Константа PI
-
Класс Student:
- Поля: name, id, grades (массив)
- Методы: addGrade(), getAverage(), isPassing()
- Валидация оценок (от 2 до 5)
Удачи в освоении ООП! Это фундамент профессионального программирования.
Источники:
- Oracle Java Tutorial - What Is an Object?
- Oracle Java Tutorial - What Is a Class?
- Oracle Java Tutorial - Classes and Objects
1.9. Наследование и полиморфизм
Материалы
Заметки
Добро пожаловать во вторую часть изучения ООП! Здесь мы освоим два мощнейших инструмента: наследование для переиспользования кода и полиморфизм для гибкости программ.
Что такое наследование?
Наследование (Inheritance) - это механизм, позволяющий создавать новые классы на основе существующих, переиспользуя их код.
Аналогия из жизни
Думайте о наследовании как о генетике:
- Дети наследуют черты от родителей
- Но при этом имеют свои уникальные особенности
Родитель (Animal)
├── глаза
├── уши
└── может дышать
↓ наследование
Ребёнок (Dog)
├── глаза (от родителя)
├── уши (от родителя)
├── может дышать (от родителя)
└── может лаять (своё!)
Зачем нужно наследование?
1. Переиспользование кода (Code Reuse) Не пишем один и тот же код многократно.
2. Расширение функциональности Добавляем новые возможности к существующим классам.
3. Иерархия и организация Создаём логическую структуру классов.
4. Полиморфизм Позволяет работать с объектами через общий интерфейс.
Базовое наследование
Синтаксис
class Родитель {
// поля и методы родителя
}
class Потомок extends Родитель {
// поля и методы потомка
// + унаследованные от родителя
}
Первый пример
// Базовый класс (родитель, суперкласс, parent class)
public class Animal {
protected String name; // protected - доступно наследникам
protected int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
System.out.println("Создано животное: " + name);
}
public void eat() {
System.out.println(name + " ест");
}
public void sleep() {
System.out.println(name + " спит");
}
public void breathe() {
System.out.println(name + " дышит");
}
public void makeSound() {
System.out.println(name + " издаёт звук");
}
}
// Производный класс (потомок, подкласс, child class)
public class Dog extends Animal {
private String breed;
public Dog(String name, int age, String breed) {
super(name, age); // вызов конструктора родителя
this.breed = breed;
System.out.println("Создана собака породы: " + breed);
}
// Новые методы - только у Dog
public void bark() {
System.out.println(name + " лает: Гав-гав!");
}
public void fetch() {
System.out.println(name + " приносит палку");
}
// Переопределяем метод родителя
@Override
public void makeSound() {
System.out.println(name + " лает громко!");
}
}
// Ещё один потомок
public class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
System.out.println("Создан кот");
}
public void meow() {
System.out.println(name + " мяукает: Мяу!");
}
public void scratch() {
System.out.println(name + " точит когти");
}
@Override
public void makeSound() {
System.out.println(name + " мяукает нежно!");
}
}
Использование
public class Main {
public static void main(String[] args) {
Dog dog = new Dog("Шарик", 3, "Лабрадор");
Cat cat = new Cat("Мурка", 2);
System.out.println("\n=== Собака ===");
// Унаследованные методы от Animal
dog.eat(); // Шарик ест
dog.sleep(); // Шарик спит
dog.breathe(); // Шарик дышит
// Собственные методы Dog
dog.bark(); // Шарик лает: Гав-гав!
dog.fetch(); // Шарик приносит палку
// Переопределённый метод
dog.makeSound(); // Шарик лает громко!
System.out.println("\n=== Кошка ===");
cat.eat(); // Мурка ест
cat.meow(); // Мурка мяукает: Мяу!
cat.scratch(); // Мурка точит когти
cat.makeSound(); // Мурка мяукает нежно!
}
}
Вывод:
Создано животное: Шарик
Создана собака породы: Лабрадор
Создано животное: Мурка
Создан кот
=== Собака ===
Шарик ест
Шарик спит
Шарик дышит
Шарик лает: Гав-гав!
Шарик приносит палку
Шарик лает громко!
=== Кошка ===
Мурка ест
Мурка мяукает: Мяу!
Мурка точит когти
Мурка мяукает нежно!
Ключевое слово super
super - это ссылка на родительский класс. Используется для:
- Вызова конструктора родителя
- Вызова методов родителя
- Доступа к полям родителя (если скрыты)
super() - вызов конструктора родителя
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
System.out.println("Animal constructor: " + name);
}
}
public class Dog extends Animal {
private String breed;
public Dog(String name, String breed) {
super(name); // ОБЯЗАТЕЛЬНО вызываем конструктор родителя
this.breed = breed;
System.out.println("Dog constructor: " + breed);
}
}
Правила super():
- ⚠️ Должен быть ПЕРВОЙ строкой в конструкторе
- ⚠️ Если не вызвать явно, Java вызовет
super()автоматически - ⚠️ Если у родителя нет конструктора без параметров - ОБЯЗАТЕЛЬНО явный вызов super()
super для вызова методов родителя
public class Animal {
public void eat() {
System.out.println("Животное ест");
}
}
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("Собака готовится к еде...");
super.eat(); // вызываем метод родителя
System.out.println("Собака наелась!");
}
}
Dog dog = new Dog();
dog.eat();
// Вывод:
// Собака готовится к еде...
// Животное ест
// Собака наелась!
super для доступа к полям родителя
public class Parent {
protected int value = 10;
}
public class Child extends Parent {
private int value = 20; // скрывает поле родителя
public void display() {
System.out.println("Child value: " + value); // 20
System.out.println("Child value: " + this.value); // 20
System.out.println("Parent value: " + super.value); // 10
}
}
Иерархия наследования
Классы могут образовывать дерево наследования:
Animal
/ \
Dog Cat
| |
Puppy Kitten
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void makeSound() {
System.out.println("Животное издаёт звук");
}
}
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(name + " лает");
}
public void bark() {
System.out.println(name + " гавкает!");
}
}
public class Puppy extends Dog {
private int monthsOld;
public Puppy(String name, int monthsOld) {
super(name);
this.monthsOld = monthsOld;
}
@Override
public void makeSound() {
System.out.println(name + " тявкает по-щенячьи");
}
public void play() {
System.out.println(name + " играет с игрушкой");
}
}
// Использование
Puppy puppy = new Puppy("Малыш", 3);
puppy.play(); // из Puppy
puppy.bark(); // из Dog
puppy.makeSound(); // из Puppy (переопределён)
// puppy наследует всё от Dog, а Dog - от Animal
Цепочка вызова конструкторов
При создании объекта конструкторы вызываются от корня к листу:
Puppy puppy = new Puppy("Малыш", 3);
// 1. Вызывается Animal("Малыш")
// 2. Вызывается Dog("Малыш")
// 3. Вызывается Puppy("Малыш", 3)
Правила и ограничения наследования
1. Java = Одиночное наследование
Java НЕ поддерживает множественное наследование классов:
// ❌ ОШИБКА! Нельзя наследовать от двух классов
class C extends A, B { // ОШИБКА КОМПИЛЯЦИИ!
}
Но можно реализовать несколько интерфейсов (об этом в следующей главе):
// ✅ OK! Можно реализовать много интерфейсов
class C extends A implements B, D, E {
}
2. Все классы наследуются от Object
public class Dog {
// неявно: extends Object
}
// Эквивалентно:
public class Dog extends Object {
}
Object - корень всей иерархии Java. Поэтому у ВСЕХ объектов есть методы:
toString()equals()hashCode()getClass()- и другие
3. private члены НЕ наследуются
public class Parent {
private int secret = 42;
public int getSecret() {
return secret;
}
}
public class Child extends Parent {
public void test() {
// System.out.println(secret); // ОШИБКА! private не наследуется
System.out.println(getSecret()); // OK через public метод
}
}
4. Конструкторы НЕ наследуются
public class Parent {
public Parent(int x) {
System.out.println("Parent: " + x);
}
}
public class Child extends Parent {
// Конструктор Parent(int) не наследуется!
// Должны создать свой конструктор
public Child(int x) {
super(x); // явный вызов родительского
}
}
// Child child = new Child(); // ОШИБКА! Нет конструктора без параметров
Child child = new Child(10); // OK
5. final класс нельзя наследовать
public final class String {
// ...
}
// public class MyString extends String {} // ОШИБКА!
6. Циклическое наследование запрещено
// ❌ ОШИБКА!
class A extends B {}
class B extends A {} // циклическое наследование!
Переопределение методов (Method Overriding)
Переопределение - это создание в подклассе метода с той же сигнатурой, что и в родителе, но с другой реализацией.
Базовый пример
public class Animal {
public void makeSound() {
System.out.println("Животное издаёт звук");
}
}
public class Dog extends Animal {
@Override // рекомендуется использовать!
public void makeSound() {
System.out.println("Собака лает!");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Кошка мяукает!");
}
}
Animal animal = new Animal();
Animal dog = new Dog();
Animal cat = new Cat();
animal.makeSound(); // Животное издаёт звук
dog.makeSound(); // Собака лает!
cat.makeSound(); // Кошка мяукает!
Аннотация @Override
@Override - это аннотация, которая говорит компилятору: “Я переопределяю метод родителя”.
Зачем нужна:
- ✅ Компилятор проверит корректность переопределения
- ✅ Защита от опечаток
- ✅ Улучшает читаемость кода
public class Dog extends Animal {
@Override
public void makeSound() { // OK
System.out.println("Гав!");
}
// @Override
// public void makeSond() { // ОШИБКА! Опечатка, нет такого метода в родителе
// System.out.println("Гав!");
// }
}
Правила переопределения
1. Сигнатура должна совпадать
// Родитель
public void method(int x) { }
// ✅ OK - точно такая же сигнатура
@Override
public void method(int x) { }
// ❌ ОШИБКА - другая сигнатура (это перегрузка, не переопределение!)
public void method(double x) { }
2. Возвращаемый тип должен быть тем же или подтипом (covariant return)
public class Parent {
public Number getValue() {
return 42;
}
}
public class Child extends Parent {
@Override
public Integer getValue() { // Integer - подтип Number, OK!
return 100;
}
}
3. Модификатор доступа не может быть более строгим
public class Parent {
protected void method() { }
}
public class Child extends Parent {
// ✅ OK - можем расширить доступ
@Override
public void method() { }
// ❌ ОШИБКА - нельзя сузить доступ
// @Override
// private void method() { }
}
4. Нельзя переопределить final метод
public class Parent {
public final void method() { // final - нельзя переопределить
System.out.println("Parent");
}
}
public class Child extends Parent {
// @Override
// public void method() { // ОШИБКА! Метод final
// System.out.println("Child");
// }
}
5. Нельзя переопределить static метод
Static методы скрываются (hiding), а не переопределяются:
public class Parent {
public static void staticMethod() {
System.out.println("Parent static");
}
}
public class Child extends Parent {
// Это скрытие (hiding), не переопределение!
public static void staticMethod() {
System.out.println("Child static");
}
}
Parent.staticMethod(); // Parent static
Child.staticMethod(); // Child static
Parent p = new Child();
p.staticMethod(); // Parent static (!)
// Зависит от типа ПЕРЕМЕННОЙ, не объекта
Полиморфизм
Полиморфизм (Polymorphism) - это способность объектов разных классов отвечать на одни и те же вызовы по-своему.
Буквально: “много форм” (poly = много, morph = форма)
Суть полиморфизма
Animal animal1 = new Dog("Шарик", 3);
Animal animal2 = new Cat("Мурка", 2);
Animal animal3 = new Animal("Попугай", 1);
// Один и тот же вызов - разное поведение!
animal1.makeSound(); // лает
animal2.makeSound(); // мяукает
animal3.makeSound(); // издаёт звук
// Тип переменной: Animal
// Тип объекта: Dog, Cat, Animal
// Вызывается метод РЕАЛЬНОГО объекта, не типа переменной!
Зачем нужен полиморфизм?
1. Универсальный код
public void feedAnimal(Animal animal) {
animal.eat(); // работает для любого животного!
}
feedAnimal(new Dog("Шарик", 3));
feedAnimal(new Cat("Мурка", 2));
feedAnimal(new Bird("Кеша", 1));
2. Массивы и коллекции разных типов
Animal[] zoo = {
new Dog("Рекс", 4),
new Cat("Барсик", 3),
new Dog("Тузик", 2),
new Cat("Мурка", 5)
};
// Кормим всех одинаково!
for (Animal animal : zoo) {
animal.eat();
animal.makeSound();
}
3. Гибкость и расширяемость
Добавляем новый класс - старый код продолжает работать:
// Старый код
public class Zoo {
public void feedAll(Animal[] animals) {
for (Animal animal : animals) {
animal.eat();
}
}
}
// Добавляем новый класс
public class Bird extends Animal {
// ...
}
// Старый код feedAll() работает и с Bird без изменений!
Практический пример: зоопарк
public class Zoo {
private Animal[] animals;
public Zoo(Animal[] animals) {
this.animals = animals;
}
public void feedingTime() {
System.out.println("=== Время кормления! ===");
for (Animal animal : animals) {
System.out.println("\nКормим " + animal.name + ":");
animal.eat();
animal.makeSound();
}
}
public void nightTime() {
System.out.println("\n=== Наступила ночь ===");
for (Animal animal : animals) {
animal.sleep();
}
}
}
// Использование
Animal[] animals = {
new Dog("Рекс", 4, "Овчарка"),
new Cat("Барсик", 3),
new Dog("Тузик", 2, "Бульдог"),
new Cat("Мурка", 5),
new Animal("Попугай", 2)
};
Zoo zoo = new Zoo(animals);
zoo.feedingTime();
zoo.nightTime();
Приведение типов и instanceof
Восходящее приведение (Upcasting)
Автоматическое, безопасное:
Dog dog = new Dog("Шарик", 3);
Animal animal = dog; // автоматическое приведение Dog → Animal
animal.eat(); // OK
// animal.bark(); // ОШИБКА! Animal не знает про bark()
Нисходящее приведение (Downcasting)
Явное, может быть небезопасным:
Animal animal = new Dog("Шарик", 3);
// Явное приведение
Dog dog = (Dog) animal; // OK, потому что объект действительно Dog
dog.bark(); // теперь можем вызвать bark()
// Опасно!
Cat cat = (Cat) animal; // ClassCastException в runtime!
instanceof - проверка типа
Безопасная проверка перед приведением:
Animal animal = new Dog("Шарик", 3);
// Старый способ
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.bark();
}
if (animal instanceof Cat) {
Cat cat = (Cat) animal;
cat.meow();
}
Pattern Matching (Java 16+)
Более элегантный способ:
Animal animal = new Dog("Шарик", 3);
// Проверка и приведение в одной строке!
if (animal instanceof Dog dog) {
dog.bark(); // dog уже приведён к типу Dog
}
if (animal instanceof Cat cat) {
cat.meow();
} else {
System.out.println("Это не кот");
}
Практический пример
public class AnimalProcessor {
public static void processAnimal(Animal animal) {
// Общие действия для всех
animal.eat();
animal.makeSound();
// Специфичные действия
if (animal instanceof Dog dog) {
dog.fetch();
dog.bark();
} else if (animal instanceof Cat cat) {
cat.scratch();
cat.meow();
} else {
System.out.println("Неизвестное животное");
}
}
public static void main(String[] args) {
processAnimal(new Dog("Рекс", 4, "Овчарка"));
processAnimal(new Cat("Мурка", 3));
processAnimal(new Animal("Попугай", 2));
}
}
Класс Object - корень иерархии
Все классы в Java неявно наследуются от Object:
public class Dog {
// неявно extends Object
}
Важные методы Object
1. toString()
Возвращает строковое представление объекта.
public class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Dog{name='" + name + "', age=" + age + "}";
}
}
Dog dog = new Dog("Шарик", 3);
System.out.println(dog); // Dog{name='Шарик', age=3}
// Без @Override было бы:
// Dog@15db9742 (имя класса @ хеш-код)
2. equals()
Сравнивает содержимое объектов.
public class Dog {
private String name;
private int age;
// ... конструктор ...
@Override
public boolean equals(Object obj) {
// 1. Проверка на тот же объект
if (this == obj) return true;
// 2. Проверка на null
if (obj == null) return false;
// 3. Проверка типа
if (getClass() != obj.getClass()) return false;
// 4. Сравнение полей
Dog other = (Dog) obj;
return age == other.age &&
Objects.equals(name, other.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
Dog dog1 = new Dog("Шарик", 3);
Dog dog2 = new Dog("Шарик", 3);
Dog dog3 = new Dog("Бобик", 5);
System.out.println(dog1.equals(dog2)); // true
System.out.println(dog1.equals(dog3)); // false
System.out.println(dog1 == dog2); // false (разные объекты!)
Контракт equals():
- Рефлексивность:
x.equals(x)должно бытьtrue - Симметричность: если
x.equals(y), тоy.equals(x) - Транзитивность: если
x.equals(y)иy.equals(z), тоx.equals(z) - Консистентность: многократные вызовы дают одинаковый результат
x.equals(null)всегдаfalse
3. hashCode()
Возвращает хеш-код объекта (для хеш-таблиц).
Важное правило: Если переопределяете equals(), ОБЯЗАТЕЛЬНО переопределите hashCode()!
@Override
public int hashCode() {
return Objects.hash(name, age);
}
4. getClass()
Возвращает класс объекта:
Dog dog = new Dog("Шарик", 3);
Class<?> clazz = dog.getClass();
System.out.println(clazz.getName()); // Dog
System.out.println(clazz.getSimpleName()); // Dog
Композиция vs Наследование
IS-A vs HAS-A
Наследование (IS-A): Собака ЯВЛЯЕТСЯ животным
class Dog extends Animal {
// Dog IS-A Animal
}
Композиция (HAS-A): Машина ИМЕЕТ двигатель
class Car {
private Engine engine; // Car HAS-A Engine
}
Когда использовать наследование?
✅ Используйте наследование если:
- Подкласс действительно является специализацией суперкласса
- Нужно переопределить поведение
- Отношение “IS-A” логично
- Классы тесно связаны
// ✅ Хорошо - Dog IS-A Animal
class Dog extends Animal { }
// ✅ Хорошо - ArrayList IS-A List
class ArrayList extends AbstractList { }
❌ НЕ используйте наследование если:
- Просто хотите переиспользовать код
- Отношение “HAS-A” больше подходит
- Нарушается принцип подстановки Лисков
// ❌ Плохо - Stack is not really an ArrayList
class Stack extends ArrayList {
// Наследует много ненужных методов
}
// ✅ Лучше - композиция
class Stack {
private List<Object> elements = new ArrayList<>();
public void push(Object item) {
elements.add(item);
}
public Object pop() {
return elements.remove(elements.size() - 1);
}
}
Проблемы наследования
1. Хрупкость базового класса
Изменения в родителе могут сломать потомков.
2. Жёсткая связанность
Потомок сильно зависит от родителя.
3. Нарушение инкапсуляции
Потомок должен знать детали реализации родителя.
Преимущества композиции
1. Гибкость
class Car {
private Engine engine;
// Можем легко заменить двигатель!
public void setEngine(Engine engine) {
this.engine = engine;
}
}
2. Слабая связанность
3. Следование принципу “Program to interface”
Правило
“Предпочитайте композицию наследованию”
— Joshua Bloch, “Effective Java”
Но это не значит “никогда не используйте наследование”! Используйте наследование для истинных IS-A отношений.
Лучшие практики
1. Используйте @Override
@Override
public void method() {
// компилятор проверит корректность
}
2. Делайте методы final если не планируете переопределение
public final void criticalMethod() {
// нельзя переопределить
}
3. Делайте классы final или проектируйте для наследования
// Либо запрещаем наследование
public final class ImmutableClass { }
// Либо документируем и проектируем для расширения
public class ExtensibleClass {
/**
* Hook method for subclasses
*/
protected void hook() { }
}
4. Не вызывайте переопределяемые методы в конструкторе
// ❌ ОПАСНО!
public class Parent {
public Parent() {
init(); // переопределяемый метод!
}
public void init() { }
}
public class Child extends Parent {
private String data = "initialized";
@Override
public void init() {
System.out.println(data); // может быть null!
}
}
5. Используйте protected для методов расширения
public class Base {
protected void extensionPoint() {
// для переопределения в потомках
}
}
Итоги
Вы освоили наследование и полиморфизм!
Ключевые концепции:
- Наследование - переиспользование кода через extends
- super - доступ к родителю
- Переопределение - изменение поведения в потомках
- Полиморфизм - один интерфейс, много реализаций
- instanceof - проверка типа
- Object - корень всей иерархии
- Композиция vs Наследование - выбор правильного инструмента
Почему это важно:
- Переиспользование кода
- Гибкость через полиморфизм
- Организация классов в иерархии
- Расширяемость программ
Задания для практики
-
Иерархия транспорта:
- Базовый класс Vehicle
- Подклассы: Car, Motorcycle, Truck
- Переопределение методов
- Полиморфный массив
-
Фигуры:
- Базовый класс Shape
- Подклассы: Circle, Rectangle, Triangle
- Метод getArea() в каждом
- Сравнение фигур по площади
-
Сотрудники:
- Базовый Employee
- Подклассы: Manager, Developer, Designer
- Разный расчёт зарплаты
- equals() и hashCode()
Удачи!
Источники:
- Oracle Java Tutorial - Inheritance
- Oracle Java Tutorial - Polymorphism
- Oracle Java Tutorial - Object Class
1.10. Интерфейсы и абстрактные классы
interface, abstract class, default methods
Материалы
Интерфейсы и абстрактные классы
Добро пожаловать в третью часть ООП! Здесь мы изучим мощные инструменты абстракции: абстрактные классы для неполной реализации и интерфейсы для контрактов поведения.
Абстракция в программировании
Абстракция - это сокрытие деталей реализации и показ только важной информации. Это четвёртый столп ООП!
Аналогия из жизни
Когда вы водите машину:
- Видите: руль, педали, приборную панель
- НЕ видите: работу двигателя, трансмиссии, электроники
Абстракция скрывает “как это работает”, показывая только “что это делает”.
Зачем нужна абстракция?
1. Упрощение Работаем с простым интерфейсом, не вникая в детали.
2. Гибкость Можем менять реализацию, не трогая код, который её использует.
3. Организация Определяем общую структуру без конкретики.
Абстрактные классы
Абстрактный класс - это класс, который:
- Не может быть создан напрямую (нельзя
new AbstractClass()) - Может содержать абстрактные методы (без реализации)
- Может содержать обычные методы (с реализацией)
- Служит шаблоном для подклассов
Синтаксис
public abstract class AbstractClassName {
// Обычные поля
private int field;
// Конструктор
public AbstractClassName() { }
// Абстрактный метод (нет реализации!)
public abstract void abstractMethod();
// Обычный метод (есть реализация)
public void normalMethod() {
System.out.println("Обычный метод");
}
}
Первый пример
// Абстрактный класс Shape
public abstract class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
// Абстрактные методы - ОБЯЗАТЕЛЬНЫ к реализации
public abstract double getArea();
public abstract double getPerimeter();
// Обычный метод - общий для всех фигур
public void displayColor() {
System.out.println("Цвет: " + color);
}
public void displayInfo() {
System.out.println("Фигура цвета " + color);
System.out.println("Площадь: " + getArea());
System.out.println("Периметр: " + getPerimeter());
}
}
// Конкретный класс Circle
public class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
// ОБЯЗАНЫ реализовать абстрактные методы!
@Override
public double getArea() {
return Math.PI * radius * radius;
}
@Override
public double getPerimeter() {
return 2 * Math.PI * radius;
}
}
// Конкретный класс Rectangle
public class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(String color, double width, double height) {
super(color);
this.width = width;
this.height = height;
}
@Override
public double getArea() {
return width * height;
}
@Override
public double getPerimeter() {
return 2 * (width + height);
}
}
// Использование
public class Main {
public static void main(String[] args) {
// Shape shape = new Shape("red"); // ОШИБКА! Абстрактный класс
Shape circle = new Circle("красный", 5);
Shape rectangle = new Rectangle("синий", 4, 6);
circle.displayInfo();
System.out.println();
rectangle.displayInfo();
// Полиморфизм!
Shape[] shapes = {circle, rectangle};
for (Shape shape : shapes) {
System.out.println("Площадь: " + shape.getArea());
}
}
}
Вывод:
Фигура цвета красный
Площадь: 78.53981633974483
Периметр: 31.41592653589793
Фигура цвета синий
Площадь: 24.0
Периметр: 20.0
Площадь: 78.53981633974483
Площадь: 24.0
Правила абстрактных классов
✅ Могут:
- Иметь абстрактные и обычные методы
- Иметь поля (любые: public, private, protected)
- Иметь конструкторы
- Иметь static методы и поля
- Иметь final методы
❌ Нельзя:
- Создать экземпляр напрямую
- Объявить abstract final (противоречие!)
- Объявить abstract static (бессмысленно)
⚠️ Наследники должны:
- Реализовать ВСЕ абстрактные методы
- ИЛИ сами быть абстрактными
abstract class Parent {
abstract void method1();
abstract void method2();
}
// Вариант 1: Реализуем все методы
class Child1 extends Parent {
@Override
void method1() { }
@Override
void method2() { }
}
// Вариант 2: Не реализуем всё - класс тоже абстрактный
abstract class Child2 extends Parent {
@Override
void method1() { }
// method2() не реализован - Child2 тоже abstract
}
Когда использовать абстрактные классы?
✅ Используйте когда:
- Есть общий код для подклассов
- Нужны поля и конструкторы
- Часть методов реализована, часть - нет
- Классы тесно связаны (IS-A отношение)
// ✅ Хорошо - общая логика животных
abstract class Animal {
protected String name; // общее поле
public Animal(String name) { // конструктор
this.name = name;
}
public void breathe() { // общая реализация
System.out.println(name + " дышит");
}
public abstract void makeSound(); // специфично для каждого
}
Интерфейсы
Интерфейс - это контракт, который определяет что класс должен уметь делать, но не как.
Базовый синтаксис
public interface InterfaceName {
// Константы (public static final автоматически)
int CONSTANT = 10;
// Абстрактные методы (public abstract автоматически)
void method1();
int method2(String param);
// Default методы (Java 8+)
default void defaultMethod() {
System.out.println("Default implementation");
}
// Static методы (Java 8+)
static void staticMethod() {
System.out.println("Static method");
}
}
Первый пример
// Интерфейс - контракт поведения
public interface Flyable {
// Абстрактные методы (public abstract автоматически)
void fly();
void land();
// Константа (public static final автоматически)
int MAX_ALTITUDE = 10000;
}
public interface Swimmable {
void swim();
void dive();
}
// Класс может реализовать НЕСКОЛЬКО интерфейсов!
public class Duck implements Flyable, Swimmable {
private String name;
public Duck(String name) {
this.name = name;
}
// Реализуем методы Flyable
@Override
public void fly() {
System.out.println(name + " летит на высоте " + Flyable.MAX_ALTITUDE);
}
@Override
public void land() {
System.out.println(name + " приземляется на воду");
}
// Реализуем методы Swimmable
@Override
public void swim() {
System.out.println(name + " плывёт");
}
@Override
public void dive() {
System.out.println(name + " ныряет за рыбой");
}
}
public class Airplane implements Flyable {
private String model;
public Airplane(String model) {
this.model = model;
}
@Override
public void fly() {
System.out.println(model + " взлетает");
}
@Override
public void land() {
System.out.println(model + " садится на взлётную полосу");
}
}
public class Fish implements Swimmable {
private String species;
public Fish(String species) {
this.species = species;
}
@Override
public void swim() {
System.out.println(species + " плывёт в воде");
}
@Override
public void dive() {
System.out.println(species + " ныряет глубже");
}
}
Использование
public class Main {
public static void main(String[] args) {
Duck duck = new Duck("Дональд");
Airplane plane = new Airplane("Boeing 747");
Fish fish = new Fish("Карп");
System.out.println("=== Летающие ===");
// Полиморфизм с интерфейсами!
Flyable[] flyers = {duck, plane};
for (Flyable flyer : flyers) {
flyer.fly();
flyer.land();
}
System.out.println("\n=== Плавающие ===");
Swimmable[] swimmers = {duck, fish};
for (Swimmable swimmer : swimmers) {
swimmer.swim();
swimmer.dive();
}
}
}
Вывод:
=== Летающие ===
Дональд летит на высоте 10000
Дональд приземляется на воду
Boeing 747 взлетает
Boeing 747 садится на взлётную полосу
=== Плавающие ===
Дональд плывёт
Дональд ныряет за рыбой
Карп плывёт в воде
Карп ныряет глубже
Множественная реализация
В отличие от классов (одиночное наследование), можно реализовать НЕСКОЛЬКО интерфейсов:
// ✅ OK - много интерфейсов
class Duck implements Flyable, Swimmable, Audible {
// ...
}
// ❌ ОШИБКА - только один родительский класс
class C extends A, B { // ОШИБКА!
}
// ✅ OK - один класс + много интерфейсов
class Duck extends Bird implements Flyable, Swimmable {
// ...
}
Правила интерфейсов
Все поля:
- Автоматически
public static final(константы)
interface Constants {
int MAX = 100; // = public static final int MAX = 100;
}
Все методы (до Java 8):
- Автоматически
public abstract
interface Old {
void method(); // = public abstract void method();
}
Java 8+ добавила:
- Default методы (с реализацией)
- Static методы
Java 9+ добавила:
- Private методы (для default методов)
❌ Нельзя:
- Создать экземпляр (
new Interface()) - Иметь конструкторы
- Иметь instance поля (кроме констант)
- Объявить final методы
Default методы (Java 8+)
Default методы позволяют добавлять новые методы в интерфейсы без поломки существующего кода.
Синтаксис
public interface Vehicle {
// Обычные абстрактные методы
void start();
void stop();
// Default метод - уже реализован!
default void honk() {
System.out.println("Бип-бип!");
}
default void displayInfo() {
System.out.println("Это транспортное средство");
}
}
public class Car implements Vehicle {
@Override
public void start() {
System.out.println("Машина заводится");
}
@Override
public void stop() {
System.out.println("Машина останавливается");
}
// honk() уже реализован в интерфейсе!
// Можем использовать как есть или переопределить:
@Override
public void honk() {
System.out.println("Машина сигналит: БИП-БИП!");
}
}
Car car = new Car();
car.start(); // Машина заводится
car.honk(); // Машина сигналит: БИП-БИП!
car.displayInfo(); // Это транспортное средство (из интерфейса)
Зачем нужны default методы?
1. Эволюция интерфейсов
Добавляем новый метод без поломки существующих реализаций:
// До Java 8
interface List<E> {
void add(E element);
E get(int index);
// Всё, добавить метод = сломать все реализации
}
// Java 8+
interface List<E> {
void add(E element);
E get(int index);
// Новый метод - не ломает старый код!
default void sort(Comparator<E> c) {
// реализация
}
}
2. Общая реализация
Предоставляем реализацию по умолчанию, которую можно переопределить при необходимости.
Конфликт default методов
Что если класс реализует два интерфейса с одинаковыми default методами?
interface A {
default void method() {
System.out.println("A");
}
}
interface B {
default void method() {
System.out.println("B");
}
}
class C implements A, B {
// ОШИБКА КОМПИЛЯЦИИ! Неоднозначность
// Должны явно разрешить конфликт:
@Override
public void method() {
A.super.method(); // вызываем метод из A
// или
B.super.method(); // вызываем метод из B
// или
System.out.println("C"); // своя реализация
}
}
Static методы в интерфейсах (Java 8+)
Интерфейсы могут иметь static методы:
public interface MathUtils {
// Static методы - вызываются через имя интерфейса
static int add(int a, int b) {
return a + b;
}
static int multiply(int a, int b) {
return a * b;
}
static double average(int... numbers) {
return Arrays.stream(numbers).average().orElse(0);
}
}
// Использование - через имя интерфейса
int sum = MathUtils.add(5, 3);
int product = MathUtils.multiply(4, 7);
double avg = MathUtils.average(1, 2, 3, 4, 5);
Зачем static методы в интерфейсах?
1. Утилитные методы
interface StringUtils {
static boolean isEmpty(String str) {
return str == null || str.isEmpty();
}
static String reverse(String str) {
return new StringBuilder(str).reverse().toString();
}
}
2. Фабричные методы
interface Animal {
void makeSound();
static Animal createDog(String name) {
return new Dog(name);
}
static Animal createCat(String name) {
return new Cat(name);
}
}
Private методы в интерфейсах (Java 9+)
Private методы для переиспользования кода внутри default методов:
public interface Logger {
// Private helper метод
private String getTimestamp() {
return LocalDateTime.now().toString();
}
// Default методы используют private
default void logInfo(String message) {
System.out.println("[INFO] " + getTimestamp() + ": " + message);
}
default void logError(String message) {
System.err.println("[ERROR] " + getTimestamp() + ": " + message);
}
}
Абстрактный класс vs Интерфейс
Сравнительная таблица
| Признак | Абстрактный класс | Интерфейс |
|---|---|---|
| Ключевое слово | abstract class | interface |
| Наследование | extends (один) | implements (много) |
| Поля | Любые | Только константы |
| Конструкторы | Есть | Нет |
| Методы | Любые | abstract, default, static |
| Модификаторы | Любые | Только public |
| Множественное | Нет | Да |
Когда что использовать?
Используйте абстрактный класс:
✅ Когда классы тесно связаны (IS-A)
// ✅ Хорошо - общая база для животных
abstract class Animal {
protected String name;
protected int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void breathe() {
System.out.println("Дышит");
}
public abstract void makeSound();
}
✅ Когда нужны поля и конструкторы
✅ Когда нужны protected/private члены
✅ Когда большая часть реализации общая
Используйте интерфейс:
✅ Для определения контракта/способностей
// ✅ Хорошо - способность летать
interface Flyable {
void fly();
void land();
}
// Разные классы могут летать: Bird, Plane, Drone
✅ Когда нужна множественная реализация
class Duck implements Flyable, Swimmable, Audible {
// утка может летать, плавать и издавать звуки
}
✅ Для слабой связанности
✅ Для API и плагинов
Правило большого пальца
-
Интерфейс = CAN-DO (может делать, способности)
Flyable,Comparable,Serializable,Runnable
-
Абстрактный класс = IS-A (является чем-то, сущность)
Animal,Shape,Vehicle,Document
Пример: Правильное использование
// Сущность - абстрактный класс
abstract class Employee {
protected String name;
protected String id;
protected double baseSalary;
public Employee(String name, String id, double baseSalary) {
this.name = name;
this.id = id;
this.baseSalary = baseSalary;
}
public abstract double calculateSalary();
}
// Способности - интерфейсы
interface Bonusable {
void awardBonus(double amount);
double getTotalBonus();
}
interface Promotable {
void promote(String newPosition);
String getPosition();
}
// Конкретная реализация
class Manager extends Employee implements Bonusable, Promotable {
private double totalBonus;
private String position;
public Manager(String name, String id, double baseSalary) {
super(name, id, baseSalary);
this.position = "Manager";
}
@Override
public double calculateSalary() {
return baseSalary + totalBonus;
}
@Override
public void awardBonus(double amount) {
totalBonus += amount;
}
@Override
public double getTotalBonus() {
return totalBonus;
}
@Override
public void promote(String newPosition) {
this.position = newPosition;
}
@Override
public String getPosition() {
return position;
}
}
Практический пример: Платёжная система
Применим всё изученное:
// Интерфейс - контракт для оплаты
public interface Payable {
boolean processPayment(double amount);
String getPaymentMethod();
}
// Интерфейс - можно вернуть деньги
public interface Refundable {
boolean refund(double amount);
}
// Абстрактный класс - базовая логика
public abstract class Payment implements Payable {
protected double balance;
protected String owner;
public Payment(String owner, double balance) {
this.owner = owner;
this.balance = balance;
}
// Общая логика проверки
protected boolean hasEnoughBalance(double amount) {
return balance >= amount;
}
protected void logTransaction(String operation, double amount) {
System.out.println("[" + getPaymentMethod() + "] " +
owner + ": " + operation + " " + amount);
}
public double getBalance() {
return balance;
}
}
// Конкретная реализация - банковская карта
public class CreditCard extends Payment implements Refundable {
private String cardNumber;
public CreditCard(String owner, String cardNumber, double balance) {
super(owner, balance);
this.cardNumber = maskCardNumber(cardNumber);
}
@Override
public boolean processPayment(double amount) {
if (amount <= 0) {
System.out.println("Некорректная сумма");
return false;
}
if (!hasEnoughBalance(amount)) {
System.out.println("Недостаточно средств");
return false;
}
balance -= amount;
logTransaction("Оплата", amount);
return true;
}
@Override
public boolean refund(double amount) {
balance += amount;
logTransaction("Возврат", amount);
return true;
}
@Override
public String getPaymentMethod() {
return "CreditCard " + cardNumber;
}
private String maskCardNumber(String number) {
if (number.length() < 4) return number;
return "**** **** **** " + number.substring(number.length() - 4);
}
}
// Конкретная реализация - электронный кошелёк
public class EWallet extends Payment implements Refundable {
private String email;
public EWallet(String owner, String email, double balance) {
super(owner, balance);
this.email = email;
}
@Override
public boolean processPayment(double amount) {
if (amount <= 0) {
System.out.println("Некорректная сумма");
return false;
}
// E-wallet может уйти в минус (кредит)
balance -= amount;
logTransaction("Оплата", amount);
return true;
}
@Override
public boolean refund(double amount) {
balance += amount;
logTransaction("Возврат", amount);
return true;
}
@Override
public String getPaymentMethod() {
return "E-Wallet (" + email + ")";
}
}
// Конкретная реализация - наличные (без возврата)
public class Cash extends Payment {
public Cash(String owner, double balance) {
super(owner, balance);
}
@Override
public boolean processPayment(double amount) {
if (!hasEnoughBalance(amount)) {
System.out.println("Недостаточно наличных");
return false;
}
balance -= amount;
logTransaction("Оплата наличными", amount);
return true;
}
@Override
public String getPaymentMethod() {
return "Cash";
}
}
// Использование
public class Shop {
public void checkout(Payable payment, double amount) {
System.out.println("\n=== Оформление заказа на " + amount + " руб. ===");
System.out.println("Метод оплаты: " + payment.getPaymentMethod());
if (payment.processPayment(amount)) {
System.out.println("✓ Оплата успешна!");
// Если поддерживается возврат
if (payment instanceof Refundable) {
System.out.println("(Возврат возможен)");
}
} else {
System.out.println("✗ Оплата не прошла");
}
}
public static void main(String[] args) {
Shop shop = new Shop();
Payable card = new CreditCard("Анна", "1234567890123456", 5000);
Payable wallet = new EWallet("Боб", "bob@example.com", 3000);
Payable cash = new Cash("Карл", 1000);
shop.checkout(card, 2000);
shop.checkout(wallet, 4000);
shop.checkout(cash, 500);
shop.checkout(cash, 1000); // недостаточно средств
// Возврат
if (card instanceof Refundable) {
((Refundable) card).refund(1000);
}
}
}
Лучшие практики
1. Интерфейсы для контрактов
// ✅ Хорошо - чёткий контракт
interface Repository<T> {
void save(T entity);
T findById(int id);
void delete(int id);
}
2. Абстрактные классы для общей базы
// ✅ Хорошо - общая реализация
abstract class BaseRepository<T> {
protected Connection connection;
public BaseRepository(Connection connection) {
this.connection = connection;
}
protected void log(String message) {
System.out.println("[DB] " + message);
}
}
3. Называйте интерфейсы по способностям
// ✅ Хорошо
interface Serializable { }
interface Comparable<T> { }
interface Runnable { }
interface Flyable { }
// ❌ Плохо - не отражает способность
interface AnimalInterface { }
4. Не создавайте пустые маркерные интерфейсы
// ❌ Плохо (устарело)
interface Marker { }
// ✅ Лучше - используйте аннотации
@Marker
class MyClass { }
5. Используйте default методы осторожно
Default методы удобны, но могут усложнить иерархию. Используйте когда действительно нужна общая реализация.
6. Проектируйте для расширения или запрещайте его
// Либо final
public final class Utility { }
// Либо документируем для расширения
/**
* This class is designed for inheritance.
* Override hook() method to customize behavior.
*/
public abstract class Extensible {
protected void hook() { }
}
Итоги
Вы освоили интерфейсы и абстрактные классы!
Ключевые концепции:
- Абстрактные классы - неполная реализация, шаблон для потомков
- Интерфейсы - контракты поведения
- Default методы - реализация в интерфейсах
- Множественная реализация - через интерфейсы
- Абстракция - сокрытие деталей, показ только важного
- Правильный выбор - когда класс, когда интерфейс
Почему это важно:
- Определяем контракты без привязки к реализации
- Слабая связанность через интерфейсы
- Организация кода через абстракцию
- Множественное “наследование” через интерфейсы
Золотое правило:
- Интерфейс для способностей (CAN-DO)
- Абстрактный класс для сущностей (IS-A)
- Предпочитайте интерфейсы для большей гибкости
Задания для практики
-
Медиаплеер:
- Интерфейс Playable
- Абстрактный класс MediaFile
- Классы: AudioFile, VideoFile, StreamFile
- Методы: play(), pause(), stop()
-
Система уведомлений:
- Интерфейс Notifiable
- Классы: EmailNotifier, SMSNotifier, PushNotifier
- Default методы для форматирования
- Static методы-фабрики
-
Коллекция фигур:
- Абстрактный Shape с площадью
- Интерфейсы: Drawable, Resizable, Rotatable
- Разные фигуры реализуют разные интерфейсы
- Полиморфная обработка
-
Игровые персонажи:
- Абстрактный Character
- Интерфейсы: Attackable, Healable, Movable
- Разные персонажи с разными способностями
Удачи в программировании! Теперь вы знаете все основы ООП.
Источники:
- Oracle Java Tutorial - Abstract Classes
- Oracle Java Tutorial - Interfaces
- Oracle Java Tutorial - Default Methods
2. Java Core
Глубокое погружение в ядро языка
В этом разделе рассматриваются ключевые компоненты Java: Collections Framework, Generics, Stream API и другие важные API.
Содержание раздела
- 2.1. Collections Framework
- 2.2. Generics
- 2.3. Stream API
- 2.4. Optional
- 2.5. Функциональные интерфейсы и лямбды
- 2.6. Date/Time API
- 2.7. Исключения
- 2.8. I/O и NIO
- 2.9. Reflection API
- 2.10. Аннотации
2.1. Collections Framework
Коллекции Java
Содержание
2.1.1. List, Set, Queue
ArrayList, LinkedList, HashSet, TreeSet, PriorityQueue
Материалы
Иерархия интерфейсов коллекций
Collection (интерфейс)
├── List
├── Set
│ ├── SortedSet
│ └── NavigableSet
└── Queue
├── Deque
├── BlockingQueue
├── TransferQueue
└── BlockingDeque
List - Упорядоченная коллекция
Основные характеристики
- Порядок элементов: сохраняется порядок добавления
- Дубликаты: разрешены
- Индексация: доступ по индексу (0-based)
- Null-элементы: обычно разрешены
Реализации
ArrayList
- Структура данных: Resizable Array (динамический массив)
- Доступ по индексу: O(1) - быстрый random access
- Вставка/удаление в конце: O(1) амортизированное
- Вставка/удаление в середине: O(n) - требует сдвига элементов
- Использование памяти: компактное, но может быть перевыделение
- Когда использовать: частый доступ по индексу, редкие вставки в середину
List<String> arrayList = new ArrayList<>();
arrayList.add("first");
arrayList.add("second");
String element = arrayList.get(0); // O(1)
LinkedList
- Структура данных: Doubly-linked list (двусвязный список)
- Доступ по индексу: O(n) - нужно пройти по списку
- Вставка/удаление в начале/конце: O(1)
- Вставка/удаление в середине: O(1) если есть итератор, O(n) если нужно найти позицию
- Использование памяти: больше накладных расходов (ссылки на prev/next)
- Когда использовать: частые вставки/удаления, использование как Queue/Deque
List<String> linkedList = new LinkedList<>();
linkedList.add("first");
linkedList.add(0, "new first"); // Вставка в начало - O(1)
Ключевые методы List
void add(int index, E element) // Вставка по индексу
E get(int index) // Получение по индексу
E set(int index, E element) // Замена элемента
E remove(int index) // Удаление по индексу
int indexOf(Object o) // Поиск индекса элемента
List<E> subList(int from, int to) // Подсписок (view)
Random Access vs Sequential Access
- Random Access (ArrayList): поддерживает маркер-интерфейс
RandomAccess - Sequential Access (LinkedList): итерация эффективнее индексированного доступа
Set - Коллекция уникальных элементов
Основные характеристики
- Уникальность: не допускает дубликаты
- Порядок: зависит от реализации
- Null-элементы: зависит от реализации
- Базируется на: обычно equals() и hashCode()
Реализации
HashSet
- Структура данных: Hash Table (хеш-таблица)
- Порядок: не гарантируется
- Производительность: O(1) для add, remove, contains (в среднем)
- Null-элементы: один null разрешен
- Когда использовать: быстрая проверка наличия элемента, порядок не важен
Set<String> hashSet = new HashSet<>();
hashSet.add("apple");
hashSet.add("banana");
hashSet.add("apple"); // Не добавится - дубликат
boolean contains = hashSet.contains("apple"); // O(1)
LinkedHashSet
- Структура данных: Hash Table + Linked List
- Порядок: сохраняется порядок добавления (insertion order)
- Производительность: O(1) для основных операций, немного медленнее HashSet
- Null-элементы: один null разрешен
- Когда использовать: нужна уникальность + предсказуемый порядок итерации
Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("first");
linkedHashSet.add("second");
// Итерация будет в порядке добавления
TreeSet
- Структура данных: Balanced Tree (красно-черное дерево)
- Порядок: отсортированный (natural ordering или Comparator)
- Производительность: O(log n) для add, remove, contains
- Null-элементы: не разрешены (с Java 7)
- Интерфейсы: реализует NavigableSet и SortedSet
- Когда использовать: нужна отсортированная коллекция уникальных элементов
Set<Integer> treeSet = new TreeSet<>();
treeSet.add(5);
treeSet.add(2);
treeSet.add(8);
// Итерация: 2, 5, 8 (отсортировано)
NavigableSet методы (TreeSet)
E lower(E e) // Наибольший элемент < e
E floor(E e) // Наибольший элемент <= e
E ceiling(E e) // Наименьший элемент >= e
E higher(E e) // Наименьший элемент > e
E pollFirst() // Удалить и вернуть первый
E pollLast() // Удалить и вернуть последний
NavigableSet<E> descendingSet() // Обратный порядок
Queue - Очередь (FIFO)
Основные характеристики
- Порядок обработки: обычно FIFO (First-In-First-Out)
- Операции: добавление в хвост, извлечение из головы
- Два набора методов: throwing exceptions vs returning special values
Методы Queue
| Операция | Throws Exception | Returns Special Value |
|---|---|---|
| Insert | add(e) | offer(e) → boolean |
| Remove | remove() | poll() → null if empty |
| Examine | element() | peek() → null if empty |
Queue<String> queue = new LinkedList<>();
queue.offer("first");
queue.offer("second");
String head = queue.poll(); // "first", удаляет из очереди
String next = queue.peek(); // "second", не удаляет
Реализации Queue
LinkedList (как Queue)
- Двусвязный список, эффективен для Queue операций
- Поддерживает null-элементы
ArrayDeque
- Структура данных: Resizable Array (циклический массив)
- Производительность: быстрее LinkedList для Queue операций
- Null-элементы: не разрешены
- Рекомендуется для Stack и Queue вместо Stack и LinkedList
Queue<String> queue = new ArrayDeque<>();
queue.offer("task1");
queue.offer("task2");
PriorityQueue
- Структура данных: Binary Heap (куча)
- Порядок: основан на natural ordering или Comparator
- Производительность: O(log n) для offer и poll, O(1) для peek
- Null-элементы: не разрешены
- Не FIFO: элементы извлекаются по приоритету
Queue<Integer> pq = new PriorityQueue<>();
pq.offer(5);
pq.offer(2);
pq.offer(8);
pq.poll(); // 2 (наименьший элемент)
Deque - Double-Ended Queue
Основные характеристики
- Двусторонняя очередь: добавление/удаление с обоих концов
- Может работать как FIFO (Queue) или LIFO (Stack)
Методы Deque
| Операция | First Element (Head) | Last Element (Tail) |
|---|---|---|
| Insert | addFirst(e) / offerFirst(e) | addLast(e) / offerLast(e) |
| Remove | removeFirst() / pollFirst() | removeLast() / pollLast() |
| Examine | getFirst() / peekFirst() | getLast() / peekLast() |
Stack операции (через Deque)
Deque<String> stack = new ArrayDeque<>();
stack.push("first"); // addFirst
stack.push("second"); // addFirst
stack.pop(); // removeFirst → "second"
stack.peek(); // peekFirst → "first"
Реализации Deque
- ArrayDeque: рекомендуется (быстрее, без null)
- LinkedList: поддерживает null
Concurrent Collections
BlockingQueue
- Назначение: потокобезопасная очередь с блокировкой
- Методы:
put(e)блокируется если очередь полная,take()блокируется если пустая
Реализации
- ArrayBlockingQueue: ограниченная очередь на массиве
- LinkedBlockingQueue: опционально ограниченная на linked nodes
- PriorityBlockingQueue: неограниченная очередь с приоритетами
- SynchronousQueue: нет внутреннего хранилища, каждая вставка ждет извлечения
BlockingDeque
- LinkedBlockingDeque: потокобезопасная двусторонняя очередь
Concurrent Set
- CopyOnWriteArraySet: для редких модификаций, частых чтений
- ConcurrentSkipListSet: потокобезопасная отсортированная set
Выбор реализации - Quick Reference
List
- ArrayList: по умолчанию, random access
- LinkedList: частые вставки/удаления, использование как Queue
Set
- HashSet: по умолчанию, максимальная скорость
- LinkedHashSet: предсказуемый порядок итерации
- TreeSet: отсортированная коллекция
Queue
- ArrayDeque: по умолчанию для Queue/Stack
- PriorityQueue: обработка по приоритету
- LinkedList: когда нужны null-элементы
Concurrent
- ConcurrentHashMap: потокобезопасная Map
- CopyOnWriteArrayList: редкие изменения, частые чтения
- BlockingQueue реализации: producer-consumer паттерн
Важные концепции
Modifiability
- Unmodifiable: нельзя изменять (add, remove, clear выбросят UnsupportedOperationException)
- Modifiable: можно изменять
- Immutable: неизменяемые, гарантия что объект не изменится
- Mutable: изменяемые
Создание unmodifiable коллекций
List<String> list = List.of("a", "b", "c"); // Java 9+, immutable
Set<String> set = Set.of("a", "b"); // Java 9+, immutable
// Из существующей коллекции
List<String> unmod = Collections.unmodifiableList(originalList);
Fail-Fast Iterators
- Большинство коллекций используют fail-fast итераторы
- Бросают
ConcurrentModificationExceptionпри модификации во время итерации - Исключение: можно использовать
iterator.remove()
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
list.remove(s); // ConcurrentModificationException!
}
// Правильно:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (condition) {
it.remove(); // OK
}
}
RandomAccess маркер
if (list instanceof RandomAccess) {
// Используем индексированный доступ
for (int i = 0; i < list.size(); i++) {
process(list.get(i));
}
} else {
// Используем итератор
for (String s : list) {
process(s);
}
}
Performance Характеристики
| Операция | ArrayList | LinkedList | HashSet | TreeSet |
|---|---|---|---|---|
| add | O(1)* | O(1) | O(1) | O(log n) |
| get(i) | O(1) | O(n) | N/A | N/A |
| contains | O(n) | O(n) | O(1) | O(log n) |
| remove | O(n) | O(n)** | O(1) | O(log n) |
| iterate | O(n) | O(n) | O(n) | O(n) |
* Амортизированное O(1), иногда O(n) при расширении массива
** O(1) если есть итератор на элемент
Типичные ошибки и best practices
1. Размер коллекции
// Плохо - много перевыделений
List<String> list = new ArrayList<>(); // capacity = 10
for (int i = 0; i < 10000; i++) {
list.add("item" + i);
}
// Хорошо - указываем ожидаемый размер
List<String> list = new ArrayList<>(10000);
2. Выбор между equals и ==
// Set использует equals() для проверки уникальности
// Нужно правильно переопределить equals() и hashCode()
class Person {
String name;
int age;
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
}
3. Не использовать устаревшие классы
- ❌
Vector→ ✅ArrayListилиCopyOnWriteArrayList - ❌
Stack→ ✅ArrayDeque - ❌
Hashtable→ ✅HashMapилиConcurrentHashMap
4. Синхронизация
// Для thread-safe коллекций используй concurrent пакет
Map<String, String> map = new ConcurrentHashMap<>();
// Или синхронизированные обертки (но concurrent лучше)
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Итоги
- List - упорядоченная коллекция с индексами и дубликатами
- Set - коллекция уникальных элементов (HashSet, TreeSet, LinkedHashSet)
- Queue - очередь FIFO, Deque - двусторонняя очередь
- ArrayList - по умолчанию для List (O(1) доступ по индексу)
- HashSet - по умолчанию для Set (O(1) операции)
- ArrayDeque - по умолчанию для Queue/Stack (быстрее LinkedList)
- Правильно переопределяй equals/hashCode для объектов в Set
- Используй concurrent коллекции для многопоточности
- Указывай initial capacity если знаешь размер заранее
- Избегай устаревших классов (Vector, Stack, Hashtable)
Задания для практики
-
ArrayList vs LinkedList: Создай список из 100000 элементов. Сравни время вставки в начало и в конец для ArrayList и LinkedList.
-
HashSet для уникальности: Дан массив строк с дубликатами. Получи список уникальных строк, сохраняя порядок первого появления.
-
PriorityQueue: Реализуй систему обработки задач с приоритетом. Task(name, priority). Задачи должны обрабатываться в порядке убывания приоритета.
-
Deque как стек: Реализуй проверку сбалансированности скобок в строке “(())” используя ArrayDeque.
-
Fail-fast итератор: Продемонстрируй ConcurrentModificationException и покажи правильный способ удаления элементов во время итерации.
2.1.2. Map и её реализации
HashMap, TreeMap, LinkedHashMap, ConcurrentHashMap
Материалы
Что такое Map
Map - это интерфейс для хранения пар ключ-значение (key-value).
- Каждый ключ уникален (не может повторяться)
- Каждый ключ соответствует максимум одному значению
- Map НЕ является коллекцией (не наследует Collection), но содержит collection-view операции
interface Map<K, V> {
V put(K key, V value);
V get(Object key);
V remove(Object key);
boolean containsKey(Object key);
boolean containsValue(Object value);
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K, V>> entrySet();
}
Иерархия Map интерфейсов
Map<K, V>
├── SortedMap<K, V>
│ └── NavigableMap<K, V>
└── ConcurrentMap<K, V>
└── ConcurrentNavigableMap<K, V>
HashMap - основная реализация
Характеристики
- Структура данных: Hash Table (хеш-таблица с массивом и списками/деревьями)
- Порядок: НЕ гарантируется и может меняться
- Производительность: O(1) для get/put/remove (в среднем)
- Null: один null-ключ разрешен, null-значения разрешены
- Синхронизация: НЕ потокобезопасен
Внутреннее устройство (Java 8+)
HashMap = Array of Buckets
bucket[0] → Node → Node → ...
bucket[1] → TreeNode (если > 8 коллизий)
bucket[2] → Node
...
Важные параметры:
- initialCapacity (по умолчанию 16) - начальный размер массива
- loadFactor (по умолчанию 0.75) - коэффициент заполнения для расширения
- При достижении
size > capacity * loadFactorпроисходит rehashing (увеличение в 2 раза)
Пример использования
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
ages.put("Alice", 31); // Заменит предыдущее значение
Integer age = ages.get("Alice"); // 31
boolean hasKey = ages.containsKey("Bob"); // true
boolean hasValue = ages.containsValue(25); // true
ages.remove("Bob");
Итерация по HashMap
// 1. Через entrySet (рекомендуется)
for (Map.Entry<String, Integer> entry : ages.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " = " + value);
}
// 2. Через keySet
for (String key : ages.keySet()) {
Integer value = ages.get(key);
System.out.println(key + " = " + value);
}
// 3. Через values (только значения)
for (Integer value : ages.values()) {
System.out.println(value);
}
// 4. forEach (Java 8+)
ages.forEach((key, value) ->
System.out.println(key + " = " + value)
);
Когда использовать HashMap
✅ По умолчанию для Map ✅ Порядок не важен ✅ Нужна максимальная скорость ✅ Однопоточное использование
Производительность
| Операция | Средний случай | Худший случай |
|---|---|---|
| get | O(1) | O(n) → O(log n) (с Java 8) |
| put | O(1) | O(n) → O(log n) |
| remove | O(1) | O(n) → O(log n) |
| containsKey | O(1) | O(n) → O(log n) |
LinkedHashMap - HashMap с порядком
Характеристики
- Структура данных: Hash Table + Doubly-Linked List
- Порядок: сохраняется порядок вставки (insertion-order) или порядок доступа (access-order)
- Производительность: чуть медленнее HashMap из-за поддержки linked list
- Null: один null-ключ разрешен, null-значения разрешены
- Синхронизация: НЕ потокобезопасен
Два режима порядка
1. Insertion-Order (по умолчанию)
Map<String, Integer> map = new LinkedHashMap<>();
map.put("First", 1);
map.put("Second", 2);
map.put("Third", 3);
// Итерация в порядке добавления: First, Second, Third
for (String key : map.keySet()) {
System.out.println(key);
}
2. Access-Order (для LRU кэша)
Map<String, Integer> lruMap = new LinkedHashMap<>(16, 0.75f, true);
// ^^^^
// accessOrder=true
lruMap.put("A", 1);
lruMap.put("B", 2);
lruMap.put("C", 3);
lruMap.get("A"); // Доступ к "A" - перемещается в конец
// Порядок теперь: B, C, A (A в конце как последний использованный)
LRU Cache на LinkedHashMap
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int maxEntries;
public LRUCache(int maxEntries) {
super(16, 0.75f, true); // accessOrder = true
this.maxEntries = maxEntries;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxEntries; // Удалять самый старый при превышении
}
}
// Использование
LRUCache<String, String> cache = new LRUCache<>(3);
cache.put("1", "one");
cache.put("2", "two");
cache.put("3", "three");
cache.put("4", "four"); // "1" удалится автоматически
Когда использовать LinkedHashMap
✅ Нужен предсказуемый порядок итерации ✅ Реализация LRU/MRU кэша ✅ История операций в порядке выполнения ✅ Сериализация/десериализация с сохранением порядка
TreeMap - отсортированная Map
Характеристики
- Структура данных: Red-Black Tree (сбалансированное бинарное дерево)
- Порядок: отсортирован по ключам (natural ordering или Comparator)
- Производительность: O(log n) для всех основных операций
- Null: null-ключи запрещены (с Java 7), null-значения разрешены
- Синхронизация: НЕ потокобезопасен
- Интерфейсы: реализует NavigableMap и SortedMap
Natural Ordering (по умолчанию)
TreeMap<String, Integer> map = new TreeMap<>();
map.put("Charlie", 30);
map.put("Alice", 25);
map.put("Bob", 28);
// Итерация в алфавитном порядке: Alice, Bob, Charlie
for (String key : map.keySet()) {
System.out.println(key + " = " + map.get(key));
}
Custom Comparator
// Сортировка по убыванию
TreeMap<String, Integer> descendingMap = new TreeMap<>(
Comparator.reverseOrder()
);
// Сортировка по длине ключа
TreeMap<String, Integer> lengthMap = new TreeMap<>(
Comparator.comparing(String::length)
.thenComparing(Comparator.naturalOrder())
);
NavigableMap методы
Поиск ближайших элементов
TreeMap<Integer, String> scores = new TreeMap<>();
scores.put(100, "Alice");
scores.put(85, "Bob");
scores.put(90, "Charlie");
scores.put(95, "Dave");
// Навигационные методы
scores.lowerKey(90); // 85 (наибольший < 90)
scores.floorKey(90); // 90 (наибольший <= 90)
scores.ceilingKey(92); // 95 (наименьший >= 92)
scores.higherKey(90); // 95 (наименьший > 90)
scores.lowerEntry(90); // Entry(85, "Bob")
scores.higherEntry(90); // Entry(95, "Dave")
Диапазоны (subMap, headMap, tailMap)
TreeMap<Integer, String> map = new TreeMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.put(4, "four");
map.put(5, "five");
// Поддиапазон [2, 4)
NavigableMap<Integer, String> subMap = map.subMap(2, true, 4, false);
// {2=two, 3=three}
// Все элементы < 3
SortedMap<Integer, String> headMap = map.headMap(3);
// {1=one, 2=two}
// Все элементы >= 3
SortedMap<Integer, String> tailMap = map.tailMap(3);
// {3=three, 4=four, 5=five}
Первый и последний элементы
Map.Entry<Integer, String> first = map.firstEntry(); // 1=one
Map.Entry<Integer, String> last = map.lastEntry(); // 5=five
Integer firstKey = map.firstKey(); // 1
Integer lastKey = map.lastKey(); // 5
// Удалить и вернуть
Map.Entry<Integer, String> removed = map.pollFirstEntry(); // 1=one
Обратный порядок
NavigableMap<Integer, String> descending = map.descendingMap();
// Итерация в обратном порядке: 5, 4, 3, 2, 1
NavigableSet<Integer> descendingKeys = map.descendingKeySet();
Когда использовать TreeMap
✅ Нужна автоматическая сортировка по ключам ✅ Нужны операции навигации (nearest, range) ✅ Диапазоны элементов (от X до Y) ✅ Нужны первый/последний элемент ✅ Подсчет элементов в диапазоне
Производительность TreeMap
| Операция | Сложность |
|---|---|
| get | O(log n) |
| put | O(log n) |
| remove | O(log n) |
| containsKey | O(log n) |
| firstKey | O(log n) |
| lastKey | O(log n) |
ConcurrentHashMap - потокобезопасная Map
Характеристики
- Структура данных: Segmented Hash Table (с Java 8 - Node array + CAS)
- Порядок: не гарантируется
- Производительность: высокая параллельность, lock-free чтение
- Null: null-ключи и null-значения ЗАПРЕЩЕНЫ
- Синхронизация: полностью потокобезопасен
Основные отличия от HashMap
// HashMap
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put(null, 1); // ✅ OK
hashMap.put("key", null); // ✅ OK
// ConcurrentHashMap
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put(null, 1); // ❌ NullPointerException
concurrentMap.put("key", null); // ❌ NullPointerException
Преимущества перед синхронизированной Map
// Старый подход - грубая синхронизация
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
synchronized (syncMap) {
syncMap.forEach((k, v) -> process(k, v)); // Блокирует всю map
}
// Современный подход - высокая параллельность
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.forEach((k, v) -> process(k, v)); // Не блокирует
Атомарные операции
putIfAbsent
// Добавить только если ключа нет
Integer oldValue = map.putIfAbsent("key", 100);
// null если ключа не было, старое значение если был
// Эквивалентно, но атомарно:
if (!map.containsKey("key")) {
map.put("key", 100);
}
computeIfAbsent
// Вычислить значение только если ключа нет
map.computeIfAbsent("key", k -> expensiveComputation());
// Типичный use case - кэширование
ConcurrentHashMap<String, User> userCache = new ConcurrentHashMap<>();
User user = userCache.computeIfAbsent(userId, id -> loadUserFromDB(id));
computeIfPresent
// Вычислить новое значение если ключ существует
map.computeIfPresent("key", (k, oldValue) -> oldValue + 1);
// null удалит entry
map.computeIfPresent("key", (k, v) -> null); // Удалит ключ
compute
// Вычислить значение независимо от наличия ключа
map.compute("key", (k, oldValue) ->
oldValue == null ? 1 : oldValue + 1
);
merge
// Объединить значения
map.merge("key", 1, Integer::sum);
// Если ключа нет - вставит 1
// Если ключ есть - прибавит 1 к старому значению
// Счетчик слов
words.forEach(word ->
wordCount.merge(word, 1, Integer::sum)
);
replace
// Заменить значение
Integer old = map.replace("key", newValue);
// Заменить только если текущее значение совпадает (CAS)
boolean replaced = map.replace("key", expectedOld, newValue);
remove с условием
// Удалить только если значение совпадает
boolean removed = map.remove("key", expectedValue);
Bulk операции (Java 8+)
forEach
// Параллельная итерация
map.forEach(1, (key, value) ->
System.out.println(key + " = " + value)
);
// ^ parallelismThreshold (1 = всегда параллельно)
search
// Поиск первого совпадения
String result = map.search(1, (key, value) ->
value > 100 ? key : null
);
reduce
// Суммирование значений
Integer sum = map.reduce(1,
(key, value) -> value, // transformer
0, // basis
Integer::sum // reducer
);
Когда использовать ConcurrentHashMap
✅ Многопоточная среда с частыми чтениями И записями ✅ Producer-Consumer с общим состоянием ✅ Кэш с множественным доступом ✅ Нужны атомарные операции (putIfAbsent, compute, merge) ✅ Высокая параллельность критична
Производительность ConcurrentHashMap
- Чтение: Lock-free, O(1)
- Запись: Segmented locking (с Java 8 - CAS), O(1)
- Итерация: Weakly consistent (может не видеть последние изменения)
Сравнительная таблица Map реализаций
| Характеристика | HashMap | LinkedHashMap | TreeMap | ConcurrentHashMap |
|---|---|---|---|---|
| Порядок | Нет | Вставки или доступа | Сортировка | Нет |
| get/put | O(1) | O(1) | O(log n) | O(1) |
| Null ключи | 1 null | 1 null | ❌ | ❌ |
| Null значения | ✅ | ✅ | ✅ | ❌ |
| Thread-safe | ❌ | ❌ | ❌ | ✅ |
| Память | Средне | Больше | Больше | Средне |
| Use case | По умолчанию | Порядок важен | Сортировка | Многопоточность |
Выбор реализации Map
Блок-схема выбора
Нужна потокобезопасность?
├─ Да → ConcurrentHashMap
└─ Нет
└─ Нужна сортировка?
├─ Да → TreeMap
└─ Нет
└─ Важен порядок вставки?
├─ Да → LinkedHashMap
└─ Нет → HashMap
Практические примеры
HashMap - стандартный кэш конфигурации
Map<String, String> config = new HashMap<>();
config.put("db.host", "localhost");
config.put("db.port", "5432");
LinkedHashMap - HTTP заголовки
Map<String, String> headers = new LinkedHashMap<>();
headers.put("Content-Type", "application/json");
headers.put("Authorization", "Bearer token");
// Порядок важен при отправке
TreeMap - телефонная книга
TreeMap<String, String> phoneBook = new TreeMap<>();
phoneBook.put("Alice", "+1234");
phoneBook.put("Bob", "+5678");
phoneBook.put("Charlie", "+9012");
// Все имена от B до C
phoneBook.subMap("B", "D").forEach((name, phone) ->
System.out.println(name + ": " + phone)
);
ConcurrentHashMap - счетчики посещений
ConcurrentHashMap<String, AtomicLong> pageViews = new ConcurrentHashMap<>();
// Из разных потоков
pageViews.computeIfAbsent("/home", k -> new AtomicLong())
.incrementAndGet();
Map.Entry - вложенный интерфейс
interface Map.Entry<K, V> {
K getKey();
V getValue();
V setValue(V value);
}
Использование Entry
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
// Можно модифицировать значение через entry
if (value < 0) {
entry.setValue(0);
}
}
Создание Entry
// Immutable entry (Java 9+)
Map.Entry<String, Integer> entry = Map.entry("key", 123);
// Создание Map из entries
Map<String, Integer> map = Map.ofEntries(
Map.entry("one", 1),
Map.entry("two", 2),
Map.entry("three", 3)
);
Неизменяемые Map (Immutable)
Java 9+ - Map.of()
// До 10 элементов
Map<String, Integer> map = Map.of(
"one", 1,
"two", 2,
"three", 3
);
// Больше 10 элементов
Map<String, Integer> largeMap = Map.ofEntries(
Map.entry("key1", 1),
Map.entry("key2", 2),
// ...
);
map.put("four", 4); // ❌ UnsupportedOperationException
Collections.unmodifiableMap()
Map<String, Integer> mutableMap = new HashMap<>();
mutableMap.put("one", 1);
Map<String, Integer> unmodifiableMap =
Collections.unmodifiableMap(mutableMap);
unmodifiableMap.put("two", 2); // ❌ UnsupportedOperationException
// Внимание: если изменить исходную map, unmodifiable тоже изменится
mutableMap.put("two", 2); // Изменит и unmodifiableMap!
Специализированные Map реализации
WeakHashMap
- Ключи хранятся как weak references
- Если ключ больше нигде не используется, entry удаляется GC
- Использование: кэши, где ключи - объекты
Map<Object, String> cache = new WeakHashMap<>();
Object key = new Object();
cache.put(key, "value");
key = null; // Больше нет strong references
System.gc(); // После GC entry удалится из map
IdentityHashMap
- Сравнение ключей через
==вместоequals() - Использование: когда важна идентичность объектов, а не равенство
Map<String, Integer> map = new IdentityHashMap<>();
String s1 = new String("key");
String s2 = new String("key");
map.put(s1, 1);
map.put(s2, 2);
map.size(); // 2 (разные объекты, хотя equals вернет true)
EnumMap
- Оптимизированная Map для enum ключей
- Внутри - массив
- Очень быстрая и компактная
enum Day { MONDAY, TUESDAY, WEDNESDAY }
Map<Day, String> schedule = new EnumMap<>(Day.class);
schedule.put(Day.MONDAY, "Meeting");
schedule.put(Day.TUESDAY, "Workshop");
Часто используемые методы Map
Получение значения с default
Integer value = map.getOrDefault("key", 0);
// Если ключа нет, вернет 0 вместо null
Массовые операции
Map<String, Integer> source = Map.of("a", 1, "b", 2);
Map<String, Integer> target = new HashMap<>();
target.putAll(source); // Копирует все entry
Проверки
boolean isEmpty = map.isEmpty();
int size = map.size();
boolean hasKey = map.containsKey("key");
boolean hasValue = map.containsValue(100);
Очистка
map.clear(); // Удаляет все элементы
Типичные ошибки и best practices
❌ Неправильный hashCode и equals
class Person {
String name;
int age;
// ❌ Не переопределены equals и hashCode
}
Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Alice", 30);
map.put(p1, "data");
Person p2 = new Person("Alice", 30);
map.get(p2); // null ! Разные объекты
Правильно:
class Person {
String name;
int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
❌ Использование mutable объектов как ключей
List<String> key = new ArrayList<>();
key.add("item");
map.put(key, "value");
key.add("another"); // ❌ Изменили ключ после вставки
map.get(key); // Может не найти! hashCode изменился
Правильно: используй immutable объекты как ключи (String, Integer, и т.д.)
❌ Изменение Map во время итерации
for (String key : map.keySet()) {
if (condition) {
map.remove(key); // ❌ ConcurrentModificationException
}
}
Правильно:
Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Integer> entry = it.next();
if (condition) {
it.remove(); // ✅ OK
}
}
// Или (Java 8+)
map.entrySet().removeIf(entry -> condition);
✅ Указание initial capacity
// Плохо
Map<String, String> map = new HashMap<>();
for (int i = 0; i < 10000; i++) {
map.put("key" + i, "value" + i); // Много rehashing
}
// Хорошо
Map<String, String> map = new HashMap<>(10000);
✅ Правильное вычисление capacity
// Для N элементов с loadFactor=0.75
int capacity = (int) (N / 0.75 + 1);
Map<String, String> map = new HashMap<>(capacity);
Функциональные методы (Java 8+)
forEach
map.forEach((key, value) -> {
System.out.println(key + " = " + value);
});
replaceAll
// Заменить все значения
map.replaceAll((key, value) -> value.toUpperCase());
computeIfAbsent - кэширование
Map<String, List<String>> groupedData = new HashMap<>();
// Старый способ
if (!groupedData.containsKey(category)) {
groupedData.put(category, new ArrayList<>());
}
groupedData.get(category).add(item);
// Новый способ (Java 8+)
groupedData.computeIfAbsent(category, k -> new ArrayList<>())
.add(item);
merge - счетчики
Map<String, Integer> wordCount = new HashMap<>();
words.forEach(word ->
wordCount.merge(word, 1, Integer::sum)
);
Итоговые рекомендации
По умолчанию:
- Выбирай HashMap - самая быстрая и универсальная
- Если нужна потокобезопасность → ConcurrentHashMap
- Если нужен порядок → LinkedHashMap
- Если нужна сортировка → TreeMap
Помни:
- Всегда переопределяй
hashCode()иequals()для кастомных ключей - Используй immutable объекты как ключи
- Указывай initial capacity если знаешь размер
- Для многопоточности используй Concurrent коллекции, а не синхронизацию
- ConcurrentHashMap не поддерживает null
- TreeMap не поддерживает null-ключи
Оптимизация:
// Правильный размер
Map<K, V> map = new HashMap<>((int)(expectedSize / 0.75 + 1));
// Для маленьких immutable map
Map<K, V> small = Map.of(k1, v1, k2, v2);
// Для больших immutable map
Map<K, V> large = Map.ofEntries(...);
Итоги
- Map хранит пары ключ-значение, ключи уникальны
- HashMap - по умолчанию, O(1) операции, порядок не гарантируется
- LinkedHashMap - сохраняет порядок вставки или доступа (LRU кэш)
- TreeMap - автоматическая сортировка по ключам, O(log n)
- ConcurrentHashMap - потокобезопасная, не поддерживает null
- Всегда переопределяй hashCode и equals для кастомных ключей
- Используй immutable объекты как ключи (String, Integer и т.д.)
- computeIfAbsent/merge - атомарные операции для обновления значений
- Не модифицируй Map во время итерации - используй iterator.remove()
- Указывай initial capacity для оптимизации производительности
Задания для практики
-
Подсчет слов: Дан текст. Создай Map<String, Integer> с частотой каждого слова. Используй merge().
-
LRU Cache: Реализуй LRU кэш на 5 элементов используя LinkedHashMap с accessOrder=true.
-
Группировка: Дан список Person(name, city). Создай Map<String, List
> - группировка по городу. -
ConcurrentHashMap: Реализуй потокобезопасный счетчик посещений страниц. Несколько потоков инкрементируют счетчики.
-
NavigableMap: Дан TreeMap с ценами товаров. Найди все товары в диапазоне цен [100, 500].
2.1.3. Выбор правильной коллекции
Критерии выбора коллекции для конкретной задачи
Материалы
Алгоритм выбора коллекции
Шаг 1: Определить тип коллекции
Нужна пара ключ-значение?
→ Map (HashMap, TreeMap, LinkedHashMap)
Нужны уникальные элементы?
→ Set (HashSet, TreeSet, LinkedHashSet)
Нужна очередь/стек?
→ Queue/Deque (ArrayDeque, PriorityQueue, LinkedList)
Нужен список с индексацией?
→ List (ArrayList, LinkedList)
Критерий 1: Операции доступа
Частый доступ по индексу
Требование: get(index) должен быть быстрым
✅ ArrayList
- O(1) доступ по индексу
- Компактное хранение в памяти
❌ LinkedList
- O(n) доступ по индексу
- Нужно проходить по узлам
// Если часто делаем list.get(i)
List<String> data = new ArrayList<>(); // ✅
List<String> data = new LinkedList<>(); // ❌
Частый поиск элемента
Требование: contains(element) должен быть быстрым
✅ HashSet / HashMap
- O(1) проверка наличия элемента
❌ ArrayList / LinkedList
- O(n) линейный поиск
// Если часто проверяем "есть ли элемент"
Set<String> users = new HashSet<>(); // ✅ O(1)
List<String> users = new ArrayList<>(); // ❌ O(n)
if (users.contains("John")) { ... }
Критерий 2: Операции модификации
Частые вставки/удаления в начале или середине
Требование: модификация в произвольных местах
✅ LinkedList
- O(1) вставка/удаление при наличии итератора
- O(n) поиск позиции
❌ ArrayList
- O(n) вставка/удаление (сдвиг элементов)
// Частые вставки в начало
List<String> buffer = new LinkedList<>(); // ✅
buffer.add(0, "new item"); // O(1)
List<String> buffer = new ArrayList<>(); // ❌
buffer.add(0, "new item"); // O(n) - сдвигает все элементы
Только добавление в конец
Требование: add(element) в конец списка
✅ ArrayList
- O(1) амортизированное добавление
- Компактнее в памяти
// Накапливаем результаты последовательно
List<Result> results = new ArrayList<>(); // ✅
for (...) {
results.add(compute());
}
Частые добавления И удаления с обоих концов
Требование: операции с головой и хвостом
✅ ArrayDeque
- O(1) для операций с обоих концов
- Быстрее LinkedList
// Очередь задач или стек вызовов
Deque<Task> taskQueue = new ArrayDeque<>(); // ✅
taskQueue.addFirst(urgentTask); // O(1)
taskQueue.addLast(normalTask); // O(1)
taskQueue.pollFirst(); // O(1)
Критерий 3: Порядок элементов
Порядок не важен
✅ HashSet / HashMap
- Максимальная производительность
- Нет накладных расходов на поддержку порядка
Set<String> uniqueWords = new HashSet<>(); // ✅ Самый быстрый
Нужен порядок вставки (insertion order)
✅ LinkedHashSet / LinkedHashMap
- Предсказуемый порядок итерации
- Небольшие накладные расходы
// История посещенных страниц - порядок важен
Set<String> visitedPages = new LinkedHashSet<>(); // ✅
visitedPages.add("/home");
visitedPages.add("/products");
// Итерация в порядке добавления
Нужна сортировка (natural или custom order)
✅ TreeSet / TreeMap
- Автоматическая сортировка
- O(log n) операции
// Топ игроков по очкам - всегда отсортировано
Set<Player> leaderboard = new TreeSet<>(
Comparator.comparing(Player::getScore).reversed()
); // ✅
leaderboard.add(new Player("Alice", 100));
leaderboard.add(new Player("Bob", 150));
// Автоматически отсортировано по убыванию очков
Нужна сортировка + операции навигации
✅ TreeSet / TreeMap
- Методы
lower(),higher(),floor(),ceiling()
TreeSet<Integer> prices = new TreeSet<>();
prices.add(100);
prices.add(200);
prices.add(300);
// Найти ближайшую цену <= 250
Integer nearestPrice = prices.floor(250); // 200 ✅
Критерий 4: Уникальность элементов
Дубликаты разрешены
✅ List (ArrayList, LinkedList)
List<String> tags = new ArrayList<>(); // ✅
tags.add("java");
tags.add("java"); // OK, дубликат разрешен
Только уникальные элементы
✅ Set (HashSet, TreeSet, LinkedHashSet)
Set<String> uniqueTags = new HashSet<>(); // ✅
uniqueTags.add("java");
uniqueTags.add("java"); // Не добавится
// size = 1
Критерий 5: Null-элементы
Null разрешен
✅ ArrayList, LinkedList, HashSet, LinkedHashSet, HashMap, LinkedHashMap
List<String> items = new ArrayList<>();
items.add(null); // ✅ OK
Set<String> set = new HashSet<>();
set.add(null); // ✅ OK (только один null)
Null запрещен
❌ TreeSet, TreeMap, ArrayDeque, PriorityQueue
- Бросят
NullPointerException
Set<String> sorted = new TreeSet<>();
sorted.add(null); // ❌ NullPointerException
Queue<String> queue = new ArrayDeque<>();
queue.add(null); // ❌ NullPointerException
Критерий 6: Многопоточность
Однопоточное использование
✅ Обычные коллекции (ArrayList, HashSet и т.д.)
- Максимальная производительность
- Без синхронизации
Многопоточное использование - частые чтения, редкие записи
✅ CopyOnWriteArrayList / CopyOnWriteArraySet
- Итерация без блокировки
- Модификации дорогие (копирование массива)
// Список слушателей событий - редко меняется, часто читается
List<EventListener> listeners = new CopyOnWriteArrayList<>(); // ✅
// В любом потоке можно итерировать без locks
for (EventListener listener : listeners) {
listener.onEvent(event); // Безопасно
}
Многопоточное использование - частые чтения И записи
✅ ConcurrentHashMap
- Lock-free чтение
- Сегментированная блокировка записи
- Высокая параллельность
// Кэш - частые чтения и записи из разных потоков
Map<String, User> userCache = new ConcurrentHashMap<>(); // ✅
// Поток 1
userCache.put("user123", user);
// Поток 2
User u = userCache.get("user123"); // Безопасно, быстро
❌ Синхронизированные обертки (для совместимости)
// Устаревший подход - ниже производительность
Map<String, String> map = Collections.synchronizedMap(new HashMap<>()); // ❌
Producer-Consumer паттерн
✅ BlockingQueue (LinkedBlockingQueue, ArrayBlockingQueue)
- Автоматическая блокировка при пустой/полной очереди
- Идеально для producer-consumer
// Очередь задач между потоками
BlockingQueue<Task> taskQueue = new LinkedBlockingQueue<>(100); // ✅
// Producer thread
taskQueue.put(task); // Блокируется если очередь полная
// Consumer thread
Task task = taskQueue.take(); // Блокируется если очередь пустая
Критерий 7: Размер коллекции
Известный размер заранее
Оптимизация: указать начальную ёмкость
// Плохо - много перевыделений
List<String> items = new ArrayList<>(); // capacity = 10
for (int i = 0; i < 10000; i++) {
items.add("item" + i); // Много раз расширяется
}
// Хорошо - одно выделение
List<String> items = new ArrayList<>(10000); // ✅
for (int i = 0; i < 10000; i++) {
items.add("item" + i);
}
Маленькая коллекция (< 10 элементов)
Оптимизация: меньше значения коэффициента загрузки для HashMap
// По умолчанию: initialCapacity=16, loadFactor=0.75
Map<String, String> config = new HashMap<>(); // 16 слотов для 3 элементов
// Оптимизировано для малых коллекций
Map<String, String> config = new HashMap<>(4, 1.0f); // ✅ 4 слота
Очень большая коллекция (миллионы элементов)
Соображения:
- Избегать ArrayList для очень больших размеров (проблемы с перевыделением)
- Рассмотреть сторонние библиотеки (Trove, Fastutil) для примитивов
- Использовать потоковую обработку вместо загрузки всего в память
Критерий 8: Приоритет обработки
FIFO (First-In-First-Out) - обычная очередь
✅ LinkedList или ArrayDeque как Queue
Queue<Task> fifoQueue = new ArrayDeque<>(); // ✅
fifoQueue.offer(task1);
fifoQueue.offer(task2);
fifoQueue.poll(); // task1 (первый добавленный)
LIFO (Last-In-First-Out) - стек
✅ ArrayDeque как Stack
Deque<String> stack = new ArrayDeque<>(); // ✅
stack.push("first");
stack.push("second");
stack.pop(); // "second" (последний добавленный)
Обработка по приоритету
✅ PriorityQueue
// Задачи с приоритетом
Queue<Task> priorityQueue = new PriorityQueue<>(
Comparator.comparing(Task::getPriority).reversed()
); // ✅
priorityQueue.offer(new Task(priority: 5));
priorityQueue.offer(new Task(priority: 10));
priorityQueue.poll(); // Task с priority=10 (наивысший)
Сравнительная таблица по критериям
| Критерий | ArrayList | LinkedList | HashSet | TreeSet | ArrayDeque | HashMap |
|---|---|---|---|---|---|---|
| Доступ по индексу | ✅ O(1) | ❌ O(n) | N/A | N/A | N/A | N/A |
| Поиск элемента | ❌ O(n) | ❌ O(n) | ✅ O(1) | ✅ O(log n) | ❌ O(n) | ✅ O(1) |
| Вставка в начало | ❌ O(n) | ✅ O(1) | N/A | N/A | ✅ O(1) | N/A |
| Вставка в конец | ✅ O(1) | ✅ O(1) | ✅ O(1) | ✅ O(log n) | ✅ O(1) | ✅ O(1) |
| Удаление | ❌ O(n) | ❌ O(n) | ✅ O(1) | ✅ O(log n) | ✅ O(1) | ✅ O(1) |
| Порядок | Вставки | Вставки | Нет | Сортировка | Вставки | Нет |
| Дубликаты | ✅ Да | ✅ Да | ❌ Нет | ❌ Нет | ✅ Да | ❌ Ключи |
| Null | ✅ Да | ✅ Да | ✅ Один | ❌ Нет | ❌ Нет | ✅ Да |
| Память | Компактно | Накладные | Средне | Средне | Компактно | Средне |
Практические примеры выбора
Пример 1: Список пользователей для отображения
Требования:
- Нужен порядок вставки
- Доступ по индексу для пагинации
- Дубликаты возможны
Выбор: ✅ ArrayList
List<User> users = new ArrayList<>();
Пример 2: Уникальные посетители сайта
Требования:
- Только уникальные ID
- Быстрая проверка “был ли пользователь”
- Порядок не важен
Выбор: ✅ HashSet
Set<String> uniqueVisitors = new HashSet<>();
if (!uniqueVisitors.contains(userId)) {
uniqueVisitors.add(userId);
// Первый визит
}
Пример 3: История команд (undo/redo)
Требования:
- LIFO для undo
- Операции только на концах
Выбор: ✅ ArrayDeque
Deque<Command> undoStack = new ArrayDeque<>();
Deque<Command> redoStack = new ArrayDeque<>();
// Undo
Command cmd = undoStack.pop();
cmd.undo();
redoStack.push(cmd);
Пример 4: Топ игроков (рейтинг)
Требования:
- Автоматическая сортировка по очкам
- Нужны операции: лучший, худший, позиция игрока
Выбор: ✅ TreeSet с кастомным компаратором
TreeSet<Player> leaderboard = new TreeSet<>(
Comparator.comparing(Player::getScore)
.reversed()
.thenComparing(Player::getName)
);
Пример 5: Кэш с LRU
Требования:
- Отслеживание порядка использования
- Быстрый доступ по ключу
- Удаление самого старого
Выбор: ✅ LinkedHashMap с accessOrder=true
Map<String, Data> lruCache = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Data> eldest) {
return size() > MAX_ENTRIES;
}
};
Пример 6: Буфер событий между потоками
Требования:
- Многопоточность
- Producer-Consumer
- Ограниченный размер
Выбор: ✅ ArrayBlockingQueue
BlockingQueue<Event> eventBuffer = new ArrayBlockingQueue<>(1000);
// Producer
eventBuffer.put(event);
// Consumer
Event event = eventBuffer.take();
Пример 7: Слова в тексте по частоте
Требования:
- Подсчет уникальных слов
- Сортировка по частоте
Решение: HashMap + PriorityQueue или TreeMap
// Подсчет
Map<String, Integer> wordCount = new HashMap<>();
for (String word : words) {
wordCount.merge(word, 1, Integer::sum);
}
// Топ N слов
PriorityQueue<Map.Entry<String, Integer>> topWords = new PriorityQueue<>(
Map.Entry.comparingByValue(Comparator.reverseOrder())
);
topWords.addAll(wordCount.entrySet());
Чек-лист выбора коллекции
Вопросы для принятия решения:
-
Структура данных:
- Нужна пара ключ-значение? → Map
- Нужны только уникальные элементы? → Set
- Нужна очередь/стек? → Queue/Deque
- Нужен список с индексами? → List
-
Операции:
- Частый доступ по индексу? → ArrayList
- Частая проверка наличия? → HashSet/HashMap
- Частые вставки/удаления в начале? → LinkedList/ArrayDeque
- Только добавление в конец? → ArrayList
-
Порядок:
- Порядок не важен? → HashSet/HashMap
- Нужен порядок вставки? → LinkedHashSet/LinkedHashMap
- Нужна автосортировка? → TreeSet/TreeMap
-
Дополнительно:
- Нужны null-элементы? → избегай TreeSet/TreeMap/ArrayDeque
- Многопоточность? → Concurrent коллекции
- Известен размер? → укажи initialCapacity
- Нужен приоритет? → PriorityQueue
Антипаттерны и частые ошибки
❌ Использование LinkedList без причины
// Плохо - LinkedList медленнее в большинстве случаев
List<String> items = new LinkedList<>();
for (int i = 0; i < items.size(); i++) {
process(items.get(i)); // O(n²) !
}
❌ Неправильный выбор для поиска
// Плохо - поиск в списке O(n)
List<String> users = new ArrayList<>();
if (users.contains("john")) { ... } // Медленно!
// Хорошо - поиск в set O(1)
Set<String> users = new HashSet<>();
if (users.contains("john")) { ... } // Быстро!
❌ Игнорирование initial capacity
// Плохо - много перевыделений памяти
Map<String, String> map = new HashMap<>();
for (int i = 0; i < 100000; i++) {
map.put("key" + i, "value" + i);
}
// Хорошо
Map<String, String> map = new HashMap<>(100000);
❌ Использование устаревших классов
// Устарело
Vector<String> vector = new Vector<>(); // → ArrayList
Stack<String> stack = new Stack<>(); // → ArrayDeque
Hashtable<String, String> table = new Hashtable<>(); // → HashMap
❌ Ручная синхронизация вместо concurrent
// Плохо - грубая синхронизация
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
synchronized (map) {
map.forEach((k, v) -> process(k, v));
}
// Хорошо - lock-free
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.forEach((k, v) -> process(k, v)); // Безопасно без locks
Итоговые рекомендации
По умолчанию используй:
- List →
ArrayList(самый частый случай) - Set →
HashSet(самый быстрый) - Queue →
ArrayDeque(быстрее LinkedList) - Map →
HashMap(самый быстрый)
Специальные случаи:
- Нужна сортировка →
TreeSet/TreeMap - Нужен порядок вставки →
LinkedHashSet/LinkedHashMap - Многопоточность →
ConcurrentHashMap/CopyOnWriteArrayList - Producer-Consumer →
BlockingQueue - Приоритеты →
PriorityQueue - Вставки в начало →
LinkedList/ArrayDeque
Правило оптимизации:
Сначала выбирай правильную структуру данных (Set vs List vs Map),
потом выбирай реализацию (HashSet vs TreeSet),
потом оптимизируй (указывай initialCapacity).
2.2. Generics
Обобщенное программирование в Java
Содержание
2.2.1. Type Parameters
Параметризованные типы
Материалы
Type Variables (Переменные типа)
Что такое Type Variable?
Type Variable (переменная типа) - это неквалифицированный идентификатор, используемый как тип в теле класса, интерфейса, метода или конструктора.
Type variable вводится через объявление type parameter (параметра типа) в:
- Generic классах (§8.1.2)
- Generic интерфейсах (§9.1.2)
- Generic методах (§8.4.4)
- Generic конструкторах (§8.8.4)
Синтаксис Type Parameter
TypeParameter:
{TypeParameterModifier} TypeIdentifier [TypeBound]
TypeParameterModifier:
Annotation
TypeBound:
extends TypeVariable
extends ClassOrInterfaceType {AdditionalBound}
AdditionalBound:
& InterfaceType
Объявление Type Parameters
1. Generic классы
// Один type parameter
class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
// Несколько type parameters
class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
}
// Использование
Box<String> stringBox = new Box<>();
Pair<String, Integer> pair = new Pair<>("age", 30);
2. Generic интерфейсы
interface Comparable<T> {
int compareTo(T other);
}
interface Map<K, V> {
V get(K key);
V put(K key, V value);
}
3. Generic методы
class Utils {
// Generic метод
public static <T> T first(List<T> list) {
return list.get(0);
}
// Несколько type parameters
public static <K, V> Map<K, V> createMap(K[] keys, V[] values) {
Map<K, V> map = new HashMap<>();
for (int i = 0; i < keys.length; i++) {
map.put(keys[i], values[i]);
}
return map;
}
}
// Использование
String first = Utils.<String>first(Arrays.asList("a", "b"));
String first = Utils.first(Arrays.asList("a", "b")); // Type inference
4. Generic конструкторы
class Foo {
// Generic конструктор в non-generic классе
<T> Foo(T arg) {
// ...
}
}
// Использование
Foo f = new <String>Foo("hello");
Foo f = new Foo("hello"); // Type inference
Type Bounds (Границы типов)
Unbounded Type Parameter (без ограничений)
class Box<T> {
private T value;
}
Правило: Если bound не указан, то подразумевается Object.
// Эквивалентно
class Box<T extends Object> {
private T value;
}
Erasure: T стирается до Object.
Bounded Type Parameter (с ограничениями)
1. Upper Bound (верхняя граница)
// T должен быть Number или его подтип
class NumberBox<T extends Number> {
private T value;
public double doubleValue() {
return value.doubleValue(); // Можем вызвать методы Number
}
}
// Использование
NumberBox<Integer> intBox = new NumberBox<>(); // ✅ OK
NumberBox<Double> doubleBox = new NumberBox<>(); // ✅ OK
NumberBox<String> stringBox = new NumberBox<>(); // ❌ Ошибка компиляции
Erasure: T стирается до Number.
2. Multiple Bounds (множественные границы)
// T должен быть подтипом Number И Comparable
class SortedNumberBox<T extends Number & Comparable<T>> {
private List<T> values = new ArrayList<>();
public void add(T value) {
values.add(value);
}
public T max() {
T max = values.get(0);
for (T value : values) {
if (value.compareTo(max) > 0) {
max = value;
}
}
return max;
}
}
// Использование
SortedNumberBox<Integer> box = new SortedNumberBox<>();
box.add(5);
box.add(2);
box.add(8);
Integer max = box.max(); // 8
Синтаксис множественных bounds:
<T extends Class/Interface & Interface1 & Interface2 & ...>
Правила:
- ✅ Первый bound может быть класс ИЛИ интерфейс ИЛИ type variable
- ❌ Последующие bounds могут быть ТОЛЬКО интерфейсы
- ❌ Нельзя указывать два класса в bounds
- ❌ Нельзя указывать type variable после первой позиции
// ✅ Правильно
<T extends Number & Comparable<T>>
<T extends Comparable<T> & Serializable>
<T extends List<String> & Cloneable>
// ❌ Неправильно
<T extends Number & String> // Два класса
<T extends Number & Integer> // Два класса
<T extends Comparable<T> & Number> // Класс не первый
Erasure: T стирается до первого типа в bound.
<T extends Number & Comparable<T>> → Number (в runtime)
<T extends Comparable<T> & Number> → Comparable (в runtime)
Важно: Порядок bounds имеет значение для erasure!
3. Type Variable как Bound
<T, S extends T> // S должен быть подтипом T
class Pair<T, S extends T> {
private T first;
private S second; // S гарантированно совместим с T
public Pair(T first, S second) {
this.first = first;
this.second = second;
}
}
// Использование
Pair<Number, Integer> pair = new Pair<>(10, 20); // ✅ Integer extends Number
Pair<Integer, Number> pair = new Pair<>(10, 20); // ❌ Number не extends Integer
Члены Type Variable
Type variable имеет те же члены (members), что и его intersection type bounds.
Пример
class C {
public void mCPublic() {}
protected void mCProtected() {}
void mCPackage() {}
private void mCPrivate() {}
}
interface I {
void mI();
}
class Test {
<T extends C & I> void test(T t) {
t.mI(); // ✅ OK - метод интерфейса I
t.mCPublic(); // ✅ OK - public метод C
t.mCProtected(); // ✅ OK - protected метод C
t.mCPackage(); // ✅ OK - package-private метод C (в том же пакете)
t.mCPrivate(); // ❌ Ошибка - private не доступен
}
}
Члены type variable T с bound C & I:
- Все public методы интерфейса I
- Все accessible методы класса C (кроме private)
Parameterized Types (Параметризованные типы)
Определение
Parameterized type - это класс или интерфейс вида C<T1, T2, ..., Tn>, где:
C- имя generic класса или интерфейса<T1, T2, ..., Tn>- список type arguments (аргументов типа)
List<String> // C = List, T1 = String
Map<String, Integer> // C = Map, T1 = String, T2 = Integer
Pair<String, String> // C = Pair, T1 = String, T2 = String
Well-formed Parameterized Type
Parameterized type корректен (well-formed) если:
- C - имя generic класса или интерфейса
- Количество type arguments = количество type parameters
- Каждый type argument соответствует своему bound
// Generic класс с 2 type parameters
class Pair<K, V> { }
// ✅ Корректные parameterized types
Pair<String, Integer>
Pair<Integer, Integer>
Pair<Object, Object>
// ❌ Некорректные parameterized types
Pair<String> // Недостаточно type arguments
Pair<String, String, String> // Слишком много type arguments
Pair<int, String> // Primitive types не допускаются
Проверка bounds при параметризации
class BoundedBox<T extends Number> {
private T value;
}
// ✅ Корректно - Integer extends Number
BoundedBox<Integer> box1 = new BoundedBox<>();
// ✅ Корректно - Double extends Number
BoundedBox<Double> box2 = new BoundedBox<>();
// ❌ Ошибка компиляции - String не extends Number
BoundedBox<String> box3 = new BoundedBox<>();
Type Arguments (Аргументы типа)
Синтаксис
TypeArguments:
< TypeArgumentList >
TypeArgumentList:
TypeArgument {, TypeArgument}
TypeArgument:
ReferenceType
Wildcard
Виды Type Arguments
1. Concrete Type (конкретный тип)
List<String> // Type argument = String
Map<String, Integer> // Type arguments = String, Integer
Box<List<String>> // Type argument = List<String> (вложенный)
2. Wildcard (подстановочный знак)
Unbounded Wildcard - ?
List<?> list; // Список неизвестного типа
Означает: список элементов какого-то типа, но мы не знаем какого.
void printList(List<?> list) {
for (Object obj : list) { // Можем читать как Object
System.out.println(obj);
}
// list.add("test"); // ❌ Нельзя добавлять (кроме null)
}
Upper Bounded Wildcard - ? extends T
List<? extends Number> numbers;
Означает: список элементов типа Number или его подтипа.
void sumNumbers(List<? extends Number> numbers) {
double sum = 0;
for (Number n : numbers) { // Можем читать как Number
sum += n.doubleValue();
}
// numbers.add(42); // ❌ Нельзя добавлять
}
// Использование
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5);
sumNumbers(ints); // ✅ OK
sumNumbers(doubles); // ✅ OK
Правило: Можно читать, нельзя писать (producer).
Lower Bounded Wildcard - ? super T
List<? super Integer> list;
Означает: список элементов типа Integer или его супертипа.
void addIntegers(List<? super Integer> list) {
list.add(1); // ✅ Можем добавлять Integer
list.add(2); // ✅ Можем добавлять Integer
// Object obj = list.get(0); // Можем читать только как Object
}
// Использование
List<Integer> ints = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addIntegers(ints); // ✅ OK
addIntegers(numbers); // ✅ OK
addIntegers(objects); // ✅ OK
Правило: Можно писать, можно читать только как Object (consumer).
PECS Principle
PECS = Producer Extends, Consumer Super
// Producer - выдаем элементы (читаем)
<T> void copy(List<? extends T> source, List<? super T> dest) {
for (T item : source) { // Читаем из source (producer)
dest.add(item); // Пишем в dest (consumer)
}
}
// Использование
List<Integer> source = Arrays.asList(1, 2, 3);
List<Number> dest = new ArrayList<>();
copy(source, dest); // ✅ OK
Provably Distinct Types
Два parameterized type provably distinct (доказуемо различны) если:
-
Это параметризации разных generic типов
List<String> и Set<String> // Провably distinct -
Любой из их type arguments provably distinct
List<String> и List<Integer> // Provably distinct List<String> и List<?> // Provably distinct
Зачем нужно?
Для проверки overloading:
// ❌ Ошибка компиляции - после erasure одинаковые сигнатуры
void process(List<String> list) { }
void process(List<Integer> list) { }
// ✅ OK - provably distinct types
void process(List<String> list) { }
void process(Set<String> set) { }
Вложенные Parameterized Types
Nested Generic Classes
class Outer<T> {
class Inner<S> {
private T outerValue;
private S innerValue;
}
}
// Использование
Outer<String>.Inner<Integer> nested = new Outer<String>().new Inner<Integer>();
Generic Member Class в Non-Generic Outer
class NonGenericOuter {
class GenericInner<T> {
private T value;
}
}
// Использование
NonGenericOuter.GenericInner<String> inner =
new NonGenericOuter().new GenericInner<String>();
Non-Generic Inner в Generic Outer
class GenericOuter<T> {
class NonGenericInner {
private T outerValue; // Может использовать T из Outer
}
}
// Использование
GenericOuter<String>.NonGenericInner inner =
new GenericOuter<String>().new NonGenericInner();
Члены Parameterized Types
Правило определения типа члена
Пусть C - generic класс с type parameters A1, ..., An.
Пусть C<T1, ..., Tn> - parameterized type.
Тип члена m в C<T1, ..., Tn>:
T[A1:=T1, A2:=T2, ..., An:=Tn]
Где T - тип члена как объявлено в C.
Примеры
class Box<T> {
private T value; // Тип: T
public void set(T v) { } // Параметр типа: T
public T get() { } // Возвращает: T
}
// В Box<String>:
// private String value;
// public void set(String v) { }
// public String get() { }
// В Box<Integer>:
// private Integer value;
// public void set(Integer v) { }
// public Integer get() { }
Static члены
Правило: Static члены generic класса НЕЛЬЗЯ обращаться через parameterized type.
class Box<T> {
private static int count; // ✅ OK - static без type parameter
public static int getCount() {
return count;
}
// ❌ Нельзя использовать T в static контексте
// private static T defaultValue;
// public static T getDefault() { ... }
}
// ❌ Ошибка - нельзя обращаться через parameterized type
Box<String>.getCount();
// ✅ OK - обращение через raw type
Box.getCount();
Почему? Static члены принадлежат классу, а не экземпляру. Type parameter T относится к экземпляру.
Recursive Type Parameters
Определение
Type parameter может ссылаться на себя в своем bound.
class Enum<E extends Enum<E>> {
// E должен быть подтипом Enum<E>
}
F-bounded Polymorphism
interface Comparable<T> {
int compareTo(T other);
}
// Класс сравним сам с собой
class Person implements Comparable<Person> {
private String name;
@Override
public int compareTo(Person other) {
return this.name.compareTo(other.name);
}
}
Recursive bound:
<T extends Comparable<T>>
Означает: T должен быть сравним с самим собой.
Пример: Generic метод для сортировки
// T должен быть comparable с самим собой
public static <T extends Comparable<T>> T max(List<T> list) {
if (list.isEmpty()) {
throw new IllegalArgumentException("Empty list");
}
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) {
max = item;
}
}
return max;
}
// Использование
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5);
Integer max = max(numbers); // 5
Type Parameter Naming Conventions
Общепринятые соглашения:
-
T - Type (общий тип)
class Box<T> { } -
E - Element (элемент коллекции)
interface List<E> { } -
K - Key (ключ)
interface Map<K, V> { } -
V - Value (значение)
interface Map<K, V> { } -
N - Number (числовой тип)
class Calculator<N extends Number> { } -
S, U, V - дополнительные type parameters
<T, S, U, V>
Правило: Используй заглавные буквы для type parameters.
Ограничения Type Parameters
1. Нельзя создать экземпляр type parameter
class Box<T> {
// ❌ Нельзя
public T create() {
return new T(); // Ошибка компиляции
}
}
Почему: Type erasure стирает T до Object (или bound).
Workaround: Передавать Class
class Box<T> {
public T create(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
}
2. Нельзя создать массив type parameter
class Box<T> {
// ❌ Нельзя
private T[] array = new T[10]; // Ошибка компиляции
}
Workaround:
class Box<T> {
private T[] array;
@SuppressWarnings("unchecked")
public Box(int size) {
array = (T[]) new Object[size]; // Unchecked cast
}
}
3. Нельзя использовать в static контексте
class Box<T> {
// ❌ Нельзя
private static T defaultValue;
// ❌ Нельзя
public static T getDefault() {
return defaultValue;
}
}
4. Нельзя использовать instanceof с type parameter
class Box<T> {
public boolean check(Object obj) {
// ❌ Нельзя
if (obj instanceof T) { // Ошибка компиляции
return true;
}
return false;
}
}
5. Нельзя перехватывать type parameter exception
// ❌ Нельзя
class GenericException<T extends Exception> extends Exception {
// ...
}
// ❌ Нельзя
public <T extends Exception> void method() {
try {
// ...
} catch (T e) { // Ошибка компиляции
// ...
}
}
Примеры использования Type Parameters
1. Generic Stack
class Stack<E> {
private List<E> elements = new ArrayList<>();
public void push(E element) {
elements.add(element);
}
public E pop() {
if (isEmpty()) {
throw new EmptyStackException();
}
return elements.remove(elements.size() - 1);
}
public E peek() {
if (isEmpty()) {
throw new EmptyStackException();
}
return elements.get(elements.size() - 1);
}
public boolean isEmpty() {
return elements.isEmpty();
}
}
// Использование
Stack<String> stack = new Stack<>();
stack.push("first");
stack.push("second");
String top = stack.pop(); // "second"
2. Generic Builder
class Builder<T> {
private T product;
public Builder(T product) {
this.product = product;
}
public Builder<T> with(Consumer<T> consumer) {
consumer.accept(product);
return this;
}
public T build() {
return product;
}
}
// Использование
Person person = new Builder<>(new Person())
.with(p -> p.setName("John"))
.with(p -> p.setAge(30))
.build();
3. Generic Repository
interface Repository<T, ID> {
T findById(ID id);
List<T> findAll();
void save(T entity);
void delete(ID id);
}
class UserRepository implements Repository<User, Long> {
@Override
public User findById(Long id) {
// implementation
}
@Override
public List<User> findAll() {
// implementation
}
@Override
public void save(User user) {
// implementation
}
@Override
public void delete(Long id) {
// implementation
}
}
4. Generic Factory
interface Factory<T> {
T create();
}
class StringFactory implements Factory<String> {
@Override
public String create() {
return new String();
}
}
// Generic метод с Factory
public static <T> List<T> createList(Factory<T> factory, int count) {
List<T> list = new ArrayList<>();
for (int i = 0; i < count; i++) {
list.add(factory.create());
}
return list;
}
Best Practices
✅ DO (рекомендуется):
-
Используй осмысленные имена для type parameters
class UserCache<User, UserId> { } // ✅ Понятно class UserCache<T, K> { } // ❌ Непонятно -
Указывай bounds когда нужны специфичные операции
<T extends Comparable<T>> T max(List<T> list) // ✅ <T> T max(List<T> list) // ❌ Нельзя вызвать compareTo -
Используй wildcards для гибкости API
void addAll(Collection<? extends E> c) // ✅ Гибко void addAll(Collection<E> c) // ❌ Слишком строго -
PECS - Producer Extends, Consumer Super
<T> void copy(List<? extends T> src, List<? super T> dest) // ✅ -
Используй type inference где возможно
List<String> list = new ArrayList<>(); // ✅ Diamond operator List<String> list = new ArrayList<String>(); // ❌ Избыточно
❌ DON’T (не рекомендуется):
-
Не используй raw types
List list = new ArrayList(); // ❌ Raw type List<Object> list = new ArrayList<>(); // ✅ -
Не создавай generic массивы
List<String>[] array = new List<String>[10]; // ❌ Не скомпилируется -
Не используй type parameters в static контексте
class Box<T> { private static T value; // ❌ } -
Не злоупотребляй wildcards
// ❌ Слишком сложно Map<? extends String, ? super List<? extends Number>> map; // ✅ Проще и понятнее Map<String, List<Number>> map;
Заключение
Ключевые моменты:
- Type Parameter - это placeholder для типа, который будет указан при использовании
- Type Bounds ограничивают допустимые типы и дают доступ к методам
- Multiple Bounds позволяют требовать реализацию нескольких интерфейсов
- Wildcards (
?,? extends,? super) добавляют гибкость - Type Erasure стирает информацию о type parameters в runtime
- PECS - важный принцип для работы с wildcards
Важные ограничения:
- ❌ Нельзя создать
new T() - ❌ Нельзя создать
new T[] - ❌ Нельзя использовать T в static
- ❌ Нельзя
instanceof T - ❌ Нельзя catch T
Паттерны использования:
- Generic коллекции (List
, Map<K,V>) - Generic методы для алгоритмов
- Builder pattern с generics
- Factory pattern с generics
- Repository pattern с generics
Type Parameters - мощный инструмент Java для создания type-safe и переиспользуемого кода.!– Добавьте свои заметки здесь –>
2.2.2. Wildcards
? extends, ? super, PECS
Материалы
Wildcards - Подстановочные знаки
Что такое Wildcards?
Wildcard (подстановочный знак) - это специальный тип-аргумент в Java дженериках, обозначаемый символом ?.
Wildcards используются когда:
- Нужна частичная информация о типе
- Требуется гибкость в работе с параметризованными типами
- Не нужна полная информация о конкретном типе
Синтаксис
Wildcard:
{Annotation} ? [WildcardBounds]
WildcardBounds:
extends ReferenceType
super ReferenceType
Виды Wildcards
1. Unbounded Wildcard - ?
Определение: Wildcard без ограничений.
List<?> list;
Означает: Список элементов какого-то типа, но мы не знаем какого.
Когда использовать?
- Когда нужно работать с коллекцией, но тип элементов не важен
- Когда используются только методы Object
- Когда пишешь универсальный код
Пример
// Печать любой коллекции
public static void printCollection(Collection<?> c) {
for (Object element : c) { // Читаем как Object
System.out.println(element);
}
}
// Использование
Collection<String> strings = Arrays.asList("a", "b", "c");
Collection<Integer> integers = Arrays.asList(1, 2, 3);
printCollection(strings); // ✅ OK
printCollection(integers); // ✅ OK
Ограничения Unbounded Wildcard
List<?> list = new ArrayList<String>();
// ✅ Можно читать как Object
Object obj = list.get(0);
// ❌ Нельзя добавлять (кроме null)
list.add("test"); // Ошибка компиляции!
list.add(123); // Ошибка компиляции!
list.add(null); // ✅ OK - null можно
// ✅ Можно вызывать методы без параметров типа
int size = list.size();
boolean isEmpty = list.isEmpty();
list.clear();
Почему нельзя добавлять?
List<?> list = new ArrayList<String>(); // На самом деле List<String>
// Если бы можно было:
list.add(123); // Добавили Integer в List<String>! Нарушение type safety
? vs Object
// ❌ Неправильно - слишком строго
void printList(List<Object> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
List<String> strings = Arrays.asList("a", "b");
printList(strings); // ❌ Ошибка! List<String> не является List<Object>
// ✅ Правильно - гибко
void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
printList(strings); // ✅ OK
Важно: List<String> НЕ является подтипом List<Object>, но ЯВЛЯЕТСЯ подтипом List<?>.
2. Upper Bounded Wildcard - ? extends T
Определение: Wildcard с верхней границей.
List<? extends Number> numbers;
Означает: Список элементов типа Number или его подтипа (Integer, Double, Float, и т.д.).
Иерархия подтипов
Number
|
┌──────┼──────┐
| | |
Integer Double Float
List<? extends Number> numbers;
// ✅ Все это можно присвоить
numbers = new ArrayList<Number>();
numbers = new ArrayList<Integer>();
numbers = new ArrayList<Double>();
numbers = new ArrayList<Float>();
// ❌ Нельзя - String не extends Number
numbers = new ArrayList<String>();
Чтение и запись
List<? extends Number> numbers = new ArrayList<Integer>();
// ✅ Можно читать как Number
Number n = numbers.get(0);
double d = numbers.get(0).doubleValue();
// ❌ Нельзя добавлять (кроме null)
numbers.add(Integer.valueOf(1)); // Ошибка компиляции!
numbers.add(Double.valueOf(2.0)); // Ошибка компиляции!
numbers.add(new Object()); // Ошибка компиляции!
numbers.add(null); // ✅ OK - только null
Почему нельзя добавлять?
List<? extends Number> numbers = new ArrayList<Integer>(); // На самом деле List<Integer>
// Если бы можно было:
numbers.add(Double.valueOf(3.14)); // Добавили Double в List<Integer>! Ошибка!
Компилятор не знает точный тип, поэтому запрещает добавление чего-либо (кроме null).
Пример: Суммирование чисел
public static double sum(List<? extends Number> numbers) {
double sum = 0.0;
for (Number n : numbers) { // Читаем как Number
sum += n.doubleValue();
}
return sum;
}
// Использование
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5, 3.5);
List<Long> longs = Arrays.asList(10L, 20L, 30L);
sum(ints); // ✅ 6.0
sum(doubles); // ✅ 7.5
sum(longs); // ✅ 60.0
Get & Put Principle
? extends T - это Producer (производитель):
- ✅ Get - можно читать (produces T)
- ❌ Put - нельзя писать (кроме null)
3. Lower Bounded Wildcard - ? super T
Определение: Wildcard с нижней границей.
List<? super Integer> list;
Означает: Список элементов типа Integer или его супертипа (Number, Object).
Иерархия супертипов
Object
|
Number
|
Integer
List<? super Integer> list;
// ✅ Все это можно присвоить
list = new ArrayList<Integer>();
list = new ArrayList<Number>();
list = new ArrayList<Object>();
// ❌ Нельзя - Double не является супертипом Integer
list = new ArrayList<Double>();
Чтение и запись
List<? super Integer> list = new ArrayList<Number>();
// ✅ Можно добавлять Integer и его подтипы
list.add(Integer.valueOf(1)); // ✅ OK
list.add(Integer.valueOf(2)); // ✅ OK
list.add(null); // ✅ OK
// ❌ Нельзя добавлять супертипы Integer
list.add(new Number()); // Ошибка компиляции!
list.add(new Object()); // Ошибка компиляции!
// ⚠️ Можно читать только как Object
Object obj = list.get(0); // ✅ OK
Integer i = list.get(0); // ❌ Ошибка компиляции!
Number n = list.get(0); // ❌ Ошибка компиляции!
Почему можно добавлять Integer?
List<? super Integer> list = new ArrayList<Number>(); // Может быть Number, Object
// Integer можно добавить в любой из супертипов
list.add(Integer.valueOf(1)); // ✅ OK - Integer -> Number
list.add(Integer.valueOf(2)); // ✅ OK - Integer -> Object
Почему нельзя читать как Integer?
List<? super Integer> list = new ArrayList<Object>(); // На самом деле List<Object>
// Если бы можно было:
Integer i = list.get(0); // get() вернет Object, не Integer! Ошибка type safety
Пример: Добавление элементов
public static void addIntegers(List<? super Integer> list, int count) {
for (int i = 0; i < count; i++) {
list.add(i); // ✅ Можем добавлять Integer
}
}
// Использование
List<Integer> integers = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addIntegers(integers, 3); // ✅ OK - [0, 1, 2]
addIntegers(numbers, 3); // ✅ OK - [0, 1, 2]
addIntegers(objects, 3); // ✅ OK - [0, 1, 2]
Get & Put Principle
? super T - это Consumer (потребитель):
- ❌ Get - можно читать только как Object
- ✅ Put - можно писать T (consumes T)
PECS Principle
Producer Extends, Consumer Super
PECS - мнемоническое правило для выбора между extends и super.
Producer → ? extends T → Читаем из коллекции (Get)
Consumer → ? super T → Пишем в коллекцию (Put)
Визуализация
? extends T
(Producer)
↓
[Collection] ----→ (Get) → Your Code
? super T
(Consumer)
↑
Your Code → (Put) → [Collection]
Классический пример: метод copy
// Копирование из source в destination
public static <T> void copy(
List<? extends T> source, // Producer - читаем из source
List<? super T> destination // Consumer - пишем в destination
) {
for (T item : source) { // Get from producer
destination.add(item); // Put to consumer
}
}
Использование:
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Number> numbers = new ArrayList<>();
copy(integers, numbers); // ✅ OK
// integers is producer (? extends T)
// numbers is consumer (? super T)
System.out.println(numbers); // [1, 2, 3]
Почему это работает?
// source: List<? extends T> где T = Number
List<Integer> integers; // ✅ Integer extends Number
// destination: List<? super T> где T = Number
List<Number> numbers; // ✅ Number super Number
List<Object> objects; // ✅ Object super Number
Пример из стандартной библиотеки
Collections.copy()
public static <T> void copy(
List<? super T> dest, // Consumer
List<? extends T> src // Producer
) {
// implementation
}
Collection.addAll()
interface Collection<E> {
boolean addAll(Collection<? extends E> c); // Producer
// ↑
// Коллекция-источник (producer)
}
Почему ? extends E, а не просто E?
List<Number> numbers = new ArrayList<>();
List<Integer> integers = Arrays.asList(1, 2, 3);
// С ? extends E
numbers.addAll(integers); // ✅ OK - гибко
// Если было бы просто E
// numbers.addAll(integers); ❌ Не скомпилировалось бы
Сравнение типов Wildcards
| Wildcard | Чтение | Запись | Use Case |
|---|---|---|---|
? | ✅ Object | ❌ (только null) | Тип не важен, используем методы Object |
? extends T | ✅ T | ❌ (только null) | Producer - читаем элементы как T |
? super T | ⚠️ Object | ✅ T | Consumer - пишем элементы типа T |
T | ✅ T | ✅ T | Полный контроль над типом |
Когда использовать каждый?
// 1. Тип не важен - используй ?
void printSize(Collection<?> c) {
System.out.println(c.size());
}
// 2. Читаем из коллекции - используй ? extends T
double sum(List<? extends Number> numbers) {
double sum = 0;
for (Number n : numbers) {
sum += n.doubleValue();
}
return sum;
}
// 3. Пишем в коллекцию - используй ? super T
void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
// 4. И читаем И пишем - используй конкретный тип T
<T> void sort(List<T> list, Comparator<? super T> c) {
// Нужен полный доступ к list
}
Wildcard Capture
Что такое Wildcard Capture?
Wildcard Capture - это процесс “захвата” wildcard в type variable.
Проблема
public static void swap(List<?> list, int i, int j) {
// ❌ Ошибка компиляции
Object temp = list.get(i);
list.set(i, list.get(j)); // Нельзя! Тип ? несовместим с Object
list.set(j, temp);
}
Проблема: ? - это “какой-то неизвестный тип”, а не Object.
Решение: Helper метод с type parameter
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j); // Делегируем к helper
}
// Helper метод захватывает wildcard в type parameter T
private static <T> void swapHelper(List<T> list, int i, int j) {
T temp = list.get(i); // ✅ OK
list.set(i, list.get(j)); // ✅ OK
list.set(j, temp); // ✅ OK
}
Как работает:
List<?>передается вswapHelper- Компилятор выводит T = capture of ?
- В
swapHelperтип известен как T - Теперь можно читать и писать
Пример: Reverse list
public static void reverse(List<?> list) {
reverseHelper(list);
}
private static <T> void reverseHelper(List<T> list) {
int size = list.size();
for (int i = 0; i < size / 2; i++) {
T temp = list.get(i);
list.set(i, list.get(size - 1 - i));
list.set(size - 1 - i, temp);
}
}
// Использование
List<String> strings = new ArrayList<>(Arrays.asList("a", "b", "c"));
reverse(strings); // ["c", "b", "a"]
Ограничения Wildcards
1. Нельзя создать экземпляр wildcard
// ❌ Нельзя
List<?> list = new ArrayList<?>(); // Ошибка компиляции
// ✅ Можно
List<?> list = new ArrayList<String>();
List<?> list = new ArrayList<>();
2. Нельзя использовать wildcard в new
// ❌ Нельзя
class Box<T> {
public static Box<?> create() {
return new Box<?>(); // Ошибка компиляции
}
}
// ✅ Можно
class Box<T> {
public static Box<?> create() {
return new Box<Object>(); // OK
}
}
3. Нельзя использовать multiple wildcards в type parameter
// ❌ Нельзя
class Pair<?, ?> { } // Ошибка компиляции
// ✅ Можно
class Pair<T, S> { }
Pair<?, ?> pair; // OK - wildcard в использовании, не в объявлении
4. Wildcard не может иметь lower bound в type parameter
// ❌ Нельзя
<T super Number> void method() { } // Ошибка компиляции
// ✅ Можно - только в использовании
void method(List<? super Number> list) { }
5. Нельзя использовать wildcard в extends для классов
// ❌ Нельзя
class MyList extends ArrayList<?> { } // Ошибка компиляции
// ✅ Можно
class MyList<T> extends ArrayList<T> { }
Взаимодействие Wildcards
Вложенные Wildcards
// List списков неизвестного типа
List<List<?>> lists = new ArrayList<>();
lists.add(new ArrayList<String>()); // ✅ OK
lists.add(new ArrayList<Integer>()); // ✅ OK
List<?> firstList = lists.get(0); // ✅ OK
Object obj = firstList.get(0); // ✅ OK
Wildcards в Map
// Map с wildcard в value
Map<String, ? extends Number> map = new HashMap<>();
// Можем читать values как Number
Number n = map.get("key");
// Map с wildcard в key
Map<? extends String, Integer> map = new HashMap<>();
// Можем читать keys как String
// Map с wildcards в обоих
Map<? extends String, ? extends Number> map = new HashMap<>();
Типичные паттерны использования
1. Чтение из коллекций разных типов
// Хотим работать с любым списком чисел
public static void printNumbers(List<? extends Number> numbers) {
for (Number n : numbers) {
System.out.println(n);
}
}
// Использование
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5);
printNumbers(ints); // ✅
printNumbers(doubles); // ✅
2. Запись в коллекции разных типов
// Хотим добавить элементы в коллекцию-супертип
public static void fillWithZeros(List<? super Integer> list, int count) {
for (int i = 0; i < count; i++) {
list.add(0);
}
}
// Использование
List<Integer> ints = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
fillWithZeros(ints, 5); // ✅
fillWithZeros(numbers, 5); // ✅
fillWithZeros(objects, 5); // ✅
3. Поиск максимума
public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
if (list.isEmpty()) {
throw new IllegalArgumentException();
}
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) {
max = item;
}
}
return max;
}
// Использование
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5);
Integer max = max(numbers); // 5
Разбор сигнатуры:
List<? extends T>- список элементов T или подтипа T (producer)T extends Comparable<? super T>- T сравним с собой или супертипом
4. Конвертация коллекций
public static <T, S extends T> List<T> convert(List<S> source) {
List<T> result = new ArrayList<>();
for (S item : source) {
result.add(item); // S автоматически преобразуется к T
}
return result;
}
// Использование
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Number> numbers = convert(integers); // Integer -> Number
5. Union множеств
public static <T> Set<T> union(
Set<? extends T> set1,
Set<? extends T> set2
) {
Set<T> result = new HashSet<>(set1); // Копируем set1
result.addAll(set2); // Добавляем set2
return result;
}
// Использование
Set<Integer> ints = Set.of(1, 2, 3);
Set<Integer> moreInts = Set.of(3, 4, 5);
Set<Integer> allInts = union(ints, moreInts); // {1, 2, 3, 4, 5}
// Можем объединять подтипы
Set<Integer> integers = Set.of(1, 2);
Set<Double> doubles = Set.of(3.0, 4.0);
Set<Number> numbers = union(integers, doubles); // {1, 2, 3.0, 4.0}
Частые ошибки и решения
Ошибка 1: Попытка записи в ? extends
// ❌ Неправильно
List<? extends Number> numbers = new ArrayList<Integer>();
numbers.add(Integer.valueOf(1)); // Ошибка компиляции!
// ✅ Правильно - используй конкретный тип
List<Integer> numbers = new ArrayList<>();
numbers.add(1); // OK
Ошибка 2: Чтение из ? super не как Object
// ❌ Неправильно
List<? super Integer> list = new ArrayList<Number>();
Integer i = list.get(0); // Ошибка компиляции!
// ✅ Правильно - читай как Object
Object obj = list.get(0);
if (obj instanceof Integer) {
Integer i = (Integer) obj;
}
Ошибка 3: Неправильный выбор между extends и super
// ❌ Неправильно - используем super когда нужно extends
public double sum(List<? super Number> numbers) {
double sum = 0;
for (Number n : numbers) { // Ошибка! Нельзя читать как Number
sum += n.doubleValue();
}
return sum;
}
// ✅ Правильно - используем extends для чтения
public double sum(List<? extends Number> numbers) {
double sum = 0;
for (Number n : numbers) { // OK
sum += n.doubleValue();
}
return sum;
}
Ошибка 4: Использование wildcard вместо type parameter
// ❌ Плохой стиль - wildcard используется только один раз
public static void addAll(Collection<? extends E> c) {
// ...
}
// ✅ Лучше использовать type parameter
public static <T extends E> void addAll(Collection<T> c) {
// ...
}
Правило: Если type parameter используется только один раз и нет взаимосвязи между типами, используй wildcard.
Эквивалентность Wildcards
? эквивалентно ? extends Object
List<?> list1;
List<? extends Object> list2; // То же самое
Multiple bounds
// В type parameter
<T extends Number & Comparable<T>>
// Нельзя в wildcard напрямую
List<? extends Number & Comparable> // ❌ Синтаксическая ошибка
// Но можно использовать type parameter
<T extends Number & Comparable<T>> void method(List<? extends T> list) {
// ...
}
Wildcards vs Type Parameters
Когда использовать Wildcard?
✅ Используй wildcard когда:
- Type используется один раз в сигнатуре
- Нет взаимосвязи между типами
- Нужна гибкость без введения type parameter
// ✅ Wildcard - тип используется один раз
void print(List<?> list) {
System.out.println(list);
}
// ✅ Wildcard - нет взаимосвязи между типами аргументов
void process(List<? extends Number> numbers, Set<String> names) {
// ...
}
Когда использовать Type Parameter?
✅ Используй type parameter когда:
- Type используется несколько раз
- Есть взаимосвязь между типами
- Нужен возвращаемый тип
- Нужны multiple bounds
// ✅ Type parameter - взаимосвязь между типами
<T> void copy(List<T> source, List<T> dest) {
// T связывает source и dest
}
// ✅ Type parameter - возвращаемый тип
<T> T getFirst(List<T> list) {
return list.get(0);
}
// ✅ Type parameter - multiple bounds
<T extends Number & Comparable<T>> T max(List<T> list) {
// ...
}
Сравнительная таблица
| Критерий | Wildcard | Type Parameter |
|---|---|---|
| Количество использований | 1 раз | Много раз |
| Возвращаемый тип | ❌ Нет | ✅ Да |
| Multiple bounds | ❌ Нет | ✅ Да |
| Взаимосвязь типов | ❌ Нет | ✅ Да |
| Простота | ✅ Проще | ❌ Сложнее |
| Гибкость | ✅ Больше | ❌ Меньше |
Best Practices
✅ DO (рекомендуется):
-
Используй PECS принцип
<T> void copy( List<? extends T> source, // Producer - extends List<? super T> dest // Consumer - super ) -
Используй
?когда тип не важенvoid printSize(Collection<?> c) { // ✅ System.out.println(c.size()); } -
Используй wildcards для гибкости API
interface Collection<E> { boolean addAll(Collection<? extends E> c); // ✅ Гибко } -
Используй type parameter когда есть взаимосвязь
<T> void swap(List<T> list, int i, int j) // ✅ -
Документируй wildcard bounds
/** * @param numbers список чисел для суммирования (producer) */ double sum(List<? extends Number> numbers)
❌ DON’T (не рекомендуется):
-
Не используй wildcard когда нужен type parameter
// ❌ Плохо List<?> reverse(List<?> list) // ✅ Хорошо <T> List<T> reverse(List<T> list) -
Не используй nested wildcards без необходимости
// ❌ Слишком сложно List<? extends List<? extends Number>> lists // ✅ Проще List<List<? extends Number>> lists -
Не путай extends и super
// ❌ Неправильно для чтения void print(List<? super String> list) // ✅ Правильно void print(List<? extends String> list) -
Не используй wildcards в return type без необходимости
// ❌ Неудобно для вызывающего кода List<?> getList() // ✅ Лучше <T> List<T> getList() -
Не создавай экземпляры с wildcard
// ❌ Нельзя List<?> list = new ArrayList<?>(); // ✅ Можно List<?> list = new ArrayList<String>();
Продвинутые примеры
1. Flexible Map Operations
public class MapUtils {
// Копирование с преобразованием типов
public static <K, V, KK extends K, VV extends V> void putAll(
Map<K, V> target,
Map<? extends KK, ? extends VV> source
) {
for (Map.Entry<? extends KK, ? extends VV> entry : source.entrySet()) {
target.put(entry.getKey(), entry.getValue());
}
}
}
// Использование
Map<Object, Object> target = new HashMap<>();
Map<String, Integer> source = Map.of("one", 1, "two", 2);
MapUtils.putAll(target, source); // ✅ String->Object, Integer->Object
2. Comparator с Wildcards
// Comparator для T или его супертипа
public static <T> void sort(
List<T> list,
Comparator<? super T> comparator
) {
// Можем использовать comparator для T или его родителей
list.sort(comparator);
}
// Использование
List<String> strings = Arrays.asList("banana", "apple", "cherry");
// Comparator<Object> может сравнивать String
Comparator<Object> objectComparator = Comparator.comparing(Object::toString);
sort(strings, objectComparator); // ✅ OK
3. Recursive Wildcards
// Enum pattern
public abstract class Enum<E extends Enum<E>> implements Comparable<E> {
@Override
public int compareTo(E other) {
return name().compareTo(other.name());
}
}
// Использование с wildcard
public static void printEnums(List<? extends Enum<?>> enums) {
for (Enum<?> e : enums) {
System.out.println(e.name());
}
}
Заключение
Ключевые моменты:
- Wildcards обеспечивают гибкость при работе с дженериками
?- неизвестный тип, используй когда тип не важен? extends T- Producer (читаем), верхняя граница? super T- Consumer (пишем), нижняя граница- PECS - Producer Extends, Consumer Super
Правила выбора:
Читаем из коллекции? → ? extends T
Пишем в коллекцию? → ? super T
И читаем И пишем? → T (без wildcard)
Тип вообще не важен? → ?
Когда использовать wildcards:
- ✅ API должен быть гибким
- ✅ Тип используется один раз
- ✅ Нет взаимосвязи между типами
- ✅ Следуй PECS принципу
Когда НЕ использовать wildcards:
- ❌ Возвращаемый тип метода
- ❌ Создание экземпляров
- ❌ Type используется многократно
- ❌ Нужны multiple bounds
Wildcards - мощный инструмент для создания гибкого и type-safe API в Java.
2.2.3. Type Erasure
Стирание типов в runtime
Материалы
Что такое Type Erasure
Type Erasure (стирание типов) - это процесс преобразования параметризованных типов (generics) в обычные классы и интерфейсы во время компиляции.
Зачем нужен Type Erasure?
-
Обратная совместимость (Backward Compatibility)
- Код с дженериками может работать со старым кодом без дженериков
- Старый байт-код может работать с новой JVM
- Библиотеки с дженериками совместимы со старыми версиями
-
Migration Compatibility
- Постепенный переход от legacy кода к коду с дженериками
- Не нужно переписывать весь код сразу
-
Единый байт-код
- Нет дублирования классов для разных типов (как в C++ templates)
- Меньший размер JAR файлов
Компромисс
❌ Цена Type Erasure:
- Информация о типе-параметре недоступна в runtime
- Невозможно создать массив дженериков:
new T[] - Невозможно использовать
instanceofс параметризованными типами - Проблемы с overloading методов
Правила Type Erasure
Type Erasure обозначается как |T| (erasure типа T).
1. Erasure параметризованного типа
|G<T1, T2, ..., Tn>| = |G|
Правило: Параметризованный тип превращается в raw type (erasure самого класса).
// До erasure (compile-time)
List<String> list = new ArrayList<String>();
Map<String, Integer> map = new HashMap<String, Integer>();
// После erasure (runtime)
List list = new ArrayList(); // String стерт
Map map = new HashMap(); // String и Integer стерты
2. Erasure вложенного типа
|T.C| = |T|.C
Правило: Сначала стирается внешний тип, внутренний остается.
// До erasure
Outer<String>.Inner inner;
// После erasure
Outer.Inner inner; // String стерт, но Inner остался
3. Erasure массива
|T[]| = |T|[]
Правило: Стирается тип элементов массива.
// До erasure
List<String>[] arrayOfLists;
// После erasure
List[] arrayOfLists; // String стерт, массив List остался
4. Erasure type variable
|T| = erasure первой границы (leftmost bound)
Правило: Type variable заменяется на erasure его первой границы (bound).
// 1. Без ограничений - становится Object
<T> → Object
// 2. С одним ограничением - становится этим типом
<T extends Number> → Number
// 3. С несколькими ограничениями - становится первым
<T extends Number & Serializable & Comparable> → Number
// 4. Type variable с interface bound
<T extends Comparable> → Comparable
Примеры:
class Box<T> {
private T value; // После erasure: Object value
public void set(T v) { // После erasure: void set(Object v)
value = v;
}
public T get() { // После erasure: Object get()
return value;
}
}
// После type erasure класс превращается в:
class Box {
private Object value;
public void set(Object v) {
value = v;
}
public Object get() {
return value;
}
}
class BoundedBox<T extends Number> {
private T value; // После erasure: Number value
public void set(T v) { // После erasure: void set(Number v)
value = v;
}
public T get() { // После erasure: Number get()
return value;
}
}
// После type erasure:
class BoundedBox {
private Number value;
public void set(Number v) {
value = v;
}
public Number get() {
return value;
}
}
5. Erasure других типов
|Primitive| = Primitive
|Class| = Class
|Interface| = Interface
Правило: Не-дженерик типы остаются без изменений.
int x; // int (без изменений)
String s; // String (без изменений)
Object o; // Object (без изменений)
Erasure сигнатур методов
Erasure применяется также к сигнатурам методов:
signature = (имя_метода, erasure_параметров)
Erasure сигнатуры:
- Имя метода остается
- Type parameters стираются
- Return type стирается (если параметризован)
- Параметры метода стираются
// До erasure
public <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// После erasure
public Comparable max(Comparable a, Comparable b) {
return a.compareTo(b) > 0 ? a : b;
}
// До erasure
public <T> List<T> createList(T... elements) {
return Arrays.asList(elements);
}
// После erasure
public List createList(Object... elements) {
return Arrays.asList(elements);
}
Bridge Methods (мостовые методы)
Компилятор автоматически создает bridge methods для сохранения полиморфизма после erasure.
Проблема без bridge methods
class Node<T> {
public T data;
public void setData(T data) {
this.data = data;
}
}
class MyNode extends Node<Integer> {
@Override
public void setData(Integer data) { // setData(Integer) - новый метод
System.out.println("Setting: " + data);
super.setData(data);
}
}
После erasure:
// Node после erasure
class Node {
public Object data;
public void setData(Object data) { ... }
}
// MyNode после erasure
class MyNode extends Node {
public void setData(Integer data) { ... } // Это НЕ override!
}
Проблема: setData(Integer) не переопределяет setData(Object)!
Решение: Bridge Method
Компилятор создает синтетический bridge method:
class MyNode extends Node {
// Реальный метод
public void setData(Integer data) {
System.out.println("Setting: " + data);
super.setData(data);
}
// Bridge method (создается компилятором)
public void setData(Object data) { // Override родительского метода
setData((Integer) data); // Делегирует к реальному методу
}
}
Bridge method:
- Создается компилятором автоматически
- Имеет сигнатуру родительского метода после erasure
- Делегирует вызов к реальному методу с cast
- Помечен флагом
ACC_BRIDGEиACC_SYNTHETICв байт-коде
Когда создаются bridge methods?
- Override метода с параметризованным типом
- Covariant return types в дженериках
class Node<T> {
public T getData() { return null; }
}
class IntegerNode extends Node<Integer> {
@Override
public Integer getData() { return 42; } // Covariant return
}
// Компилятор создаст bridge:
// public Object getData() {
// return getData(); // Вызовет Integer getData()
// }
Последствия Type Erasure
1. Невозможно получить тип параметра в runtime
public class Box<T> {
public void printType() {
// ❌ Не скомпилируется
System.out.println(T.class);
// ❌ Не скомпилируется
T instance = new T();
// ❌ Runtime будет видеть только Object
if (value instanceof T) { } // Ошибка компиляции
}
}
Почему? В runtime T стирается до Object (или до bound).
2. Невозможно создать массив параметризованного типа
public class GenericArray<T> {
private T[] array;
// ❌ Не скомпилируется
public GenericArray(int size) {
array = new T[size]; // Generic array creation
}
// ✅ Workaround через Object[]
@SuppressWarnings("unchecked")
public GenericArray(int size) {
array = (T[]) new Object[size]; // Unchecked cast warning
}
}
Почему? JVM не знает какой тип создавать в runtime, так как T стерт.
Правильное решение:
public class GenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(Class<T> type, int size) {
// Используем reflection и передаем Class<T>
array = (T[]) Array.newInstance(type, size);
}
}
// Использование
GenericArray<String> arr = new GenericArray<>(String.class, 10);
3. Невозможно использовать instanceof с параметризованными типами
public boolean check(Object obj) {
// ❌ Не скомпилируется
if (obj instanceof List<String>) { }
// ✅ Можно проверить только raw type
if (obj instanceof List) { }
// ✅ Можно проверить с unbounded wildcard
if (obj instanceof List<?>) { } // То же что List
}
Почему? В runtime List<String> и List<Integer> неразличимы - оба стерты до List.
4. Проблемы с overloading
public class MyClass {
// ❌ Ошибка компиляции: same erasure
public void process(List<String> list) { }
public void process(List<Integer> list) { }
// После erasure оба метода имеют одинаковую сигнатуру:
// public void process(List list)
}
Почему? После erasure оба метода имеют одинаковую сигнатуру.
Решение: использовать разные имена методов или дополнительные параметры.
public class MyClass {
// ✅ Разные имена
public void processStrings(List<String> list) { }
public void processIntegers(List<Integer> list) { }
// ✅ Дополнительный параметр
public void process(List<String> list, String dummy) { }
public void process(List<Integer> list, Integer dummy) { }
}
5. Невозможно создать generic exception
// ❌ Не скомпилируется
public class GenericException<T> extends Exception {
private T data;
}
// ❌ Нельзя catch параметризованный тип
catch (GenericException<String> e) { }
Почему? JVM проверяет exception types в runtime, а после erasure все параметры стерты.
6. Static контекст не знает о type parameters
public class Box<T> {
// ❌ Не скомпилируется
private static T defaultValue;
// ❌ Не скомпилируется
public static T getDefault() {
return defaultValue;
}
// ❌ Не скомпилируется
public static void process(T value) { }
}
Почему? Static члены принадлежат классу, а не instance. Type parameter T относится к instance.
Reifiable Types (восстановимые типы)
Reifiable type - тип, чья полная информация доступна в runtime.
Типы, которые reifiable:
- ✅ Primitive types:
int,double,boolean - ✅ Non-generic классы и интерфейсы:
String,Object,Integer - ✅ Raw types:
List,Map - ✅ Параметризованные типы с unbounded wildcards:
List<?>,Map<?, ?> - ✅ Массивы reifiable типов:
int[],String[],List<?>[]
Типы, которые НЕ reifiable:
- ❌ Параметризованные типы:
List<String>,Map<String, Integer> - ❌ Type variables:
T,E,K,V - ❌ Параметризованные типы с type variables:
List<T> - ❌ Bounded wildcards:
List<? extends Number>,List<? super Integer>
Зачем нужна reifiability?
Используется для:
- Создания массивов:
new String[10]✅,new List<String>[10]❌ instanceofchecks:obj instanceof String✅,obj instanceof List<String>❌- Проверки типов в runtime
// ✅ Reifiable - можно создать массив
String[] strings = new String[10];
List[] lists = new List[10]; // Raw type - reifiable
// ❌ Non-reifiable - нельзя создать массив
List<String>[] arrayOfLists = new List<String>[10]; // Ошибка компиляции
Raw Types (сырые типы)
Raw type - это использование дженерик-класса без указания type arguments.
// Generic type
List<String> list = new ArrayList<String>();
// Raw type (без type arguments)
List list = new ArrayList(); // Предупреждение компилятора
Определение Raw Type
Raw type = erasure параметризованного типа.
List<String> → List (raw type)
Map<K, V> → Map (raw type)
Box<T> → Box (raw type)
Зачем нужны Raw Types?
- Совместимость с legacy кодом (до Java 5)
- Migration compatibility
// Старый код (до Java 5)
List list = new ArrayList();
list.add("String");
list.add(Integer.valueOf(42)); // Можно добавить что угодно
// Новый код может работать со старым
List<String> typedList = list; // Unchecked warning
Проблемы с Raw Types
List rawList = new ArrayList();
List<String> stringList = rawList; // Unchecked warning
rawList.add(42); // Ошибка не видна компилятору
String s = stringList.get(0); // ClassCastException в runtime!
Unchecked Warnings
Компилятор выдает unchecked warnings при:
- Присваивании raw type к параметризованному типу
- Вызове методов на raw type, где erasure меняет сигнатуру
- Приведении типов (cast) к параметризованному типу
List rawList = new ArrayList();
List<String> stringList = rawList; // Unchecked assignment warning
rawList.add("test"); // Unchecked call warning
Не используйте raw types в новом коде! Это deprecated practice.
Workarounds для ограничений Type Erasure
1. Передача Class для создания экземпляров
public class Factory<T> {
private Class<T> type;
public Factory(Class<T> type) {
this.type = type;
}
public T create() throws Exception {
return type.getDeclaredConstructor().newInstance();
}
}
// Использование
Factory<String> factory = new Factory<>(String.class);
String str = factory.create();
2. Type Token Pattern
public class TypeReference<T> {
private final Type type;
protected TypeReference() {
Type superclass = getClass().getGenericSuperclass();
this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
}
public Type getType() {
return type;
}
}
// Использование
TypeReference<List<String>> typeRef = new TypeReference<List<String>>() {};
Type type = typeRef.getType(); // List<String> сохранен!
Это работает потому что анонимный класс сохраняет информацию о параметризованном superclass.
3. Супер Type Token (библиотеки Guava, Jackson)
// Guava
TypeToken<List<String>> token = new TypeToken<List<String>>() {};
Type type = token.getType();
// Jackson
JavaType type = mapper.getTypeFactory()
.constructCollectionType(List.class, String.class);
4. Реификация через массивы
@SuppressWarnings("unchecked")
public <T> T[] createArray(T... elements) {
return elements; // Компилятор создаст массив нужного типа
}
// Использование
String[] strings = createArray("a", "b", "c"); // T = String
Heap Pollution (загрязнение кучи)
Heap pollution - ситуация когда переменная параметризованного типа ссылается на объект не того типа.
public static void addToList(List list, Object obj) {
list.add(obj); // Unchecked warning
}
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
addToList(strings, 42); // Добавили Integer в List<String>
// Heap pollution! strings содержит Integer
String s = strings.get(0); // ClassCastException в runtime
}
Причины Heap Pollution
- Unchecked warnings игнорируются
- Raw types используются
- Unchecked casts выполняются
- Varargs с дженериками
// Varargs создает массив
@SafeVarargs // Подавляет предупреждение
public static <T> void addAll(List<T> list, T... elements) {
for (T element : elements) {
list.add(element);
}
}
// Проблема: компилятор создаст Object[]
List<String> strings = new ArrayList<>();
addAll(strings, "a", "b"); // OK
// Но можно сделать так:
List<String> strings = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
addAll(strings, "a", "b");
addAll(numbers, 1, 2);
Как избежать Heap Pollution
- ✅ Не игнорируйте unchecked warnings
- ✅ Не используйте raw types
- ✅ Будьте осторожны с unchecked casts
- ✅ Используйте
@SafeVarargsтолько когда метод точно безопасен - ✅ Используйте
@SuppressWarnings("unchecked")локально и осторожно
Сравнение с другими языками
Java vs C++
| Java Generics | C++ Templates |
|---|---|
| Type Erasure | Code Generation (каждый тип = отдельный класс) |
| Один байт-код для всех типов | Отдельный код для каждого типа |
| Информация о типе теряется | Информация о типе сохраняется |
| Меньший размер программы | Больший размер программы |
| Ограничения в runtime | Больше возможностей в runtime |
| Обратная совместимость | Нет обратной совместимости |
Java vs C#
| Java | C# |
|---|---|
| Type Erasure | Reified Generics |
| Информация стирается | Информация сохраняется в runtime |
Нельзя: new T[] | Можно: new T[] |
Нельзя: T.class | Можно: typeof(T) |
| Только reference types (до Java 10+) | Value types и reference types |
| Bridge methods | Нет bridge methods |
Примеры Type Erasure в действии
Пример 1: Generic класс
// Исходный код
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
// После erasure
public class Pair {
private Object key;
private Object value;
public Pair(Object key, Object value) {
this.key = key;
this.value = value;
}
public Object getKey() { return key; }
public Object getValue() { return value; }
}
Пример 2: Bounded type parameter
// Исходный код
public class NumberBox<T extends Number> {
private T value;
public void set(T value) {
this.value = value;
}
public double getDoubleValue() {
return value.doubleValue(); // Можем вызвать методы Number
}
}
// После erasure
public class NumberBox {
private Number value; // T стерся до Number
public void set(Number value) {
this.value = value;
}
public double getDoubleValue() {
return value.doubleValue();
}
}
Пример 3: Generic метод
// Исходный код
public <T> T first(List<T> list) {
return list.get(0);
}
// После erasure
public Object first(List list) {
return list.get(0);
}
// Использование
List<String> strings = Arrays.asList("a", "b");
String s = first(strings); // Компилятор вставит cast: (String)first(strings)
Пример 4: Множественные bounds
// Исходный код
public <T extends Number & Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// После erasure (первая граница = Number)
public Number max(Number a, Number b) {
return ((Comparable)a).compareTo(b) > 0 ? a : b;
}
Best Practices
✅ DO (рекомендуется):
-
Понимайте ограничения Type Erasure
- Не пытайтесь получить информацию о типе в runtime
- Используйте Class
или TypeToken когда нужна информация о типе
-
Используйте параметризованные типы везде
List<String> list = new ArrayList<>(); // ✅ -
Обращайте внимание на unchecked warnings
- Не игнорируйте без причины
- Понимайте почему warning возникает
-
Используйте bounded type parameters
<T extends Number> T add(T a, T b) // ✅ -
Передавайте Class
когда нужна реификация public <T> T create(Class<T> clazz) throws Exception { return clazz.getDeclaredConstructor().newInstance(); }
❌ DON’T (не рекомендуется):
-
Не используйте raw types
List list = new ArrayList(); // ❌ Raw type -
Не пытайтесь создать массив параметризованных типов
List<String>[] array = new List<String>[10]; // ❌ Не скомпилируется -
Не используйте instanceof с параметризованными типами
if (obj instanceof List<String>) { } // ❌ Не скомпилируется -
Не перегружайте методы с одинаковым erasure
void process(List<String> list) { } // ❌ void process(List<Integer> list) { } // Same erasure -
Не полагайтесь на тип параметра в static контексте
public class Box<T> { private static T instance; // ❌ Не скомпилируется }
Заключение
Ключевые моменты:
-
Type Erasure - это компромисс между:
- Обратной совместимостью ✅
- Информацией о типе в runtime ❌
-
В runtime:
List<String>=List<Integer>=List- Вся информация о type arguments стирается
-
Компилятор вставляет casts автоматически:
String s = list.get(0); → String s = (String)list.get(0); -
Bridge methods сохраняют полиморфизм после erasure
-
Raw types существуют для совместимости, но не используйте их в новом коде
Важно помнить:
- Generics - это compile-time feature
- Type safety проверяется только во время компиляции
- В runtime JVM видит только erased types
- Unchecked warnings - признак потенциальных проблем
Type Erasure - фундаментальная особенность дженериков в Java, которую нужно понимать для эффективного использования системы типов.
2.3. Stream API
Функциональная обработка коллекций
Содержание
- 2.3.1. Создание стримов
- 2.3.2. Промежуточные операции
- 2.3.3. Терминальные операции
- 2.3.4. Collectors
2.3.1. Создание стримов
Stream.of(), Collection.stream(), Arrays.stream()
Материалы
Что такое Stream?
Stream - это последовательность элементов, поддерживающая последовательные и параллельные агрегатные операции.
Ключевые характеристики Stream
-
No storage (Не хранит данные)
- Stream не является структурой данных
- Передает элементы из источника через pipeline операций
-
Functional in nature (Функциональная природа)
- Операции не модифицируют источник
- Создают новый stream с результатом
-
Laziness-seeking (Ленивые вычисления)
- Промежуточные операции выполняются только при терминальной операции
- Оптимизация вычислений
-
Possibly unbounded (Могут быть бесконечными)
- Stream может быть бесконечным
- Short-circuiting операции позволяют завершить обработку
-
Consumable (Одноразовые)
- Элементы можно посетить только один раз
- Для повторной обработки нужен новый stream
Типы Stream
1. Stream - Stream объектов
Stream<String> stringStream;
Stream<Integer> integerStream;
Stream<Person> personStream;
2. IntStream - Stream примитивов int
IntStream intStream;
3. LongStream - Stream примитивов long
LongStream longStream;
4. DoubleStream - Stream примитивов double
DoubleStream doubleStream;
Почему примитивные стримы?
- Избегают boxing/unboxing
- Специализированные методы (sum, average, max, min)
- Лучшая производительность
Способы создания Stream
1. Из Collections - collection.stream()
Последовательный stream
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
Set<Integer> set = Set.of(1, 2, 3);
Stream<Integer> stream = set.stream();
Map<String, Integer> map = Map.of("a", 1, "b", 2);
Stream<Map.Entry<String, Integer>> stream = map.entrySet().stream();
Параллельный stream
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> parallelStream = list.parallelStream();
// Или преобразование последовательного в параллельный
Stream<String> parallelStream = list.stream().parallel();
Примеры с разными коллекциями:
// ArrayList
ArrayList<String> arrayList = new ArrayList<>(Arrays.asList("x", "y", "z"));
Stream<String> stream1 = arrayList.stream();
// LinkedList
LinkedList<Integer> linkedList = new LinkedList<>(Arrays.asList(1, 2, 3));
Stream<Integer> stream2 = linkedList.stream();
// HashSet
Set<String> hashSet = new HashSet<>(Arrays.asList("one", "two"));
Stream<String> stream3 = hashSet.stream();
// TreeSet (отсортированный)
TreeSet<Integer> treeSet = new TreeSet<>(Arrays.asList(5, 2, 8, 1));
Stream<Integer> stream4 = treeSet.stream(); // Упорядоченный: 1, 2, 5, 8
// Queue
Queue<String> queue = new LinkedList<>(Arrays.asList("first", "second"));
Stream<String> stream5 = queue.stream();
2. Из массивов - Arrays.stream()
Для массивов объектов
String[] array = {"a", "b", "c", "d"};
Stream<String> stream = Arrays.stream(array);
// С указанием диапазона (inclusive start, exclusive end)
Stream<String> stream = Arrays.stream(array, 1, 3); // "b", "c"
Для массивов примитивов
// int[]
int[] intArray = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(intArray);
// long[]
long[] longArray = {1L, 2L, 3L};
LongStream longStream = Arrays.stream(longArray);
// double[]
double[] doubleArray = {1.0, 2.0, 3.0};
DoubleStream doubleStream = Arrays.stream(doubleArray);
// С диапазоном
IntStream rangeStream = Arrays.stream(intArray, 1, 4); // 2, 3, 4
Пример обработки:
int[] numbers = {1, 2, 3, 4, 5};
int sum = Arrays.stream(numbers)
.filter(n -> n % 2 == 0) // Четные числа
.sum(); // 2 + 4 = 6
3. Stream.of() - Варадик метод
Создание из перечисленных элементов
// Из элементов
Stream<String> stream = Stream.of("a", "b", "c");
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
// Один элемент
Stream<String> singleElement = Stream.of("only one");
// Из массива
String[] array = {"x", "y", "z"};
Stream<String> stream = Stream.of(array);
Примеры использования:
// Создание и обработка в одной цепочке
long count = Stream.of("apple", "banana", "cherry")
.filter(s -> s.length() > 5)
.count(); // 2 (banana, cherry)
// Различные типы
Stream<Object> mixed = Stream.of("text", 123, 45.6, true);
4. Stream.empty() - Пустой stream
Stream<String> emptyStream = Stream.empty();
// Проверка
emptyStream.count(); // 0
// Полезно для возврата из методов
public Stream<String> findUsers(String query) {
if (query.isEmpty()) {
return Stream.empty(); // Вместо null
}
// ... поиск пользователей
}
5. Stream.generate() - Бесконечный stream с генератором
Синтаксис:
static <T> Stream<T> generate(Supplier<T> s)
Генерация случайных чисел
// Бесконечный stream случайных чисел
Stream<Double> randomNumbers = Stream.generate(Math::random);
// Ограничение количества элементов
Stream<Double> tenRandomNumbers = Stream.generate(Math::random)
.limit(10);
List<Double> numbers = tenRandomNumbers.collect(Collectors.toList());
Генерация константных значений
// Бесконечный stream с одним значением
Stream<String> constants = Stream.generate(() -> "constant");
// Первые 5 элементов
Stream.generate(() -> "Hello")
.limit(5)
.forEach(System.out::println); // Hello (5 раз)
Генерация с изменяемым состоянием (НЕ рекомендуется)
// ❌ Плохой пример - stateful generator
AtomicInteger counter = new AtomicInteger(0);
Stream<Integer> numbers = Stream.generate(counter::incrementAndGet)
.limit(5); // 1, 2, 3, 4, 5
// ✅ Лучше использовать Stream.iterate() для последовательностей
Генерация объектов
// Генерация UUID
Stream<UUID> uuids = Stream.generate(UUID::randomUUID)
.limit(10);
// Генерация дат
Stream<LocalDateTime> timestamps = Stream.generate(LocalDateTime::now)
.limit(3);
6. Stream.iterate() - Бесконечный stream с итерацией
Базовая форма (Java 8+)
Синтаксис:
static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
// Последовательность: 0, 1, 2, 3, 4, ...
Stream<Integer> numbers = Stream.iterate(0, n -> n + 1);
// Первые 10 чисел
Stream.iterate(0, n -> n + 1)
.limit(10)
.forEach(System.out::println); // 0, 1, 2, ..., 9
// Четные числа
Stream.iterate(0, n -> n + 2)
.limit(5)
.forEach(System.out::println); // 0, 2, 4, 6, 8
// Степени двойки
Stream.iterate(1, n -> n * 2)
.limit(10)
.forEach(System.out::println); // 1, 2, 4, 8, 16, ...
Форма с условием (Java 9+)
Синтаксис:
static <T> Stream<T> iterate(T seed, Predicate<T> hasNext, UnaryOperator<T> next)
// Числа от 0 до 9 (эквивалент for loop)
Stream.iterate(0, n -> n < 10, n -> n + 1)
.forEach(System.out::println);
// Фибоначчи до 100
Stream.iterate(new int[]{0, 1},
f -> f[0] < 100,
f -> new int[]{f[1], f[0] + f[1]})
.map(f -> f[0])
.forEach(System.out::println); // 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
Примеры последовательностей:
// Геометрическая прогрессия
Stream.iterate(1.0, n -> n * 1.5)
.limit(5)
.forEach(System.out::println); // 1.0, 1.5, 2.25, 3.375, 5.0625
// Строки
Stream.iterate("a", s -> s + "a")
.limit(5)
.forEach(System.out::println); // a, aa, aaa, aaaa, aaaaa
// Даты
Stream.iterate(LocalDate.now(), date -> date.plusDays(1))
.limit(7)
.forEach(System.out::println); // Следующие 7 дней
7. IntStream, LongStream, DoubleStream методы
IntStream.range() и IntStream.rangeClosed()
// range(start, end) - exclusive end
IntStream.range(1, 5)
.forEach(System.out::println); // 1, 2, 3, 4
// rangeClosed(start, end) - inclusive end
IntStream.rangeClosed(1, 5)
.forEach(System.out::println); // 1, 2, 3, 4, 5
// Полезно для циклов
IntStream.range(0, 10)
.map(i -> i * i)
.forEach(System.out::println); // Квадраты: 0, 1, 4, 9, ..., 81
LongStream.range() и LongStream.rangeClosed()
// Для long значений
LongStream.range(1L, 1000000L)
.parallel()
.sum();
LongStream.rangeClosed(1L, 100L)
.forEach(System.out::println);
Создание из массивов примитивов
// IntStream
IntStream intStream = IntStream.of(1, 2, 3, 4, 5);
// LongStream
LongStream longStream = LongStream.of(10L, 20L, 30L);
// DoubleStream
DoubleStream doubleStream = DoubleStream.of(1.1, 2.2, 3.3);
Генерация примитивных стримов
// IntStream.generate()
IntStream randomInts = IntStream.generate(() -> (int)(Math.random() * 100))
.limit(10);
// IntStream.iterate()
IntStream evenNumbers = IntStream.iterate(0, n -> n + 2)
.limit(10); // 0, 2, 4, ..., 18
8. Stream.builder() - Построитель stream
// Создание builder
Stream.Builder<String> builder = Stream.builder();
// Добавление элементов
builder.add("a");
builder.add("b");
builder.add("c");
// Построение stream
Stream<String> stream = builder.build();
// Или в цепочке
Stream<Integer> stream = Stream.<Integer>builder()
.add(1)
.add(2)
.add(3)
.build();
// Пример динамического построения
Stream.Builder<String> builder = Stream.builder();
for (String name : names) {
if (name.length() > 3) {
builder.add(name);
}
}
Stream<String> filteredStream = builder.build();
Примитивные builders:
// IntStream.Builder
IntStream.Builder intBuilder = IntStream.builder();
intBuilder.add(1).add(2).add(3);
IntStream intStream = intBuilder.build();
// LongStream.Builder
LongStream longStream = LongStream.builder()
.add(10L)
.add(20L)
.build();
// DoubleStream.Builder
DoubleStream doubleStream = DoubleStream.builder()
.add(1.5)
.add(2.5)
.build();
9. Stream.concat() - Объединение stream’ов
Stream<String> stream1 = Stream.of("a", "b", "c");
Stream<String> stream2 = Stream.of("d", "e", "f");
// Объединение
Stream<String> combined = Stream.concat(stream1, stream2);
combined.forEach(System.out::println); // a, b, c, d, e, f
// Объединение нескольких stream'ов
Stream<String> result = Stream.concat(
Stream.concat(stream1, stream2),
stream3
);
// Или через flatMap (рекомендуется для >2 стримов)
Stream<String> result = Stream.of(stream1, stream2, stream3)
.flatMap(s -> s);
Примеры:
// Объединение коллекций
List<Integer> list1 = Arrays.asList(1, 2, 3);
List<Integer> list2 = Arrays.asList(4, 5, 6);
Stream<Integer> combined = Stream.concat(
list1.stream(),
list2.stream()
);
// Объединение с примитивными стримами
IntStream combined = IntStream.concat(
IntStream.range(1, 5),
IntStream.range(10, 15)
); // 1, 2, 3, 4, 10, 11, 12, 13, 14
10. Из строк - String методы
chars() - IntStream символов
String text = "Hello";
IntStream chars = text.chars();
chars.forEach(c -> System.out.print((char)c + " ")); // H e l l o
// Подсчет гласных
long vowelCount = "Hello World".chars()
.filter(c -> "aeiouAEIOU".indexOf(c) != -1)
.count(); // 3
// Преобразование в символы
Stream<Character> charStream = "test".chars()
.mapToObj(c -> (char) c);
codePoints() - IntStream кодовых точек Unicode
String emoji = "Hello 😀 World 🌍";
IntStream codePoints = emoji.codePoints();
codePoints.forEach(cp -> System.out.print(
Character.getName(cp) + " "
));
lines() - Stream строк (Java 11+)
String multiline = "line1\nline2\nline3";
Stream<String> lines = multiline.lines();
lines.forEach(System.out::println);
// line1
// line2
// line3
11. Из файлов - Files и BufferedReader
Files.lines() - Читать строки из файла
import java.nio.file.Files;
import java.nio.file.Paths;
// Чтение всех строк файла
try (Stream<String> lines = Files.lines(Paths.get("file.txt"))) {
lines.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
// С указанием кодировки
try (Stream<String> lines = Files.lines(Paths.get("file.txt"),
StandardCharsets.UTF_8)) {
long count = lines.filter(line -> line.contains("Java"))
.count();
System.out.println("Lines with 'Java': " + count);
}
Важно: Stream нужно закрывать (try-with-resources)!
BufferedReader.lines()
BufferedReader reader = new BufferedReader(
new FileReader("file.txt")
);
Stream<String> lines = reader.lines();
try (Stream<String> stream = lines) {
stream.filter(line -> !line.isEmpty())
.forEach(System.out::println);
}
Files.walk() - Обход дерева файлов
// Рекурсивный обход директории
try (Stream<Path> paths = Files.walk(Paths.get("src"))) {
paths.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
// С ограничением глубины
try (Stream<Path> paths = Files.walk(Paths.get("src"), 2)) {
// Только 2 уровня вглубь
paths.forEach(System.out::println);
}
Files.list() - Список файлов в директории
// Не рекурсивный (только текущая директория)
try (Stream<Path> paths = Files.list(Paths.get("."))) {
paths.filter(Files::isDirectory)
.forEach(System.out::println);
}
Files.find() - Поиск файлов с условием
try (Stream<Path> paths = Files.find(
Paths.get("src"),
Integer.MAX_VALUE, // max depth
(path, attrs) -> path.toString().endsWith(".java")
)) {
paths.forEach(System.out::println);
}
12. Random числа - Random класс
Random.ints()
Random random = new Random();
// Бесконечный stream случайных int
IntStream infiniteInts = random.ints();
// Ограниченное количество
IntStream tenInts = random.ints(10); // 10 случайных int
// С диапазоном [origin, bound)
IntStream boundedInts = random.ints(10, 0, 100); // 10 чисел от 0 до 99
// Использование
random.ints(5, 1, 11) // 5 чисел от 1 до 10
.forEach(System.out::println);
Random.longs()
Random random = new Random();
// Случайные long числа
LongStream longs = random.longs(5);
// С границами
LongStream boundedLongs = random.longs(10, 0L, 1000L);
Random.doubles()
Random random = new Random();
// Случайные double числа [0.0, 1.0)
DoubleStream doubles = random.doubles(10);
// С границами
DoubleStream boundedDoubles = random.doubles(5, 0.0, 100.0);
// Пример: генерация случайных цен
random.doubles(10, 9.99, 99.99)
.forEach(price -> System.out.printf("$%.2f%n", price));
ThreadLocalRandom (для многопоточности)
// Лучше для многопоточных приложений
IntStream randomInts = ThreadLocalRandom.current()
.ints(10, 0, 100);
13. Pattern.splitAsStream() - Разделение строки
import java.util.regex.Pattern;
String text = "one,two,three,four";
Pattern pattern = Pattern.compile(",");
Stream<String> parts = pattern.splitAsStream(text);
parts.forEach(System.out::println);
// one
// two
// three
// four
// Сложная регулярка
Pattern whitespace = Pattern.compile("\\s+");
Stream<String> words = whitespace.splitAsStream("Hello World Test");
words.forEach(System.out::println); // Hello, World, Test
14. BitSet.stream() - Stream установленных битов
import java.util.BitSet;
BitSet bitSet = new BitSet();
bitSet.set(1);
bitSet.set(3);
bitSet.set(5);
bitSet.set(7);
// Stream индексов установленных битов
IntStream indices = bitSet.stream();
indices.forEach(System.out::println); // 1, 3, 5, 7
15. JarFile.stream() - Stream записей JAR файла
import java.util.jar.JarFile;
try (JarFile jar = new JarFile("library.jar")) {
Stream<JarEntry> entries = jar.stream();
entries.filter(entry -> entry.getName().endsWith(".class"))
.forEach(entry -> System.out.println(entry.getName()));
}
16. Optional.stream() - Stream из Optional (Java 9+)
Optional<String> optional = Optional.of("value");
// Преобразование Optional в Stream
Stream<String> stream = optional.stream();
stream.forEach(System.out::println); // value
// Пустой Optional -> пустой Stream
Optional<String> empty = Optional.empty();
Stream<String> emptyStream = empty.stream(); // Пустой stream
// Полезно для flatMap
List<Optional<String>> optionals = Arrays.asList(
Optional.of("a"),
Optional.empty(),
Optional.of("b")
);
Stream<String> values = optionals.stream()
.flatMap(Optional::stream);
values.forEach(System.out::println); // a, b (empty пропущен)
17. Collection специфичные методы
Map.entrySet().stream()
Map<String, Integer> map = Map.of(
"one", 1,
"two", 2,
"three", 3
);
// Stream из Entry
Stream<Map.Entry<String, Integer>> entries = map.entrySet().stream();
// Обработка
map.entrySet().stream()
.filter(entry -> entry.getValue() > 1)
.forEach(entry -> System.out.println(
entry.getKey() + ": " + entry.getValue()
));
Map.keySet().stream()
Stream<String> keys = map.keySet().stream();
keys.forEach(System.out::println); // one, two, three
Map.values().stream()
Stream<Integer> values = map.values().stream();
int sum = values.mapToInt(Integer::intValue).sum();
18. StreamSupport - Низкоуровневое создание
Из Spliterator
import java.util.stream.StreamSupport;
List<String> list = Arrays.asList("a", "b", "c");
Spliterator<String> spliterator = list.spliterator();
Stream<String> stream = StreamSupport.stream(spliterator, false);
// ^^^^
// parallel?
Из Supplier
Supplier<Spliterator<String>> supplier = () -> list.spliterator();
Stream<String> stream = StreamSupport.stream(supplier,
Spliterator.ORDERED,
false);
Преобразование между типами Stream
Объектный Stream → Примитивный Stream
// Stream<Integer> -> IntStream
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);
IntStream intStream = integerStream.mapToInt(Integer::intValue);
// Stream<Long> -> LongStream
Stream<Long> longObjStream = Stream.of(1L, 2L, 3L);
LongStream longStream = longObjStream.mapToLong(Long::longValue);
// Stream<Double> -> DoubleStream
Stream<Double> doubleObjStream = Stream.of(1.0, 2.0, 3.0);
DoubleStream doubleStream = doubleObjStream.mapToDouble(Double::doubleValue);
Примитивный Stream → Объектный Stream
// IntStream -> Stream<Integer>
IntStream intStream = IntStream.range(1, 5);
Stream<Integer> boxedStream = intStream.boxed();
// LongStream -> Stream<Long>
LongStream longStream = LongStream.range(1, 5);
Stream<Long> boxedLongs = longStream.boxed();
// DoubleStream -> Stream<Double>
DoubleStream doubleStream = DoubleStream.of(1.0, 2.0);
Stream<Double> boxedDoubles = doubleStream.boxed();
// Через mapToObj
IntStream intStream = IntStream.range(1, 5);
Stream<String> strings = intStream.mapToObj(i -> "Number: " + i);
Между примитивными Stream
// IntStream -> LongStream
IntStream intStream = IntStream.range(1, 5);
LongStream longStream = intStream.asLongStream();
// IntStream -> DoubleStream
IntStream intStream2 = IntStream.range(1, 5);
DoubleStream doubleStream = intStream2.asDoubleStream();
// LongStream -> DoubleStream
LongStream longStream2 = LongStream.range(1, 5);
DoubleStream doubleStream2 = longStream2.asDoubleStream();
Практические примеры создания Stream
1. Чтение CSV файла
try (Stream<String> lines = Files.lines(Paths.get("data.csv"))) {
lines.skip(1) // Пропустить заголовок
.map(line -> line.split(","))
.forEach(columns -> System.out.println(
"Name: " + columns[0] + ", Age: " + columns[1]
));
}
2. Генерация тестовых данных
// 100 случайных пользователей
List<User> users = Stream.generate(() -> new User(
UUID.randomUUID().toString(),
"User" + ThreadLocalRandom.current().nextInt(1000),
ThreadLocalRandom.current().nextInt(18, 80)
))
.limit(100)
.collect(Collectors.toList());
3. Последовательность дат
// Все дни текущего месяца
LocalDate start = LocalDate.now().withDayOfMonth(1);
LocalDate end = start.plusMonths(1);
Stream<LocalDate> datesInMonth = Stream.iterate(start,
date -> date.isBefore(end),
date -> date.plusDays(1));
datesInMonth.forEach(System.out::println);
4. Объединение данных из разных источников
// Из базы данных
Stream<User> dbUsers = userRepository.findAll().stream();
// Из файла
Stream<User> fileUsers = Files.lines(Paths.get("users.txt"))
.map(User::fromString);
// Из API
Stream<User> apiUsers = apiClient.getUsers().stream();
// Объединение
Stream<User> allUsers = Stream.of(dbUsers, fileUsers, apiUsers)
.flatMap(s -> s)
.distinct(); // Удалить дубликаты
5. Фильтрация файлов
// Найти все .java файлы больше 1KB
try (Stream<Path> paths = Files.walk(Paths.get("src"))) {
paths.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.filter(p -> {
try {
return Files.size(p) > 1024;
} catch (IOException e) {
return false;
}
})
.forEach(System.out::println);
}
6. Генерация последовательности Фибоначчи
// Первые 20 чисел Фибоначчи
Stream.iterate(new BigInteger[]{BigInteger.ZERO, BigInteger.ONE},
f -> new BigInteger[]{f[1], f[0].add(f[1])})
.limit(20)
.map(f -> f[0])
.forEach(System.out::println);
7. Обработка логов
try (Stream<String> logs = Files.lines(Paths.get("app.log"))) {
// Подсчет ошибок
long errorCount = logs.filter(line -> line.contains("ERROR"))
.count();
System.out.println("Total errors: " + errorCount);
}
Параллельные Stream
Создание параллельного stream
// Из коллекции
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> parallelStream = numbers.parallelStream();
// Преобразование последовательного в параллельный
Stream<Integer> stream = numbers.stream();
Stream<Integer> parallel = stream.parallel();
// Проверка режима
boolean isParallel = stream.isParallel();
// Обратно в последовательный
Stream<Integer> sequential = parallel.sequential();
Когда использовать параллельный stream?
✅ Используй параллельный stream когда:
- Большой объем данных (>10000 элементов)
- Операции CPU-intensive
- Операции независимы (stateless)
- Нет гонки данных (race conditions)
❌ НЕ используй параллельный stream когда:
- Маленький набор данных
- I/O операции
- Операции с shared state
- Порядок элементов важен
Пример:
// Хорошо для параллельной обработки
List<Integer> largeList = IntStream.range(0, 1_000_000)
.boxed()
.collect(Collectors.toList());
long sum = largeList.parallelStream()
.mapToLong(i -> i * i)
.sum();
// Плохо для параллельной обработки (I/O)
files.parallelStream() // ❌ Не рекомендуется
.forEach(file -> writeToDatabase(file));
Best Practices
✅ DO (рекомендуется):
-
Используй try-with-resources для файловых stream’ов
try (Stream<String> lines = Files.lines(Paths.get("file.txt"))) { lines.forEach(System.out::println); } -
Используй примитивные stream’ы для примитивов
IntStream.range(1, 100).sum(); // ✅ Эффективно Stream.of(1, 2, 3).mapToInt(i -> i).sum(); // ❌ Избыточно -
Ограничивай бесконечные stream’ы
Stream.iterate(0, n -> n + 1) .limit(100) // ✅ Ограничение обязательно .forEach(System.out::println); -
Используй метод reference когда возможно
stream.map(String::toUpperCase) // ✅ stream.map(s -> s.toUpperCase()) // Работает, но менее идиоматично -
Закрывай stream’ы из Files
try (Stream<String> lines = Files.lines(path)) { // обработка } // Автоматически закроется
❌ DON’T (не рекомендуется):
-
Не переиспользуй stream
Stream<String> stream = list.stream(); stream.forEach(System.out::println); stream.forEach(System.out::println); // ❌ IllegalStateException! -
Не модифицируй источник во время обработки
list.stream() .forEach(item -> list.add(item)); // ❌ ConcurrentModificationException -
Не используй parallelStream() бездумно
smallList.parallelStream() // ❌ Overhead > benefit .filter(...) .collect(Collectors.toList()); -
Не забывай про limit() для бесконечных stream’ов
Stream.generate(Math::random) .forEach(System.out::println); // ❌ Бесконечный цикл! -
Не используй peek() для изменения элементов
stream.peek(list::add) // ❌ Не гарантируется выполнение .collect(Collectors.toList()); // ✅ Используй map или forEach
Сводная таблица способов создания
| Источник | Метод | Пример |
|---|---|---|
| Collection | .stream() | list.stream() |
| Collection | .parallelStream() | list.parallelStream() |
| Массив | Arrays.stream() | Arrays.stream(array) |
| Элементы | Stream.of() | Stream.of(1, 2, 3) |
| Пустой | Stream.empty() | Stream.empty() |
| Генератор | Stream.generate() | Stream.generate(Math::random) |
| Итерация | Stream.iterate() | Stream.iterate(0, n -> n + 1) |
| Диапазон | IntStream.range() | IntStream.range(1, 10) |
| Builder | Stream.builder() | Stream.builder().add(1).build() |
| Конкатенация | Stream.concat() | Stream.concat(s1, s2) |
| Файл | Files.lines() | Files.lines(path) |
| Директория | Files.walk() | Files.walk(path) |
| Случайные числа | Random.ints() | new Random().ints(10) |
| Строка | .chars() | "text".chars() |
| Регулярка | Pattern.splitAsStream() | pattern.splitAsStream(text) |
| Optional | .stream() | optional.stream() |
Заключение
Ключевые моменты:
- Stream - это абстракция для работы с последовательностями данных
- Существует много способов создания stream’ов
- Примитивные stream’ы (IntStream, LongStream, DoubleStream) эффективнее
- Stream’ы одноразовые - после использования нужно создавать новый
- Ленивые вычисления - промежуточные операции выполняются только при терминальной
- Параллельные stream’ы эффективны только для больших данных
- Файловые stream’ы нужно закрывать
Выбор способа создания:
Коллекция? → collection.stream()
Массив? → Arrays.stream(array)
Известные элементы? → Stream.of(...)
Нужен диапазон? → IntStream.range(start, end)
Генерация данных? → Stream.generate() или Stream.iterate()
Файл? → Files.lines(path)
Случайные числа? → Random.ints()
Строка? → string.chars() или string.lines()
Stream API - мощный инструмент для функционального программирования в Java!
2.3.2. Промежуточные операции
filter(), map(), flatMap(), sorted(), distinct(), peek(), limit(), skip()
Материалы
| Тип | Ссылка |
|---|---|
| Документ | Stream Operations |
| Видео | ссылка |
Что такое промежуточные операции?
Промежуточные операции (intermediate operations) - это операции, которые:
- Возвращают новый Stream
- Ленивые (lazy) - выполняются только при вызове терминальной операции
- Можно объединять в цепочки (pipeline)
Источник → filter() → map() → sorted() → [терминальная операция]
↑ ↑ ↑
промежуточные операции (lazy)
Stateless vs Stateful операции
Stateless (без состояния):
- Обрабатывают каждый элемент независимо
filter(),map(),flatMap(),peek()
Stateful (с состоянием):
- Требуют знания о других элементах
sorted(),distinct(),limit(),skip()- Могут требовать буферизации всего потока
filter() - Фильтрация элементов
Оставляет только элементы, удовлетворяющие условию (predicate).
Сигнатура
Stream<T> filter(Predicate<? super T> predicate)
Базовые примеры
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Только четные числа
List<Integer> evens = numbers.stream()
.filter(n -> n % 2 == 0)
.toList(); // [2, 4, 6, 8, 10]
// Только положительные
List<Integer> positive = numbers.stream()
.filter(n -> n > 0)
.toList();
// Фильтрация строк
List<String> names = List.of("Alice", "Bob", "Anna", "Alex");
List<String> startsWithA = names.stream()
.filter(name -> name.startsWith("A"))
.toList(); // [Alice, Anna, Alex]
Множественные условия
// Несколько filter() подряд
List<Integer> result = numbers.stream()
.filter(n -> n > 3)
.filter(n -> n < 8)
.filter(n -> n % 2 == 0)
.toList(); // [4, 6]
// Или объединенный predicate
List<Integer> result = numbers.stream()
.filter(n -> n > 3 && n < 8 && n % 2 == 0)
.toList(); // [4, 6]
Фильтрация объектов
record Person(String name, int age, String city) {}
List<Person> people = List.of(
new Person("Alice", 30, "Moscow"),
new Person("Bob", 25, "SPb"),
new Person("Charlie", 35, "Moscow")
);
// Взрослые из Москвы
List<Person> result = people.stream()
.filter(p -> p.age() >= 30)
.filter(p -> p.city().equals("Moscow"))
.toList();
Фильтрация с null-безопасностью
List<String> items = Arrays.asList("a", null, "b", null, "c");
// Убрать null
List<String> nonNull = items.stream()
.filter(Objects::nonNull)
.toList(); // [a, b, c]
// Или через лямбду
List<String> nonNull = items.stream()
.filter(s -> s != null)
.toList();
Использование method reference
// Вместо лямбды
List<String> nonEmpty = strings.stream()
.filter(s -> !s.isEmpty())
.toList();
// Method reference с Predicate.not()
List<String> nonEmpty = strings.stream()
.filter(Predicate.not(String::isEmpty))
.toList();
map() - Преобразование элементов
Преобразует каждый элемент в другой элемент (возможно другого типа).
Сигнатура
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
Базовые примеры
List<String> names = List.of("alice", "bob", "charlie");
// К верхнему регистру
List<String> upper = names.stream()
.map(String::toUpperCase)
.toList(); // [ALICE, BOB, CHARLIE]
// Длина каждой строки
List<Integer> lengths = names.stream()
.map(String::length)
.toList(); // [5, 3, 7]
// Числа -> их квадраты
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
List<Integer> squares = numbers.stream()
.map(n -> n * n)
.toList(); // [1, 4, 9, 16, 25]
Преобразование типов
// String -> Integer
List<String> strNumbers = List.of("1", "2", "3");
List<Integer> integers = strNumbers.stream()
.map(Integer::parseInt)
.toList(); // [1, 2, 3]
// Person -> String (имя)
List<String> names = people.stream()
.map(Person::name)
.toList();
// Person -> PersonDTO
List<PersonDTO> dtos = people.stream()
.map(p -> new PersonDTO(p.name(), p.age()))
.toList();
Цепочки map()
List<String> result = names.stream()
.map(String::trim) // Убрать пробелы
.map(String::toLowerCase) // К нижнему регистру
.map(s -> s + "!") // Добавить суффикс
.toList();
mapToInt(), mapToLong(), mapToDouble()
Для преобразования в примитивные стримы (избегают boxing).
List<String> words = List.of("one", "two", "three");
// Stream<String> -> IntStream
int totalLength = words.stream()
.mapToInt(String::length)
.sum(); // 11
// Среднее значение
double avgLength = words.stream()
.mapToInt(String::length)
.average()
.orElse(0.0); // 3.67
// Сумма возрастов
int totalAge = people.stream()
.mapToInt(Person::age)
.sum();
flatMap() - Выравнивание вложенных структур
Преобразует каждый элемент в Stream и объединяет все потоки в один.
Сигнатура
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
Визуализация
map(): [1, 2] -> [[1], [2]] (Stream of Streams)
flatMap(): [1, 2] -> [1, 2] (плоский Stream)
Пример:
[[a, b], [c, d, e]] -> flatMap -> [a, b, c, d, e]
Базовые примеры
// Список списков -> плоский список
List<List<Integer>> nested = List.of(
List.of(1, 2, 3),
List.of(4, 5),
List.of(6, 7, 8, 9)
);
List<Integer> flat = nested.stream()
.flatMap(List::stream)
.toList(); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
// Массивы
String[][] arrays = {{"a", "b"}, {"c", "d", "e"}};
List<String> flat = Arrays.stream(arrays)
.flatMap(Arrays::stream)
.toList(); // [a, b, c, d, e]
Разделение строк
List<String> sentences = List.of(
"Hello world",
"Java Stream API",
"flatMap example"
);
// Все слова
List<String> words = sentences.stream()
.flatMap(s -> Arrays.stream(s.split(" ")))
.toList();
// [Hello, world, Java, Stream, API, flatMap, example]
// Все символы
List<Character> chars = sentences.stream()
.flatMap(s -> s.chars().mapToObj(c -> (char) c))
.toList();
flatMap с Optional
List<Optional<String>> optionals = List.of(
Optional.of("a"),
Optional.empty(),
Optional.of("b"),
Optional.empty(),
Optional.of("c")
);
// Извлечь только присутствующие значения
List<String> values = optionals.stream()
.flatMap(Optional::stream)
.toList(); // [a, b, c]
Практический пример: заказы и товары
record Order(String id, List<Item> items) {}
record Item(String name, double price) {}
List<Order> orders = List.of(
new Order("1", List.of(new Item("Book", 20), new Item("Pen", 5))),
new Order("2", List.of(new Item("Laptop", 1000))),
new Order("3", List.of(new Item("Mouse", 50), new Item("Keyboard", 100)))
);
// Все товары из всех заказов
List<Item> allItems = orders.stream()
.flatMap(order -> order.items().stream())
.toList();
// Все названия товаров
List<String> itemNames = orders.stream()
.flatMap(order -> order.items().stream())
.map(Item::name)
.toList(); // [Book, Pen, Laptop, Mouse, Keyboard]
// Общая сумма всех заказов
double total = orders.stream()
.flatMap(order -> order.items().stream())
.mapToDouble(Item::price)
.sum(); // 1175.0
flatMapToInt(), flatMapToLong(), flatMapToDouble()
List<int[]> arrays = List.of(
new int[]{1, 2},
new int[]{3, 4, 5}
);
int sum = arrays.stream()
.flatMapToInt(Arrays::stream)
.sum(); // 15
sorted() - Сортировка
Сортирует элементы потока.
Сигнатуры
Stream<T> sorted() // natural order
Stream<T> sorted(Comparator<? super T> c) // custom order
Natural ordering
List<Integer> numbers = List.of(5, 2, 8, 1, 9, 3);
// По возрастанию (natural order)
List<Integer> sorted = numbers.stream()
.sorted()
.toList(); // [1, 2, 3, 5, 8, 9]
// Строки - лексикографически
List<String> names = List.of("Charlie", "Alice", "Bob");
List<String> sorted = names.stream()
.sorted()
.toList(); // [Alice, Bob, Charlie]
Custom Comparator
// По убыванию
List<Integer> descending = numbers.stream()
.sorted(Comparator.reverseOrder())
.toList(); // [9, 8, 5, 3, 2, 1]
// По длине строки
List<String> byLength = names.stream()
.sorted(Comparator.comparing(String::length))
.toList(); // [Bob, Alice, Charlie]
// По длине, затем алфавитно
List<String> sorted = names.stream()
.sorted(Comparator.comparing(String::length)
.thenComparing(Comparator.naturalOrder()))
.toList();
Сортировка объектов
record Person(String name, int age) {}
List<Person> people = List.of(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Charlie", 35)
);
// По возрасту
List<Person> byAge = people.stream()
.sorted(Comparator.comparing(Person::age))
.toList();
// По имени в обратном порядке
List<Person> byNameDesc = people.stream()
.sorted(Comparator.comparing(Person::name).reversed())
.toList();
// По возрасту, затем по имени
List<Person> sorted = people.stream()
.sorted(Comparator.comparing(Person::age)
.thenComparing(Person::name))
.toList();
Сортировка с null
List<String> withNulls = Arrays.asList("b", null, "a", null, "c");
// null в начало
List<String> sorted = withNulls.stream()
.sorted(Comparator.nullsFirst(Comparator.naturalOrder()))
.toList(); // [null, null, a, b, c]
// null в конец
List<String> sorted = withNulls.stream()
.sorted(Comparator.nullsLast(Comparator.naturalOrder()))
.toList(); // [a, b, c, null, null]
Важно: sorted() - stateful операция
// sorted() буферизует все элементы перед сортировкой
// Не подходит для бесконечных потоков!
Stream.iterate(0, n -> n + 1)
.sorted() // Зависнет - пытается собрать бесконечный поток
.limit(10)
.forEach(System.out::println);
// Правильно: сначала limit(), потом sorted()
Stream.iterate(0, n -> n + 1)
.limit(10)
.sorted()
.forEach(System.out::println);
distinct() - Удаление дубликатов
Удаляет повторяющиеся элементы (по equals()).
Сигнатура
Stream<T> distinct()
Базовые примеры
List<Integer> numbers = List.of(1, 2, 2, 3, 3, 3, 4, 4, 4, 4);
List<Integer> unique = numbers.stream()
.distinct()
.toList(); // [1, 2, 3, 4]
// Строки
List<String> words = List.of("apple", "banana", "apple", "cherry", "banana");
List<String> unique = words.stream()
.distinct()
.toList(); // [apple, banana, cherry]
Порядок сохраняется
// distinct() сохраняет порядок первого появления
List<Integer> numbers = List.of(3, 1, 2, 1, 3, 2);
List<Integer> unique = numbers.stream()
.distinct()
.toList(); // [3, 1, 2] - порядок первого появления
distinct() для объектов
Важно: distinct() использует equals() и hashCode().
record Person(String name, int age) {}
List<Person> people = List.of(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Alice", 30), // дубликат
new Person("Charlie", 35)
);
List<Person> unique = people.stream()
.distinct()
.toList(); // 3 элемента (Alice,30 только один раз)
Уникальность по полю
// distinct() по конкретному полю - нужен workaround
// Способ 1: через Set
Set<String> seen = new HashSet<>();
List<Person> uniqueByName = people.stream()
.filter(p -> seen.add(p.name())) // add возвращает false если уже есть
.toList();
// Способ 2: через Collectors.toMap
List<Person> uniqueByName = people.stream()
.collect(Collectors.toMap(
Person::name,
p -> p,
(p1, p2) -> p1 // при дубликате - оставить первый
))
.values()
.stream()
.toList();
peek() - Отладка и побочные эффекты
Выполняет действие для каждого элемента без изменения потока.
Сигнатура
Stream<T> peek(Consumer<? super T> action)
Отладка pipeline
List<Integer> result = List.of(1, 2, 3, 4, 5).stream()
.filter(n -> n > 2)
.peek(n -> System.out.println("После filter: " + n))
.map(n -> n * 2)
.peek(n -> System.out.println("После map: " + n))
.toList();
// Вывод:
// После filter: 3
// После map: 6
// После filter: 4
// После map: 8
// После filter: 5
// После map: 10
Предупреждение: peek() не гарантирует выполнение
// peek() может не выполниться для всех элементов!
List.of(1, 2, 3).stream()
.peek(System.out::println); // Ничего не выведет - нет терминальной операции
// Даже с терминальной операцией - зависит от оптимизаций
List.of(1, 2, 3).stream()
.peek(System.out::println)
.count(); // Может не вызвать peek() (оптимизация в Java 9+)
Когда использовать peek()
peek() предназначен для отладки.
Не используй для побочных эффектов в production коде!
// Плохо - побочный эффект в peek()
List<String> collected = new ArrayList<>();
stream.peek(collected::add) // Непредсказуемо!
.count();
// Хорошо - используй forEach или collect
stream.forEach(collected::add);
// или
List<String> collected = stream.toList();
limit() - Ограничение количества элементов
Ограничивает поток первыми N элементами.
Сигнатура
Stream<T> limit(long maxSize)
Базовые примеры
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Первые 3 элемента
List<Integer> first3 = numbers.stream()
.limit(3)
.toList(); // [1, 2, 3]
// Топ-5 после сортировки
List<Integer> top5 = numbers.stream()
.sorted(Comparator.reverseOrder())
.limit(5)
.toList(); // [10, 9, 8, 7, 6]
limit() с бесконечными потоками
// Без limit() - бесконечный цикл
Stream.generate(Math::random)
.forEach(System.out::println); // Никогда не закончится
// С limit() - работает
List<Double> randomNumbers = Stream.generate(Math::random)
.limit(5)
.toList(); // 5 случайных чисел
// Последовательность чисел
List<Integer> sequence = Stream.iterate(0, n -> n + 1)
.limit(10)
.toList(); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Short-circuiting
limit() - это short-circuiting операция: она может завершить обработку раньше.
List.of(1, 2, 3, 4, 5).stream()
.peek(n -> System.out.println("Processing: " + n))
.limit(3)
.toList();
// Вывод:
// Processing: 1
// Processing: 2
// Processing: 3
// Элементы 4 и 5 не обрабатываются!
skip() - Пропуск элементов
Пропускает первые N элементов.
Сигнатура
Stream<T> skip(long n)
Базовые примеры
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Пропустить первые 3
List<Integer> result = numbers.stream()
.skip(3)
.toList(); // [4, 5, 6, 7, 8, 9, 10]
// Пропустить больше чем есть
List<Integer> result = numbers.stream()
.skip(100)
.toList(); // [] (пустой список)
Пагинация: skip() + limit()
int pageSize = 5;
int pageNumber = 2; // начиная с 0
List<Integer> page = numbers.stream()
.skip((long) pageNumber * pageSize)
.limit(pageSize)
.toList(); // Страница 2 (индекс с 0): элементы 10-14
Пример пагинации
public <T> List<T> getPage(List<T> items, int page, int size) {
return items.stream()
.skip((long) page * size)
.limit(size)
.toList();
}
// Использование
List<String> items = List.of("a", "b", "c", "d", "e", "f", "g", "h");
List<String> page0 = getPage(items, 0, 3); // [a, b, c]
List<String> page1 = getPage(items, 1, 3); // [d, e, f]
List<String> page2 = getPage(items, 2, 3); // [g, h]
takeWhile() и dropWhile() (Java 9+)
takeWhile() - брать пока условие true
Stream<T> takeWhile(Predicate<? super T> predicate)
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 1, 2);
// Брать пока < 4
List<Integer> result = numbers.stream()
.takeWhile(n -> n < 4)
.toList(); // [1, 2, 3]
// Отличие от filter():
// filter() проверяет ВСЕ элементы
// takeWhile() останавливается при первом false
dropWhile() - пропускать пока условие true
Stream<T> dropWhile(Predicate<? super T> predicate)
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 1, 2);
// Пропускать пока < 4, затем взять остальное
List<Integer> result = numbers.stream()
.dropWhile(n -> n < 4)
.toList(); // [4, 5, 1, 2]
Практический пример
// Логи с временными метками
List<LogEntry> logs = ...;
// Пропустить старые логи, взять начиная с определенного времени
LocalDateTime startTime = LocalDateTime.of(2024, 1, 1, 12, 0);
List<LogEntry> recentLogs = logs.stream()
.dropWhile(log -> log.timestamp().isBefore(startTime))
.toList();
mapMulti() (Java 16+)
Альтернатива flatMap() для более эффективного маппинга.
Сигнатура
<R> Stream<R> mapMulti(BiConsumer<T, Consumer<R>> mapper)
Пример
List<Integer> numbers = List.of(1, 2, 3);
// С flatMap
List<Integer> result = numbers.stream()
.flatMap(n -> Stream.of(n, n * 2))
.toList(); // [1, 2, 2, 4, 3, 6]
// С mapMulti (эффективнее - не создает промежуточные Stream)
List<Integer> result = numbers.stream()
.<Integer>mapMulti((n, consumer) -> {
consumer.accept(n);
consumer.accept(n * 2);
})
.toList(); // [1, 2, 2, 4, 3, 6]
Когда использовать mapMulti
- Когда из одного элемента нужно получить 0, 1 или несколько элементов
- Когда создание Stream для каждого элемента неэффективно
- Когда логика генерации сложная (условная)
// Фильтрация + преобразование в одной операции
List<Integer> result = objects.stream()
.<Integer>mapMulti((obj, consumer) -> {
if (obj instanceof String s) {
consumer.accept(s.length());
}
})
.toList();
Порядок операций имеет значение
Эффективный порядок
// Плохо: сначала сортировка всех, потом фильтрация
List<Person> result = people.stream()
.sorted(Comparator.comparing(Person::age)) // Сортирует ВСЕ элементы
.filter(p -> p.age() > 30)
.limit(10)
.toList();
// Хорошо: сначала фильтрация, потом сортировка меньшего набора
List<Person> result = people.stream()
.filter(p -> p.age() > 30) // Уменьшает набор
.sorted(Comparator.comparing(Person::age)) // Сортирует только отфильтрованные
.limit(10)
.toList();
Правило оптимизации
1. filter() - уменьшить количество элементов
2. map() - преобразовать
3. sorted() - сортировать (уже уменьшенный набор)
4. limit() - ограничить
Комбинирование операций
Практический пример: обработка данных
record Transaction(String id, String type, double amount, LocalDate date) {}
List<Transaction> transactions = ...;
// Топ-5 крупных покупок за последний месяц
List<Transaction> result = transactions.stream()
.filter(t -> t.type().equals("PURCHASE"))
.filter(t -> t.date().isAfter(LocalDate.now().minusMonths(1)))
.filter(t -> t.amount() > 1000)
.sorted(Comparator.comparing(Transaction::amount).reversed())
.limit(5)
.toList();
Пример: обработка текста
String text = " Hello World! This is a Test ";
List<String> words = Arrays.stream(text.split("\\s+"))
.map(String::trim)
.filter(Predicate.not(String::isEmpty))
.map(String::toLowerCase)
.distinct()
.sorted()
.toList(); // [a, hello, is, test, this, world!]
Сводная таблица промежуточных операций
| Операция | Описание | Stateless/Stateful | Short-circuit |
|---|---|---|---|
filter() | Фильтрация по условию | Stateless | Нет |
map() | Преобразование элементов | Stateless | Нет |
flatMap() | Выравнивание вложенных структур | Stateless | Нет |
sorted() | Сортировка | Stateful | Нет |
distinct() | Удаление дубликатов | Stateful | Нет |
peek() | Побочный эффект (отладка) | Stateless | Нет |
limit() | Ограничить N элементами | Stateful | Да |
skip() | Пропустить N элементов | Stateful | Нет |
takeWhile() | Брать пока true | Stateless | Да |
dropWhile() | Пропускать пока true | Stateless | Нет |
mapMulti() | Гибкий маппинг | Stateless | Нет |
Итоги
- Промежуточные операции ленивые - выполняются только при терминальной операции
- filter() - отбирает элементы по условию
- map() - преобразует каждый элемент
- flatMap() - выравнивает вложенные структуры
- sorted() - сортирует (stateful, буферизует все элементы)
- distinct() - удаляет дубликаты (по equals/hashCode)
- peek() - для отладки, не для побочных эффектов
- limit()/skip() - для пагинации и ограничения
- Порядок операций важен - filter до sorted эффективнее
Задания для практики
-
Фильтрация и преобразование: Дан список чисел от 1 до 100. Получи список квадратов всех четных чисел больше 50.
-
flatMap: Дан список предложений. Получи список всех уникальных слов длиннее 3 символов, отсортированный по алфавиту.
-
Пагинация: Реализуй метод
paginate(List<T> items, int page, int size), возвращающий элементы указанной страницы. -
Сортировка объектов: Дан список
Employee(name, department, salary). Отсортируй по департаменту, затем по зарплате (убывание). -
Комплексная задача: Дан список заказов
Order(id, customerId, List<OrderItem>)гдеOrderItem(productName, quantity, price). Найди топ-3 клиента по общей сумме заказов.
2.3.3. Терминальные операции
collect(), forEach(), reduce(), count(), findFirst(), anyMatch()
Материалы
| Тип | Ссылка |
|---|---|
| Документ | Stream Operations |
| Видео | ссылка |
Что такое терминальные операции?
Терминальные операции (terminal operations) - это операции, которые:
- Запускают выполнение всего pipeline
- Возвращают результат (не Stream)
- После выполнения Stream нельзя использовать повторно
Источник → filter() → map() → [терминальная операция] → Результат
↑
Запускает весь pipeline
Классификация терминальных операций
| Тип | Операции | Возвращает |
|---|---|---|
| Собирающие | collect(), toArray(), toList() | Коллекция/массив |
| Итерирующие | forEach(), forEachOrdered() | void |
| Редуцирующие | reduce(), count(), sum(), min(), max() | Значение |
| Поисковые | findFirst(), findAny() | Optional |
| Проверяющие | anyMatch(), allMatch(), noneMatch() | boolean |
forEach() - Итерация по элементам
Выполняет действие для каждого элемента.
Сигнатура
void forEach(Consumer<? super T> action)
void forEachOrdered(Consumer<? super T> action) // гарантирует порядок
Базовые примеры
List<String> names = List.of("Alice", "Bob", "Charlie");
// Вывод каждого элемента
names.stream()
.forEach(System.out::println);
// С лямбдой
names.stream()
.forEach(name -> System.out.println("Hello, " + name));
// После обработки
names.stream()
.filter(n -> n.length() > 3)
.map(String::toUpperCase)
.forEach(System.out::println); // ALICE, CHARLIE
forEach vs forEachOrdered
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// Параллельный stream - порядок НЕ гарантирован
numbers.parallelStream()
.forEach(System.out::println); // Может быть: 3, 1, 5, 2, 4
// Параллельный stream - порядок ГАРАНТИРОВАН
numbers.parallelStream()
.forEachOrdered(System.out::println); // Всегда: 1, 2, 3, 4, 5
Побочные эффекты в forEach
// Допустимо - вывод, логирование
names.stream()
.forEach(name -> logger.info("Processing: " + name));
// Плохо - модификация внешних структур
List<String> result = new ArrayList<>();
names.stream()
.forEach(result::add); // Работает, но лучше collect()
// Хорошо
List<String> result = names.stream()
.collect(Collectors.toList());
forEach на Map
Map<String, Integer> ages = Map.of("Alice", 30, "Bob", 25);
ages.forEach((name, age) ->
System.out.println(name + " is " + age + " years old")
);
collect() - Сборка результата
Собирает элементы в коллекцию или другую структуру.
Сигнатуры
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner)
<R, A> R collect(Collector<? super T, A, R> collector)
Базовые коллекторы
List<String> names = List.of("Alice", "Bob", "Charlie");
// В List
List<String> list = names.stream()
.filter(n -> n.length() > 3)
.collect(Collectors.toList());
// В Set
Set<String> set = names.stream()
.collect(Collectors.toSet());
// toList() (Java 16+) - неизменяемый список
List<String> immutableList = names.stream()
.toList();
toArray() - В массив
// В Object[]
Object[] array = names.stream().toArray();
// В String[]
String[] stringArray = names.stream()
.toArray(String[]::new);
// В Integer[] (с преобразованием)
Integer[] numbers = Stream.of("1", "2", "3")
.map(Integer::parseInt)
.toArray(Integer[]::new);
joining() - Объединение строк
List<String> words = List.of("Hello", "World", "Java");
// Простое объединение
String joined = words.stream()
.collect(Collectors.joining()); // "HelloWorldJava"
// С разделителем
String csv = words.stream()
.collect(Collectors.joining(", ")); // "Hello, World, Java"
// С разделителем, префиксом и суффиксом
String json = words.stream()
.collect(Collectors.joining(", ", "[", "]")); // "[Hello, World, Java]"
Подробнее о Collectors - см. раздел 2.3.4
reduce() - Свертка
Сводит все элементы к одному значению.
Сигнатуры
Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
Визуализация reduce
[1, 2, 3, 4, 5]
↓ reduce((a, b) -> a + b)
1 + 2 = 3
3 + 3 = 6
6 + 4 = 10
10 + 5 = 15
Результат: 15
reduce без identity (возвращает Optional)
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// Сумма
Optional<Integer> sum = numbers.stream()
.reduce((a, b) -> a + b);
sum.ifPresent(System.out::println); // 15
// Или с method reference
Optional<Integer> sum = numbers.stream()
.reduce(Integer::sum);
// Максимум
Optional<Integer> max = numbers.stream()
.reduce(Integer::max); // 5
// Конкатенация строк
List<String> words = List.of("a", "b", "c");
Optional<String> concat = words.stream()
.reduce((a, b) -> a + b); // "abc"
reduce с identity (возвращает значение)
// Сумма с начальным значением 0
int sum = numbers.stream()
.reduce(0, Integer::sum); // 15
// Произведение с начальным значением 1
int product = numbers.stream()
.reduce(1, (a, b) -> a * b); // 120
// Пустой stream - вернет identity
int sumEmpty = Stream.<Integer>empty()
.reduce(0, Integer::sum); // 0
Выбор identity
// Сумма: identity = 0 (a + 0 = a)
reduce(0, Integer::sum)
// Произведение: identity = 1 (a * 1 = a)
reduce(1, (a, b) -> a * b)
// Максимум: identity = Integer.MIN_VALUE
reduce(Integer.MIN_VALUE, Integer::max)
// Конкатенация: identity = ""
reduce("", String::concat)
reduce с преобразованием типа
// Сумма длин строк
List<String> words = List.of("Hello", "World");
int totalLength = words.stream()
.reduce(0, // identity
(sum, word) -> sum + word.length(), // accumulator
Integer::sum); // combiner (для parallel)
// 10
reduce vs специализированные методы
// reduce для суммы
int sum1 = numbers.stream()
.reduce(0, Integer::sum);
// Лучше: mapToInt + sum
int sum2 = numbers.stream()
.mapToInt(Integer::intValue)
.sum();
// reduce для максимума
Optional<Integer> max1 = numbers.stream()
.reduce(Integer::max);
// Лучше: max() с компаратором
Optional<Integer> max2 = numbers.stream()
.max(Integer::compareTo);
Практические примеры reduce
record Product(String name, double price) {}
List<Product> products = List.of(
new Product("Book", 20),
new Product("Laptop", 1000),
new Product("Phone", 500)
);
// Общая стоимость
double total = products.stream()
.map(Product::price)
.reduce(0.0, Double::sum); // 1520.0
// Самый дорогой товар
Optional<Product> mostExpensive = products.stream()
.reduce((p1, p2) -> p1.price() > p2.price() ? p1 : p2);
// Или через max()
Optional<Product> mostExpensive = products.stream()
.max(Comparator.comparing(Product::price));
count() - Подсчет элементов
Возвращает количество элементов.
Сигнатура
long count()
Примеры
List<String> names = List.of("Alice", "Bob", "Charlie", "Diana");
// Общее количество
long total = names.stream().count(); // 4
// Количество после фильтрации
long startsWithA = names.stream()
.filter(n -> n.startsWith("A"))
.count(); // 1
// Количество уникальных
long unique = names.stream()
.distinct()
.count();
Оптимизация count()
// С Java 9+ count() оптимизирован
// Не итерирует элементы если размер известен
List<String> list = List.of("a", "b", "c");
long count = list.stream().count(); // O(1), не O(n)
// Но после операций - итерация нужна
long count = list.stream()
.filter(s -> s.length() > 0)
.count(); // O(n)
min() и max() - Поиск экстремумов
Находят минимальный/максимальный элемент.
Сигнатуры
Optional<T> min(Comparator<? super T> comparator)
Optional<T> max(Comparator<? super T> comparator)
Базовые примеры
List<Integer> numbers = List.of(5, 2, 8, 1, 9);
// Минимум
Optional<Integer> min = numbers.stream()
.min(Integer::compareTo); // 1
// Максимум
Optional<Integer> max = numbers.stream()
.max(Integer::compareTo); // 9
// Для Comparable типов
Optional<String> first = List.of("Charlie", "Alice", "Bob").stream()
.min(Comparator.naturalOrder()); // "Alice"
min/max для объектов
record Person(String name, int age) {}
List<Person> people = List.of(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Charlie", 35)
);
// Самый молодой
Optional<Person> youngest = people.stream()
.min(Comparator.comparing(Person::age)); // Bob, 25
// Самый старший
Optional<Person> oldest = people.stream()
.max(Comparator.comparing(Person::age)); // Charlie, 35
// По имени (алфавитно последний)
Optional<Person> lastByName = people.stream()
.max(Comparator.comparing(Person::name)); // Charlie
IntStream/LongStream/DoubleStream min/max
// Примитивные стримы - без компаратора
OptionalInt min = IntStream.of(5, 2, 8, 1, 9).min(); // 1
OptionalLong max = LongStream.of(100L, 200L, 300L).max(); // 300
OptionalDouble minDouble = DoubleStream.of(1.5, 2.5, 0.5).min(); // 0.5
findFirst() и findAny() - Поиск элемента
Находят первый или любой элемент.
Сигнатуры
Optional<T> findFirst()
Optional<T> findAny()
Разница между findFirst и findAny
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// findFirst - всегда первый элемент
Optional<Integer> first = numbers.stream()
.filter(n -> n > 2)
.findFirst(); // 3 (всегда)
// findAny - любой элемент (быстрее в parallel)
Optional<Integer> any = numbers.parallelStream()
.filter(n -> n > 2)
.findAny(); // 3, 4 или 5 (недетерминировано)
Когда использовать findAny
// Если порядок не важен + parallel stream
boolean hasEven = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.findAny()
.isPresent();
// Если порядок важен
Optional<Integer> firstEven = numbers.stream()
.filter(n -> n % 2 == 0)
.findFirst();
Short-circuiting
// findFirst/findAny - short-circuiting
// Останавливаются при первом совпадении
Stream.iterate(1, n -> n + 1) // Бесконечный поток
.filter(n -> n > 100)
.findFirst(); // 101 (не зависает!)
Практические примеры
record User(String name, String email, boolean active) {}
List<User> users = ...;
// Найти первого активного пользователя
Optional<User> firstActive = users.stream()
.filter(User::active)
.findFirst();
// Найти пользователя по email
Optional<User> byEmail = users.stream()
.filter(u -> u.email().equals("alice@example.com"))
.findFirst();
// С orElse
User user = users.stream()
.filter(u -> u.name().equals("Alice"))
.findFirst()
.orElse(new User("Guest", "guest@example.com", false));
// С orElseThrow
User user = users.stream()
.filter(u -> u.name().equals("Alice"))
.findFirst()
.orElseThrow(() -> new UserNotFoundException("Alice"));
anyMatch(), allMatch(), noneMatch() - Проверки
Проверяют соответствие элементов условию.
Сигнатуры
boolean anyMatch(Predicate<? super T> predicate) // хотя бы один
boolean allMatch(Predicate<? super T> predicate) // все
boolean noneMatch(Predicate<? super T> predicate) // ни один
anyMatch - хотя бы один
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// Есть ли четные?
boolean hasEven = numbers.stream()
.anyMatch(n -> n % 2 == 0); // true
// Есть ли больше 10?
boolean hasLarge = numbers.stream()
.anyMatch(n -> n > 10); // false
allMatch - все элементы
List<Integer> numbers = List.of(2, 4, 6, 8);
// Все четные?
boolean allEven = numbers.stream()
.allMatch(n -> n % 2 == 0); // true
// Все положительные?
boolean allPositive = numbers.stream()
.allMatch(n -> n > 0); // true
// Внимание: пустой stream
boolean emptyCheck = Stream.<Integer>empty()
.allMatch(n -> n > 100); // true (!)
noneMatch - ни один элемент
List<Integer> numbers = List.of(1, 3, 5, 7);
// Нет четных?
boolean noEven = numbers.stream()
.noneMatch(n -> n % 2 == 0); // true
// Нет отрицательных?
boolean noNegative = numbers.stream()
.noneMatch(n -> n < 0); // true
Short-circuiting поведение
// anyMatch - останавливается при первом true
Stream.of(1, 2, 3, 4, 5)
.peek(System.out::println)
.anyMatch(n -> n == 3);
// Выведет: 1, 2, 3 (дальше не идет)
// allMatch - останавливается при первом false
Stream.of(2, 4, 5, 6, 8)
.peek(System.out::println)
.allMatch(n -> n % 2 == 0);
// Выведет: 2, 4, 5 (5 - нечетное, останавливается)
// noneMatch - останавливается при первом true
Stream.of(1, 3, 4, 5, 7)
.peek(System.out::println)
.noneMatch(n -> n % 2 == 0);
// Выведет: 1, 3, 4 (4 - четное, останавливается)
Практические примеры
record Product(String name, double price, boolean inStock) {}
List<Product> products = ...;
// Все товары в наличии?
boolean allInStock = products.stream()
.allMatch(Product::inStock);
// Есть дорогие товары (> 1000)?
boolean hasExpensive = products.stream()
.anyMatch(p -> p.price() > 1000);
// Нет бесплатных товаров?
boolean noFree = products.stream()
.noneMatch(p -> p.price() == 0);
// Валидация
boolean allValid = users.stream()
.allMatch(u -> u.email() != null && u.email().contains("@"));
Взаимосвязь операций
// noneMatch(predicate) эквивалентно !anyMatch(predicate)
// allMatch(predicate) эквивалентно noneMatch(predicate.negate())
Predicate<Integer> isEven = n -> n % 2 == 0;
boolean result1 = numbers.stream().noneMatch(isEven);
boolean result2 = !numbers.stream().anyMatch(isEven);
// result1 == result2
Примитивные стримы: sum(), average(), summaryStatistics()
sum()
int[] numbers = {1, 2, 3, 4, 5};
int sum = Arrays.stream(numbers).sum(); // 15
// Для Stream<Integer> - нужен mapToInt
List<Integer> list = List.of(1, 2, 3, 4, 5);
int sum = list.stream()
.mapToInt(Integer::intValue)
.sum(); // 15
average()
OptionalDouble avg = IntStream.of(1, 2, 3, 4, 5)
.average(); // 3.0
double average = avg.orElse(0.0);
// Пустой стрим
OptionalDouble empty = IntStream.empty().average(); // OptionalDouble.empty
summaryStatistics()
IntSummaryStatistics stats = IntStream.of(1, 2, 3, 4, 5)
.summaryStatistics();
stats.getCount(); // 5
stats.getSum(); // 15
stats.getMin(); // 1
stats.getMax(); // 5
stats.getAverage(); // 3.0
// Для объектов
DoubleSummaryStatistics priceStats = products.stream()
.mapToDouble(Product::price)
.summaryStatistics();
iterator() и spliterator()
iterator() - Получить Iterator
Stream<String> stream = Stream.of("a", "b", "c");
Iterator<String> iterator = stream.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
// Внимание: Stream после этого нельзя использовать
spliterator() - Для параллельной обработки
Stream<String> stream = Stream.of("a", "b", "c");
Spliterator<String> spliterator = stream.spliterator();
// Характеристики
spliterator.characteristics(); // ORDERED, SIZED, etc.
spliterator.estimateSize(); // Примерное количество элементов
Обработка результатов Optional
Основные методы Optional
Optional<String> result = names.stream()
.filter(n -> n.startsWith("X"))
.findFirst();
// Проверка наличия
if (result.isPresent()) {
System.out.println(result.get());
}
// orElse - значение по умолчанию
String name = result.orElse("Unknown");
// orElseGet - ленивое вычисление
String name = result.orElseGet(() -> computeDefault());
// orElseThrow - бросить исключение
String name = result.orElseThrow(() -> new NoSuchElementException());
// ifPresent - выполнить действие
result.ifPresent(System.out::println);
// ifPresentOrElse (Java 9+)
result.ifPresentOrElse(
System.out::println,
() -> System.out.println("Not found")
);
Цепочки с Optional
Optional<String> email = users.stream()
.filter(u -> u.name().equals("Alice"))
.findFirst()
.map(User::email) // Optional<String>
.filter(e -> e.contains("@")) // Optional<String>
.map(String::toLowerCase); // Optional<String>
Порядок терминальных операций
Ленивое выполнение
// Ничего не выполняется - нет терминальной операции
Stream<String> stream = names.stream()
.filter(n -> {
System.out.println("Filtering: " + n);
return n.length() > 3;
})
.map(n -> {
System.out.println("Mapping: " + n);
return n.toUpperCase();
});
// Теперь выполняется - есть терминальная операция
stream.forEach(System.out::println);
Порядок обработки
List.of("Alice", "Bob", "Charlie").stream()
.filter(n -> {
System.out.println("filter: " + n);
return n.length() > 3;
})
.map(n -> {
System.out.println("map: " + n);
return n.toUpperCase();
})
.forEach(n -> System.out.println("forEach: " + n));
// Вывод (элемент за элементом, не операция за операцией):
// filter: Alice
// map: Alice
// forEach: ALICE
// filter: Bob
// filter: Charlie
// map: Charlie
// forEach: CHARLIE
Сводная таблица терминальных операций
| Операция | Описание | Возвращает | Short-circuit |
|---|---|---|---|
forEach() | Действие для каждого | void | Нет |
forEachOrdered() | forEach с гарантией порядка | void | Нет |
collect() | Сборка в коллекцию | R | Нет |
toArray() | Сборка в массив | Object[]/T[] | Нет |
toList() | Сборка в List | List | Нет |
reduce() | Свертка в одно значение | Optional | Нет |
count() | Количество элементов | long | Нет |
min() | Минимальный элемент | Optional | Нет |
max() | Максимальный элемент | Optional | Нет |
findFirst() | Первый элемент | Optional | Да |
findAny() | Любой элемент | Optional | Да |
anyMatch() | Есть ли совпадение | boolean | Да |
allMatch() | Все ли совпадают | boolean | Да |
noneMatch() | Нет ли совпадений | boolean | Да |
sum() | Сумма (примитивы) | int/long/double | Нет |
average() | Среднее (примитивы) | OptionalDouble | Нет |
Итоги
- Терминальные операции запускают pipeline - без них промежуточные операции не выполняются
- forEach - для побочных эффектов (вывод, логирование)
- collect - для сборки результата в коллекцию
- reduce - для свертки в одно значение
- findFirst/findAny - для поиска элемента (short-circuit)
- anyMatch/allMatch/noneMatch - для проверок (short-circuit)
- count/min/max/sum/average - для агрегации
- Stream одноразовый - после терминальной операции использовать нельзя
- Результат часто Optional - обрабатывай отсутствие значения
Задания для практики
-
reduce: Дан список чисел. Найди произведение всех положительных чисел с помощью reduce.
-
findFirst + filter: Дан список пользователей
User(name, email, age). Найди первого совершеннолетнего (age >= 18) пользователя с email на gmail.com. -
allMatch/anyMatch: Дан список заказов
Order(items, status). Проверь: а) все ли заказы доставлены, б) есть ли отмененные заказы. -
collect + reduce: Дан список транзакций
Transaction(type, amount). Посчитай баланс (сумма DEPOSIT минус сумма WITHDRAW). -
Комплексная задача: Дан список студентов
Student(name, grades: List<Integer>). Найди студента с наивысшим средним баллом. Если таких несколько - любого из них.
2.3.4. Collectors
toList(), toSet(), toMap(), groupingBy(), partitioningBy(), joining()
Материалы
| Тип | Ссылка |
|---|---|
| Документ | Collectors |
| Видео | ссылка |
Что такое Collector?
Collector - это объект, который описывает как собирать элементы Stream в результат.
// Collector используется с методом collect()
List<String> result = stream.collect(Collectors.toList());
Структура Collector
interface Collector<T, A, R> {
Supplier<A> supplier(); // Создает контейнер
BiConsumer<A, T> accumulator(); // Добавляет элемент
BinaryOperator<A> combiner(); // Объединяет контейнеры (parallel)
Function<A, R> finisher(); // Преобразует в результат
Set<Characteristics> characteristics();
}
// T - тип входных элементов
// A - тип промежуточного контейнера
// R - тип результата
Класс Collectors
java.util.stream.Collectors содержит фабричные методы для создания стандартных коллекторов.
import java.util.stream.Collectors;
import static java.util.stream.Collectors.*; // Статический импорт
Базовые коллекторы
toList() - В List
List<String> names = List.of("Alice", "Bob", "Charlie");
// Collectors.toList() - изменяемый ArrayList
List<String> list = names.stream()
.filter(n -> n.length() > 3)
.collect(Collectors.toList());
list.add("Diana"); // OK, можно модифицировать
// Stream.toList() (Java 16+) - неизменяемый список
List<String> immutable = names.stream()
.filter(n -> n.length() > 3)
.toList();
immutable.add("Diana"); // UnsupportedOperationException!
toSet() - В Set
// Collectors.toSet() - обычно HashSet
Set<String> set = names.stream()
.map(String::toLowerCase)
.collect(Collectors.toSet());
// Конкретная реализация Set
TreeSet<String> treeSet = names.stream()
.collect(Collectors.toCollection(TreeSet::new));
LinkedHashSet<String> linkedSet = names.stream()
.collect(Collectors.toCollection(LinkedHashSet::new));
toCollection() - В любую коллекцию
// ArrayList
ArrayList<String> arrayList = names.stream()
.collect(Collectors.toCollection(ArrayList::new));
// LinkedList
LinkedList<String> linkedList = names.stream()
.collect(Collectors.toCollection(LinkedList::new));
// TreeSet с компаратором
TreeSet<String> sortedSet = names.stream()
.collect(Collectors.toCollection(
() -> new TreeSet<>(Comparator.reverseOrder())
));
// ArrayDeque
ArrayDeque<String> deque = names.stream()
.collect(Collectors.toCollection(ArrayDeque::new));
toUnmodifiableList(), toUnmodifiableSet() (Java 10+)
// Неизменяемый List
List<String> immutableList = names.stream()
.collect(Collectors.toUnmodifiableList());
// Неизменяемый Set
Set<String> immutableSet = names.stream()
.collect(Collectors.toUnmodifiableSet());
// Попытка модификации бросит UnsupportedOperationException
toMap() - В Map
Базовый toMap
record Person(int id, String name) {}
List<Person> people = List.of(
new Person(1, "Alice"),
new Person(2, "Bob"),
new Person(3, "Charlie")
);
// id -> Person
Map<Integer, Person> byId = people.stream()
.collect(Collectors.toMap(
Person::id, // keyMapper
p -> p // valueMapper (или Function.identity())
));
// {1=Person[id=1, name=Alice], 2=Person[id=2, name=Bob], ...}
// id -> name
Map<Integer, String> idToName = people.stream()
.collect(Collectors.toMap(
Person::id,
Person::name
));
// {1=Alice, 2=Bob, 3=Charlie}
Обработка дубликатов ключей
List<Person> peopleWithDupes = List.of(
new Person(1, "Alice"),
new Person(1, "Alicia"), // Дубликат id!
new Person(2, "Bob")
);
// Без mergeFunction - IllegalStateException при дубликате!
// Map<Integer, String> map = peopleWithDupes.stream()
// .collect(Collectors.toMap(Person::id, Person::name));
// С mergeFunction
Map<Integer, String> map = peopleWithDupes.stream()
.collect(Collectors.toMap(
Person::id,
Person::name,
(existing, replacement) -> existing // Оставить первое
));
// {1=Alice, 2=Bob}
// Объединить значения
Map<Integer, String> merged = peopleWithDupes.stream()
.collect(Collectors.toMap(
Person::id,
Person::name,
(v1, v2) -> v1 + ", " + v2 // Объединить через запятую
));
// {1=Alice, Alicia, 2=Bob}
Указание типа Map
// LinkedHashMap (сохраняет порядок вставки)
LinkedHashMap<Integer, String> linkedMap = people.stream()
.collect(Collectors.toMap(
Person::id,
Person::name,
(v1, v2) -> v1,
LinkedHashMap::new
));
// TreeMap (сортировка по ключу)
TreeMap<Integer, String> treeMap = people.stream()
.collect(Collectors.toMap(
Person::id,
Person::name,
(v1, v2) -> v1,
TreeMap::new
));
// ConcurrentHashMap
ConcurrentHashMap<Integer, String> concurrentMap = people.stream()
.collect(Collectors.toMap(
Person::id,
Person::name,
(v1, v2) -> v1,
ConcurrentHashMap::new
));
toUnmodifiableMap() (Java 10+)
Map<Integer, String> immutableMap = people.stream()
.collect(Collectors.toUnmodifiableMap(
Person::id,
Person::name
));
immutableMap.put(4, "Diana"); // UnsupportedOperationException!
Практические примеры toMap
record Product(String sku, String name, double price) {}
List<Product> products = List.of(
new Product("SKU001", "Book", 20.0),
new Product("SKU002", "Laptop", 1000.0),
new Product("SKU003", "Phone", 500.0)
);
// SKU -> Product (справочник)
Map<String, Product> catalog = products.stream()
.collect(Collectors.toMap(Product::sku, Function.identity()));
// SKU -> Price (прайс-лист)
Map<String, Double> priceList = products.stream()
.collect(Collectors.toMap(Product::sku, Product::price));
// Name -> SKU (обратный индекс)
Map<String, String> nameToSku = products.stream()
.collect(Collectors.toMap(Product::name, Product::sku));
groupingBy() - Группировка
Базовая группировка
record Person(String name, String city, int age) {}
List<Person> people = List.of(
new Person("Alice", "Moscow", 30),
new Person("Bob", "SPb", 25),
new Person("Charlie", "Moscow", 35),
new Person("Diana", "SPb", 28)
);
// Группировка по городу
Map<String, List<Person>> byCity = people.stream()
.collect(Collectors.groupingBy(Person::city));
// {Moscow=[Alice, Charlie], SPb=[Bob, Diana]}
// Группировка по возрастной категории
Map<String, List<Person>> byAgeGroup = people.stream()
.collect(Collectors.groupingBy(p ->
p.age() < 30 ? "Young" : "Adult"
));
// {Young=[Bob, Diana], Adult=[Alice, Charlie]}
groupingBy с downstream collector
// Группировка + подсчет
Map<String, Long> countByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
Collectors.counting()
));
// {Moscow=2, SPb=2}
// Группировка + сумма возрастов
Map<String, Integer> totalAgeByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
Collectors.summingInt(Person::age)
));
// {Moscow=65, SPb=53}
// Группировка + средний возраст
Map<String, Double> avgAgeByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
Collectors.averagingInt(Person::age)
));
// {Moscow=32.5, SPb=26.5}
// Группировка + максимальный возраст
Map<String, Optional<Person>> oldestByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
Collectors.maxBy(Comparator.comparing(Person::age))
));
// Группировка + список имен
Map<String, List<String>> namesByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
Collectors.mapping(Person::name, Collectors.toList())
));
// {Moscow=[Alice, Charlie], SPb=[Bob, Diana]}
// Группировка + объединение имен в строку
Map<String, String> joinedNamesByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
Collectors.mapping(
Person::name,
Collectors.joining(", ")
)
));
// {Moscow=Alice, Charlie, SPb=Bob, Diana}
groupingBy с указанием типа Map
// TreeMap (отсортированные ключи)
TreeMap<String, List<Person>> sortedByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
TreeMap::new,
Collectors.toList()
));
// LinkedHashMap (порядок вставки)
LinkedHashMap<String, List<Person>> orderedByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
LinkedHashMap::new,
Collectors.toList()
));
Многоуровневая группировка
record Employee(String name, String dept, String team) {}
List<Employee> employees = List.of(
new Employee("Alice", "IT", "Backend"),
new Employee("Bob", "IT", "Frontend"),
new Employee("Charlie", "HR", "Recruiting"),
new Employee("Diana", "IT", "Backend")
);
// Группировка по отделу, затем по команде
Map<String, Map<String, List<Employee>>> byDeptAndTeam = employees.stream()
.collect(Collectors.groupingBy(
Employee::dept,
Collectors.groupingBy(Employee::team)
));
// {IT={Backend=[Alice, Diana], Frontend=[Bob]}, HR={Recruiting=[Charlie]}}
// Группировка по отделу, затем подсчет по команде
Map<String, Map<String, Long>> countByDeptAndTeam = employees.stream()
.collect(Collectors.groupingBy(
Employee::dept,
Collectors.groupingBy(
Employee::team,
Collectors.counting()
)
));
// {IT={Backend=2, Frontend=1}, HR={Recruiting=1}}
groupingByConcurrent() - Параллельная группировка
// Для parallel streams - потокобезопасная группировка
ConcurrentMap<String, List<Person>> concurrentByCity = people.parallelStream()
.collect(Collectors.groupingByConcurrent(Person::city));
partitioningBy() - Разделение на две группы
Базовое разделение
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Разделение на четные и нечетные
Map<Boolean, List<Integer>> evenOdd = numbers.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
// {false=[1, 3, 5, 7, 9], true=[2, 4, 6, 8, 10]}
List<Integer> evens = evenOdd.get(true); // [2, 4, 6, 8, 10]
List<Integer> odds = evenOdd.get(false); // [1, 3, 5, 7, 9]
partitioningBy с downstream collector
// Разделение + подсчет
Map<Boolean, Long> countEvenOdd = numbers.stream()
.collect(Collectors.partitioningBy(
n -> n % 2 == 0,
Collectors.counting()
));
// {false=5, true=5}
// Разделение + сумма
Map<Boolean, Integer> sumEvenOdd = numbers.stream()
.collect(Collectors.partitioningBy(
n -> n % 2 == 0,
Collectors.summingInt(Integer::intValue)
));
// {false=25, true=30}
Практические примеры partitioningBy
record Student(String name, int score) {}
List<Student> students = List.of(
new Student("Alice", 85),
new Student("Bob", 45),
new Student("Charlie", 90),
new Student("Diana", 55)
);
// Разделение на сдавших и не сдавших
Map<Boolean, List<Student>> passedFailed = students.stream()
.collect(Collectors.partitioningBy(s -> s.score() >= 60));
List<Student> passed = passedFailed.get(true); // [Alice, Charlie]
List<Student> failed = passedFailed.get(false); // [Bob, Diana]
// Разделение + имена
Map<Boolean, List<String>> passedFailedNames = students.stream()
.collect(Collectors.partitioningBy(
s -> s.score() >= 60,
Collectors.mapping(Student::name, Collectors.toList())
));
// {false=[Bob, Diana], true=[Alice, Charlie]}
groupingBy vs partitioningBy
// groupingBy - произвольное количество групп
Map<String, List<Person>> byCity = people.stream()
.collect(Collectors.groupingBy(Person::city));
// Может быть 0, 1, 2, ... групп
// partitioningBy - ровно 2 группы (true/false)
Map<Boolean, List<Person>> adults = people.stream()
.collect(Collectors.partitioningBy(p -> p.age() >= 18));
// Всегда 2 ключа: true и false (даже если список пустой)
joining() - Объединение строк
Базовое объединение
List<String> words = List.of("Hello", "World", "Java");
// Простое объединение
String joined = words.stream()
.collect(Collectors.joining());
// "HelloWorldJava"
// С разделителем
String csv = words.stream()
.collect(Collectors.joining(", "));
// "Hello, World, Java"
// С разделителем, префиксом и суффиксом
String json = words.stream()
.collect(Collectors.joining(", ", "[", "]"));
// "[Hello, World, Java]"
Практические примеры joining
record Person(String name, int age) {}
List<Person> people = List.of(
new Person("Alice", 30),
new Person("Bob", 25)
);
// Список имен через запятую
String names = people.stream()
.map(Person::name)
.collect(Collectors.joining(", "));
// "Alice, Bob"
// SQL IN clause
String sqlIn = people.stream()
.map(p -> "'" + p.name() + "'")
.collect(Collectors.joining(", ", "(", ")"));
// "('Alice', 'Bob')"
// HTML список
String htmlList = people.stream()
.map(p -> "<li>" + p.name() + "</li>")
.collect(Collectors.joining("\n", "<ul>\n", "\n</ul>"));
// <ul>
// <li>Alice</li>
// <li>Bob</li>
// </ul>
// CSV строка
String csvLine = Stream.of("Alice", "30", "Moscow")
.collect(Collectors.joining(","));
// "Alice,30,Moscow"
Агрегирующие коллекторы
counting() - Подсчет
long count = people.stream()
.collect(Collectors.counting());
// Обычно используется как downstream
Map<String, Long> countByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
Collectors.counting()
));
summingInt(), summingLong(), summingDouble()
// Сумма возрастов
int totalAge = people.stream()
.collect(Collectors.summingInt(Person::age));
// Сумма цен
double totalPrice = products.stream()
.collect(Collectors.summingDouble(Product::price));
// С группировкой
Map<String, Integer> totalAgeByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
Collectors.summingInt(Person::age)
));
averagingInt(), averagingLong(), averagingDouble()
// Средний возраст
double avgAge = people.stream()
.collect(Collectors.averagingInt(Person::age));
// С группировкой
Map<String, Double> avgAgeByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
Collectors.averagingInt(Person::age)
));
summarizingInt(), summarizingLong(), summarizingDouble()
// Полная статистика
IntSummaryStatistics stats = people.stream()
.collect(Collectors.summarizingInt(Person::age));
stats.getCount(); // количество
stats.getSum(); // сумма
stats.getMin(); // минимум
stats.getMax(); // максимум
stats.getAverage(); // среднее
// С группировкой
Map<String, IntSummaryStatistics> statsByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
Collectors.summarizingInt(Person::age)
));
maxBy(), minBy()
// Максимальный по возрасту
Optional<Person> oldest = people.stream()
.collect(Collectors.maxBy(Comparator.comparing(Person::age)));
// Минимальный по возрасту
Optional<Person> youngest = people.stream()
.collect(Collectors.minBy(Comparator.comparing(Person::age)));
// С группировкой
Map<String, Optional<Person>> oldestByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
Collectors.maxBy(Comparator.comparing(Person::age))
));
Преобразующие коллекторы
mapping() - Преобразование перед сборкой
// Собрать только имена
List<String> names = people.stream()
.collect(Collectors.mapping(
Person::name,
Collectors.toList()
));
// То же самое проще:
List<String> names = people.stream()
.map(Person::name)
.toList();
// Но mapping() полезен как downstream collector
Map<String, List<String>> namesByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
Collectors.mapping(Person::name, Collectors.toList())
));
flatMapping() (Java 9+)
record Order(String id, List<String> items) {}
List<Order> orders = List.of(
new Order("1", List.of("Book", "Pen")),
new Order("2", List.of("Laptop")),
new Order("3", List.of("Mouse", "Keyboard"))
);
// Все товары (плоский список)
Set<String> allItems = orders.stream()
.collect(Collectors.flatMapping(
order -> order.items().stream(),
Collectors.toSet()
));
// [Book, Pen, Laptop, Mouse, Keyboard]
// С группировкой - все товары по какому-то признаку
Map<Integer, Set<String>> itemsByOrderLength = orders.stream()
.collect(Collectors.groupingBy(
o -> o.items().size(),
Collectors.flatMapping(
o -> o.items().stream(),
Collectors.toSet()
)
));
filtering() (Java 9+)
// Фильтрация внутри группы
Map<String, List<Person>> adultsByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
Collectors.filtering(
p -> p.age() >= 18,
Collectors.toList()
)
));
// Отличие от filter() перед groupingBy:
// - filtering() сохраняет все группы (даже пустые)
// - filter() удаляет элементы ДО группировки
collectingAndThen() - Финальное преобразование
// Собрать в List, затем сделать неизменяемым
List<String> immutableNames = people.stream()
.map(Person::name)
.collect(Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList
));
// Собрать в List, затем получить размер
int count = people.stream()
.collect(Collectors.collectingAndThen(
Collectors.toList(),
List::size
));
// Собрать максимум, затем извлечь из Optional
Person oldest = people.stream()
.collect(Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparing(Person::age)),
opt -> opt.orElseThrow()
));
reducing() - Свертка
Базовый reducing
// Сумма возрастов
Optional<Integer> totalAge = people.stream()
.map(Person::age)
.collect(Collectors.reducing(Integer::sum));
// С identity
int totalAge = people.stream()
.map(Person::age)
.collect(Collectors.reducing(0, Integer::sum));
// С mapper
int totalAge = people.stream()
.collect(Collectors.reducing(
0, // identity
Person::age, // mapper
Integer::sum // reducer
));
reducing vs reduce
// Stream.reduce() - терминальная операция
int sum1 = numbers.stream()
.reduce(0, Integer::sum);
// Collectors.reducing() - коллектор (для downstream)
Map<String, Integer> totalAgeByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
Collectors.reducing(0, Person::age, Integer::sum)
));
teeing() (Java 12+) - Объединение двух коллекторов
Базовый teeing
// Одновременно найти мин и макс
record MinMax(Integer min, Integer max) {}
MinMax result = numbers.stream()
.collect(Collectors.teeing(
Collectors.minBy(Integer::compareTo),
Collectors.maxBy(Integer::compareTo),
(min, max) -> new MinMax(
min.orElse(null),
max.orElse(null)
)
));
Практические примеры teeing
// Сумма и количество одновременно
record SumCount(int sum, long count) {}
SumCount result = numbers.stream()
.collect(Collectors.teeing(
Collectors.summingInt(Integer::intValue),
Collectors.counting(),
SumCount::new
));
// Вычисление среднего вручную
double average = numbers.stream()
.collect(Collectors.teeing(
Collectors.summingInt(Integer::intValue),
Collectors.counting(),
(sum, count) -> count == 0 ? 0 : (double) sum / count
));
// Разделение на две категории с подсчетом
record PassFailCount(long passed, long failed) {}
PassFailCount counts = students.stream()
.collect(Collectors.teeing(
Collectors.filtering(s -> s.score() >= 60, Collectors.counting()),
Collectors.filtering(s -> s.score() < 60, Collectors.counting()),
PassFailCount::new
));
Создание кастомного Collector
Простой кастомный коллектор
// Коллектор для сборки в ImmutableList (Guava)
Collector<String, ?, ImmutableList<String>> toImmutableList =
Collector.of(
ImmutableList::<String>builder, // supplier
ImmutableList.Builder::add, // accumulator
(b1, b2) -> b1.addAll(b2.build()), // combiner
ImmutableList.Builder::build // finisher
);
ImmutableList<String> result = names.stream()
.collect(toImmutableList);
Коллектор для StringBuilder
Collector<String, StringBuilder, String> joining =
Collector.of(
StringBuilder::new,
StringBuilder::append,
StringBuilder::append,
StringBuilder::toString
);
String result = Stream.of("a", "b", "c")
.collect(joining); // "abc"
Коллектор с характеристиками
Collector<String, ?, Set<String>> toUnorderedSet =
Collector.of(
HashSet::new,
Set::add,
(s1, s2) -> { s1.addAll(s2); return s1; },
Collector.Characteristics.UNORDERED,
Collector.Characteristics.IDENTITY_FINISH
);
Сводная таблица Collectors
| Collector | Описание | Результат |
|---|---|---|
toList() | В List | List<T> |
toSet() | В Set | Set<T> |
toCollection(supplier) | В указанную коллекцию | C |
toMap(key, value) | В Map | Map<K, V> |
toUnmodifiableList() | В неизменяемый List | List<T> |
toUnmodifiableSet() | В неизменяемый Set | Set<T> |
toUnmodifiableMap() | В неизменяемую Map | Map<K, V> |
groupingBy(classifier) | Группировка | Map<K, List<T>> |
partitioningBy(predicate) | Разделение на 2 группы | Map<Boolean, List<T>> |
joining() | Объединение строк | String |
counting() | Подсчет | Long |
summingInt/Long/Double() | Сумма | int/long/double |
averagingInt/Long/Double() | Среднее | Double |
summarizingInt/Long/Double() | Статистика | *SummaryStatistics |
maxBy(comparator) | Максимум | Optional<T> |
minBy(comparator) | Минимум | Optional<T> |
mapping(mapper, downstream) | Преобразование | зависит от downstream |
flatMapping(mapper, downstream) | Выравнивание | зависит от downstream |
filtering(predicate, downstream) | Фильтрация | зависит от downstream |
collectingAndThen(collector, finisher) | Финализация | зависит от finisher |
reducing() | Свертка | Optional<T> или T |
teeing(c1, c2, merger) | Объединение коллекторов | зависит от merger |
Best Practices
Выбор коллектора
// Для простого списка - toList() или Stream.toList()
List<String> list = stream.toList(); // Java 16+, неизменяемый
// Для изменяемого списка
List<String> mutableList = stream.collect(Collectors.toList());
// Для конкретной реализации
TreeSet<String> sorted = stream
.collect(Collectors.toCollection(TreeSet::new));
Избегай лишних операций
// Плохо - лишний map
Map<String, Integer> ages = people.stream()
.collect(Collectors.toMap(
p -> p.name(),
p -> p.age()
));
// Хорошо - method reference
Map<String, Integer> ages = people.stream()
.collect(Collectors.toMap(Person::name, Person::age));
Обработка дубликатов в toMap
// Всегда думай о дубликатах!
// Плохо - бросит исключение при дубликате
Map<String, Person> byName = people.stream()
.collect(Collectors.toMap(Person::name, p -> p));
// Хорошо - явная обработка
Map<String, Person> byName = people.stream()
.collect(Collectors.toMap(
Person::name,
Function.identity(),
(existing, replacement) -> existing
));
Итоги
- Collectors - мощный инструмент для сбора элементов Stream
- toList/toSet/toMap - базовые коллекторы для сборки в коллекции
- groupingBy - группировка по ключу, поддерживает downstream коллекторы
- partitioningBy - разделение на 2 группы (true/false)
- joining - объединение строк с разделителем
- Агрегирующие коллекторы - counting, summing, averaging, summarizing
- mapping/flatMapping/filtering - преобразование перед сборкой
- collectingAndThen - финальное преобразование результата
- teeing (Java 12+) - объединение двух коллекторов
- Всегда обрабатывай дубликаты в toMap
Задания для практики
-
groupingBy + counting: Дан список слов. Сгруппируй по длине и посчитай количество слов каждой длины.
-
toMap с дубликатами: Дан список
Person(name, city). Создай Map<city, names> где names - строка с именами через запятую. -
partitioningBy + статистика: Дан список оценок (0-100). Раздели на сдавших (>=60) и не сдавших. Для каждой группы выведи статистику (мин, макс, среднее).
-
Многоуровневая группировка: Дан список
Transaction(type, category, amount). Сгруппируй по типу, затем по категории, и посчитай сумму в каждой подгруппе. -
teeing: Дан список чисел. Одним проходом найди сумму положительных и сумму отрицательных чисел.
2.x. optional
optional
Материалы
Optional<T> — контейнер, который может содержать или не содержать значение. Введён в Java 8 как способ явно выразить отсутствие результата без использования null.
Зачем нужен Optional
Проблема null — одна из самых распространённых причин ошибок в Java:
// Опасный код — NullPointerException подстерегает
String city = user.getAddress().getCity().toUpperCase();
// Защитное программирование — громоздко
String city = null;
if (user != null) {
Address address = user.getAddress();
if (address != null) {
String c = address.getCity();
if (c != null) {
city = c.toUpperCase();
}
}
}
Optional решает эту проблему, делая возможное отсутствие значения явным на уровне типа:
// Сигнатура метода явно говорит: результата может не быть
Optional<User> findUserById(long id);
// Компилятор заставляет обработать оба случая
String city = findUserById(42)
.flatMap(User::getAddress)
.map(Address::getCity)
.map(String::toUpperCase)
.orElse("Unknown");
Создание Optional
Optional.empty() — пустой контейнер
Optional<String> empty = Optional.empty();
System.out.println(empty.isPresent()); // false
System.out.println(empty.isEmpty()); // true (Java 11+)
Optional.of(value) — значение гарантированно не null
Optional<String> name = Optional.of("Alice");
// NullPointerException! of() не принимает null
Optional<String> oops = Optional.of(null); // Бросит исключение
Используйте of() когда уверены, что значение не null. Это документирует намерение и упадёт сразу, если предположение нарушено.
Optional.ofNullable(value) — значение может быть null
String possiblyNull = getUserInput();
Optional<String> maybe = Optional.ofNullable(possiblyNull);
// Если possiblyNull == null, получим Optional.empty()
// Иначе — Optional со значением
Используйте ofNullable() при работе с внешними данными или legacy-кодом.
Проверка наличия значения
Optional<String> opt = Optional.of("Hello");
// isPresent() — есть ли значение
if (opt.isPresent()) {
System.out.println("Значение: " + opt.get());
}
// isEmpty() — пуст ли контейнер (Java 11+)
if (opt.isEmpty()) {
System.out.println("Пусто");
}
Важно: Избегайте
isPresent()+get()— это возврат к проверкам наnull. Используйте функциональные методы.
Получение значения
get() — небезопасно!
Optional<String> opt = Optional.empty();
String value = opt.get(); // NoSuchElementException!
Предупреждение:
get()бросает исключение для пустого Optional. ПредпочитайтеorElseThrow()— он явно выражает намерение.
orElse(defaultValue) — значение по умолчанию
String name = Optional.ofNullable(userName)
.orElse("Anonymous");
// Если userName == null, вернёт "Anonymous"
Особенность: значение по умолчанию вычисляется всегда:
String name = Optional.of("Alice")
.orElse(computeExpensiveDefault()); // computeExpensiveDefault() вызовется!
orElseGet(supplier) — ленивое значение по умолчанию
String name = Optional.ofNullable(userName)
.orElseGet(() -> computeExpensiveDefault());
// computeExpensiveDefault() вызовется ТОЛЬКО если userName == null
Используйте orElseGet() когда вычисление значения по умолчанию дорогое.
orElseThrow() — исключение если пусто
// NoSuchElementException с понятным сообщением (Java 10+)
User user = findUserById(id).orElseThrow();
// Своё исключение
User user = findUserById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
// Ссылка на конструктор
User user = findUserById(id)
.orElseThrow(UserNotFoundException::new);
Условные действия
ifPresent(consumer) — выполнить если есть значение
Optional<User> user = findUserById(42);
// Вместо if (user.isPresent()) { ... }
user.ifPresent(u -> System.out.println("Found: " + u.getName()));
// Ссылка на метод
user.ifPresent(System.out::println);
ifPresentOrElse(consumer, runnable) — обработать оба случая (Java 9+)
findUserById(42).ifPresentOrElse(
user -> System.out.println("Found: " + user.getName()),
() -> System.out.println("User not found")
);
Трансформации
map(function) — преобразовать значение
Optional<String> name = Optional.of("alice");
Optional<String> upperName = name.map(String::toUpperCase);
// Optional["ALICE"]
Optional<Integer> length = name.map(String::length);
// Optional[5]
// Если Optional пуст, map возвращает пустой Optional
Optional<String> empty = Optional.<String>empty().map(String::toUpperCase);
// Optional.empty()
Важно: Если функция возвращает null, результат — пустой Optional:
Optional<String> result = Optional.of("test")
.map(s -> null); // Optional.empty()
flatMap(function) — для вложенных Optional
Когда функция сама возвращает Optional, используйте flatMap() чтобы избежать Optional<Optional<T>>:
class User {
Optional<Address> getAddress() { ... }
}
class Address {
Optional<String> getCity() { ... }
}
// map создал бы Optional<Optional<Address>>
Optional<User> user = findUserById(42);
// flatMap "разворачивает" вложенный Optional
Optional<String> city = user
.flatMap(User::getAddress)
.flatMap(Address::getCity);
Сравнение map vs flatMap:
Optional<String> opt = Optional.of("hello");
// Функция возвращает обычное значение → map
Optional<Integer> length = opt.map(String::length);
// Функция возвращает Optional → flatMap
Optional<Character> firstChar = opt.flatMap(s ->
s.isEmpty() ? Optional.empty() : Optional.of(s.charAt(0))
);
filter(predicate) — фильтрация по условию
Optional<String> name = Optional.of("Alice");
Optional<String> longName = name.filter(n -> n.length() > 3);
// Optional["Alice"]
Optional<String> shortName = name.filter(n -> n.length() > 10);
// Optional.empty()
Пример использования:
// Найти пользователя, только если он активен
Optional<User> activeUser = findUserById(42)
.filter(User::isActive);
Комбинирование Optional
or(supplier) — альтернативный Optional (Java 9+)
Optional<String> primary = Optional.empty();
Optional<String> fallback = Optional.of("fallback");
Optional<String> result = primary.or(() -> fallback);
// Optional["fallback"]
// Цепочка альтернатив
Optional<User> user = findInCache(id)
.or(() -> findInDatabase(id))
.or(() -> findInRemoteService(id));
Отличие от orElseGet(): or() возвращает Optional, а не значение.
Интеграция со Stream
stream() — Optional как Stream (Java 9+)
Optional<String> opt = Optional.of("hello");
Stream<String> stream = opt.stream();
// Stream с одним элементом или пустой Stream
Главное применение — фильтрация пустых Optional в потоке:
List<Optional<String>> optionals = List.of(
Optional.of("a"),
Optional.empty(),
Optional.of("b"),
Optional.empty()
);
// Извлечь только присутствующие значения
List<String> values = optionals.stream()
.flatMap(Optional::stream)
.toList();
// ["a", "b"]
// До Java 9 приходилось писать так:
List<String> valuesBefore9 = optionals.stream()
.filter(Optional::isPresent)
.map(Optional::get)
.toList();
Примитивные версии
Для избежания boxing существуют специализированные классы:
OptionalInt optInt = OptionalInt.of(42);
OptionalLong optLong = OptionalLong.of(1_000_000_000L);
OptionalDouble optDouble = OptionalDouble.of(3.14);
// Получение значения
int value = optInt.orElse(0);
int value2 = optInt.orElseThrow();
// Проверка
if (optInt.isPresent()) {
optInt.ifPresent(System.out::println);
}
// Создание пустых
OptionalInt empty = OptionalInt.empty();
Ограничение: Примитивные Optional не имеют методов
map(),flatMap(),filter(),or(). Они проще, но менее функциональны.
// Для трансформаций нужно конвертировать
OptionalInt optInt = OptionalInt.of(42);
Optional<String> asString = optInt.isPresent()
? Optional.of(String.valueOf(optInt.getAsInt()))
: Optional.empty();
// Или через stream (Java 9+)
Optional<String> asString2 = optInt.stream()
.mapToObj(String::valueOf)
.findFirst();
Цепочки преобразований
Optional раскрывает свою мощь в цепочках:
record User(String name, Address address) {}
record Address(String city, String street) {}
Optional<User> user = findUserById(42);
// Безопасная навигация по вложенным объектам
String street = user
.map(User::address) // Optional<Address>
.map(Address::street) // Optional<String>
.map(String::toUpperCase) // Optional<String>
.orElse("UNKNOWN");
// С Optional-полями используйте flatMap
record UserV2(String name, Optional<Address> address) {}
String city = findUserByIdV2(42)
.flatMap(UserV2::address) // flatMap для Optional<Optional<Address>> → Optional<Address>
.map(Address::city)
.filter(c -> !c.isBlank())
.orElse("Unknown");
Практический пример
public class OrderService {
private final UserRepository userRepo;
private final OrderRepository orderRepo;
private final DiscountService discountService;
/**
* Вычисляет финальную цену заказа с учётом скидки пользователя.
* @return финальная цена или empty, если заказ не найден
*/
public Optional<BigDecimal> calculateFinalPrice(long orderId) {
return orderRepo.findById(orderId)
.map(order -> {
BigDecimal basePrice = order.getTotalPrice();
// Получить скидку пользователя (может не быть)
BigDecimal discount = userRepo.findById(order.getUserId())
.flatMap(discountService::getDiscount)
.orElse(BigDecimal.ZERO);
return basePrice.subtract(
basePrice.multiply(discount)
);
});
}
/**
* Найти последний заказ пользователя определённой категории.
*/
public Optional<Order> findLastOrderInCategory(long userId, Category category) {
return userRepo.findById(userId)
.flatMap(user -> orderRepo.findLastByUserId(user.getId()))
.filter(order -> order.getCategory() == category);
}
}
Лучшие практики
✅ Используйте Optional для возвращаемых значений
// Хорошо: явно показывает, что результата может не быть
public Optional<User> findUserById(long id) {
User user = database.query(id);
return Optional.ofNullable(user);
}
❌ Не используйте Optional для параметров методов
// Плохо: усложняет API
public void processUser(Optional<User> user) { ... }
// Хорошо: перегрузка или @Nullable
public void processUser(User user) { ... }
public void processUser() { ... } // без пользователя
❌ Не используйте Optional для полей класса
// Плохо: Optional не Serializable, накладные расходы
class User {
private Optional<String> middleName; // Не делайте так
}
// Хорошо: nullable поле или пустая строка
class User {
private String middleName; // null означает отсутствие
public Optional<String> getMiddleName() {
return Optional.ofNullable(middleName);
}
}
❌ Не используйте Optional в коллекциях
// Плохо
List<Optional<User>> users;
// Хорошо: просто фильтруйте null
List<User> users; // без null-элементов
❌ Не используйте get() без проверки
// Плохо — эквивалентно работе с null
if (optional.isPresent()) {
return optional.get();
}
// Хорошо
return optional.orElse(defaultValue);
return optional.orElseThrow(() -> new NotFoundException());
✅ Предпочитайте функциональный стиль
// Плохо — императивный стиль
Optional<User> opt = findUser();
String name;
if (opt.isPresent()) {
name = opt.get().getName().toUpperCase();
} else {
name = "ANONYMOUS";
}
// Хорошо — функциональный стиль
String name = findUser()
.map(User::getName)
.map(String::toUpperCase)
.orElse("ANONYMOUS");
✅ orElseGet() для дорогих вычислений
// Плохо: createDefaultUser() вызывается всегда
user.orElse(createDefaultUser());
// Хорошо: вызывается только при необходимости
user.orElseGet(() -> createDefaultUser());
user.orElseGet(this::createDefaultUser);
Антипаттерны
// ❌ Optional.of(null)
Optional.of(possiblyNullValue); // NPE!
// ✅ Optional.ofNullable(possiblyNullValue);
// ❌ Сравнение с null
if (optional == null) { ... }
// ✅ Optional никогда не должен быть null
if (optional.isEmpty()) { ... }
// ❌ Вложенный Optional
Optional<Optional<String>> nested;
// ✅ Используйте flatMap
// ❌ optional.get() без проверки
return optional.get();
// ✅ return optional.orElseThrow();
// ❌ isPresent + get
if (opt.isPresent()) return opt.get();
// ✅ return opt.orElse(default);
// ❌ Возврат null вместо Optional.empty()
public Optional<User> find() {
if (notFound) return null; // Никогда!
}
// ✅ return Optional.empty();
Сравнение с Rust Option
Для знакомых с Rust — соответствие API:
| Rust Option | Java Optional |
|---|---|
Some(value) | Optional.of(value) |
None | Optional.empty() |
is_some() | isPresent() |
is_none() | isEmpty() |
unwrap() | get() / orElseThrow() |
unwrap_or(default) | orElse(default) |
unwrap_or_else(f) | orElseGet(f) |
map(f) | map(f) |
and_then(f) | flatMap(f) |
filter(p) | filter(p) |
or(other) | or(supplier) |
or_else(f) | or(f) |
Ключевое отличие: в Rust Option — это enum, pattern matching встроен в язык. В Java используется method chaining.
Резюме
Optional — это:
- Контейнер для значения, которое может отсутствовать
- Замена
nullдля возвращаемых значений - Инструмент для явного выражения намерений в API
Основные методы:
| Создание | empty(), of(v), ofNullable(v) |
|---|---|
| Проверка | isPresent(), isEmpty() |
| Извлечение | orElse(), orElseGet(), orElseThrow() |
| Действия | ifPresent(), ifPresentOrElse() |
| Преобразование | map(), flatMap(), filter() |
| Комбинирование | or(), stream() |
Когда использовать:
- ✅ Возвращаемые значения методов
- ✅ Цепочки преобразований
- ❌ Параметры методов
- ❌ Поля классов
- ❌ Коллекции
2.x. functional
functional
Материалы
Функциональные интерфейсы и лямбда-выражения — основа функционального программирования в Java, введённая в Java 8 (JSR-335). Они позволяют передавать поведение как данные, делая код лаконичнее и выразительнее.
Функциональные интерфейсы
Функциональный интерфейс — это интерфейс с ровно одним абстрактным методом (SAM — Single Abstract Method). Этот метод называется функциональным методом.
// Функциональный интерфейс
interface Converter {
String convert(int value);
}
// Использование с лямбдой
Converter hexConverter = value -> Integer.toHexString(value);
String result = hexConverter.convert(255); // "ff"
Правила определения
Интерфейс считается функциональным, если:
- Имеет ровно один абстрактный метод
- Может иметь любое количество
defaultиstaticметодов - Может иметь абстрактные методы из
Object(equals,hashCode,toString)
@FunctionalInterface
interface Processor<T> {
// Единственный абстрактный метод — функциональный
T process(T input);
// default-методы не считаются
default T processOrDefault(T input, T defaultValue) {
return input != null ? process(input) : defaultValue;
}
// static-методы не считаются
static <T> Processor<T> identity() {
return t -> t;
}
// Методы из Object не считаются
boolean equals(Object obj);
String toString();
}
Аннотация @FunctionalInterface
Аннотация @FunctionalInterface не обязательна, но рекомендуется — она документирует намерение и заставляет компилятор проверять, что интерфейс действительно функциональный:
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
// Ошибка компиляции: два абстрактных метода
@FunctionalInterface
interface Invalid {
void method1();
void method2(); // Компилятор укажет на ошибку
}
Стандартные функциональные интерфейсы
Пакет java.util.function содержит 43 готовых функциональных интерфейса. Основные четыре:
| Интерфейс | Метод | Описание | Пример |
|---|---|---|---|
Function<T,R> | R apply(T t) | Преобразование T → R | String::length |
Consumer<T> | void accept(T t) | Потребление значения | System.out::println |
Supplier<T> | T get() | Поставка значения | ArrayList::new |
Predicate<T> | boolean test(T t) | Проверка условия | String::isEmpty |
Function — преобразование
import java.util.function.Function;
Function<String, Integer> length = s -> s.length();
Function<String, Integer> length2 = String::length; // То же самое
int len = length.apply("Hello"); // 5
// Композиция функций
Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, String> trimAndUpper = trim.andThen(upper);
String result = trimAndUpper.apply(" hello "); // "HELLO"
Function<String, String> upperThenTrim = trim.compose(upper);
// Сначала upper, потом trim
Consumer — потребление
import java.util.function.Consumer;
Consumer<String> printer = s -> System.out.println(s);
Consumer<String> printer2 = System.out::println; // То же самое
printer.accept("Hello"); // Выведет: Hello
// Цепочка потребителей
Consumer<String> log = s -> System.out.println("[LOG] " + s);
Consumer<String> save = s -> database.save(s);
Consumer<String> logAndSave = log.andThen(save);
logAndSave.accept("message");
Supplier — поставка
import java.util.function.Supplier;
Supplier<Double> random = () -> Math.random();
Supplier<Double> random2 = Math::random; // То же самое
double value = random.get();
// Ленивая инициализация
Supplier<Connection> connectionSupplier = () -> {
System.out.println("Создаём соединение...");
return DriverManager.getConnection(url);
};
// Соединение создастся только при вызове get()
Predicate — проверка
import java.util.function.Predicate;
Predicate<String> isEmpty = s -> s.isEmpty();
Predicate<String> isEmpty2 = String::isEmpty; // То же самое
boolean result = isEmpty.test(""); // true
// Комбинирование предикатов
Predicate<String> isNotEmpty = isEmpty.negate();
Predicate<String> isShort = s -> s.length() < 10;
Predicate<String> isLong = s -> s.length() > 100;
Predicate<String> isNotEmptyAndShort = isNotEmpty.and(isShort);
Predicate<String> isEmptyOrLong = isEmpty.or(isLong);
// Фильтрация
List<String> filtered = strings.stream()
.filter(isNotEmptyAndShort)
.toList();
Бинарные версии (Bi-)
Для операций с двумя аргументами:
import java.util.function.*;
BiFunction<String, String, Integer> compare =
(a, b) -> a.compareTo(b);
BiConsumer<String, Integer> printWithIndex =
(s, i) -> System.out.println(i + ": " + s);
BiPredicate<String, String> startsWith =
(s, prefix) -> s.startsWith(prefix);
Операторы (Operator)
Специализированные Function, где входной и выходной типы совпадают:
import java.util.function.*;
// UnaryOperator<T> extends Function<T, T>
UnaryOperator<String> toUpper = String::toUpperCase;
String result = toUpper.apply("hello"); // "HELLO"
// BinaryOperator<T> extends BiFunction<T, T, T>
BinaryOperator<Integer> sum = (a, b) -> a + b;
BinaryOperator<Integer> max = Integer::max;
int total = sum.apply(10, 20); // 30
Примитивные специализации
Для избежания boxing/unboxing существуют специализированные версии:
// Вместо Function<Integer, Integer> — избегаем boxing
IntUnaryOperator square = n -> n * n;
int result = square.applyAsInt(5); // 25
// Вместо Predicate<Integer>
IntPredicate isPositive = n -> n > 0;
// Вместо Consumer<Double>
DoubleConsumer printer = System.out::println;
// Вместо Supplier<Long>
LongSupplier currentTime = System::currentTimeMillis;
// Конвертация между примитивами
IntToDoubleFunction intToDouble = n -> n * 1.5;
ToIntFunction<String> stringLength = String::length;
Соглашения об именовании:
IntXxx,LongXxx,DoubleXxx— аргумент примитивныйToIntXxx,ToLongXxx,ToDoubleXxx— результат примитивныйObjIntConsumer— смешанные типы (объект + примитив)
Лямбда-выражения
Лямбда-выражение — это компактная запись анонимной функции, которая может быть присвоена функциональному интерфейсу.
Синтаксис
// Полная форма
(Type1 param1, Type2 param2) -> { statements; return result; }
// Краткие формы
(param1, param2) -> { statements; return result; } // Типы выводятся
(param1, param2) -> expression // Одно выражение
param -> expression // Один параметр, без скобок
() -> expression // Без параметров
Примеры:
// Полная форма
Comparator<String> comp1 = (String a, String b) -> {
return a.compareToIgnoreCase(b);
};
// Вывод типов
Comparator<String> comp2 = (a, b) -> {
return a.compareToIgnoreCase(b);
};
// Одно выражение — return и {} не нужны
Comparator<String> comp3 = (a, b) -> a.compareToIgnoreCase(b);
// Один параметр — скобки не нужны
Function<String, Integer> length = s -> s.length();
// Без параметров
Runnable task = () -> System.out.println("Running");
// Блок с несколькими операторами
Consumer<String> logger = message -> {
String timestamp = LocalDateTime.now().toString();
System.out.println(timestamp + ": " + message);
};
Явные типы параметров (var в Java 11+)
// Явные типы нужны для аннотаций
BiFunction<String, String, String> concat =
(@NonNull String a, @NonNull String b) -> a + b;
// С Java 11 можно использовать var
BiFunction<String, String, String> concat2 =
(@NonNull var a, @NonNull var b) -> a + b;
Правило: либо все параметры с явными типами, либо все без. Смешивать нельзя:
(String a, b)— ошибка.
Целевой тип (Target Type)
Лямбда-выражение не имеет собственного типа — его тип определяется целевым типом из контекста:
// Контекст присваивания
Runnable r = () -> System.out.println("run");
Callable<String> c = () -> "result";
// Контекст аргумента метода
executor.submit(() -> System.out.println("task"));
// Контекст приведения типа
Object obj = (Runnable) () -> System.out.println("run");
// Контекст возврата
Supplier<Runnable> factory() {
return () -> System.out.println("created");
}
Одна и та же лямбда может соответствовать разным интерфейсам:
// Одна лямбда, разные целевые типы
Runnable r = () -> System.out.println("hello");
Supplier<Void> s = () -> { System.out.println("hello"); return null; };
// Вызов метода
interface Printer { void print(); }
interface Logger { void print(); }
void process(Printer p) { p.print(); }
void process(Logger l) { l.print(); }
// Неоднозначность! Нужно явное приведение:
process((Printer) () -> System.out.println("text"));
Ссылки на методы (Method References)
Ссылки на методы — это сокращённая запись лямбд, которые просто вызывают существующий метод.
Четыре вида ссылок
| Вид | Синтаксис | Эквивалентная лямбда |
|---|---|---|
| Статический метод | ClassName::staticMethod | (args) -> ClassName.staticMethod(args) |
| Метод экземпляра (конкретного объекта) | instance::method | (args) -> instance.method(args) |
| Метод экземпляра (произвольного объекта) | ClassName::method | (obj, args) -> obj.method(args) |
| Конструктор | ClassName::new | (args) -> new ClassName(args) |
Статический метод
// Лямбда
Function<String, Integer> parse1 = s -> Integer.parseInt(s);
// Ссылка на метод
Function<String, Integer> parse2 = Integer::parseInt;
// Использование
List<Integer> numbers = strings.stream()
.map(Integer::parseInt)
.toList();
Метод конкретного экземпляра
String prefix = "[INFO] ";
// Лямбда
Function<String, String> addPrefix1 = s -> prefix.concat(s);
// Ссылка на метод
Function<String, String> addPrefix2 = prefix::concat;
// Пример с System.out
Consumer<String> printer = System.out::println;
Метод произвольного экземпляра
// Лямбда: первый аргумент становится получателем метода
Comparator<String> comp1 = (a, b) -> a.compareToIgnoreCase(b);
// Ссылка на метод
Comparator<String> comp2 = String::compareToIgnoreCase;
// Унарный случай
Function<String, String> upper1 = s -> s.toUpperCase();
Function<String, String> upper2 = String::toUpperCase;
// Использование
List<String> sorted = names.stream()
.sorted(String::compareToIgnoreCase)
.toList();
Ссылка на конструктор
// Конструктор без аргументов
Supplier<ArrayList<String>> listFactory1 = () -> new ArrayList<>();
Supplier<ArrayList<String>> listFactory2 = ArrayList::new;
// Конструктор с аргументом
Function<String, StringBuilder> sbFactory1 = s -> new StringBuilder(s);
Function<String, StringBuilder> sbFactory2 = StringBuilder::new;
// Выбор конструктора определяется целевым типом
Supplier<Person> noArg = Person::new; // Person()
Function<String, Person> withName = Person::new; // Person(String name)
// Использование
List<Person> people = names.stream()
.map(Person::new)
.toList();
Ссылка на конструктор массива
// Создание массива заданной длины
IntFunction<String[]> arrayFactory1 = n -> new String[n];
IntFunction<String[]> arrayFactory2 = String[]::new;
// Полезно для toArray()
String[] array = stream.toArray(String[]::new);
Захват переменных (Capturing)
Лямбды могут использовать переменные из окружающего контекста, но только если они effectively final.
Effectively final
Переменная effectively final, если она инициализируется один раз и никогда не изменяется:
String prefix = "Hello, "; // effectively final
Consumer<String> greeter = name -> {
System.out.println(prefix + name); // OK: prefix захвачена
};
greeter.accept("World"); // "Hello, World"
String message = "Initial";
Consumer<String> printer = s -> {
System.out.println(message); // Ошибка компиляции!
};
message = "Changed"; // message больше не effectively final
Правила захвата
class Example {
private String field = "field";
void method() {
String local = "local";
Runnable r = () -> {
// this — как в окружающем контексте
System.out.println(this.field); // OK
// Захват локальной переменной
System.out.println(local); // OK, если local effectively final
// Изменение поля объекта
this.field = "new"; // OK, меняем поле, не ссылку
};
}
}
Обход ограничения
Если нужно “изменять” захваченное значение:
// Используем контейнер
int[] counter = {0}; // массив — это ссылка, она не меняется
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
numbers.forEach(n -> counter[0] += n);
System.out.println(counter[0]); // 15
// Или AtomicInteger
AtomicInteger atomicCounter = new AtomicInteger(0);
numbers.forEach(n -> atomicCounter.addAndGet(n));
Предупреждение: Изменение состояния в лямбдах может привести к проблемам с потокобезопасностью в параллельных stream’ах.
this в лямбдах
В отличие от анонимных классов, this в лямбде ссылается на окружающий объект, а не на саму лямбду:
class Button {
private String name = "Button";
void setupWithLambda() {
// this — это Button
setOnClick(() -> System.out.println(this.name));
}
void setupWithAnonymous() {
setOnClick(new ClickHandler() {
@Override
public void handle() {
// this — это анонимный ClickHandler!
// Для доступа к Button нужно Button.this.name
System.out.println(Button.this.name);
}
});
}
}
Лямбды vs анонимные классы
| Аспект | Лямбда | Анонимный класс |
|---|---|---|
| Синтаксис | Компактный | Многословный |
this | Окружающий объект | Сам анонимный класс |
| Состояние | Нет полей | Может иметь поля |
| Несколько методов | Нет | Да |
| Наследование от класса | Нет | Да |
| Сериализация | Особые правила | Стандартная |
// Лямбда — кратко
Comparator<String> c1 = (a, b) -> a.length() - b.length();
// Анонимный класс — многословно
Comparator<String> c2 = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
};
Создание собственных функциональных интерфейсов
Создавайте собственные интерфейсы, когда стандартные не подходят:
@FunctionalInterface
interface TriFunction<A, B, C, R> {
R apply(A a, B b, C c);
default <V> TriFunction<A, B, C, V> andThen(Function<R, V> after) {
return (a, b, c) -> after.apply(apply(a, b, c));
}
}
// Использование
TriFunction<Integer, Integer, Integer, Integer> sum3 =
(a, b, c) -> a + b + c;
int result = sum3.apply(1, 2, 3); // 6
@FunctionalInterface
interface ThrowingSupplier<T, E extends Exception> {
T get() throws E;
static <T> Supplier<T> unchecked(ThrowingSupplier<T, ?> supplier) {
return () -> {
try {
return supplier.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
}
// Использование с checked исключениями
Supplier<Connection> conn = ThrowingSupplier.unchecked(
() -> DriverManager.getConnection(url)
);
Практические примеры
Стратегия валидации
@FunctionalInterface
interface Validator<T> {
ValidationResult validate(T value);
default Validator<T> and(Validator<T> other) {
return value -> {
ValidationResult result = this.validate(value);
return result.isValid() ? other.validate(value) : result;
};
}
}
// Валидаторы как функции
Validator<String> notEmpty = s ->
s != null && !s.isEmpty()
? ValidationResult.ok()
: ValidationResult.error("Не может быть пустым");
Validator<String> maxLength = s ->
s.length() <= 100
? ValidationResult.ok()
: ValidationResult.error("Слишком длинное");
Validator<String> emailFormat = s ->
s.contains("@")
? ValidationResult.ok()
: ValidationResult.error("Неверный формат email");
// Композиция
Validator<String> emailValidator = notEmpty
.and(maxLength)
.and(emailFormat);
ValidationResult result = emailValidator.validate("user@example.com");
Ленивые вычисления
class Lazy<T> {
private final Supplier<T> supplier;
private T value;
private boolean computed = false;
private Lazy(Supplier<T> supplier) {
this.supplier = supplier;
}
public static <T> Lazy<T> of(Supplier<T> supplier) {
return new Lazy<>(supplier);
}
public T get() {
if (!computed) {
value = supplier.get();
computed = true;
}
return value;
}
public <R> Lazy<R> map(Function<T, R> mapper) {
return Lazy.of(() -> mapper.apply(get()));
}
}
// Использование
Lazy<ExpensiveObject> lazy = Lazy.of(() -> {
System.out.println("Создаём дорогой объект...");
return new ExpensiveObject();
});
// Объект ещё не создан
System.out.println("Перед get()");
ExpensiveObject obj = lazy.get(); // Теперь создаётся
ExpensiveObject obj2 = lazy.get(); // Возвращает кэшированный
Построитель с fluent API
class QueryBuilder {
private final List<Predicate<Record>> filters = new ArrayList<>();
private Comparator<Record> sorter = null;
private int limit = Integer.MAX_VALUE;
public QueryBuilder filter(Predicate<Record> predicate) {
filters.add(predicate);
return this;
}
public QueryBuilder sortBy(Function<Record, Comparable> keyExtractor) {
this.sorter = Comparator.comparing(keyExtractor);
return this;
}
public QueryBuilder limit(int n) {
this.limit = n;
return this;
}
public List<Record> execute(List<Record> data) {
Stream<Record> stream = data.stream();
for (Predicate<Record> filter : filters) {
stream = stream.filter(filter);
}
if (sorter != null) {
stream = stream.sorted(sorter);
}
return stream.limit(limit).toList();
}
}
// Использование
List<Record> results = new QueryBuilder()
.filter(r -> r.getStatus() == Status.ACTIVE)
.filter(r -> r.getAmount() > 1000)
.sortBy(Record::getDate)
.limit(10)
.execute(records);
Резюме
Функциональные интерфейсы и лямбды — мощные инструменты Java:
- Функциональный интерфейс имеет один абстрактный метод и служит целевым типом для лямбд
- @FunctionalInterface документирует намерение и включает проверку компилятора
- java.util.function содержит готовые интерфейсы:
Function,Consumer,Supplier,Predicateи их варианты - Лямбда-выражения — компактная запись анонимных функций
- Ссылки на методы (
::) — ещё более краткая форма для существующих методов - Лямбды захватывают только effectively final переменные
- this в лямбде ссылается на окружающий объект, а не на саму лямбду
- Лямбды компактнее анонимных классов, но ограничены одним методом
2.x. datetime
datetime
Материалы
Дата и время
Пакет java.time, введённый в Java 8, предоставляет современный API для работы с датами и временем. Все основные классы этого пакета неизменяемы (immutable) и потокобезопасны.
Обзор основных классов
| Класс | Описание | Пример |
|---|---|---|
LocalDate | Дата без времени и часового пояса | 2025-01-26 |
LocalTime | Время без даты и часового пояса | 14:30:15.123 |
LocalDateTime | Дата и время без часового пояса | 2025-01-26T14:30:15 |
ZonedDateTime | Дата и время с часовым поясом | 2025-01-26T14:30:15+03:00[Europe/Moscow] |
OffsetDateTime | Дата и время со смещением от UTC | 2025-01-26T14:30:15+03:00 |
Instant | Момент времени на временной шкале (timestamp) | 2025-01-26T11:30:15Z |
Duration | Интервал времени (часы, минуты, секунды) | PT2H30M |
Period | Интервал дат (годы, месяцы, дни) | P1Y2M10D |
LocalDate — дата без времени
LocalDate представляет дату в формате год-месяц-день без привязки к времени или часовому поясу. Используйте его для дней рождения, праздников, дедлайнов.
import java.time.LocalDate;
import java.time.Month;
// Текущая дата
LocalDate today = LocalDate.now();
// Конкретная дата
LocalDate birthday = LocalDate.of(1990, Month.MARCH, 15);
LocalDate christmas = LocalDate.of(2025, 12, 25);
// Парсинг из строки
LocalDate parsed = LocalDate.parse("2025-01-26");
Получение компонентов даты:
LocalDate date = LocalDate.of(2025, 1, 26);
int year = date.getYear(); // 2025
Month month = date.getMonth(); // JANUARY
int monthValue = date.getMonthValue(); // 1
int dayOfMonth = date.getDayOfMonth(); // 26
DayOfWeek dayOfWeek = date.getDayOfWeek(); // SUNDAY
int dayOfYear = date.getDayOfYear(); // 26
Арифметика с датами:
LocalDate date = LocalDate.of(2025, 1, 26);
LocalDate tomorrow = date.plusDays(1); // 2025-01-27
LocalDate nextWeek = date.plusWeeks(1); // 2025-02-02
LocalDate nextMonth = date.plusMonths(1); // 2025-02-26
LocalDate nextYear = date.plusYears(1); // 2026-01-26
LocalDate yesterday = date.minusDays(1); // 2025-01-25
Сравнение дат:
LocalDate date1 = LocalDate.of(2025, 1, 26);
LocalDate date2 = LocalDate.of(2025, 2, 15);
boolean isBefore = date1.isBefore(date2); // true
boolean isAfter = date1.isAfter(date2); // false
boolean isEqual = date1.isEqual(date1); // true
LocalTime — время без даты
LocalTime представляет время с точностью до наносекунд без привязки к дате или часовому поясу. Используйте его для времени открытия магазина, будильника, расписания.
import java.time.LocalTime;
// Текущее время
LocalTime now = LocalTime.now();
// Конкретное время
LocalTime morning = LocalTime.of(8, 30); // 08:30
LocalTime precise = LocalTime.of(14, 30, 15); // 14:30:15
LocalTime nanos = LocalTime.of(14, 30, 15, 123456789); // 14:30:15.123456789
// Специальные константы
LocalTime midnight = LocalTime.MIDNIGHT; // 00:00
LocalTime noon = LocalTime.NOON; // 12:00
LocalTime max = LocalTime.MAX; // 23:59:59.999999999
// Парсинг
LocalTime parsed = LocalTime.parse("14:30:15");
Получение компонентов и арифметика:
LocalTime time = LocalTime.of(14, 30, 15);
int hour = time.getHour(); // 14
int minute = time.getMinute(); // 30
int second = time.getSecond(); // 15
LocalTime later = time.plusHours(2).plusMinutes(15); // 16:45:15
LocalTime earlier = time.minusMinutes(45); // 13:45:15
LocalDateTime — дата и время
LocalDateTime объединяет дату и время, но не содержит информации о часовом поясе. Это описание даты и времени, как на настенных часах, без привязки к конкретной точке на временной шкале.
import java.time.LocalDateTime;
// Текущая дата и время
LocalDateTime now = LocalDateTime.now();
// Конкретные дата и время
LocalDateTime meeting = LocalDateTime.of(2025, Month.JANUARY, 26, 14, 30);
LocalDateTime precise = LocalDateTime.of(2025, 1, 26, 14, 30, 15, 123456789);
// Из LocalDate и LocalTime
LocalDate date = LocalDate.of(2025, 1, 26);
LocalTime time = LocalTime.of(14, 30);
LocalDateTime combined = LocalDateTime.of(date, time);
LocalDateTime atTime = date.atTime(14, 30);
LocalDateTime atDate = time.atDate(date);
// Парсинг
LocalDateTime parsed = LocalDateTime.parse("2025-01-26T14:30:15");
Извлечение компонентов:
LocalDateTime dt = LocalDateTime.of(2025, 1, 26, 14, 30, 15);
LocalDate date = dt.toLocalDate(); // 2025-01-26
LocalTime time = dt.toLocalTime(); // 14:30:15
int year = dt.getYear();
Month month = dt.getMonth();
int day = dt.getDayOfMonth();
int hour = dt.getHour();
int minute = dt.getMinute();
Важно:
LocalDateTimeне представляет момент на временной шкале. Он не может быть преобразован вInstantбез указания часового пояса. ИспользуйтеZonedDateTimeилиOffsetDateTime, когда нужна конкретная точка во времени.
Instant — момент времени
Instant представляет точку на временной шкале — количество секунд (и наносекунд) с Unix-эпохи (1 января 1970 года, 00:00:00 UTC). Это машинное представление времени, идеальное для логирования, timestamps и вычислений.
import java.time.Instant;
// Текущий момент
Instant now = Instant.now();
// Из Unix timestamp (секунды)
Instant fromEpoch = Instant.ofEpochSecond(1706270400L);
// Из миллисекунд
Instant fromMillis = Instant.ofEpochMilli(1706270400000L);
// Парсинг ISO-8601 (всегда UTC)
Instant parsed = Instant.parse("2025-01-26T11:30:00Z");
Получение значений:
Instant instant = Instant.now();
long epochSecond = instant.getEpochSecond(); // Секунды с эпохи
int nano = instant.getNano(); // Наносекунды (0-999999999)
long epochMilli = instant.toEpochMilli(); // Миллисекунды с эпохи
Арифметика:
Instant instant = Instant.parse("2025-01-26T12:00:00Z");
Instant later = instant.plusSeconds(3600); // +1 час
Instant earlier = instant.minusMillis(500); // -500 мс
Instant muchLater = instant.plus(Duration.ofHours(5)); // +5 часов
Ограничение:
Instantне поддерживает операции с днями, месяцами, годами, так как они зависят от часового пояса и календаря. Для таких операций используйтеZonedDateTime.
Часовые пояса: ZoneId и ZoneOffset
ZoneId — географический часовой пояс
ZoneId представляет часовой пояс в формате “регион/город” (например, Europe/Moscow). Он учитывает правила перехода на летнее время.
import java.time.ZoneId;
// Системный часовой пояс
ZoneId systemZone = ZoneId.systemDefault();
// Конкретный часовой пояс
ZoneId moscow = ZoneId.of("Europe/Moscow");
ZoneId tokyo = ZoneId.of("Asia/Tokyo");
ZoneId newYork = ZoneId.of("America/New_York");
ZoneId utc = ZoneId.of("UTC");
// Все доступные зоны
Set<String> zones = ZoneId.getAvailableZoneIds();
ZoneOffset — фиксированное смещение
ZoneOffset представляет фиксированное смещение от UTC, например +03:00. Он не знает о летнем времени.
import java.time.ZoneOffset;
ZoneOffset plusThree = ZoneOffset.of("+03:00");
ZoneOffset minusFive = ZoneOffset.of("-05:00");
ZoneOffset utc = ZoneOffset.UTC; // +00:00
ZoneOffset hours = ZoneOffset.ofHours(3);
ZoneOffset hoursMinutes = ZoneOffset.ofHoursMinutes(5, 30);
ZonedDateTime — дата и время с часовым поясом
ZonedDateTime — это полное представление даты и времени с часовым поясом. Он учитывает переход на летнее время и другие правила часовых поясов.
import java.time.ZonedDateTime;
import java.time.ZoneId;
// Текущее время в системном часовом поясе
ZonedDateTime now = ZonedDateTime.now();
// Текущее время в конкретном поясе
ZonedDateTime nowInTokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
// Конкретная дата/время в поясе
ZonedDateTime meeting = ZonedDateTime.of(
2025, 1, 26, 14, 30, 0, 0,
ZoneId.of("Europe/Moscow")
);
// Из LocalDateTime
LocalDateTime local = LocalDateTime.of(2025, 1, 26, 14, 30);
ZonedDateTime zoned = local.atZone(ZoneId.of("Europe/Moscow"));
// Парсинг
ZonedDateTime parsed = ZonedDateTime.parse("2025-01-26T14:30:00+03:00[Europe/Moscow]");
Конвертация между часовыми поясами:
ZonedDateTime moscowTime = ZonedDateTime.of(
2025, 1, 26, 14, 30, 0, 0,
ZoneId.of("Europe/Moscow")
);
// Тот же момент в другом поясе (время изменится)
ZonedDateTime tokyoTime = moscowTime.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
// 2025-01-26T20:30+09:00[Asia/Tokyo]
// То же локальное время в другом поясе (момент изменится)
ZonedDateTime sameClock = moscowTime.withZoneSameLocal(ZoneId.of("Asia/Tokyo"));
// 2025-01-26T14:30+09:00[Asia/Tokyo]
Проблемы перехода на летнее время
При переходе на летнее время возникают gap (пропуск) и overlap (наложение):
// Gap: при переходе на летнее время 2:00 -> 3:00
// Время 2:30 не существует!
ZonedDateTime gapTime = ZonedDateTime.of(
LocalDateTime.of(2024, 3, 31, 2, 30),
ZoneId.of("Europe/Berlin")
);
// Java автоматически сдвинет на 3:30
// Overlap: при переходе на зимнее время 3:00 -> 2:00
// Время 2:30 существует дважды
ZonedDateTime overlapTime = ZonedDateTime.of(
LocalDateTime.of(2024, 10, 27, 2, 30),
ZoneId.of("Europe/Berlin")
);
// Java выберет более раннее смещение
// Для контроля используйте:
ZonedDateTime withLaterOffset = overlapTime.withLaterOffsetAtOverlap();
OffsetDateTime — дата и время со смещением
OffsetDateTime хранит дату, время и фиксированное смещение от UTC, но без информации о часовом поясе. Используйте его для сериализации, хранения в базе данных, передачи по сети.
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
// Текущее время со смещением
OffsetDateTime now = OffsetDateTime.now();
// С конкретным смещением
OffsetDateTime withOffset = OffsetDateTime.now(ZoneOffset.of("+03:00"));
// Из LocalDateTime
LocalDateTime local = LocalDateTime.of(2025, 1, 26, 14, 30);
OffsetDateTime offset = local.atOffset(ZoneOffset.of("+03:00"));
// Парсинг
OffsetDateTime parsed = OffsetDateTime.parse("2025-01-26T14:30:00+03:00");
Когда использовать ZonedDateTime vs OffsetDateTime
Используйте ZonedDateTime | Используйте OffsetDateTime |
|---|---|
| Для UI и отображения пользователю | Для хранения в базе данных |
| Когда важны правила перехода времени | Для сериализации в JSON/XML |
| Для планирования событий в будущем | Для логирования |
| Для расчётов с учётом часового пояса | Когда достаточно фиксированного смещения |
Duration — интервал времени
Duration представляет интервал времени в секундах и наносекундах. Используйте его для измерения времени между моментами.
import java.time.Duration;
import java.time.temporal.ChronoUnit;
// Создание
Duration tenSeconds = Duration.ofSeconds(10);
Duration fiveMinutes = Duration.ofMinutes(5);
Duration twoHours = Duration.ofHours(2);
Duration oneDay = Duration.ofDays(1); // 24 часа ровно
Duration complex = Duration.ofHours(2).plusMinutes(30).plusSeconds(15);
// Парсинг ISO-8601
Duration parsed = Duration.parse("PT2H30M15S"); // 2 часа 30 минут 15 секунд
// Между двумя моментами
Instant start = Instant.now();
// ... выполнение кода ...
Instant end = Instant.now();
Duration elapsed = Duration.between(start, end);
// Между LocalDateTime
LocalDateTime dt1 = LocalDateTime.of(2025, 1, 26, 10, 0);
LocalDateTime dt2 = LocalDateTime.of(2025, 1, 26, 14, 30);
Duration between = Duration.between(dt1, dt2); // PT4H30M
Получение компонентов:
Duration duration = Duration.parse("PT2H30M15.5S");
long seconds = duration.getSeconds(); // 9015
int nanos = duration.getNano(); // 500000000
long totalMinutes = duration.toMinutes(); // 150
long totalHours = duration.toHours(); // 2
long totalMillis = duration.toMillis(); // 9015500
Арифметика:
Duration duration = Duration.ofHours(2);
Duration doubled = duration.multipliedBy(2); // PT4H
Duration halved = duration.dividedBy(2); // PT1H
Duration negated = duration.negated(); // PT-2H
Duration longer = duration.plusMinutes(30); // PT2H30M
Duration shorter = duration.minusSeconds(15); // PT1H59M45S
Period — интервал дат
Period представляет интервал в годах, месяцах и днях. Используйте его для календарных вычислений.
import java.time.Period;
// Создание
Period tenDays = Period.ofDays(10);
Period twoMonths = Period.ofMonths(2);
Period oneYear = Period.ofYears(1);
Period complex = Period.of(1, 2, 10); // 1 год 2 месяца 10 дней
// Парсинг ISO-8601
Period parsed = Period.parse("P1Y2M10D");
// Между двумя датами
LocalDate start = LocalDate.of(2024, 1, 1);
LocalDate end = LocalDate.of(2025, 3, 15);
Period between = Period.between(start, end); // P1Y2M14D
Получение компонентов:
Period period = Period.between(
LocalDate.of(2024, 1, 1),
LocalDate.of(2025, 3, 15)
);
int years = period.getYears(); // 1
int months = period.getMonths(); // 2
int days = period.getDays(); // 14
// Общее количество месяцев
long totalMonths = period.toTotalMonths(); // 14
Применение к датам:
LocalDate date = LocalDate.of(2025, 1, 26);
Period period = Period.of(1, 2, 10);
LocalDate future = date.plus(period); // 2026-04-05
LocalDate past = date.minus(period); // 2023-11-16
Разница между Duration и Period:
Durationпредставляет точное количество времени (1 день = ровно 24 часа).Periodпредставляет календарный интервал (1 месяц может быть 28, 29, 30 или 31 день).
ChronoUnit — единицы измерения
ChronoUnit позволяет измерять расстояние между датами/временем в конкретных единицах:
import java.time.temporal.ChronoUnit;
LocalDate date1 = LocalDate.of(2024, 1, 1);
LocalDate date2 = LocalDate.of(2025, 6, 15);
long days = ChronoUnit.DAYS.between(date1, date2); // 531
long weeks = ChronoUnit.WEEKS.between(date1, date2); // 75
long months = ChronoUnit.MONTHS.between(date1, date2); // 17
long years = ChronoUnit.YEARS.between(date1, date2); // 1
LocalDateTime dt1 = LocalDateTime.of(2025, 1, 26, 10, 0);
LocalDateTime dt2 = LocalDateTime.of(2025, 1, 26, 14, 30);
long hours = ChronoUnit.HOURS.between(dt1, dt2); // 4
long minutes = ChronoUnit.MINUTES.between(dt1, dt2); // 270
DateTimeFormatter — форматирование и парсинг
DateTimeFormatter используется для преобразования дат в строки и обратно.
Предопределённые форматтеры
import java.time.format.DateTimeFormatter;
LocalDateTime dt = LocalDateTime.of(2025, 1, 26, 14, 30, 15);
// ISO форматы
String iso = dt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
// "2025-01-26T14:30:15"
String isoDate = dt.format(DateTimeFormatter.ISO_LOCAL_DATE);
// "2025-01-26"
String isoTime = dt.format(DateTimeFormatter.ISO_LOCAL_TIME);
// "14:30:15"
Пользовательские паттерны
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
LocalDateTime dt = LocalDateTime.of(2025, 1, 26, 14, 30);
// Форматирование
String formatted = dt.format(formatter); // "26.01.2025 14:30"
// Парсинг
LocalDateTime parsed = LocalDateTime.parse("26.01.2025 14:30", formatter);
Основные символы паттернов:
| Символ | Значение | Пример |
|---|---|---|
y | Год | 2025 |
M | Месяц | 1, 01, Jan, January |
d | День месяца | 26 |
E | День недели | Mon, Monday |
H | Час (0-23) | 14 |
h | Час (1-12) | 2 |
m | Минуты | 30 |
s | Секунды | 15 |
S | Доли секунды | 123 |
a | AM/PM | PM |
z | Название часового пояса | MSK |
Z | Смещение | +0300 |
X | Смещение с Z для UTC | +03 или Z |
Примеры паттернов:
// dd.MM.yyyy -> 26.01.2025
// yyyy-MM-dd -> 2025-01-26
// d MMMM yyyy -> 26 января 2025
// EEE, MMM d, yyyy -> Sun, Jan 26, 2025
// HH:mm:ss -> 14:30:15
// hh:mm a -> 02:30 PM
// yyyy-MM-dd'T'HH:mm:ss.SSSXXX -> 2025-01-26T14:30:15.000+03:00
Локализованные форматы
import java.time.format.FormatStyle;
import java.util.Locale;
LocalDateTime dt = LocalDateTime.of(2025, 1, 26, 14, 30);
// С текущей локалью
DateTimeFormatter full = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL);
DateTimeFormatter medium = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);
// С конкретной локалью
DateTimeFormatter russian = DateTimeFormatter
.ofLocalizedDate(FormatStyle.LONG)
.withLocale(new Locale("ru", "RU"));
String formatted = dt.toLocalDate().format(russian); // "26 января 2025 г."
TemporalAdjusters — корректировщики дат
TemporalAdjusters предоставляет готовые методы для сложных операций с датами:
import java.time.temporal.TemporalAdjusters;
LocalDate date = LocalDate.of(2025, 1, 26);
// Первый/последний день месяца
LocalDate firstDay = date.with(TemporalAdjusters.firstDayOfMonth()); // 2025-01-01
LocalDate lastDay = date.with(TemporalAdjusters.lastDayOfMonth()); // 2025-01-31
// Первый/последний день года
LocalDate firstOfYear = date.with(TemporalAdjusters.firstDayOfYear()); // 2025-01-01
LocalDate lastOfYear = date.with(TemporalAdjusters.lastDayOfYear()); // 2025-12-31
// Следующий/предыдущий день недели
LocalDate nextMonday = date.with(TemporalAdjusters.next(DayOfWeek.MONDAY)); // 2025-01-27
LocalDate prevFriday = date.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY)); // 2025-01-24
// Первый конкретный день недели в месяце
LocalDate firstMondayOfMonth = date.with(
TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)
); // 2025-01-06
// Последний конкретный день недели в месяце
LocalDate lastFridayOfMonth = date.with(
TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY)
); // 2025-01-31
Конвертация из java.util.Date
При работе с legacy-кодом часто требуется конвертация между старым java.util.Date и новыми классами.
Date → новые классы
import java.util.Date;
import java.time.*;
Date legacyDate = new Date();
// Date -> Instant
Instant instant = legacyDate.toInstant();
// Date -> LocalDate
LocalDate localDate = legacyDate.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDate();
// Date -> LocalDateTime
LocalDateTime localDateTime = legacyDate.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
// Date -> ZonedDateTime
ZonedDateTime zonedDateTime = legacyDate.toInstant()
.atZone(ZoneId.systemDefault());
Новые классы → Date
// Instant -> Date
Instant instant = Instant.now();
Date fromInstant = Date.from(instant);
// LocalDate -> Date
LocalDate localDate = LocalDate.now();
Date fromLocalDate = Date.from(
localDate.atStartOfDay(ZoneId.systemDefault()).toInstant()
);
// LocalDateTime -> Date
LocalDateTime localDateTime = LocalDateTime.now();
Date fromLocalDateTime = Date.from(
localDateTime.atZone(ZoneId.systemDefault()).toInstant()
);
// ZonedDateTime -> Date
ZonedDateTime zonedDateTime = ZonedDateTime.now();
Date fromZoned = Date.from(zonedDateTime.toInstant());
Пример: планировщик событий
Рассмотрим практический пример — расчёт времени прибытия авиарейса:
import java.time.*;
import java.time.format.DateTimeFormatter;
public class FlightScheduler {
public static void main(String[] args) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d MMM yyyy HH:mm z");
// Вылет из Москвы 26 января в 14:30
LocalDateTime departureLocal = LocalDateTime.of(2025, 1, 26, 14, 30);
ZoneId moscowZone = ZoneId.of("Europe/Moscow");
ZonedDateTime departure = ZonedDateTime.of(departureLocal, moscowZone);
System.out.println("Вылет: " + departure.format(formatter));
// Вылет: 26 янв. 2025 14:30 MSK
// Время полёта: 9 часов 45 минут
Duration flightDuration = Duration.ofHours(9).plusMinutes(45);
// Прибытие в Токио
ZoneId tokyoZone = ZoneId.of("Asia/Tokyo");
ZonedDateTime arrival = departure
.plus(flightDuration)
.withZoneSameInstant(tokyoZone);
System.out.println("Прибытие: " + arrival.format(formatter));
// Прибытие: 27 янв. 2025 06:15 JST
// Местное время в Москве при прибытии
ZonedDateTime arrivalMoscowTime = arrival.withZoneSameInstant(moscowZone);
System.out.println("По Москве: " + arrivalMoscowTime.format(formatter));
// По Москве: 27 янв. 2025 00:15 MSK
}
}
Рекомендации по выбору класса
Нужно хранить момент времени (timestamp)?
├── Да → Instant
└── Нет, нужна читаемая дата/время
├── Нужен часовой пояс?
│ ├── Да, с учётом летнего времени → ZonedDateTime
│ └── Да, только смещение → OffsetDateTime
└── Нет, достаточно локального значения
├── Только дата → LocalDate
├── Только время → LocalTime
└── Дата и время → LocalDateTime
Резюме
Пакет java.time предоставляет мощный и выразительный API для работы с датами и временем:
- Используйте
LocalDate,LocalTime,LocalDateTimeдля дат/времени без часовых поясов - Используйте
Instantдля машинных timestamps - Используйте
ZonedDateTimeдля полной поддержки часовых поясов с правилами перехода времени - Используйте
OffsetDateTimeдля сериализации и хранения в базах данных - Используйте
Durationдля временных интервалов иPeriodдля календарных - Используйте
DateTimeFormatterдля форматирования и парсинга - Все классы неизменяемы и потокобезопасны
2.x. exceptions
exceptions
Материалы
Когда программа нарушает семантические правила Java, виртуальная машина сигнализирует об этом через исключение. Исключение вызывает нелокальную передачу управления от точки возникновения к обработчику, который может её перехватить.
В отличие от возврата специальных значений (вроде -1 или null), исключения нельзя случайно проигнорировать — они требуют явной обработки или объявления.
Иерархия исключений
Все исключения в Java представлены объектами, унаследованными от класса Throwable:
Throwable
├── Error (unchecked)
│ ├── VirtualMachineError
│ │ ├── StackOverflowError
│ │ └── OutOfMemoryError
│ ├── LinkageError
│ │ ├── NoClassDefFoundError
│ │ └── UnsatisfiedLinkError
│ └── AssertionError
│
└── Exception (checked*)
├── IOException
│ ├── FileNotFoundException
│ └── EOFException
├── SQLException
├── ReflectiveOperationException
│ ├── ClassNotFoundException
│ └── NoSuchMethodException
│
└── RuntimeException (unchecked)
├── NullPointerException
├── IllegalArgumentException
│ └── NumberFormatException
├── IllegalStateException
├── IndexOutOfBoundsException
│ ├── ArrayIndexOutOfBoundsException
│ └── StringIndexOutOfBoundsException
├── ArithmeticException
├── ClassCastException
└── UnsupportedOperationException
Checked vs Unchecked исключения
Java делит исключения на две категории:
Checked (проверяемые)
Checked исключения — это все исключения, кроме RuntimeException, Error и их подклассов. Компилятор требует их обработки или объявления в throws.
// FileNotFoundException — checked, компилятор требует обработки
public void readFile(String path) throws FileNotFoundException {
FileInputStream fis = new FileInputStream(path);
// ...
}
Checked исключения представляют восстановимые ситуации: файл не найден, сеть недоступна, неверный формат данных. Программа может и должна на них реагировать.
Unchecked (непроверяемые)
Unchecked исключения — это RuntimeException, Error и их подклассы. Компилятор не требует их обработки.
// NullPointerException — unchecked, обработка не обязательна
public int getLength(String s) {
return s.length(); // Может бросить NPE, но компилятор не требует try-catch
}
RuntimeException представляет ошибки программирования: обращение к null, выход за границы массива, деление на ноль. Такие ошибки должны исправляться в коде, а не обрабатываться в runtime.
Error представляет серьёзные проблемы JVM, от которых программа обычно не может восстановиться: нехватка памяти, переполнение стека, ошибки загрузки классов.
| Тип | Проверка компилятором | Примеры | Когда использовать |
|---|---|---|---|
| Checked | Да | IOException, SQLException | Восстановимые внешние ошибки |
| RuntimeException | Нет | NullPointerException, IllegalArgumentException | Ошибки программирования |
| Error | Нет | OutOfMemoryError, StackOverflowError | Критические проблемы JVM |
Бросание исключений
Исключение бросается оператором throw:
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("Возраст не может быть отрицательным: " + age);
}
this.age = age;
}
Причины возникновения исключений
Исключение может быть брошено в четырёх случаях:
- Явный
throw— программист бросает исключение - Провал
assert— приassert false - Синхронная ошибка JVM — деление на ноль, выход за границы массива,
null-разыменование - Асинхронная ошибка JVM — критические проблемы вроде
OutOfMemoryError
// 1. Явный throw
throw new RuntimeException("Что-то пошло не так");
// 2. Провал assert
assert value > 0 : "Значение должно быть положительным";
// 3. Синхронные ошибки JVM
int result = 10 / 0; // ArithmeticException
String s = null; s.length(); // NullPointerException
int[] arr = new int[5]; arr[10] = 1; // ArrayIndexOutOfBoundsException
Объявление throws
Метод, который может бросить checked исключение, должен объявить его в сигнатуре:
public void readConfig(String path) throws IOException, ParseException {
// ...
}
Объявление throws — это контракт между методом и вызывающим кодом. Вызывающий код обязан либо обработать исключение, либо передать его дальше.
// Вариант 1: обработать
public void loadSettings() {
try {
readConfig("settings.json");
} catch (IOException e) {
useDefaultSettings();
} catch (ParseException e) {
throw new IllegalStateException("Некорректный файл конфигурации", e);
}
}
// Вариант 2: передать дальше
public void loadSettings() throws IOException, ParseException {
readConfig("settings.json");
}
Перехват исключений: try-catch-finally
Базовый try-catch
try {
riskyOperation();
} catch (SpecificException e) {
// Обработка конкретного исключения
handleError(e);
}
При возникновении исключения JVM ищет подходящий catch-блок, проверяя каждый по порядку. Блок подходит, если класс исключения — это указанный класс или его подкласс.
try {
readFile("data.txt");
} catch (FileNotFoundException e) {
System.out.println("Файл не найден: " + e.getMessage());
} catch (IOException e) {
System.out.println("Ошибка ввода-вывода: " + e.getMessage());
}
Важно: Более специфичные исключения должны идти раньше более общих.
catch (Exception e)передcatch (IOException e)вызовет ошибку компиляции.
Multi-catch (Java 7+)
Если обработка нескольких исключений одинакова, можно объединить их в один блок:
try {
processData();
} catch (IOException | ParseException | ValidationException e) {
logger.error("Ошибка обработки данных", e);
throw new ProcessingException("Не удалось обработать данные", e);
}
В multi-catch параметр e неявно final — его нельзя переприсвоить.
finally
Блок finally выполняется всегда — независимо от того, возникло исключение или нет:
FileInputStream fis = null;
try {
fis = new FileInputStream("data.txt");
processStream(fis);
} catch (IOException e) {
handleError(e);
} finally {
// Выполнится в любом случае
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
// Игнорируем ошибку закрытия
}
}
}
finally выполняется даже если:
- В
tryилиcatchестьreturn - В
tryилиcatchброшено исключение tryзавершился нормально
public int getValue() {
try {
return 1;
} finally {
System.out.println("finally выполнен"); // Выведется!
}
}
Предупреждение: Если
finallyбросает исключение или содержитreturn, это подавляет исходное исключение изtry/catch. Избегайте этого.
try {
throw new RuntimeException("Оригинальное исключение");
} finally {
throw new RuntimeException("Исключение из finally");
// Оригинальное исключение потеряно!
}
try-with-resources (Java 7+)
Для ресурсов, реализующих AutoCloseable, используйте try-with-resources — ресурс будет автоматически закрыт:
try (FileInputStream fis = new FileInputStream("data.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) {
String line;
while ((line = reader.readLine()) != null) {
processLine(line);
}
} catch (IOException e) {
handleError(e);
}
// fis и reader автоматически закрыты здесь
Ресурсы закрываются в обратном порядке объявления. Если close() бросает исключение, а в try уже было исключение, то исключение из close() добавляется как suppressed.
try (ProblematicResource r = new ProblematicResource()) {
throw new RuntimeException("Основное исключение");
}
// Если r.close() тоже бросит исключение, оно будет suppressed
// Получить suppressed исключения:
catch (RuntimeException e) {
Throwable[] suppressed = e.getSuppressed();
}
Класс Throwable
Все исключения наследуют от Throwable, который предоставляет:
public class Throwable {
// Сообщение об ошибке
String getMessage();
// Подробное сообщение (обычно совпадает с getMessage)
String getLocalizedMessage();
// Причина исключения (для chained exceptions)
Throwable getCause();
// Установить причину (можно вызвать только один раз)
Throwable initCause(Throwable cause);
// Стек вызовов
StackTraceElement[] getStackTrace();
// Печать стека вызовов
void printStackTrace();
void printStackTrace(PrintStream s);
void printStackTrace(PrintWriter s);
// Подавленные исключения (suppressed)
void addSuppressed(Throwable exception);
Throwable[] getSuppressed();
}
Сообщение об ошибке
Всегда передавайте информативное сообщение:
// Плохо
throw new IllegalArgumentException();
// Хорошо
throw new IllegalArgumentException(
"Порт должен быть в диапазоне 1-65535, получено: " + port
);
Stack trace
Stack trace показывает цепочку вызовов до точки исключения:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke method on null
at com.example.Service.process(Service.java:42)
at com.example.Controller.handle(Controller.java:28)
at com.example.Main.main(Main.java:15)
Читается снизу вверх: main вызвал handle, который вызвал process, где произошло исключение на строке 42.
Chained exceptions (цепочка исключений)
При преобразовании исключения сохраняйте оригинальную причину:
try {
lowLevelOperation();
} catch (SQLException e) {
// Сохраняем оригинальное исключение как причину
throw new ServiceException("Не удалось выполнить операцию", e);
}
Это позволяет видеть полную цепочку в stack trace:
ServiceException: Не удалось выполнить операцию
at com.example.Service.doWork(Service.java:50)
...
Caused by: java.sql.SQLException: Connection refused
at com.mysql.jdbc.Driver.connect(Driver.java:123)
...
Создание собственных исключений
Создавайте собственные исключения для domain-специфичных ошибок:
// Checked исключение
public class InsufficientFundsException extends Exception {
private final BigDecimal balance;
private final BigDecimal amount;
public InsufficientFundsException(BigDecimal balance, BigDecimal amount) {
super(String.format(
"Недостаточно средств: баланс %.2f, требуется %.2f",
balance, amount
));
this.balance = balance;
this.amount = amount;
}
public BigDecimal getBalance() { return balance; }
public BigDecimal getAmount() { return amount; }
}
// Unchecked исключение
public class ConfigurationException extends RuntimeException {
public ConfigurationException(String message) {
super(message);
}
public ConfigurationException(String message, Throwable cause) {
super(message, cause);
}
}
Какой тип выбрать?
| Выбирайте | Когда |
|---|---|
Checked (extends Exception) | Вызывающий код может и должен обработать ошибку |
Unchecked (extends RuntimeException) | Ошибка программирования или неисправимая ситуация |
Примечание: Класс исключения не может быть generic (
class MyException<T> extends Exception— ошибка компиляции).
Переопределение методов и throws
При переопределении метода нельзя объявлять новые checked исключения или более широкие, чем в родительском методе:
class Parent {
void process() throws IOException { }
}
class Child extends Parent {
// OK: то же исключение
@Override
void process() throws IOException { }
}
class Child2 extends Parent {
// OK: более узкое исключение (подкласс)
@Override
void process() throws FileNotFoundException { }
}
class Child3 extends Parent {
// OK: вообще без исключений
@Override
void process() { }
}
class Child4 extends Parent {
// ОШИБКА: SQLException не объявлен в Parent
@Override
void process() throws SQLException { } // Не компилируется!
}
Это правило обеспечивает подстановку Лисков: код, работающий с Parent, ожидает только IOException.
Обработка исключений: лучшие практики
1. Не игнорируйте исключения
// ПЛОХО: исключение проглочено
try {
doSomething();
} catch (Exception e) {
// пусто
}
// Как минимум — логируйте
try {
doSomething();
} catch (Exception e) {
logger.error("Ошибка в doSomething", e);
}
2. Не ловите Exception или Throwable без необходимости
// ПЛОХО: ловит всё, включая NullPointerException
try {
processData();
} catch (Exception e) {
// Можем скрыть баги
}
// ЛУЧШЕ: ловим конкретные исключения
try {
processData();
} catch (IOException e) {
handleIOError(e);
} catch (ParseException e) {
handleParseError(e);
}
3. Используйте try-with-resources для ресурсов
// ПЛОХО: многословно и легко ошибиться
FileInputStream fis = null;
try {
fis = new FileInputStream("file.txt");
// ...
} finally {
if (fis != null) {
try { fis.close(); } catch (IOException e) { }
}
}
// ХОРОШО
try (FileInputStream fis = new FileInputStream("file.txt")) {
// ...
}
4. Fail fast — бросайте исключения рано
public void processOrder(Order order) {
// Проверяем аргументы сразу
if (order == null) {
throw new NullPointerException("order не может быть null");
}
if (order.getItems().isEmpty()) {
throw new IllegalArgumentException("Заказ не может быть пустым");
}
// Основная логика
// ...
}
5. Документируйте исключения
/**
* Переводит средства между счетами.
*
* @param from счёт-источник
* @param to счёт-получатель
* @param amount сумма перевода
* @throws InsufficientFundsException если на счёте-источнике недостаточно средств
* @throws AccountLockedException если один из счетов заблокирован
* @throws IllegalArgumentException если amount <= 0
*/
public void transfer(Account from, Account to, BigDecimal amount)
throws InsufficientFundsException, AccountLockedException {
// ...
}
6. Преобразуйте исключения на границах слоёв
// В слое репозитория
public User findById(Long id) throws UserNotFoundException {
try {
return jdbcTemplate.queryForObject(SQL, mapper, id);
} catch (EmptyResultDataAccessException e) {
throw new UserNotFoundException("Пользователь не найден: " + id, e);
}
// SQLException пробрасывается как DataAccessException (unchecked)
}
Пример: обработка ошибок в приложении
public class OrderService {
private final OrderRepository repository;
private final PaymentGateway paymentGateway;
public Order placeOrder(OrderRequest request) throws OrderException {
// Валидация (fail fast)
validateRequest(request);
Order order = createOrder(request);
try {
// Попытка оплаты
PaymentResult result = paymentGateway.charge(
request.getPaymentMethod(),
order.getTotal()
);
if (!result.isSuccessful()) {
throw new PaymentFailedException(result.getErrorMessage());
}
order.setPaymentId(result.getTransactionId());
order.setStatus(OrderStatus.PAID);
// Сохранение
return repository.save(order);
} catch (PaymentGatewayException e) {
// Преобразуем в domain-исключение
throw new OrderException("Ошибка при обработке платежа", e);
} catch (DataAccessException e) {
// Пытаемся отменить платёж
tryRefund(order);
throw new OrderException("Ошибка при сохранении заказа", e);
}
}
private void validateRequest(OrderRequest request) {
if (request == null) {
throw new IllegalArgumentException("request не может быть null");
}
if (request.getItems() == null || request.getItems().isEmpty()) {
throw new IllegalArgumentException("Заказ должен содержать товары");
}
if (request.getPaymentMethod() == null) {
throw new IllegalArgumentException("Способ оплаты обязателен");
}
}
private void tryRefund(Order order) {
if (order.getPaymentId() != null) {
try {
paymentGateway.refund(order.getPaymentId());
} catch (Exception e) {
// Логируем, но не бросаем — основное исключение важнее
logger.error("Не удалось отменить платёж: {}", order.getPaymentId(), e);
}
}
}
}
Резюме
Система исключений Java обеспечивает надёжную обработку ошибок:
- Checked исключения требуют явной обработки и представляют восстановимые ошибки
- Unchecked исключения (
RuntimeException) представляют ошибки программирования - Error — критические проблемы JVM, которые обычно не обрабатываются
- Используйте try-with-resources для автоматического закрытия ресурсов
- Сохраняйте цепочку исключений через конструктор с
cause - Fail fast — бросайте исключения при первых признаках проблемы
- Не игнорируйте исключения — как минимум логируйте их
- Документируйте исключения в Javadoc
2.x. io nio
io nio
Материалы
Java предоставляет две системы ввода-вывода: классический IO (пакет java.io, с Java 1.0) и NIO (пакеты java.nio.*, с Java 1.4/1.7). Они решают одни задачи разными способами и часто используются совместно.
Обзор и сравнение
| Аспект | IO (java.io) | NIO (java.nio) |
|---|---|---|
| Модель | Потоковая (Stream) | Буферная (Buffer + Channel) |
| Направление | Однонаправленные потоки | Двунаправленные каналы |
| Блокировка | Блокирующий | Блокирующий / неблокирующий |
| Подход | Байт за байтом | Блоками данных |
| Файловая система | File | Path + Files (NIO.2) |
| Появление | Java 1.0 | Java 1.4 (NIO), Java 7 (NIO.2) |
Часть 1: Классический IO (java.io)
Иерархия потоков
IO построен на четырёх абстрактных классах:
Байтовые потоки:
InputStream ─────► OutputStream
│ │
FileInputStream FileOutputStream
BufferedInputStream BufferedOutputStream
DataInputStream DataOutputStream
ByteArrayInputStream ByteArrayOutputStream
Символьные потоки:
Reader ─────────► Writer
│ │
FileReader FileWriter
BufferedReader BufferedWriter
InputStreamReader OutputStreamWriter
StringReader StringWriter
Байтовые потоки (InputStream / OutputStream)
Работают с сырыми байтами — для бинарных данных, изображений, архивов:
// Чтение файла побайтово
try (InputStream in = new FileInputStream("image.png")) {
int byteValue;
while ((byteValue = in.read()) != -1) {
// byteValue содержит значение 0-255
process(byteValue);
}
}
// Чтение блоками — эффективнее
try (InputStream in = new FileInputStream("data.bin")) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
// bytesRead — сколько байт прочитано в buffer
process(buffer, 0, bytesRead);
}
}
// Запись в файл
try (OutputStream out = new FileOutputStream("output.bin")) {
out.write(65); // Один байт
out.write(new byte[]{66, 67, 68}); // Массив байтов
}
Символьные потоки (Reader / Writer)
Работают с символами Unicode — для текста:
// Чтение текстового файла
try (Reader reader = new FileReader("text.txt", StandardCharsets.UTF_8)) {
int charValue;
while ((charValue = reader.read()) != -1) {
char c = (char) charValue;
System.out.print(c);
}
}
// Запись текста
try (Writer writer = new FileWriter("output.txt", StandardCharsets.UTF_8)) {
writer.write("Привет, мир!");
writer.write('\n');
writer.write(new char[]{'A', 'B', 'C'});
}
Буферизация
Буферизованные потоки значительно повышают производительность, снижая количество обращений к ОС:
// Буферизованное чтение текста построчно
try (BufferedReader reader = new BufferedReader(
new FileReader("log.txt", StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
// Буферизованная запись
try (BufferedWriter writer = new BufferedWriter(
new FileWriter("output.txt", StandardCharsets.UTF_8))) {
writer.write("Первая строка");
writer.newLine(); // Платформозависимый перевод строки
writer.write("Вторая строка");
}
// Буферизованные байтовые потоки
try (BufferedInputStream in = new BufferedInputStream(
new FileInputStream("large.bin"), 65536)) { // 64KB буфер
// Чтение теперь идёт из буфера в памяти
}
Совет: Всегда оборачивайте файловые потоки в буферизованные. Размер буфера по умолчанию — 8192 байта, но для больших файлов можно увеличить.
Мост между байтами и символами
InputStreamReader и OutputStreamWriter преобразуют байты в символы и обратно:
// Чтение из байтового потока как текст
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("data.txt"),
StandardCharsets.UTF_8))) {
String line = reader.readLine();
}
// Запись текста в байтовый поток
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream("data.txt"),
StandardCharsets.UTF_8))) {
writer.write("Текст в UTF-8");
}
// Чтение из System.in (стандартный ввод)
BufferedReader console = new BufferedReader(
new InputStreamReader(System.in, Charset.defaultCharset()));
String userInput = console.readLine();
Потоки данных (DataInputStream / DataOutputStream)
Для чтения/записи примитивных типов в бинарном формате:
// Запись примитивов
try (DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream("data.bin")))) {
out.writeInt(42);
out.writeDouble(3.14159);
out.writeUTF("Hello"); // Modified UTF-8
out.writeBoolean(true);
}
// Чтение в том же порядке
try (DataInputStream in = new DataInputStream(
new BufferedInputStream(new FileInputStream("data.bin")))) {
int i = in.readInt();
double d = in.readDouble();
String s = in.readUTF();
boolean b = in.readBoolean();
}
PrintStream и PrintWriter
Удобные методы для форматированного вывода:
// PrintStream — для байтовых потоков (System.out — это PrintStream)
try (PrintStream ps = new PrintStream("output.txt", StandardCharsets.UTF_8)) {
ps.println("Строка");
ps.printf("Число: %d, Дробь: %.2f%n", 42, 3.14);
ps.print(true);
}
// PrintWriter — для символьных потоков
try (PrintWriter pw = new PrintWriter(
new BufferedWriter(new FileWriter("output.txt")))) {
pw.println("Строка");
pw.printf("Дата: %tF%n", LocalDate.now());
}
Важно:
PrintStreamиPrintWriterне бросаютIOException— они подавляют исключения. ПроверяйтеcheckError()если нужна надёжность.
Класс File
Представление пути к файлу или директории:
File file = new File("data/config.txt");
File dir = new File("/home/user/documents");
// Информация о файле
boolean exists = file.exists();
boolean isFile = file.isFile();
boolean isDir = file.isDirectory();
long size = file.length();
long modified = file.lastModified();
String name = file.getName(); // "config.txt"
String path = file.getPath(); // "data/config.txt"
String absPath = file.getAbsolutePath();
// Операции с файлами
file.createNewFile();
file.delete();
file.renameTo(new File("new-name.txt"));
// Работа с директориями
dir.mkdir(); // Создать одну директорию
dir.mkdirs(); // Создать все директории в пути
String[] names = dir.list(); // Имена файлов
File[] files = dir.listFiles(); // Объекты File
File[] filtered = dir.listFiles(f -> f.getName().endsWith(".txt"));
Класс RandomAccessFile
Произвольный доступ к файлу — чтение и запись в любой позиции:
try (RandomAccessFile raf = new RandomAccessFile("data.bin", "rw")) {
// "r" — только чтение, "rw" — чтение и запись
// Позиция в файле
long position = raf.getFilePointer(); // Текущая позиция
raf.seek(100); // Перейти к позиции 100
// Чтение
int value = raf.readInt();
// Запись
raf.seek(0);
raf.writeInt(42);
// Размер файла
long length = raf.length();
raf.setLength(1024); // Установить размер
}
Часть 2: NIO — Каналы и буферы
NIO (New I/O) использует другую модель: каналы читают/пишут в буферы.
Buffer — контейнер данных
Буфер — это массив примитивов с метаданными для управления позицией:
┌─────────────────────────────────────────────────────────────┐
│ 0 1 2 3 4 5 6 7 8 9 ... │
│ [A] [B] [C] [D] [E] [ ] [ ] [ ] [ ] [ ] ... capacity=100 │
│ ▲ ▲ ▲ │
│ position=4 limit=6 capacity=100 │
└─────────────────────────────────────────────────────────────┘
Ключевые свойства:
- capacity — максимальный размер, неизменен
- limit — текущая граница для чтения/записи
- position — текущая позиция
- mark — отметка для возврата
Инвариант: 0 ≤ mark ≤ position ≤ limit ≤ capacity
// Создание буферов
ByteBuffer heapBuffer = ByteBuffer.allocate(1024); // В куче
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024); // В нативной памяти
ByteBuffer wrapped = ByteBuffer.wrap(new byte[1024]); // Обёртка массива
// Специализированные буферы
CharBuffer charBuf = CharBuffer.allocate(100);
IntBuffer intBuf = IntBuffer.allocate(100);
DoubleBuffer doubleBuf = DoubleBuffer.allocate(100);
Операции с буфером
ByteBuffer buffer = ByteBuffer.allocate(10);
// position=0, limit=10, capacity=10
// === ЗАПИСЬ В БУФЕР ===
buffer.put((byte) 'H');
buffer.put((byte) 'i');
// position=2, limit=10
buffer.put(new byte[]{'!', '!'});
// position=4, limit=10
// === ПЕРЕКЛЮЧЕНИЕ В РЕЖИМ ЧТЕНИЯ ===
buffer.flip();
// position=0, limit=4 (limit стал равен бывшему position)
// === ЧТЕНИЕ ИЗ БУФЕРА ===
while (buffer.hasRemaining()) {
byte b = buffer.get();
System.out.print((char) b); // Выведет: Hi!!
}
// position=4, limit=4
// === ПОДГОТОВКА К ПОВТОРНОЙ ЗАПИСИ ===
buffer.clear(); // position=0, limit=capacity — очистить полностью
// или
buffer.compact(); // Сдвинуть непрочитанное в начало, position после данных
Ключевые методы:
| Метод | Действие | Результат |
|---|---|---|
flip() | Запись → Чтение | limit=position, position=0 |
clear() | Сброс | position=0, limit=capacity |
compact() | Сдвиг непрочитанного | Копирует остаток в начало |
rewind() | Перечитать | position=0, limit не меняется |
mark() / reset() | Закладка | Запомнить/вернуться к позиции |
Прямые буферы (Direct Buffers)
// Обычный буфер — в куче Java
ByteBuffer heapBuf = ByteBuffer.allocate(1024);
// Прямой буфер — в нативной памяти
ByteBuffer directBuf = ByteBuffer.allocateDirect(1024);
| Аспект | Heap Buffer | Direct Buffer |
|---|---|---|
| Расположение | Java heap | Нативная память |
| Аллокация | Быстрая | Медленная |
| GC | Управляется GC | Только ссылка в GC |
| I/O операции | Требует копирования | Прямой доступ ОС |
| Когда использовать | Короткоживущие | Долгоживущие, большие |
Совет: Используйте прямые буферы для долгоживущих буферов с интенсивным I/O. Для временных операций heap-буферы эффективнее.
Channel — двунаправленный канал
Каналы соединяют буферы с источниками/приёмниками данных:
// FileChannel из потоков
FileInputStream fis = new FileInputStream("input.txt");
FileChannel readChannel = fis.getChannel();
FileOutputStream fos = new FileOutputStream("output.txt");
FileChannel writeChannel = fos.getChannel();
// Или напрямую (предпочтительно)
try (FileChannel channel = FileChannel.open(
Path.of("data.txt"),
StandardOpenOption.READ,
StandardOpenOption.WRITE,
StandardOpenOption.CREATE)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
// Чтение
int bytesRead = channel.read(buffer);
buffer.flip();
// Запись
channel.write(buffer);
}
Копирование файла через NIO
public static void copyFile(Path source, Path target) throws IOException {
try (FileChannel srcChannel = FileChannel.open(source, StandardOpenOption.READ);
FileChannel dstChannel = FileChannel.open(target,
StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
// Способ 1: через буфер
ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
while (srcChannel.read(buffer) != -1 || buffer.position() > 0) {
buffer.flip();
dstChannel.write(buffer);
buffer.compact();
}
// Способ 2: transferTo (эффективнее, использует DMA)
// srcChannel.transferTo(0, srcChannel.size(), dstChannel);
}
}
Memory-Mapped Files
Отображение файла в память — очень эффективно для больших файлов:
try (FileChannel channel = FileChannel.open(
Path.of("large-file.dat"), StandardOpenOption.READ)) {
// Отобразить весь файл в память
MappedByteBuffer mappedBuffer = channel.map(
FileChannel.MapMode.READ_ONLY,
0,
channel.size()
);
// Теперь файл доступен как массив байтов
while (mappedBuffer.hasRemaining()) {
byte b = mappedBuffer.get();
}
// Для записи
// MapMode.READ_WRITE — изменения пишутся в файл
// MapMode.PRIVATE — copy-on-write, изменения не сохраняются
}
Часть 3: NIO.2 — Files и Path (Java 7+)
NIO.2 — современный API для работы с файловой системой.
Path — замена File
// Создание Path
Path path1 = Path.of("data", "config.txt"); // Java 11+
Path path2 = Paths.get("data", "config.txt"); // Java 7+
Path path3 = Path.of("/home/user/documents");
// Информация о пути
String fileName = path1.getFileName().toString(); // "config.txt"
Path parent = path1.getParent(); // "data"
Path root = path3.getRoot(); // "/"
int nameCount = path1.getNameCount(); // 2
// Манипуляции с путями
Path resolved = path3.resolve("file.txt"); // /home/user/documents/file.txt
Path sibling = path1.resolveSibling("other.txt"); // data/other.txt
Path relative = path3.relativize(Path.of("/home/user/other")); // ../other
Path normalized = Path.of("data/../config/./app.txt").normalize(); // config/app.txt
Path absolute = path1.toAbsolutePath();
// Сравнение
boolean same = path1.equals(path2);
boolean startsWith = path3.startsWith("/home");
boolean endsWith = path1.endsWith("config.txt");
Files — утилиты для файловых операций
Path path = Path.of("example.txt");
// === ПРОВЕРКИ ===
boolean exists = Files.exists(path);
boolean isRegular = Files.isRegularFile(path);
boolean isDir = Files.isDirectory(path);
boolean isReadable = Files.isReadable(path);
boolean isWritable = Files.isWritable(path);
boolean isHidden = Files.isHidden(path);
// === ЧТЕНИЕ И ЗАПИСЬ (простые случаи) ===
// Чтение всего файла
String content = Files.readString(path, StandardCharsets.UTF_8); // Java 11+
byte[] bytes = Files.readAllBytes(path);
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
// Запись
Files.writeString(path, "Hello, World!", StandardCharsets.UTF_8); // Java 11+
Files.write(path, bytes);
Files.write(path, lines, StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
// === ПОТОКОВОЕ ЧТЕНИЕ ===
try (Stream<String> stream = Files.lines(path, StandardCharsets.UTF_8)) {
stream.filter(line -> !line.isBlank())
.forEach(System.out::println);
}
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
// ...
}
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
// ...
}
Операции с файлами и директориями
// === СОЗДАНИЕ ===
Files.createFile(Path.of("new-file.txt"));
Files.createDirectory(Path.of("new-dir"));
Files.createDirectories(Path.of("path/to/nested/dir")); // Все промежуточные
Path tempFile = Files.createTempFile("prefix", ".tmp");
Path tempDir = Files.createTempDirectory("prefix");
// === КОПИРОВАНИЕ ===
Files.copy(source, target);
Files.copy(source, target,
StandardCopyOption.REPLACE_EXISTING, // Перезаписать
StandardCopyOption.COPY_ATTRIBUTES); // Копировать атрибуты
// Копирование из/в поток
Files.copy(inputStream, targetPath);
Files.copy(sourcePath, outputStream);
// === ПЕРЕМЕЩЕНИЕ ===
Files.move(source, target);
Files.move(source, target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE); // Атомарно (если поддерживается)
// === УДАЛЕНИЕ ===
Files.delete(path); // Бросает исключение если не существует
Files.deleteIfExists(path); // Возвращает boolean
// === АТРИБУТЫ ===
long size = Files.size(path);
FileTime modified = Files.getLastModifiedTime(path);
Files.setLastModifiedTime(path, FileTime.from(Instant.now()));
// Детальные атрибуты
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
FileTime created = attrs.creationTime();
boolean isSymlink = attrs.isSymbolicLink();
Обход дерева директорий
Path startDir = Path.of("/home/user/projects");
// === Files.list() — только содержимое директории ===
try (Stream<Path> stream = Files.list(startDir)) {
stream.filter(Files::isRegularFile)
.forEach(System.out::println);
}
// === Files.walk() — рекурсивный обход ===
try (Stream<Path> stream = Files.walk(startDir)) {
List<Path> javaFiles = stream
.filter(p -> p.toString().endsWith(".java"))
.toList();
}
// С ограничением глубины
try (Stream<Path> stream = Files.walk(startDir, 2)) { // maxDepth=2
// ...
}
// === Files.find() — обход с фильтром по атрибутам ===
try (Stream<Path> stream = Files.find(startDir, Integer.MAX_VALUE,
(path, attrs) -> attrs.isRegularFile() &&
path.toString().endsWith(".log") &&
attrs.size() > 1024 * 1024)) { // > 1MB
stream.forEach(System.out::println);
}
// === Files.walkFileTree() — полный контроль ===
Files.walkFileTree(startDir, new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println("Entering: " + dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println("File: " + file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("Failed: " + file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
return FileVisitResult.CONTINUE;
}
});
FileVisitResult:
CONTINUE— продолжить обходSKIP_SUBTREE— пропустить поддиректориюSKIP_SIBLINGS— пропустить оставшиеся файлы в директорииTERMINATE— прекратить обход
Рекурсивное удаление директории
public static void deleteRecursively(Path dir) throws IOException {
Files.walkFileTree(dir, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
WatchService — мониторинг файловой системы
try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
Path dir = Path.of("/home/user/watched");
dir.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE);
while (true) {
WatchKey key = watchService.take(); // Блокирует до события
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == StandardWatchEventKinds.OVERFLOW) {
continue; // События были потеряны
}
@SuppressWarnings("unchecked")
WatchEvent<Path> pathEvent = (WatchEvent<Path>) event;
Path fileName = pathEvent.context();
System.out.printf("%s: %s%n", kind.name(), fileName);
}
boolean valid = key.reset(); // Сбросить для следующих событий
if (!valid) {
break; // Директория больше недоступна
}
}
}
Часть 4: Неблокирующий и асинхронный I/O
Selector и неблокирующие каналы (NIO)
Позволяют одному потоку обрабатывать множество соединений:
try (Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // Неблокирующий режим
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // Блокирует до готовности каналов
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
keys.remove();
if (key.isAcceptable()) {
// Новое входящее соединение
SocketChannel client = serverChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// Данные готовы для чтения
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = client.read(buffer);
if (bytesRead == -1) {
client.close();
} else {
buffer.flip();
// Обработка данных...
key.interestOps(SelectionKey.OP_WRITE);
}
} else if (key.isWritable()) {
// Канал готов к записи
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
client.write(buffer);
if (!buffer.hasRemaining()) {
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
Асинхронные каналы (NIO.2)
Операции возвращают Future или вызывают CompletionHandler:
// === С Future ===
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(
Path.of("data.txt"), StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
Future<Integer> future = channel.read(buffer, 0);
// Делаем что-то другое пока идёт чтение...
int bytesRead = future.get(); // Блокирует до завершения
buffer.flip();
}
// === С CompletionHandler ===
AsynchronousFileChannel channel = AsynchronousFileChannel.open(
Path.of("data.txt"), StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer bytesRead, ByteBuffer attachment) {
attachment.flip();
System.out.println("Прочитано: " + bytesRead + " байт");
// Обработка данных...
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.err.println("Ошибка: " + exc.getMessage());
}
});
Асинхронный сервер
AsynchronousServerSocketChannel server =
AsynchronousServerSocketChannel.open()
.bind(new InetSocketAddress(8080));
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel client, Void attachment) {
// Принять следующее соединение
server.accept(null, this);
// Обработать текущее
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer bytesRead, ByteBuffer buf) {
buf.flip();
// Обработка и ответ...
}
@Override
public void failed(Throwable exc, ByteBuffer buf) {
// Ошибка чтения
}
});
}
@Override
public void failed(Throwable exc, Void attachment) {
// Ошибка accept
}
});
Сравнение подходов
| Подход | Поток на соединение | Производительность | Сложность |
|---|---|---|---|
| Blocking IO | Да | Низкая при многих соединениях | Простая |
| NIO + Selector | Нет (мультиплексирование) | Высокая | Средняя |
| NIO.2 Async | Пул потоков | Высокая | Высокая |
Практический пример: файловый процессор
public class LogAnalyzer {
public static void main(String[] args) throws IOException {
Path logsDir = Path.of("logs");
// Найти все .log файлы, прочитать и проанализировать
Map<String, Long> errorCounts = new ConcurrentHashMap<>();
try (Stream<Path> logFiles = Files.walk(logsDir)
.filter(p -> p.toString().endsWith(".log"))) {
logFiles.parallel().forEach(logFile -> {
try (Stream<String> lines = Files.lines(logFile, StandardCharsets.UTF_8)) {
lines.filter(line -> line.contains("ERROR"))
.map(LogAnalyzer::extractErrorType)
.forEach(errorType ->
errorCounts.merge(errorType, 1L, Long::sum));
} catch (IOException e) {
System.err.println("Ошибка чтения: " + logFile);
}
});
}
// Вывод результатов
Path report = Path.of("error-report.txt");
List<String> reportLines = errorCounts.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.map(e -> String.format("%s: %d", e.getKey(), e.getValue()))
.toList();
Files.write(report, reportLines, StandardCharsets.UTF_8);
System.out.println("Отчёт сохранён: " + report.toAbsolutePath());
}
private static String extractErrorType(String line) {
// Извлечь тип ошибки из строки лога
int start = line.indexOf("ERROR") + 6;
int end = line.indexOf(":", start);
return end > start ? line.substring(start, end).trim() : "Unknown";
}
}
Резюме
Когда использовать что:
| Задача | Рекомендуемый API |
|---|---|
| Простое чтение/запись текста | Files.readString() / Files.writeString() |
| Построчное чтение | Files.lines() или BufferedReader |
| Бинарные данные | Files.readAllBytes() или FileChannel |
| Большие файлы | FileChannel + ByteBuffer или Memory-mapped |
| Много соединений | NIO Selector или NIO.2 Async |
| Обход директорий | Files.walk() или Files.walkFileTree() |
| Мониторинг изменений | WatchService |
Ключевые принципы:
- Всегда используйте try-with-resources для потоков и каналов
- Указывайте кодировку явно (
StandardCharsets.UTF_8) - Буферизуйте файловые потоки
- Для современного кода предпочитайте
Path/FilesнадFile - Выбирайте NIO для высоконагруженных сценариев
2.x. reflection
reflection
Материалы
Reflection (Рефлексия)
Reflection — механизм, позволяющий исследовать и модифицировать структуру и поведение программы во время выполнения. С помощью рефлексии можно анализировать классы, создавать объекты, вызывать методы и изменять поля — даже приватные.
Зачем нужна рефлексия
Рефлексия используется когда структура классов неизвестна на этапе компиляции:
- Фреймворки — Spring, Hibernate, JUnit используют рефлексию для инъекции зависимостей, маппинга объектов, запуска тестов
- Сериализация — Jackson, Gson анализируют поля объектов для преобразования в JSON
- IDE и инструменты — автодополнение, отладчики, анализаторы кода
- Плагины — загрузка и использование классов, неизвестных во время компиляции
- ORM — создание объектов и заполнение полей из базы данных
Получение объекта Class
Class<T> — входная точка в Reflection API. Три способа получить:
// 1. Через литерал .class (известен во время компиляции)
Class<String> stringClass = String.class;
Class<int[]> intArrayClass = int[].class;
// 2. Через объект (getClass())
String str = "Hello";
Class<?> cls = str.getClass();
// 3. По имени класса (динамическая загрузка)
Class<?> cls = Class.forName("java.util.ArrayList");
Class<?> nested = Class.forName("java.util.Map$Entry"); // Вложенный класс
// Для примитивов
Class<Integer> intWrapper = Integer.class;
Class<Integer> intPrimitive = Integer.TYPE; // int.class
Class<Void> voidClass = Void.TYPE; // void.class
Исследование класса
Базовая информация
Class<?> cls = ArrayList.class;
// Имя
String name = cls.getName(); // "java.util.ArrayList"
String simpleName = cls.getSimpleName(); // "ArrayList"
String canonical = cls.getCanonicalName(); // "java.util.ArrayList"
// Пакет и модуль
Package pkg = cls.getPackage(); // java.util
Module module = cls.getModule(); // java.base
// Тип класса
boolean isInterface = cls.isInterface();
boolean isEnum = cls.isEnum();
boolean isRecord = cls.isRecord(); // Java 16+
boolean isAnnotation = cls.isAnnotation();
boolean isArray = cls.isArray();
boolean isPrimitive = cls.isPrimitive();
boolean isAnonymous = cls.isAnonymousClass();
boolean isLocal = cls.isLocalClass();
boolean isMember = cls.isMemberClass();
boolean isSynthetic = cls.isSynthetic(); // Сгенерирован компилятором
// Модификаторы
int modifiers = cls.getModifiers();
boolean isPublic = Modifier.isPublic(modifiers);
boolean isAbstract = Modifier.isAbstract(modifiers);
boolean isFinal = Modifier.isFinal(modifiers);
Иерархия классов
Class<?> cls = ArrayList.class;
// Суперкласс
Class<?> superclass = cls.getSuperclass(); // AbstractList
// Интерфейсы (только непосредственно реализуемые)
Class<?>[] interfaces = cls.getInterfaces();
// [List, RandomAccess, Cloneable, Serializable]
// Все интерфейсы включая унаследованные
Set<Class<?>> allInterfaces = new HashSet<>();
for (Class<?> c = cls; c != null; c = c.getSuperclass()) {
allInterfaces.addAll(Arrays.asList(c.getInterfaces()));
}
// Проверка наследования
boolean isList = List.class.isAssignableFrom(cls); // true
boolean isInstance = cls.isInstance(new ArrayList<>()); // true
Работа с полями (Field)
Получение полей
Class<?> cls = Person.class;
// Только public поля (включая унаследованные)
Field[] publicFields = cls.getFields();
// Все поля класса (включая private, но без унаследованных)
Field[] allFields = cls.getDeclaredFields();
// Конкретное поле
Field nameField = cls.getDeclaredField("name");
Field idField = cls.getField("id"); // Только public
Информация о поле
Field field = Person.class.getDeclaredField("name");
String name = field.getName(); // "name"
Class<?> type = field.getType(); // String.class
int modifiers = field.getModifiers();
boolean isPrivate = Modifier.isPrivate(modifiers);
boolean isStatic = Modifier.isStatic(modifiers);
boolean isFinal = Modifier.isFinal(modifiers);
boolean isTransient = Modifier.isTransient(modifiers);
boolean isVolatile = Modifier.isVolatile(modifiers);
// Для generic-полей
Type genericType = field.getGenericType();
// Например: List<String> → ParameterizedType
Чтение и запись значений
class Person {
private String name = "John";
private static int count = 0;
}
Person person = new Person();
Field nameField = Person.class.getDeclaredField("name");
// Для private полей нужно открыть доступ
nameField.setAccessible(true);
// Чтение
String value = (String) nameField.get(person); // "John"
// Запись
nameField.set(person, "Alice");
// Статические поля — передаём null вместо объекта
Field countField = Person.class.getDeclaredField("count");
countField.setAccessible(true);
int count = (int) countField.get(null);
countField.set(null, 42);
Работа с final полями
class Config {
private final String value = "original";
}
Config config = new Config();
Field field = Config.class.getDeclaredField("value");
field.setAccessible(true);
// Работает, но опасно!
field.set(config, "modified");
System.out.println(field.get(config)); // "modified"
Предупреждение: Модификация
finalполей через рефлексию — плохая практика. JVM может оптимизировать доступ к final полям, и изменения могут не быть видны. Начиная с Java 12 некоторые поля защищены от рефлексии.
Работа с методами (Method)
Получение методов
Class<?> cls = String.class;
// Только public методы (включая унаследованные)
Method[] publicMethods = cls.getMethods();
// Все методы класса (включая private, без унаследованных)
Method[] declaredMethods = cls.getDeclaredMethods();
// Конкретный метод (имя + типы параметров)
Method substring = cls.getMethod("substring", int.class, int.class);
Method privateMethod = cls.getDeclaredMethod("secretMethod");
Информация о методе
Method method = String.class.getMethod("substring", int.class, int.class);
String name = method.getName(); // "substring"
Class<?> returnType = method.getReturnType(); // String.class
Class<?>[] paramTypes = method.getParameterTypes(); // [int.class, int.class]
Class<?>[] exceptions = method.getExceptionTypes();
int modifiers = method.getModifiers();
// Параметры с именами (если скомпилировано с -parameters)
Parameter[] params = method.getParameters();
for (Parameter p : params) {
System.out.println(p.getName() + ": " + p.getType());
}
// Информация о generic
Type genericReturnType = method.getGenericReturnType();
Type[] genericParamTypes = method.getGenericParameterTypes();
// Аннотации
Annotation[] annotations = method.getAnnotations();
boolean isDeprecated = method.isAnnotationPresent(Deprecated.class);
// Специальные свойства
boolean isVarArgs = method.isVarArgs();
boolean isBridge = method.isBridge(); // Мост для generic
boolean isSynthetic = method.isSynthetic(); // Сгенерирован компилятором
boolean isDefault = method.isDefault(); // default в интерфейсе
Вызов методов
class Calculator {
public int add(int a, int b) {
return a + b;
}
private String secret() {
return "secret";
}
public static double pi() {
return 3.14159;
}
}
Calculator calc = new Calculator();
// Вызов public метода
Method addMethod = Calculator.class.getMethod("add", int.class, int.class);
int result = (int) addMethod.invoke(calc, 10, 20); // 30
// Вызов private метода
Method secretMethod = Calculator.class.getDeclaredMethod("secret");
secretMethod.setAccessible(true);
String secret = (String) secretMethod.invoke(calc); // "secret"
// Вызов static метода — передаём null
Method piMethod = Calculator.class.getMethod("pi");
double pi = (double) piMethod.invoke(null); // 3.14159
Вызов методов с varargs
class Formatter {
public String format(String pattern, Object... args) {
return String.format(pattern, args);
}
}
Method formatMethod = Formatter.class.getMethod("format", String.class, Object[].class);
Formatter formatter = new Formatter();
// Важно: varargs передаётся как массив
String result = (String) formatMethod.invoke(formatter,
"Hello, %s! You have %d messages.",
new Object[]{"Alice", 5});
Работа с конструкторами (Constructor)
Получение конструкторов
Class<?> cls = ArrayList.class;
// Только public конструкторы
Constructor<?>[] publicConstructors = cls.getConstructors();
// Все конструкторы (включая private)
Constructor<?>[] allConstructors = cls.getDeclaredConstructors();
// Конкретный конструктор
Constructor<?> defaultCtor = cls.getConstructor();
Constructor<?> capacityCtor = cls.getConstructor(int.class);
Constructor<?> collectionCtor = cls.getConstructor(Collection.class);
Создание объектов
class Person {
private String name;
private int age;
public Person() {
this("Unknown", 0);
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private Person(String name) {
this(name, 0);
}
}
// Через Class.newInstance() — deprecated в Java 9+
// Person p = (Person) Person.class.newInstance();
// Через Constructor (рекомендуемый способ)
Constructor<Person> ctor = Person.class.getConstructor(String.class, int.class);
Person person = ctor.newInstance("Alice", 25);
// Private конструктор
Constructor<Person> privateCtor = Person.class.getDeclaredConstructor(String.class);
privateCtor.setAccessible(true);
Person p2 = privateCtor.newInstance("Bob");
Работа с массивами (Array)
import java.lang.reflect.Array;
// Создание массива
int[] intArray = (int[]) Array.newInstance(int.class, 10);
String[][] matrix = (String[][]) Array.newInstance(String.class, 3, 4);
// Доступ к элементам
Array.set(intArray, 0, 42);
int value = Array.getInt(intArray, 0); // 42
Array.set(matrix, 0, new String[]{"a", "b", "c", "d"});
// Информация о массиве
int length = Array.getLength(intArray); // 10
Class<?> componentType = intArray.getClass().getComponentType(); // int.class
// Проверка типа массива
boolean isArray = intArray.getClass().isArray(); // true
Работа с аннотациями
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@interface MyAnnotation {
String value();
int priority() default 0;
}
@MyAnnotation(value = "test", priority = 5)
class AnnotatedClass {
@MyAnnotation("field")
private String name;
@MyAnnotation("method")
public void doSomething() {}
}
// На классе
MyAnnotation classAnno = AnnotatedClass.class.getAnnotation(MyAnnotation.class);
String value = classAnno.value(); // "test"
int priority = classAnno.priority(); // 5
// На поле
Field field = AnnotatedClass.class.getDeclaredField("name");
MyAnnotation fieldAnno = field.getAnnotation(MyAnnotation.class);
// На методе
Method method = AnnotatedClass.class.getMethod("doSomething");
if (method.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation methodAnno = method.getAnnotation(MyAnnotation.class);
}
// Все аннотации
Annotation[] allAnnotations = AnnotatedClass.class.getAnnotations();
Annotation[] declaredAnnotations = AnnotatedClass.class.getDeclaredAnnotations();
Работа с Generic-типами
class Container<T> {
private List<String> strings;
private Map<String, List<Integer>> complex;
public <E> E transform(T input) { return null; }
}
// Generic поля
Field stringsField = Container.class.getDeclaredField("strings");
Type genericType = stringsField.getGenericType();
if (genericType instanceof ParameterizedType pt) {
Type rawType = pt.getRawType(); // List.class
Type[] typeArgs = pt.getActualTypeArguments(); // [String.class]
}
// Вложенные generic
Field complexField = Container.class.getDeclaredField("complex");
ParameterizedType mapType = (ParameterizedType) complexField.getGenericType();
// Map<String, List<Integer>>
Type[] mapArgs = mapType.getActualTypeArguments();
// [String.class, ParameterizedType(List<Integer>)]
ParameterizedType listType = (ParameterizedType) mapArgs[1];
Type[] listArgs = listType.getActualTypeArguments(); // [Integer.class]
// Type parameters класса
TypeVariable<?>[] typeParams = Container.class.getTypeParameters();
// [T]
// Generic метод
Method transform = Container.class.getMethod("transform", Object.class);
TypeVariable<?>[] methodTypeParams = transform.getTypeParameters();
// [E]
Dynamic Proxy
Proxy позволяет создавать объекты, реализующие интерфейсы с динамической обработкой вызовов:
interface UserService {
User findById(long id);
void save(User user);
}
// InvocationHandler обрабатывает все вызовы
class LoggingHandler implements InvocationHandler {
private final Object target;
public LoggingHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Calling: " + method.getName());
long start = System.currentTimeMillis();
try {
Object result = method.invoke(target, args);
System.out.println("Returned: " + result);
return result;
} finally {
long elapsed = System.currentTimeMillis() - start;
System.out.println("Took: " + elapsed + "ms");
}
}
}
// Создание proxy
UserService realService = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class<?>[] { UserService.class },
new LoggingHandler(realService)
);
// Все вызовы проходят через handler
proxy.findById(42); // Логируется автоматически
Несколько интерфейсов
interface Readable { String read(); }
interface Writable { void write(String data); }
Object proxy = Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class<?>[] { Readable.class, Writable.class },
(proxyObj, method, args) -> {
if (method.getName().equals("read")) {
return "data";
} else if (method.getName().equals("write")) {
System.out.println("Writing: " + args[0]);
return null;
}
throw new UnsupportedOperationException();
}
);
Readable readable = (Readable) proxy;
Writable writable = (Writable) proxy;
setAccessible и модульная система
До Java 9
Field privateField = SomeClass.class.getDeclaredField("secret");
privateField.setAccessible(true); // Работало всегда
Java 9+ (JPMS)
Модульная система ограничивает рефлексию:
// Может бросить InaccessibleObjectException
field.setAccessible(true);
Решения:
# 1. Флаг запуска JVM
java --add-opens java.base/java.lang=ALL-UNNAMED MyApp
# 2. В module-info.java (если контролируете модуль)
module mymodule {
opens com.mypackage to framework.module;
}
// 3. Проверка в коде
if (field.canAccess(object)) {
// Доступ разрешён
} else if (field.trySetAccessible()) {
// Успешно открыли доступ
} else {
// Не удалось получить доступ
}
Исключения Reflection API
try {
Class<?> cls = Class.forName("com.example.MyClass");
Object obj = cls.getConstructor().newInstance();
Method method = cls.getMethod("doSomething", String.class);
method.invoke(obj, "test");
} catch (ClassNotFoundException e) {
// Класс не найден
} catch (NoSuchMethodException e) {
// Метод или конструктор не найден
} catch (NoSuchFieldException e) {
// Поле не найдено
} catch (InstantiationException e) {
// Не удалось создать экземпляр (абстрактный класс, интерфейс)
} catch (IllegalAccessException e) {
// Нет доступа (private без setAccessible)
} catch (IllegalArgumentException e) {
// Неверные аргументы
} catch (InvocationTargetException e) {
// Исключение в вызванном методе
Throwable cause = e.getCause(); // Оригинальное исключение
} catch (InaccessibleObjectException e) {
// Java 9+: модульная система запрещает доступ
}
Практический пример: простой DI-контейнер
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Inject {}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Component {}
class SimpleDIContainer {
private final Map<Class<?>, Object> instances = new HashMap<>();
public void register(Class<?> cls) throws Exception {
if (!cls.isAnnotationPresent(Component.class)) {
throw new IllegalArgumentException("Class must be annotated with @Component");
}
Object instance = cls.getConstructor().newInstance();
instances.put(cls, instance);
}
public void injectDependencies() throws Exception {
for (Object instance : instances.values()) {
for (Field field : instance.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(Inject.class)) {
Object dependency = instances.get(field.getType());
if (dependency != null) {
field.setAccessible(true);
field.set(instance, dependency);
}
}
}
}
}
@SuppressWarnings("unchecked")
public <T> T get(Class<T> cls) {
return (T) instances.get(cls);
}
}
// Использование
@Component
class Repository {
public String getData() { return "data"; }
}
@Component
class Service {
@Inject
private Repository repository;
public void process() {
System.out.println(repository.getData());
}
}
SimpleDIContainer container = new SimpleDIContainer();
container.register(Repository.class);
container.register(Service.class);
container.injectDependencies();
Service service = container.get(Service.class);
service.process(); // "data"
Практический пример: сериализация в JSON
public class SimpleJsonSerializer {
public String toJson(Object obj) throws Exception {
StringBuilder json = new StringBuilder("{");
Field[] fields = obj.getClass().getDeclaredFields();
boolean first = true;
for (Field field : fields) {
if (Modifier.isStatic(field.getModifiers())) continue;
if (Modifier.isTransient(field.getModifiers())) continue;
field.setAccessible(true);
Object value = field.get(obj);
if (!first) json.append(",");
first = false;
json.append("\"").append(field.getName()).append("\":");
json.append(formatValue(value));
}
return json.append("}").toString();
}
private String formatValue(Object value) {
if (value == null) return "null";
if (value instanceof String) return "\"" + value + "\"";
if (value instanceof Number || value instanceof Boolean) return value.toString();
return "\"" + value.toString() + "\"";
}
}
// Использование
class User {
private String name = "Alice";
private int age = 25;
private transient String password = "secret";
}
String json = new SimpleJsonSerializer().toJson(new User());
// {"name":"Alice","age":25}
Производительность
Рефлексия значительно медленнее прямого доступа:
// Прямой вызов: ~1 наносекунда
person.getName();
// Через рефлексию: ~100-1000 наносекунд (первый вызов)
// ~10-50 наносекунд (последующие вызовы с кэшированным Method)
method.invoke(person);
Оптимизация
// 1. Кэшируйте Method/Field/Constructor объекты
class ReflectionCache {
private static final Map<String, Method> methodCache = new ConcurrentHashMap<>();
public static Method getMethod(Class<?> cls, String name, Class<?>... params)
throws NoSuchMethodException {
String key = cls.getName() + "." + name + Arrays.toString(params);
return methodCache.computeIfAbsent(key, k -> {
try {
Method m = cls.getDeclaredMethod(name, params);
m.setAccessible(true);
return m;
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
});
}
}
// 2. Используйте MethodHandle (Java 7+) для лучшей производительности
import java.lang.invoke.*;
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle handle = lookup.findVirtual(String.class, "length",
MethodType.methodType(int.class));
int length = (int) handle.invoke("hello"); // 5
// 3. Используйте VarHandle (Java 9+) для полей
VarHandle varHandle = MethodHandles.lookup()
.findVarHandle(Person.class, "name", String.class);
varHandle.set(person, "Bob");
Лучшие практики
✅ Когда использовать
// Фреймворки и библиотеки
// Тестирование (доступ к private для проверки состояния)
// Сериализация/десериализация
// Динамическая загрузка плагинов
❌ Когда избегать
// Обычный бизнес-код — используйте прямой доступ
// Обход инкапсуляции без веских причин
// Критичные к производительности участки
// Когда есть нормальный API
Безопасность
// 1. Валидируйте имена классов из внешних источников
String className = userInput;
if (!className.matches("^[a-zA-Z][a-zA-Z0-9.]*$")) {
throw new SecurityException("Invalid class name");
}
// 2. Используйте белый список классов
Set<String> allowedClasses = Set.of(
"com.myapp.plugins.Plugin1",
"com.myapp.plugins.Plugin2"
);
if (!allowedClasses.contains(className)) {
throw new SecurityException("Class not allowed");
}
// 3. Минимизируйте setAccessible
// Открывайте доступ только когда действительно нужно
Резюме
| Класс | Назначение |
|---|---|
Class<T> | Метаданные класса, точка входа |
Field | Информация и доступ к полям |
Method | Информация и вызов методов |
Constructor<T> | Создание экземпляров |
Modifier | Декодирование модификаторов |
Array | Работа с массивами |
Proxy | Динамические прокси |
Parameter | Информация о параметрах методов |
AnnotatedElement | Доступ к аннотациям |
Ключевые методы:
| Категория | public (+ унаследованные) | Все (включая private) |
|---|---|---|
| Поля | getFields() | getDeclaredFields() |
| Методы | getMethods() | getDeclaredMethods() |
| Конструкторы | getConstructors() | getDeclaredConstructors() |
| Классы | getClasses() | getDeclaredClasses() |
Помните:
- Рефлексия мощный инструмент, но используйте с осторожностью
- Кэшируйте объекты
Method,Field,Constructor - В Java 9+ учитывайте ограничения модульной системы
- Для критичной производительности рассмотрите
MethodHandle/VarHandle
2.x. annotations
annotations
Материалы
Аннотации — это форма метаданных, которые предоставляют информацию о программе, но не являются частью самой программы. Аннотации не влияют напрямую на выполнение кода, который они аннотируют.
Аннотации используются для:
- Информирования компилятора — например, для обнаружения ошибок или подавления предупреждений
- Обработки во время компиляции и развёртывания — инструменты могут генерировать код, XML-файлы и т.д.
- Обработки во время выполнения — некоторые аннотации доступны через рефлексию
Синтаксис использования
В простейшей форме аннотация выглядит так:
@Override
void myMethod() { ... }
Символ @ указывает компилятору, что далее следует аннотация. Аннотация может включать элементы — именованные или неименованные параметры со значениями:
@Author(
name = "Иван Петров",
date = "2025-01-15"
)
class MyClass { ... }
Если аннотация не имеет элементов, скобки можно опустить, как в примере с @Override.
Если у аннотации есть только один элемент с именем value, имя можно опустить:
@SuppressWarnings("unchecked")
void myMethod() { ... }
Можно применить несколько аннотаций к одному объявлению:
@Author(name = "Иван Петров")
@Reviewer(name = "Мария Сидорова")
class MyClass { ... }
Начиная с Java 8, аннотации можно применять не только к объявлениям, но и к использованиям типов:
// Создание объекта
new @Interned MyObject();
// Приведение типа
myString = (@NonNull String) str;
// Реализация интерфейса
class UnmodifiableList<T> implements @Readonly List<@Readonly T> { ... }
// Объявление исключений
void monitorTemperature() throws @Critical TemperatureException { ... }
Встроенные аннотации
Java предоставляет набор предопределённых аннотаций в пакетах java.lang и java.lang.annotation.
@Override
Аннотация @Override информирует компилятор, что метод предназначен для переопределения метода суперкласса:
class Animal {
void makeSound() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof!");
}
}
Если метод, помеченный @Override, на самом деле не переопределяет метод суперкласса (например, из-за опечатки в имени), компилятор выдаст ошибку. Это защищает от неочевидных багов.
@Deprecated
Аннотация @Deprecated указывает, что помеченный элемент устарел и не должен использоваться:
/**
* @deprecated Используйте {@link #newMethod()} вместо этого метода.
*/
@Deprecated
void oldMethod() { ... }
Компилятор выдаёт предупреждение при использовании устаревших элементов. Рекомендуется также использовать Javadoc-тег @deprecated для документирования альтернативы.
Начиная с Java 9, аннотация @Deprecated имеет два дополнительных элемента:
@Deprecated(since = "9", forRemoval = true)
void legacyMethod() { ... }
since— указывает версию, с которой элемент устарелforRemoval— еслиtrue, элемент планируется к удалению в будущих версиях
@SuppressWarnings
Аннотация @SuppressWarnings указывает компилятору подавить определённые предупреждения:
@SuppressWarnings("deprecation")
void useDeprecatedMethod() {
object.deprecatedMethod(); // Предупреждение подавлено
}
Можно подавить несколько категорий предупреждений:
@SuppressWarnings({"unchecked", "deprecation"})
void myMethod() { ... }
Основные категории предупреждений:
deprecation— использование устаревших APIunchecked— непроверенные операции с genericsrawtypes— использование raw-типов вместо genericsserial— отсутствиеserialVersionUIDв сериализуемом классе
Совет: Применяйте
@SuppressWarningsк минимально возможной области (метод, переменная), а не к целому классу. Не используйте@SuppressWarnings("all")— это скрывает важные предупреждения.
@SafeVarargs
Аннотация @SafeVarargs применяется к методам и конструкторам, подавляя предупреждения о потенциально небезопасных операциях с varargs-параметрами:
@SafeVarargs
static <T> List<T> asList(T... elements) {
return Arrays.asList(elements);
}
Аннотацию можно применять только к методам, которые нельзя переопределить:
staticметодыfinalметодыprivateметоды (начиная с Java 9)- Конструкторы
@FunctionalInterface
Аннотация @FunctionalInterface указывает, что интерфейс предназначен для использования в качестве функционального интерфейса — интерфейса с единственным абстрактным методом:
@FunctionalInterface
interface Calculator {
int calculate(int x, int y);
// default-методы не считаются
default void printResult(int result) {
System.out.println("Result: " + result);
}
}
Функциональные интерфейсы можно использовать с лямбда-выражениями:
Calculator addition = (x, y) -> x + y;
Calculator multiplication = (x, y) -> x * y;
System.out.println(addition.calculate(5, 3)); // 8
System.out.println(multiplication.calculate(5, 3)); // 15
Аннотация не обязательна для создания функционального интерфейса, но она защищает от случайного добавления второго абстрактного метода.
Объявление аннотаций
Для создания собственной аннотации используется ключевое слово @interface:
public @interface MyAnnotation {
String author();
String date();
int revision() default 1;
String[] reviewers() default {};
}
Объявление аннотации похоже на объявление интерфейса, но ключевому слову interface предшествует символ @. Технически аннотации — это особая форма интерфейса.
Элементы аннотаций
Методы, объявленные в теле аннотации, называются элементами. Они определяют параметры аннотации:
public @interface Author {
String name();
String date() default "N/A";
}
Ограничения для элементов:
- Не могут иметь параметров
- Не могут объявлять исключения (throws)
- Возвращаемый тип ограничен:
- Примитивные типы (
int,boolean, и т.д.) StringClassилиClass<T>- Enum-типы
- Другие аннотации
- Массивы перечисленных выше типов
- Примитивные типы (
public @interface ComplexAnnotation {
int count();
String name();
Class<?> targetClass();
ElementType[] targets();
Deprecated nested(); // Вложенная аннотация
String[] tags() default {}; // Массив с default-значением
}
Виды аннотаций
По количеству элементов аннотации делятся на три вида:
Маркерные аннотации — без элементов:
public @interface Preliminary { }
// Использование
@Preliminary
public class UnfinishedClass { }
Одноэлементные аннотации — с одним элементом. По соглашению, элемент называется value:
public @interface Copyright {
String value();
}
// Использование — имя "value" можно опустить
@Copyright("2025 My Company")
public class MyClass { }
Многоэлементные аннотации — с несколькими элементами:
@Author(name = "Иван", date = "2025-01-15")
public class MyClass { }
Мета-аннотации
Мета-аннотации — это аннотации, применяемые к другим аннотациям. Они определяют, как пользовательская аннотация будет себя вести.
@Retention
Определяет, как долго аннотация будет сохраняться:
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation { }
Возможные значения RetentionPolicy:
| Политика | Описание |
|---|---|
SOURCE | Аннотация сохраняется только в исходном коде, отбрасывается компилятором |
CLASS | Сохраняется в .class-файле, но недоступна во время выполнения (по умолчанию) |
RUNTIME | Сохраняется в .class-файле и доступна через рефлексию во время выполнения |
Выбирайте минимально необходимый уровень:
SOURCE— для аннотаций, обрабатываемых только компилятором или процессорами аннотацийCLASS— для аннотаций, читаемых из байткода инструментами, но не нужных в runtimeRUNTIME— для аннотаций, обрабатываемых через рефлексию во время выполнения
@Target
Определяет, к каким элементам программы можно применять аннотацию:
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
public @interface Transactional { }
Возможные значения ElementType:
| Тип | Применяется к |
|---|---|
TYPE | Классы, интерфейсы, enum, record |
FIELD | Поля (включая константы enum) |
METHOD | Методы |
PARAMETER | Параметры методов |
CONSTRUCTOR | Конструкторы |
LOCAL_VARIABLE | Локальные переменные |
ANNOTATION_TYPE | Другие аннотации |
PACKAGE | Пакеты (в package-info.java) |
TYPE_PARAMETER | Параметры типов (class MyClass<@Ann T>) |
TYPE_USE | Любое использование типа |
MODULE | Модули (в module-info.java) |
RECORD_COMPONENT | Компоненты record |
Если @Target не указан, аннотация может применяться к любому объявлению (кроме параметров типов).
@Documented
Указывает, что аннотация должна быть включена в Javadoc-документацию:
@Documented
@Target(ElementType.METHOD)
public @interface Beta { }
По умолчанию аннотации не отображаются в документации. Используйте @Documented для важных публичных аннотаций.
@Inherited
Указывает, что аннотация наследуется подклассами:
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Secured { }
@Secured
public class BaseService { }
// UserService также считается @Secured
public class UserService extends BaseService { }
Без @Inherited подклассы не наследуют аннотацию суперкласса. Важно: @Inherited работает только для классов, не для интерфейсов и методов.
@Repeatable
Позволяет применять одну и ту же аннотацию несколько раз к одному элементу (начиная с Java 8):
// Контейнерная аннотация
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Schedules {
Schedule[] value();
}
// Повторяемая аннотация
@Repeatable(Schedules.class)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Schedule {
String dayOfWeek();
String time();
}
// Использование
@Schedule(dayOfWeek = "Monday", time = "09:00")
@Schedule(dayOfWeek = "Friday", time = "15:00")
void weeklyMeeting() { }
Для повторяемой аннотации необходимо:
- Объявить контейнерную аннотацию с элементом
value(), возвращающим массив повторяемой аннотации - Пометить повторяемую аннотацию
@Repeatable, указав класс контейнера - Контейнер должен иметь такую же или более широкую политику
@Retention
Type Annotations
Начиная с Java 8, аннотации можно применять везде, где используется тип. Для этого аннотация должна иметь @Target(ElementType.TYPE_USE):
@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.RUNTIME)
public @interface NonNull { }
Примеры использования type annotations:
// Создание объекта
@NonNull String name = new @Interned String("hello");
// Приведение типа
String str = (@NonNull String) value;
// Реализация интерфейса
class MyList implements @Readonly List<@NonNull String> { }
// Параметры типов
Map<@NonNull String, @NonNull Integer> map = new HashMap<>();
// Границы типов
class Folder<F extends @Existing File> { }
// Исключения
void process() throws @Critical IOException { }
// Массивы
@NonNull String @Nullable [] array; // Nullable массив NonNull строк
Type annotations предназначены для использования с инструментами статического анализа кода (например, Checker Framework), которые могут проверять дополнительные ограничения типов во время компиляции.
Аннотация с TYPE_PARAMETER применяется к объявлениям параметров типов:
class MyClass<@TypeParam T> { }
Чтение аннотаций через рефлексию
Аннотации с RetentionPolicy.RUNTIME доступны во время выполнения через Reflection API:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
String description() default "";
}
public class MyTestClass {
@Test(description = "Проверка сложения")
public void testAddition() { }
@Test
public void testSubtraction() { }
public void regularMethod() { }
}
Получение аннотаций:
import java.lang.reflect.Method;
public class AnnotationProcessor {
public static void main(String[] args) {
Class<?> clazz = MyTestClass.class;
for (Method method : clazz.getDeclaredMethods()) {
// Проверка наличия аннотации
if (method.isAnnotationPresent(Test.class)) {
// Получение аннотации
Test test = method.getAnnotation(Test.class);
System.out.println("Найден тестовый метод: " + method.getName());
System.out.println("Описание: " + test.description());
}
}
}
}
Основные методы для работы с аннотациями:
| Метод | Описание |
|---|---|
isAnnotationPresent(Class) | Проверяет наличие аннотации |
getAnnotation(Class) | Возвращает аннотацию или null |
getAnnotations() | Возвращает все аннотации |
getDeclaredAnnotations() | Возвращает только явно объявленные аннотации (без унаследованных) |
getAnnotationsByType(Class) | Возвращает все экземпляры повторяемой аннотации |
Для type annotations используйте методы getAnnotatedReturnType(), getAnnotatedParameterTypes() и другие в классе Method:
Method method = MyClass.class.getMethod("myMethod");
AnnotatedType returnType = method.getAnnotatedReturnType();
Annotation[] annotations = returnType.getAnnotations();
Пример: создание простого тестового фреймворка
Рассмотрим практический пример создания минимального тестового фреймворка:
import java.lang.annotation.*;
// Аннотация для пометки тестовых методов
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SimpleTest {
String name() default "";
boolean enabled() default true;
}
// Аннотация для ожидаемых исключений
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExpectedException {
Class<? extends Throwable> value();
}
public class Calculator {
public int divide(int a, int b) {
return a / b;
}
}
public class CalculatorTest {
private Calculator calc = new Calculator();
@SimpleTest(name = "Деление положительных чисел")
public void testDivision() {
assert calc.divide(10, 2) == 5 : "10 / 2 должно быть 5";
}
@SimpleTest(name = "Деление на ноль")
@ExpectedException(ArithmeticException.class)
public void testDivisionByZero() {
calc.divide(10, 0);
}
@SimpleTest(enabled = false)
public void skippedTest() {
// Этот тест не будет запущен
}
}
import java.lang.reflect.Method;
public class TestRunner {
public static void main(String[] args) throws Exception {
Class<?> testClass = CalculatorTest.class;
Object testInstance = testClass.getDeclaredConstructor().newInstance();
int passed = 0, failed = 0, skipped = 0;
for (Method method : testClass.getDeclaredMethods()) {
if (!method.isAnnotationPresent(SimpleTest.class)) {
continue;
}
SimpleTest test = method.getAnnotation(SimpleTest.class);
String testName = test.name().isEmpty() ? method.getName() : test.name();
if (!test.enabled()) {
System.out.println("⏭ Пропущен: " + testName);
skipped++;
continue;
}
try {
method.invoke(testInstance);
// Если ожидалось исключение, но его не было — тест провален
if (method.isAnnotationPresent(ExpectedException.class)) {
System.out.println("✗ Провален: " + testName + " (ожидалось исключение)");
failed++;
} else {
System.out.println("✓ Пройден: " + testName);
passed++;
}
} catch (Exception e) {
Throwable cause = e.getCause();
ExpectedException expected = method.getAnnotation(ExpectedException.class);
if (expected != null && expected.value().isInstance(cause)) {
System.out.println("✓ Пройден: " + testName);
passed++;
} else {
System.out.println("✗ Провален: " + testName + " (" + cause + ")");
failed++;
}
}
}
System.out.println("\nРезультаты: " + passed + " пройдено, "
+ failed + " провалено, " + skipped + " пропущено");
}
}
Вывод программы:
✓ Пройден: Деление положительных чисел
✓ Пройден: Деление на ноль
⏭ Пропущен: skippedTest
Результаты: 2 пройдено, 0 провалено, 1 пропущено
Резюме
Аннотации — мощный инструмент Java для добавления метаданных в код:
- Используйте встроенные аннотации (
@Override,@Deprecated,@SuppressWarnings,@FunctionalInterface) для улучшения качества кода - Создавайте собственные аннотации с помощью
@interfaceдля специфичных задач - Контролируйте поведение аннотаций через мета-аннотации (
@Retention,@Target,@Documented,@Inherited,@Repeatable) - Используйте type annotations для статического анализа и расширенной проверки типов
- Читайте аннотации через Reflection API во время выполнения
Аннотации широко используются в современных Java-фреймворках: Spring, Hibernate, JPA, JUnit и многих других полагаются на аннотации для конфигурации и определения поведения.
3. JVM, JRE, JDK
Под капотом Java платформы
Понимание внутренней работы JVM критически важно для написания производительных приложений.
Содержание раздела
- 3.1. Архитектура JVM
- 3.2. Управление памятью
- 3.3. GC алгоритмы
- 3.4. JIT-компиляция
- 3.5. JVM параметры и тюнинг
- 3.6. Профилирование и мониторинг
3.1. Архитектура JVM
Компоненты Java Virtual Machine
Содержание
Architecture Component
Материалы
ClassLoader — механизм JVM, отвечающий за поиск и загрузку классов в память. Понимание работы загрузчиков классов критично для диагностики ClassNotFoundException, NoClassDefFoundError, создания плагинных систем и изоляции кода.
Жизненный цикл класса в JVM
Прежде чем класс можно использовать, JVM выполняет три этапа:
┌─────────────┐ ┌─────────────┐ ┌────────────────┐
│ Loading │───>│ Linking │───>│ Initialization │
│ (Загрузка) │ │(Связывание) │ │(Инициализация) │
└─────────────┘ └─────────────┘ └────────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│Verification│ │Preparation │ │ Resolution │
│ (Проверка) │ │(Подготовка)│ │(Разрешение)│
└────────────┘ └────────────┘ └────────────┘
Loading (Загрузка)
Загрузчик находит байт-код класса (обычно .class файл) и создаёт объект Class<?> в памяти JVM:
- Находит бинарное представление класса по имени
- Создаёт объект
Classв method area - Записывает связь между классом и загрузчиком
Linking (Связывание)
Verification (Проверка) — JVM проверяет корректность байт-кода:
- Формат файла соответствует спецификации
- Нет нарушений типобезопасности
- Стек операндов не переполняется
Preparation (Подготовка) — выделение памяти для статических полей и установка значений по умолчанию:
class Example {
static int count; // Устанавливается в 0 (не в значение из кода!)
static Object ref; // Устанавливается в null
static final int MAX = 100; // Устанавливается в 100 (ConstantValue)
}
Resolution (Разрешение) — символические ссылки заменяются прямыми. Может происходить лениво (при первом использовании) или сразу.
Initialization (Инициализация)
Выполняется статический инициализатор класса <clinit>:
class Example {
static int count = 42; // Теперь присваивается 42
static List<String> list;
static {
list = new ArrayList<>();
list.add("init");
}
}
Важно: Инициализация происходит только при первом активном использовании класса: создание экземпляра, доступ к статическому полю/методу, рефлексия, инициализация подкласса.
Иерархия встроенных загрузчиков
JVM имеет три встроенных загрузчика классов, образующих иерархию:
┌─────────────────────┐
│ Bootstrap (null) │ ← java.base, java.lang.*
│ Загружает ядро JDK│
└──────────┬──────────┘
│ parent
┌──────────▼──────────┐
│ Platform ("platform")│ ← Java SE API, реализации
│ Платформенные классы │
└──────────┬──────────┘
│ parent
┌──────────▼──────────┐
│ Application ("app") │ ← classpath, module path
│ Классы приложения │
└─────────────────────┘
Bootstrap Class Loader
Загружает базовые классы JDK из модуля java.base: java.lang.*, java.util.*, java.io.* и т.д.
// Bootstrap loader представлен как null
String.class.getClassLoader(); // null
Object.class.getClassLoader(); // null
// Проверка через имя
ClassLoader cl = ArrayList.class.getClassLoader();
System.out.println(cl); // null — загружен bootstrap
Реализован на нативном коде (C/C++), а не на Java.
Platform Class Loader
Загружает платформенные классы Java SE, которые не входят в java.base:
ClassLoader platform = ClassLoader.getPlatformClassLoader();
System.out.println(platform.getName()); // "platform"
// Например, классы из java.sql
java.sql.Connection.class.getClassLoader(); // platform loader
Application (System) Class Loader
Загружает классы приложения из classpath и module path:
ClassLoader app = ClassLoader.getSystemClassLoader();
System.out.println(app.getName()); // "app"
// Ваши классы загружаются этим загрузчиком
MyClass.class.getClassLoader(); // app loader
Classpath определяется:
- Системным свойством
java.class.path - Опцией
-cp/-classpath - Переменной окружения
CLASSPATH - Атрибутом
Class-Pathв MANIFEST.MF
Модель делегирования (Parent Delegation)
Ключевой принцип работы загрузчиков — делегирование родителю:
// Псевдокод алгоритма loadClass
protected Class<?> loadClass(String name, boolean resolve) {
synchronized (getClassLoadingLock(name)) {
// 1. Проверить, не загружен ли класс ранее
Class<?> c = findLoadedClass(name);
if (c == null) {
// 2. Делегировать родителю
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// Родитель не нашёл — это нормально
}
// 3. Если родитель не загрузил — искать самостоятельно
if (c == null) {
c = findClass(name);
}
}
// 4. Опционально: линковка
if (resolve) {
resolveClass(c);
}
return c;
}
}
Порядок поиска класса com.example.MyClass
Application ClassLoader
│
├─► "Уже загружен?" ─► НЕТ
│
├─► Делегирует Platform ClassLoader
│ │
│ ├─► "Уже загружен?" ─► НЕТ
│ │
│ ├─► Делегирует Bootstrap ClassLoader
│ │ │
│ │ └─► "Не найден в java.base"
│ │
│ └─► "Не найден в платформе"
│
└─► Ищет в classpath ─► НАЙДЕН!
Зачем нужна делегация
- Безопасность — нельзя подменить системные классы:
// Даже если создать свой java/lang/String.class в classpath,
// он не загрузится — bootstrap загрузит настоящий String первым
- Уникальность — один класс загружается один раз:
// Класс String всегда один и тот же во всём приложении
String.class == String.class // true, независимо от контекста
- Видимость — дочерние загрузчики видят классы родительских:
// Application loader видит java.util.List (загружен bootstrap)
// Bootstrap loader НЕ видит com.example.MyClass (загружен application)
API класса ClassLoader
Получение загрузчиков
// Загрузчик конкретного класса
ClassLoader cl = MyClass.class.getClassLoader();
// Системный (application) загрузчик
ClassLoader system = ClassLoader.getSystemClassLoader();
// Платформенный загрузчик
ClassLoader platform = ClassLoader.getPlatformClassLoader();
// Родительский загрузчик
ClassLoader parent = cl.getParent();
// Контекстный загрузчик текущего потока
ClassLoader context = Thread.currentThread().getContextClassLoader();
Загрузка классов
// Через Class.forName (инициализирует класс)
Class<?> cls = Class.forName("com.example.MyClass");
// Через Class.forName с контролем инициализации
Class<?> cls = Class.forName("com.example.MyClass", false, classLoader);
// Через ClassLoader.loadClass (НЕ инициализирует)
Class<?> cls = classLoader.loadClass("com.example.MyClass");
Разница:
Class.forName()по умолчанию инициализирует класс (выполняет static-блоки),loadClass()— нет.
Загрузка ресурсов
ClassLoader cl = MyClass.class.getClassLoader();
// Один ресурс
URL url = cl.getResource("config/app.properties");
InputStream is = cl.getResourceAsStream("config/app.properties");
// Все ресурсы с данным именем (из разных JAR)
Enumeration<URL> urls = cl.getResources("META-INF/services/MyService");
// Stream API (Java 9+)
Stream<URL> stream = cl.resources("META-INF/MANIFEST.MF");
Путь к ресурсу:
- Без
/— относительно корня classpath:"config/app.properties" - Ищется через делегацию, как и классы
// Через Class (относительно пакета класса или абсолютно)
MyClass.class.getResource("local.txt"); // В том же пакете
MyClass.class.getResource("/config/global.txt"); // От корня classpath
Создание собственного ClassLoader
Базовая структура
public class CustomClassLoader extends ClassLoader {
private final Path classesDir;
public CustomClassLoader(Path classesDir, ClassLoader parent) {
super(parent);
this.classesDir = classesDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// Преобразуем имя класса в путь к файлу
String fileName = name.replace('.', '/') + ".class";
Path classFile = classesDir.resolve(fileName);
try {
byte[] bytes = Files.readAllBytes(classFile);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException("Class not found: " + name, e);
}
}
}
Ключевые методы для переопределения
| Метод | Когда переопределять |
|---|---|
findClass(String) | Основной метод — откуда брать байт-код |
loadClass(String, boolean) | Изменить стратегию делегирования |
findResource(String) | Откуда брать ресурсы |
findLibrary(String) | Где искать native-библиотеки |
Network ClassLoader
Загрузчик, скачивающий классы по сети:
public class NetworkClassLoader extends ClassLoader {
private final URL baseUrl;
public NetworkClassLoader(URL baseUrl, ClassLoader parent) {
super(parent);
this.baseUrl = baseUrl;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = name.replace('.', '/') + ".class";
try {
URL classUrl = new URL(baseUrl, path);
try (InputStream is = classUrl.openStream()) {
byte[] bytes = is.readAllBytes();
return defineClass(name, bytes, 0, bytes.length);
}
} catch (IOException e) {
throw new ClassNotFoundException("Failed to load: " + name, e);
}
}
}
// Использование
URL serverUrl = new URL("https://plugins.example.com/classes/");
ClassLoader loader = new NetworkClassLoader(serverUrl, getClass().getClassLoader());
Class<?> pluginClass = loader.loadClass("com.example.Plugin");
Object plugin = pluginClass.getConstructor().newInstance();
Encrypting ClassLoader
Загрузчик зашифрованных классов:
public class DecryptingClassLoader extends ClassLoader {
private final Path encryptedDir;
private final SecretKey key;
public DecryptingClassLoader(Path dir, SecretKey key, ClassLoader parent) {
super(parent);
this.encryptedDir = dir;
this.key = key;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = name.replace('.', '/') + ".class.enc";
Path encryptedFile = encryptedDir.resolve(fileName);
try {
byte[] encrypted = Files.readAllBytes(encryptedFile);
byte[] decrypted = decrypt(encrypted);
return defineClass(name, decrypted, 0, decrypted.length);
} catch (Exception e) {
throw new ClassNotFoundException("Failed to load: " + name, e);
}
}
private byte[] decrypt(byte[] data) throws Exception {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
return cipher.doFinal(data);
}
}
Child-First (Parent-Last) ClassLoader
Иногда нужно загружать классы до делегации родителю — например, для изоляции версий библиотек:
public class ChildFirstClassLoader extends URLClassLoader {
private final Set<String> childFirstPackages;
public ChildFirstClassLoader(URL[] urls, ClassLoader parent,
Set<String> childFirstPackages) {
super(urls, parent);
this.childFirstPackages = childFirstPackages;
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// Проверяем кэш
Class<?> c = findLoadedClass(name);
if (c == null) {
// Проверяем, нужно ли загружать самим (child-first)
if (shouldLoadFirst(name)) {
try {
c = findClass(name);
} catch (ClassNotFoundException e) {
// Если не нашли — делегируем родителю
}
}
// Стандартная делегация
if (c == null) {
c = super.loadClass(name, false);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
private boolean shouldLoadFirst(String name) {
// Системные классы всегда через родителя
if (name.startsWith("java.") || name.startsWith("javax.") ||
name.startsWith("sun.") || name.startsWith("jdk.")) {
return false;
}
// Проверяем список пакетов для child-first
for (String pkg : childFirstPackages) {
if (name.startsWith(pkg)) {
return true;
}
}
return false;
}
}
Parallel Capable ClassLoaders
В многопоточной среде важно избегать deadlock при загрузке классов. Parallel capable загрузчики используют отдельные блокировки для каждого класса:
public class ParallelClassLoader extends ClassLoader {
// Регистрация в static-блоке — ДО создания экземпляров!
static {
registerAsParallelCapable();
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// getClassLoadingLock возвращает объект блокировки для конкретного класса
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
c = findClass(name);
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// Реализация загрузки
return super.findClass(name);
}
}
// Проверка
System.out.println(loader.isRegisteredAsParallelCapable()); // true
Важно:
registerAsParallelCapable()должен вызываться в static-блоке до создания любых экземпляров, и все родительские классы (кроме Object) тоже должны быть parallel capable.
URLClassLoader
Стандартный загрузчик для работы с JAR-файлами и директориями:
// Создание с указанием URL
URL[] urls = {
new File("/path/to/classes/").toURI().toURL(),
new File("/path/to/library.jar").toURI().toURL(),
new URL("https://repo.example.com/plugin.jar")
};
URLClassLoader loader = new URLClassLoader(urls, getClass().getClassLoader());
// Загрузка класса
Class<?> cls = loader.loadClass("com.example.Plugin");
// Важно: закрывать после использования (реализует Closeable)
loader.close();
// Или с try-with-resources
try (URLClassLoader loader = new URLClassLoader(urls)) {
Class<?> cls = loader.loadClass("com.example.Plugin");
// ...
}
Динамическое добавление JAR (до Java 9)
// До Java 9 можно было добавлять URL динамически через рефлексию
// В Java 9+ URLClassLoader — read-only после создания
// Альтернатива: создавать новый загрузчик с расширенным списком URL
Context ClassLoader
Контекстный загрузчик потока решает проблему “обратной видимости”, когда код из родительского загрузчика должен загрузить класс из дочернего:
// Проблема: JDBC-драйвер загружен в Application ClassLoader,
// а DriverManager — в Bootstrap ClassLoader
// Решение: контекстный загрузчик
ClassLoader contextCL = Thread.currentThread().getContextClassLoader();
// SPI (ServiceLoader) использует контекстный загрузчик
ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);
// Эквивалентно:
ServiceLoader.load(Driver.class, Thread.currentThread().getContextClassLoader());
Установка контекстного загрузчика
ClassLoader myLoader = new CustomClassLoader(...);
Thread.currentThread().setContextClassLoader(myLoader);
try {
// Код, который должен использовать myLoader
ServiceLoader<MyPlugin> plugins = ServiceLoader.load(MyPlugin.class);
} finally {
// Восстановить оригинальный
Thread.currentThread().setContextClassLoader(originalLoader);
}
Проблемы и решения
ClassNotFoundException vs NoClassDefFoundError
// ClassNotFoundException — класс не найден при явном запросе
try {
Class.forName("com.nonexistent.Class");
} catch (ClassNotFoundException e) {
// Checked exception — нужно обрабатывать
}
// NoClassDefFoundError — класс был доступен при компиляции,
// но не найден при выполнении
try {
new SomeClass(); // SomeClass зависит от MissingDependency
} catch (NoClassDefFoundError e) {
// Error — обычно фатальная ошибка
// Часто обёртка над ClassNotFoundException
Throwable cause = e.getCause(); // Может быть ClassNotFoundException
}
Class Identity
Класс уникален парой: бинарное имя + defining class loader:
URLClassLoader loader1 = new URLClassLoader(urls);
URLClassLoader loader2 = new URLClassLoader(urls);
Class<?> cls1 = loader1.loadClass("com.example.MyClass");
Class<?> cls2 = loader2.loadClass("com.example.MyClass");
cls1 == cls2; // false! Разные классы
cls1.equals(cls2); // false!
Object obj1 = cls1.getConstructor().newInstance();
cls2.isInstance(obj1); // false! obj1 — не экземпляр cls2
cls2.cast(obj1); // ClassCastException!
Это позволяет изолировать версии:
┌─────────────────┐ ┌─────────────────┐
│ Plugin A │ │ Plugin B │
│ guava-31.0.jar │ │ guava-28.0.jar │
│ (LoaderA) │ │ (LoaderB) │
└─────────────────┘ └─────────────────┘
Memory Leaks
Классы не выгружаются, пока жив их загрузчик:
// Утечка памяти в сервлет-контейнерах
public class BadServlet extends HttpServlet {
// Статическая ссылка на Thread держит ClassLoader
private static Thread worker = new Thread(() -> {
while (true) { /* работа */ }
});
}
Решение:
// Останавливать потоки при undeploy
@Override
public void destroy() {
worker.interrupt();
worker = null;
}
// Избегать статических ссылок на классы из webapp classloader
// Использовать слабые ссылки (WeakReference) где возможно
Практический пример: Plugin System
public interface Plugin {
String getName();
void execute();
}
public class PluginManager {
private final Map<String, PluginInfo> plugins = new ConcurrentHashMap<>();
private final Path pluginsDir;
public PluginManager(Path pluginsDir) {
this.pluginsDir = pluginsDir;
}
public void loadPlugin(String jarName) throws Exception {
Path jarPath = pluginsDir.resolve(jarName);
URL[] urls = { jarPath.toUri().toURL() };
// Каждый плагин — свой загрузчик (изоляция)
URLClassLoader loader = new URLClassLoader(urls,
Plugin.class.getClassLoader());
// Читаем имя класса из манифеста
try (JarFile jar = new JarFile(jarPath.toFile())) {
String mainClass = jar.getManifest()
.getMainAttributes()
.getValue("Plugin-Class");
Class<?> cls = loader.loadClass(mainClass);
Plugin plugin = (Plugin) cls.getConstructor().newInstance();
plugins.put(plugin.getName(), new PluginInfo(plugin, loader));
System.out.println("Loaded: " + plugin.getName());
}
}
public void unloadPlugin(String name) throws Exception {
PluginInfo info = plugins.remove(name);
if (info != null) {
info.loader().close(); // Закрываем загрузчик
System.out.println("Unloaded: " + name);
}
}
public void executePlugin(String name) {
PluginInfo info = plugins.get(name);
if (info != null) {
info.plugin().execute();
}
}
private record PluginInfo(Plugin plugin, URLClassLoader loader) {}
}
// Использование
PluginManager manager = new PluginManager(Path.of("plugins"));
manager.loadPlugin("my-plugin.jar");
manager.executePlugin("MyPlugin");
manager.unloadPlugin("MyPlugin");
Модульная система и ClassLoader (Java 9+)
В модульной системе загрузчики работают с модулями:
// Unnamed module — классы из classpath
Module unnamed = MyClass.class.getModule();
unnamed.isNamed(); // false
// Named module
Module base = String.class.getModule();
base.getName(); // "java.base"
// Каждый загрузчик имеет свой unnamed module
ClassLoader cl = getClass().getClassLoader();
Module unnamedModule = cl.getUnnamedModule();
Слои (Layers) позволяют организовывать модули:
ModuleLayer bootLayer = ModuleLayer.boot();
// Содержит модули из java.base и т.д.
// Можно создавать пользовательские слои с custom class loaders
Резюме
| Загрузчик | Имя | Что загружает |
|---|---|---|
| Bootstrap | null | java.base, java.lang., java.util. |
| Platform | "platform" | Java SE Platform API |
| Application | "app" | classpath, module path |
Ключевые принципы:
- Делегирование — сначала родитель, потом сам
- Уникальность — класс = имя + defining loader
- Видимость — дочерние видят классы родительских, но не наоборот
Когда создавать свой ClassLoader:
- Загрузка из нестандартных источников (сеть, БД, шифрованные файлы)
- Изоляция плагинов/модулей с конфликтующими зависимостями
- Hot-reload кода без перезапуска JVM
- Байт-код манипуляции (инструментирование, AOP)
Методы для переопределения:
| Цель | Метод |
|---|---|
| Откуда брать байт-код | findClass(String) |
| Изменить делегирование | loadClass(String, boolean) |
| Откуда брать ресурсы | findResource(String) |
| Где искать native-библиотеки | findLibrary(String) |
Architecture Component
Материалы
Заметки
Architecture Component
Материалы
Заметки
3.2. Управление памятью
Модель памяти JVM
Содержание
Memory Management
Материалы
Заметки
Memory Management
Материалы
Заметки
Memory Management
Материалы
Заметки
3.3. GC алгоритмы
Алгоритмы сборки мусора
Содержание
GC Algorithm
Материалы
Заметки
GC Algorithm
Материалы
Заметки
GC Algorithm
Материалы
Заметки
GC Algorithm
Материалы
Заметки
JVM Topic
Материалы
Заметки
JVM Topic
Материалы
Заметки
JVM Topic
Материалы
Заметки
4. Java Concurrency
Многопоточность и параллелизм
Многопоточное программирование - одна из самых сложных и важных тем в Java.
Содержание раздела
- 4.1. Потоки и процессы
- 4.2. Синхронизация
- 4.3. java.util.concurrent
- 4.4. ExecutorService
- 4.5. Fork/Join Framework
- 4.6. Проблемы многопоточности
- 4.7. Virtual Threads (Project Loom)
4.1. Потоки и процессы
Содержание
Threads Topic
Материалы
Заметки
Threads Topic
Материалы
Заметки
4.2. Синхронизация
Содержание
Synchronization Topic
Материалы
Заметки
Synchronization Topic
Материалы
Заметки
Synchronization Topic
Материалы
Заметки
4.3. java.util.concurrent
Содержание
Concurrent Topic
Материалы
Заметки
Concurrent Topic
Материалы
Заметки
Concurrent Topic
Материалы
Заметки
4.4. ExecutorService
Содержание
Executor Topic
Материалы
Заметки
Executor Topic
Материалы
Заметки
Concurrency Topic
Материалы
Заметки
4.6. Проблемы многопоточности
Содержание
Concurrency Problem
Материалы
Заметки
Concurrency Problem
Материалы
Заметки
Concurrency Problem
Материалы
Заметки
Concurrency Problem
Материалы
Заметки
Concurrency Topic
Материалы
Заметки
5. Algorithms
Фундамент эффективного кода
Знание алгоритмов и структур данных - основа хорошего программиста.
Содержание раздела
- 5.1. Сложность алгоритмов (Big O)
- 5.2. Структуры данных
- 5.3. Алгоритмы сортировки
- 5.4. Алгоритмы поиска
- 5.5. Динамическое программирование
- 5.6. Жадные алгоритмы
- 5.7. Алгоритмы на графах
Algorithm Topic
Материалы
Заметки
5.2. Структуры данных
Содержание
Data Structure
Материалы
Заметки
Data Structure
Материалы
Заметки
Data Structure
Материалы
Заметки
Data Structure
Материалы
Заметки
Algorithm Topic
Материалы
Заметки
Algorithm Topic
Материалы
Заметки
Algorithm Topic
Материалы
Заметки
Algorithm Topic
Материалы
Заметки
Algorithm Topic
Материалы
Заметки
6. Patterns
Проверенные решения типичных задач
Паттерны проектирования помогают создавать гибкий и поддерживаемый код.
Содержание раздела
- 6.1. Порождающие паттерны
- 6.2. Структурные паттерны
- 6.3. Поведенческие паттерны
- 6.4. Архитектурные паттерны
- 6.5. SOLID принципы
- 6.6. DRY, KISS, YAGNI
6.1. Порождающие паттерны
Содержание
Creational Pattern
Материалы
Заметки
Creational Pattern
Материалы
Заметки
Creational Pattern
Материалы
Заметки
Creational Pattern
Материалы
Заметки
Creational Pattern
Материалы
Заметки
6.2. Структурные паттерны
Содержание
Structural Pattern
Материалы
Заметки
Structural Pattern
Материалы
Заметки
Structural Pattern
Материалы
Заметки
Structural Pattern
Материалы
Заметки
Structural Pattern
Материалы
Заметки
Structural Pattern
Материалы
Заметки
6.3. Поведенческие паттерны
Содержание
- 6.3.1. Chain of Responsibility
- 6.3.2. Command
- 6.3.3. Observer
- 6.3.4. Strategy
- 6.3.5. Template Method
- 6.3.6. State
Behavioral Pattern
Материалы
Заметки
Behavioral Pattern
Материалы
Заметки
Behavioral Pattern
Материалы
Заметки
Behavioral Pattern
Материалы
Заметки
Behavioral Pattern
Материалы
Заметки
Behavioral Pattern
Материалы
Заметки
6.4. Архитектурные паттерны
Содержание
Architectural Pattern
Материалы
Заметки
Architectural Pattern
Материалы
Заметки
Architectural Pattern
Материалы
Заметки
Design Principles
Материалы
Заметки
Design Principles
Материалы
Заметки
7. Spring
Основа современной Java разработки
Spring Framework - фундамент экосистемы Spring.
Содержание раздела
- 7.1. Введение в Spring
- 7.2. Inversion of Control (IoC)
- 7.3. Dependency Injection
- 7.4. Spring Beans
- 7.5. ApplicationContext
- 7.6. Spring AOP
- 7.7. Spring Events
- 7.8. Конфигурация
Spring Topic
Материалы
Заметки
Spring Topic
Материалы
Заметки
7.3. Dependency Injection
Содержание
DI Type
Материалы
Заметки
DI Type
Материалы
Заметки
DI Type
Материалы
Заметки
7.4. Spring Beans
Содержание
Bean Topic
Материалы
Заметки
Bean Topic
Материалы
Заметки
Spring Topic
Материалы
Заметки
Spring Topic
Материалы
Заметки
Spring Topic
Материалы
Заметки
7.8. Конфигурация Spring
Содержание
Configuration Type
Материалы
Заметки
Configuration Type
Материалы
Заметки
Configuration Type
Материалы
Заметки
8. Spring Boot
Быстрый старт с минимумом конфигурации
Spring Boot упрощает создание production-ready приложений.
Содержание раздела
- 8.1. Философия Spring Boot
- 8.2. Стартеры (Starters)
- 8.3. Auto-configuration
- 8.4. Структура проекта
- 8.5. application.properties / application.yml
- 8.6. Профили
- 8.7. Spring Boot Actuator
- 8.8. Embedded Servers
- 8.9. Spring Boot DevTools
- 8.10. Создание REST API
- 8.11. Spring Data JPA
- 8.12. Тестирование
Spring Boot Topic
Материалы
Заметки
Spring Boot Topic
Материалы
Заметки
Spring Boot Topic
Материалы
Заметки
Spring Boot Topic
Материалы
Заметки
Spring Boot Topic
Материалы
Заметки
Spring Boot Topic
Материалы
Заметки
Spring Boot Topic
Материалы
Заметки
Spring Boot Topic
Материалы
Заметки
Spring Boot Topic
Материалы
Заметки
8.10. Создание REST API
Содержание
- 8.10.1. Controllers и RestControllers
- 8.10.2. Request/Response handling
- 8.10.3. Validation
- 8.10.4. Exception Handling
REST API Topic
Материалы
Заметки
REST API Topic
Материалы
Заметки
REST API Topic
Материалы
Заметки
REST API Topic
Материалы
Заметки
8.11. Spring Data JPA
Содержание
Spring Data JPA Topic
Материалы
Заметки
Spring Data JPA Topic
Материалы
Заметки
Spring Data JPA Topic
Материалы
Заметки
8.12. Тестирование
Содержание
Testing Topic
Материалы
Заметки
Testing Topic
Материалы
Заметки
Testing Topic
Материалы
Заметки
9. Spring Security
Защита приложений
Spring Security - мощный фреймворк для аутентификации и авторизации.
Содержание раздела
- 9.1. Основные концепции
- 9.2. Authentication
- 9.3. Authorization
- 9.4. Security Filter Chain
- 9.5. CSRF Protection
- 9.6. CORS Configuration
- 9.7. Password Encoding
- 9.8. Security Best Practices
Security Topic
Материалы
Заметки
9.2. Authentication
Содержание
- 9.2.1. Form-based Authentication
- 9.2.2. Basic Authentication
- 9.2.3. JWT Authentication
- 9.2.4. OAuth 2.0 / OpenID Connect
Authentication Type
Материалы
Заметки
Authentication Type
Материалы
Заметки
Authentication Type
Материалы
Заметки
Authentication Type
Материалы
Заметки
9.3. Authorization
Содержание
Authorization Topic
Материалы
Заметки
Authorization Topic
Материалы
Заметки
Authorization Topic
Материалы
Заметки
Security Topic
Материалы
Заметки
Security Topic
Материалы
Заметки
Security Topic
Материалы
Заметки
Security Topic
Материалы
Заметки
Security Topic
Материалы
Заметки
10. Spring Cloud
Микросервисная архитектура
Spring Cloud предоставляет инструменты для построения распределенных систем.
Содержание раздела
- 10.1. Введение в микросервисы
- 10.2. Spring Cloud Config
- 10.3. Service Discovery
- 10.4. API Gateway
- 10.5. Load Balancing
- 10.6. Circuit Breaker
- 10.7. Distributed Tracing
- 10.8. Spring Cloud Stream
- 10.9. Паттерны микросервисов
Spring Cloud Topic
Материалы
Заметки
Spring Cloud Topic
Материалы
Заметки
10.3. Service Discovery
Содержание
Service Discovery
Материалы
Заметки
Service Discovery
Материалы
Заметки
10.4. API Gateway
Содержание
Spring Cloud Gateway
Материалы
Заметки
Spring Cloud Topic
Материалы
Заметки
Spring Cloud Topic
Материалы
Заметки
10.7. Distributed Tracing
Содержание
Tracing Tool
Материалы
Заметки
Tracing Tool
Материалы
Заметки
Spring Cloud Topic
Материалы
Заметки
10.9. Паттерны микросервисов
Содержание
Microservices Pattern
Материалы
Заметки
Microservices Pattern
Материалы
Заметки
Microservices Pattern
Материалы
Заметки
11. Project Reactor & Spring WebFlux
Реактивное программирование
Реактивный подход к построению высоконагруженных приложений.
Содержание раздела
- 11.1. Реактивный манифест
- 11.2. Project Reactor
- 11.3. Spring WebFlux
- 11.4. R2DBC
- 11.5. Reactive Security
- 11.6. Testing Reactive Code
- 11.7. Когда использовать WebFlux
WebFlux Topic
Материалы
Заметки
11.2. Project Reactor
Содержание
Project Reactor Topic
Материалы
Заметки
Project Reactor Topic
Материалы
Заметки
Project Reactor Topic
Материалы
Заметки
Project Reactor Topic
Материалы
Заметки
11.3. Spring WebFlux
Содержание
WebFlux Topic
Материалы
Заметки
WebFlux Topic
Материалы
Заметки
WebFlux Topic
Материалы
Заметки
WebFlux Topic
Материалы
Заметки
WebFlux Topic
Материалы
Заметки
WebFlux Topic
Материалы
Заметки
WebFlux Topic
Материалы
Заметки
12. DBMS
Хранение и управление данными
Работа с базами данных - ключевой навык backend-разработчика.
Содержание раздела
- 12.1. Реляционные БД
- 12.2. SQL основы
- 12.3. Индексы
- 12.4. Транзакции
- 12.5. Нормализация
- 12.6. ORM и JPA
- 12.7. NoSQL базы данных
- 12.8. Миграции
12.1. Реляционные БД
Содержание
Database
Материалы
Заметки
Database
Материалы
Заметки
12.2. SQL основы
Содержание
SQL Topic
Материалы
Заметки
SQL Topic
Материалы
Заметки
SQL Topic
Материалы
Заметки
SQL Topic
Материалы
Заметки
12.3. Индексы
Содержание
Index Type
Материалы
Заметки
Index Type
Материалы
Заметки
Index Type
Материалы
Заметки
12.4. Транзакции
Содержание
Transaction Topic
Материалы
Заметки
Transaction Topic
Материалы
Заметки
12.5. Нормализация
Нормальные формы БД
Материалы
Заметки
12.6. ORM и JPA
Содержание
ORM Topic
Материалы
Заметки
ORM Topic
Материалы
Заметки
ORM Topic
Материалы
Заметки
ORM Topic
Материалы
Заметки
12.7. NoSQL базы данных
Содержание
NoSQL Database
Материалы
Заметки
NoSQL Database
Материалы
Заметки
NoSQL Database
Материалы
Заметки
12.8. Миграции
Содержание
Migration Tool
Материалы
Заметки
Migration Tool
Материалы
Заметки
13. Message Brokers
Асинхронное взаимодействие
Брокеры сообщений обеспечивают надежную асинхронную коммуникацию между сервисами.
Содержание раздела
- 13.1. Messaging Patterns
- 13.2. Apache Kafka
- 13.3. RabbitMQ
- 13.4. Гарантии доставки
- 13.5. Dead Letter Queues
- 13.6. Идемпотентность
Messaging Topic
Материалы
Заметки
13.2. Apache Kafka
Содержание
- 13.2.1. Архитектура
- 13.2.2. Topics и Partitions
- 13.2.3. Producers и Consumers
- 13.2.4. Consumer Groups
- 13.2.5. Spring Kafka
Kafka Topic
Материалы
Заметки
Kafka Topic
Материалы
Заметки
Kafka Topic
Материалы
Заметки
Kafka Topic
Материалы
Заметки
Kafka Topic
Материалы
Заметки
13.3. RabbitMQ
Содержание
RabbitMQ Topic
Материалы
Заметки
RabbitMQ Topic
Материалы
Заметки
RabbitMQ Topic
Материалы
Заметки
13.4. Гарантии доставки
Содержание
Delivery Guarantee
Материалы
Заметки
Delivery Guarantee
Материалы
Заметки
Delivery Guarantee
Материалы
Заметки
Messaging Topic
Материалы
Заметки
Messaging Topic
Материалы
Заметки
14. Maven и Gradle
Сборка и управление зависимостями
Инструменты сборки - неотъемлемая часть разработки на Java.
Содержание раздела
14.1. Maven
Содержание
- 14.1.1. POM файл
- 14.1.2. Жизненный цикл сборки
- 14.1.3. Зависимости и scope
- 14.1.4. Плагины
- 14.1.5. Multi-module проекты
Maven Topic
Материалы
Заметки
Maven Topic
Материалы
Заметки
Maven Topic
Материалы
Заметки
Maven Topic
Материалы
Заметки
Maven Topic
Материалы
Заметки
14.2. Gradle
Содержание
Gradle Topic
Материалы
Заметки
Gradle Topic
Материалы
Заметки
Gradle Topic
Материалы
Заметки
Gradle Topic
Материалы
Заметки
14.3. Maven vs Gradle
Сравнение систем сборки
Материалы
Заметки
15. Docker & SSH
Контейнеризация и деплой
Современный backend-разработчик должен уметь работать с контейнерами.
Содержание раздела
- 15.1. Основы Docker
- 15.2. Docker для Java
- 15.3. Docker Compose
- 15.4. Docker Networking
- 15.5. Volumes
- 15.6. SSH
- 15.7. CI/CD основы
15.1. Основы Docker
Содержание
Docker Topic
Материалы
Заметки
Docker Topic
Материалы
Заметки
Docker Topic
Материалы
Заметки
15.2. Docker для Java
Содержание
Docker Java Topic
Материалы
Заметки
Docker Java Topic
Материалы
Заметки
Docker Java Topic
Материалы
Заметки
Docker Topic
Материалы
Заметки
Docker Topic
Материалы
Заметки
Docker Topic
Материалы
Заметки
15.6. SSH
Содержание
SSH Topic
Материалы
Заметки
SSH Topic
Материалы
Заметки
15.7. CI/CD основы
Содержание
CI/CD Tool
Материалы
Заметки
CI/CD Tool
Материалы
Заметки
CI/CD Tool
Материалы
Заметки
16. Kotlin Basic
Современная альтернатива для JVM
Kotlin - современный язык программирования, полностью совместимый с Java.
Содержание раздела
- 16.1. Введение в Kotlin
- 16.2. Синтаксис Kotlin
- 16.3. Kotlin Collections
- 16.4. Coroutines
- 16.5. Kotlin + Spring
- 16.6. Java Interoperability
Kotlin Topic
Материалы
Заметки
16.2. Синтаксис Kotlin
Содержание
Kotlin Syntax Topic
Материалы
Заметки
Kotlin Syntax Topic
Материалы
Заметки
Kotlin Syntax Topic
Материалы
Заметки
Kotlin Syntax Topic
Материалы
Заметки
Kotlin Topic
Материалы
Заметки
Kotlin Topic
Материалы
Заметки
Kotlin Topic
Материалы
Заметки
Kotlin Topic
Материалы
Заметки
17. Agile
Методологии разработки
Понимание Agile методологий важно для эффективной работы в команде.
Содержание раздела
- 17.1. Agile манифест
- 17.2. Scrum
- 17.3. Kanban
- 17.4. User Stories
- 17.5. Story Points и оценка
- 17.6. Definition of Done