Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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: синтаксис, типы данных, управляющие конструкции и основы объектно-ориентированного программирования.

Содержание раздела

1.1. Введение в Java

Материалы

ТипСсылка
Документссылка
Видеоссылка

Добро пожаловать в мир Java! Если вы слышали о Java но не знаете с чего начать, или просто хотите понять что это такое - вы в правильном месте.

Что такое Java?

Java - это одновременно язык программирования и платформа. Да, сразу две вещи в одной! Давайте разберёмся что это значит.

Java как язык программирования

Java - это высокоуровневый язык, созданный быть:

  • Простым - легче чем C++, но мощным
  • Объектно-ориентированным - всё вращается вокруг объектов
  • Безопасным - множество проверок защищают от ошибок
  • Надёжным - сборщик мусора управляет памятью за вас
  • Переносимым - “напиши один раз, запускай везде”
  • Многопоточным - встроенная поддержка параллелизма
  • Быстрым - современные JVM очень оптимизированы

Java как платформа

Обычная платформа - это комбинация операционной системы и оборудования. Java платформа другая - это программная платформа поверх обычных платформ.

Java платформа состоит из двух компонентов:

  1. Java Virtual Machine (JVM) - виртуальная машина, которая запускает ваш код
  2. 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 - это “компьютер внутри компьютера”. Это программа, которая:

  1. Загружает ваш байт-код
  2. Проверяет его на безопасность
  3. Выполняет его
  4. Оптимизирует горячие участки кода прямо во время работы

Современные 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 Calculator
  • MyProgram.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 использует разные стили для разных элементов:

ЭлементСтильПримеры
КлассыPascalCaseMyClass, ArrayList, StringBuilder
ИнтерфейсыPascalCaseRunnable, Comparable, List
МетодыcamelCasegetName(), calculateTotal(), isEmpty()
ПеременныеcamelCasefirstName, totalCount, isActive
КонстантыUPPER_SNAKE_CASEMAX_VALUE, DEFAULT_SIZE, PI
Пакетыlowercasejava.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-код.

Задания для практики

  1. Создайте программу Calculator.java в пакете com.example.math с методами для базовых операций

  2. Напишите класс с правильными Javadoc-комментариями для всех публичных методов

  3. Отформатируйте следующий код по стандартам 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 меньше или равно");}}}
    
  4. Определите, какие из следующих идентификаторов допустимы:

    • myVariable
    • 2ndValue
    • _count
    • class
    • my-value
    • MAX_SIZE

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

byte, short, int, long, float, double, boolean, char

Материалы

ТипСсылка
ДокументPrimitive Data Types
ВидеоJava Data Types

Java — строго типизированный язык. Каждая переменная должна иметь объявленный тип. В этом разделе мы изучим 8 примитивных типов данных — фундамент Java.

Обзор примитивных типов

Java имеет ровно 8 примитивных типов:

ТипРазмерДиапазон значенийЗначение по умолчанию
byte1 байт-128 … 1270
short2 байта-32 768 … 32 7670
int4 байта-2 147 483 648 … 2 147 483 6470
long8 байт-9 223 372 036 854 775 808 … 9 223 372 036 854 775 8070L
float4 байта~±3.4×10^38 (6-7 значащих цифр)0.0f
double8 байт~±1.7×10^308 (15-17 значащих цифр)0.0
char2 байта0 … 65 535 (Unicode)‘\u0000’
boolean~1 битtrue / falsefalse

