Immutable Objects

Что такое Immutable объекты

Immutable объект — это объект, состояние которого нельзя изменить после создания. Все поля должны быть final, а методы не должны изменять внутреннее состояние.

Почему важно для Senior'а:

  • Thread-safety без синхронизации
  • Безопасность в многопоточности
  • Отсутствие побочных эффектов
  • Кэширование hash code
  • Использование в качестве ключей Map

Основные принципы создания

1. Базовая структура

public final class Person {
    private final String name;
    private final int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() { return name; }
    public int getAge() { return age; }
}

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

  • final class — запрещает наследование
  • private final поля — неизменяемые после инициализации
  • Только геттеры, никаких сеттеров
  • Конструктор для инициализации

2. Работа с коллекциями (defensive copying)

public final class StudentGroup {
    private final String groupName;
    private final List<String> students;
    
    public StudentGroup(String groupName, List<String> students) {
        this.groupName = groupName;
        // Defensive copy для защиты от внешних изменений
        this.students = new ArrayList<>(students);
    }
    
    public List<String> getStudents() {
        // Возвращаем копию, а не оригинал
        return new ArrayList<>(students);
    }
}

Defensive copying — создание копий коллекций для предотвращения внешних изменений через ссылки.

3. Использование Collections.unmodifiableList()

public final class Course {
    private final List<String> modules;
    
    public Course(List<String> modules) {
        this.modules = Collections.unmodifiableList(
            new ArrayList<>(modules)
        );
    }
    
    public List<String> getModules() {
        return modules; // Уже immutable
    }
}

Collections.unmodifiableList() — создает неизменяемую обертку над коллекцией.

Builder Pattern для сложных объектов

public final class Address {
    private final String street;
    private final String city;
    private final String zipCode;
    
    private Address(Builder builder) {
        this.street = builder.street;
        this.city = builder.city;
        this.zipCode = builder.zipCode;
    }
    
    public static class Builder {
        private String street;
        private String city;
        private String zipCode;
        
        public Builder street(String street) {
            this.street = street;
            return this;
        }
        
        public Builder city(String city) {
            this.city = city;
            return this;
        }
        
        public Address build() {
            return new Address(this);
        }
    }
}

// Использование:
Address addr = new Address.Builder()
    .street("Main St")
    .city("New York")
    .build();

Builder Pattern — позволяет создавать сложные immutable объекты пошагово.

Records (Java 14+)

public record PersonRecord(String name, int age) {
    // Автоматически генерируется:
    // - private final поля
    // - конструктор
    // - геттеры
    // - equals(), hashCode(), toString()
}

// Валидация в конструкторе
public record ValidatedPerson(String name, int age) {
    public ValidatedPerson {
        if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
        if (name == null) throw new IllegalArgumentException("Name cannot be null");
    }
}

Records — компактный синтаксис для создания immutable data классов.

Value Objects с кэшированием

public final class Money {
    private final BigDecimal amount;
    private final String currency;
    private volatile int hashCode; // Ленивое вычисление
    
    public Money(BigDecimal amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }
    
    @Override
    public int hashCode() {
        int result = hashCode;
        if (result == 0) {
            result = Objects.hash(amount, currency);
            hashCode = result;
        }
        return result;
    }
    
    // Операции возвращают новые объекты
    public Money add(Money other) {
        if (!currency.equals(other.currency)) {
            throw new IllegalArgumentException("Currency mismatch");
        }
        return new Money(amount.add(other.amount), currency);
    }
}

Value Objects — объекты, идентичность которых определяется их значением, а не ссылкой.

Глубокая immutability

public final class Company {
    private final String name;
    private final Address address; // Тоже должен быть immutable
    private final List<Person> employees;
    
    public Company(String name, Address address, List<Person> employees) {
        this.name = name;
        this.address = address; // Address уже immutable
        // Создаем копию и делаем immutable
        this.employees = employees.stream()
            .collect(Collectors.toUnmodifiableList());
    }
}

Глубокая immutability — все вложенные объекты тоже должны быть неизменяемыми.

Библиотечные решения

Guava

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

public final class Configuration {
    private final ImmutableList<String> servers;
    private final ImmutableMap<String, String> properties;
    
    public Configuration(List<String> servers, Map<String, String> props) {
        this.servers = ImmutableList.copyOf(servers);
        this.properties = ImmutableMap.copyOf(props);
    }
}

Guava Immutable Collections — производительные неизменяемые коллекции.

Lombok

import lombok.Value;

@Value
public class Product {
    String name;
    BigDecimal price;
    List<String> categories;
    
    // Автоматически генерируется immutable класс
}

@Value — Lombok аннотация для создания immutable классов.

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

Объектный пул для часто используемых значений

public final class Status {
    public static final Status ACTIVE = new Status("ACTIVE");
    public static final Status INACTIVE = new Status("INACTIVE");
    
    private static final Map<String, Status> CACHE = new HashMap<>();
    
    private final String value;
    
    private Status(String value) {
        this.value = value;
    }
    
    public static Status of(String value) {
        return CACHE.computeIfAbsent(value, Status::new);
    }
}

Оптимизация для коллекций

// Предпочитайте List.of() для малых коллекций (Java 9+)
List<String> items = List.of("item1", "item2", "item3");

// Для больших коллекций используйте Stream API
List<String> processedItems = originalList.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toUnmodifiableList());

Распространенные ошибки

❌ Неправильно

public final class BadExample {
    private final List<String> items;
    
    public BadExample(List<String> items) {
        this.items = items; // Опасно! Внешние изменения влияют на объект
    }
    
    public List<String> getItems() {
        return items; // Можно изменить извне
    }
}

✅ Правильно

public final class GoodExample {
    private final List<String> items;
    
    public GoodExample(List<String> items) {
        this.items = List.copyOf(items); // Создаем immutable копию
    }
    
    public List<String> getItems() {
        return items; // Уже immutable
    }
}

Вопросы на собеседовании

Q: Почему String immutable в Java? A: Thread-safety, оптимизация (пул строк), безопасность (пароли), производительность кэширования hash code.

Q: Различие между Collections.unmodifiableList() и ImmutableList? A: unmodifiableList() — это view, изменения исходной коллекции видны. ImmutableList — полная копия.

Q: Как обеспечить immutability для Date/Calendar? A: Использовать LocalDate/LocalDateTime (Java 8+) или создавать defensive copies для legacy Date.

Q: Производительность immutable vs mutable? A: Immutable: больше объектов в памяти, но thread-safe без синхронизации. Mutable: меньше памяти, но нужна синхронизация.

Класс Object

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

Object - это корневой класс всех классов в Java. Все остальные классы наследуются от Object либо напрямую, либо через цепочку наследования.

Ключевые методы класса Object

1. equals(Object obj)

public boolean equals(Object obj)
  • Сравнивает объекты на равенство
  • По умолчанию сравнивает ссылки (как ==)
  • Часто переопределяется для логического сравнения

Пример переопределения:

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || getClass() != obj.getClass()) return false;
    Person person = (Person) obj;
    return age == person.age && Objects.equals(name, person.name);
}

2. hashCode()

public int hashCode()
  • Возвращает хеш-код объекта
  • Используется в HashMap, HashSet и других коллекциях
  • Правило: если equals() возвращает true, то hashCode() должен возвращать одинаковые значения

Пример переопределения:

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

3. toString()

public String toString()
  • Возвращает строковое представление объекта
  • По умолчанию: имя_класса@хеш-код

Пример переопределения:

@Override
public String toString() {
    return "Person{name='" + name + "', age=" + age + "}";
}

4. clone()

protected Object clone() throws CloneNotSupportedException
  • Создает копию объекта
  • Класс должен реализовать интерфейс Cloneable
  • По умолчанию выполняет поверхностное копирование

Пример использования:

public class Person implements Cloneable {
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

5. getClass()

public final Class<?> getClass()
  • Возвращает объект Class, представляющий класс объекта
  • Нельзя переопределить (final)

Пример:

String str = "Hello";
Class<?> clazz = str.getClass();
System.out.println(clazz.getName()); // java.lang.String

6. finalize()

protected void finalize() throws Throwable
  • Вызывается сборщиком мусора перед удалением объекта
  • Deprecated с Java 9
  • Не рекомендуется использовать

7. wait(), notify(), notifyAll()

public final void wait() throws InterruptedException
public final void notify()
public final void notifyAll()
  • Используются для синхронизации потоков
  • Могут вызываться только в синхронизированном блоке

Контракт equals() и hashCode()

Правила для equals():

  1. Рефлексивность: x.equals(x) должно быть true
  2. Симметричность: если x.equals(y) true, то y.equals(x) тоже true
  3. Транзитивность: если x.equals(y) и y.equals(z) true, то x.equals(z) тоже true
  4. Постоянство: результат не должен изменяться без изменения объектов
  5. Для null: x.equals(null) должно быть false

Правила для hashCode():

  1. Если объекты равны по equals(), их hashCode() должны быть равны
  2. Если hashCode() равны, объекты не обязательно равны
  3. hashCode() должен возвращать одинаковое значение при повторных вызовах

Примеры использования

Полный пример класса с переопределением методов Object:

public class Student implements Cloneable {
    private String name;
    private int id;
    
    public Student(String name, int id) {
        this.name = name;
        this.id = id;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Student student = (Student) obj;
        return id == student.id && Objects.equals(name, student.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, id);
    }
    
    @Override
    public String toString() {
        return "Student{name='" + name + "', id=" + id + "}";
    }
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

Важные замечания

  • Всегда переопределяйте equals() и hashCode() вместе
  • Используйте Objects.equals() и Objects.hash() для упрощения
  • Проверяйте на null в equals()
  • Не забывайте аннотацию @Override
  • Тестируйте переопределенные методы

Utility класс Objects (Java 7+)

// Безопасное сравнение с null
Objects.equals(obj1, obj2)

// Генерация hash-кода
Objects.hash(field1, field2, field3)

// Проверка на null
Objects.requireNonNull(obj)

// toString с default значением
Objects.toString(obj, "default")

Java Stream API

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

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

Характеристики Stream:

  • Не хранит данные - работает с источником данных
  • Функциональный подход - операции не изменяют источник
  • Ленивые вычисления - промежуточные операции выполняются только при терминальной
  • Одноразовый - каждый stream можно использовать только один раз

Создание Stream

Из коллекций:

List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
Stream<String> parallelStream = list.parallelStream();

Из массивов:

String[] array = {"a", "b", "c"};
Stream<String> stream = Arrays.stream(array);
Stream<String> stream2 = Stream.of("a", "b", "c");

Генерация Stream:

// Пустой stream
Stream<String> empty = Stream.empty();

// Бесконечный stream
Stream<Integer> infinite = Stream.iterate(0, n -> n + 1);

// Генерация с условием (Java 9+)
Stream<Integer> finite = Stream.iterate(0, n -> n < 10, n -> n + 1);

// Случайные значения
Stream<Double> random = Stream.generate(Math::random);

// Числовые потоки
IntStream range = IntStream.range(1, 10);      // 1-9
IntStream rangeClosed = IntStream.rangeClosed(1, 10); // 1-10

Промежуточные операции (Intermediate Operations)

filter() - фильтрация

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> even = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());
// Результат: [2, 4, 6]

map() - преобразование

List<String> words = Arrays.asList("hello", "world");
List<String> upper = words.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());
// Результат: ["HELLO", "WORLD"]

flatMap() - плоское преобразование

List<List<String>> nested = Arrays.asList(
    Arrays.asList("a", "b"),
    Arrays.asList("c", "d")
);
List<String> flattened = nested.stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList());
// Результат: ["a", "b", "c", "d"]

distinct() - уникальные элементы

List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 4);
List<Integer> unique = numbers.stream()
    .distinct()
    .collect(Collectors.toList());
// Результат: [1, 2, 3, 4]

sorted() - сортировка

List<String> words = Arrays.asList("banana", "apple", "cherry");
List<String> sorted = words.stream()
    .sorted()
    .collect(Collectors.toList());
// Результат: ["apple", "banana", "cherry"]

// С компаратором
List<String> sortedByLength = words.stream()
    .sorted(Comparator.comparing(String::length))
    .collect(Collectors.toList());

peek() - промежуточный просмотр

List<String> result = Stream.of("a", "b", "c")
    .peek(System.out::println)
    .map(String::toUpperCase)
    .collect(Collectors.toList());

limit() и skip() - ограничение и пропуск

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Первые 5 элементов
List<Integer> first5 = numbers.stream()
    .limit(5)
    .collect(Collectors.toList());

// Пропустить первые 3, взять следующие 5
List<Integer> middle = numbers.stream()
    .skip(3)
    .limit(5)
    .collect(Collectors.toList());

Терминальные операции (Terminal Operations)

collect() - сбор в коллекцию

List<String> words = Arrays.asList("java", "stream", "api");

// В список
List<String> list = words.stream().collect(Collectors.toList());

// В множество
Set<String> set = words.stream().collect(Collectors.toSet());

// В карту
Map<String, Integer> map = words.stream()
    .collect(Collectors.toMap(
        word -> word,
        String::length
    ));

// Объединение в строку
String joined = words.stream()
    .collect(Collectors.joining(", "));
// Результат: "java, stream, api"

forEach() - выполнение действия для каждого элемента

List<String> words = Arrays.asList("a", "b", "c");
words.stream().forEach(System.out::println);

reduce() - сведение к одному значению

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// Сумма
int sum = numbers.stream()
    .reduce(0, (a, b) -> a + b);
// или
int sum2 = numbers.stream()
    .reduce(0, Integer::sum);

// Максимум
Optional<Integer> max = numbers.stream()
    .reduce(Integer::max);

// Произведение
int product = numbers.stream()
    .reduce(1, (a, b) -> a * b);

Поиск элементов:

List<String> words = Arrays.asList("apple", "banana", "cherry");

// Найти любой элемент
Optional<String> any = words.stream()
    .filter(w -> w.startsWith("a"))
    .findAny();

// Найти первый элемент
Optional<String> first = words.stream()
    .filter(w -> w.startsWith("a"))
    .findFirst();

// Проверка условий
boolean anyMatch = words.stream()
    .anyMatch(w -> w.length() > 5);

boolean allMatch = words.stream()
    .allMatch(w -> w.length() > 3);

boolean noneMatch = words.stream()
    .noneMatch(w -> w.isEmpty());

Подсчет и статистика:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// Количество элементов
long count = numbers.stream().count();

// Минимум и максимум
Optional<Integer> min = numbers.stream().min(Integer::compareTo);
Optional<Integer> max = numbers.stream().max(Integer::compareTo);

// Для числовых потоков
IntSummaryStatistics stats = numbers.stream()
    .mapToInt(Integer::intValue)
    .summaryStatistics();
System.out.println("Среднее: " + stats.getAverage());
System.out.println("Сумма: " + stats.getSum());

Специальные числовые Stream

IntStream, LongStream, DoubleStream

// Создание
IntStream intStream = IntStream.range(1, 10);
DoubleStream doubleStream = DoubleStream.of(1.0, 2.0, 3.0);

// Преобразование
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
    .mapToInt(Integer::intValue)
    .sum();

double average = numbers.stream()
    .mapToInt(Integer::intValue)
    .average()
    .orElse(0.0);

Группировка и разбиение

Группировка (groupingBy):

List<Person> people = Arrays.asList(
    new Person("John", 25),
    new Person("Jane", 30),
    new Person("Bob", 25)
);

// Группировка по возрасту
Map<Integer, List<Person>> byAge = people.stream()
    .collect(Collectors.groupingBy(Person::getAge));

// Группировка с подсчетом
Map<Integer, Long> countByAge = people.stream()
    .collect(Collectors.groupingBy(
        Person::getAge,
        Collectors.counting()
    ));

Разбиение (partitioningBy):

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

Map<Boolean, List<Integer>> partitioned = numbers.stream()
    .collect(Collectors.partitioningBy(n -> n % 2 == 0));
// Результат: {false=[1, 3, 5], true=[2, 4, 6]}

Параллельные потоки

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Параллельная обработка
int sum = numbers.parallelStream()
    .filter(n -> n % 2 == 0)
    .mapToInt(Integer::intValue)
    .sum();

// Преобразование в параллельный поток
int sum2 = numbers.stream()
    .parallel()
    .filter(n -> n % 2 == 0)
    .mapToInt(Integer::intValue)
    .sum();

Примеры практического использования

Обработка списка сотрудников:

List<Employee> employees = getEmployees();

// Найти всех сотрудников старше 30 лет с зарплатой > 50000
List<Employee> filtered = employees.stream()
    .filter(emp -> emp.getAge() > 30)
    .filter(emp -> emp.getSalary() > 50000)
    .collect(Collectors.toList());

// Средняя зарплата по департаментам
Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingDouble(Employee::getSalary)
    ));

// Топ-5 сотрудников по зарплате
List<Employee> top5 = employees.stream()
    .sorted(Comparator.comparing(Employee::getSalary).reversed())
    .limit(5)
    .collect(Collectors.toList());

Обработка текста:

String text = "Java Stream API is powerful and flexible";

// Количество слов длиннее 4 символов
long count = Arrays.stream(text.split(" "))
    .filter(word -> word.length() > 4)
    .count();

// Уникальные символы в нижнем регистре
Set<Character> uniqueChars = text.toLowerCase()
    .chars()
    .filter(c -> c != ' ')
    .mapToObj(c -> (char) c)
    .collect(Collectors.toSet());

Важные замечания

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

  • Используйте примитивные потоки (IntStream, LongStream) для чисел
  • Параллельные потоки эффективны для больших данных и CPU-intensive операций
  • Избегайте создания промежуточных коллекций

Лучшие практики:

  • Используйте method references где возможно
  • Избегайте побочных эффектов в lambda-выражениях
  • Не переиспользуйте потоки
  • Предпочитайте collect() вместо reduce() для мутабельных операций

Отладка:

List<String> result = stream
    .filter(s -> s.length() > 3)
    .peek(s -> System.out.println("Filtered: " + s))
    .map(String::toUpperCase)
    .peek(s -> System.out.println("Mapped: " + s))
    .collect(Collectors.toList());

Массивы

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

Массив - это объект, который содержит фиксированное количество элементов одного типа. Массивы в Java являются объектами и наследуются от класса Object.

Характеристики массивов:

  • Фиксированный размер - определяется при создании
  • Индексация с нуля - первый элемент имеет индекс 0
  • Гомогенность - все элементы одного типа
  • Ссылочный тип - массив является объектом

Объявление и создание массивов

Одномерные массивы:

// Объявление
int[] numbers;                    // Предпочтительный стиль
int numbers[];                    // Альтернативный стиль

// Создание с указанием размера
int[] numbers = new int[5];       // Массив из 5 элементов (по умолчанию 0)

// Создание с инициализацией
int[] numbers = {1, 2, 3, 4, 5};  // Краткая форма
int[] numbers = new int[]{1, 2, 3, 4, 5};  // Полная форма

// Объявление и инициализация в разных строках
int[] numbers;
numbers = new int[]{1, 2, 3, 4, 5};

Многомерные массивы:

// Двумерный массив
int[][] matrix = new int[3][4];   // 3 строки, 4 столбца

// Инициализация двумерного массива
int[][] matrix = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

// Трёхмерный массив
int[][][] cube = new int[2][3][4];

// Зубчатый массив (разная длина строк)
int[][] jagged = new int[3][];
jagged[0] = new int[2];
jagged[1] = new int[4];
jagged[2] = new int[3];

Инициализация и значения по умолчанию

Значения по умолчанию:

// Для примитивных типов
int[] integers = new int[5];      // [0, 0, 0, 0, 0]
double[] doubles = new double[3]; // [0.0, 0.0, 0.0]
boolean[] booleans = new boolean[2]; // [false, false]
char[] chars = new char[3];       // ['\u0000', '\u0000', '\u0000']

// Для объектов
String[] strings = new String[3]; // [null, null, null]
Object[] objects = new Object[2]; // [null, null]

Инициализация всех элементов:

// Заполнение одним значением
int[] numbers = new int[5];
Arrays.fill(numbers, 10);         // [10, 10, 10, 10, 10]

// Заполнение диапазона
Arrays.fill(numbers, 1, 3, 20);   // [10, 20, 20, 10, 10]

// Инициализация с помощью цикла
int[] squares = new int[10];
for (int i = 0; i < squares.length; i++) {
    squares[i] = i * i;
}

Доступ к элементам и свойства

Обращение к элементам:

int[] numbers = {10, 20, 30, 40, 50};

// Получение элемента
int first = numbers[0];           // 10
int last = numbers[numbers.length - 1]; // 50

