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():
- Рефлексивность:
x.equals(x)
должно быть true - Симметричность: если
x.equals(y)
true, тоy.equals(x)
тоже true - Транзитивность: если
x.equals(y)
иy.equals(z)
true, тоx.equals(z)
тоже true - Постоянство: результат не должен изменяться без изменения объектов
- Для null:
x.equals(null)
должно быть false
Правила для hashCode():
- Если объекты равны по equals(), их hashCode() должны быть равны
- Если hashCode() равны, объекты не обязательно равны
- 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
Лучшие практики
- Используйте Arrays.toString() для вывода массивов
- Проверяйте границы массива перед обращением к элементам
- Используйте for-each для простого перебора без изменения элементов
- Предпочитайте коллекции для динамических данных
- Используйте Arrays.copyOf() вместо ручного копирования
- Инициализируйте массивы при объявлении, когда это возможно
- Используйте константы для размеров массивов
- Документируйте предположения о размере и содержимом массивов
Сравнение с коллекциями
Характеристика | Массивы | Коллекции |
---|---|---|
Размер | Фиксированный | Динамический |
Типы данных | Примитивы и объекты | Только объекты |
Производительность | Высокая | Немного ниже |
Функциональность | Ограниченная | Богатая |
Синтаксис | Простой | Более сложный |
Безопасность типов | Полная | Полная (с 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)
);
Контракт сравнения
Требования для корректной реализации:
- Антирефлексивность:
compare(a, a) == 0
- Антисимметричность: если
compare(a, b) > 0
, тоcompare(b, a) < 0
- Транзитивность: если
compare(a, b) > 0
иcompare(b, c) > 0
, тоcompare(a, c) > 0
- Согласованность с 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);
Лучшие практики
-
Используйте интерфейсы в объявлениях:
Map<String, Integer> map = new HashMap<>(); // ХОРОШО HashMap<String, Integer> map = new HashMap<>(); // ПЛОХО
-
Задавайте начальную емкость для HashMap:
Map<String, Integer> map = new HashMap<>(expectedSize * 4 / 3);
-
Правильно реализуйте equals() и hashCode() для ключей
-
Используйте неизменяемые объекты как ключи
-
Для сортированных данных выбирайте TreeMap
-
Для многопоточности используйте ConcurrentHashMap
-
Проверяйте на 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 │
└─────────────────────────────────────────────────────────────┘
Жизненный цикл объектов
- Создание — объекты создаются в Eden space
- Первая сборка — выжившие объекты → Survivor 0
- Вторая сборка — объекты → Survivor 1
- Повышение — после N сборок → Old Generation
- Финализация — объекты без ссылок удаляются
Типы ссылок в 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:
- Young-only — сборка только Young Generation
- Concurrent Marking — маркировка старых объектов
- Mixed — сборка Young + часть Old Generation
- 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. Тюнинг производительности
Метрики для оценки
-
Throughput — процент времени работы приложения
Throughput = (Total time - GC time) / Total time
-
Latency — время пауз GC
- P50, P95, P99 значения пауз
- Максимальная пауза
-
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 паузы
Диагностика:
- Анализ GC логов
- Проверка размеров поколений
- Поиск 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
- Начните с параметров по умолчанию
- Измеряйте baseline производительность
- Меняйте один параметр за раз
- Тестируйте под реальной нагрузкой
- Мониторьте в 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 разработчика.