Целочисленные типы

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Возврат каретки
\\Обратный слэш
\'Одинарная кавычка
\"Двойная кавычка
\uXXXXUnicode символ (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
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

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 при сравнении объектов через ==

Задания для практики

  1. Объявите переменные всех 8 примитивных типов и выведите их значения

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

  3. Сравните результат 0.1 + 0.2 с 0.3 и объясните, почему они не равны

  4. Напишите метод, который определяет, является ли символ цифрой, без использования Character.isDigit()

  5. Продемонстрируйте разницу между Integer.valueOf(100) == Integer.valueOf(100) и Integer.valueOf(1000) == Integer.valueOf(1000)

Операторы и выражения

Арифметические, логические, битовые операторы

Ресурсы

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

Выражение (expression) - это конструкция, которая вычисляется и возвращает результат. Результатом может быть:

  • Переменная (lvalue)
  • Значение (value)
  • Ничего (void - для методов без возвращаемого значения)

При вычислении выражение может завершиться:

  • Нормально (normal completion) - если все шаги выполнены без исключений
  • Резко (abrupt completion) - если возникло исключение

Типы выражений

Выражения классифицируются по синтаксическим формам:

  • Имена выражений
  • Первичные выражения (литералы, this, создание объектов)
  • Унарные операторы
  • Бинарные операторы
  • Тернарный оператор ? :
  • Лямбда-выражения
  • Switch-выражения

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

Java гарантирует строгий порядок вычисления выражений:

Основные правила

  1. Левый операнд вычисляется первым - в бинарных операторах левый операнд всегда вычисляется до правого
  2. Операнды вычисляются до операции - все операнды вычисляются полностью перед выполнением операции
  3. Соблюдение скобок и приоритета - порядок определяется скобками и приоритетом операторов
  4. Аргументы слева направо - аргументы методов вычисляются слева направо
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; // ОШИБКА компиляции! (требуется явное приведение)

Приоритет операторов

От высшего к низшему (сверху вниз):

  1. Постфиксные: expr++, expr--
  2. Унарные: ++expr, --expr, +, -, ~, !
  3. Приведение типов: (type)
  4. Мультипликативные: *, /, %
  5. Аддитивные: +, -
  6. Сдвиг: <<, >>, >>>
  7. Сравнение: <, >, <=, >=, instanceof
  8. Равенство: ==, !=
  9. Побитовое И: &
  10. Побитовое XOR: ^
  11. Побитовое ИЛИ: |
  12. Логическое И: &&
  13. Логическое ИЛИ: ||
  14. Тернарный: ? :
  15. Присваивание: =, +=, -=, *=, /=, %=, &=, ^=, |=, <<=, >>=, >>>=
  16. Лямбда: ->
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 - операция с null
  • ArithmeticException - деление на ноль (целые числа)
  • 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

  1. Используйте скобки для ясности - не полагайтесь только на приоритет операторов
  2. Избегайте сложных выражений - разбивайте на несколько строк для читаемости
  3. Осторожно с автоупаковкой - может вызвать NullPointerException
  4. Используйте equals() для объектов - не == (если не проверяете идентичность)
  5. Проверяйте null перед && - используйте короткое замыкание
  6. Избегайте деления целых на ноль - проверяйте делитель
  7. Осторожно с NaN - проверяйте через Double.isNaN()
  8. Используйте составные операторы - они короче и автоматически приводят тип
// Хорошо
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 состоит из трёх частей:

  1. Инициализация (int i = 0) - выполняется один раз в начале
  2. Условие (i < 5) - проверяется перед каждой итерацией
  3. Обновление (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

Теперь вы можете эффективно управлять потоком выполнения ваших программ!

Практика

Попробуйте написать программы для:

  1. Проверки является ли год високосным
  2. Вычисления факториала числа
  3. Поиска всех простых чисел до N
  4. Конвертации температуры между Цельсием и Фаренгейтом

Удачи в программировании!

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. Нахождения второго по величине элемента в массиве
  2. Проверки является ли массив палиндромом
  3. Слияния двух отсортированных массивов в один отсортированный
  4. Подсчёта частоты каждого элемента в массиве

Удачи!

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 выбирает нужный метод на основе:

  1. Количества параметров
  2. Типов параметров
  3. Порядка параметров

Примечание: Только тип возвращаемого значения не может различать перегруженные методы!

// ЭТО НЕ РАБОТАЕТ!
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

  1. Varargs должен быть последним параметром:
// Правильно
public static void print(String prefix, int... numbers) {
    // ...
}

// НЕПРАВИЛЬНО - не компилируется!
public static void print(int... numbers, String prefix) {
    // ...
}
  1. Только один 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

Важные правила рекурсии

  1. Базовый случай - условие остановки
  2. Рекурсивный случай - вызов самого себя с изменёнными параметрами
  3. Движение к базовому случаю - параметры должны приближать к остановке
// Плохая рекурсия - бесконечная!
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:

  • Понятные имена (глагол + существительное)
  • Один метод — одна задача
  • Короткие методы (помещаются на экран)
  • Валидация параметров
  • Избегайте побочных эффектов

Задания для практики

  1. Калькулятор:

    • Методы add, subtract, multiply, divide
    • Перегрузка для int и double
    • Валидация деления на ноль
  2. Палиндром:

    • Метод isPalindrome(String)
    • Рекурсивная и итеративная версии
    • Сравните производительность
  3. Поиск в массиве:

    • Метод indexOf(int[] array, int value)
    • Метод contains(int[] array, int value)
    • Varargs версия sum(int… numbers)
  4. Класс 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();
  1. Dog - тип переменной
  2. myDog - имя переменной (ссылка на объект)
  3. new - оператор создания объекта
  4. 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 - ссылка на текущий объект
  • Инкапсуляция - скрытие деталей реализации
  • Модификаторы доступа - контроль видимости
  • Геттеры/сеттеры - контролируемый доступ к полям

Почему это важно:

  • Организация кода в логические блоки
  • Защита данных от некорректного использования
  • Переиспользование через создание множества объектов
  • Модульность и простота поддержки

Задания для практики

  1. Класс Book:

    • Поля: title, author, pages, price
    • Конструкторы: полный и упрощённый
    • Геттеры/сеттеры с валидацией
    • Метод displayInfo()
  2. Класс BankAccount:

    • Поля: accountNumber, balance, owner
    • Методы: deposit(), withdraw(), transfer()
    • Валидация всех операций
    • История транзакций
  3. Класс Circle:

    • Поле: radius
    • Методы: getArea(), getCircumference(), getDiameter()
    • Сравнение двух окружностей
    • Константа PI
  4. Класс Student:

    • Поля: name, id, grades (массив)
    • Методы: addGrade(), getAverage(), isPassing()
    • Валидация оценок (от 2 до 5)

Удачи в освоении ООП! Это фундамент профессионального программирования.


Источники:

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 - это ссылка на родительский класс. Используется для:

  1. Вызова конструктора родителя
  2. Вызова методов родителя
  3. Доступа к полям родителя (если скрыты)

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 Наследование - выбор правильного инструмента

Почему это важно:

  • Переиспользование кода
  • Гибкость через полиморфизм
  • Организация классов в иерархии
  • Расширяемость программ

Задания для практики

  1. Иерархия транспорта:

    • Базовый класс Vehicle
    • Подклассы: Car, Motorcycle, Truck
    • Переопределение методов
    • Полиморфный массив
  2. Фигуры:

    • Базовый класс Shape
    • Подклассы: Circle, Rectangle, Triangle
    • Метод getArea() в каждом
    • Сравнение фигур по площади
  3. Сотрудники:

    • Базовый Employee
    • Подклассы: Manager, Developer, Designer
    • Разный расчёт зарплаты
    • equals() и hashCode()

Удачи!


Источники:

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 classinterface
Наследование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)
  • Предпочитайте интерфейсы для большей гибкости

Задания для практики

  1. Медиаплеер:

    • Интерфейс Playable
    • Абстрактный класс MediaFile
    • Классы: AudioFile, VideoFile, StreamFile
    • Методы: play(), pause(), stop()
  2. Система уведомлений:

    • Интерфейс Notifiable
    • Классы: EmailNotifier, SMSNotifier, PushNotifier
    • Default методы для форматирования
    • Static методы-фабрики
  3. Коллекция фигур:

    • Абстрактный Shape с площадью
    • Интерфейсы: Drawable, Resizable, Rotatable
    • Разные фигуры реализуют разные интерфейсы
    • Полиморфная обработка
  4. Игровые персонажи:

    • Абстрактный Character
    • Интерфейсы: Attackable, Healable, Movable
    • Разные персонажи с разными способностями

Удачи в программировании! Теперь вы знаете все основы ООП.


Источники:

2. Java Core

Глубокое погружение в ядро языка

В этом разделе рассматриваются ключевые компоненты Java: Collections Framework, Generics, Stream API и другие важные API.

Содержание раздела

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 (отсортировано)
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 ExceptionReturns Special Value
Insertadd(e)offer(e) → boolean
Removeremove()poll() → null if empty
Examineelement()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)
InsertaddFirst(e) / offerFirst(e)addLast(e) / offerLast(e)
RemoveremoveFirst() / pollFirst()removeLast() / pollLast()
ExaminegetFirst() / 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 Характеристики