// Изменение элемента
numbers[2] = 100;                 // [10, 20, 100, 40, 50]

// Получение длины массива
int length = numbers.length;      // 5

Работа с многомерными массивами:

int[][] matrix = {{1, 2, 3}, {4, 5, 6}};

// Доступ к элементу
int element = matrix[1][2];       // 6

// Изменение элемента
matrix[0][1] = 20;               // {{1, 20, 3}, {4, 5, 6}}

// Длина массива
int rows = matrix.length;         // 2
int cols = matrix[0].length;      // 3

Перебор массивов

Обычный for:

int[] numbers = {1, 2, 3, 4, 5};

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

Расширенный for (for-each):

int[] numbers = {1, 2, 3, 4, 5};

for (int num : numbers) {
    System.out.println(num);
}

// Для многомерных массивов
int[][] matrix = {{1, 2}, {3, 4}, {5, 6}};

for (int[] row : matrix) {
    for (int element : row) {
        System.out.print(element + " ");
    }
    System.out.println();
}

While и do-while:

int[] numbers = {1, 2, 3, 4, 5};
int i = 0;

while (i < numbers.length) {
    System.out.println(numbers[i]);
    i++;
}

Класс Arrays

Вывод массива:

int[] numbers = {1, 2, 3, 4, 5};

// Одномерный массив
System.out.println(Arrays.toString(numbers));
// Вывод: [1, 2, 3, 4, 5]

// Многомерный массив
int[][] matrix = {{1, 2}, {3, 4}};
System.out.println(Arrays.deepToString(matrix));
// Вывод: [[1, 2], [3, 4]]

Сортировка:

int[] numbers = {5, 2, 8, 1, 9};

// Сортировка всего массива
Arrays.sort(numbers);
System.out.println(Arrays.toString(numbers)); // [1, 2, 5, 8, 9]

// Сортировка части массива
int[] array = {5, 2, 8, 1, 9, 3};
Arrays.sort(array, 1, 4);  // сортирует элементы с индекса 1 по 3
System.out.println(Arrays.toString(array)); // [5, 1, 2, 8, 9, 3]

// Сортировка объектов
String[] words = {"banana", "apple", "cherry"};
Arrays.sort(words);
System.out.println(Arrays.toString(words)); // [apple, banana, cherry]

// Сортировка с компаратором
Arrays.sort(words, Comparator.reverseOrder());
System.out.println(Arrays.toString(words)); // [cherry, banana, apple]

Поиск элементов:

int[] numbers = {1, 2, 5, 8, 9};

// Бинарный поиск (массив должен быть отсортирован)
int index = Arrays.binarySearch(numbers, 5);
System.out.println(index); // 2

// Поиск в диапазоне
int index2 = Arrays.binarySearch(numbers, 1, 4, 8);
System.out.println(index2); // 3

// Если элемент не найден, возвращается отрицательное число
int notFound = Arrays.binarySearch(numbers, 6);
System.out.println(notFound); // -4 (-(insertion point) - 1)

Сравнение массивов:

int[] array1 = {1, 2, 3};
int[] array2 = {1, 2, 3};
int[] array3 = {1, 2, 4};

// Сравнение массивов
boolean equal1 = Arrays.equals(array1, array2); // true
boolean equal2 = Arrays.equals(array1, array3); // false

// Для многомерных массивов
int[][] matrix1 = {{1, 2}, {3, 4}};
int[][] matrix2 = {{1, 2}, {3, 4}};
boolean deepEqual = Arrays.deepEquals(matrix1, matrix2); // true

Копирование массивов:

int[] original = {1, 2, 3, 4, 5};

// Полное копирование
int[] copy1 = Arrays.copyOf(original, original.length);

// Копирование с изменением размера
int[] copy2 = Arrays.copyOf(original, 3);     // [1, 2, 3]
int[] copy3 = Arrays.copyOf(original, 7);     // [1, 2, 3, 4, 5, 0, 0]

// Копирование диапазона
int[] copy4 = Arrays.copyOfRange(original, 1, 4); // [2, 3, 4]

// System.arraycopy для лучшей производительности
int[] destination = new int[5];
System.arraycopy(original, 0, destination, 0, original.length);

Заполнение массивов:

int[] numbers = new int[5];

// Заполнение одним значением
Arrays.fill(numbers, 10);        // [10, 10, 10, 10, 10]

// Заполнение диапазона
Arrays.fill(numbers, 1, 3, 20);  // [10, 20, 20, 10, 10]

// Параллельное заполнение (Java 8+)
int[] array = new int[10];
Arrays.parallelSetAll(array, i -> i * i); // [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Работа с массивами и коллекциями

Преобразование массива в список:

String[] array = {"a", "b", "c"};

// Создание списка на основе массива
List<String> list = Arrays.asList(array);

// Создание изменяемого списка
List<String> mutableList = new ArrayList<>(Arrays.asList(array));

// Java 9+ - создание неизменяемого списка
List<String> immutableList = List.of(array);

Преобразование списка в массив:

List<String> list = Arrays.asList("a", "b", "c");

// Метод toArray()
String[] array1 = list.toArray(new String[0]);
String[] array2 = list.toArray(new String[list.size()]);

// Универсальный способ
Object[] array3 = list.toArray();

Использование Stream API с массивами:

int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// Фильтрация четных чисел
int[] evenNumbers = Arrays.stream(numbers)
    .filter(n -> n % 2 == 0)
    .toArray();

// Сумма всех элементов
int sum = Arrays.stream(numbers).sum();

// Максимальный элемент
OptionalInt max = Arrays.stream(numbers).max();

// Преобразование в список
List<Integer> list = Arrays.stream(numbers)
    .boxed()
    .collect(Collectors.toList());

Практические примеры

Поиск элементов в массиве:

public class ArrayUtils {
    // Линейный поиск
    public static int linearSearch(int[] array, int target) {
        for (int i = 0; i < array.length; i++) {
            if (array[i] == target) {
                return i;
            }
        }
        return -1;
    }
    
    // Поиск максимального элемента
    public static int findMax(int[] array) {
        if (array.length == 0) {
            throw new IllegalArgumentException("Array is empty");
        }
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max) {
                max = array[i];
            }
        }
        return max;
    }
    
    // Поиск минимального элемента
    public static int findMin(int[] array) {
        if (array.length == 0) {
            throw new IllegalArgumentException("Array is empty");
        }
        int min = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] < min) {
                min = array[i];
            }
        }
        return min;
    }
}

Операции с двумерными массивами:

public class MatrixUtils {
    // Вывод матрицы
    public static void printMatrix(int[][] matrix) {
        for (int[] row : matrix) {
            for (int element : row) {
                System.out.printf("%4d", element);
            }
            System.out.println();
        }
    }
    
    // Сумма элементов матрицы
    public static int sumMatrix(int[][] matrix) {
        int sum = 0;
        for (int[] row : matrix) {
            for (int element : row) {
                sum += element;
            }
        }
        return sum;
    }
    
    // Транспонирование матрицы
    public static int[][] transpose(int[][] matrix) {
        int rows = matrix.length;
        int cols = matrix[0].length;
        int[][] transposed = new int[cols][rows];
        
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                transposed[j][i] = matrix[i][j];
            }
        }
        return transposed;
    }
}

Сортировка массивов:

public class SortingExamples {
    // Сортировка пузырьком
    public static void bubbleSort(int[] array) {
        int n = array.length;
        for (int i = 0; i < n - 1; i++) {
            for (int j = 0; j < n - i - 1; j++) {
                if (array[j] > array[j + 1]) {
                    // Обмен элементов
                    int temp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                }
            }
        }
    }
    
    // Сортировка выбором
    public static void selectionSort(int[] array) {
        int n = array.length;
        for (int i = 0; i < n - 1; i++) {
            int minIndex = i;
            for (int j = i + 1; j < n; j++) {
                if (array[j] < array[minIndex]) {
                    minIndex = j;
                }
            }
            // Обмен элементов
            int temp = array[minIndex];
            array[minIndex] = array[i];
            array[i] = temp;
        }
    }
}

Частые ошибки и их избежание

ArrayIndexOutOfBoundsException:

int[] numbers = {1, 2, 3};

// НЕПРАВИЛЬНО
// int value = numbers[3]; // Исключение!

// ПРАВИЛЬНО
if (index >= 0 && index < numbers.length) {
    int value = numbers[index];
}

// Или используйте try-catch
try {
    int value = numbers[index];
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("Неверный индекс: " + index);
}

NullPointerException:

int[] numbers = null;

// НЕПРАВИЛЬНО
// int length = numbers.length; // Исключение!

// ПРАВИЛЬНО
if (numbers != null) {
    int length = numbers.length;
}

Сравнение массивов:

int[] array1 = {1, 2, 3};
int[] array2 = {1, 2, 3};

// НЕПРАВИЛЬНО
// boolean equal = (array1 == array2); // false - сравнивает ссылки

// ПРАВИЛЬНО
boolean equal = Arrays.equals(array1, array2); // true

Лучшие практики

  1. Используйте Arrays.toString() для вывода массивов
  2. Проверяйте границы массива перед обращением к элементам
  3. Используйте for-each для простого перебора без изменения элементов
  4. Предпочитайте коллекции для динамических данных
  5. Используйте Arrays.copyOf() вместо ручного копирования
  6. Инициализируйте массивы при объявлении, когда это возможно
  7. Используйте константы для размеров массивов
  8. Документируйте предположения о размере и содержимом массивов

Сравнение с коллекциями

Характеристика Массивы Коллекции
Размер Фиксированный Динамический
Типы данных Примитивы и объекты Только объекты
Производительность Высокая Немного ниже
Функциональность Ограниченная Богатая
Синтаксис Простой Более сложный
Безопасность типов Полная Полная (с generics)

Comparator и Comparable

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

Comparable и Comparator - это два интерфейса в Java для сравнения объектов и их сортировки.

Ключевые различия:

Характеристика Comparable Comparator
Пакет java.lang java.util
Метод compareTo(T o) compare(T o1, T o2)
Реализация В самом классе Отдельный класс/lambda
Количество способов сортировки Один (естественный) Множество
Изменение класса Требуется Не требуется

Интерфейс Comparable

Определение:

public interface Comparable<T> {
    int compareTo(T o);
}

Правила реализации compareTo():

  • Возвращает отрицательное число если this < other
  • Возвращает 0 если this == other
  • Возвращает положительное число если this > other

Базовый пример:

public class Person implements Comparable<Person> {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }
    
    // getters, toString...
}

Сложное сравнение (несколько полей):

public class Employee implements Comparable<Employee> {
    private String department;
    private String name;
    private int salary;
    
    public Employee(String department, String name, int salary) {
        this.department = department;
        this.name = name;
        this.salary = salary;
    }
    
    @Override
    public int compareTo(Employee other) {
        // Сначала по отделу
        int deptComparison = this.department.compareTo(other.department);
        if (deptComparison != 0) {
            return deptComparison;
        }
        
        // Затем по зарплате (по убыванию)
        int salaryComparison = Integer.compare(other.salary, this.salary);
        if (salaryComparison != 0) {
            return salaryComparison;
        }
        
        // Наконец по имени
        return this.name.compareTo(other.name);
    }
}

Использование Comparable:

List<Person> people = Arrays.asList(
    new Person("Alice", 30),
    new Person("Bob", 25),
    new Person("Charlie", 35)
);

// Сортировка с использованием естественного порядка
Collections.sort(people);
// Результат: [Bob(25), Alice(30), Charlie(35)]

// Или с Java 8+
people.sort(null); // null означает естественный порядок

Безопасная реализация с null:

public class SafePerson implements Comparable<SafePerson> {
    private String name;
    private Integer age;
    
    @Override
    public int compareTo(SafePerson other) {
        if (other == null) {
            return 1; // this больше null
        }
        
        // Сравнение с учетом null значений
        if (this.age == null && other.age == null) {
            return 0;
        }
        if (this.age == null) {
            return -1; // null меньше любого значения
        }
        if (other.age == null) {
            return 1; // любое значение больше null
        }
        
        return this.age.compareTo(other.age);
    }
}

Интерфейс Comparator

Определение:

public interface Comparator<T> {
    int compare(T o1, T o2);
    
    // Методы по умолчанию (Java 8+)
    default Comparator<T> reversed() { ... }
    default Comparator<T> thenComparing(...) { ... }
    // и другие...
}

Реализация через отдельный класс:

public class PersonAgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person p1, Person p2) {
        return Integer.compare(p1.getAge(), p2.getAge());
    }
}

public class PersonNameComparator implements Comparator<Person> {
    @Override
    public int compare(Person p1, Person p2) {
        return p1.getName().compareTo(p2.getName());
    }
}

Использование отдельных компараторов:

List<Person> people = Arrays.asList(
    new Person("Alice", 30),
    new Person("Bob", 25),
    new Person("Charlie", 35)
);

// Сортировка по возрасту
Collections.sort(people, new PersonAgeComparator());

// Сортировка по имени
Collections.sort(people, new PersonNameComparator());

Анонимные классы:

List<Person> people = getPersons();

// Сортировка по возрасту по убыванию
Collections.sort(people, new Comparator<Person>() {
    @Override
    public int compare(Person p1, Person p2) {
        return Integer.compare(p2.getAge(), p1.getAge());
    }
});

Lambda-выражения (Java 8+):

List<Person> people = getPersons();

// Сортировка по возрасту
people.sort((p1, p2) -> Integer.compare(p1.getAge(), p2.getAge()));

// Сортировка по имени
people.sort((p1, p2) -> p1.getName().compareTo(p2.getName()));

// Сортировка по возрасту по убыванию
people.sort((p1, p2) -> Integer.compare(p2.getAge(), p1.getAge()));

Method References (Java 8+):

List<Person> people = getPersons();

// Используя статические методы Comparator
people.sort(Comparator.comparing(Person::getName));
people.sort(Comparator.comparing(Person::getAge));
people.sort(Comparator.comparing(Person::getAge).reversed());

Статические методы Comparator (Java 8+)

Основные методы:

List<Person> people = getPersons();

// comparing() - сравнение по полю
people.sort(Comparator.comparing(Person::getName));
people.sort(Comparator.comparing(Person::getAge));

// comparingInt(), comparingLong(), comparingDouble() - для примитивов
people.sort(Comparator.comparingInt(Person::getAge));

// naturalOrder() - естественный порядок
people.sort(Comparator.naturalOrder()); // Требует Comparable

// reverseOrder() - обратный естественный порядок
people.sort(Comparator.reverseOrder());

// nullsFirst() и nullsLast() - обработка null
people.sort(Comparator.nullsFirst(Comparator.comparing(Person::getName)));
people.sort(Comparator.nullsLast(Comparator.comparing(Person::getName)));

Составные компараторы:

List<Employee> employees = getEmployees();

// Многоуровневая сортировка
employees.sort(
    Comparator.comparing(Employee::getDepartment)
        .thenComparing(Employee::getSalary)
        .thenComparing(Employee::getName)
);

// С обратным порядком для отдельных полей
employees.sort(
    Comparator.comparing(Employee::getDepartment)
        .thenComparing(Employee::getSalary, Comparator.reverseOrder())
        .thenComparing(Employee::getName)
);

// thenComparingInt(), thenComparingLong(), thenComparingDouble()
employees.sort(
    Comparator.comparing(Employee::getDepartment)
        .thenComparingInt(Employee::getSalary)
        .thenComparing(Employee::getName)
);

Практические примеры

Сортировка студентов:

public class Student {
    private String name;
    private int grade;
    private double gpa;
    
    // конструктор, геттеры...
}

List<Student> students = getStudents();

// По оценке, затем по GPA (по убыванию), затем по имени
students.sort(
    Comparator.comparingInt(Student::getGrade)
        .thenComparing(Student::getGpa, Comparator.reverseOrder())
        .thenComparing(Student::getName)
);

// Топ студенты (высший GPA, затем по имени)
students.sort(
    Comparator.comparing(Student::getGpa, Comparator.reverseOrder())
        .thenComparing(Student::getName)
);

Сортировка дат:

public class Event {
    private String name;
    private LocalDateTime dateTime;
    private int priority;
    
    // конструктор, геттеры...
}

List<Event> events = getEvents();

// По дате, затем по приоритету (высший приоритет первым)
events.sort(
    Comparator.comparing(Event::getDateTime)
        .thenComparing(Event::getPriority, Comparator.reverseOrder())
);

// Только будущие события, отсортированные по дате
LocalDateTime now = LocalDateTime.now();
events.stream()
    .filter(event -> event.getDateTime().isAfter(now))
    .sorted(Comparator.comparing(Event::getDateTime))
    .forEach(System.out::println);

Сортировка строк:

List<String> words = Arrays.asList("apple", "Banana", "cherry", "Date");

// Без учета регистра
words.sort(String.CASE_INSENSITIVE_ORDER);

// По длине, затем лексикографически
words.sort(
    Comparator.comparing(String::length)
        .thenComparing(Comparator.naturalOrder())
);

// По длине (по убыванию), затем лексикографически
words.sort(
    Comparator.comparing(String::length, Comparator.reverseOrder())
        .thenComparing(Comparator.naturalOrder())
);

Обработка null значений:

public class Product {
    private String name;
    private Double price; // может быть null
    private Integer rating; // может быть null
    
    // конструктор, геттеры...
}

List<Product> products = getProducts();

// null цены в конце, затем по возрастанию цены
products.sort(
    Comparator.comparing(Product::getPrice, 
        Comparator.nullsLast(Comparator.naturalOrder()))
);

// Сложная сортировка с null
products.sort(
    Comparator.comparing(Product::getName)
        .thenComparing(Product::getPrice, 
            Comparator.nullsLast(Comparator.reverseOrder()))
        .thenComparing(Product::getRating, 
            Comparator.nullsFirst(Comparator.reverseOrder()))
);

Использование с коллекциями

Arrays:

Person[] peopleArray = {
    new Person("Alice", 30),
    new Person("Bob", 25),
    new Person("Charlie", 35)
};

// С Comparable
Arrays.sort(peopleArray);

// С Comparator
Arrays.sort(peopleArray, Comparator.comparing(Person::getName));
Arrays.sort(peopleArray, (p1, p2) -> 
    Integer.compare(p2.getAge(), p1.getAge()));

TreeSet и TreeMap:

// TreeSet с естественным порядком (Comparable)
Set<Person> peopleSet = new TreeSet<>();

// TreeSet с Comparator
Set<Person> peopleByName = new TreeSet<>(
    Comparator.comparing(Person::getName));

// TreeMap с Comparator для ключей
Map<Person, String> peopleMap = new TreeMap<>(
    Comparator.comparing(Person::getAge)
        .thenComparing(Person::getName));

PriorityQueue:

// PriorityQueue с естественным порядком
Queue<Integer> numbers = new PriorityQueue<>();

// PriorityQueue с Comparator (максимальная куча)
Queue<Integer> maxHeap = new PriorityQueue<>(
    Comparator.reverseOrder());

// Для сложных объектов
Queue<Person> peopleQueue = new PriorityQueue<>(
    Comparator.comparing(Person::getAge));

Stream API и сортировка

Основные операции:

List<Person> people = getPersons();

// sorted() с естественным порядком
List<Person> sorted = people.stream()
    .sorted()
    .collect(Collectors.toList());

// sorted() с Comparator
List<Person> sortedByName = people.stream()
    .sorted(Comparator.comparing(Person::getName))
    .collect(Collectors.toList());

// Сложная сортировка и фильтрация
List<Person> result = people.stream()
    .filter(p -> p.getAge() >= 18)
    .sorted(Comparator.comparing(Person::getAge)
        .thenComparing(Person::getName))
    .limit(10)
    .collect(Collectors.toList());

min() и max():

List<Person> people = getPersons();

// Поиск минимума и максимума
Optional<Person> youngest = people.stream()
    .min(Comparator.comparing(Person::getAge));

Optional<Person> oldest = people.stream()
    .max(Comparator.comparing(Person::getAge));

// Самое длинное имя
Optional<Person> longestName = people.stream()
    .max(Comparator.comparing(p -> p.getName().length()));

Лучшие практики

1. Константы для часто используемых компараторов:

public class PersonComparators {
    public static final Comparator<Person> BY_AGE = 
        Comparator.comparing(Person::getAge);
    
    public static final Comparator<Person> BY_NAME = 
        Comparator.comparing(Person::getName);
    
    public static final Comparator<Person> BY_AGE_DESC = 
        Comparator.comparing(Person::getAge, Comparator.reverseOrder());
        
    public static final Comparator<Person> BY_AGE_THEN_NAME = 
        BY_AGE.thenComparing(BY_NAME);
}

// Использование
people.sort(PersonComparators.BY_AGE_THEN_NAME);

2. Безопасная работа с null:

// ПЛОХО - может выбросить NullPointerException
Comparator.comparing(Person::getName)

// ХОРОШО - безопасно с null
Comparator.comparing(Person::getName, 
    Comparator.nullsLast(Comparator.naturalOrder()))

// Или использовать утилитные методы
public static <T> Comparator<T> nullSafeComparing(
        Function<T, String> keyExtractor) {
    return Comparator.comparing(keyExtractor, 
        Comparator.nullsLast(Comparator.naturalOrder()));
}

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

// Для примитивных типов используйте специализированные методы
Comparator.comparingInt(Person::getAge)      // Лучше чем
Comparator.comparing(Person::getAge)         // это

// Для больших объемов данных рассмотрите параллельную сортировку
people.parallelStream()
    .sorted(Comparator.comparing(Person::getAge))
    .collect(Collectors.toList());

4. Читаемость кода:

// ПЛОХО - сложно читать
people.sort((p1, p2) -> {
    int ageComp = Integer.compare(p1.getAge(), p2.getAge());
    if (ageComp != 0) return ageComp;
    return p1.getName().compareTo(p2.getName());
});

// ХОРОШО - ясно и понятно
people.sort(
    Comparator.comparing(Person::getAge)
        .thenComparing(Person::getName)
);

Контракт сравнения

Требования для корректной реализации:

  1. Антирефлексивность: compare(a, a) == 0
  2. Антисимметричность: если compare(a, b) > 0, то compare(b, a) < 0
  3. Транзитивность: если compare(a, b) > 0 и compare(b, c) > 0, то compare(a, c) > 0
  4. Согласованность с equals: если compare(a, b) == 0, желательно чтобы a.equals(b) == true

Пример нарушения контракта:

// НЕПРАВИЛЬНО - нарушает транзитивность
public class BadComparator implements Comparator<Integer> {
    @Override
    public int compare(Integer a, Integer b) {
        // Некорректная логика
        return (a - b) % 3; // Может быть 0, 1, или 2
    }
}

Часто встречающиеся ошибки

1. Переполнение при вычитании:

// НЕПРАВИЛЬНО - может переполниться
public int compareTo(Person other) {
    return this.age - other.age; // Опасно!
}

// ПРАВИЛЬНО
public int compareTo(Person other) {
    return Integer.compare(this.age, other.age);
}

2. Неправильная обработка null:

// НЕПРАВИЛЬНО
public int compare(String s1, String s2) {
    return s1.compareTo(s2); // NullPointerException если s1 или s2 null
}

// ПРАВИЛЬНО
public int compare(String s1, String s2) {
    if (s1 == null && s2 == null) return 0;
    if (s1 == null) return -1;
    if (s2 == null) return 1;
    return s1.compareTo(s2);
}

3. Нарушение согласованности с equals:

// ПРОБЛЕМАТИЧНО
public class Person implements Comparable<Person> {
    private String name;
    private int age;
    
    @Override
    public int compareTo(Person other) {
        return this.name.compareTo(other.name); // Только по имени
    }
    
    @Override
    public boolean equals(Object obj) {
        // Сравнивает по имени И возрасту
        Person other = (Person) obj;
        return this.name.equals(other.name) && this.age == other.age;
    }
}

Эта шпаргалка поможет эффективно использовать Comparable и Comparator для сортировки и сравнения объектов в Java.

Collections Framework

Иерархия коллекций

Collection (interface)
├── List (interface)
│   ├── ArrayList
│   ├── LinkedList
│   └── Vector
│       └── Stack
├── Set (interface)
│   ├── HashSet
│   │   └── LinkedHashSet
│   ├── TreeSet
│   └── EnumSet
└── Queue (interface)
    ├── PriorityQueue
    ├── ArrayDeque
    └── Deque (interface)
        ├── ArrayDeque
        └── LinkedList

Map (interface) - отдельная иерархия
├── HashMap
│   └── LinkedHashMap
├── TreeMap
├── Hashtable
├── WeakHashMap
├── IdentityHashMap
└── EnumMap

Интерфейс Collection

Основные методы:

Collection<String> collection = new ArrayList<>();

// Добавление элементов
boolean add(E element)
boolean addAll(Collection<? extends E> c)

// Удаление элементов
boolean remove(Object o)
boolean removeAll(Collection<?> c)
boolean retainAll(Collection<?> c)  // оставляет только пересечение
void clear()

// Проверка содержимого
boolean contains(Object o)
boolean containsAll(Collection<?> c)
boolean isEmpty()
int size()

// Преобразование
Object[] toArray()
<T> T[] toArray(T[] a)

// Итерация
Iterator<E> iterator()

Примеры использования:

Collection<String> fruits = new ArrayList<>();
fruits.add("apple");
fruits.add("banana");
fruits.add("orange");

// Проверки
boolean hasApple = fruits.contains("apple");     // true
boolean empty = fruits.isEmpty();                // false
int count = fruits.size();                       // 3

// Массовые операции
Collection<String> moreFruits = Arrays.asList("grape", "kiwi");
fruits.addAll(moreFruits);

// Преобразование в массив
String[] array = fruits.toArray(new String[0]);

List Interface

Характеристики:

  • Упорядоченная коллекция (последовательность)
  • Разрешает дубликаты
  • Доступ по индексу

Дополнительные методы List:

// Доступ по индексу
E get(int index)
E set(int index, E element)
void add(int index, E element)
E remove(int index)

// Поиск
int indexOf(Object o)
int lastIndexOf(Object o)

// Подсписки
List<E> subList(int fromIndex, int toIndex)

// Итераторы
ListIterator<E> listIterator()
ListIterator<E> listIterator(int index)

ArrayList

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

// Особенности:
// - Основан на массиве
// - Быстрый доступ по индексу O(1)
// - Медленные вставки/удаления в середине O(n)
// - Не синхронизирован

// Создание с начальной емкостью
List<String> list = new ArrayList<>(100);

// Создание из другой коллекции
List<String> copy = new ArrayList<>(existingList);

// Примеры операций
arrayList.add("first");
arrayList.add(0, "zero");          // Вставка по индексу
String element = arrayList.get(1);  // Получение по индексу
arrayList.set(0, "new zero");      // Замена элемента

LinkedList

List<String> linkedList = new LinkedList<>();

// Особенности:
// - Основан на двусвязном списке
// - Медленный доступ по индексу O(n)
// - Быстрые вставки/удаления O(1)
// - Реализует также Deque

// Методы как Deque
linkedList.addFirst("first");
linkedList.addLast("last");
String first = linkedList.removeFirst();
String last = linkedList.removeLast();

// Использование как стек
linkedList.push("top");
String top = linkedList.pop();

// Использование как очередь
linkedList.offer("element");
String head = linkedList.poll();

Vector и Stack

// Vector - устаревший, синхронизированный ArrayList
Vector<String> vector = new Vector<>();

// Stack - устаревший, наследует Vector
Stack<String> stack = new Stack<>();
stack.push("item");
String item = stack.pop();
String peek = stack.peek();     // Без удаления
boolean empty = stack.empty();

// Лучше использовать ArrayDeque как стек
Deque<String> modernStack = new ArrayDeque<>();

Set Interface

Характеристики:

  • Не допускает дубликаты
  • Основан на equals() и hashCode()

HashSet

Set<String> hashSet = new HashSet<>();

// Особенности:
// - Основан на хеш-таблице
// - Быстрые операции O(1) в среднем
// - Не гарантирует порядок
// - Разрешает один null

hashSet.add("apple");
hashSet.add("banana");
hashSet.add("apple");           // Дубликат - не добавится

boolean contains = hashSet.contains("apple");   // true
boolean removed = hashSet.remove("banana");     // true

// Создание из другой коллекции
Set<String> fromList = new HashSet<>(Arrays.asList("a", "b", "c", "a"));
// Результат: ["a", "b", "c"] - дубликаты удалены

LinkedHashSet

Set<String> linkedHashSet = new LinkedHashSet<>();

// Особенности:
// - Поддерживает порядок вставки
// - Немного медленнее HashSet
// - Полезен когда нужен и Set, и порядок

linkedHashSet.add("first");
linkedHashSet.add("second");
linkedHashSet.add("third");
// Порядок итерации: first, second, third

TreeSet

Set<String> treeSet = new TreeSet<>();

// Особенности:
// - Основан на красно-черном дереве
// - Элементы всегда отсортированы
// - Операции O(log n)
// - Требует Comparable или Comparator

treeSet.add("banana");
treeSet.add("apple");
treeSet.add("cherry");
// Порядок итерации: apple, banana, cherry

// С компаратором
Set<String> reversedSet = new TreeSet<>(Comparator.reverseOrder());

// Дополнительные методы TreeSet
TreeSet<Integer> numbers = new TreeSet<>();
numbers.addAll(Arrays.asList(5, 2, 8, 1, 9));

Integer first = numbers.first();        // 1
Integer last = numbers.last();          // 9
Integer lower = numbers.lower(5);       // 2 (меньше 5)
Integer higher = numbers.higher(5);     // 8 (больше 5)
Integer floor = numbers.floor(6);       // 5 (≤ 6)
Integer ceiling = numbers.ceiling(6);   // 8 (≥ 6)

// Подмножества
SortedSet<Integer> head = numbers.headSet(5);      // [1, 2]
SortedSet<Integer> tail = numbers.tailSet(5);      // [5, 8, 9]
SortedSet<Integer> sub = numbers.subSet(2, 8);     // [2, 5]

Queue Interface

Характеристики:

  • FIFO (First In, First Out) по умолчанию
  • Два набора методов: throw exception vs return special value
Операция Throws Exception Returns Special Value
Insert add(e) offer(e)
Remove remove() poll()
Examine element() peek()

PriorityQueue

Queue<Integer> priorityQueue = new PriorityQueue<>();

// Особенности:
// - Куча (heap) структура
// - Элементы упорядочены по приоритету
// - Не разрешает null
// - Не потокобезопасна

priorityQueue.offer(5);
priorityQueue.offer(2);
priorityQueue.offer(8);
priorityQueue.offer(1);

// Извлечение в порядке приоритета
while (!priorityQueue.isEmpty()) {
    System.out.println(priorityQueue.poll()); // 1, 2, 5, 8
}

// С компаратором (максимальная куча)
Queue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());

// Для сложных объектов
Queue<Task> taskQueue = new PriorityQueue<>(
    Comparator.comparing(Task::getPriority).reversed()
);

ArrayDeque

Deque<String> deque = new ArrayDeque<>();

// Особенности:
// - Основан на массиве
// - Может работать как стек и очередь
// - Быстрее Stack и LinkedList
// - Не разрешает null

// Как очередь (FIFO)
deque.addLast("first");
deque.addLast("second");
String head = deque.removeFirst();

// Как стек (LIFO)
deque.addFirst("top");
String top = deque.removeFirst();

// Альтернативные методы
deque.push("item");     // addFirst()
String item = deque.pop();      // removeFirst()

// Безопасные методы (возвращают null вместо исключения)
deque.offer("element");         // addLast()
String polled = deque.poll();   // removeFirst()
String peeked = deque.peek();   // peekFirst()

Map Interface

Характеристики:

  • Ключ-значение пары
  • Уникальные ключи
  • Один null ключ (в большинстве реализаций)

Основные методы:

Map<String, Integer> map = new HashMap<>();

// Добавление и обновление
V put(K key, V value)
V putIfAbsent(K key, V value)
void putAll(Map<? extends K, ? extends V> m)

// Получение
V get(Object key)
V getOrDefault(Object key, V defaultValue)

// Удаление
V remove(Object key)
boolean remove(Object key, Object value)
void clear()

// Проверки
boolean containsKey(Object key)
boolean containsValue(Object value)
boolean isEmpty()
int size()

// Представления
Set<K> keySet()
Collection<V> values()
Set<Map.Entry<K, V>> entrySet()

HashMap

Map<String, Integer> hashMap = new HashMap<>();

// Особенности:
// - Основан на хеш-таблице
// - Быстрые операции O(1) в среднем
// - Не гарантирует порядок
// - Разрешает один null ключ и null значения

hashMap.put("apple", 5);
hashMap.put("banana", 3);
hashMap.put("orange", 7);

Integer apples = hashMap.get("apple");           // 5
Integer grapes = hashMap.getOrDefault("grape", 0); // 0

// Java 8+ методы
hashMap.putIfAbsent("kiwi", 2);
hashMap.replace("apple", 5, 6);                  // заменит только если значение = 5
hashMap.computeIfAbsent("mango", k -> k.length()); // вычислит значение если ключа нет
hashMap.merge("apple", 1, Integer::sum);         // объединит значения

// Итерация
for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
    System.out.println(entry.getKey() + " = " + entry.getValue());
}

// Java 8+ forEach
hashMap.forEach((key, value) -> 
    System.out.println(key + " = " + value));

LinkedHashMap

Map<String, Integer> linkedHashMap = new LinkedHashMap<>();

// Особенности:
// - Поддерживает порядок вставки
// - Или порядок доступа (при accessOrder = true)

// Порядок доступа (LRU cache)
Map<String, Integer> lruCache = new LinkedHashMap<String, Integer>(16, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {
        return size() > 100; // Максимум 100 элементов
    }
};

TreeMap

Map<String, Integer> treeMap = new TreeMap<>();

// Особенности:
// - Основан на красно-черном дереве
// - Ключи всегда отсортированы
// - Операции O(log n)
// - Требует Comparable ключи или Comparator

treeMap.put("banana", 3);
treeMap.put("apple", 5);
treeMap.put("cherry", 2);
// Порядок: apple, banana, cherry

// Дополнительные методы TreeMap
TreeMap<Integer, String> numbers = new TreeMap<>();
numbers.put(1, "one");
numbers.put(5, "five");
numbers.put(3, "three");
numbers.put(8, "eight");

Map.Entry<Integer, String> first = numbers.firstEntry();    // 1=one
Map.Entry<Integer, String> last = numbers.lastEntry();      // 8=eight
Integer lowerKey = numbers.lowerKey(5);                     // 3
Integer higherKey = numbers.higherKey(5);                   // 8

// Подкарты
SortedMap<Integer, String> head = numbers.headMap(5);       // {1=one, 3=three}
SortedMap<Integer, String> tail = numbers.tailMap(5);       // {5=five, 8=eight}
SortedMap<Integer, String> sub = numbers.subMap(3, 8);      // {3=three, 5=five}

Утилитарный класс Collections

Сортировка:

List<String> list = Arrays.asList("c", "a", "b");

// Естественная сортировка
Collections.sort(list);                          // [a, b, c]

// С компаратором
Collections.sort(list, Comparator.reverseOrder()); // [c, b, a]

// Перемешивание
Collections.shuffle(list);

// Обращение
Collections.reverse(list);

Поиск:

List<Integer> numbers = Arrays.asList(1, 3, 5, 7, 9);

// Бинарный поиск (список должен быть отсортирован)
int index = Collections.binarySearch(numbers, 5);   // 2
int notFound = Collections.binarySearch(numbers, 4); // -3

// Минимум и максимум
Integer min = Collections.min(numbers);              // 1
Integer max = Collections.max(numbers);              // 9

// С компаратором
String longest = Collections.max(words, 
    Comparator.comparing(String::length));

Заполнение и замена:

List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));

// Заполнение
Collections.fill(list, "x");                    // [x, x, x]

// Замена
Collections.replaceAll(list, "x", "y");         // [y, y, y]

// Копирование
List<String> dest = new ArrayList<>(Collections.nCopies(list.size(), ""));
Collections.copy(dest, list);

// Поворот
Collections.rotate(list, 1);                    // последний элемент становится первым

Неизменяемые коллекции:

List<String> list = Arrays.asList("a", "b", "c");

// Неизменяемые обертки
List<String> unmodifiableList = Collections.unmodifiableList(list);
Set<String> unmodifiableSet = Collections.unmodifiableSet(new HashSet<>(list));
Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map);

// Пустые неизменяемые коллекции
List<String> emptyList = Collections.emptyList();
Set<String> emptySet = Collections.emptySet();
Map<String, Integer> emptyMap = Collections.emptyMap();

// Одноэлементные неизменяемые коллекции
List<String> singletonList = Collections.singletonList("only");
Set<String> singletonSet = Collections.singleton("only");
Map<String, Integer> singletonMap = Collections.singletonMap("key", 1);

Синхронизированные коллекции:

List<String> list = new ArrayList<>();
Set<String> set = new HashSet<>();
Map<String, Integer> map = new HashMap<>();

// Синхронизированные обертки
List<String> syncList = Collections.synchronizedList(list);
Set<String> syncSet = Collections.synchronizedSet(set);
Map<String, Integer> syncMap = Collections.synchronizedMap(map);

// ВАЖНО: Для итерации нужна внешняя синхронизация
synchronized (syncList) {
    for (String item : syncList) {
        // обработка
    }
}

Concurrent Collections (java.util.concurrent)

ConcurrentHashMap:

ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();

// Особенности:
// - Потокобезопасная
// - Сегментированная блокировка
// - Не разрешает null ключи и значения

concurrentMap.put("key", 1);

// Атомарные операции
concurrentMap.putIfAbsent("key2", 2);
concurrentMap.replace("key", 1, 10);
concurrentMap.compute("key", (k, v) -> v == null ? 1 : v + 1);
concurrentMap.merge("key", 1, Integer::sum);

CopyOnWriteArrayList:

CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();

// Особенности:
// - Копирует массив при каждой модификации
// - Отлично для чтения, плохо для записи
// - Итераторы не выбрасывают ConcurrentModificationException

cowList.add("item1");
cowList.add("item2");

// Безопасная итерация без блокировок
for (String item : cowList) {
    // Можно безопасно модифицировать список в другом потоке
}

BlockingQueue реализации:

// ArrayBlockingQueue - ограниченная очередь
BlockingQueue<String> arrayQueue = new ArrayBlockingQueue<>(10);

// LinkedBlockingQueue - опционально ограниченная
BlockingQueue<String> linkedQueue = new LinkedBlockingQueue<>();

// PriorityBlockingQueue - приоритетная неограниченная
BlockingQueue<Task> priorityQueue = new PriorityBlockingQueue<>();

// Блокирующие операции
try {
    arrayQueue.put("item");          // Блокируется если очередь полная
    String item = arrayQueue.take();  // Блокируется если очередь пустая
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

// Неблокирующие операции с таймаутом
boolean added = arrayQueue.offer("item", 1, TimeUnit.SECONDS);
String polled = arrayQueue.poll(1, TimeUnit.SECONDS);

Java 9+ Factory Methods

Неизменяемые коллекции:

// List
List<String> list = List.of("a", "b", "c");
List<Integer> numbers = List.of(1, 2, 3, 4, 5);

// Set
Set<String> set = Set.of("apple", "banana", "cherry");

// Map
Map<String, Integer> map = Map.of(
    "apple", 5,
    "banana", 3,
    "cherry", 8
);

// Map с большим количеством элементов
Map<String, Integer> bigMap = Map.ofEntries(
    Map.entry("key1", 1),
    Map.entry("key2", 2),
    Map.entry("key3", 3)
);

Выбор подходящей коллекции

Когда использовать List:

  • Нужен доступ по индексу
  • Важен порядок элементов
  • Допустимы дубликаты
  • ArrayList: частые чтения, редкие вставки/удаления
  • LinkedList: частые вставки/удаления, редкие обращения по индексу

Когда использовать Set:

  • Нужны уникальные элементы
  • HashSet: быстрые операции, порядок не важен
  • LinkedHashSet: нужен порядок вставки
  • TreeSet: нужна сортировка

Когда использовать Queue/Deque:

  • FIFO или LIFO обработка
  • PriorityQueue: нужна приоритизация
  • ArrayDeque: стек или обычная очередь

Когда использовать Map:

  • Связь ключ-значение
  • HashMap: быстрые операции, порядок не важен
  • LinkedHashMap: нужен порядок или LRU cache
  • TreeMap: нужна сортировка по ключам

Сложность операций

Коллекция get/containsKey add/put remove iterate
ArrayList O(1) O(1)* O(n) O(n)
LinkedList O(n) O(1) O(1)** O(n)
HashSet/HashMap O(1) O(1) O(1) O(n)
TreeSet/TreeMap O(log n) O(log n) O(log n) O(n)
ArrayDeque O(1) O(1)* O(1)** O(n)

*амортизированная сложность
**если известна позиция

Лучшие практики

1. Используйте интерфейсы в объявлениях:

// ХОРОШО
List<String> list = new ArrayList<>();
Set<Integer> set = new HashSet<>();
Map<String, Object> map = new HashMap<>();

// ПЛОХО
ArrayList<String> list = new ArrayList<>();
HashMap<String, Object> map = new HashMap<>();

2. Задавайте начальную емкость:

// Если знаете примерный размер
List<String> list = new ArrayList<>(1000);
Set<String> set = new HashSet<>(500);
Map<String, Integer> map = new HashMap<>(200);

3. Правильно реализуйте equals() и hashCode():

public class Person {
    private String name;
    private int age;
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return age == person.age && Objects.equals(name, person.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

4. Используйте try-with-resources для потоков:

// При работе с потоками из коллекций
try (Stream<String> stream = list.stream()) {
    stream.filter(s -> s.length() > 5)
          .forEach(System.out::println);
}

5. Избегайте ConcurrentModificationException:

List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));

// НЕПРАВИЛЬНО
for (String item : list) {
    if (item.equals("b")) {
        list.remove(item); // ConcurrentModificationException!
    }
}

// ПРАВИЛЬНО
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    if (iterator.next().equals("b")) {
        iterator.remove();
    }
}