ОперацияArrayListLinkedListHashSetTreeSet
addO(1)*O(1)O(1)O(log n)
get(i)O(1)O(n)N/AN/A
containsO(n)O(n)O(1)O(log n)
removeO(n)O(n)**O(1)O(log n)
iterateO(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<>());

Итоги

  1. List - упорядоченная коллекция с индексами и дубликатами
  2. Set - коллекция уникальных элементов (HashSet, TreeSet, LinkedHashSet)
  3. Queue - очередь FIFO, Deque - двусторонняя очередь
  4. ArrayList - по умолчанию для List (O(1) доступ по индексу)
  5. HashSet - по умолчанию для Set (O(1) операции)
  6. ArrayDeque - по умолчанию для Queue/Stack (быстрее LinkedList)
  7. Правильно переопределяй equals/hashCode для объектов в Set
  8. Используй concurrent коллекции для многопоточности
  9. Указывай initial capacity если знаешь размер заранее
  10. Избегай устаревших классов (Vector, Stack, Hashtable)

Задания для практики

  1. ArrayList vs LinkedList: Создай список из 100000 элементов. Сравни время вставки в начало и в конец для ArrayList и LinkedList.

  2. HashSet для уникальности: Дан массив строк с дубликатами. Получи список уникальных строк, сохраняя порядок первого появления.

  3. PriorityQueue: Реализуй систему обработки задач с приоритетом. Task(name, priority). Задачи должны обрабатываться в порядке убывания приоритета.

  4. Deque как стек: Реализуй проверку сбалансированности скобок в строке “(())” используя ArrayDeque.

  5. 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 ✅ Порядок не важен ✅ Нужна максимальная скорость ✅ Однопоточное использование

Производительность

ОперацияСредний случайХудший случай
getO(1)O(n) → O(log n) (с Java 8)
putO(1)O(n) → O(log n)
removeO(1)O(n) → O(log n)
containsKeyO(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())
);

Поиск ближайших элементов

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

ОперацияСложность
getO(log n)
putO(log n)
removeO(log n)
containsKeyO(log n)
firstKeyO(log n)
lastKeyO(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 = всегда параллельно)
// Поиск первого совпадения
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 реализаций

ХарактеристикаHashMapLinkedHashMapTreeMapConcurrentHashMap
ПорядокНетВставки или доступаСортировкаНет
get/putO(1)O(1)O(log n)O(1)
Null ключи1 null1 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(...);

Итоги

  1. Map хранит пары ключ-значение, ключи уникальны
  2. HashMap - по умолчанию, O(1) операции, порядок не гарантируется
  3. LinkedHashMap - сохраняет порядок вставки или доступа (LRU кэш)
  4. TreeMap - автоматическая сортировка по ключам, O(log n)
  5. ConcurrentHashMap - потокобезопасная, не поддерживает null
  6. Всегда переопределяй hashCode и equals для кастомных ключей
  7. Используй immutable объекты как ключи (String, Integer и т.д.)
  8. computeIfAbsent/merge - атомарные операции для обновления значений
  9. Не модифицируй Map во время итерации - используй iterator.remove()
  10. Указывай initial capacity для оптимизации производительности