// ИЛИ с Java 8+
list.removeIf(item -> item.equals("b"));

Эта шпаргалка покрывает основные аспекты работы с коллекциями в Java и поможет выбрать подходящую структуру данных для конкретной задачи.

HashMap и TreeMap

Сравнение HashMap и TreeMap

Характеристика HashMap TreeMap
Структура данных Хеш-таблица Красно-черное дерево
Сложность операций O(1) средняя, O(n) худшая O(log n)
Порядок элементов Не гарантирован Отсортированы по ключу
Null ключи Один null ключ Не допускает null ключи
Null значения Допускает Допускает
Требования к ключам equals() и hashCode() Comparable или Comparator
Потокобезопасность Нет Нет
Интерфейс Map NavigableMap, SortedMap

HashMap

Основы и создание

// Создание HashMap
Map<String, Integer> hashMap = new HashMap<>();

// С начальной емкостью
Map<String, Integer> map = new HashMap<>(16);

// С емкостью и load factor
Map<String, Integer> map2 = new HashMap<>(16, 0.75f);

// Из другой Map
Map<String, Integer> copy = new HashMap<>(existingMap);

// Java 9+ - factory методы
Map<String, Integer> immutable = Map.of("key1", 1, "key2", 2);

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

Map<String, Integer> map = new HashMap<>();

// Добавление элементов
map.put("apple", 5);
map.put("banana", 3);
map.put("orange", 7);

// Получение элементов
Integer apples = map.get("apple");                    // 5
Integer grapes = map.get("grape");                    // null
Integer grapesDefault = map.getOrDefault("grape", 0); // 0

// Проверка существования
boolean hasApple = map.containsKey("apple");          // true
boolean hasValue5 = map.containsValue(5);             // true

// Удаление
Integer removed = map.remove("banana");               // 3
boolean removedConditional = map.remove("apple", 5);  // true (удалит если значение = 5)

// Размер и проверка на пустоту
int size = map.size();                                // количество элементов
boolean empty = map.isEmpty();                        // false

Java 8+ методы

Map<String, Integer> map = new HashMap<>();
map.put("apple", 5);
map.put("banana", 3);

// putIfAbsent - добавляет только если ключа нет
map.putIfAbsent("cherry", 2);                        // добавит
map.putIfAbsent("apple", 10);                        // не добавит, ключ уже есть

// replace - заменяет значение
map.replace("apple", 6);                             // заменит на 6
map.replace("apple", 6, 8);                          // заменит только если текущее значение = 6

// compute - вычисляет новое значение
map.compute("apple", (key, value) -> value == null ? 1 : value + 1);

// computeIfAbsent - вычисляет значение если ключа нет
map.computeIfAbsent("kiwi", key -> key.length());    // добавит "kiwi" -> 4

// computeIfPresent - вычисляет значение если ключ есть
map.computeIfPresent("apple", (key, value) -> value * 2);

// merge - объединяет значения
map.merge("apple", 1, Integer::sum);                 // прибавит 1 к текущему значению
map.merge("mango", 5, Integer::sum);                 // добавит новый ключ со значением 5

Итерация по HashMap

Map<String, Integer> map = new HashMap<>();
map.put("apple", 5);
map.put("banana", 3);
map.put("orange", 7);

// Итерация по ключам
for (String key : map.keySet()) {
    System.out.println("Key: " + key + ", Value: " + map.get(key));
}

// Итерация по значениям
for (Integer value : map.values()) {
    System.out.println("Value: " + value);
}

// Итерация по парам ключ-значение
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + " = " + entry.getValue());
}

// Java 8+ forEach
map.forEach((key, value) -> 
    System.out.println(key + " = " + value));

// Stream API
map.entrySet().stream()
    .filter(entry -> entry.getValue() > 3)
    .forEach(entry -> System.out.println(entry.getKey()));

Работа с коллизиями и производительность

// Пример плохого hashCode (много коллизий)
class BadKey {
    private String value;
    
    @Override
    public int hashCode() {
        return 1; // Все объекты будут в одном bucket!
    }
}

// Хороший hashCode
class GoodKey {
    private String name;
    private int id;
    
    @Override
    public int hashCode() {
        return Objects.hash(name, id);
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        GoodKey goodKey = (GoodKey) obj;
        return id == goodKey.id && Objects.equals(name, goodKey.name);
    }
}

// Настройка производительности
Map<String, Integer> optimizedMap = new HashMap<>(
    1000,  // начальная емкость (ожидаемое количество элементов / 0.75)
    0.75f  // load factor (по умолчанию 0.75)
);

LinkedHashMap - сохранение порядка

// Порядок вставки
Map<String, Integer> insertionOrder = new LinkedHashMap<>();

// Порядок доступа (LRU)
Map<String, Integer> accessOrder = new LinkedHashMap<>(16, 0.75f, true);

// LRU Cache реализация
class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int maxSize;
    
    public LRUCache(int maxSize) {
        super(16, 0.75f, true);
        this.maxSize = maxSize;
    }
    
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > maxSize;
    }
}

LRUCache<String, String> cache = new LRUCache<>(100);

TreeMap

Основы и создание

// Создание TreeMap с естественным порядком
Map<String, Integer> treeMap = new TreeMap<>();

// С компаратором
Map<String, Integer> reverseMap = new TreeMap<>(Comparator.reverseOrder());

// Для пользовательских объектов
Map<Person, String> personMap = new TreeMap<>(
    Comparator.comparing(Person::getAge)
        .thenComparing(Person::getName)
);

// Из другой Map
Map<String, Integer> copy = new TreeMap<>(existingMap);

// Из SortedMap
TreeMap<String, Integer> treeCopy = new TreeMap<>(existingSortedMap);

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

TreeMap<String, Integer> map = new TreeMap<>();

// Добавление элементов (автоматически сортируются)
map.put("banana", 3);
map.put("apple", 5);
map.put("cherry", 2);
map.put("date", 8);

// Порядок в TreeMap: apple, banana, cherry, date

// Основные операции аналогичны HashMap
Integer value = map.get("apple");
map.put("elderberry", 4);
map.remove("banana");

Навигационные методы

TreeMap<Integer, String> numbers = new TreeMap<>();
numbers.put(1, "one");
numbers.put(3, "three");
numbers.put(5, "five");
numbers.put(7, "seven");
numbers.put(9, "nine");

// Получение крайних элементов
Map.Entry<Integer, String> first = numbers.firstEntry();     // 1=one
Map.Entry<Integer, String> last = numbers.lastEntry();       // 9=nine

Integer firstKey = numbers.firstKey();                       // 1
Integer lastKey = numbers.lastKey();                         // 9

// Поиск ближайших элементов
Integer lower = numbers.lowerKey(5);                         // 3 (строго меньше 5)
Integer floor = numbers.floorKey(4);                         // 3 (меньше или равно 4)
Integer ceiling = numbers.ceilingKey(4);                     // 5 (больше или равно 4)
Integer higher = numbers.higherKey(5);                       // 7 (строго больше 5)

// Entry версии
Map.Entry<Integer, String> lowerEntry = numbers.lowerEntry(5);
Map.Entry<Integer, String> higherEntry = numbers.higherEntry(5);

// Удаление крайних элементов
Map.Entry<Integer, String> removedFirst = numbers.pollFirstEntry();
Map.Entry<Integer, String> removedLast = numbers.pollLastEntry();

Подкарты (SubMaps)

TreeMap<Integer, String> map = new TreeMap<>();
map.put(1, "one");
map.put(3, "three");
map.put(5, "five");
map.put(7, "seven");
map.put(9, "nine");

// Подкарты
SortedMap<Integer, String> headMap = map.headMap(5);         // ключи < 5: {1=one, 3=three}
SortedMap<Integer, String> tailMap = map.tailMap(5);         // ключи >= 5: {5=five, 7=seven, 9=nine}
SortedMap<Integer, String> subMap = map.subMap(3, 8);        // 3 <= ключи < 8: {3=three, 5=five, 7=seven}

// NavigableMap подкарты (более гибкие)
NavigableMap<Integer, String> headMapInclusive = map.headMap(5, true);   // ключи <= 5
NavigableMap<Integer, String> tailMapExclusive = map.tailMap(5, false);  // ключи > 5
NavigableMap<Integer, String> subMapExclusive = map.subMap(3, false, 8, false); // 3 < ключи < 8

// Обратные карты
NavigableMap<Integer, String> descendingMap = map.descendingMap();
NavigableSet<Integer> descendingKeys = map.descendingKeySet();

Итерация по TreeMap

TreeMap<String, Integer> map = new TreeMap<>();
map.put("banana", 3);
map.put("apple", 5);
map.put("cherry", 2);

// Обычная итерация (в отсортированном порядке)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + " = " + entry.getValue());
}
// Вывод: apple=5, banana=3, cherry=2

// Обратная итерация
for (Map.Entry<String, Integer> entry : map.descendingMap().entrySet()) {
    System.out.println(entry.getKey() + " = " + entry.getValue());
}
// Вывод: cherry=2, banana=3, apple=5

// Итерация по ключам в обратном порядке
for (String key : map.descendingKeySet()) {
    System.out.println(key + " = " + map.get(key));
}

Пользовательские компараторы

// Сортировка по длине строки
TreeMap<String, Integer> byLength = new TreeMap<>(
    Comparator.comparing(String::length)
        .thenComparing(Comparator.naturalOrder())
);

// Сортировка чисел как строк
TreeMap<Integer, String> numberAsString = new TreeMap<>(
    Comparator.comparing(Object::toString)
);

// Для пользовательских объектов
class Employee {
    private String name;
    private int salary;
    private String department;
    
    // конструктор, геттеры...
}

// Сложный компаратор
TreeMap<Employee, String> employees = new TreeMap<>(
    Comparator.comparing(Employee::getDepartment)
        .thenComparing(Employee::getSalary, Comparator.reverseOrder())
        .thenComparing(Employee::getName)
);

Практические примеры

Подсчет частоты слов

public Map<String, Integer> countWords(String text) {
    Map<String, Integer> wordCount = new HashMap<>();
    
    String[] words = text.toLowerCase().split("\\W+");
    for (String word : words) {
        if (!word.isEmpty()) {
            wordCount.merge(word, 1, Integer::sum);
        }
    }
    
    return wordCount;
}

// Использование
String text = "hello world hello java world";
Map<String, Integer> counts = countWords(text);
// Результат: {hello=2, world=2, java=1}

Группировка данных

class Student {
    private String name;
    private String grade;
    private int score;
    
    // конструктор, геттеры...
}

public Map<String, List<Student>> groupByGrade(List<Student> students) {
    Map<String, List<Student>> grouped = new HashMap<>();
    
    for (Student student : students) {
        grouped.computeIfAbsent(student.getGrade(), k -> new ArrayList<>())
                .add(student);
    }
    
    return grouped;
}

// С Stream API
Map<String, List<Student>> grouped = students.stream()
    .collect(Collectors.groupingBy(Student::getGrade));

Кэш с временными метками

class TimestampedValue<T> {
    private final T value;
    private final long timestamp;
    
    public TimestampedValue(T value) {
        this.value = value;
        this.timestamp = System.currentTimeMillis();
    }
    
    public boolean isExpired(long ttlMillis) {
        return System.currentTimeMillis() - timestamp > ttlMillis;
    }
    
    // геттеры...
}

class ExpiringCache<K, V> {
    private final Map<K, TimestampedValue<V>> cache = new HashMap<>();
    private final long ttlMillis;
    
    public ExpiringCache(long ttlMillis) {
        this.ttlMillis = ttlMillis;
    }
    
    public void put(K key, V value) {
        cache.put(key, new TimestampedValue<>(value));
    }
    
    public V get(K key) {
        TimestampedValue<V> timestamped = cache.get(key);
        if (timestamped == null || timestamped.isExpired(ttlMillis)) {
            cache.remove(key);
            return null;
        }
        return timestamped.getValue();
    }
}

Топ-K элементов с TreeMap

public class TopK<T> {
    private final TreeMap<T, Integer> countMap;
    private final int k;
    
    public TopK(int k, Comparator<T> comparator) {
        this.k = k;
        this.countMap = new TreeMap<>(comparator);
    }
    
    public void add(T item) {
        countMap.merge(item, 1, Integer::sum);
    }
    
    public List<Map.Entry<T, Integer>> getTopK() {
        return countMap.entrySet().stream()
            .sorted(Map.Entry.<T, Integer>comparingByValue().reversed())
            .limit(k)
            .collect(Collectors.toList());
    }
}

// Использование
TopK<String> topWords = new TopK<>(5, String::compareTo);
// добавляем слова...
List<Map.Entry<String, Integer>> top5 = topWords.getTopK();

Интервальная карта (Range Map)

class RangeMap<V> {
    private final TreeMap<Integer, V> map = new TreeMap<>();
    
    public void put(int start, int end, V value) {
        // Упрощенная реализация
        for (int i = start; i < end; i++) {
            map.put(i, value);
        }
    }
    
    public V get(int point) {
        return map.get(point);
    }
    
    public Map.Entry<Integer, V> getFloorEntry(int point) {
        return map.floorEntry(point);
    }
    
    public Map<Integer, V> getRange(int start, int end) {
        return map.subMap(start, end);
    }
}

// Использование
RangeMap<String> schedule = new RangeMap<>();
schedule.put(9, 12, "Meeting");
schedule.put(14, 16, "Development");
schedule.put(16, 17, "Testing");

String activity = schedule.get(15); // "Development"

Производительность и оптимизация

HashMap оптимизация

// Правильный размер для избежания resize
int expectedSize = 1000;
int optimalInitialCapacity = (int) (expectedSize / 0.75f) + 1;
Map<String, Integer> optimized = new HashMap<>(optimalInitialCapacity);

// Использование StringBuilder для ключей-строк
Map<String, Object> map = new HashMap<>();
StringBuilder keyBuilder = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    keyBuilder.setLength(0);
    keyBuilder.append("prefix_").append(i);
    map.put(keyBuilder.toString(), someValue);
}

TreeMap vs HashMap выбор

// Когда использовать HashMap:
// - Нужна максимальная скорость операций
// - Порядок элементов не важен
// - Ключи имеют хороший hashCode()

Map<String, Object> fastLookup = new HashMap<>();

// Когда использовать TreeMap:
// - Нужен отсортированный порядок
// - Нужны операции поиска диапазонов
// - Нужны навигационные операции

Map<LocalDate, Event> sortedEvents = new TreeMap<>();

Потокобезопасность

Проблемы с многопоточностью

// ОПАСНО - может привести к бесконечному циклу
Map<String, Integer> unsafeMap = new HashMap<>();

// Concurrent модификация
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
    final int value = i;
    executor.submit(() -> {
        unsafeMap.put("key" + value, value); // Race condition!
    });
}

Решения для потокобезопасности

// 1. Синхронизированная обертка
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
// ВАЖНО: итерация требует внешней синхронизации
synchronized (syncMap) {
    for (Map.Entry<String, Integer> entry : syncMap.entrySet()) {
        // обработка
    }
}

// 2. ConcurrentHashMap (рекомендуется)
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();

// 3. Concurrent TreeMap не существует, используйте:
ConcurrentSkipListMap<String, Integer> concurrentSorted = new ConcurrentSkipListMap<>();

Частые ошибки и решения

1. Изменение ключей после добавления в Map

class MutableKey {
    private String value;
    
    // Если изменить value после добавления в HashMap,
    // объект может "потеряться"
}

// РЕШЕНИЕ: делайте ключи неизменяемыми
class ImmutableKey {
    private final String value;
    
    public ImmutableKey(String value) {
        this.value = value;
    }
    
    // equals, hashCode...
}

2. Плохая реализация equals/hashCode

// ПЛОХО
class BadKey {
    private String name;
    
    @Override
    public boolean equals(Object obj) {
        return name.equals(((BadKey) obj).name); // NPE риск!
    }
    
    // hashCode не переопределен!
}

// ХОРОШО
class GoodKey {
    private String name;
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        GoodKey goodKey = (GoodKey) obj;
        return Objects.equals(name, goodKey.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}

3. ConcurrentModificationException

Map<String, Integer> map = new HashMap<>();
// заполняем map...

// НЕПРАВИЛЬНО
for (String key : map.keySet()) {
    if (someCondition) {
        map.remove(key); // ConcurrentModificationException!
    }
}

// ПРАВИЛЬНО
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<String, Integer> entry = iterator.next();
    if (someCondition) {
        iterator.remove();
    }
}

// ИЛИ с Java 8+
map.entrySet().removeIf(entry -> someCondition);

Лучшие практики

  1. Используйте интерфейсы в объявлениях:

    Map<String, Integer> map = new HashMap<>(); // ХОРОШО
    HashMap<String, Integer> map = new HashMap<>(); // ПЛОХО
    
  2. Задавайте начальную емкость для HashMap:

    Map<String, Integer> map = new HashMap<>(expectedSize * 4 / 3);
    
  3. Правильно реализуйте equals() и hashCode() для ключей

  4. Используйте неизменяемые объекты как ключи

  5. Для сортированных данных выбирайте TreeMap

  6. Для многопоточности используйте ConcurrentHashMap

  7. Проверяйте на null перед использованием значений:

    Integer value = map.get(key);
    if (value != null) {
        // обработка
    }
    // ИЛИ
    Integer value = map.getOrDefault(key, 0);
    

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

Внутреннее устройство HashMap

Основная структура данных

Массив buckets (корзин)

// Упрощенная структура HashMap
class HashMap<K, V> {
    // Массив корзин (buckets)
    transient Node<K,V>[] table;
    
    // Количество элементов
    transient int size;
    
    // Порог для resize (capacity * load factor)
    int threshold;
    
    // Коэффициент загрузки
    final float loadFactor;
    
    // Счетчик модификаций
    transient int modCount;
}

Узел (Node) - основной элемент

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;    // Хеш-код ключа
    final K key;       // Ключ
    V value;           // Значение
    Node<K,V> next;    // Ссылка на следующий узел (для коллизий)
    
    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
}

Принцип работы

1. Вычисление hash

// Упрощенная версия hash-функции HashMap
static final int hash(Object key) {
    int h;
    // XOR старших и младших битов для лучшего распределения
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// Пример работы:
String key = "hello";
int hashCode = key.hashCode();     // 99162322
int hash = hashCode ^ (hashCode >>> 16);  // XOR с сдвигом

2. Определение индекса bucket

// Получение индекса в массиве
int index = (table.length - 1) & hash;

// Почему (length - 1) & hash вместо hash % length?
// 1. Более быстрая операция (битовое И вместо деления)
// 2. Работает только когда length - степень двойки
// 3. (length - 1) создает маску из единиц

// Пример:
// length = 16 (10000 в двоичном)
// length - 1 = 15 (01111 в двоичном)
// hash = 25 (11001 в двоичном)
// index = 15 & 25 = 01111 & 11001 = 01001 = 9

3. Размещение в bucket

// Псевдокод операции put
public V put(K key, V value) {
    int hash = hash(key);
    int index = (table.length - 1) & hash;
    
    Node<K,V> node = table[index];
    
    if (node == null) {
        // Bucket пустой - создаем новый узел
        table[index] = new Node<>(hash, key, value, null);
    } else {
        // Есть коллизия - ищем в цепочке
        while (node != null) {
            if (node.hash == hash && 
                (node.key == key || key.equals(node.key))) {
                // Ключ найден - обновляем значение
                V oldValue = node.value;
                node.value = value;
                return oldValue;
            }
            if (node.next == null) {
                // Добавляем в конец цепочки
                node.next = new Node<>(hash, key, value, null);
                break;
            }
            node = node.next;
        }
    }
    
    size++;
    // Проверяем необходимость resize
    if (size > threshold) {
        resize();
    }
    return null;
}

Эволюция структуры данных

До Java 8: только связные списки

Bucket 0: [Node1] -> [Node2] -> [Node3] -> null
Bucket 1: [Node4] -> null
Bucket 2: null
Bucket 3: [Node5] -> [Node6] -> null

Java 8+: связные списки + красно-черные деревья

// Константы для treeification
static final int TREEIFY_THRESHOLD = 8;     // Порог для превращения в дерево
static final int UNTREEIFY_THRESHOLD = 6;   // Порог для превращения обратно в список
static final int MIN_TREEIFY_CAPACITY = 64; // Минимальная емкость для treeification

// TreeNode для красно-черного дерева
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // Родитель
    TreeNode<K,V> left;    // Левый потомок
    TreeNode<K,V> right;   // Правый потомок
    TreeNode<K,V> prev;    // Предыдущий в linked list
    boolean red;           // Цвет узла (красный/черный)
}

Процесс treeification

// Упрощенная логика treeification
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index;
    Node<K,V> e;
    
    // Если таблица слишком мала - просто resize
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) {
        resize();
    } else if ((e = tab[index = (n - 1) & hash]) != null) {
        // Превращаем цепочку в дерево
        TreeNode<K,V> hd = null, tl = null;
        
        // Создаем TreeNode для каждого Node
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null) {
                hd = p;
            } else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        
        // Размещаем корень дерева в bucket
        if ((tab[index] = hd) != null) {
            hd.treeify(tab);
        }
    }
}

Процесс resize

Когда происходит resize

// Resize происходит когда:
// size > threshold, где threshold = capacity * loadFactor

// Значения по умолчанию:
static final int DEFAULT_INITIAL_CAPACITY = 16;  // 1 << 4
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int MAXIMUM_CAPACITY = 1 << 30;     // 2^30

// Пример:
// capacity = 16, loadFactor = 0.75
// threshold = 16 * 0.75 = 12
// Resize произойдет при добавлении 13-го элемента

Алгоритм resize

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    
    // Удваиваем размер
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                   oldCap >= DEFAULT_INITIAL_CAPACITY) {
            newThr = oldThr << 1; // Удваиваем threshold
        }
    }
    
    // Создаем новую таблицу
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    threshold = newThr;
    
    // Перемещаем элементы
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                redistributeNodes(e, newTab, newCap);
            }
        }
    }
    return newTab;
}

Перераспределение узлов при resize

// Ключевая оптимизация Java 8+
// При удвоении размера элемент либо остается на том же индексе,
// либо смещается на oldCapacity позиций

void redistributeNodes(Node<K,V> e, Node<K,V>[] newTab, int newCap) {
    Node<K,V> loHead = null, loTail = null;  // "low" цепочка
    Node<K,V> hiHead = null, hiTail = null;  // "high" цепочка
    Node<K,V> next;
    
    do {
        next = e.next;
        // Проверяем бит, соответствующий старой емкости
        if ((e.hash & oldCap) == 0) {
            // Остается на том же индексе
            if (loTail == null) {
                loHead = e;
            } else {
                loTail.next = e;
            }
            loTail = e;
        } else {
            // Смещается на oldCap позиций
            if (hiTail == null) {
                hiHead = e;
            } else {
                hiTail.next = e;
            }
            hiTail = e;
        }
    } while ((e = next) != null);
    
    // Размещаем цепочки в новой таблице
    if (loTail != null) {
        loTail.next = null;
        newTab[j] = loHead;
    }
    if (hiTail != null) {
        hiTail.next = null;
        newTab[j + oldCap] = hiHead;
    }
}

Коллизии и их обработка

Типы коллизий

// 1. Hash collision - разные ключи дают одинаковый hash
String key1 = "Aa";    // hashCode = 2112
String key2 = "BB";    // hashCode = 2112

// 2. Index collision - разные hash попадают в один bucket
// hash1 = 17, hash2 = 33, capacity = 16
// index1 = 17 & 15 = 1
// index2 = 33 & 15 = 1

// 3. Равные ключи (замена значения)
map.put("key", "value1");
map.put("key", "value2");  // Заменит value1 на value2

Разрешение коллизий

Метод цепочек (Chaining)

// До Java 8: простые связные списки
Bucket[5]: Node1 -> Node2 -> Node3 -> null

// Java 8+: списки превращаются в деревья при большом количестве коллизий
Bucket[5]: TreeNode (корень красно-черного дерева)
           /        \
      TreeNode    TreeNode
      /      \    /      \
   ...      ... ...    ...

Производительность при коллизиях

// Связный список: O(n) для поиска
// Красно-черное дерево: O(log n) для поиска

// Пример влияния коллизий:
class BadHashKey {
    private String value;
    
    @Override
    public int hashCode() {
        return 1; // Все ключи в один bucket!
    }
    
    // Производительность деградирует до O(n)
}

class GoodHashKey {
    private String value;
    
    @Override
    public int hashCode() {
        return Objects.hash(value); // Хорошее распределение
    }
    
    // Средняя производительность O(1)
}

Load Factor и производительность

Влияние Load Factor

// loadFactor = 0.5 (низкий)
// - Мало коллизий
// - Много пустых buckets
// - Больше памяти

// loadFactor = 0.75 (оптимальный)
// - Хороший баланс времени и памяти
// - Стандартное значение

// loadFactor = 1.0 (высокий)
// - Больше коллизий
// - Экономия памяти
// - Снижение производительности

Выбор начальной емкости

// Плохо: частые resize
Map<String, Integer> map = new HashMap<>(); // capacity = 16
// При добавлении 1000 элементов произойдет много resize

// Хорошо: правильная начальная емкость
int expectedSize = 1000;
int initialCapacity = (int) (expectedSize / 0.75f) + 1; // ~1334
Map<String, Integer> map = new HashMap<>(initialCapacity);

// Еще лучше: степень двойки
int capacity = Integer.highestOneBit(initialCapacity - 1) << 1; // 2048
Map<String, Integer> map = new HashMap<>(capacity);

Детали реализации

Хеш-функция

// Полная версия hash-функции
static final int hash(Object key) {
    int h;
    // 1. Получаем hashCode ключа
    // 2. XOR с правым сдвигом на 16 бит
    // Это смешивает старшие и младшие биты для лучшего распределения
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// Пример:
// key = "hello"
// hashCode = 99162322 (в двоичном: 101111100100110110110010)
// h >>> 16 = 1514 (в двоичном: 10111101001)
// result = 99162322 ^ 1514 = 99163044

Почему размер всегда степень двойки

// 1. Быстрое вычисление индекса: (n-1) & hash вместо hash % n
// 2. Равномерное распределение при хорошей hash-функции
// 3. Оптимизация resize - элементы остаются или смещаются на n позиций

// Пример с capacity = 16:
// n-1 = 15 = 00001111 (в двоичном)
// Любой hash & 00001111 даст значение от 0 до 15

// При resize до 32:
// old_n-1 = 15 = 00001111
// new_n-1 = 31 = 00011111
// Дополнительный бит определяет, остается элемент или смещается

Операция get

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; 
    Node<K,V> first, e; 
    int n; 
    K k;
    
    // Проверяем что таблица не пустая
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        
        // Проверяем первый элемент
        if (first.hash == hash &&
            ((k = first.key) == key || (key != null && key.equals(k)))) {
            return first;
        }
        
        // Ищем в цепочке или дереве
        if ((e = first.next) != null) {
            if (first instanceof TreeNode) {
                // Поиск в красно-черном дереве: O(log n)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            }
            
            // Поиск в связном списке: O(n)
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))) {
                    return e;
                }
            } while ((e = e.next) != null);
        }
    }
    return null;
}

Проблемы и оптимизации

Проблема бесконечного цикла (до Java 8)

// В старых версиях HashMap при concurrent resize
// могли образовываться циклические ссылки

// Thread 1: resize()
// Thread 2: resize() одновременно
// Результат: Node1.next -> Node2.next -> Node1 (цикл!)

// Решение в Java 8+:
// 1. Изменен порядок линковки при resize
// 2. Использование head/tail указателей
// 3. Все еще не thread-safe, но без бесконечных циклов

Memory overhead

// Память на одну Entry:
// - Node: 32 байта (4 поля × 8 байт на 64-bit системе)
// - Ключ и значение: зависит от типов
// - Массив buckets: capacity × 8 байт

// Для TreeNode дополнительно:
// - 4 дополнительных указателя
// - boolean для цвета
// Итого: ~56 байт вместо 32

// Оптимизация памяти:
// 1. Правильная начальная емкость
// 2. Подходящий load factor
// 3. Избегание treeification (хорошие hash-функции)

String ключи оптимизация

// В современных JVM String.hashCode() кэшируется
public final class String {
    private int hash; // Кэш hash-кода
    
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            // Вычисляем только один раз
            char val[] = value;
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
}

Практические рекомендации

1. Правильная реализация hashCode

// Плохо - много коллизий
class BadKey {
    String name;
    int age;
    
    @Override
    public int hashCode() {
        return name.length(); // Слишком много коллизий!
    }
}

// Хорошо - равномерное распределение
class GoodKey {
    String name;
    int age;
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age); // Использует качественную hash-функцию
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof GoodKey)) return false;
        GoodKey other = (GoodKey) obj;
        return age == other.age && Objects.equals(name, other.name);
    }
}

2. Избегание автобоксинга

// Плохо - много автобоксинга
Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < 1000; i++) {
    map.put(i, "value" + i); // Автобоксинг int -> Integer
}

// Лучше - специализированные коллекции
TIntObjectHashMap<String> map = new TIntObjectHashMap<>(); // Trove library
// Или Eclipse Collections
MutableIntObjectMap<String> map = new IntObjectHashMap<>();

3. Мониторинг коллизий

// Простая проверка распределения
public void analyzeDistribution(HashMap<?, ?> map) {
    try {
        Field tableField = HashMap.class.getDeclaredField("table");
        tableField.setAccessible(true);
        Object[] table = (Object[]) tableField.get(map);
        
        if (table != null) {
            int emptyBuckets = 0;
            int maxChainLength = 0;
            
            for (Object bucket : table) {
                if (bucket == null) {
                    emptyBuckets++;
                } else {
                    int chainLength = getChainLength(bucket);
                    maxChainLength = Math.max(maxChainLength, chainLength);
                }
            }
            
            System.out.println("Empty buckets: " + emptyBuckets + "/" + table.length);
            System.out.println("Max chain length: " + maxChainLength);
            System.out.println("Load factor: " + (double) map.size() / table.length);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

4. Измерение производительности

// Тест производительности с разными load factors
public void performanceTest() {
    int[] sizes = {1000, 10000, 100000};
    float[] loadFactors = {0.5f, 0.75f, 1.0f};
    
    for (int size : sizes) {
        for (float lf : loadFactors) {
            Map<String, Integer> map = new HashMap<>(size, lf);
            
            // Заполнение
            long startTime = System.nanoTime();
            for (int i = 0; i < size; i++) {
                map.put("key" + i, i);
            }
            long putTime = System.nanoTime() - startTime;
            
            // Поиск
            startTime = System.nanoTime();
            for (int i = 0; i < size; i++) {
                map.get("key" + i);
            }
            long getTime = System.nanoTime() - startTime;
            
            System.out.printf("Size: %d, LF: %.2f, Put: %d ns, Get: %d ns%n",
                size, lf, putTime, getTime);
        }
    }
}

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

Память в Java

Общая архитектура памяти JVM

┌─────────────────────────────────────────────────────────┐
│                    JVM Memory                           │
├─────────────────────────────────────────────────────────┤
│                  Heap Memory                            │
│  ┌─────────────────┐  ┌─────────────────────────────────┐ │
│  │  Young Gen      │  │        Old Gen                  │ │
│  │ ┌─────┐ ┌─────┐ │  │                                 │ │
│  │ │Eden │ │ S0  │ │  │                                 │ │
│  │ │     │ │     │ │  │                                 │ │
│  │ └─────┘ │ S1  │ │  │                                 │ │
│  │         └─────┘ │  │                                 │ │
│  └─────────────────┘  └─────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│                  Non-Heap Memory                        │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│  │ Method Area │ │ Code Cache  │ │ Compressed Class    │ │
│  │ (Metaspace) │ │             │ │ Space (if enabled)  │ │
│  └─────────────┘ └─────────────┘ └─────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│                  Direct Memory                          │
│         (Off-heap, управляется вручную)                 │
├─────────────────────────────────────────────────────────┤
│                  Stack Memory                           │
│              (для каждого потока)                       │
└─────────────────────────────────────────────────────────┘

Heap Memory (Куча)

Структура Heap

// Размеры по умолчанию зависят от доступной памяти
// Обычно: Young Gen = 1/3 Heap, Old Gen = 2/3 Heap

// Eden Space: 80% от Young Gen
// Survivor Spaces (S0, S1): по 10% от Young Gen каждый

Young Generation (Молодое поколение)

Eden Space

// Все новые объекты создаются в Eden
String str = new String("Hello");        // Создается в Eden
List<Integer> list = new ArrayList<>();  // Создается в Eden
Person person = new Person("John");      // Создается в Eden

// Когда Eden заполняется - происходит Minor GC

Survivor Spaces (S0, S1)

// Объекты, пережившие Minor GC, перемещаются в Survivor
// Используется алгоритм copying - всегда один Survivor пустой

// Пример жизненного цикла объекта:
// 1. Создание в Eden
// 2. После Minor GC -> S0 (age = 1)
// 3. После следующего Minor GC -> S1 (age = 2)
// 4. После нескольких GC -> Old Generation (обычно age >= 15)

Old Generation (Старое поколение)

// Долгоживущие объекты попадают в Old Generation
// - Объекты с age >= threshold (обычно 15)
// - Большие объекты (превышающие порог)
// - Объекты, которые не помещаются в Survivor

// Примеры долгоживущих объектов:
static final Map<String, Object> cache = new HashMap<>();  // Static переменные
class Singleton { /* ... */ }                              // Singleton объекты

Размеры и настройка Heap

# Настройка размеров Heap
-Xms512m          # Начальный размер heap
-Xmx2g            # Максимальный размер heap
-XX:NewRatio=3    # Соотношение Old/Young (Old = 3 * Young)
-XX:SurvivorRatio=8  # Соотношение Eden/Survivor (Eden = 8 * Survivor)

# Конкретные размеры поколений
-XX:NewSize=256m      # Начальный размер Young Generation
-XX:MaxNewSize=512m   # Максимальный размер Young Generation
-XX:MaxTenuringThreshold=15  # Максимальный age для перехода в Old Gen

Non-Heap Memory

Method Area (Metaspace с Java 8+)

// Хранит метаданные классов
class MyClass {
    static int staticVar = 10;        // Статические переменные
    
    public void method() { }          // Метаданные методов
    
    // Константы из пула строк
    String constant = "Hello World"; // Ссылка на строку в String Pool
}

// Настройка Metaspace
// -XX:MetaspaceSize=128m     # Начальный размер
// -XX:MaxMetaspaceSize=512m  # Максимальный размер (по умолчанию unlimited)

Code Cache

// Хранит скомпилированный нативный код (JIT)
// HotSpot компилирует часто используемые методы в нативный код

public void hotMethod() {
    // Этот метод будет скомпилирован JIT'ом если вызывается часто
}

// Настройка Code Cache
// -XX:InitialCodeCacheSize=32m   # Начальный размер
// -XX:ReservedCodeCacheSize=128m # Максимальный размер

String Pool

// String Pool - часть Heap (с Java 7+), раньше был в Method Area
String s1 = "Hello";           // В String Pool
String s2 = "Hello";           // Ссылка на тот же объект в Pool
String s3 = new String("Hello"); // Новый объект в Heap

System.out.println(s1 == s2);  // true - один объект
System.out.println(s1 == s3);  // false - разные объекты

// Интернирование строк
String s4 = s3.intern();       // Добавляет в Pool или возвращает существующую
System.out.println(s1 == s4);  // true

// Настройка String Pool
// -XX:StringTableSize=60013  # Размер хеш-таблицы String Pool

Stack Memory

Структура Stack

// Каждый поток имеет свой stack
// Stack содержит фреймы методов

public void methodA() {
    int localVar = 10;        // Локальная переменная в стеке
    String str = "Hello";     // Ссылка в стеке, объект в Heap
    
    methodB(localVar);        // Новый фрейм добавляется в стек
}

public void methodB(int param) {
    Object obj = new Object(); // Ссылка в стеке, объект в Heap
    // Фрейм содержит: параметры, локальные переменные, return address
}

// Настройка размера стека
// -Xss1m  # Размер стека для каждого потока (по умолчанию 1MB)

Содержимое Stack Frame

public int calculate(int a, int b) {
    int temp = a + b;         // Локальная переменная
    return temp * 2;          // Возвращаемое значение
}

/*
Stack Frame содержит:
┌──────────────────────┐
│ Локальные переменные │  <- temp, a, b
├──────────────────────┤
│ Operand Stack        │  <- Операнды для вычислений
├──────────────────────┤
│ Ссылка на            │  <- Метаданные метода
│ константный пул      │
├──────────────────────┤
│ Return Address       │  <- Адрес возврата
└──────────────────────┘
*/

Direct Memory (Off-Heap)

ByteBuffer и Direct Memory

import java.nio.ByteBuffer;

// Heap-based buffer
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);

// Direct buffer (off-heap)
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);

// Direct memory не управляется GC напрямую
// Освобождается при finalization или вызове Cleaner

// Настройка Direct Memory
// -XX:MaxDirectMemorySize=512m  # Максимальный размер Direct Memory

Использование Direct Memory

// Пример работы с большими данными
public class LargeDataProcessor {
    private ByteBuffer buffer;
    
    public LargeDataProcessor(int size) {
        // Для больших буферов лучше использовать direct memory
        this.buffer = ByteBuffer.allocateDirect(size);
    }
    
    public void processData(byte[] data) {
        buffer.clear();
        buffer.put(data);
        buffer.flip();
        
        // Обработка данных...
    }
    
    public void cleanup() {
        // Ручное освобождение (опционально)
        if (buffer instanceof sun.nio.ch.DirectBuffer) {
            ((sun.nio.ch.DirectBuffer) buffer).cleaner().clean();
        }
    }
}

Garbage Collection

Алгоритмы GC

Serial GC

# Однопоточный GC для небольших приложений
-XX:+UseSerialGC

# Подходит для:
# - Однопоточные приложения
# - Heap < 100MB
# - Клиентские приложения

Parallel GC (по умолчанию)

# Многопоточный GC
-XX:+UseParallelGC
-XX:ParallelGCThreads=4    # Количество потоков для GC

# Подходит для:
# - Многопоточные приложения
# - Batch обработка
# - Когда важна пропускная способность

G1GC

# Low-latency GC для больших heap
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200   # Целевое время паузы

# Подходит для:
# - Heap > 4GB
# - Низкие требования к latency
# - Интерактивные приложения

ZGC (Java 11+)

# Ultra-low latency GC
-XX:+UseZGC
-XX:+UnlockExperimentalVMOptions  # Для Java 11-14

# Характеристики:
# - Паузы < 10ms
# - Heap до нескольких TB
# - Concurrent сборка мусора

Типы GC событий

// Minor GC - очистка Young Generation
// Происходит часто (секунды/минуты)
// Быстрый (обычно < 100ms)

// Major GC - очистка Old Generation
// Происходит редко (минуты/часы)
// Медленный (может быть секунды)

// Full GC - очистка всего Heap + Method Area
// Самый медленный, останавливает все потоки

Memory Leaks и проблемы

Типичные Memory Leaks

// 1. Статические коллекции
public class MemoryLeak {
    private static List<Object> cache = new ArrayList<>();
    
    public void addToCache(Object obj) {
        cache.add(obj);  // Объекты никогда не удаляются!
    }
}

// 2. Слушатели событий
public class EventSource {
    private List<EventListener> listeners = new ArrayList<>();
    
    public void addListener(EventListener listener) {
        listeners.add(listener);
        // Забыли убрать listener - он никогда не будет собран GC
    }
}

// 3. ThreadLocal без очистки
public class ThreadLocalLeak {
    private static ThreadLocal<List<Object>> threadLocal = new ThreadLocal<>();
    
    public void process() {
        threadLocal.set(new ArrayList<>());
        // Забыли вызвать threadLocal.remove()!
    }
}

// 4. Неправильное закрытие ресурсов
public class ResourceLeak {
    public void readFile(String filename) throws IOException {
        FileInputStream fis = new FileInputStream(filename);
        // Забыли закрыть поток!
        // fis.close();
    }
}

Решения Memory Leaks

// 1. Правильное управление коллекциями
public class CacheWithEviction {
    private Map<String, Object> cache = new LinkedHashMap<String, Object>(100, 0.75f, true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
            return size() > 100;  // LRU eviction
        }
    };
}