Задания для практики

  1. Подсчет слов: Дан текст. Создай Map<String, Integer> с частотой каждого слова. Используй merge().

  2. LRU Cache: Реализуй LRU кэш на 5 элементов используя LinkedHashMap с accessOrder=true.

  3. Группировка: Дан список Person(name, city). Создай Map<String, List> - группировка по городу.

  4. ConcurrentHashMap: Реализуй потокобезопасный счетчик посещений страниц. Несколько потоков инкрементируют счетчики.

  5. 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 (наивысший)

Сравнительная таблица по критериям

КритерийArrayListLinkedListHashSetTreeSetArrayDequeHashMap
Доступ по индексу✅ O(1)❌ O(n)N/AN/AN/AN/A
Поиск элемента❌ O(n)❌ O(n)✅ O(1)✅ O(log n)❌ O(n)✅ O(1)
Вставка в начало❌ O(n)✅ O(1)N/AN/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());

Чек-лист выбора коллекции

Вопросы для принятия решения:

  1. Структура данных:

    • Нужна пара ключ-значение? → Map
    • Нужны только уникальные элементы? → Set
    • Нужна очередь/стек? → Queue/Deque
    • Нужен список с индексами? → List
  2. Операции:

    • Частый доступ по индексу? → ArrayList
    • Частая проверка наличия? → HashSet/HashMap
    • Частые вставки/удаления в начале? → LinkedList/ArrayDeque
    • Только добавление в конец? → ArrayList
  3. Порядок:

    • Порядок не важен? → HashSet/HashMap
    • Нужен порядок вставки? → LinkedHashSet/LinkedHashMap
    • Нужна автосортировка? → TreeSet/TreeMap
  4. Дополнительно:

    • Нужны 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

Итоговые рекомендации

По умолчанию используй:

  • ListArrayList (самый частый случай)
  • SetHashSet (самый быстрый)
  • QueueArrayDeque (быстрее LinkedList)
  • MapHashMap (самый быстрый)

Специальные случаи:

  • Нужна сортировка → 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 & ...>

Правила:

  1. ✅ Первый bound может быть класс ИЛИ интерфейс ИЛИ type variable
  2. ❌ Последующие bounds могут быть ТОЛЬКО интерфейсы
  3. ❌ Нельзя указывать два класса в bounds
  4. ❌ Нельзя указывать 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) если:

  1. C - имя generic класса или интерфейса
  2. Количество type arguments = количество type parameters
  3. Каждый 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 (доказуемо различны) если:

  1. Это параметризации разных generic типов

    List<String> и Set<String>  // Провably distinct
    
  2. Любой из их 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 (рекомендуется):

  1. Используй осмысленные имена для type parameters

    class UserCache<User, UserId> { }  // ✅ Понятно
    class UserCache<T, K> { }          // ❌ Непонятно
    
  2. Указывай bounds когда нужны специфичные операции

    <T extends Comparable<T>> T max(List<T> list)  // ✅
    <T> T max(List<T> list)  // ❌ Нельзя вызвать compareTo
    
  3. Используй wildcards для гибкости API

    void addAll(Collection<? extends E> c)  // ✅ Гибко
    void addAll(Collection<E> c)            // ❌ Слишком строго
    
  4. PECS - Producer Extends, Consumer Super

    <T> void copy(List<? extends T> src, List<? super T> dest)  // ✅
    
  5. Используй type inference где возможно

    List<String> list = new ArrayList<>();  // ✅ Diamond operator
    List<String> list = new ArrayList<String>();  // ❌ Избыточно
    

❌ DON’T (не рекомендуется):

  1. Не используй raw types

    List list = new ArrayList();  // ❌ Raw type
    List<Object> list = new ArrayList<>();  // ✅
    
  2. Не создавай generic массивы

    List<String>[] array = new List<String>[10];  // ❌ Не скомпилируется
    
  3. Не используй type parameters в static контексте

    class Box<T> {
        private static T value;  // ❌
    }
    
  4. Не злоупотребляй wildcards

    // ❌ Слишком сложно
    Map<? extends String, ? super List<? extends Number>> map;
    
    // ✅ Проще и понятнее
    Map<String, List<Number>> map;
    

Заключение

Ключевые моменты:

  1. Type Parameter - это placeholder для типа, который будет указан при использовании
  2. Type Bounds ограничивают допустимые типы и дают доступ к методам
  3. Multiple Bounds позволяют требовать реализацию нескольких интерфейсов
  4. Wildcards (?, ? extends, ? super) добавляют гибкость
  5. Type Erasure стирает информацию о type parameters в runtime
  6. 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✅ TConsumer - пишем элементы типа 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
}

Как работает:

  1. List<?> передается в swapHelper
  2. Компилятор выводит T = capture of ?
  3. В swapHelper тип известен как T
  4. Теперь можно читать и писать

Пример: 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) {
    // ...
}

Сравнительная таблица

КритерийWildcardType Parameter
Количество использований1 разМного раз
Возвращаемый тип❌ Нет✅ Да
Multiple bounds❌ Нет✅ Да
Взаимосвязь типов❌ Нет✅ Да
Простота✅ Проще❌ Сложнее
Гибкость✅ Больше❌ Меньше

Best Practices

✅ DO (рекомендуется):

  1. Используй PECS принцип

    <T> void copy(
        List<? extends T> source,   // Producer - extends
        List<? super T> dest        // Consumer - super
    )
    
  2. Используй ? когда тип не важен

    void printSize(Collection<?> c) {  // ✅
        System.out.println(c.size());
    }
    
  3. Используй wildcards для гибкости API

    interface Collection<E> {
        boolean addAll(Collection<? extends E> c);  // ✅ Гибко
    }
    
  4. Используй type parameter когда есть взаимосвязь

    <T> void swap(List<T> list, int i, int j)  // ✅
    
  5. Документируй wildcard bounds

    /**
     * @param numbers список чисел для суммирования (producer)
     */
    double sum(List<? extends Number> numbers)
    

❌ DON’T (не рекомендуется):

  1. Не используй wildcard когда нужен type parameter

    // ❌ Плохо
    List<?> reverse(List<?> list)
    
    // ✅ Хорошо
    <T> List<T> reverse(List<T> list)
    
  2. Не используй nested wildcards без необходимости

    // ❌ Слишком сложно
    List<? extends List<? extends Number>> lists
    
    // ✅ Проще
    List<List<? extends Number>> lists
    
  3. Не путай extends и super

    // ❌ Неправильно для чтения
    void print(List<? super String> list)
    
    // ✅ Правильно
    void print(List<? extends String> list)
    
  4. Не используй wildcards в return type без необходимости

    // ❌ Неудобно для вызывающего кода
    List<?> getList()
    
    // ✅ Лучше
    <T> List<T> getList()
    
  5. Не создавай экземпляры с 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());
    }
}

Заключение