// 2. WeakReference для listeners
public class EventSource {
    private List<WeakReference<EventListener>> listeners = new ArrayList<>();
    
    public void addListener(EventListener listener) {
        listeners.add(new WeakReference<>(listener));
    }
    
    private void cleanupListeners() {
        listeners.removeIf(ref -> ref.get() == null);
    }
}

// 3. Try-with-resources
public void readFile(String filename) throws IOException {
    try (FileInputStream fis = new FileInputStream(filename)) {
        // Автоматическое закрытие ресурса
    }
}

// 4. Очистка ThreadLocal
public class ThreadLocalSafe {
    private static ThreadLocal<List<Object>> threadLocal = new ThreadLocal<>();
    
    public void process() {
        try {
            threadLocal.set(new ArrayList<>());
            // Работа с threadLocal
        } finally {
            threadLocal.remove();  // Обязательная очистка!
        }
    }
}

OutOfMemoryError типы

HeapSpace

// java.lang.OutOfMemoryError: Java heap space
List<byte[]> list = new ArrayList<>();
while (true) {
    list.add(new byte[1024 * 1024]);  // Создаем 1MB массивы
}

// Решения:
// -Xmx4g                    # Увеличить heap
// Проверить memory leaks
// Оптимизировать структуры данных

Metaspace

// java.lang.OutOfMemoryError: Metaspace
// Слишком много классов загружено

// Причины:
// - Динамическая генерация классов (CGLib, Reflection)
// - Memory leaks в ClassLoader
// - Слишком много зависимостей

// Решения:
// -XX:MaxMetaspaceSize=512m  # Увеличить Metaspace
// Проверить ClassLoader leaks

Direct buffer memory

// java.lang.OutOfMemoryError: Direct buffer memory
ByteBuffer buffer = ByteBuffer.allocateDirect(Integer.MAX_VALUE);

// Решения:
// -XX:MaxDirectMemorySize=1g  # Увеличить Direct Memory
// Правильно освобождать direct buffers

Unable to create new native thread

// java.lang.OutOfMemoryError: unable to create new native thread
for (int i = 0; i < 10000; i++) {
    new Thread(() -> {
        try {
            Thread.sleep(Long.MAX_VALUE);
        } catch (InterruptedException e) {}
    }).start();
}

// Решения:
// -Xss512k                  # Уменьшить размер стека
// Использовать Thread pools
// Проверить лимиты ОС на количество потоков

Мониторинг памяти

JVM флаги для мониторинга

# Подробное логирование GC
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCApplicationStoppedTime
-Xloggc:gc.log

# Java 9+ unified logging
-Xlog:gc*:gc.log

# Heap dump при OutOfMemoryError
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/heapdumps/

# Мониторинг через JMX
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.authenticate=false

Программный мониторинг

import java.lang.management.*;

public class MemoryMonitor {
    private final MemoryMXBean memoryBean;
    private final List<GarbageCollectorMXBean> gcBeans;
    
    public MemoryMonitor() {
        this.memoryBean = ManagementFactory.getMemoryMXBean();
        this.gcBeans = ManagementFactory.getGarbageCollectorMXBeans();
    }
    
    public void printMemoryUsage() {
        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
        MemoryUsage nonHeapUsage = memoryBean.getNonHeapMemoryUsage();
        
        System.out.println("=== Heap Memory ===");
        printMemoryUsage("Heap", heapUsage);
        
        System.out.println("=== Non-Heap Memory ===");
        printMemoryUsage("Non-Heap", nonHeapUsage);
        
        System.out.println("=== GC Statistics ===");
        for (GarbageCollectorMXBean gcBean : gcBeans) {
            System.out.printf("%s: %d collections, %d ms%n",
                gcBean.getName(),
                gcBean.getCollectionCount(),
                gcBean.getCollectionTime()
            );
        }
    }
    
    private void printMemoryUsage(String name, MemoryUsage usage) {
        System.out.printf("%s - Used: %d MB, Committed: %d MB, Max: %d MB%n",
            name,
            usage.getUsed() / 1024 / 1024,
            usage.getCommitted() / 1024 / 1024,
            usage.getMax() / 1024 / 1024
        );
    }
    
    public void monitorMemory() {
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        
        // Настройка уведомлений о превышении порога
        List<MemoryPoolMXBean> pools = ManagementFactory.getMemoryPoolMXBeans();
        for (MemoryPoolMXBean pool : pools) {
            if (pool.getType() == MemoryType.HEAP && pool.isUsageThresholdSupported()) {
                long threshold = pool.getUsage().getMax() * 80 / 100; // 80%
                pool.setUsageThreshold(threshold);
                
                // Добавить NotificationListener для получения уведомлений
            }
        }
    }
}

Профилирование памяти

jstat - мониторинг в реальном времени

# Мониторинг GC каждые 2 секунды
jstat -gc <pid> 2s

# Мониторинг использования heap
jstat -gccapacity <pid>

# Мониторинг частоты GC
jstat -gcutil <pid> 5s

jmap - heap dumps и анализ

# Создание heap dump
jmap -dump:format=b,file=heapdump.hprof <pid>

# Гистограмма объектов в heap
jmap -histo <pid>

# Информация о heap
jmap -heap <pid>

Анализ heap dumps

// Инструменты для анализа:
// 1. Eclipse MAT (Memory Analyzer Tool)
// 2. VisualVM
// 3. JProfiler
// 4. YourKit

// Что искать в heap dump:
// - Самые большие объекты
// - Объекты с большим количеством ссылок
// - Duplicate strings
// - Memory leaks (объекты, которые должны быть собраны GC)

Оптимизация памяти

Выбор структур данных

// Плохо - много памяти
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    list.add(i);  // Autoboxing: int -> Integer (12 байт вместо 4)
}

// Лучше - примитивные коллекции
TIntArrayList list = new TIntArrayList(); // Trove library
for (int i = 0; i < 1000; i++) {
    list.add(i);  // Только 4 байта на элемент
}

// Компактные представления
// Вместо Map<String, Integer> для небольших карт:
// Используйте два массива String[] и int[]

Object pooling

// Пул для дорогих объектов
public class ObjectPool<T> {
    private final Queue<T> pool = new ConcurrentLinkedQueue<>();
    private final Supplier<T> factory;
    
    public ObjectPool(Supplier<T> factory) {
        this.factory = factory;
    }
    
    public T acquire() {
        T object = pool.poll();
        return object != null ? object : factory.get();
    }
    
    public void release(T object) {
        // Сброс состояния объекта
        resetObject(object);
        pool.offer(object);
    }
    
    private void resetObject(T object) {
        // Сброс состояния для повторного использования
    }
}

// Использование
ObjectPool<StringBuilder> stringBuilderPool = 
    new ObjectPool<>(() -> new StringBuilder(256));

StringBuilder sb = stringBuilderPool.acquire();
try {
    sb.append("Hello").append(" World");
    return sb.toString();
} finally {
    stringBuilderPool.release(sb);
}

Lazy initialization

public class ExpensiveResource {
    private volatile HeavyObject heavyObject;
    
    // Ленивая инициализация с double-checked locking
    public HeavyObject getHeavyObject() {
        HeavyObject result = heavyObject;
        if (result == null) {
            synchronized (this) {
                result = heavyObject;
                if (result == null) {
                    heavyObject = result = new HeavyObject();
                }
            }
        }
        return result;
    }
}

Лучшие практики

1. Правильная настройка JVM

# Продакшн настройки для веб-приложения
-Xms2g -Xmx2g                    # Фиксированный размер heap
-XX:+UseG1GC                     # G1 для low latency
-XX:MaxGCPauseMillis=200         # Цель по паузам GC
-XX:+HeapDumpOnOutOfMemoryError  # Heap dump при OOM
-XX:+PrintGCDetails              # Логирование GC

2. Избегание memory leaks

// ✅ Правильно
public class ServiceClass {
    private final List<WeakReference<Listener>> listeners = new ArrayList<>();
    
    public void addListener(Listener listener) {
        listeners.add(new WeakReference<>(listener));
    }
    
    @PreDestroy
    public void cleanup() {
        listeners.clear();
    }
}

// ❌ Неправильно
public class ServiceClass {
    private final List<Listener> listeners = new ArrayList<>();
    
    public void addListener(Listener listener) {
        listeners.add(listener); // Сильная ссылка!
    }
    // Нет cleanup метода
}

3. Оптимизация строк

// ✅ Эффективная конкатенация
StringBuilder sb = new StringBuilder(estimatedSize);
for (String s : strings) {
    sb.append(s);
}
return sb.toString();

// ❌ Неэффективная конкатенация
String result = "";
for (String s : strings) {
    result += s; // Создает новый String на каждой итерации!
}

4. Правильное управление ресурсами

// ✅ Try-with-resources
public void processFile(String filename) throws IOException {
    try (BufferedReader reader = Files.newBufferedReader(Paths.get(filename))) {
        // Обработка файла
        // Ресурс автоматически закроется
    }
}

// ✅ Явная очистка кэшей
@Scheduled(fixedRate = 3600000) // Каждый час
public void cleanupCache() {
    cache.entrySet().removeIf(entry -> 
        entry.getValue().isExpired());
}

Эта шпаргалка поможет понять устройство памяти в Java, правильно настроить JVM, избежать memory leaks и оптимизировать использование памяти в приложениях.

Garbage Collector в Java

1. Основы управления памятью в Java

Структура памяти JVM

┌─────────────────────────────────────────────────────────────┐
│                        JVM Memory                           │
├─────────────────────────────────────────────────────────────┤
│  Method Area (Metaspace в Java 8+)                        │
│  - Class metadata, constant pool, static variables         │
├─────────────────────────────────────────────────────────────┤
│  Heap Memory                                               │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Young Generation                                   │   │
│  │  ┌─────────────┬─────────────┬─────────────────────┐│   │
│  │  │    Eden     │ Survivor 0  │    Survivor 1       ││   │
│  │  │   Space     │   (S0)      │      (S1)           ││   │
│  │  └─────────────┴─────────────┴─────────────────────┘│   │
│  └─────────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Old Generation (Tenured)                          │   │
│  └─────────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────────┤
│  PC Registers, Native Method Stack, JVM Stack              │
└─────────────────────────────────────────────────────────────┘

Жизненный цикл объектов

  1. Создание — объекты создаются в Eden space
  2. Первая сборка — выжившие объекты → Survivor 0
  3. Вторая сборка — объекты → Survivor 1
  4. Повышение — после N сборок → Old Generation
  5. Финализация — объекты без ссылок удаляются

Типы ссылок в Java

  • Strong Reference — обычные ссылки, не удаляются GC
  • Weak Reference — удаляются при нехватке памяти
  • Soft Reference — удаляются только при критической нехватке памяти
  • Phantom Reference — для очистки ресурсов после финализации

2. Алгоритмы сборки мусора

2.1 Serial GC (-XX:+UseSerialGC)

Характеристики:

  • Однопоточный сборщик
  • Stop-the-world паузы
  • Подходит для небольших приложений (<100MB heap)

Алгоритм:

  • Young Generation: Serial copying
  • Old Generation: Serial Mark-Sweep-Compact

Использование:

java -XX:+UseSerialGC -Xms512m -Xmx1g MyApplication

2.2 Parallel GC (-XX:+UseParallelGC)

Характеристики:

  • Многопоточный сборщик
  • Высокая пропускная способность
  • Подходит для server applications

Настройки:

-XX:+UseParallelGC
-XX:ParallelGCThreads=<n>          # Количество потоков GC
-XX:MaxGCPauseMillis=<n>           # Максимальная пауза (мс)
-XX:GCTimeRatio=<n>                # Соотношение времени GC/работы

2.3 G1 GC (-XX:+UseG1GC)

Характеристики:

  • Низкие задержки (<10ms)
  • Инкрементальная сборка
  • Подходит для больших heap'ов (>6GB)

Ключевые параметры:

-XX:+UseG1GC
-XX:MaxGCPauseMillis=200           # Целевая пауза (по умолчанию 200ms)
-XX:G1HeapRegionSize=<n>           # Размер региона
-XX:G1NewSizePercent=<n>           # Процент Young Generation
-XX:G1MaxNewSizePercent=<n>        # Максимальный процент Young
-XX:G1MixedGCCountTarget=<n>       # Количество mixed GC циклов

Фазы работы G1:

  1. Young-only — сборка только Young Generation
  2. Concurrent Marking — маркировка старых объектов
  3. Mixed — сборка Young + часть Old Generation
  4. Full GC — полная сборка (редко)

2.4 ZGC (-XX:+UseZGC)

Характеристики:

  • Ультранизкие задержки (<1ms)
  • Масштабируется до 16TB heap
  • Доступен с Java 11+

Настройки:

-XX:+UseZGC
-XX:SoftMaxHeapSize=<size>         # Мягкий лимит heap'а
-XX:ZCollectionInterval=<n>        # Интервал сборки

2.5 Shenandoah GC (-XX:+UseShenandoahGC)

Характеристики:

  • Низкие задержки
  • Параллельная сборка
  • Доступен с Java 12+

Настройки:

-XX:+UseShenandoahGC
-XX:ShenandoahGCHeuristics=<mode>  # adaptive, static, compact

3. Настройка размеров памяти

Основные параметры heap'а

-Xms<size>                         # Начальный размер heap'а
-Xmx<size>                         # Максимальный размер heap'а
-Xmn<size>                         # Размер Young Generation
-XX:NewRatio=<ratio>               # Соотношение Old/Young (например, 3:1)
-XX:SurvivorRatio=<ratio>          # Соотношение Eden/Survivor (например, 8:1)

Примеры настройки:

# Для приложения с 4GB heap
java -Xms4g -Xmx4g -Xmn1g -XX:SurvivorRatio=8 MyApp

# Автоматическая настройка
java -Xms2g -Xmx8g -XX:NewRatio=3 MyApp

Настройка Metaspace (Java 8+)

-XX:MetaspaceSize=<size>           # Начальный размер Metaspace
-XX:MaxMetaspaceSize=<size>        # Максимальный размер Metaspace
-XX:CompressedClassSpaceSize=<size> # Размер Compressed Class Space

4. Мониторинг и логирование

Включение GC логов (Java 8)

-XX:+PrintGC                       # Базовая информация
-XX:+PrintGCDetails                # Детальная информация
-XX:+PrintGCTimeStamps             # Временные метки
-XX:+PrintGCApplicationStoppedTime # Время остановки приложения
-Xloggc:gc.log                     # Файл для логов
-XX:+UseGCLogFileRotation          # Ротация логов
-XX:NumberOfGCLogFiles=5           # Количество файлов логов
-XX:GCLogFileSize=100M             # Размер файла лога

Включение GC логов (Java 9+)

-Xlog:gc*:gc.log:time,tags         # Все GC события
-Xlog:gc,heap:gc.log:time,tags     # GC + информация о heap

Анализ GC логов

# Пример лога Parallel GC
[GC (Allocation Failure) [PSYoungGen: 131072K->8192K(153600K)] 
131072K->8192K(500000K), 0.0123456 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]

# Расшифровка:
# - Allocation Failure: причина GC
# - PSYoungGen: Young Generation (Parallel Scavenge)
# - 131072K->8192K: размер до->после в Young Generation
# - (153600K): общий размер Young Generation
# - 131072K->8192K(500000K): размер всего heap до->после
# - 0.0123456 secs: время выполнения GC

5. Инструменты мониторинга

JVM встроенные инструменты

# Статистика GC
jstat -gc <pid> 1s                 # Каждую секунду
jstat -gccapacity <pid>            # Размеры поколений
jstat -gcutil <pid>                # Использование в процентах

# Информация о heap
jmap -heap <pid>                   # Конфигурация heap'а
jmap -dump:format=b,file=dump.hprof <pid>  # Создание heap dump

# Информация о процессе
jinfo <pid>                        # JVM флаги и свойства
jps                                # Список Java процессов

Внешние инструменты

  • JVisualVM — графический мониторинг
  • JProfiler — коммерческий профилировщик
  • Eclipse MAT — анализ heap dump'ов
  • GCViewer — анализ GC логов
  • CRaC — координированное восстановление в checkpoint

6. Тюнинг производительности

Метрики для оценки

  1. Throughput — процент времени работы приложения

    Throughput = (Total time - GC time) / Total time
    
  2. Latency — время пауз GC

    • P50, P95, P99 значения пауз
    • Максимальная пауза
  3. Footprint — использование памяти

    • Размер heap'а
    • Использование каждого поколения

Стратегии оптимизации

Для высокой пропускной способности

# Parallel GC с настройками
-XX:+UseParallelGC
-XX:ParallelGCThreads=8
-XX:GCTimeRatio=19                 # 5% времени на GC
-Xms8g -Xmx8g                      # Фиксированный размер heap

Для низких задержек

# G1 GC с настройками
-XX:+UseG1GC
-XX:MaxGCPauseMillis=10
-XX:G1HeapRegionSize=32m
-Xms4g -Xmx4g

Для критически низких задержек

# ZGC или Shenandoah
-XX:+UseZGC
-Xms16g -Xmx16g

7. Диагностика проблем

OutOfMemoryError

Java heap space

# Увеличить heap
-Xmx8g

# Анализ heap dump
jmap -dump:format=b,file=heap.hprof <pid>
# Открыть в Eclipse MAT

Metaspace

# Увеличить Metaspace
-XX:MaxMetaspaceSize=512m
-XX:MetaspaceSize=256m

Direct memory

# Увеличить direct memory
-XX:MaxDirectMemorySize=1g

Долгие GC паузы

Диагностика:

  1. Анализ GC логов
  2. Проверка размеров поколений
  3. Поиск memory leak'ов

Решения:

# Увеличить Young Generation
-Xmn2g

# Настроить G1 для коротких пауз
-XX:+UseG1GC
-XX:MaxGCPauseMillis=50

# Увеличить количество потоков GC
-XX:ParallelGCThreads=8

Memory Leaks

Типичные причины:

  • Статические коллекции
  • Listeners не удаляются
  • ThreadLocal переменные
  • Незакрытые ресурсы

Диагностика:

# Создание heap dump через интервалы
jmap -dump:format=b,file=heap1.hprof <pid>
# ... подождать ...
jmap -dump:format=b,file=heap2.hprof <pid>

# Анализ в Eclipse MAT:
# 1. Сравнить heap dump'ы
# 2. Найти растущие объекты
# 3. Проанализировать GC Roots

8. Best Practices

Настройка GC

  1. Начните с параметров по умолчанию
  2. Измеряйте baseline производительность
  3. Меняйте один параметр за раз
  4. Тестируйте под реальной нагрузкой
  5. Мониторьте в production

Оптимизация кода

// Плохо: создание объектов в цикле
for (int i = 0; i < 1000000; i++) {
    String s = "Item " + i;
    list.add(s);
}

// Хорошо: переиспользование StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000000; i++) {
    sb.setLength(0);
    sb.append("Item ").append(i);
    list.add(sb.toString());
}

// Лучше: предварительное выделение памяти
List<String> list = new ArrayList<>(1000000);

Управление ресурсами

// Всегда используйте try-with-resources
try (FileInputStream fis = new FileInputStream("file.txt");
     BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) {
    // работа с файлом
} // автоматическое закрытие ресурсов

9. Чек-лист настройки GC

Для разработки

Для тестирования

Для production

10. Полезные команды и скрипты

Мониторинг в реальном времени

# Постоянный мониторинг GC
while true; do
    jstat -gc <pid> | tail -1
    sleep 1
done

# Анализ heap dump
jhat -J-Xmx4g heap.hprof
# Открыть http://localhost:7000

Автоматизация

# Скрипт для создания heap dump при OutOfMemoryError
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heap-dumps/

# Скрипт для мониторинга GC
#!/bin/bash
PID=$1
while true; do
    jstat -gc $PID | awk '{print strftime("%Y-%m-%d %H:%M:%S"), $0}'
    sleep 5
done

Эта подробная шпаргалка поможет вам эффективно настраивать и мониторить Garbage Collector в Java приложениях.

JVM, JRE, JDK

Схема взаимосвязи