Ключевые моменты:

  1. Wildcards обеспечивают гибкость при работе с дженериками
  2. ? - неизвестный тип, используй когда тип не важен
  3. ? extends T - Producer (читаем), верхняя граница
  4. ? super T - Consumer (пишем), нижняя граница
  5. 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?

  1. Обратная совместимость (Backward Compatibility)

    • Код с дженериками может работать со старым кодом без дженериков
    • Старый байт-код может работать с новой JVM
    • Библиотеки с дженериками совместимы со старыми версиями
  2. Migration Compatibility

    • Постепенный переход от legacy кода к коду с дженериками
    • Не нужно переписывать весь код сразу
  3. Единый байт-код

    • Нет дублирования классов для разных типов (как в 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?

  1. Override метода с параметризованным типом
  2. 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:

  1. ✅ Primitive types: int, double, boolean
  2. ✅ Non-generic классы и интерфейсы: String, Object, Integer
  3. ✅ Raw types: List, Map
  4. ✅ Параметризованные типы с unbounded wildcards: List<?>, Map<?, ?>
  5. ✅ Массивы reifiable типов: int[], String[], List<?>[]

Типы, которые НЕ reifiable:

  1. ❌ Параметризованные типы: List<String>, Map<String, Integer>
  2. ❌ Type variables: T, E, K, V
  3. ❌ Параметризованные типы с type variables: List<T>
  4. ❌ Bounded wildcards: List<? extends Number>, List<? super Integer>

Зачем нужна reifiability?

Используется для:

  • Создания массивов: new String[10] ✅, new List<String>[10]
  • instanceof checks: 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?

  1. Совместимость с legacy кодом (до Java 5)
  2. 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

  1. Unchecked warnings игнорируются
  2. Raw types используются
  3. Unchecked casts выполняются
  4. 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

  1. ✅ Не игнорируйте unchecked warnings
  2. ✅ Не используйте raw types
  3. ✅ Будьте осторожны с unchecked casts
  4. ✅ Используйте @SafeVarargs только когда метод точно безопасен
  5. ✅ Используйте @SuppressWarnings("unchecked") локально и осторожно

Сравнение с другими языками

Java vs C++

Java GenericsC++ Templates
Type ErasureCode Generation (каждый тип = отдельный класс)
Один байт-код для всех типовОтдельный код для каждого типа
Информация о типе теряетсяИнформация о типе сохраняется
Меньший размер программыБольший размер программы
Ограничения в runtimeБольше возможностей в runtime
Обратная совместимостьНет обратной совместимости

Java vs C#

JavaC#
Type ErasureReified 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 (рекомендуется):

  1. Понимайте ограничения Type Erasure

    • Не пытайтесь получить информацию о типе в runtime
    • Используйте Class или TypeToken когда нужна информация о типе
  2. Используйте параметризованные типы везде

    List<String> list = new ArrayList<>();  // ✅
    
  3. Обращайте внимание на unchecked warnings

    • Не игнорируйте без причины
    • Понимайте почему warning возникает
  4. Используйте bounded type parameters

    <T extends Number> T add(T a, T b)  // ✅
    
  5. Передавайте Class когда нужна реификация

    public <T> T create(Class<T> clazz) throws Exception {
        return clazz.getDeclaredConstructor().newInstance();
    }
    

❌ DON’T (не рекомендуется):

  1. Не используйте raw types

    List list = new ArrayList();  // ❌ Raw type
    
  2. Не пытайтесь создать массив параметризованных типов

    List<String>[] array = new List<String>[10];  // ❌ Не скомпилируется
    
  3. Не используйте instanceof с параметризованными типами

    if (obj instanceof List<String>) { }  // ❌ Не скомпилируется
    
  4. Не перегружайте методы с одинаковым erasure

    void process(List<String> list) { }   // ❌
    void process(List<Integer> list) { }  // Same erasure
    
  5. Не полагайтесь на тип параметра в static контексте

    public class Box<T> {
        private static T instance;  // ❌ Не скомпилируется
    }
    

Заключение

Ключевые моменты:

  1. Type Erasure - это компромисс между:

    • Обратной совместимостью ✅
    • Информацией о типе в runtime ❌
  2. В runtime:

    • List<String> = List<Integer> = List
    • Вся информация о type arguments стирается
  3. Компилятор вставляет casts автоматически:

    String s = list.get(0);  →  String s = (String)list.get(0);
    
  4. Bridge methods сохраняют полиморфизм после erasure

  5. Raw types существуют для совместимости, но не используйте их в новом коде

Важно помнить:

  • Generics - это compile-time feature
  • Type safety проверяется только во время компиляции
  • В runtime JVM видит только erased types
  • Unchecked warnings - признак потенциальных проблем

Type Erasure - фундаментальная особенность дженериков в Java, которую нужно понимать для эффективного использования системы типов.

2.3. Stream API

Функциональная обработка коллекций

Содержание

2.3.1. Создание стримов

Stream.of(), Collection.stream(), Arrays.stream()

Материалы

ТипСсылка
Документссылка
Видеоссылка

Что такое Stream?

Stream - это последовательность элементов, поддерживающая последовательные и параллельные агрегатные операции.

Ключевые характеристики Stream

  1. No storage (Не хранит данные)

    • Stream не является структурой данных
    • Передает элементы из источника через pipeline операций
  2. Functional in nature (Функциональная природа)

    • Операции не модифицируют источник
    • Создают новый stream с результатом
  3. Laziness-seeking (Ленивые вычисления)

    • Промежуточные операции выполняются только при терминальной операции
    • Оптимизация вычислений
  4. Possibly unbounded (Могут быть бесконечными)

    • Stream может быть бесконечным
    • Short-circuiting операции позволяют завершить обработку
  5. 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 (рекомендуется):

  1. Используй try-with-resources для файловых stream’ов

    try (Stream<String> lines = Files.lines(Paths.get("file.txt"))) {
        lines.forEach(System.out::println);
    }
    
  2. Используй примитивные stream’ы для примитивов

    IntStream.range(1, 100).sum();  // ✅ Эффективно
    Stream.of(1, 2, 3).mapToInt(i -> i).sum();  // ❌ Избыточно
    
  3. Ограничивай бесконечные stream’ы

    Stream.iterate(0, n -> n + 1)
          .limit(100)  // ✅ Ограничение обязательно
          .forEach(System.out::println);
    
  4. Используй метод reference когда возможно

    stream.map(String::toUpperCase)  // ✅
    stream.map(s -> s.toUpperCase())  // Работает, но менее идиоматично
    
  5. Закрывай stream’ы из Files

    try (Stream<String> lines = Files.lines(path)) {
        // обработка
    }  // Автоматически закроется
    

❌ DON’T (не рекомендуется):

  1. Не переиспользуй stream

    Stream<String> stream = list.stream();
    stream.forEach(System.out::println);
    stream.forEach(System.out::println);  // ❌ IllegalStateException!
    
  2. Не модифицируй источник во время обработки

    list.stream()
        .forEach(item -> list.add(item));  // ❌ ConcurrentModificationException
    
  3. Не используй parallelStream() бездумно

    smallList.parallelStream()  // ❌ Overhead > benefit
             .filter(...)
             .collect(Collectors.toList());
    
  4. Не забывай про limit() для бесконечных stream’ов

    Stream.generate(Math::random)
          .forEach(System.out::println);  // ❌ Бесконечный цикл!
    
  5. Не используй 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)
BuilderStream.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()

Заключение

Ключевые моменты:

  1. Stream - это абстракция для работы с последовательностями данных
  2. Существует много способов создания stream’ов
  3. Примитивные stream’ы (IntStream, LongStream, DoubleStream) эффективнее
  4. Stream’ы одноразовые - после использования нужно создавать новый
  5. Ленивые вычисления - промежуточные операции выполняются только при терминальной
  6. Параллельные stream’ы эффективны только для больших данных
  7. Файловые 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/StatefulShort-circuit
filter()Фильтрация по условиюStatelessНет
map()Преобразование элементовStatelessНет
flatMap()Выравнивание вложенных структурStatelessНет
sorted()СортировкаStatefulНет
distinct()Удаление дубликатовStatefulНет
peek()Побочный эффект (отладка)StatelessНет
limit()Ограничить N элементамиStatefulДа
skip()Пропустить N элементовStatefulНет
takeWhile()Брать пока trueStatelessДа
dropWhile()Пропускать пока trueStatelessНет
mapMulti()Гибкий маппингStatelessНет

Итоги

  1. Промежуточные операции ленивые - выполняются только при терминальной операции
  2. filter() - отбирает элементы по условию
  3. map() - преобразует каждый элемент
  4. flatMap() - выравнивает вложенные структуры
  5. sorted() - сортирует (stateful, буферизует все элементы)
  6. distinct() - удаляет дубликаты (по equals/hashCode)
  7. peek() - для отладки, не для побочных эффектов
  8. limit()/skip() - для пагинации и ограничения
  9. Порядок операций важен - filter до sorted эффективнее

Задания для практики

  1. Фильтрация и преобразование: Дан список чисел от 1 до 100. Получи список квадратов всех четных чисел больше 50.

  2. flatMap: Дан список предложений. Получи список всех уникальных слов длиннее 3 символов, отсортированный по алфавиту.

  3. Пагинация: Реализуй метод paginate(List<T> items, int page, int size), возвращающий элементы указанной страницы.

  4. Сортировка объектов: Дан список Employee(name, department, salary). Отсортируй по департаменту, затем по зарплате (убывание).

  5. Комплексная задача: Дан список заказов 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()Сборка в ListListНет
reduce()Свертка в одно значениеOptional/TНет
count()Количество элементовlongНет
min()Минимальный элементOptionalНет
max()Максимальный элементOptionalНет
findFirst()Первый элементOptionalДа
findAny()Любой элементOptionalДа
anyMatch()Есть ли совпадениеbooleanДа
allMatch()Все ли совпадаютbooleanДа
noneMatch()Нет ли совпаденийbooleanДа
sum()Сумма (примитивы)int/long/doubleНет
average()Среднее (примитивы)OptionalDoubleНет

Итоги

  1. Терминальные операции запускают pipeline - без них промежуточные операции не выполняются
  2. forEach - для побочных эффектов (вывод, логирование)
  3. collect - для сборки результата в коллекцию
  4. reduce - для свертки в одно значение
  5. findFirst/findAny - для поиска элемента (short-circuit)
  6. anyMatch/allMatch/noneMatch - для проверок (short-circuit)
  7. count/min/max/sum/average - для агрегации
  8. Stream одноразовый - после терминальной операции использовать нельзя
  9. Результат часто Optional - обрабатывай отсутствие значения

Задания для практики

  1. reduce: Дан список чисел. Найди произведение всех положительных чисел с помощью reduce.

  2. findFirst + filter: Дан список пользователей User(name, email, age). Найди первого совершеннолетнего (age >= 18) пользователя с email на gmail.com.

  3. allMatch/anyMatch: Дан список заказов Order(items, status). Проверь: а) все ли заказы доставлены, б) есть ли отмененные заказы.

  4. collect + reduce: Дан список транзакций Transaction(type, amount). Посчитай баланс (сумма DEPOSIT минус сумма WITHDRAW).

  5. Комплексная задача: Дан список студентов 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()В ListList<T>
toSet()В SetSet<T>
toCollection(supplier)В указанную коллекциюC
toMap(key, value)В MapMap<K, V>
toUnmodifiableList()В неизменяемый ListList<T>
toUnmodifiableSet()В неизменяемый SetSet<T>
toUnmodifiableMap()В неизменяемую MapMap<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
    ));

Итоги

  1. Collectors - мощный инструмент для сбора элементов Stream
  2. toList/toSet/toMap - базовые коллекторы для сборки в коллекции
  3. groupingBy - группировка по ключу, поддерживает downstream коллекторы
  4. partitioningBy - разделение на 2 группы (true/false)
  5. joining - объединение строк с разделителем
  6. Агрегирующие коллекторы - counting, summing, averaging, summarizing
  7. mapping/flatMapping/filtering - преобразование перед сборкой
  8. collectingAndThen - финальное преобразование результата
  9. teeing (Java 12+) - объединение двух коллекторов
  10. Всегда обрабатывай дубликаты в toMap

Задания для практики

  1. groupingBy + counting: Дан список слов. Сгруппируй по длине и посчитай количество слов каждой длины.

  2. toMap с дубликатами: Дан список Person(name, city). Создай Map<city, names> где names - строка с именами через запятую.

  3. partitioningBy + статистика: Дан список оценок (0-100). Раздели на сдавших (>=60) и не сдавших. Для каждой группы выведи статистику (мин, макс, среднее).

  4. Многоуровневая группировка: Дан список Transaction(type, category, amount). Сгруппируй по типу, затем по категории, и посчитай сумму в каждой подгруппе.

  5. 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 OptionJava Optional
Some(value)Optional.of(value)
NoneOptional.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"

Правила определения

Интерфейс считается функциональным, если:

  1. Имеет ровно один абстрактный метод
  2. Может иметь любое количество default и static методов
  3. Может иметь абстрактные методы из 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 → RString::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Дата и время со смещением от UTC2025-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
aAM/PMPM
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;
}

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

Исключение может быть брошено в четырёх случаях:

  1. Явный throw — программист бросает исключение
  2. Провал assert — при assert false
  3. Синхронная ошибка JVM — деление на ноль, выход за границы массива, null-разыменование
  4. Асинхронная ошибка 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)
НаправлениеОднонаправленные потокиДвунаправленные каналы
БлокировкаБлокирующийБлокирующий / неблокирующий
ПодходБайт за байтомБлоками данных
Файловая системаFilePath + Files (NIO.2)
ПоявлениеJava 1.0Java 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 BufferDirect 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