┌─────────────────────────────────────────────────────────────────┐
│                           JDK                                   │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                       JRE                                 │  │
│  │  ┌─────────────────────────────────────────────────────┐  │  │
│  │  │                   JVM                               │  │  │
│  │  │                                                     │  │  │
│  │  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  │  │  │
│  │  │  │Class Loader │  │   Runtime   │  │   Native    │  │  │  │
│  │  │  │ Subsystem   │  │Data Areas   │  │   Method    │  │  │  │
│  │  │  │             │  │             │  │ Interface   │  │  │  │
│  │  │  └─────────────┘  └─────────────┘  └─────────────┘  │  │  │
│  │  │                                                     │  │  │
│  │  │  ┌─────────────┐  ┌─────────────┐                  │  │  │
│  │  │  │ Execution   │  │     GC      │                  │  │  │
│  │  │  │   Engine    │  │             │                  │  │  │
│  │  │  └─────────────┘  └─────────────┘                  │  │  │
│  │  └─────────────────────────────────────────────────────┘  │  │
│  │                                                           │  │
│  │  Java Standard Libraries (rt.jar, charsets.jar, etc.)    │  │
│  │  JavaFX, Swing, AWT, Collections, IO, NIO, etc.          │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  Development Tools:                                             │
│  javac, javadoc, jar, jdb, javap, keytool, etc.               │
└─────────────────────────────────────────────────────────────────┘

JVM (Java Virtual Machine)

Определение и назначение

// JVM - виртуальная машина для исполнения Java байт-кода
// Обеспечивает платформонезависимость:
// "Write Once, Run Anywhere" (WORA)

// Java код → javac → .class файлы → JVM → нативный код
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}
// ↓ javac HelloWorld.java
// HelloWorld.class (байт-код)
// ↓ java HelloWorld
// JVM интерпретирует/компилирует в нативный код

Основные компоненты JVM

1. Class Loader Subsystem

// Загрузка классов происходит по иерархии:

// Bootstrap ClassLoader (встроенный, написан на C++)
// - Загружает классы из rt.jar (String, Object, System)
// - Родительский для всех остальных

// Extension ClassLoader  
// - Загружает классы из jre/lib/ext
// - Дочерний для Bootstrap

// Application ClassLoader
// - Загружает классы из CLASSPATH
// - Дочерний для Extension

public class ClassLoaderExample {
    public static void main(String[] args) {
        // Получение информации о ClassLoader
        System.out.println("String ClassLoader: " + String.class.getClassLoader());       // null (Bootstrap)
        System.out.println("Custom ClassLoader: " + ClassLoaderExample.class.getClassLoader()); // Application
    }
}

2. Runtime Data Areas

// Области памяти JVM:

// Method Area (Metaspace с Java 8+)
class MyClass {
    static int staticVar = 10;    // Хранится в Method Area
    public void method() { }      // Метаданные метода в Method Area
}

// Heap Memory
Object obj = new Object();        // Объект создается в Heap
String str = "Hello";            // String Pool (часть Heap с Java 7+)

// Stack Memory (для каждого потока)
public void method() {
    int localVar = 5;            // Локальная переменная в Stack
    Object ref = new Object();   // Ссылка в Stack, объект в Heap
}

// PC (Program Counter) Register
// - Содержит адрес текущей исполняемой инструкции
// - Отдельный для каждого потока

// Native Method Stack
// - Для native методов (JNI)
// - Написанных на C/C++

3. Execution Engine

// Interpreter - интерпретирует байт-код построчно
// JIT Compiler - компилирует "горячий" код в нативный

public class HotMethod {
    public static void main(String[] args) {
        for (int i = 0; i < 100000; i++) {
            calculate(i);  // Этот метод станет "горячим"
        }
    }
    
    public static int calculate(int x) {
        return x * x + 2 * x + 1;  // Будет скомпилирован JIT'ом
    }
}

// JIT компиляция происходит на основе счетчиков:
// - Метод вызывается часто → JIT компиляция
// - Оптимизации: inline, loop unrolling, dead code elimination

Реализации JVM

HotSpot JVM (Oracle/OpenJDK)

# Флаги для HotSpot JVM
-server              # Серверный режим (по умолчанию для серверных машин)
-client              # Клиентский режим (быстрый старт, меньше оптимизаций)

# JIT компиляция
-XX:CompileThreshold=10000     # Порог для JIT компиляции
-XX:+PrintCompilation          # Логирование JIT компиляции
-XX:+UnlockDiagnosticVMOptions # Разблокировка диагностических опций

OpenJ9 (Eclipse)

# IBM/Eclipse OpenJ9 - альтернативная JVM
# Особенности:
# - Меньшее потребление памяти
# - Быстрый старт приложений
# - Shared classes cache

-Xshareclasses        # Включение shared classes cache
-Xquickstart          # Быстрый старт (меньше JIT оптимизаций)

GraalVM

# GraalVM - полиглот VM
# Поддерживает Java, JavaScript, Python, R, Ruby

# Native Image - компиляция в нативный исполняемый файл
native-image MyApp

# Особенности:
# - Очень быстрый старт
# - Меньшее потребление памяти
# - Но нет JIT оптимизаций runtime

JVM аргументы

Память

# Heap
-Xms2g                    # Начальный размер heap
-Xmx4g                    # Максимальный размер heap
-Xmn1g                    # Размер Young Generation

# Stack
-Xss1m                    # Размер stack на поток

# Metaspace (Java 8+)
-XX:MetaspaceSize=128m    # Начальный размер Metaspace
-XX:MaxMetaspaceSize=512m # Максимальный размер Metaspace

# Direct Memory
-XX:MaxDirectMemorySize=1g # Максимальный размер Direct Memory

Garbage Collection

# Выбор GC
-XX:+UseSerialGC          # Serial GC
-XX:+UseParallelGC        # Parallel GC (по умолчанию)
-XX:+UseG1GC              # G1 GC
-XX:+UseZGC               # ZGC (Java 11+)
-XX:+UseShenandoahGC      # Shenandoah GC

# Настройки GC
-XX:MaxGCPauseMillis=200  # Цель по паузам GC (для G1)
-XX:ParallelGCThreads=4   # Количество потоков для parallel GC

Debugging и мониторинг

# JMX
-Dcom.sun.management.jmxremote=true
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.authenticate=false

# Heap dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dumps/

# GC логирование
-XX:+PrintGC
-XX:+PrintGCDetails
-Xloggc:gc.log

# Crash dumps
-XX:+CreateCoredumpOnCrash
-XX:ErrorFile=/path/to/error.log

JRE (Java Runtime Environment)

Состав JRE

JRE/
├── bin/
│   ├── java           # JVM лаунчер
│   ├── javaws         # Java Web Start
│   ├── keytool        # Управление сертификатами
│   └── ...
├── lib/
│   ├── rt.jar         # Стандартные библиотеки (до Java 8)
│   ├── charsets.jar   # Кодировки
│   ├── jce.jar        # Java Cryptography Extension
│   └── modules/       # Модули (Java 9+)
└── conf/
    ├── security/      # Настройки безопасности
    └── ...

Стандартные библиотеки

// Core Libraries
import java.lang.*;     // Object, String, Thread, System (автоматически)
import java.util.*;     // Collections, Date, Random
import java.io.*;       // File, InputStream, OutputStream
import java.nio.*;      // New I/O, ByteBuffer, Channel

// Сетевое программирование
import java.net.*;      // URL, Socket, ServerSocket
import java.net.http.*; // HTTP Client (Java 11+)

// GUI
import javax.swing.*;   // Swing компоненты
import java.awt.*;      // AWT компоненты
import javafx.*;        // JavaFX (отдельно с Java 11+)

// База данных
import java.sql.*;      // JDBC

// XML
import javax.xml.*;     // XML парсинг и обработка

// Безопасность
import java.security.*; // Криптография, сертификаты
import javax.crypto.*;  // Шифрование

// Reflection
import java.lang.reflect.*; // Class, Method, Field

// Аннотации
import java.lang.annotation.*;

// Concurrent programming
import java.util.concurrent.*; // ExecutorService, Future, etc.

Модульная система (Java 9+)

// module-info.java
module my.application {
    requires java.base;        // Базовый модуль (автоматически)
    requires java.sql;         // JDBC
    requires java.net.http;    // HTTP Client
    
    exports com.mycompany.api; // Экспорт пакета
    
    uses com.mycompany.spi.Service;    // Использование SPI
    provides com.mycompany.spi.Service  // Предоставление реализации SPI
        with com.mycompany.impl.ServiceImpl;
}

// Запуск модульного приложения
// java --module-path /path/to/modules -m my.application/com.mycompany.Main

JRE vs OpenJDK

# Oracle JRE (до Java 8 - бесплатная, с Java 11+ - коммерческая)
# - Включает Oracle-специфичные компоненты
# - Java Plugin для браузеров
# - Java Web Start
# - Коммерческая поддержка

# OpenJDK (открытая реализация)
# - Базовая функциональность Java
# - Без проприетарных компонентов Oracle
# - Бесплатная для всех версий
# - Основа для большинства дистрибутивов

JDK (Java Development Kit)

Структура JDK

JDK/
├── bin/                    # Инструменты разработки
│   ├── javac               # Компилятор Java
│   ├── java                # JVM лаунчер
│   ├── javadoc             # Генератор документации
│   ├── jar                 # Архиватор JAR
│   ├── jdb                 # Отладчик
│   ├── javap               # Дизассемблер
│   ├── jconsole            # JMX консоль
│   ├── jvisualvm           # Профайлер (до Java 8)
│   ├── jstat               # Статистика JVM
│   ├── jmap                # Heap dump и анализ
│   ├── jstack              # Thread dump
│   ├── keytool             # Управление ключами
│   └── ...
├── include/                # C/C++ заголовки для JNI
├── jmods/                  # Модули (Java 9+)
├── lib/                    # Библиотеки JDK
│   └── tools.jar           # Инструменты (до Java 8)
└── [JRE содержимое]        # Все компоненты JRE

Основные инструменты JDK

javac - компилятор

# Компиляция одного файла
javac HelloWorld.java

# Компиляция с classpath
javac -cp /path/to/libs/*.jar MyClass.java

# Установка версии target
javac -source 8 -target 8 MyClass.java

# Компиляция в определенную директорию
javac -d build/classes src/**/*.java

# Генерация отладочной информации
javac -g MyClass.java

# Включение предупреждений
javac -Xlint:all MyClass.java

# Модульная компиляция (Java 9+)
javac --module-path /path/to/modules -d build/modules src/module-info.java src/**/*.java

jar - архиватор

# Создание JAR файла
jar cf myapp.jar *.class

# Создание JAR с манифестом
jar cfm myapp.jar META-INF/MANIFEST.MF *.class

# Создание исполняемого JAR
jar cfe myapp.jar com.example.Main *.class

# Просмотр содержимого JAR
jar tf myapp.jar

# Извлечение из JAR
jar xf myapp.jar

# Обновление JAR
jar uf myapp.jar NewClass.class

javadoc - генератор документации

/**
 * Класс для демонстрации javadoc
 * @author Имя автора
 * @version 1.0
 * @since 1.0
 */
public class MyClass {
    
    /**
     * Вычисляет сумму двух чисел
     * @param a первое число
     * @param b второе число
     * @return сумма a и b
     * @throws IllegalArgumentException если a или b отрицательные
     */
    public int sum(int a, int b) {
        if (a < 0 || b < 0) {
            throw new IllegalArgumentException("Отрицательные числа не поддерживаются");
        }
        return a + b;
    }
}
# Генерация документации
javadoc -d docs src/*.java

# С включением приватных методов
javadoc -private -d docs src/*.java

# С определенным classpath
javadoc -cp /path/to/libs/*.jar -d docs src/*.java

Отладочные инструменты

jdb - отладчик
# Запуск с отладкой
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 MyClass

# Подключение jdb
jdb -attach 5005

# Команды jdb
# stop at MyClass:10    - точка останова на строке 10
# step                  - шаг
# next                  - следующая строка
# print variable        - печать переменной
# locals                - локальные переменные
# quit                  - выход
javap - дизассемблер
# Просмотр байт-кода
javap -c MyClass

# Подробная информация
javap -verbose MyClass

# Приватные методы
javap -private MyClass

# Пример вывода:
# Compiled from "MyClass.java"
# public class MyClass {
#   public int sum(int, int);
#     Code:
#        0: iload_1
#        1: iload_2
#        2: iadd
#        3: ireturn
# }

Мониторинг и профилирование

jstat - статистика JVM

# Статистика GC
jstat -gc <pid> 2s 10      # Каждые 2 сек, 10 раз

# Использование heap
jstat -gccapacity <pid>

# Статистика classloader
jstat -class <pid>

# Компиляция JIT
jstat -compiler <pid>

jmap - анализ heap

# Heap dump
jmap -dump:format=b,file=heap.hprof <pid>

# Гистограмма объектов
jmap -histo <pid>

# Информация о heap
jmap -heap <pid>

# Finalization queue
jmap -finalizerinfo <pid>

jstack - анализ потоков

# Thread dump
jstack <pid>

# Thread dump в файл
jstack <pid> > threads.txt

# Определение deadlock
jstack -l <pid>

jconsole - JMX консоль

# Локальное подключение
jconsole <pid>

# Удаленное подключение
jconsole <host>:<port>

# Мониторинг:
# - Memory usage
# - Thread count
# - Class loading
# - MBean operations

Версии JDK

Схема версионирования

# До Java 8: 1.x.x_update
java version "1.8.0_291"

# Java 9+: x.y.z
java version "17.0.1"

# LTS версии: 8, 11, 17, 21, ...
# Non-LTS: 9, 10, 12, 13, 14, 15, 16, 18, 19, 20, ...

Дистрибутивы JDK

# Oracle JDK
# - Коммерческая поддержка
# - Дополнительные инструменты (Mission Control)

# OpenJDK
# - Референсная реализация
# - Открытый исходный код

# Amazon Corretto
# - Бесплатная долгосрочная поддержка
# - Оптимизации для AWS

# Azul Zulu
# - TCK сертифицированный
# - Коммерческая поддержка

# Eclipse Temurin (AdoptOpenJDK)
# - Популярный бинарный дистрибутив
# - Бесплатный

# Red Hat OpenJDK
# - Интеграция с Red Hat экосистемой

# GraalVM
# - Полиглот VM
# - Native Image компиляция

Примеры использования

Создание проекта

# Структура проекта
mkdir -p myproject/src/main/java/com/example
mkdir -p myproject/src/test/java/com/example
mkdir -p myproject/lib
mkdir -p myproject/docs

# Создание исходного кода
cat > myproject/src/main/java/com/example/App.java << 'EOF'
package com.example;

public class App {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}
EOF

# Компиляция
cd myproject
javac -d build/classes src/main/java/com/example/*.java

# Создание JAR
jar cfe app.jar com.example.App -C build/classes .

# Запуск
java -jar app.jar

Работа с модулями (Java 9+)

# module-info.java
cat > myproject/src/main/java/module-info.java << 'EOF'
module com.example.app {
    requires java.base;
    exports com.example;
}
EOF

# Компиляция модуля
javac -d build/modules --module-source-path src/main/java src/main/java/module-info.java src/main/java/com/example/*.java

# Создание модульного JAR
jar --create --file=app.jar --main-class=com.example.App -C build/modules .

# Запуск модуля
java --module-path app.jar -m com.example.app/com.example.App

Выбор между JVM, JRE, JDK

Когда что использовать

Только запуск приложений

# Нужна только JRE
# - Запуск готовых Java приложений
# - Серверное окружение для деплоя
# - Клиентские машины для Java апплетов/приложений

# Установка только JRE (если доступна)
sudo apt-get install openjdk-11-jre

Разработка приложений

# Нужен полный JDK
# - Компиляция исходного кода
# - Создание JAR файлов
# - Генерация документации
# - Отладка и профилирование

# Установка JDK
sudo apt-get install openjdk-11-jdk

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

# Проверка JRE
java -version

# Проверка JDK
javac -version

# Определение JAVA_HOME
echo $JAVA_HOME

# Поиск Java установок (Linux)
sudo update-alternatives --config java

Настройка окружения

Переменные среды

# Linux/macOS ~/.bashrc или ~/.zshrc
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk
export PATH=$JAVA_HOME/bin:$PATH

# Windows
set JAVA_HOME=C:\Program Files\Java\jdk-11
set PATH=%JAVA_HOME%\bin;%PATH%

# Проверка
echo $JAVA_HOME
java -version
javac -version

Управление версиями Java

# SDKMAN (Linux/macOS)
curl -s "https://get.sdkman.io" | bash
sdk install java 11.0.12-open
sdk use java 11.0.12-open
sdk default java 11.0.12-open

# jenv (macOS)
brew install jenv
jenv add /Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home
jenv global 11.0

# Windows
# Chocolatey
choco install openjdk11

Лучшие практики

1. Выбор версии JDK

# Для продакшн приложений - LTS версии
# Java 8 - до 2030
# Java 11 - до 2026  
# Java 17 - до 2029
# Java 21 - до 2031

# Для новых проектов - последняя LTS (Java 17+)
# Для экспериментов - последняя версия

2. Настройка JVM для продакшн

# Базовые настройки для веб-приложения
-Xms2g -Xmx2g                    # Фиксированный heap
-XX:+UseG1GC                     # G1 для низкой latency
-XX:MaxGCPauseMillis=200         # Цель по паузам
-XX:+HeapDumpOnOutOfMemoryError  # Диагностика OOM
-XX:+ExitOnOutOfMemoryError      # Перезапуск при OOM
-Dfile.encoding=UTF-8            # Кодировка
-Duser.timezone=UTC              # Временная зона

3. Мониторинг JVM

// Программный мониторинг
import java.lang.management.*;

public class JVMMonitor {
    public static void printJVMInfo() {
        RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
        MemoryMXBean memory = ManagementFactory.getMemoryMXBean();
        
        System.out.println("JVM Name: " + runtime.getVmName());
        System.out.println("JVM Version: " + runtime.getVmVersion());
        System.out.println("Uptime: " + runtime.getUptime() + " ms");
        
        MemoryUsage heapUsage = memory.getHeapMemoryUsage();
        System.out.println("Heap Used: " + heapUsage.getUsed() / 1024 / 1024 + " MB");
        System.out.println("Heap Max: " + heapUsage.getMax() / 1024 / 1024 + " MB");
    }
}

4. Безопасность

# Отключение устаревших алгоритмов
-Djdk.tls.disabledAlgorithms=SSLv3,RC4,MD5withRSA

# Настройка Trust Store
-Djavax.net.ssl.trustStore=/path/to/truststore.jks
-Djavax.net.ssl.trustStorePassword=password

# Ограничение сериализации
-Djdk.serialFilter=!com.untrusted.**;java.base/**;!*

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

String Pool и другие pool в Java

1. String Pool (Intern Pool)

Что это такое

String Pool — специальная область памяти в heap'е, где хранятся уникальные строковые литералы для экономии памяти.

Расположение в памяти

Java 7-:  PermGen (Method Area)
Java 8+:  Heap (обычная heap память)

Как работает String Pool

Создание строк

// Литералы попадают в String Pool автоматически
String s1 = "Hello";        // В String Pool
String s2 = "Hello";        // Ссылка на тот же объект из Pool
String s3 = new String("Hello"); // Новый объект в heap, НЕ в Pool

System.out.println(s1 == s2);        // true - одинаковые ссылки
System.out.println(s1 == s3);        // false - разные объекты
System.out.println(s1.equals(s3));   // true - одинаковое содержимое

Метод intern()

String s1 = "Hello";
String s2 = new String("Hello");
String s3 = s2.intern();             // Возвращает ссылку из Pool

System.out.println(s1 == s2);        // false
System.out.println(s1 == s3);        // true
System.out.println(s2 == s3);        // false

Конкатенация строк

// Compile-time константы попадают в Pool
String s1 = "Hello" + "World";       // "HelloWorld" в Pool
final String hello = "Hello";
String s2 = hello + "World";         // "HelloWorld" в Pool (compile-time)

// Runtime конкатенация НЕ попадает в Pool
String hello2 = "Hello";
String s3 = hello2 + "World";        // Новый объект в heap
String s4 = s3.intern();             // Теперь в Pool

Настройка String Pool

Размер String Pool

# Java 7+
-XX:StringTableSize=<size>            # Количество bucket'ов (по умолчанию 60013)

# Примеры
-XX:StringTableSize=120000            # Для приложений с множеством строк
-XX:StringTableSize=1000000           # Для очень больших приложений

Мониторинг String Pool

# Статистика String Pool
jmap -dump:format=b,file=heap.hprof <pid>

# Анализ в Eclipse MAT или VisualVM
# Поиск по классу java.lang.String

Оптимизация String Deduplication (Java 8u20+)

# Включить дедупликацию строк (только для G1 GC)
-XX:+UseG1GC
-XX:+UseStringDeduplication

# Настройки дедупликации
-XX:StringDeduplicationAgeThreshold=3  # Возраст объекта для дедупликации

Практические примеры

Хорошие практики

// Используйте литералы когда возможно
String status = "ACTIVE";             // Хорошо

// Кэшируйте часто используемые строки
private static final String DEFAULT_ENCODING = "UTF-8";

// Используйте StringBuilder для построения строк
StringBuilder sb = new StringBuilder();
sb.append("Hello").append(" ").append("World");
String result = sb.toString();

Избегайте

// Избегайте new String() без необходимости
String bad = new String("Hello");     // Плохо - создает лишний объект

// Не делайте intern() часто используемых строк
String userInput = scanner.nextLine();
String cached = userInput.intern();   // Может переполнить String Pool

2. Integer Pool (Integer Cache)

Диапазон кэширования

// Кэшируются значения от -128 до 127 (по умолчанию)
Integer a = 100;                      // Из кэша
Integer b = 100;                      // Из кэша
System.out.println(a == b);           // true

Integer c = 200;                      // Новый объект
Integer d = 200;                      // Новый объект
System.out.println(c == d);           // false

Настройка Integer Cache

# Изменить верхнюю границу кэша
-XX:AutoBoxCacheMax=<value>           # По умолчанию 127

# Пример
-XX:AutoBoxCacheMax=1000             # Кэшировать до 1000

Реализация Integer Cache

// Внутренняя реализация (упрощенно)
private static class IntegerCache {
    static final int low = -128;
    static final int high = 127;        // Может быть изменено через -XX:AutoBoxCacheMax
    static final Integer cache[] = new Integer[high - low + 1];
    
    static {
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);
    }
}

Другие примитивные кэши

// Boolean - кэшируются TRUE и FALSE
Boolean b1 = true;                    // Из кэша
Boolean b2 = Boolean.valueOf(true);   // Из кэша
System.out.println(b1 == b2);         // true

// Byte - кэшируются все значения (-128 до 127)
Byte byte1 = 100;                     // Из кэша
Byte byte2 = 100;                     // Из кэша
System.out.println(byte1 == byte2);   // true

// Short - кэшируются от -128 до 127
Short short1 = 100;                   // Из кэша
Short short2 = 100;                   // Из кэша
System.out.println(short1 == short2); // true

// Character - кэшируются от 0 до 127
Character char1 = 'A';                // Из кэша (65)
Character char2 = 'A';                // Из кэша
System.out.println(char1 == char2);   // true

// Long - НЕ кэшируется по умолчанию
Long long1 = 100L;                    // Новый объект
Long long2 = 100L;                    // Новый объект
System.out.println(long1 == long2);   // false

3. Connection Pool

Зачем нужен Connection Pool

  • Дорогие операции — создание/закрытие соединений
  • Ограничения БД — лимит одновременных соединений
  • Производительность — переиспользование соединений

Популярные реализации

HikariCP (самый популярный)

// Настройка HikariCP
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("user");
config.setPassword("password");

// Настройки пула
config.setMaximumPoolSize(20);        // Максимум соединений
config.setMinimumIdle(5);             // Минимум idle соединений
config.setConnectionTimeout(30000);   // Таймаут получения соединения
config.setIdleTimeout(300000);        // Таймаут idle соединения
config.setMaxLifetime(1800000);       // Максимальное время жизни соединения

HikariDataSource dataSource = new HikariDataSource(config);

Apache DBCP2

// Настройка DBCP2
BasicDataSource dataSource = new BasicDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/mydb");
dataSource.setUsername("user");
dataSource.setPassword("password");

dataSource.setInitialSize(5);         // Начальный размер пула
dataSource.setMaxTotal(20);           // Максимум соединений
dataSource.setMaxIdle(10);            // Максимум idle соединений
dataSource.setMinIdle(5);             // Минимум idle соединений
dataSource.setMaxWaitMillis(30000);   // Максимальное время ожидания

C3P0

// Настройка C3P0
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
dataSource.setUser("user");
dataSource.setPassword("password");

dataSource.setMinPoolSize(5);
dataSource.setMaxPoolSize(20);
dataSource.setAcquireIncrement(5);
dataSource.setMaxIdleTime(300);

Лучшие практики Connection Pool

// Всегда используйте try-with-resources
try (Connection conn = dataSource.getConnection();
     PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
    
    stmt.setInt(1, userId);
    ResultSet rs = stmt.executeQuery();
    // обработка результатов
} // соединение автоматически возвращается в пул

Мониторинг Connection Pool

// HikariCP предоставляет метрики
HikariPoolMXBean poolMXBean = dataSource.getHikariPoolMXBean();
System.out.println("Active connections: " + poolMXBean.getActiveConnections());
System.out.println("Idle connections: " + poolMXBean.getIdleConnections());
System.out.println("Total connections: " + poolMXBean.getTotalConnections());

4. Thread Pool

Зачем нужен Thread Pool

  • Дорогие операции — создание/уничтожение потоков
  • Контроль ресурсов — ограничение количества потоков
  • Переиспользование — один поток выполняет множество задач

Типы Thread Pool в Java

Fixed Thread Pool

// Фиксированное количество потоков
ExecutorService executor = Executors.newFixedThreadPool(5);

// Выполнение задач
executor.submit(() -> {
    System.out.println("Task executed by " + Thread.currentThread().getName());
});

// Обязательно закрывайте executor
executor.shutdown();

Cached Thread Pool

// Создает потоки по требованию, переиспользует idle потоки
ExecutorService executor = Executors.newCachedThreadPool();

// Подходит для коротких асинхронных задач
executor.submit(() -> {
    // быстрая задача
});

Single Thread Executor

// Один поток для последовательного выполнения
ExecutorService executor = Executors.newSingleThreadExecutor();

// Гарантирует порядок выполнения
executor.submit(() -> System.out.println("Task 1"));
executor.submit(() -> System.out.println("Task 2"));

Scheduled Thread Pool

// Для задач с расписанием
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);

// Выполнить через 5 секунд
executor.schedule(() -> System.out.println("Delayed task"), 5, TimeUnit.SECONDS);

// Выполнять каждые 10 секунд
executor.scheduleAtFixedRate(() -> System.out.println("Periodic task"), 0, 10, TimeUnit.SECONDS);

Кастомный Thread Pool

// Создание кастомного ThreadPoolExecutor
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5,                              // corePoolSize
    10,                             // maximumPoolSize
    60L, TimeUnit.SECONDS,          // keepAliveTime
    new LinkedBlockingQueue<>(100), // workQueue
    new ThreadFactory() {           // threadFactory
        private int counter = 0;
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, "CustomThread-" + counter++);
        }
    },
    new ThreadPoolExecutor.CallerRunsPolicy() // rejectedExecutionHandler
);

Политики отклонения задач

// AbortPolicy - бросает RejectedExecutionException (по умолчанию)
new ThreadPoolExecutor.AbortPolicy()

// CallerRunsPolicy - выполняет задачу в вызывающем потоке
new ThreadPoolExecutor.CallerRunsPolicy()

// DiscardPolicy - молча отбрасывает задачу
new ThreadPoolExecutor.DiscardPolicy()

// DiscardOldestPolicy - отбрасывает самую старую задачу
new ThreadPoolExecutor.DiscardOldestPolicy()

Мониторинг Thread Pool

ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(5);

// Мониторинг
System.out.println("Active threads: " + executor.getActiveCount());
System.out.println("Pool size: " + executor.getPoolSize());
System.out.println("Queue size: " + executor.getQueue().size());
System.out.println("Completed tasks: " + executor.getCompletedTaskCount());

5. Object Pool

Когда использовать Object Pool

  • Дорогие объекты — создание требует много ресурсов
  • Ограниченные ресурсы — файлы, сокеты, БД соединения
  • Частое создание/уничтожение — много временных объектов

Простая реализация Object Pool

public class ObjectPool<T> {
    private final Queue<T> pool = new ConcurrentLinkedQueue<>();
    private final Supplier<T> objectFactory;
    private final int maxSize;
    
    public ObjectPool(Supplier<T> objectFactory, int maxSize) {
        this.objectFactory = objectFactory;
        this.maxSize = maxSize;
    }
    
    public T borrowObject() {
        T object = pool.poll();
        return object != null ? object : objectFactory.get();
    }
    
    public void returnObject(T object) {
        if (pool.size() < maxSize) {
            // Сбросить состояние объекта
            if (object instanceof Resetable) {
                ((Resetable) object).reset();
            }
            pool.offer(object);
        }
    }
}

Пример использования

// Пул для StringBuilder
ObjectPool<StringBuilder> stringBuilderPool = new ObjectPool<>(
    () -> new StringBuilder(256),  // Фабрика объектов
    10                             // Максимум объектов в пуле
);

// Использование
StringBuilder sb = stringBuilderPool.borrowObject();
try {
    sb.append("Hello").append(" World");
    String result = sb.toString();
} finally {
    sb.setLength(0);               // Очистить
    stringBuilderPool.returnObject(sb);
}

Apache Commons Pool

// Фабрика объектов
class StringBuilderFactory extends BasePooledObjectFactory<StringBuilder> {
    @Override
    public StringBuilder create() {
        return new StringBuilder();
    }
    
    @Override
    public PooledObject<StringBuilder> wrap(StringBuilder obj) {
        return new DefaultPooledObject<>(obj);
    }
    
    @Override
    public void passivateObject(PooledObject<StringBuilder> p) {
        p.getObject().setLength(0);  // Сбросить состояние
    }
}

// Создание пула
GenericObjectPool<StringBuilder> pool = new GenericObjectPool<>(new StringBuilderFactory());
pool.setMaxTotal(10);
pool.setMaxIdle(5);
pool.setMinIdle(2);

// Использование
StringBuilder sb = pool.borrowObject();
try {
    sb.append("Hello World");
    String result = sb.toString();
} finally {
    pool.returnObject(sb);
}

6. Сравнение производительности

String Pool vs new String()

// Тест производительности
public class StringPoolTest {
    public static void main(String[] args) {
        int iterations = 1000000;
        
        // Тест 1: Литералы (используют String Pool)
        long start = System.currentTimeMillis();
        for (int i = 0; i < iterations; i++) {
            String s = "Hello World";
        }
        long end = System.currentTimeMillis();
        System.out.println("String literals: " + (end - start) + "ms");
        
        // Тест 2: new String()
        start = System.currentTimeMillis();
        for (int i = 0; i < iterations; i++) {
            String s = new String("Hello World");
        }
        end = System.currentTimeMillis();
        System.out.println("new String(): " + (end - start) + "ms");
        
        // Результат: литералы значительно быстрее
    }
}

Autoboxing Performance

// Тест autoboxing
public class AutoboxingTest {
    public static void main(String[] args) {
        int iterations = 10000000;
        
        // Тест 1: Кэшированные значения
        long start = System.currentTimeMillis();
        for (int i = 0; i < iterations; i++) {
            Integer val = 100;  // Из кэша
        }
        long end = System.currentTimeMillis();
        System.out.println("Cached integers: " + (end - start) + "ms");
        
        // Тест 2: Некэшированные значения
        start = System.currentTimeMillis();
        for (int i = 0; i < iterations; i++) {
            Integer val = 1000; // Новый объект
        }
        end = System.currentTimeMillis();
        System.out.println("Non-cached integers: " + (end - start) + "ms");
    }
}

7. Best Practices

String Pool

// ✅ Хорошие практики
// Использовать литералы для константных строк
private static final String STATUS_ACTIVE = "ACTIVE";

// Использовать StringBuilder для построения строк
StringBuilder sb = new StringBuilder();
sb.append("Hello").append(" ").append("World");

// Кэшировать только нужные строки
String cached = computeExpensiveString().intern();

// ❌ Избегать
// Не создавать String через new без необходимости
String bad = new String("Hello");

// Не делать intern() для пользовательского ввода
String userInput = scanner.nextLine().intern(); // Может переполнить пул

Integer Cache

// ✅ Хорошие практики
// Использовать valueOf() вместо new Integer()
Integer good = Integer.valueOf(100);

// Понимать диапазон кэширования
if (value >= -128 && value <= 127) {
    // Будет использован кэш
}

// ❌ Избегать
// Не использовать == для сравнения Integer вне кэша
Integer a = 200;
Integer b = 200;
if (a == b) { // false! Используйте equals()
    // Никогда не выполнится
}

Connection Pool

// ✅ Хорошие практики
// Всегда используйте try-with-resources
try (Connection conn = dataSource.getConnection()) {
    // работа с соединением
} // автоматически возвращается в пул

// Настраивайте размер пула под нагрузку
// Для веб-приложений: обычно 10-50 соединений
// Для batch-обработки: может быть больше

// ❌ Избегать
// Не забывайте закрывать соединения
Connection conn = dataSource.getConnection();
// ... работа ...
// conn.close(); // Забыли закрыть - утечка ресурсов

Thread Pool

// ✅ Хорошие практики
// Всегда закрывайте ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(5);
try {
    // работа с executor
} finally {
    executor.shutdown();
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        executor.shutdownNow();
    }
}

// Используйте подходящий тип пула
// - FixedThreadPool для CPU-интенсивных задач
// - CachedThreadPool для I/O-интенсивных задач
// - SingleThreadExecutor для последовательных задач

// ❌ Избегать
// Не создавайте потоки вручную для простых задач
new Thread(() -> {
    // задача
}).start(); // Плохо - используйте Thread Pool

8. Мониторинг и диагностика

Мониторинг String Pool

# Анализ String Pool в heap dump
jmap -dump:format=b,file=heap.hprof <pid>

# В Eclipse MAT:
# 1. Найти java.lang.String
# 2. Посмотреть на duplicate strings
# 3. Проанализировать размер String Pool

Мониторинг Thread Pool

// JMX мониторинг
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(5);

// Метрики
System.out.println("Active: " + executor.getActiveCount());
System.out.println("Pool size: " + executor.getPoolSize());
System.out.println("Queue size: " + executor.getQueue().size());
System.out.println("Completed: " + executor.getCompletedTaskCount());

// Настройка алертов
if (executor.getQueue().size() > 100) {
    // Слишком много задач в очереди
    logger.warn("Thread pool queue is getting full");
}

Профилирование

// Измерение времени выполнения
long start = System.nanoTime();
// операция
long duration = System.nanoTime() - start;
System.out.println("Duration: " + duration / 1_000_000 + "ms");

// Использование JMH для микробенчмарков
@Benchmark
public void testStringPool() {
    String s = "Hello World";
}

@Benchmark
public void testNewString() {
    String s = new String("Hello World");
}

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

Java Generics

Основные концепции

Generics — механизм параметризации типов, введенный в Java 5. Позволяет создавать классы, интерфейсы и методы с параметрами типов, обеспечивая type safety на этапе компиляции.

Type Parameter — формальный параметр типа (например, T, E, K, V) Type Argument — конкретный тип, передаваемый параметру (например, String, Integer)

// T - type parameter, String - type argument
List<String> names = new ArrayList<String>();

Базовый синтаксис

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

public class Box<T> {
    private T content;
    
    public void set(T content) { this.content = content; }
    public T get() { return content; }
}

// Использование
Box<String> stringBox = new Box<>();
Box<Integer> intBox = new Box<>();

Параметризованные методы

public class Utils {
    // Generic метод - <T> перед возвращаемым типом
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    
    // Множественные параметры типов
    public static <T, U> boolean compare(Pair<T, U> p1, Pair<T, U> p2) {
        return p1.getFirst().equals(p2.getFirst()) && 
               p1.getSecond().equals(p2.getSecond());
    }
}

Bounded Type Parameters (Ограниченные типы)

Upper Bounded Wildcards

// T должен быть Number или его наследником
public class NumberBox<T extends Number> {
    private T value;
    
    public double getDoubleValue() {
        return value.doubleValue(); // Можем вызывать методы Number
    }
}

// Множественные границы
public class ComparableBox<T extends Number & Comparable<T>> {
    // T должен наследовать Number И реализовывать Comparable
}

Lower Bounded Wildcards

// Можем добавлять Integer и его супертипы
public void addNumbers(List<? super Integer> list) {
    list.add(42);        // OK
    list.add(42.0);      // Compile error - Double не является Integer
}

Wildcards (Подстановочные знаки)

Unbounded Wildcards

// Принимает List любого типа
public void printList(List<?> list) {
    for (Object item : list) {
        System.out.println(item);
    }
}

PECS принцип (Producer Extends, Consumer Super)

// Producer - используем extends для чтения
public double sumNumbers(List<? extends Number> numbers) {
    double sum = 0;
    for (Number num : numbers) {
        sum += num.doubleValue(); // Читаем - безопасно
    }
    return sum;
}

// Consumer - используем super для записи
public void addIntegers(List<? super Integer> list) {
    list.add(10);    // Записываем - безопасно
    list.add(20);
}

Type Erasure (Стирание типов)

Ключевая особенность: Java удаляет информацию о типах на этапе компиляции для обратной совместимости.

// Во время выполнения оба списка имеют тип List
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();

// Это приведет к ClassCastException во время выполнения
List raw = strings;
raw.add(42); // Компилируется, но опасно!

Мостовые методы (Bridge Methods)

class Node<T> {
    public T data;
    public void setData(T data) { this.data = data; }
}

class MyNode extends Node<String> {
    // Компилятор создаст мостовой метод:
    // public void setData(Object data) { setData((String) data); }
    
    public void setData(String data) { this.data = data; }
}

Ограничения Generics

Нельзя создавать экземпляры типов

public class GenericClass<T> {
    // Compile error
    // private T instance = new T();
    
    // Решение через Class<T>
    private Class<T> type;
    public GenericClass(Class<T> type) { this.type = type; }
    
    public T createInstance() throws Exception {
        return type.getDeclaredConstructor().newInstance();
    }
}

Нельзя создавать массивы параметризованных типов

// Compile error
// List<String>[] arrays = new List<String>[10];

// Решение
@SuppressWarnings("unchecked")
List<String>[] arrays = (List<String>[]) new List[10];

Продвинутые паттерны

Type Tokens

// Для сохранения информации о типе во время выполнения
public abstract 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>>() {};

Recursive Type Bounds

// Enum pattern
public class Enum<E extends Enum<E>> implements Comparable<E> {
    // Реализация
}

// Builder pattern
public class Builder<T extends Builder<T>> {
    @SuppressWarnings("unchecked")
    protected T self() { return (T) this; }
    
    public T setValue(String value) {
        // Логика
        return self();
    }
}

Практические примеры

Типобезопасный Factory

public class Factory {
    private static final Map<Class<?>, Supplier<?>> creators = new HashMap<>();
    
    @SuppressWarnings("unchecked")
    public static <T> void register(Class<T> type, Supplier<T> creator) {
        creators.put(type, creator);
    }
    
    @SuppressWarnings("unchecked")
    public static <T> T create(Class<T> type) {
        Supplier<T> creator = (Supplier<T>) creators.get(type);
        return creator != null ? creator.get() : null;
    }
}

Generic Repository Pattern

public interface Repository<T, ID> {
    Optional<T> findById(ID id);
    List<T> findAll();
    T save(T entity);
    void deleteById(ID id);
}

public class JpaRepository<T, ID> implements Repository<T, ID> {
    private final Class<T> entityClass;
    
    public JpaRepository(Class<T> entityClass) {
        this.entityClass = entityClass;
    }
    
    @Override
    public Optional<T> findById(ID id) {
        // JPA логика с использованием entityClass
        return Optional.empty();
    }
}

Частые ошибки и как их избежать

Raw Types

// Плохо - теряем type safety
List list = new ArrayList();
list.add("string");
list.add(42);

// Хорошо
List<Object> list = new ArrayList<>();

Неправильное использование wildcards

// Неэффективно - слишком ограничивающий тип
public void process(List<Object> list) { /* ... */ }

// Лучше - принимаем любой тип
public void process(List<?> list) { /* ... */ }

Вопросы для собеседования

В1: Чем отличается List<?> от List<Object>? Ответ: List<?> может содержать любой тип, но мы не можем добавлять элементы (кроме null). List<Object> может содержать только Object и его наследники, но позволяет добавление.

В2: Почему нельзя создать массив List<String>[]? Ответ: Из-за type erasure массивы не могут гарантировать type safety для параметризованных типов, что может привести к ClassCastException.

В3: Что такое heap pollution? Ответ: Ситуация, когда переменная параметризованного типа ссылается на объект, который не является экземпляром этого типа. Возникает при смешивании raw types и generics.

Эта шпаргалка покрывает основные концепции Java Generics, необходимые для успешного прохождения собеседования на позицию Senior Java Backend разработчика.