Ключевые принципы:

  1. Всегда используйте try-with-resources для потоков и каналов
  2. Указывайте кодировку явно (StandardCharsets.UTF_8)
  3. Буферизуйте файловые потоки
  4. Для современного кода предпочитайте Path/Files над File
  5. Выбирайте 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 — использование устаревших API
  • unchecked — непроверенные операции с generics
  • rawtypes — использование raw-типов вместо generics
  • serial — отсутствие 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, и т.д.)
    • String
    • Class или 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 — для аннотаций, читаемых из байткода инструментами, но не нужных в runtime
  • RUNTIME — для аннотаций, обрабатываемых через рефлексию во время выполнения

@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() { }

Для повторяемой аннотации необходимо:

  1. Объявить контейнерную аннотацию с элементом value(), возвращающим массив повторяемой аннотации
  2. Пометить повторяемую аннотацию @Repeatable, указав класс контейнера
  3. Контейнер должен иметь такую же или более широкую политику @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

Компоненты Java Virtual Machine

Содержание

Architecture Component

Материалы

ТипСсылка
Документссылка
Видеоссылка

ClassLoader — механизм JVM, отвечающий за поиск и загрузку классов в память. Понимание работы загрузчиков классов критично для диагностики ClassNotFoundException, NoClassDefFoundError, создания плагинных систем и изоляции кода.

Жизненный цикл класса в JVM

Прежде чем класс можно использовать, JVM выполняет три этапа:

┌─────────────┐    ┌─────────────┐    ┌────────────────┐
│   Loading   │───>│   Linking   │───>│ Initialization │
│  (Загрузка) │    │(Связывание) │    │(Инициализация) │
└─────────────┘    └─────────────┘    └────────────────┘
                         │
           ┌─────────────┼─────────────┐
           ▼             ▼             ▼
    ┌────────────┐ ┌────────────┐ ┌────────────┐
    │Verification│ │Preparation │ │ Resolution │
    │ (Проверка) │ │(Подготовка)│ │(Разрешение)│
    └────────────┘ └────────────┘ └────────────┘

Loading (Загрузка)

Загрузчик находит байт-код класса (обычно .class файл) и создаёт объект Class<?> в памяти JVM:

  1. Находит бинарное представление класса по имени
  2. Создаёт объект Class в method area
  3. Записывает связь между классом и загрузчиком

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 ─► НАЙДЕН!

Зачем нужна делегация

  1. Безопасность — нельзя подменить системные классы:
// Даже если создать свой java/lang/String.class в classpath,
// он не загрузится — bootstrap загрузит настоящий String первым
  1. Уникальность — один класс загружается один раз:
// Класс String всегда один и тот же во всём приложении
String.class == String.class  // true, независимо от контекста
  1. Видимость — дочерние загрузчики видят классы родительских:
// 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

Резюме

ЗагрузчикИмяЧто загружает
Bootstrapnulljava.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. Потоки и процессы

Содержание

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

Фундамент эффективного кода

Знание алгоритмов и структур данных - основа хорошего программиста.

Содержание раздела

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. Порождающие паттерны

Содержание

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. Поведенческие паттерны

Содержание

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.

Содержание раздела

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 приложений.

Содержание раздела

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

Содержание

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 - мощный фреймворк для аутентификации и авторизации.

Содержание раздела

Security Topic

Материалы

ТипСсылка
Документссылка
Видеоссылка

Заметки

9.2. Authentication

Содержание

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 предоставляет инструменты для построения распределенных систем.

Содержание раздела

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

Реактивное программирование

Реактивный подход к построению высоконагруженных приложений.

Содержание раздела

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. Реляционные БД

Содержание

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

Асинхронное взаимодействие

Брокеры сообщений обеспечивают надежную асинхронную коммуникацию между сервисами.

Содержание раздела

Messaging Topic

Материалы

ТипСсылка
Документссылка
Видеоссылка

Заметки

13.2. Apache 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

Содержание

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

Содержание

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.

Содержание раздела

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 методологий важно для эффективной работы в команде.

Содержание раздела

Agile Topic

Материалы

ТипСсылка
Документссылка
Видеоссылка

Заметки

17.2. Scrum

Содержание

Scrum Topic

Материалы

ТипСсылка
Документссылка
Видеоссылка

Заметки

Scrum Topic

Материалы

ТипСсылка
Документссылка
Видеоссылка

Заметки

Scrum Topic

Материалы

ТипСсылка
Документссылка
Видеоссылка

Заметки

Agile Topic

Материалы

ТипСсылка
Документссылка
Видеоссылка

Заметки

Agile Topic

Материалы

ТипСсылка
Документссылка
Видеоссылка

Заметки

Agile Topic

Материалы

ТипСсылка
Документссылка
Видеоссылка

Заметки

Agile Topic

Материалы

ТипСсылка
Документссылка
Видеоссылка

Заметки

Приложение A: Чеклист для собеседований

Java Core вопросы

Spring вопросы

Database вопросы

System Design вопросы

Приложение B: Полезные ресурсы

Книги

Онлайн-курсы

Документация

Приложение C: Глоссарий