Skip to content

Java Шпаргалка

Корпоративный объектно-ориентированный язык, напиши один раз, запускай где угодно.

01

Начало работы

Hello World

Каждая программа Java начинается с main(). Имя файла должно совпадать с именем публичного класса (Main.java → Main.class). javac компилирует в байткод (.class), java запускает его на JVM. System.out.println печатает в stdout; printf поддерживает спецификаторы формата (%s, %d, %f, %n для новой строки).

java
// Main.java
public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
        System.out.printf("Name: %s, Age: %d%n", "Alice", 30);
    }
}

// Compile: javac Main.java  -> Main.class
// Run:     java Main
// Package: java -cp . com.example.Main

// Every Java program needs:
// 1. A class (public class, file name must match)
// 2. A main method: public static void main(String[] args)

Переменные и примитивные типы

Java имеет 8 примитивных типов (int, long, double, float, boolean, char, byte, short) и ссылочные типы (String, массивы, объекты). Используйте 'final' для констант. 'var' (Java 10+) выводит тип при компиляции — используйте для локальных переменных с очевидными типами. Подчёркивания в числах (100_000) улучшают читаемость.

java
// Primitive types (8 total)
int age = 30;              // 32-bit integer
long bigNum = 100_000L;   // 64-bit integer
double price = 19.99;     // 64-bit float (default for decimals)
float pi = 3.14f;         // 32-bit float
boolean active = true;    // true/false
char grade = 'A';         // 16-bit Unicode character
byte b = 127;             // 8-bit signed
short s = 32767;          // 16-bit signed

// Reference types
String name = "Alice";    // Object (not primitive)
int[] nums = {1, 2, 3};  // Array object

// Constants
final double PI = 3.14159; // can't be reassigned

// var (Java 10+, local type inference)
var count = 42;    // inferred as int
var list = new ArrayList<String>(); // inferred as ArrayList<String>

Wrapper-классы и boxing

Wrapper-классы (Integer, Double, Boolean и т.д.) — объектные версии примитивов. Autoboxing/unboxing конвертирует автоматически. Integer кэширует значения от -128 до 127, поэтому == работает для маленьких чисел, но не для больших — всегда используйте .equals(). Wrappers нужны для Collections (которые не могут хранить примитивы).

java
// Wrapper classes (Object versions of primitives)
Integer wrapped = Integer.valueOf(42);  // explicit
Integer auto = 42;                       // autoboxing
int unboxed = auto;                      // unboxing

// Useful methods
int max = Integer.MAX_VALUE;     // 2147483647
String bin = Integer.toBinaryString(42);
int parsed = Integer.parseInt("42");
String s = String.valueOf(42);

// Other wrappers: Double, Boolean, Character, Long, Float
Double d = 3.14;
Boolean b = Boolean.TRUE;
Character c = 'A';

// Be careful with == on wrappers
Integer a = 127, b2 = 127;  // a == b2: true (cached)
Integer x = 128, y = 128;   // x == y: false (not cached)
// Always use .equals() for Integer comparison

Пакеты и импорты

Пакеты организуют классы и предотвращают конфликты имён. Конвенция: обратное доменное имя (com.example.app). Импортируйте конкретные классы или используйте wildcards (*). Static imports приносят константы и методы (Math.PI, Math.sqrt). Полные квалифицированные имена работают без импортов, но многословны. Пакет java.lang авто-импортируется.

java
// Package declaration (must be first line)
package com.example.app;

// Import specific class
import java.util.List;
import java.util.ArrayList;

// Import all classes in a package
import java.util.*;

// Static import (for static members)
import static java.lang.Math.PI;
import static java.lang.Math.sqrt;

// Usage
double area = PI * 5 * 5;
double root = sqrt(16);

// Fully qualified name (no import needed)
java.time.LocalDate today = java.time.LocalDate.now();

// Package naming convention: reverse domain
// com.google.gson, org.apache.commons, io.netty.channel

Ввод и вывод

System.out (stdout), System.err (stderr), System.in (stdin). Scanner — простейший способ читать консольный ввод — он парсит токены (nextInt, nextDouble, nextLine). Всегда закрывайте Scanner для освобождения ресурсов. Аргументы командной строки в args[] (args[0] — первый аргумент, а не имя программы, как в C).

java
import java.util.Scanner;

// Console output
System.out.println("Hello");      // with newline
System.out.print("No newline");   // without newline
System.out.printf("Pi: %.2f%n", 3.14159); // formatted

// Console input with Scanner
Scanner scanner = new Scanner(System.in);
System.out.print("Enter name: ");
String name = scanner.nextLine();

System.out.print("Enter age: ");
int age = scanner.nextInt();

System.out.printf("Hi %s, age %d%n", name, age);
scanner.close(); // always close

// Command-line arguments
// java Main arg1 arg2
// args[0] = "arg1", args[1] = "arg2"
02

Строки и форматирование

Методы String

Строки неизменяемы — методы возвращают новые строки. Всегда используйте .equals() для сравнения содержимого (== сравнивает ссылки). compareTo() возвращает отрицательное/ноль/положительное для упорядочивания (полезно для сортировки). split() возвращает String[]. Для изменяемых строк используйте StringBuilder.

java
String s = "Hello, World";

// Length and access
int len = s.length();        // 12
char c = s.charAt(0);        // 'H'

// Comparison
s.equals("Hello, World");    // true (content comparison)
s.equalsIgnoreCase("hello, world"); // true
s.compareTo("Apple");        // positive (s > "Apple")
"abc".compareTo("abd");      // negative

// Search
s.indexOf("World");          // 7 (-1 if not found)
s.lastIndexOf("l");          // 10
s.contains("World");         // true
s.startsWith("Hello");       // true
s.endsWith("World");         // true

// Extract
s.substring(7);              // "World"
s.substring(0, 5);           // "Hello"

// Transform
s.toUpperCase();             // "HELLO, WORLD"
s.toLowerCase();             // "hello, world"
s.replace("o", "0");         // "Hell0, W0rld"
s.trim();                    // remove whitespace
s.split(", ");               // ["Hello", "World"]

StringBuilder и конкатенация

Конкатенация строк с + создаёт новую String каждый раз (неэффективно в циклах). StringBuilder изменяем и эффективен для инкрементального построения строк. StringBuffer — потокобезопасная версия (редко нужна). String.join() объединяет с разделителем. Java 11+ добавляет repeat() для умножения строк.

java
// String concatenation (creates new String each time)
String s = "Hello" + ", " + "World";
String formatted = String.format("%s is %d", "Alice", 30);

// StringBuilder (mutable, efficient for many concatenations)
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(", ");
sb.append("World");
sb.insert(5, " there");
sb.delete(5, 11);
sb.reverse();
String result = sb.toString();

// StringBuffer (thread-safe, slower than StringBuilder)
StringBuffer sbf = new StringBuffer("thread-safe");

// Join strings
String joined = String.join(", ", "a", "b", "c"); // "a, b, c"

// Repeat (Java 11+)
String repeated = "ab".repeat(3); // "ababab"

Форматирование строк

printf/format использует C-стиль спецификаторов формата: %d (int), %f (float), %s (string), %c (char), %b (boolean), %x (hex). Ширина (%5d), левое выравнивание (%-5d), zero-pad (%05d), точность (%.2f). %n — платформенный перевод строки. Текстовые блоки (Java 15+) с тройными кавычками обеспечивают многострочные строки без экранирования.

java
// printf / format specifiers
System.out.printf("Int: %d%n", 42);
System.out.printf("Float: %.2f%n", 3.14159);  // 3.14
System.out.printf("String: %s%n", "hello");
System.out.printf("Char: %c%n", 'A');
System.out.printf("Bool: %b%n", true);
System.out.printf("Hex: %x%n", 255);           // ff
System.out.printf("Octal: %o%n", 8);           // 10

// Width and padding
System.out.printf("[%5d]%n", 42);      // [   42]
System.out.printf("[%-5d]%n", 42);     // [42   ]
System.out.printf("[%05d]%n", 42);     // [00042]
System.out.printf("[%8.2f]%n", 3.14);  // [    3.14]

// String.format returns a String
String s = String.format("Name: %s, Age: %d", "Alice", 30);

// Text blocks (Java 15+)
String json = """
    {
        "name": "Alice",
        "age": 30
    }
    """;

Регулярные выражения

Java regex использует Pattern (скомпилированный) и Matcher (применённый к входу). Методы String (matches, split, replaceAll) — удобные ярлыки. Обратные слеши должны удваиваться в строковых литералах Java (\\d для \d). Группы захватываются скобками и ссылаются как $1, $2 в заменах. Всегда компилируйте паттерны один раз, если используете многократно.

java
import java.util.regex.*;

// String methods
"hello123".matches("[a-z]+\d+"); // true
"a,b,c".split(",");               // ["a", "b", "c"]
"hello".replaceAll("l", "L");     // "heLLo"

// Pattern and Matcher
Pattern p = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
Matcher m = p.matcher("Date: 2024-01-15");

if (m.find()) {
    System.out.println(m.group()); // "2024-01-15"
}

// Find all matches
while (m.find()) {
    System.out.println(m.group());
}

// Replace with regex
String result = "2024-01-15".replaceAll(
    "(\\d{4})-(\\d{2})-(\\d{2})",
    "$3/$2/$1"); // "15/01/2024"

// Common patterns
String email = "^[\\w.]+@[\\w.]+\\.\\w+$";
String phone = "^\\d{3}-\\d{4}$";

Числа и Math

Класс Math предоставляет статические математические функции. Math.random() возвращает 0.0-1.0. Для большего контроля используйте java.util.Random (с seed) или java.security.SecureRandom (криптографический). Integer/Double имеют статические utility-методы. Будьте осторожны с точностью floating-point — используйте BigDecimal для финансовых вычислений.

java
// Math class
double sqrt = Math.sqrt(16);     // 4.0
double pow = Math.pow(2, 10);    // 1024.0
int abs = Math.abs(-5);          // 5
int max = Math.max(3, 7);        // 7
int min = Math.min(3, 7);        // 3
double rounded = Math.round(3.7); // 4
double ceil = Math.ceil(3.1);    // 4.0
double floor = Math.floor(3.9);  // 3.0
double random = Math.random();   // 0.0 to 1.0

// Constants
double pi = Math.PI;             // 3.14159...
double e = Math.E;               // 2.71828...

// Integer/Long methods
int sum = Integer.sum(3, 4);     // 7
int max2 = Integer.max(3, 7);    // 7

// Rounding modes
double r = Math.round(3.5);      // 4 (round half up)
double r2 = Math.floor(3.5 + 0.5); // alternative

// Random (java.util.Random)
import java.util.Random;
Random rand = new Random();
int n = rand.nextInt(100);  // 0-99
double d = rand.nextDouble(); // 0.0-1.0
boolean b = rand.nextBoolean();
03

Управление потоком

If / Else

Java if/else работает как C/C++. Условия должны быть boolean — нет truthy/falsy как в JavaScript (0 и непустые строки НЕ truthy). Тернарный оператор (cond ? a : b) — выражение, а не инструкция. Используйте фигурные скобки даже для однострочных тел (лучшая практика стиля кода).

java
int score = 85;

if (score >= 90) {
    System.out.println("A");
} else if (score >= 80) {
    System.out.println("B");
} else if (score >= 70) {
    System.out.println("C");
} else {
    System.out.println("F");
}

// Ternary operator
String grade = score >= 60 ? "Pass" : "Fail";

// Nested if
if (score >= 60) {
    if (score >= 90) {
        System.out.println("Excellent");
    }
}

// Note: conditions must be boolean (no truthy/falsy)
// if (score) { } // Error: int is not boolean

Switch и выражения

Традиционный switch имеет fall-through (используйте break). Switch-выражения Java 14+ (->) не проваливаются и могут возвращать значения. Используйте запятые для нескольких case-меток (case 1, 2, 3). 'yield' возвращает значение из сложного блока. Switch-выражения исчерпывающие — нужен default или все случаи для enum'ов.

java
// Traditional switch (fall-through)
int day = 3;
switch (day) {
    case 1:
        System.out.println("Mon");
        break;
    case 2:
    case 3:
    case 4:
        System.out.println("Midweek");
        break;
    case 6:
    case 7:
        System.out.println("Weekend");
        break;
    default:
        System.out.println("Invalid");
}

// Switch expression (Java 14+, no fall-through)
String type = switch (day) {
    case 1, 2, 3, 4, 5 -> "Weekday";
    case 6, 7 -> "Weekend";
    default -> "Invalid";
};

// Switch with yield (for complex blocks)
int result = switch (day) {
    case 1, 2, 3, 4, 5 -> {
        int hours = 8;
        yield hours * 5;
    }
    case 6, 7 -> 0;
    default -> -1;
};

Циклы

Java имеет циклы for, while и do-while. Расширенный for (for-each) работает с массивами и любым Iterable. break выходит из цикла; continue переходит к следующей итерации. Для коллекций предпочитайте for-each или streams вместо индексных циклов. do-while выполняется минимум один раз (редко используется).

java
// For loop
for (int i = 0; i < 5; i++) {
    System.out.println(i);
}

// Enhanced for (for-each)
int[] nums = {1, 2, 3, 4, 5};
for (int n : nums) {
    System.out.println(n);
}

List<String> names = List.of("Alice", "Bob");
for (String name : names) {
    System.out.println(name);
}

// While loop
int count = 0;
while (count < 3) {
    System.out.println(count);
    count++;
}

// Do-while (runs at least once)
int i = 0;
do {
    System.out.println(i);
    i++;
} while (i < 3);

// Break and continue
for (int j = 0; j < 10; j++) {
    if (j == 5) break;      // exit loop
    if (j % 2 == 0) continue; // skip iteration
    System.out.println(j);
}

Меченые break и continue

Метки (outer:) позволяют прерывать/продолжать внешние циклы из вложенных. Это редко нужно — извлечение в метод с return обычно чище. Метки ставятся перед циклом, после двоеточия. break label выходит из меченого цикла; continue label переходит к его следующей итерации.

java
// Labels for breaking out of nested loops
outer:
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        if (i == 1 && j == 1) {
            break outer; // exits both loops
        }
        System.out.println(i + "," + j);
    }
}

// Labeled continue
outer:
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        if (j == 1) {
            continue outer; // skip to next i
        }
        System.out.println(i + "," + j);
    }
}

// Alternative: extract to method and use return
void findPair(int[][] matrix, int target) {
    for (int i = 0; i < matrix.length; i++) {
        for (int j = 0; j < matrix[i].length; j++) {
            if (matrix[i][j] == target) return; // exit method
        }
    }
}

Массивы

Массивы имеют фиксированную длину (используйте ArrayList для динамических). Arrays.sort() сортирует на месте. Arrays.toString() даёт читаемое представление. Arrays.copyOf() создаёт копию с новой длиной. Для многомерных массивов каждая строка может иметь разную длину (jagged arrays). Используйте Arrays для utility-методов над массивами.

java
// Declare and initialize
int[] nums = {1, 2, 3, 4, 5};
int[] empty = new int[5]; // [0, 0, 0, 0, 0]
String[] names = new String[3]; // [null, null, null]

// Access and modify
nums[0] = 10;
int first = nums[0]; // 10
int length = nums.length; // 5

// Multidimensional
int[][] matrix = {{1, 2}, {3, 4}};
int val = matrix[0][1]; // 2

// Arrays utility class
import java.util.Arrays;
int[] sorted = {3, 1, 2};
Arrays.sort(sorted); // [1, 2, 3]
int[] copy = Arrays.copyOf(nums, 3);
String str = Arrays.toString(nums); // "[10, 2, 3, 4, 5]"
boolean eq = Arrays.equals(nums, copy);

// Fill
int[] filled = new int[5];
Arrays.fill(filled, 42); // [42, 42, 42, 42, 42]

// Binary search (sorted array only)
int idx = Arrays.binarySearch(sorted, 2); // index of 2
04

Методы и функции

Определение метода

Методы Java всегда внутри класса. 'static' означает, что метод принадлежит классу (вызов без экземпляра). Возвращаемый тип (int, String, void) объявляется перед именем. Параметры типизированы. В Java нет значений параметров по умолчанию — используйте перегрузку методов вместо этого.

java
public class Calculator {
    // Method with return type
    public static int add(int a, int b) {
        return a + b;
    }

    // Void method (no return)
    public static void printResult(int result) {
        System.out.println("Result: " + result);
    }

    // Method with default (no overloading needed)
    public static String greet(String name, String greeting) {
        return greeting + ", " + name + "!";
    }

    public static void main(String[] args) {
        int sum = add(3, 4);
        printResult(sum);

        String msg = greet("Alice", "Hello");
        System.out.println(msg);
    }
}

Перегрузка методов

Перегрузка методов позволяет несколько методов с одним именем, но разными списками параметров (тип, количество или порядок). Java разрешает перегрузки при компиляции на основе типов аргументов. Перегрузка распространена для конструкторов и utility-методов. Это отличается от переопределения (которое включает наследование и runtime-диспетчеризацию).

java
public class MathUtils {
    // Overloaded methods (same name, different params)
    public static int add(int a, int b) {
        return a + b;
    }

    public static double add(double a, double b) {
        return a + b;
    }

    public static int add(int a, int b, int c) {
        return a + b + c;
    }

    public static String add(String a, String b) {
        return a + b;
    }
}

// Java picks the most specific match
MathUtils.add(1, 2);        // int version -> 3
MathUtils.add(1.5, 2.5);    // double version -> 4.0
MathUtils.add(1, 2, 3);     // 3-param version -> 6
MathUtils.add("Hello", "!"); // String version -> "Hello!"

Varargs и pass-by-value

Varargs (Type... name) позволяют переменные аргументы, получаемые как массив. Java всегда pass-by-value: примитивы копируются, ссылки на объекты копируются (но указывают на тот же объект). Поэтому модификация параметра внутри метода не влияет на переменную вызывающего, но модификация объекта, на который он указывает, влияет.

java
// Varargs: variable number of arguments
public static int sum(int... nums) {
    int total = 0;
    for (int n : nums) {
        total += n;
    }
    return total;
}

sum(1, 2, 3);           // 6
sum(1, 2, 3, 4, 5);     // 15
sum();                  // 0 (empty array)
int[] arr = {1, 2, 3};
sum(arr);               // 6 (pass array to varargs)

// Java is ALWAYS pass-by-value
public static void modify(int x) {
    x = 100; // doesn't affect the caller's variable
}

int n = 5;
modify(n);
System.out.println(n); // still 5

// For objects, the reference is passed by value
public static void addItem(List<String> list) {
    list.add("new"); // modifies the same list object
}

Рекурсия

Рекурсия — когда метод вызывает сам себя. Всегда имейте базовый случай для остановки. Java не оптимизирует хвостовую рекурсию (в отличие от некоторых языков), поэтому глубокая рекурсия может вызвать StackOverflowError. Для performance-critical или глубокой рекурсии преобразуйте в итерацию. Мемоизация (кэширование результатов) может ускорить рекурсивные решения вроде Fibonacci.

java
// Factorial
public static int factorial(int n) {
    if (n <= 1) return 1;        // base case
    return n * factorial(n - 1); // recursive case
}
// factorial(5) = 5 * 4 * 3 * 2 * 1 = 120

// Fibonacci
public static int fib(int n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

// Tail recursion (Java doesn't optimize this)
public static int factorialTail(int n, int acc) {
    if (n <= 1) return acc;
    return factorialTail(n - 1, n * acc);
}
// Call: factorialTail(5, 1)

// Be careful: deep recursion causes StackOverflowError
// For deep recursion, use iteration or a loop instead

Lambda-выражения

Lambda (Java 8+) — анонимные функции. Тип — функциональный интерфейс (один абстрактный метод). Распространённые: Function<T,R> (вход→выход), Predicate<T> (boolean-проверка), Consumer<T> (потребление, без возврата), Supplier<T> (производство, без входа). Ссылки на методы (String::length) — сокращение для lambda, вызывающих один метод.

java
import java.util.function.*;

// Lambda syntax: (params) -> expression
Function<Integer, Integer> square = x -> x * x;
Function<String, Integer> length = s -> s.length();
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;

// With type annotations
BinaryOperator<Integer> multiply = (Integer a, Integer b) -> a * b;

// Multi-line lambda
Function<String, String> process = s -> {
    String upper = s.toUpperCase();
    return upper.substring(0, 3);
};

// Predicate (boolean test)
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<String> isEmpty = String::isEmpty; // method reference

// Consumer (no return)
Consumer<String> printer = s -> System.out.println(s);
Consumer<String> printer2 = System.out::println; // method reference

// Supplier (no input, produces value)
Supplier<Double> random = () -> Math.random();

// Usage
int result = square.apply(5); // 25
boolean even = isEven.test(4); // true
printer.accept("Hello"); // prints "Hello"
05

Классы и ООП

Класс и конструктор

Классы — шаблоны для объектов. Поля хранят состояние, методы определяют поведение. Конструкторы инициализируют новые объекты (используйте 'this' для различения полей и параметров). @Override указывает, что метод переопределяет метод суперкласса (toString из Object). Инкапсуляция: приватные поля, публичные геттеры/сеттеры.

java
public class Person {
    // Fields (instance variables)
    private String name;
    private int age;

    // Constructor
    public Person(String name, int age) {
        this.name = name;  // 'this' refers to the current instance
        this.age = age;
    }

    // Methods
    public String getName() { return name; }
    public int getAge() { return age; }

    public void setAge(int age) {
        if (age >= 0) this.age = age;
    }

    public String greet() {
        return "Hi, I'm " + name;
    }

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

// Usage
Person p = new Person("Alice", 30);
System.out.println(p.getName());  // "Alice"
System.out.println(p);            // uses toString()

Модификаторы доступа и static

Модификаторы доступа: public (везде), private (только класс), protected (класс + подклассы + пакет), default/package-private (тот же пакет). Static-члены принадлежат классу, не экземплярам — разделяются всеми объектами. Static-инициализаторы запускаются один раз при загрузке класса. Используйте static для констант (static final), utility-методов и счётчиков.

java
public class BankAccount {
    // Access modifiers:
    public String owner;      // accessible everywhere
    private double balance;   // class only
    protected String type;    // class + subclasses + same package
    String id;                // package-private (default)

    // Static field (shared by all instances)
    private static int accountCount = 0;

    // Static constant
    public static final double MIN_BALANCE = 100.0;

    // Static method (call without instance)
    public static int getAccountCount() {
        return accountCount;
    }

    // Static initializer (runs once when class loads)
    static {
        System.out.println("BankAccount class loaded");
    }

    public BankAccount(String owner) {
        this.owner = owner;
        this.balance = MIN_BALANCE;
        accountCount++; // increment shared counter
    }
}

int count = BankAccount.getAccountCount(); // static method call

Наследование и super

Java использует 'extends' для наследования классов (только одиночное). super() вызывает конструктор родителя (должен быть первой строкой). @Override указывает переопределение метода (runtime-полиморфизм). Dog IS-A Animal. Используйте наследование для отношений 'is-a'; композицию (has-a) — для переиспользования кода. Java 17+ поддерживает sealed-классы для ограничения наследования.

java
// Parent class
class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
        System.out.println("Animal constructor");
    }

    public void eat() {
        System.out.println(name + " is eating");
    }
}

// Child class (extends)
class Dog extends Animal {
    private String breed;

    public Dog(String name, String breed) {
        super(name); // must be first line — call parent constructor
        this.breed = breed;
    }

    // Override parent method
    @Override
    public void eat() {
        super.eat(); // call parent's eat()
        System.out.println(name + " the " + breed + " eats dog food");
    }

    public void bark() {
        System.out.println("Woof!");
    }
}

Dog dog = new Dog("Rex", "Labrador");
dog.eat();   // calls Dog's eat()
dog.bark();  // Dog-specific method

Абстрактные классы и интерфейсы

Абстрактные классы нельзя создать и они могут иметь как абстрактные (без тела), так и конкретные методы. Интерфейсы определяют контракты — все методы public abstract по умолчанию. Java 8+ разрешает default-методы (с телом) и static-методы в интерфейсах. Класс расширяет один абстрактный класс, но может реализовать несколько интерфейсов. Используйте абстрактные классы для общего кода, интерфейсы — для контрактов.

java
// Abstract class (can't be instantiated)
abstract class Shape {
    protected String color;

    public Shape(String color) {
        this.color = color;
    }

    // Abstract method (must be implemented by subclasses)
    public abstract double area();

    // Concrete method (inherited)
    public String describe() {
        return color + " " + this.getClass().getSimpleName();
    }
}

// Interface (pure contract, Java 8+ can have default methods)
interface Drawable {
    void draw(); // abstract by default

    // Default method (Java 8+)
    default void drawTwice() {
        draw();
        draw();
    }

    // Static method in interface
    static Drawable empty() {
        return () -> System.out.println("nothing");
    }
}

// A class can extend one class and implement multiple interfaces
class Circle extends Shape implements Drawable {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }

    @Override
    public void draw() {
        System.out.println("Drawing " + describe());
    }
}

Полиморфизм и приведение

Полиморфизм: ссылка на родителя может содержать объект-наследник. Вызовы методов диспетчеризуются к реализации фактического объекта (runtime-полиморфизм). Используйте instanceof перед downcast'ом, чтобы избежать ClassCastException. Java 16+ pattern matching (instanceof Circle c) объединяет проверку и приведение. Переопределяйте equals() и hashCode() вместе для корректного поведения в коллекциях.

java
// Polymorphism: one interface, many forms
Shape s1 = new Circle("red", 5);
Shape s2 = new Square("blue", 3);

// Calls the overridden method (runtime dispatch)
System.out.println(s1.area()); // Circle's area
System.out.println(s2.area()); // Square's area

// instanceof check
if (s1 instanceof Circle) {
    Circle c = (Circle) s1; // downcast
    System.out.println("Radius: " + c.radius);
}

// Pattern matching (Java 16+)
if (s1 instanceof Circle c) {
    System.out.println("Radius: " + c.radius); // c is already cast
}

// Upcasting (automatic)
Circle circle = new Circle("green", 2);
Shape shape = circle; // upcast (no explicit cast needed)

// Object class methods (all classes inherit from Object)
circle.equals(circle);   // reference equality by default
circle.hashCode();       // hash code
circle.getClass();       // Class<Circle>
circle.toString();       // string representation

Records и Enums

Records (Java 16+) — неизменяемые классы данных — компилятор генерирует конструктор, геттеры, equals, hashCode и toString. Используйте для DTO и value-объектов. Enums — типобезопасные константы, которые могут иметь поля, методы и конструкторы. Enums реализуют Comparable и имеют методы values() и valueOf(). Оба существенны для современного Java.

java
// Record (Java 16+): concise data class
public record Point(int x, int y) {}

// Equivalent to a class with:
// - final fields x, y
// - constructor
// - getters x(), y()
// - equals, hashCode, toString

Point p = new Point(3, 4);
System.out.println(p.x());      // 3
System.out.println(p.y());      // 4
System.out.println(p);          // Point[x=3, y=4]

// Compact constructor (validation)
public record Age(int value) {
    public Age {
        if (value < 0 || value > 150) {
            throw new IllegalArgumentException("Invalid age");
        }
    }
}

// Enum (named constants)
public enum Direction {
    UP, DOWN, LEFT, RIGHT;

    public Direction opposite() {
        return switch (this) {
            case UP -> DOWN;
            case DOWN -> UP;
            case LEFT -> RIGHT;
            case RIGHT -> LEFT;
        };
    }
}

Direction d = Direction.UP;
Direction opp = d.opposite(); // DOWN
06

Коллекции и дженерики

List (ArrayList и LinkedList)

ArrayList поддерживается массивом (быстрый get/set, медленные insert/delete в середине). LinkedList поддерживается двусвязным списком (быстрый insert/delete на концах, медленный случайный доступ). List.of() создаёт неизменяемые списки. Используйте ArrayList для большинства случаев; LinkedList только для частых операций на концах. Оба реализуют интерфейс List.

java
import java.util.*;

// ArrayList (fast random access, slow insert/delete in middle)
List<String> list = new ArrayList<>();
list.add("Alice");
list.add("Bob");
list.add(0, "Carol");     // insert at index
list.set(1, "Dave");      // replace at index
String name = list.get(0); // "Carol"
list.remove(0);            // remove by index
list.remove("Dave");       // remove by value
int size = list.size();    // 1
boolean has = list.contains("Bob");

// LinkedList (fast insert/delete at ends)
LinkedList<Integer> linked = new LinkedList<>();
linked.addFirst(1);
linked.addLast(2);
linked.removeFirst();
linked.peek(); // see first element

// Immutable list (Java 9+)
List<String> immutable = List.of("a", "b", "c");
// immutable.add("d"); // UnsupportedOperationException

// Iterate
for (String s : list) {
    System.out.println(s);
}
list.forEach(System.out::println); // method reference

Set (HashSet и TreeSet)

Set хранит уникальные элементы. HashSet самый быстрый, но неупорядоченный. TreeSet хранит элементы отсортированными (естественный порядок или Comparator). LinkedHashSet поддерживает порядок вставки. Операции над множествами: addAll (union), retainAll (intersection), removeAll (difference). Для пользовательских объектов в HashSet переопределяйте equals() и hashCode().

java
import java.util.*;

// HashSet (fast, unordered)
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
set.add("apple"); // duplicate ignored
System.out.println(set.size()); // 2
System.out.println(set.contains("apple")); // true
set.remove("banana");

// TreeSet (sorted, slower)
Set<Integer> sorted = new TreeSet<>();
sorted.add(3);
sorted.add(1);
sorted.add(2);
System.out.println(sorted); // [1, 2, 3]

// LinkedHashSet (maintains insertion order)
Set<String> ordered = new LinkedHashSet<>();
ordered.add("c");
ordered.add("a");
ordered.add("b");
System.out.println(ordered); // [c, a, b]

// Set operations
Set<Integer> a = new HashSet<>(Set.of(1, 2, 3));
Set<Integer> b = new HashSet<>(Set.of(2, 3, 4));
a.addAll(b);    // union: [1, 2, 3, 4]
a.retainAll(b); // intersection: [2, 3]
a.removeAll(b); // difference: [1]

// Immutable set
Set<String> immutable = Set.of("x", "y", "z");

Map (HashMap и TreeMap)

Map хранит пары ключ-значение. HashMap самый быстрый (неупорядоченный). TreeMap сортирует по ключам. LinkedHashMap поддерживает порядок вставки. getOrDefault избегает проверок на null. compute/merge мощны для обновления значений. Для пользовательских ключей переопределяйте equals() и hashCode(). Map.of() создаёт неизменяемые map (Java 9+).

java
import java.util.*;

// HashMap (fast, unordered)
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
ages.put("Alice", 31); // overwrite

// Access
int age = ages.get("Alice"); // 31
int defaultAge = ages.getOrDefault("Eve", 0); // 0

// Check
boolean has = ages.containsKey("Alice");
boolean hasVal = ages.containsValue(25);

// Remove
ages.remove("Bob");

// Iterate
for (Map.Entry<String, Integer> entry : ages.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

ages.forEach((key, val) -> System.out.println(key + "=" + val));

// Compute (Java 8+)
ages.compute("Alice", (k, v) -> v + 1); // increment
ages.putIfAbsent("Carol", 28);
ages.merge("Alice", 1, Integer::sum); // add 1

// TreeMap (sorted by keys)
Map<String, Integer> sorted = new TreeMap<>();
// LinkedHashMap (maintains insertion order)
Map<String, Integer> ordered = new LinkedHashMap<>();

Queue и Deque

Queue — FIFO (first-in-first-out). Deque — двусторонняя (можно добавлять/удалять с обоих концов). PriorityQueue упорядочивает элементы по естественному порядку или Comparator (min-heap по умолчанию). Для стека используйте ArrayDeque (push/pop) вместо устаревшего класса Stack. ArrayDeque быстрее LinkedList для операций queue/deque.

java
import java.util.*;

// Queue (FIFO)
Queue<String> queue = new LinkedList<>();
queue.add("first");   // throws if full (capacity-restricted)
queue.offer("second"); // returns false if full
String head = queue.peek(); // see head (null if empty)
String removed = queue.poll(); // remove and return head

// Deque (double-ended)
Deque<Integer> deque = new ArrayDeque<>();
deque.addFirst(1);
deque.addLast(2);
deque.peekFirst(); // 1
deque.peekLast();  // 2
deque.pollFirst(); // 1
deque.pollLast();  // 2

// PriorityQueue (min-heap by default)
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.add(3);
pq.add(1);
pq.add(2);
System.out.println(pq.poll()); // 1 (smallest first)

// Max-heap (reverse order)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
maxHeap.add(1);
maxHeap.add(3);
System.out.println(maxHeap.poll()); // 3 (largest first)

// Stack (legacy, prefer Deque)
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // add to front
stack.push(2);
stack.pop();   // 2 (remove from front)

Дженерики

Дженерики обеспечивают типобезопасные коллекции и классы. <T> — параметр типа. Ограниченные типы (<T extends Comparable<T>>) ограничивают типами с определённым поведением. Wildcards: ? (любой), ? extends T (ковариантный, только чтение), ? super T (контравариантный, только запись). Дженерики используют type erasure — типы проверяются при компиляции, стираются в runtime.

java
// Generic class
public class Box<T> {
    private T value;

    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

Box<String> stringBox = new Box<>();
stringBox.set("hello");
String s = stringBox.get();

Box<Integer> intBox = new Box<>();
intBox.set(42);

// Generic method
public static <T> T firstOf(List<T> list) {
    return list.get(0);
}

String first = firstOf(List.of("a", "b"));

// Bounded type parameter
public static <T extends Comparable<T>> T max(List<T> list) {
    T result = list.get(0);
    for (T item : list) {
        if (item.compareTo(result) > 0) {
            result = item;
        }
    }
    return result;
}

// Wildcards
void process(List<?> list) { }          // any type
void processNums(List<? extends Number> list) { } // Number or subclass
void addNums(List<? super Integer> list) { }      // Integer or superclass

Итераторы и Comparable

Iterator позволяет безопасное удаление во время итерации (it.remove()). ListIterator добавляет двунаправленный обход и set/add. Comparable определяет естественный порядок (compareTo). Comparator определяет пользовательский порядок (comparing, comparingInt, reversed, thenComparing). Используйте Comparator.comparing() для fluent-сортировки. Collections.sort() использует естественный порядок.

java
import java.util.*;

// Iterator
List<String> list = List.of("a", "b", "c");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    System.out.println(s);
    // it.remove(); // safe removal during iteration
}

// ListIterator (bidirectional)
ListIterator<String> lit = list.listIterator();
while (lit.hasNext()) {
    lit.set(lit.next().toUpperCase()); // replace
}

// Comparable (natural ordering)
public class Person implements Comparable<Person> {
    String name;
    int age;

    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }
}

// Comparator (custom ordering)
Comparator<Person> byName = Comparator.comparing(p -> p.name);
Comparator<Person> byAgeDesc = Comparator.comparingInt((Person p) -> p.age).reversed();

List<Person> people = new ArrayList<>();
people.sort(byName);
people.sort(byAgeDesc);
Collections.sort(people); // uses Comparable
07

Streams и функциональное

Основы Stream

Streams (Java 8+) обеспечивают декларативную обработку данных. Создание через .stream() (коллекции) или Stream.of(). Промежуточные операции (filter, map, sorted) ленивые — они выполняются только при вызове терминальной операции (collect, reduce, count, forEach). toList() (Java 16+) — лаконичная альтернатива collect(Collectors.toList()).

java
import java.util.*;
import java.util.stream.*;

List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);

// Filter and collect
List<Integer> evens = nums.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList()); // [2, 4, 6]

// Map (transform)
List<String> doubled = nums.stream()
    .map(n -> "num" + n)
    .collect(Collectors.toList());

// Reduce
int sum = nums.stream().reduce(0, Integer::sum); // 21
int product = nums.stream().reduce(1, (a, b) -> a * b);

// Count
long count = nums.stream().filter(n -> n > 3).count(); // 3

// Find
Optional<Integer> first = nums.stream().filter(n -> n > 3).findFirst();
boolean anyMatch = nums.stream().anyMatch(n -> n > 5);
boolean allMatch = nums.stream().allMatch(n -> n > 0);

// ForEach
nums.stream().forEach(System.out::println);

// toList() shortcut (Java 16+)
List<Integer> result = nums.stream().filter(n -> n > 3).toList();

Операции Stream

sorted() упорядочивает элементы (естественный или с Comparator). distinct() удаляет дубликаты. limit(n)/skip(n) пагинируют. flatMap выравнивает вложенные streams — существенен для one-to-many преобразований. peek() — для отладки (побочные эффекты). groupingBy создаёт map, группируя элементы по классификатору. Streams ленивы — операции цепляются эффективно.

java
List<String> names = List.of("Alice", "Bob", "Charlie", "David");

// Sorted
List<String> sorted = names.stream()
    .sorted()
    .toList();

// Sorted by length
List<String> byLength = names.stream()
    .sorted(Comparator.comparing(String::length))
    .toList();

// Distinct
List<Integer> distinct = List.of(1, 2, 2, 3, 3, 3).stream()
    .distinct()
    .toList(); // [1, 2, 3]

// Limit and Skip
List<Integer> limited = nums.stream()
    .skip(2)   // skip first 2
    .limit(3)  // take next 3
    .toList();

// FlatMap (flatten nested structures)
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4));
List<Integer> flat = nested.stream()
    .flatMap(List::stream)
    .toList(); // [1, 2, 3, 4]

// Peek (debug, side-effect)
nums.stream()
    .peek(n -> System.out.println("before: " + n))
    .filter(n -> n > 2)
    .peek(n -> System.out.println("after: " + n))
    .toList();

// Grouping
Map<Integer, List<String>> byLength = names.stream()
    .collect(Collectors.groupingBy(String::length));

Collectors и редукция

Collectors предоставляют богатые операции редукции: joining (конкатенация строк), groupingBy (группировка по ключу), partitioningBy (разделение по boolean), toMap (создание map), summarizingInt (статистика: count, sum, min, max, average). Collectors компонуются (groupingBy с downstream collector). Они заменяют многословные циклы декларативными one-liner'ами.

java
import java.util.stream.*;

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

// Join strings
String joined = people.stream()
    .map(Person::getName)
    .collect(Collectors.joining(", ")); // "Alice, Bob, Charlie"

// Group by
Map<Integer, List<Person>> byAge = people.stream()
    .collect(Collectors.groupingBy(Person::getAge));

// Partition (boolean)
Map<Boolean, List<Person>> partition = people.stream()
    .collect(Collectors.partitioningBy(p -> p.getAge() > 28));

// Count by group
Map<Integer, Long> countByAge = people.stream()
    .collect(Collectors.groupingBy(Person::getAge, Collectors.counting()));

// Summarizing
IntSummaryStatistics stats = people.stream()
    .collect(Collectors.summarizingInt(Person::getAge));
System.out.println(stats.getAverage()); // 30.0
System.out.println(stats.getMax());     // 35

// To map
Map<String, Integer> nameToAge = people.stream()
    .collect(Collectors.toMap(Person::getName, Person::getAge));

// Reducing
int totalAge = people.stream()
    .collect(Collectors.reducing(0, Person::getAge, Integer::sum));

Optional

Optional<T> — контейнер, который может или не может содержать значение. Он форсирует явную обработку отсутствия — больше никакого NullPointerException. Используйте of() для non-null значений, ofNullable() для возможно-null. Цепочечный через map/flatMap/filter. Никогда не используйте get() без isPresent() — предпочитайте orElse/orElseThrow. Optional разработан для возвращаемых типов, не полей.

java
import java.util.Optional;

// Creating Optional
Optional<String> present = Optional.of("hello");
Optional<String> empty = Optional.empty();
Optional<String> nullable = Optional.ofNullable(null); // empty if null

// Checking
present.isPresent(); // true
empty.isEmpty();     // true (Java 11+)

// Getting values
String val = present.get(); // throws if empty (avoid!)
String safe = present.orElse("default");
String computed = present.orElseGet(() -> computeDefault());
String orThrow = present.orElseThrow(() -> new RuntimeException("missing"));

// Transform (map/flatMap)
Optional<Integer> length = present.map(String::length); // Optional[5]
Optional<String> upper = present.map(s -> s.toUpperCase());

// Filter
Optional<String> filtered = present.filter(s -> s.length() > 3);

// ifPresent
present.ifPresent(s -> System.out.println(s));
present.ifPresentOrElse(
    s -> System.out.println("Got: " + s),
    () -> System.out.println("Empty")
);

// Chaining (avoid null checks)
String result = getUser(1)
    .map(User::getProfile)
    .map(Profile::getEmail)
    .orElse("no email");

Функциональные интерфейсы

Функциональные интерфейсы имеют ровно один абстрактный метод (могут иметь несколько default-методов). @FunctionalInterface опционален, но документирует намерение. Java предоставляет множество в java.util.function: Function, Predicate, Consumer, Supplier, плюс Bi- и примитивные варианты. Используйте их вместо создания пользовательских интерфейсов, когда возможно. Они обеспечивают lambda-выражения и ссылки на методы.

java
import java.util.function.*;

// Built-in functional interfaces
Function<String, Integer> strToInt = Integer::parseInt;
BiFunction<String, String, String> concat = String::concat;

Predicate<String> isEmpty = String::isEmpty;
BiPredicate<String, String> contains = String::contains;

Consumer<String> printer = System.out::println;
BiConsumer<String, Integer> printPair = (s, i) -> System.out.println(s + ":" + i);

Supplier<List<String>> listFactory = ArrayList::new;

// Primitive specializations
IntFunction<String> intToStr = String::valueOf;
ToIntFunction<String> length = String::length;
IntPredicate isPositive = n -> n > 0;
IntConsumer intPrinter = System.out::println;
IntSupplier randomInt = () -> (int)(Math.random() * 100);

// Binary operators
BinaryOperator<Integer> max = Integer::max;
IntBinaryOperator sum = Integer::sum;

// Unary operators
UnaryOperator<String> trim = String::trim;
IntUnaryOperator negate = n -> -n;

// Custom functional interface
@FunctionalInterface
interface StringProcessor {
    String process(String input);

    // Can have default methods
    default StringProcessor andThen(StringProcessor after) {
        return input -> after.process(process(input));
    }
}
08

Исключения и I/O

Try / Catch / Finally

try/catch/finally обрабатывает исключения. finally всегда выполняется (используйте для очистки). Multi-catch (catch A | B) обрабатывает несколько исключений вместе. Try-with-resources авто-закрывает любой AutoCloseable (файлы, соединения, потоки) — предпочтительнее ручной очистки в finally. Ресурсы закрываются в обратном порядке объявления.

java
try {
    int result = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println("Math error: " + e.getMessage());
} catch (Exception e) {
    System.out.println("General error: " + e);
} finally {
    // Always runs (even if return/throw in try/catch)
    System.out.println("Cleanup");
}

// Multi-catch (Java 7+)
try {
    riskyOperation();
} catch (IOException | SQLException e) {
    // Handle both exceptions the same way
    log.error(e);
}

// Try-with-resources (auto-close, Java 7+)
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"));
     PrintWriter pw = new PrintWriter("output.txt")) {
    String line = br.readLine();
    pw.println(line);
} catch (IOException e) {
    e.printStackTrace();
}
// br and pw are auto-closed (in reverse order)

Checked vs Unchecked

Checked-исключения (extends Exception) должны быть объявлены через 'throws' или пойманы — компилятор это требует. Используйте для восстановимых условий (файл не найден, сетевая ошибка). Unchecked-исключения (extends RuntimeException) не требуют объявления — используйте для ошибок программирования (null pointer, неверный аргумент). Дебаты: checked-исключения форсируют обработку, но могут загромождать код; многие фреймворки предпочитают unchecked.

java
// Checked exceptions (must be declared or caught)
public void readFile(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    // IOException is checked — compiler enforces handling
}

// Unchecked exceptions (RuntimeException, no need to declare)
public int divide(int a, int b) {
    if (b == 0) {
        throw new IllegalArgumentException("Divisor cannot be zero");
        // RuntimeException — no 'throws' needed
    }
    return a / b;
}

// Common checked exceptions:
// IOException, SQLException, ClassNotFoundException

// Common unchecked exceptions:
// NullPointerException, IllegalArgumentException,
// IndexOutOfBoundsException, ArithmeticException,
// ClassCastException, IllegalStateException

// Custom checked exception
class DataException extends Exception {
    public DataException(String msg) { super(msg); }
}

// Custom unchecked exception
class ValidationException extends RuntimeException {
    public ValidationException(String msg) { super(msg); }
}

Файловый I/O (NIO.2)

NIO.2 (java.nio.file) — современный файловый API. Files.readString/writeString (Java 11+) удобны для текста. Files.lines() возвращает ленивый Stream — эффективен для больших файлов (нужно закрыть через try-with-resources). Path.of() заменяет старый класс File. Files.createDirectories() создаёт полный путь. Всегда обрабатывайте IOException.

java
import java.nio.file.*;
import java.io.*;

// Read entire file (small files)
List<String> lines = Files.readAllLines(Path.of("input.txt"));
String content = Files.readString(Path.of("config.json")); // Java 11+
byte[] bytes = Files.readAllBytes(Path.of("image.png"));

// Write file
Files.writeString(Path.of("output.txt"), "Hello, World!");
Files.write(Path.of("data.bin"), bytes);

// Append
Files.writeString(Path.of("log.txt"), "entry\n",
    StandardOpenOption.APPEND, StandardOpenOption.CREATE);

// Stream lines (large files, lazy)
try (Stream<String> lineStream = Files.lines(Path.of("large.txt"))) {
    lineStream.filter(l -> l.contains("ERROR"))
              .forEach(System.out::println);
}

// Copy, move, delete
Files.copy(Path.of("src.txt"), Path.of("dest.txt"));
Files.move(Path.of("old.txt"), Path.of("new.txt"));
Files.delete(Path.of("temp.txt"));

// Check existence
boolean exists = Files.exists(Path.of("file.txt"));

// Create directories
Files.createDirectories(Path.of("a/b/c"));

Reader и Writer (текст)

BufferedReader/Writer эффективны для текстового I/O (буферизация уменьшает системные вызовы). PrintWriter предлагает printf-стиль форматирования. Scanner парсит ввод (nextInt, nextDouble, nextLine). InputStreamReader мостит байтовые потоки к символьным (укажите charset для non-UTF-8). Всегда используйте try-with-resources для гарантии закрытия потоков.

java
import java.io.*;
import java.nio.file.*;

// BufferedReader (efficient text reading)
try (BufferedReader br = Files.newBufferedReader(Path.of("input.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
}

// BufferedWriter (efficient text writing)
try (BufferedWriter bw = Files.newBufferedWriter(Path.of("output.txt"))) {
    bw.write("First line");
    bw.newLine();
    bw.write("Second line");
}

// PrintWriter (convenient formatting)
try (PrintWriter pw = new PrintWriter("output.txt")) {
    pw.println("Hello");
    pw.printf("Name: %s, Age: %d%n", "Alice", 30);
}

// Scanner (parsing input)
try (Scanner sc = new Scanner(Path.of("data.txt"))) {
    while (sc.hasNextLine()) {
        String line = sc.nextLine();
        // Parse tokens
        Scanner lineSc = new Scanner(line);
        if (lineSc.hasNextInt()) {
            int n = lineSc.nextInt();
        }
    }
}

// InputStreamReader (bytes to chars, e.g., from InputStream)
Reader reader = new InputStreamReader(System.in);

Дата и время (java.time)

java.time (Java 8+) — современный API даты/времени, заменяющий старые Date/Calendar. LocalDate (только дата), LocalTime (только время), LocalDateTime (оба), ZonedDateTime (с часовым поясом). Все неизменяемые и потокобезопасные. Используйте Period для разницы дат, Duration для разницы времени. DateTimeFormatter для парсинга/форматирования. Instant для машинных временных меток (UTC).

java
import java.time.*;
import java.time.format.*;
import java.time.temporal.*;

// Current date/time
LocalDate today = LocalDate.now();      // 2024-01-15
LocalTime now = LocalTime.now();        // 14:30:45.123
LocalDateTime dt = LocalDateTime.now(); // both
ZonedDateTime zdt = ZonedDateTime.now(); // with timezone

// Create specific
LocalDate date = LocalDate.of(2024, 1, 15);
LocalTime time = LocalTime.of(14, 30, 0);
LocalDateTime specific = LocalDateTime.of(2024, 1, 15, 14, 30);

// Parsing and formatting
LocalDate parsed = LocalDate.parse("2024-01-15");
String formatted = date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
// "15/01/2024"

// Manipulation (immutable, returns new)
LocalDate tomorrow = today.plusDays(1);
LocalDate lastMonth = today.minusMonths(1);
LocalDate nextYear = today.plusYears(1);

// Period (date-based)
Period age = Period.between(LocalDate.of(1990, 1, 1), today);
System.out.println(age.getYears()); // 34

// Duration (time-based)
Duration dur = Duration.between(time, LocalTime.now());
System.out.println(dur.toMinutes());

// Instant (machine time, UTC)
Instant instant = Instant.now();
Instant epoch = Instant.ofEpochSecond(0);

Основы конкурентности

Конкурентность Java: Thread (низкоуровневый), ExecutorService (пулы потоков — предпочтительно), CompletableFuture (async-композиция, как Promises). parallelStream() использует ForkJoinPool для параллельной обработки. synchronized-блоки защищают разделяемое состояние. Atomic-переменные (AtomicInteger и т.д.) обеспечивают lock-free потокобезопасные операции. Для сложной конкурентности используйте коллекции java.util.concurrent (ConcurrentHashMap, BlockingQueue).

java
import java.util.concurrent.*;

// Create a thread
Thread thread = new Thread(() -> {
    System.out.println("Running in: " + Thread.currentThread().getName());
});
thread.start();
thread.join(); // wait for completion

// ExecutorService (thread pool)
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<Integer> future = executor.submit(() -> {
    Thread.sleep(1000);
    return 42;
});
Integer result = future.get(); // blocks until done
executor.shutdown();

// CompletableFuture (async, Java 8+)
CompletableFuture<String> cf = CompletableFuture
    .supplyAsync(() -> "Hello")
    .thenApply(s -> s + " World")
    .thenApply(String::toUpperCase);
String asyncResult = cf.join(); // "HELLO WORLD"

// Parallel stream
List<Integer> nums = List.of(1, 2, 3, 4, 5);
int sum = nums.parallelStream().mapToInt(Integer::intValue).sum();

// Synchronized
synchronized (this) {
    // only one thread at a time
}

// Atomic variables
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.compareAndSet(0, 1);
09

Lambda-выражения

Основы синтаксиса Lambda

Lambda (Java 8+) — лаконичные реализации функциональных интерфейсов. Синтаксис: (params) -> выражение или (params) -> { инструкции; }. Компилятор выводит типы параметров из целевого типа. Lambda с одним параметром может опустить скобки; с нулём параметров нужны пустые скобки. Lambda обеспечивает функциональное программирование и является основой Streams API.

java
// Anonymous class (verbose, pre-Java 8)
Runnable r1 = new Runnable() {
    public void run() {
        System.out.println("Old way");
    }
};

// Lambda expression (Java 8+)
Runnable r2 = () -> System.out.println("Lambda");

// With parameters and body
Comparator<Integer> cmp = (a, b) -> {
    int diff = a - b;
    return diff;
};

// Type inference (omit types)
Comparator<Integer> cmp2 = (a, b) -> a - b;

// Single param, no parentheses needed
Consumer<String> printer = s -> System.out.println(s);

// Zero params need empty parens
Runnable noop = () -> {};

Функциональные интерфейсы

Функциональный интерфейс имеет ровно один абстрактный метод (SAM type). Аннотация @FunctionalInterface заставляет компилятор это требовать. Lambda могут нацеливаться только на функциональные интерфейсы. Default- и static-методы разрешены и не нарушают правило одного метода. Это основа, делающая lambda работающими в системе типов Java.

java
// Functional interface: exactly one abstract method
@FunctionalInterface
interface MathOperation {
    int operate(int a, int b);
}

// Implement with lambda
MathOperation add = (a, b) -> a + b;
MathOperation mul = (a, b) -> a * b;
MathOperation max = (a, b) -> Math.max(a, b);

int result = add.operate(3, 4);  // 7
int m = mul.operate(3, 4);       // 12

// @FunctionalInterface is optional but recommended
// It prevents accidentally adding a second abstract method
// Default and static methods don't count toward the limit
interface StringProcessor {
    String process(String s);
    default StringProcessor andThen(StringProcessor next) {
        return s -> next.process(this.process(s));
    }
}

Встроенные функциональные интерфейсы

java.util.function предоставляет ~40 готовых функциональных интерфейсов, поэтому редко нужно писать свои. Основная четвёрка: Function (преобразование), Predicate (проверка), Consumer (потребление), Supplier (производство). Bi- варианты принимают два аргумента. Примитивные варианты (IntFunction, ToIntFunction и т.д.) избегают накладных расходов autoboxing. Используйте их вместо создания пользовательских интерфейсов.

java
import java.util.function.*;

// Function<T,R>: input -> output
Function<String, Integer> len = String::length;
int n = len.apply("hello");  // 5

// Predicate<T>: input -> boolean (for filtering)
Predicate<String> isEmpty = String::isEmpty;
boolean e = isEmpty.test("");  // true

// Consumer<T>: input -> void (side effects)
Consumer<String> print = System.out::println;
print.accept("hi");

// Supplier<T>: no input -> output (factories, lazy)
Supplier<Double> random = Math::random;
double r = random.get();

// BiFunction<T,U,R>: two inputs -> output
BiFunction<String, Integer, String> repeat =
    (s, i) -> s.repeat(i);

// Primitives variants avoid boxing
IntFunction<String> f = i -> "n=" + i;
IntPredicate positive = i -> i > 0;
ToIntFunction<String> length = String::length;
IntBinaryOperator sum = (a, b) -> a + b;

Ссылки на методы

Ссылки на методы (::) — сокращение для lambda, которые просто вызывают один метод. Четыре вида: static (Class::static), bound instance (obj::method), unbound instance (Class::method — первый параметр становится получателем) и constructor (Class::new). Используйте, когда lambda только перенаправляет к одному методу — они более читаемы. Иначе оставайтесь с явными lambda.

java
import java.util.*;

List<String> names = List.of("alice", "bob", "charlie");

// Lambda form
names.forEach(s -> System.out.println(s));
// Method reference (shorthand)
names.forEach(System.out::println);

// Four kinds of method references:

// 1. Static method: ClassName::staticMethod
names.stream().map(String::toUpperCase);

// 2. Instance method of particular object: instance::method
var printer = System.out;
names.forEach(printer::println);

// 3. Instance method of arbitrary object: ClassName::instanceMethod
List<String> upper = names.stream()
    .map(String::toUpperCase)
    .toList();

// 4. Constructor: ClassName::new
Supplier<ArrayList<String>> factory = ArrayList::new;
ArrayList<String> list = factory.get();

Захват переменных (effectively final)

Lambda могут захватывать локальные переменные, но они должны быть final или 'effectively final' (никогда не переприсваиваются). Это потому, что lambda может пережить стековый кадр. Для обхода используйте одноэлементный массив или AtomicInteger/holder-объект. Instance- и static-поля не имеют такого ограничения. 'this' внутри lambda ссылается на экземпляр объемлющего класса, а не на саму lambda.

java
import java.util.function.*;

int x = 10;
// Capturing a local variable — must be final or effectively final
Supplier<Integer> getter = () -> x * 2;
System.out.println(getter.get());  // 20

// x = 20;  // ERROR: would break the lambda capture
// Local variables captured by lambdas must be final/effectively final

// Workaround: use an array or wrapper (mutable container)
int[] counter = {0};
Runnable inc = () -> counter[0]++;
inc.run();
inc.run();
System.out.println(counter[0]);  // 2

// Instance/static fields CAN be modified (no restriction)
class Holder {
    int value = 0;
    Runnable bump = () -> value++;  // OK, field access
}

// 'this' inside a lambda refers to the enclosing instance
class Outer {
    String name = "Outer";
    Runnable r = () -> System.out.println(this.name);  // "Outer"
}

Ссылки на конструкторы

Ссылки на конструкторы (ClassName::new) лаконично создают новые экземпляры. Полезно с Collectors.toCollection() для выбора типа результата, с созданием массивов (Type[]::new) и в фабричных паттернах. Для records и неизменяемых объектов ссылки на конструкторы — идиоматичный способ построения копий. Они естественно парятся с Function/Supplier.

java
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

// Supplier constructor reference
Supplier<StringBuilder> sbFactory = StringBuilder::new;
StringBuilder sb = sbFactory.get();

// Function constructor reference (with one arg)
Function<String, StringBuilder> sbFromString = StringBuilder::new;
StringBuilder named = sbFromString.apply("Hello");

// In streams: collect into a specific collection
List<String> names = List.of("a", "b", "c");
ArrayList<String> copy = names.stream()
    .collect(Collectors.toCollection(ArrayList::new));

// Array constructor reference
IntFunction<String[]> arrayFactory = String[]::new;
String[] arr = arrayFactory.apply(5);  // new String[5]

// Copying via constructor
record Point(int x, int y) {}
Function<Point, Point> copyCtor = Point::new;
Point p = copyCtor.apply(new Point(1, 2));
10

Optional и null-безопасность

Создание Optional

Optional — контейнер, который может или не может содержать значение. Используйте empty() для отсутствия значения, of() когда уверены, что значение non-null (иначе бросает NPE), ofNullable() когда возможен null. Optional заставляет вызывающих явно обрабатывать случай отсутствия. Никогда не возвращайте null, где ожидается Optional — это разрушает цель.

java
import java.util.Optional;

// empty() — no value
Optional<String> empty = Optional.empty();

// of() — value must be non-null (throws NPE if null)
Optional<String> present = Optional.of("hello");

// ofNullable() — accepts null safely
Optional<String> maybe = Optional.ofNullable(getName());

// From a stream that may produce 0 or 1 elements
Optional<Integer> first = List.of(1, 2, 3).stream().findFirst();

// Common helper pattern
public Optional<User> findUser(long id) {
    User u = db.lookup(id);
    return Optional.ofNullable(u);
}

String getName() { return Math.random() > 0.5 ? "Alice" : null; }
class User {}
class Db { User lookup(long id) { return null; } }
Db db = new Db();

Безопасное потребление значений

Предпочитайте ifPresent/ifPresentOrElse вместо isPresent+get. orElse возвращает константное значение по умолчанию; orElseGet принимает Supplier, поэтому default вычисляется лениво (важно, когда default дорогой). orElseThrow преобразует отсутствие в исключение. Цель — никогда не вызывать .get() вслепую — это вновь вводит риск NPE, который Optional должен был устранить.

java
import java.util.Optional;

Optional<String> name = Optional.of("Alice");

// isPresent / isEmpty (Java 11+)
if (name.isPresent()) {
    System.out.println(name.get());  // "Alice"
}
// Avoid .get() without checking — throws NoSuchElementException

// ifPresent: run action only if value exists
name.ifPresent(System.out::println);

// ifPresentOrElse (Java 9+)
name.ifPresentOrElse(
    System.out::println,
    () -> System.out.println("No name")
);

// orElse: provide default
String s1 = name.orElse("Anonymous");

// orElseGet: lazy default (computed only if needed)
String s2 = name.orElseGet(() -> expensiveDefault());

// orElseThrow: throw if absent
String s3 = name.orElseThrow(() -> new IllegalStateException("missing"));

String expensiveDefault() { return "computed"; }

Преобразование с map и flatMap

map преобразует содержащееся значение (Optional<T> -> Optional<R>). flatMap используется, когда функция отображения сама возвращает Optional, предотвращая вложенные Optional. filter оставляет значение только если предикат совпадает. Цепочечный map/filter/flatMap позволяет строить конвейеры, которые коротко замыкаются на первом пустом — намного чище, чем вложенные проверки на null.

java
import java.util.Optional;

Optional<String> name = Optional.of("Alice");

// map: transform the value if present
Optional<Integer> length = name.map(String::length);  // Optional[5]
Optional<String> upper = name.map(String::toUpperCase);

// flatMap: when the transform itself returns Optional
// (avoids Optional<Optional<T>>)
public Optional<String> findEmail(long id) {
    return Optional.ofNullable(db.get(id));
}
Optional<String> email = Optional.of(1L)
    .flatMap(this::findEmail);  // Optional<email> not Optional<Optional<email>>

// filter: keep only if predicate matches
Optional<Integer> adultAge = Optional.of(25)
    .filter(a -> a >= 18);  // Optional[25]
Optional<Integer> kid = Optional.of(10)
    .filter(a -> a >= 18);  // Optional.empty

// Chain transformations
String label = Optional.of("alice")
    .map(String::strip)
    .filter(s -> !s.isEmpty())
    .map(s -> s.substring(0, 1).toUpperCase() + s.substring(1))
    .orElse("unknown");  // "Alice"

class Db { String get(long id) { return "[email protected]"; } }
Db db = new Db();
Optional<String> findEmail(long id) { return Optional.ofNullable(db.get(id)); }

Антипаттерны для избегания

Optional разработан для возвращаемых типов, не полей или параметров. Он не Serializable и добавляет накладные расходы как поле. Не используйте .get() без проверки и не используйте isPresent()+get() — это просто многословная проверка на null. Коллекции уже выражают пустоту, поэтому не оборачивайте их в Optional. Используйте Optional как сигнал возвращаемого типа, что значение может отсутствовать.

java
import java.util.Optional;

// BAD: using Optional for fields (not serializable, wastes memory)
class Bad {
    private Optional<String> name;  // DON'T
}

// GOOD: use plain field, return Optional from accessor
class Good {
    private String name;
    public Optional<String> getName() { return Optional.ofNullable(name); }
}

// BAD: Optional as method parameter (clutters API)
public void process(Optional<String> input) {}  // DON'T

// GOOD: method overloading or nullable param
public void process(String input) {}
public void process() { process(null); }

// BAD: .get() without check
String x = findName().get();  // throws if empty

// BAD: .isPresent() + .get() — defeats the purpose
Optional<String> opt = findName();
if (opt.isPresent()) {
    use(opt.get());  // just use ifPresent or map instead
}

// BAD: returning Optional from collections
public Optional<Item> find(...) {
    // Collections already express emptiness — return empty List, not Optional<List>
    return Optional.ofNullable(items);
}

Optional со Streams

Optional.stream() (Java 9+) возвращает Stream из 0 или 1 элемента, что позволяет элегантно flatMap'ить Optional из stream'а. Это самый чистый способ пропустить отсутствующие значения во время stream-обработки. Он избегает многословного паттерна filter(isPresent).map(get) и сохраняет конвейер декларативным.

java
import java.util.*;
import java.util.stream.*;

// stream() on Optional: 0 or 1 element stream
Optional<String> opt = Optional.of("hi");
opt.stream().forEach(System.out::println);

// Useful: flatMap Optional out of a stream
class User {
    String email;  // may be null
    User(String e) { email = e; }
    Optional<String> getEmail() { return Optional.ofNullable(email); }
}

List<User> users = List.of(
    new User("[email protected]"),
    new User(null),
    new User("[email protected]")
);

// Extract emails, skipping nulls — clean with Optional::stream
List<String> emails = users.stream()
    .flatMap(u -> u.getEmail().stream())
    .toList();  // [[email protected], [email protected]]

// Without Optional::stream you'd need filter+map
List<String> emails2 = users.stream()
    .map(User::getEmail)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .toList();
11

Углублённое изучение Streams API

Создание Streams

Streams можно создавать из коллекций, массивов или статических фабрик. iterate() и generate() производят бесконечные streams — всегда следуйте с limit(). iterate с предикатом (Java 9+) безопаснее, чем bare iterate. IntStream/LongStream/DoubleStream избегают boxing для числовой работы. Streams одноразовые: после запуска терминальной операции stream потреблён.

java
import java.util.*;
import java.util.stream.*;

// From collections
Stream<String> s1 = List.of("a", "b").stream();
Stream<String> s2 = Set.of("x").stream();

// From arrays
int[] nums = {1, 2, 3};
IntStream s3 = Arrays.stream(nums);
Stream<String> s4 = Arrays.stream(new String[]{"a", "b"});

// Static factory methods
Stream<Integer> s5 = Stream.of(1, 2, 3);
Stream<Integer> s6 = Stream.empty();
Stream<Integer> s7 = Stream.iterate(1, n -> n * 2);  // infinite
Stream<Integer> s8 = Stream.iterate(1, n -> n < 100, n -> n + 1);  // bounded (Java 9+)
Stream<Double> s9 = Stream.generate(Math::random);  // infinite
Stream<String> s10 = Stream.ofNullable(null);  // 0 or 1 element (Java 9+)

// From functions (infinite, must limit)
List<Integer> powers = Stream.iterate(1, n -> n * 2)
    .limit(10)
    .toList();

// Numeric ranges
IntStream range = IntStream.range(0, 5);       // 0,1,2,3,4
IntStream closed = IntStream.rangeClosed(1, 5); // 1,2,3,4,5

Промежуточные операции

Промежуточные операции ленивые — они не запускаются, пока не вызвана терминальная операция. filter оставляет элементы, map преобразует 1:1, flatMap преобразует 1:many. distinct/sorted/limit/skip stateful. takeWhile/dropWhile (Java 9+) останавливаются на первом несовпадающем элементе (в отличие от filter, который сканирует всё). Используйте peek для отладки, не для побочных эффектов в production.

java
import java.util.*;
import java.util.stream.*;

List<Integer> nums = List.of(3, 1, 4, 1, 5, 9, 2, 6, 5);

// filter: keep matching
List<Integer> evens = nums.stream()
    .filter(n -> n % 2 == 0).toList();

// map: transform
List<String> labels = nums.stream()
    .map(n -> "n=" + n).toList();

// flatMap: one-to-many
List<Integer> expanded = List.of(List.of(1, 2), List.of(3))
    .stream().flatMap(List::stream).toList();  // [1,2,3]

// distinct: remove duplicates
List<Integer> uniq = nums.stream().distinct().toList();

// sorted
List<Integer> asc = nums.stream().sorted().toList();
List<Integer> desc = nums.stream().sorted(Comparator.reverseOrder()).toList();

// peek: inspect (mainly for debugging)
nums.stream().peek(n -> System.out.println("seen " + n)).count();

// limit / skip
List<Integer> first3 = nums.stream().limit(3).toList();
List<Integer> after2 = nums.stream().skip(2).toList();

// takeWhile / dropWhile (Java 9+)
List<Integer> lt5 = nums.stream().takeWhile(n -> n < 5).toList();

Collectors: grouping и partitioning

Collectors.groupingBy — это SQL GROUP BY для Java streams. Функция-классификатор определяет ключ; опциональный downstream collector обрабатывает каждую группу (counting, summing, mapping и т.д.). partitioningBy — частный случай с boolean-предикатом (ровно две корзины). Передайте TreeMap supplier для отсортированных ключей. Они компонуются мощно — можно строить многоуровневые группировки.

java
import java.util.*;
import java.util.stream.*;

record Person(String name, String city, int age) {}

List<Person> people = List.of(
    new Person("Alice", "NYC", 30),
    new Person("Bob", "LA", 25),
    new Person("Carol", "NYC", 35),
    new Person("Dave", "LA", 40)
);

// groupingBy: Map<key, List<item>>
Map<String, List<Person>> byCity = people.stream()
    .collect(Collectors.groupingBy(Person::city));
// {NYC=[Alice,Carol], LA=[Bob,Dave]}

// groupingBy with downstream collector
Map<String, Long> countByCity = people.stream()
    .collect(Collectors.groupingBy(Person::city, Collectors.counting()));

Map<String, Integer> sumAgeByCity = people.stream()
    .collect(Collectors.groupingBy(Person::city,
        Collectors.summingInt(Person::age)));

Map<String, List<String>> namesByCity = people.stream()
    .collect(Collectors.groupingBy(Person::city,
        Collectors.mapping(Person::name, Collectors.toList())));

// partitioningBy: Map<Boolean, List> (2 buckets)
Map<Boolean, List<Person>> byAge = people.stream()
    .collect(Collectors.partitioningBy(p -> p.age() >= 30));

// groupingBy with TreeMap for sorted keys
Map<String, List<Person>> sorted = people.stream()
    .collect(Collectors.groupingBy(Person::city, TreeMap::new, Collectors.toList()));

Редукция и статистика

reduce комбинирует все элементы в одно значение — предоставьте identity для безопасности с пустыми streams. summaryStatistics даёт count/sum/min/max/avg за один проход. Collectors.joining удобен для построения разделённых строк. teeing (Java 12+) запускает два collector'а параллельно и объединяет их результаты — полезно, когда нужны два агрегата (например, min и max) за один проход.

java
import java.util.*;
import java.util.stream.*;

List<Integer> nums = List.of(1, 2, 3, 4, 5);

// reduce: combine all into one
int sum = nums.stream().reduce(0, Integer::sum);          // 15
Optional<Integer> product = nums.stream().reduce((a, b) -> a * b);  // Optional[120]

// Built-in summary collectors
IntSummaryStatistics stats = nums.stream()
    .mapToInt(Integer::intValue)
    .summaryStatistics();
// stats.getCount(), getSum(), getMin(), getMax(), getAverage()

// Common terminal collectors
long count = nums.stream().collect(Collectors.counting());
double avg = nums.stream().collect(Collectors.averagingInt(Integer::intValue));
int max = nums.stream().collect(Collectors.maxBy(Comparator.naturalOrder())).orElse(0);

// join strings
String joined = List.of("a", "b", "c").stream()
    .collect(Collectors.joining(", ", "[", "]"));  // "[a, b, c]"

// teeing (Java 12+): two collectors, merge results
record Range(int min, int max) {}
Range range = nums.stream().collect(Collectors.teeing(
    Collectors.minBy(Comparator.naturalOrder()),
    Collectors.maxBy(Comparator.naturalOrder()),
    (mn, mx) -> new Range(mn.orElse(0), mx.orElse(0))
));

Числовые streams

IntStream/LongStream/DoubleStream — примитивные специализации, избегающие накладных расходов autoboxing — используйте их для числовой работы. mapToInt/mapToLong/mapToDouble конвертируют объектные streams в примитивные; boxed() — обратно. Примитивные streams имеют специализированные терминальные ops (sum, average, max), возвращающие OptionalInt/Double для обработки пустых streams. Отлично для performance-sensitive числовых конвейеров.

java
import java.util.stream.*;
import java.util.*;

// IntStream / LongStream / DoubleStream avoid boxing
IntStream range = IntStream.rangeClosed(1, 100);
int sum = range.sum();                       // 5050
double avg = IntStream.of(1, 2, 3).average().orElse(0);

// mapToInt / mapToLong / mapToDouble from object stream
int totalAge = people().stream().mapToInt(Person::age).sum();

// boxed: convert primitive stream back to object stream
List<Integer> list = IntStream.range(0, 5).boxed().toList();

// mapToObj: primitive -> objects
List<String> labels = IntStream.range(1, 4)
    .mapToObj(i -> "item-" + i).toList();

// asLongStream / asDoubleStream
LongStream longs = IntStream.range(0, 5).asLongStream();

// Common numeric operations
int max = IntStream.of(3, 1, 4, 1, 5).max().orElse(Integer.MIN_VALUE);
boolean anyEven = IntStream.of(1, 3, 5).anyMatch(n -> n % 2 == 0);

// Iterate to build numeric sequences
List<Integer> fib = Stream.iterate(new int[]{0, 1}, a -> new int[]{a[1], a[0] + a[1]})
    .limit(10).mapToInt(a -> a[0]).boxed().toList();

List<Person> people() { return List.of(new Person("a", "c", 30)); }
record Person(String name, String city, int age) {}

Параллельные streams

parallelStream разделяет работу по общему ForkJoinPool (размер по CPU cores). Используйте только для больших датасетов с CPU-интенсивными, stateless, order-independent операциями — для маленьких данных накладные расходы превышают выгоду. Избегайте разделяемого изменяемого состояния (вызывает гонки). I/O в параллельных streams блокирует общий пул — используйте кастомный ForkJoinPool для блокирующей работы. Измеряйте перед предположением, что параллельность быстрее.

java
import java.util.*;
import java.util.stream.*;

// parallelStream: uses common ForkJoinPool
long sum = List.of(1, 2, 3, 4, 5).parallelStream()
    .mapToInt(Integer::intValue).sum();

// Convert sequential to parallel
long count = IntStream.range(0, 1_000_000).parallel()
    .filter(n -> n % 2 == 0).count();

// Order may differ — use forEachOrdered if order matters
List.of(1, 2, 3, 4).parallelStream()
    .forEachOrdered(System.out::println);

// Collecting preserves encounter order (but work is parallel)
List<Integer> doubled = IntStream.range(0, 1000).parallel()
    .map(n -> n * 2).boxed().toList();

// Custom thread pool (avoid blocking the common pool)
import java.util.concurrent.ForkJoinPool;
ForkJoinPool pool = new ForkJoinPool(8);
int result = pool.submit(() ->
    IntStream.range(0, 1000).parallel().sum()
).get();

// WHEN to use parallel: large dataset, CPU-heavy per element,
// order-independent, stateless operations
// WHEN NOT: small data, I/O-bound, shared mutable state, ordered ops
12

Углублённое изучение дженериков

Дженерик-классы и методы

Дженерики обеспечивают типобезопасный переиспользуемый код. Классы объявляют параметры типа (<T>); методы тоже (<T> перед возвращаемым типом). Diamond-оператор <> выводит тип при конструировании. Дженерики проверяются при компиляции — они делают коллекции и API безопаснее, отлавливая ошибки типов рано, вместо runtime через ClassCastException.

java
// Generic class
public class Box<T> {
    private T value;
    public void set(T v) { value = v; }
    public T get() { return value; }
}

Box<String> strBox = new Box<>();
strBox.set("hello");
String s = strBox.get();  // no cast needed

// Multiple type parameters
public class Pair<K, V> {
    private final K key;
    private final V value;
    public Pair(K k, V v) { key = k; value = v; }
    public K key() { return key; }
    public V value() { return value; }
}

Pair<String, Integer> p = new Pair<>("age", 30);

// Generic method (independent of class type params)
public static <T> T first(List<T> list) {
    return list.get(0);
}

// Generic method with multiple type params
public static <K, V> Map<K, V> zip(List<K> keys, List<V> values) {
    Map<K, V> m = new HashMap<>();
    for (int i = 0; i < keys.size(); i++) m.put(keys.get(i), values.get(i));
    return m;
}

Ограниченные параметры типа

Ограниченные параметры типа (<T extends Bound>) ограничивают используемые типы и позволяют вызывать методы bound'а. <T extends Number> означает, что T должен быть Number или подтипом. Несколько bounds используют & — максимум один класс (должен быть первым), остальные интерфейсы. Bounds существенны для написания алгоритмов, требующих специфических возможностей (сравнимость, числовые операции).

java
// Upper bound: T must be a subtype of Number
public static <T extends Number> double sum(List<T> nums) {
    double total = 0;
    for (Number n : nums) total += n.doubleValue();
    return total;
}

sum(List.of(1, 2, 3));        // Integer is a Number
sum(List.of(1.0, 2.5));       // Double is a Number
// sum(List.of("a"));          // compile error

// Multiple bounds: T must extend all (first is class, rest interfaces)
interface Comparable<T> { int compareTo(T o); }
interface Serializable {}

public static <T extends Number & Comparable<T> & Serializable>
    T max(List<T> list) {
    T best = list.get(0);
    for (T t : list) if (t.compareTo(best) > 0) best = t;
    return best;
}

// Bound lets you call methods of the bound
public static <T extends CharSequence> int totalLength(List<T> items) {
    int len = 0;
    for (CharSequence c : items) len += c.length();  // can call .length()
    return len;
}

Wildcards: ?, extends, super

Wildcards делают дженерик-типы гибкими. ? extends T (ковариантный) позволяет читать T, но не писать — используйте для производителей. ? super T (контравариантный) позволяет писать T, но только читать Object — используйте для потребителей. Правило PECS (Producer Extends, Consumer Super) направляет, что использовать. copy(dest, src) — классический пример: dest потребитель (super), src производитель (extends).

java
import java.util.*;

// ? (unbounded wildcard) — any type
void printAll(List<?> list) {
    for (Object o : list) System.out.println(o);
}

// ? extends T (upper-bounded / covariant) — producer (PECS: Producer Extends)
double sum(List<? extends Number> nums) {
    double total = 0;
    for (Number n : nums) total += n.doubleValue();
    return total;
}
sum(List.of(1, 2, 3));     // List<Integer> OK
sum(List.of(1.0, 2.0));    // List<Double> OK
// nums.add(5);  // ERROR: can't add (don't know exact type)

// ? super T (lower-bounded / contravariant) — consumer (PECS: Consumer Super)
void addNumbers(List<? super Integer> list) {
    list.add(1); list.add(2); list.add(3);  // OK to add Integer
}
addNumbers(new ArrayList<Number>());  // OK
addNumbers(new ArrayList<Object>());  // OK
// Number n = list.get(0);  // only safe to read as Object

// PECS rule: Producer Extends, Consumer Super
// If you read from a collection, use ? extends T
// If you write to a collection, use ? super T
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T t : src) dest.add(t);
}

Type erasure

Дженерики Java используют type erasure — дженерик-типы существуют только при компиляции; в runtime List<String> и List<Integer> оба просто List. Это обеспечивает обратную совместимость, но имеет ограничения: нельзя new T(), создавать дженерик-массивы, использовать instanceof с дженериками или иметь перегруженные методы с одинаково стёртыми сигнатурами. Heap pollution возникает, когда unchecked casts помещают неверные типы в дженерики, откладывая ошибки в runtime.

java
import java.util.*;

// At runtime, generic types are erased to their bounds (or Object)
// List<String>, List<Integer>, List<?> all become List at runtime

List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
// Runtime: both are just ArrayList

// You CANNOT do these due to erasure:
// new T()              — can't instantiate type param
// new T[]              — can't create generic array
// instanceof List<String>  — only instanceof List (raw)
// class MyException<T> extends Exception  — can't extend Throwable generically
// static T field       — no static generic fields

// Erasure means overloads clash:
// void process(List<String> list) {}
// void process(List<Integer> list) {}  // ERROR: same erasure

// Checking types at runtime requires Class<T>
public static <T> List<T> filter(List<?> items, Class<T> type) {
    List<T> result = new ArrayList<>();
    for (Object o : items) {
        if (type.isInstance(o)) result.add(type.cast(o));
    }
    return result;
}

// Heap pollution: when unchecked warnings lead to runtime ClassCastException
List<String> polluted = (List<String>)(List) List.of(1, 2);  // unchecked
// String s = polluted.get(0);  // ClassCastException at runtime

Дженерик-методы и вывод

Вывод типов позволяет компилятору определить аргументы типа из контекста (аргументы и целевой тип), поэтому редко нужно писать их явно. Diamond-оператор <> — вывод для конструкторов. Target typing использует ожидаемый тип переменной. Используйте явный type witness (Class.<T>method()) только когда вывод не может разрешить неоднозначность. Вывод делает дженерик-код таким же читаемым, как не-дженерик.

java
import java.util.*;

// Type inference: compiler figures out T from arguments
public static <T> T pick(T a, T b) { return Math.random() > 0.5 ? a : b; }
String s = pick("hello", "world");      // T inferred as String
Number n = pick(1, 2.0);                // T inferred as Number (common supertype)

// Target typing: inference uses the expected type
List<String> list = Collections.emptyList();  // T inferred from target

// Inference with method chains
List<Integer> nums = List.of(1, 2, 3);
String joined = nums.stream()
    .map(Object::toString)              // Stream<String>
    .collect(Collectors.joining(","));  // inferred

// Explicit type witness (rarely needed)
Collections.<String>emptyList();

// Generic constructor
class Holder<T> {
    private T value;
    <U extends T> Holder(U init) { value = init; }  // constructor type param
    T get() { return value; }
}
Holder<Number> h = new Holder<>(42);  // U=Integer, T=Number

import java.util.stream.Collectors;
class Math { static double random() { return 0.5; } }

Дженерик-интерфейсы и паттерны

Дженерик-интерфейсы (как Repository<T,ID>) определяют переиспользуемые контракты. Паттерн self-referencing bound (class X implements Comparable<X>) гарантирует, что compareTo принимает только тот же тип. Паттерн type token (использование Class<T> как ключ) обходит erasure, обеспечивая runtime-типобезопасность в гетерогенных контейнерах. Эти паттерны — основа фреймворков вроде Spring Data.

java
// Generic interface
interface Repository<T, ID> {
    Optional<T> findById(ID id);
    List<T> findAll();
    void save(T entity);
}

// Implement with concrete types
class UserRepository implements Repository<User, Long> {
    public Optional<User> findById(Long id) { /* ... */ return Optional.empty(); }
    public List<User> findAll() { return List.of(); }
    public void save(User entity) {}
}

// Generic interface with self-referencing bound (Comparable pattern)
interface Comparable<T> {
    int compareTo(T other);
}
class Temperature implements Comparable<Temperature> {
    private final double celsius;
    Temperature(double c) { celsius = c; }
    public int compareTo(Temperature other) {
        return Double.compare(celsius, other.celsius);
    }
}

// Generic builder pattern
class Builder<T> {
    private T value;
    public Builder<T> set(T v) { value = v; return this; }
    public T build() { return value; }
}

// Type token pattern for runtime type safety
class TypeSafeMap {
    private final Map<Class<?>, Object> map = new HashMap<>();
    public <T> void put(Class<T> type, T value) { map.put(type, value); }
    public <T> T get(Class<T> type) { return type.cast(map.get(type)); }
}

record User(String name) {}
import java.util.Optional;
13

Аннотации

Встроенные аннотации

Встроенные аннотации Java: @Override (отлавливает опечатки в переопределении — всегда используйте), @Deprecated (сигнализирует, что API не следует использовать, с метаданными since/forRemoval), @SuppressWarnings (заглушает конкретные предупреждения — используйте узко), @FunctionalInterface (требует правило SAM). Это повседневные аннотации, улучшающие compile-time безопасность и документацию.

java
import java.util.*;

// @Override: declares intent to override (compiler checks)
class Animal {
    public void sound() { System.out.println("..."); }
}
class Dog extends Animal {
    @Override
    public void sound() { System.out.println("Woof"); }
}

// @Deprecated: marks API as outdated
class OldApi {
    @Deprecated(since = "1.5", forRemoval = true)
    public void legacy() {}
}

// @SuppressWarnings: silence compiler warnings
@SuppressWarnings("unchecked")
List<String> list = (List<String>) new ArrayList();

// @FunctionalInterface: enforces single abstract method
@FunctionalInterface
interface Op { int apply(int a, int b); }

// Common warning keys: unchecked, deprecation, rawtypes, null

// Java 17+ sealed-related
@Deprecated
class ToRemove {}

Пользовательские аннотации

Пользовательские аннотации объявляются через @interface. Члены выглядят как методы, но являются атрибутами аннотации — могут иметь значения по умолчанию. Используйте @Target для ограничения применения (TYPE, METHOD, FIELD и т.д.) и @Retention для управления доступностью. Marker-аннотации (без членов) просто помечают элементы. Аннотации сами не несут поведения — процессоры (reflection, annotation tools) читают их и действуют.

java
import java.lang.annotation.*;

// Define an annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
    String value() default "";
    long timeout() default 0L;
}

// Use it
class MyTests {
    @Test
    public void quickCheck() {}

    @Test(timeout = 5000)
    public void slowCheck() {}

    @Test("custom-name")
    public void named() {}
}

// Annotation with default values
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface Entity {
    String table() default "";
    String[] columns() default {};
}

@Entity(table = "users", columns = {"id", "name"})
class User {}

// Marker annotation (no members)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Service {}

Retention и Target

@Retention контролирует, как долго аннотация сохраняется: SOURCE (только compile-time, как @Override), CLASS (в байткоде, но не видна в runtime — по умолчанию), RUNTIME (доступна через reflection). @Target ограничивает, где может появляться аннотация. Java 8+ добавила TYPE_USE и TYPE_PARAMETER, позволяя аннотировать дженерики и приведения (List<@NonNull String>). Выбирайте RUNTIME только если нужен доступ через reflection.

java
import java.lang.annotation.*;

// RetentionPolicy.SOURCE: discarded by compiler (e.g., @Override)
@Retention(RetentionPolicy.SOURCE)
@interface CompileOnly {}

// RetentionPolicy.CLASS: kept in .class but not loaded (default)
@Retention(RetentionPolicy.CLASS)
@interface BytecodeOnly {}

// RetentionPolicy.RUNTIME: available via reflection at runtime
@Retention(RetentionPolicy.RUNTIME)
@interface RuntimeVisible {}

// ElementType targets
@Target(ElementType.TYPE)         // classes, interfaces, enums
@interface ForType {}

@Target(ElementType.METHOD)
@interface ForMethod {}

@Target(ElementType.FIELD)
@interface ForField {}

@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
@interface ForTypeUse {}

// Java 8+ type-use annotations (annotate any type occurrence)
@RuntimeVisible String[] names;  // example usage
List<@RuntimeVisible String> typed;

Чтение аннотаций через reflection

Аннотации с RUNTIME retention можно читать через reflection: isAnnotationPresent() проверяет существование, getAnnotation() извлекает. Так фреймворки (Spring, JUnit, JAX-RS) декларативно подключают поведение — вы помечаете методы/классы, фреймворк сканирует и диспетчеризирует. Обработка аннотаций при компиляции (annotation processors) — альтернатива для генерации кода без runtime-reflection.

java
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Route {
    String path();
    String method() default "GET";
}

class Api {
    @Route(path = "/users", method = "GET")
    public void listUsers() {}

    @Route(path = "/users", method = "POST")
    public void createUser() {}
}

// Scan methods for @Route at runtime
for (Method m : Api.class.getDeclaredMethods()) {
    if (m.isAnnotationPresent(Route.class)) {
        Route r = m.getAnnotation(Route.class);
        System.out.println(r.method() + " " + r.path() + " -> " + m.getName());
    }
}
// Output:
// GET /users -> listUsers
// POST /users -> createUser

// Reading annotations on a class
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Table { String name(); }

@Table(name = "orders")
class Order {}
Table t = Order.class.getAnnotation(Table.class);
System.out.println(t.name());  // "orders"

Repeatable и мета-аннотации

@Repeatable (Java 8+) позволяет применять одну аннотацию несколько раз через определение контейнерной аннотации. @Inherited распространяет аннотации на подклассы (только для class-level аннотаций). @Documented включает аннотацию в Javadoc. @Target с ANNOTATION_TYPE создаёт мета-аннотации (аннотации, аннотирующие другие аннотации) — так Spring строит компонуемые аннотационные стереотипы вроде @RestController = @Controller + @ResponseBody.

java
import java.lang.annotation.*;

// Repeatable: allow same annotation multiple times
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedule {
    String cron();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedules {
    Schedule[] value();  // container annotation
}

// Make Schedule repeatable
@Repeatable(Schedules.class)
@interface Schedule2 {
    String cron();
}

// Now you can repeat it (Java 8+)
class Job {
    @Schedule2(cron = "0 0 * * *")
    @Schedule2(cron = "0 30 * * *")
    public void run() {}
}

// Meta-annotations: annotations on annotations
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)  // can only annotate other annotations
@interface TestCategory {}

@TestCategory
@Retention(RetentionPolicy.RUNTIME)
@interface UnitTest {}

// Inherited: subclass inherits the annotation
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Persistent {}
@Persistent class Base {}
class Child extends Base {}  // Child also has @Persistent

// Documented: appears in Javadoc
@Documented
@interface PublicApi {}
14

Reflection

Объект Class и получение классов

Каждый загруженный тип имеет уникальный объект Class — точка входа в reflection. Получите через class literal (Type.class), instance.getClass(), или Class.forName() (динамическая загрузка, бросает ClassNotFoundException). Объект Class раскрывает name, modifiers, superclass, interfaces и проверки типов (isInterface, isArray, isEnum, isRecord). isAssignableFrom проверяет полиморфные отношения.

java
import java.lang.reflect.*;

// Three ways to get a Class object
Class<String> c1 = String.class;                  // class literal
Class<?> c2 = "hello".getClass();                 // from instance
Class<?> c3 = Class.forName("java.lang.String");  // by name (throws checked)

// Basic introspection
Class<?> c = String.class;
System.out.println(c.getName());          // "java.lang.String"
System.out.println(c.getSimpleName());    // "String"
System.out.println(c.getPackage());       // "package java.lang"
System.out.println(c.getSuperclass());    // "class java.lang.Object"
System.out.println(Modifier.toString(c.getModifiers()));  // "public final"

// Check type relationships
System.out.println(c.isInterface());      // false
System.out.println(c.isArray());          // false
System.out.println(c.isEnum());           // false
System.out.println(c.isRecord());         // false (Java 16+)
System.out.println(CharSequence.class.isAssignableFrom(c));  // true

// Primitive class objects
Class<?> intClass = int.class;
Class<?> intArrayClass = int[].class;
System.out.println(intClass.isPrimitive());  // true

Инспекция полей, методов, конструкторов

getDeclaredFields/Methods/Constructors возвращают ВСЕ члены (включая приватные), объявленные только в этом классе. getFields/getMethods возвращают только public-члены, но включают унаследованные. Для поиска конкретного члена используйте getDeclaredField(name) или getDeclaredMethod(name, paramTypes...) — типы параметров нужны для различения перегрузок. Reflection обходит контроль доступа, если вызвать setAccessible(true).

java
import java.lang.reflect.*;
import java.util.*;

class Sample {
    public String name;
    private int count;
    public Sample() {}
    public Sample(String n) { name = n; }
    private void secret() {}
    public int compute(int x) { return x * 2; }
}

Class<?> c = Sample.class;

// Fields: getDeclaredFields includes private; getFields only public
for (Field f : c.getDeclaredFields()) {
    System.out.println(f.getName() + " : " + f.getType().getSimpleName()
        + " (" + Modifier.toString(f.getModifiers()) + ")");
}

// Methods
for (Method m : c.getDeclaredMethods()) {
    System.out.println(m.getName()
        + " params=" + Arrays.toString(m.getParameterTypes())
        + " returns=" + m.getReturnType().getSimpleName());
}

// Constructors
for (Constructor<?> ctor : c.getConstructors()) {
    System.out.println("ctor params=" + Arrays.toString(ctor.getParameterTypes()));
}

// Lookup specific member
Field nameField = c.getDeclaredField("name");
Method compute = c.getDeclaredMethod("compute", int.class);
Constructor<?> ctor = c.getDeclaredConstructor(String.class);

Вызов методов и создание экземпляров

Method.invoke(obj, args...) вызывает метод рефлективно — всегда возвращает Object, поэтому приводите результат. setAccessible(true) обходит проверки доступа Java (приватные члены становятся доступными; может требовать --add-opens на модулях). Constructor.newInstance() создаёт объекты — рефлективный эквивалент new. Array.newInstance создаёт массивы компонентного типа, известного в runtime. Reflection медленнее прямых вызовов и обходит compile-time безопасность.

java
import java.lang.reflect.*;

class Greeter {
    public String greet(String name) { return "Hello, " + name; }
    private String secret() { return "hidden"; }
}

Object obj = new Greeter();
Class<?> c = obj.getClass();

// Invoke public method
Method greet = c.getMethod("greet", String.class);
String result = (String) greet.invoke(obj, "Alice");  // "Hello, Alice"

// Invoke private method
Method sec = c.getDeclaredMethod("secret");
sec.setAccessible(true);  // bypass access check
String s = (String) sec.invoke(obj);  // "hidden"

// Create instances via constructor
Constructor<?> noArg = c.getConstructor();
Object o1 = noArg.newInstance();

// With args
class Person {
    String name;
    public Person(String n) { name = n; }
    public String toString() { return "Person(" + name + ")"; }
}
Constructor<?> ctor = Person.class.getConstructor(String.class);
Object p = ctor.newInstance("Bob");
System.out.println(p);  // "Person(Bob)"

// Array creation via reflection
Object strArray = Array.newInstance(String.class, 5);
Array.set(strArray, 0, "first");
String v = (String) Array.get(strArray, 0);

Чтение и модификация полей

Field.get(instance) читает значение поля; Field.set(instance, value) записывает. Для примитивов используйте type-specific аксессоры (getInt/setInt) для избежания boxing. Static-поля принимают null как аргумент экземпляра. setAccessible(true) требуется для приватных полей. Доступ к полям через reflection — как библиотеки сериализации (Jackson, Gson) и ORM-фреймворки читают/пишут состояние объектов генерически.

java
import java.lang.reflect.*;

class Config {
    public String env = "dev";
    private int retries = 3;
    public static String VERSION = "1.0";
}

Config cfg = new Config();
Class<?> c = cfg.getClass();

// Read public field
Field env = c.getField("env");
String e = (String) env.get(cfg);  // "dev"

// Read private field
Field retries = c.getDeclaredField("retries");
retries.setAccessible(true);
int r = retries.getInt(cfg);  // 3
// For objects: Object val = field.get(instance);

// Modify fields
env.set(cfg, "prod");
retries.setInt(cfg, 5);
System.out.println(cfg.env);     // "prod"

// Static fields: pass null as the instance
Field version = c.getField("VERSION");
String v = (String) version.get(null);  // "1.0"
version.set(null, "2.0");

// Type-specific getters/setters avoid boxing
// getInt/setInt, getLong/setLong, getBoolean/setBoolean, etc.
// For reference types, use get()/set()

Динамические прокси и use-кейсы

java.lang.reflect.Proxy создаёт динамические прокси, реализующие интерфейсы в runtime — InvocationHandler перехватывает каждый вызов. Так работают Spring AOP, Hibernate lazy loading и Mockito mocks. Reflection питает большинство Java-фреймворков (DI, ORM, сериализация, тестирование), но имеет затраты: медленнее прямых вызовов, слабее типобезопасность и ограничения module-system. Используйте, когда нужна runtime-гибкость, не для обычного кода.

java
import java.lang.reflect.*;
import java.util.*;

// JDK dynamic proxy: implements interfaces at runtime
interface UserService {
    String getUser(long id);
    void deleteUser(long id);
}

// InvocationHandler intercepts every method call
class LoggingHandler implements InvocationHandler {
    private final Object target;
    LoggingHandler(Object t) { target = t; }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Calling " + method.getName() + "(" + Arrays.toString(args) + ")");
        long start = System.nanoTime();
        Object result = method.invoke(target, args);
        System.out.println("  -> " + result + " (" + (System.nanoTime() - start) + "ns)");
        return result;
    }
}

UserService real = id -> "user-" + id;
UserService proxied = (UserService) Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class<?>[]{UserService.class},
    new LoggingHandler(real)
);
proxied.getUser(42);  // logs the call and result

// Common reflection use cases:
// - Frameworks: Spring DI, JPA entities, Jackson serialization
// - AOP/proxies: transactions, logging, security
// - Annotation processing at runtime
// - Test frameworks: JUnit discovers @Test methods
// - Code generation tools

// Caution: reflection is slower, bypasses compile-time checks,
// and can break under modules (--add-opens). Prefer alternatives when possible.
15

JDBC и доступ к БД

Connection и DriverManager

DriverManager.getConnection() открывает соединение с БД — всегда оборачивайте в try-with-resources для избежания утечек. Начиная с JDBC 4, драйверы авто-регистрируются через ServiceLoader, поэтому Class.forName() редко нужен. Формат URL варьируется по вендору. Используйте Properties для опций соединения (SSL, timeouts). В production предпочитайте пул соединений (HikariCP) прямым вызовам DriverManager.

java
import java.sql.*;

// Basic connection (try-with-resources auto-closes)
String url = "jdbc:postgresql://localhost:5432/mydb";
try (Connection conn = DriverManager.getConnection(url, "user", "pass")) {
    System.out.println("Connected: " + conn.getSchema());
    // ... use connection
}

// Modern: no need for Class.forName() with JDBC 4+ (auto-discovery)
// Legacy: Class.forName("org.postgresql.Driver");

// Common URL patterns:
// jdbc:postgresql://host:5432/db
// jdbc:mysql://host:3306/db
// jdbc:oracle:thin:@host:1521:db
// jdbc:sqlite:/path/to/db.sqlite
// jdbc:h2:mem:test  (in-memory H2)

// Connection properties
import java.util.Properties;
Properties props = new Properties();
props.setProperty("user", "user");
props.setProperty("password", "pass");
props.setProperty("ssl", "true");
try (Connection c = DriverManager.getConnection(url, props)) {
    // ...
}

Statement vs PreparedStatement

Всегда используйте PreparedStatement вместо Statement для любого запроса с параметрами — он предотвращает SQL injection, разделяя структуру SQL и данные. Параметры устанавливаются по 1-базовому индексу с type-specific сеттерами. PreparedStatement можно переиспользовать (переустановить params и выполнить снова) и поддерживает batching (addBatch/executeBatch) для bulk-операций. Statement подходит только для статического, доверенного SQL вроде DDL.

java
import java.sql.*;

// Statement: plain SQL (vulnerable to injection — avoid for user input)
try (Connection c = DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
     Statement st = c.createStatement()) {

    st.execute("CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))");
    st.execute("INSERT INTO users VALUES (1, 'Alice')");

    // NEVER do this with user input — SQL injection!
    // st.execute("SELECT * FROM users WHERE name = '" + userInput + "'");
}

// PreparedStatement: parameterized, safe from injection, can be reused
String sql = "INSERT INTO users (id, name) VALUES (?, ?)";
try (Connection c = DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
     PreparedStatement ps = c.prepareStatement(sql)) {

    // Set parameters by index (1-based!)
    ps.setInt(1, 1);
    ps.setString(2, "Alice");
    ps.executeUpdate();

    // Reuse with different params
    ps.setInt(1, 2);
    ps.setString(2, "Bob");
    ps.executeUpdate();

    // Batch inserts
    for (int i = 3; i <= 100; i++) {
        ps.setInt(1, i);
        ps.setString(2, "user" + i);
        ps.addBatch();
    }
    ps.executeBatch();
}

ResultSet и запросы

ResultSet — курсор по строкам запроса — вызывайте next() для продвижения (возвращает false в конце). Читайте столбцы по имени (читаемо) или 1-базовому индексу. wasNull() различает SQL NULL от примитивного default (например, getInt возвращает 0 для NULL). Default ResultSets forward-only; TYPE_SCROLL_INSENSITIVE + CONCUR_UPDATABLE включает случайный доступ и обновление на месте, хотя это редко используется в современных приложениях.

java
import java.sql.*;
import java.util.*;

record User(int id, String name, String email) {}

// Execute query and map rows
String sql = "SELECT id, name, email FROM users WHERE active = ?";
List<User> users = new ArrayList<>();
try (Connection c = getConnection();
     PreparedStatement ps = c.prepareStatement(sql)) {
    ps.setBoolean(1, true);
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {  // advance to next row
            users.add(new User(
                rs.getInt("id"),       // by column name
                rs.getString("name"),
                rs.getString("email")  // by column name (preferred)
            ));
        }
    }
}

// Column access by index (1-based) or name (more readable)
// rs.getInt(1), rs.getString(2), rs.getBoolean("active")

// Handle NULLs
String nick = rs.getString("nickname");
if (rs.wasNull()) nick = "anonymous";  // distinguish NULL from real null

// Scrollable/updatable ResultSet (needs specific flags)
Statement st = c.createStatement(
    ResultSet.TYPE_SCROLL_INSENSITIVE,
    ResultSet.CONCUR_UPDATABLE
);
ResultSet rs = st.executeQuery("SELECT * FROM users");
rs.absolute(5);  // jump to row 5
rs.updateString("name", "newname");
rs.updateRow();  // persist change

Connection getConnection() throws SQLException {
    return DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
}

Транзакции и batch

JDBC авто-коммитит каждый оператор по умолчанию — установите autoCommit(false) для группировки операторов в транзакцию. Commit при успехе, rollback при неудаче. Savepoints позволяют частичный rollback внутри транзакции. Уровни изоляции контролируют видимость параллельных изменений (READ_COMMITTED — общий default; SERIALIZABLE — безопаснее, но медленнее). Всегда восстанавливайте autoCommit или закрывайте соединение для избежания утечки состояния транзакции.

java
import java.sql.*;

// Transactions: disabled by default (auto-commit = true)
try (Connection c = getConnection()) {
    c.setAutoCommit(false);  // start transaction
    try (PreparedStatement ps = c.prepareStatement(
            "UPDATE accounts SET balance = balance - ? WHERE id = ?")) {
        ps.setInt(1, 100); ps.setInt(2, 1); ps.executeUpdate();  // debit
        ps.setInt(1, -100); ps.setInt(2, 2); ps.executeUpdate();  // credit
    }
    c.commit();  // commit both

    // If any step fails, rollback
} catch (SQLException e) {
    // connection auto-closed; rollback happens implicitly on close if not committed
}

// Explicit rollback pattern
try (Connection c = getConnection()) {
    c.setAutoCommit(false);
    try {
        // ... multiple statements
        c.commit();
    } catch (SQLException e) {
        c.rollback();  // undo all changes in this transaction
        throw e;
    }
}

// Savepoints: partial rollback
Statement st = c.createStatement();
st.execute("INSERT INTO log VALUES (1)");
Savepoint sp = c.setSavepoint("before-risky");
st.execute("INSERT INTO log VALUES (2)");
c.rollback(sp);  // undo only after savepoint
c.commit();

// Transaction isolation levels
c.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
// Levels: NONE, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE

Connection getConnection() throws SQLException {
    return DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
}

Пулинг соединений (HikariCP)

Пулы соединений (HikariCP — де-факто стандарт) держат соединения тёплыми и переиспользуют их, избегая затрат 10-100ms на открытие нового TCP+auth-соединения на запрос. Настраивайте max pool size (ограничено ёмкостью БД), timeouts и lifetime. Borrow через getConnection(), return через close (возвращается в пул, не закрывается). Всегда закрывайте DataSource при выключении. В Spring Boot HikariCP авто-конфигурируется.

java
import com.zaxxer.hikari.*;
import java.sql.*;
import java.util.*;

// HikariCP: high-performance JDBC connection pool
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
config.setUsername("user");
config.setPassword("pass");
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.setConnectionTimeout(30_000);   // ms to wait for a connection
config.setIdleTimeout(600_000);        // ms before idle connections close
config.setMaxLifetime(1_800_000);      // ms max connection lifetime
config.setPoolName("app-pool");

HikariDataSource ds = new HikariDataSource(config);

// Borrow a connection, use it, return it (auto via try-with-resources)
try (Connection c = ds.getConnection();
     PreparedStatement ps = c.prepareStatement("SELECT * FROM users")) {
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) System.out.println(rs.getString("name"));
    }
}  // connection returns to pool here

// Shutdown pool when app stops
ds.close();

// Why pool? Opening a TCP+auth connection is expensive (~10-100ms).
// A pool keeps connections warm and reuses them across requests,
// dramatically reducing latency under load.

// Alternative pools: Apache DBCP, c3p0, Tomcat JDBC, Agroal
16

Углублённое изучение I/O и NIO

InputStream и OutputStream (байты)

InputStream/OutputStream обрабатывают сырые байты. Всегда оборачивайте в Buffered* варианты — небуферизованный I/O делает системный вызов на байт, что катастрофично для производительности. read() возвращает -1 в конце потока. transferTo() (Java 9+) делает эффективное bulk-копирование. readAllBytes() удобен, но загружает весь поток в память — только для маленьких файлов. Всегда закрывайте streams (try-with-resources).

java
import java.io.*;

// Read bytes from a file
try (InputStream in = new FileInputStream("input.bin")) {
    int b;
    while ((b = in.read()) != -1) {  // -1 = end of stream
        // process byte
    }
}

// Buffered for performance (8KB default buffer)
try (InputStream in = new BufferedInputStream(new FileInputStream("big.bin"))) {
    byte[] buffer = new byte[8192];
    int read;
    while ((read = in.read(buffer)) != -1) {
        // process buffer[0..read]
    }
}

// readAllBytes (small files only — loads everything into memory)
byte[] all = new FileInputStream("small.bin").readAllBytes();

// Write bytes
try (OutputStream out = new BufferedOutputStream(new FileOutputStream("out.bin"))) {
    out.write(65);  // single byte
    out.write(new byte[]{66, 67, 68});
    out.flush();  // force buffered data to disk
}

// Copy streams (Java 9+)
try (InputStream in = new FileInputStream("src.bin");
     OutputStream out = new FileOutputStream("dst.bin")) {
    in.transferTo(out);  // efficient bulk copy
}

// Standard streams
System.in.read();   // stdin (InputStream)
System.out.write(65);  // stdout (PrintStream)

Channels и Buffers (NIO)

NIO Channels + ByteBuffers — высокопроизводительная альтернатива потокам. Buffers имеют position/limit/capacity; flip() переключает из режима записи в чтение, clear() сбрасывает для записи, compact() сохраняет непрочитанные данные. Direct buffers (allocateDirect) живут вне кучи JVM и избегают шага копирования для большого I/O. transferTo между channels может использовать zero-copy на поддерживаемых ОС. Используйте NIO, когда важна streaming-производительность.

java
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.io.*;

// Channel: high-performance, block-oriented I/O
try (FileChannel ch = FileChannel.open(Path.of("data.bin"),
        StandardOpenOption.READ, StandardOpenOption.WRITE)) {

    // ByteBuffer: fixed-capacity block of bytes
    ByteBuffer buf = ByteBuffer.allocate(1024);
    int read = ch.read(buf);  // fill buffer from channel
    buf.flip();  // switch from write mode to read mode

    while (buf.hasRemaining()) {
        byte b = buf.get();
    }
    buf.clear();  // reset for next read (or compact() to preserve unread)
}

// Direct buffer: outside JVM heap, faster for large I/O (no copy)
ByteBuffer direct = ByteBuffer.allocateDirect(64 * 1024);

// Scatter/gather: read into multiple buffers / write from multiple
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ch.read(new ByteBuffer[]{header, body});  // scatter

// Transfer between channels (zero-copy on some OSes)
try (FileChannel src = FileChannel.open(Path.of("a.bin"));
     FileChannel dst = FileChannel.open(Path.of("b.bin"), StandardOpenOption.WRITE)) {
    src.transferTo(0, src.size(), dst);
}

// ByteOrder
buf.order(ByteOrder.LITTLE_ENDIAN);
int value = buf.getInt();

Операции Path (NIO.2)

Path (NIO.2) заменяет старый класс File. Операции Path (getFileName, getParent, resolve, normalize, relativize) — чистая строковая математика — они не касаются диска. Методы Files.* взаимодействуют с файловой системой: size, timestamps, permissions, symlinks. normalize() очищает сегменты . и ... resolveSibling удобен для переименования (та же директория, другое имя). Предпочитайте Path вместо File в современном коде.

java
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.IOException;

// Creating Path objects
Path p1 = Path.of("a/b/c.txt");          // relative
Path p2 = Path.of("/usr/local/bin");     // absolute
Path p3 = Paths.get("config.json");      // older API, same thing

// Path manipulation (doesn't touch filesystem)
System.out.println(p1.getFileName());    // "c.txt"
System.out.println(p1.getParent());      // "a/b"
System.out.println(p1.getRoot());        // "" (relative)
System.out.println(p2.getRoot());        // "/"
System.out.println(p1.toAbsolutePath()); // "/cwd/a/b/c.txt"
System.out.println(p1.normalize());      // removes . and ..
System.out.println(p1.resolve("d.txt")); // "a/b/c.txt/d.txt"
System.out.println(p1.resolveSibling("x.txt")); // "a/b/x.txt"
System.out.println(p1.relativize(Path.of("a/b")));  // "../.."

// File metadata
Path file = Path.of("notes.txt");
System.out.println(Files.size(file));            // bytes
System.out.println(Files.getLastModifiedTime(file));
System.out.println(Files.isReadable(file));
System.out.println(Files.isWritable(file));

// PosixFilePermissions (Linux/macOS)
String perms = "rwxr-xr--";
Set<PosixFilePermission> set = PosixFilePermissions.fromString(perms);
Files.setPosixFilePermissions(file, set);

// Symbolic links
Path link = Path.of("link.txt");
Files.createSymbolicLink(link, Path.of("target.txt"));
Path target = Files.readSymbolicLink(link);

Обход директорий и файловые деревья

Files.list() перечисляет один уровень; Files.walk() рекурсивно обходит дерево (возвращает Stream, нужно закрыть). Files.find() фильтрует по пути и атрибутам во время обхода. Для полного контроля walkFileTree с FileVisitor позволяет пропускать поддеревья, обрабатывать ошибки и действовать до/после посещения директорий. Все возвращают Streams, держащие файловые дескрипторы — всегда используйте try-with-resources. Используйте max depth для ограничения дорогих обходов.

java
import java.nio.file.*;
import java.io.IOException;
import java.util.stream.*;

// List directory entries (one level)
try (Stream<Path> entries = Files.list(Path.of("."))) {
    entries.filter(Files::isRegularFile)
           .map(Path::getFileName)
           .forEach(System.out::println);
}

// Walk file tree recursively (depth-first)
try (Stream<Path> walk = Files.walk(Path.of("/project"))) {
    walk.filter(Files::isRegularFile)
        .filter(p -> p.toString().endsWith(".java"))
        .forEach(System.out::println);
}

// Walk with depth limit
try (Stream<Path> walk = Files.walk(Path.of("/project"), 3)) {
    walk.forEach(System.out::println);
}

// Find with BiPredicate (path + attributes)
try (Stream<Path> found = Files.find(Path.of("/logs"), 10,
        (path, attrs) -> attrs.isRegularFile()
            && attrs.size() > 1_000_000
            && path.toString().endsWith(".log"))) {
    found.forEach(p -> System.out.println("Large log: " + p));
}

// FileVisitor for full control (pre/post visit, skip subtrees)
Files.walkFileTree(Path.of("/project"), new SimpleFileVisitor<>() {
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        System.out.println("Visiting " + file);
        return FileVisitResult.CONTINUE;
    }
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
        if (dir.getFileName().toString().equals("target")) {
            return FileVisitResult.SKIP_SUBTREE;  // skip target dirs
        }
        return FileVisitResult.CONTINUE;
    }
});

import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.BasicFileAttributes;

WatchService и файловые события

WatchService получает файловые события (create, modify, delete) для зарегистрированных директорий. take() блокируется до прибытия события; pollEvents() дренирует их. Следите за родительской директорией и фильтруйте по context() для отслеживания конкретного файла. События могут объединяться или теряться (OVERFLOW). Рекурсивное наблюдение требует регистрации каждой поддиректории. WatchService OS-native (inotify на Linux, FSEvents на macOS), но его API низкоуровневый — рассмотрите библиотеки для сложных нужд.

java
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
import java.io.IOException;

// Watch a directory for changes
try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
    Path dir = Path.of("/var/log");
    dir.register(watcher,
        ENTRY_CREATE,
        ENTRY_DELETE,
        ENTRY_MODIFY);

    System.out.println("Watching " + dir + "...");
    while (true) {
        WatchKey key = watcher.take();  // blocks until event
        for (WatchEvent<?> event : key.pollEvents()) {
            Path changed = dir.resolve((Path) event.context());
            System.out.println(event.kind() + " -> " + changed);

            if (event.kind() == OVERFLOW) continue;  // events lost

            // React to change
            if (event.kind() == ENTRY_CREATE) {
                System.out.println("New file: " + changed);
            }
        }
        if (!key.reset()) break;  // key no longer valid (dir deleted)
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

// Note: WatchService watches directories, not individual files.
// To watch a file, watch its parent dir and filter by context().
// Events may be coalesced (rapid modifications may yield one event).
// For recursive watching, register each subdirectory manually
// or use a library like Apache Commons IO's FileAlterationMonitor.
17

Records и pattern matching

Основы Records

Records (Java 16+) — прозрачные неизменяемые носители данных. Заголовок `record Name(Type1 f1, Type2 f2)` генерирует конструктор, аксессоры (f1(), f2() — не getF1()), equals, hashCode и toString. Они идеальны для DTO, value-объектов и результатов функций. Compact-конструктор (просто `{ ... }`) валидирует или нормализует без переприсваивания полей. Records могут реализовывать интерфейсы, но не могут расширять классы.

java
// Record: concise immutable data carrier (Java 16+)
public record Point(int x, int y) {}

// Equivalent to writing:
// - final class with private final int x, y
// - constructor Point(int x, int y)
// - accessors x(), y()
// - equals, hashCode, toString (all auto-generated)

Point p = new Point(3, 4);
System.out.println(p.x());          // 3 (accessor, not getX())
System.out.println(p.y());          // 4
System.out.println(p);              // "Point[x=3, y=4]"
System.out.println(p.equals(new Point(3, 4)));  // true

// Immutable: no setters
// p.setX(5);  // no such method

// Custom record with validation
public record Age(int value) {
    public Age {
        if (value < 0 || value > 150) {
            throw new IllegalArgumentException("Invalid age: " + value);
        }
    }
}

// Record with multiple components
public record Employee(String name, int id, double salary, String dept) {}

// Records implement interfaces
public interface Named { String name(); }
public record Customer(String name, long id) implements Named {}

Compact-конструкторы и пользовательские методы

Compact-конструктор (`public Name { ... }`) запускается перед присваиванием полей — присваивайте параметру для нормализации, и компилятор присвоит его полю. Records могут иметь дополнительные методы и static-фабрики, но не instance-поля сверх заголовка. Non-canonical конструкторы должны делегировать canonical через this(...). Используйте static-фабрики для ясного конструирования (Point.origin()) и кэширования общих экземпляров.

java
// Compact constructor: validation without reassigning
public record Email(String address) {
    public Email {
        if (!address.contains("@")) {
            throw new IllegalArgumentException("Bad email: " + address);
        }
        address = address.toLowerCase().strip();  // normalize
    }
}

// Add custom methods (but not mutable fields)
public record Money(double amount, String currency) {
    public Money plus(Money other) {
        if (!currency.equals(other.currency)) {
            throw new IllegalArgumentException("Currency mismatch");
        }
        return new Money(amount + other.amount, currency);
    }
    public Money times(int factor) {
        return new Money(amount * factor, currency);
    }
    public static Money usd(double amt) { return new Money(amt, "USD"); }
}

// Canonical + non-canonical constructors
public record Range(int start, int end) {
    // Compact canonical (validation)
    public Range {
        if (start > end) throw new IllegalArgumentException();
    }
    // Non-canonical convenience constructor
    public Range(int end) {
        this(0, end);
    }
}

// Static factories are common on records
public record Point(int x, int y) {
    public static Point origin() { return new Point(0, 0); }
    public static Point of(int x, int y) { return new Point(x, y); }
}

Sealed-классы

Sealed-классы/интерфейсы (Java 17+) ограничивают, какие типы могут их расширять, через предложение `permits`. Каждый разрешённый подтип должен быть final, sealed или non-sealed. В сочетании с records они образуют алгебраические типы данных (закрытое наследование + неизменяемые данные). Ключевая выгода: компилятор знает все подтипы, поэтому switch-выражения могут быть исчерпывающими без default-ветки — при добавлении нового подтипа компилятор отмечает каждый switch, нуждающийся в обновлении.

java
// Sealed class: restricts which classes can extend it (Java 17+)
public sealed interface Shape
    permits Circle, Square, Triangle {}

public record Circle(double radius) implements Shape {}
public record Square(double side) implements Shape {}
public record Triangle(double base, double height) implements Shape {}

// Every permitted subtype must be final, sealed, or non-sealed
public non-sealed class WeirdShape implements Shape {}  // open again

// Why sealed? Enables exhaustive pattern matching
public double area(Shape s) {
    return switch (s) {
        case Circle c -> Math.PI * c.radius() * c.radius();
        case Square sq -> sq.side() * sq.side();
        case Triangle t -> 0.5 * t.base() * t.height();
        // No default needed — compiler knows all subtypes
    };
}

// Sealed class hierarchy with records = algebraic data types
// Combines: closed inheritance (sealed) + immutable data (record)

// Sealed classes also work with classes (not just interfaces)
public sealed abstract class Result permits Success, Failure {}
public final class Success<T>(T value) extends Result {}
public final class Failure(String error) extends Result {}

Pattern matching для instanceof

Pattern matching для instanceof (Java 16+) объявляет переменную, привязываемую только при успехе проверки, устраняя явное приведение. Область видимости переменной течёт из истинности паттерна — используется в &&-продолжениях и после ранних return. Java 21 добавила pattern matching в switch (case Type var when guard), обеспечивая type-based диспетчеризацию с guard'ами. Это делает type-checking-код намного лаконичнее и менее ошибочным.

java
// Old way: cast after instanceof
Object obj = "hello";
if (obj instanceof String) {
    String s = (String) obj;  // redundant cast
    System.out.println(s.length());
}

// Java 16+: pattern variable
if (obj instanceof String s) {
    System.out.println(s.length());  // s is in scope, no cast
}

// The variable is scoped by the pattern's truth
if (obj instanceof String s && s.length() > 3) {
    System.out.println("Long string: " + s);
}
// s is NOT in scope here if the && short-circuited

// Negation scoping
if (!(obj instanceof String s)) {
    // s NOT in scope here
    return;
}
// s IS in scope here (the instanceof must have been true)

// Combining with other patterns
Object data = 42;
String desc = switch (data) {
    case Integer i when i > 0 -> "positive int: " + i;
    case Integer i -> "non-positive int: " + i;
    case String s -> "string: " + s;
    case null -> "null";
    default -> "other";
};

Switch pattern matching (Java 21)

Switch pattern matching (Java 21, final) позволяет switch по типам, деконструировать records и добавлять guard'ы (when). В сочетании с sealed-типами компилятор проверяет исчерпываемость — default не нужен, если все подтипы покрыты. Record-паттерны (case Point(int x, int y)) деконструируют за один шаг. null case явный (без NPE). Это приближает Java к pattern matching ML/Scala для декларативного моделирования доменов.

java
// Java 21: switch pattern matching with type patterns and guards
sealed interface Event permits Login, Logout, Message, Error {}
record Login(String user) implements Event {}
record Logout(String user) implements Event {}
record Message(String from, String text) implements Event {}
record Error(int code, String detail) implements Event {}

String describe(Event e) {
    return switch (e) {
        case Login l -> l.user() + " logged in";
        case Logout l -> l.user() + " logged out";
        case Message m when m.from().equals("system") -> "[system] " + m.text();
        case Message m -> m.from() + ": " + m.text();
        case Error(int code, String detail) when code >= 500 -> "SERVER ERROR " + code;
        case Error(int code, String detail) -> "error " + code + ": " + detail;
        case null -> "no event";  // explicit null handling
    };
}

// Record patterns: destructure records in one go
record Point(int x, int y) {}
String classify(Object o) {
    return switch (o) {
        case Point(int x, int y) when x == y -> "diagonal";
        case Point(int x, int y) -> "point at (" + x + "," + y + ")";
        default -> "not a point";
    };
}

// Nested patterns
record Box(Point p) {}
String describe2(Object o) {
    return switch (o) {
        case Box(Point(int x, int y)) -> "box at " + x + "," + y;
        default -> "unknown";
    };
}
18

Модули (JPMS)

Основы module-info.java

module-info.java объявляет модуль (Java 9+ JPMS). requires добавляет зависимость; exports делает пакеты доступными; opens разрешает рефлективный доступ (нужно serialization/DI-фреймворкам); uses/provides подключают ServiceLoader. Без этого файла код живёт на classpath как 'unnamed module' с legacy-поведением. Модули дают сильную инкапсуляцию (только экспортируемые пакеты публичны) и надёжную конфигурацию (явные зависимости).

java
// File: src/com.example.app/module-info.java
module com.example.app {
    requires java.sql;           // depends on java.sql module
    requires transitive com.example.lib;  // re-export (consumers see it too)
    requires static java.annotation;  // compile-time only (optional at runtime)

    exports com.example.app.api;       // public API visible to all
    exports com.example.app.internal to com.example.test;  // qualified export

    opens com.example.app.model to com.fasterxml.jackson.databind;  // reflection only
    opens com.example.app.dynamic;  // open to all for reflection

    uses com.example.app.spi.Plugin;  // service consumer
    provides com.example.app.spi.Plugin with com.example.app.plugins.DefaultPlugin;  // service provider
}

// Key directives:
// requires: depends on another module
// exports: makes packages public to other modules
// opens: allows deep reflection (for frameworks like Jackson, Hibernate)
// uses/provides: service loader integration

// A module without module-info.java is an "unnamed module" (classpath behavior)

requires, exports, opens

requires объявляет зависимость; requires transitive распространяет её (используйте, когда публичный API раскрывает типы из этого модуля). exports делает пакеты публичными; exports to ограничивает именованными модулями (qualified exports). opens предоставляет рефлективный доступ (глубокий reflection) — существенен для фреймворков, вызывающих setAccessible(true). Различие между exports (публичный API) и opens (reflection) ключевое: сильная инкапсуляция по умолчанию, opt-in по пакетам.

java
// Module A: com.example.library
module com.example.library {
    // Public API anyone can use
    exports com.example.library.api;

    // Internal package: only visible to specific modules
    exports com.example.library.internal to com.example.app;

    // Allow reflection for frameworks (Jackson, JPA)
    opens com.example.library.model;

    // Only specific module can reflect
    opens com.example.library.config to com.example.app;

    // Dependencies
    requires java.logging;
    requires transitive java.sql;  // consumers of A also get java.sql
}

// Module B: com.example.app (consumer)
module com.example.app {
    requires com.example.library;  // use A's exported packages
    // Note: transitive means java.sql is also available here

    requires com.fasterxml.jackson.databind;
}

// 'requires transitive X' means: any module requiring this one
// also reads X. Use when your exported API exposes X's types.

// 'opens' vs 'exports':
// exports: compile-time + runtime access to public members
// opens: runtime reflective access to ALL members (including private)

// Reflective access without 'opens' fails with InaccessibleObjectException
// in Java 16+ (strong encapsulation enforced by default).

ServiceLoader и сервисы

ServiceLoader реализует SPI-паттерн: интерфейс в одном модуле, реализации обнаруживаются в runtime. API-модуль экспортирует интерфейс; модули-провайдеры объявляют `provides X with Y`; модули-потребители объявляют `uses X`. ServiceLoader.load(X.class) находит всех провайдеров на module path. Это развязывает интерфейс от реализации — JDBC-драйверы, logging-бэкенды (SLF4J) и Charset-провайдеры работают так. Без compile-time зависимости от реализации.

java
// SPI (Service Provider Interface) pattern with modules

// 1. Define the service interface in an API module
module com.example.spi {
    exports com.example.spi;
}
package com.example.spi;
public interface Plugin {
    String name();
    void run();
}

// 2. Implement in a provider module
module com.example.plugin.impl {
    requires com.example.spi;
    provides com.example.spi.Plugin with com.example.plugin.impl.DefaultPlugin;
}
package com.example.plugin.impl;
import com.example.spi.Plugin;
public class DefaultPlugin implements Plugin {
    public String name() { return "default"; }
    public void run() { System.out.println("running"); }
}

// 3. Consume in an app module
module com.example.app {
    requires com.example.spi;
    uses com.example.spi.Plugin;  // declares intent to load services
}

// Loading services at runtime
import java.util.ServiceLoader;
ServiceLoader<Plugin> loader = ServiceLoader.load(Plugin.class);
for (Plugin p : loader) {
    System.out.println("Found: " + p.name());
    p.run();
}

// ServiceLoader is how JDBC drivers, SLF4J backends, and many
// plugin systems are discovered without compile-time dependencies.

Module path vs classpath

Module path (--module-path) содержит модульные JAR с сильной инкапсуляцией; classpath (-cp) содержит legacy-JAR как unnamed-модули без инкапсуляции. Модульные JAR работают на обоих. Automatic modules — JAR без module-info, размещённые на module path — их имя берётся из имени файла или manifest-атрибута Automatic-Module-Name. --add-opens — escape-hatch для legacy-reflection, ломающегося под сильной инкапсуляцией.

java
// Compile a module
//   javac -d out/com.example.app \
//         --module-source-path src \
//         --module com.example.app

// Run a modular app
//   java --module-path out --module com.example.app/com.example.app.Main

// Package as a modular JAR (includes module-info.class)
//   jar --create --file app.jar --main-class com.example.app.Main -C out/com.example.app .

// Module path vs classpath:
// --module-path: modules with module-info, strong encapsulation enforced
// --class-path (or -cp): legacy "unnamed module", everything public, no encapsulation

// Mixing: modular JARs can be used on the classpath too (automatic module)
//   java -cp lib/app.jar:lib/dep.jar com.example.app.Main
// An automatic module: a JAR without module-info on the module path.
// Its name is derived from the JAR filename (Automatic-Module-Name in MANIFEST.MF
// gives an explicit name).

// Inspect a module JAR
//   jar --describe-module --file app.jar

// List observable modules
//   java --list-modules
//   java --describe-module java.sql

// Add opens at runtime for legacy reflection (escape hatch)
//   java --add-opens com.example.app/com.example.app.internal=ALL-UNNAMED

jdeps и jlink (кастомные runtime)

jdeps анализирует байткод для перечисления модульных зависимостей — полезно для миграции на модули и поиска неиспользуемых deps. jlink создаёт кастомный JRE, содержащий только модули, нужные вашему приложению, производя self-contained, меньший, быстрее запускающийся runtime. Это идеально для Docker-образов и установщиков: поставляйте приложение плюс 30-50MB JRE вместо требования 300MB JDK. Вместе jdeps + jlink обеспечивают lean, self-contained Java-развёртывания.

java
# jdeps: analyze dependencies (find unused, list required modules)

# List dependencies of a JAR
jdeps --module-path lib app.jar

# Generate module-info.java for an existing JAR
jdeps --generate-module-info ./out app.jar

# Show which JDK modules a JAR uses
jdeps --print-module-deps --ignore-missing-deps app.jar
# Output: java.base,java.logging,java.sql

# jlink: create a custom stripped-down JRE containing only needed modules
jlink \
  --module-path "$JAVA_HOME/jmods:./out" \
  --add-modules com.example.app \
  --output ./custom-jre \
  --strip-debug \
  --compress=zip-6 \
  --no-header-files \
  --no-man-pages \
  --launcher app=com.example.app/com.example.app.Main

# The custom JRE is self-contained:
#   ./custom-jre/bin/app   # launches the app
#   ./custom-jre/bin/java  # the stripped JVM

# Benefits of jlink:
# - Smaller distribution (only needed modules)
# - Faster startup (less to load)
# - No need to install Java on target machine
# - Can cross-target (different OS/arch) with matching jmods

# Common workflow:
# 1. jdeps to find required modules
# 2. jlink to build a custom runtime
# 3. Package app + custom JRE together (Docker image, installer)
19

Углублённое изучение конкурентности

Locks: ReentrantLock и ReadWriteLock

ReentrantLock предлагает больше контроля, чем synchronized: tryLock (non-blocking/timed), fairness, interruptibility и инспекция состояния блокировки. Всегда unlock в finally. ReadWriteLock разрешает много одновременных читателей, но эксклюзивных писателей — отлично для read-heavy кэшей. StampedLock (Java 8+) добавляет optimistic reads для ещё лучшей пропускной способности чтения. Предпочитайте synchronized для простых случаев; Lock — когда нужны его продвинутые функции.

java
import java.util.concurrent.locks.*;

// ReentrantLock: more flexible than synchronized
class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();  // MUST be in finally
        }
    }

    public int get() {
        lock.lock();
        try { return count; }
        finally { lock.unlock(); }
    }
}

// tryLock with timeout (avoids deadlock-induced hangs)
if (lock.tryLock(1, java.util.concurrent.TimeUnit.SECONDS)) {
    try { /* work */ } finally { lock.unlock(); }
}

// Fair lock (FIFO ordering, slower)
ReentrantLock fair = new ReentrantLock(true);

// ReadWriteLock: many readers OR one writer
class Cache {
    private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    private final Lock read = rw.readLock();
    private final Lock write = rw.writeLock();
    private java.util.Map<String, String> data = new java.util.HashMap<>();

    public String get(String key) {
        read.lock();
        try { return data.get(key); }
        finally { read.unlock(); }
    }
    public void put(String key, String val) {
        write.lock();
        try { data.put(key, val); }
        finally { write.unlock(); }
    }
}

Concurrent-коллекции

ConcurrentHashMap — рабочая лошадка потокобезопасных map — используйте compute/merge для атомарных обновлений вместо check-then-act. CopyOnWriteArrayList лучше всего для read-heavy, write-rare списков (event listeners) — записи копируют массив. BlockingQueue — основа producer-consumer конвейеров (put блокируется при полном, take при пустом). ConcurrentLinkedQueue — non-blocking и unbounded. Они заменяют synchronized-обёртки (Collections.synchronizedX), которые coarse-grained и медленнее.

java
import java.util.concurrent.*;
import java.util.*;

// ConcurrentHashMap: thread-safe HashMap (no null keys/values)
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
map.putIfAbsent("a", 2);  // only if absent
map.compute("a", (k, v) -> v == null ? 1 : v + 1);  // atomic update
map.merge("a", 1, Integer::sum);  // add 1 atomically
Integer val = map.getOrDefault("a", 0);

// CopyOnWriteArrayList: snapshot semantics, fast reads, slow writes
CopyOnWriteArrayList<String> listeners = new CopyOnWriteArrayList<>();
listeners.add("x");  // copies entire array
for (String l : listeners) { /* safe iteration, no ConcurrentModificationException */ }

// BlockingQueue: producer-consumer pattern
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
// Producer
queue.put("task");        // blocks if full
boolean added = queue.offer("task", 1, TimeUnit.SECONDS);  // timed
// Consumer
String task = queue.take();  // blocks if empty
String polled = queue.poll(1, TimeUnit.SECONDS);

// ConcurrentLinkedQueue: non-blocking, unbounded
Queue<String> q = new ConcurrentLinkedQueue<>();
q.offer("a");

// SkipListMap / SkipListSet: concurrent sorted collections
ConcurrentNavigableMap<Integer, String> sorted = new ConcurrentSkipListMap<>();

import java.util.concurrent.TimeUnit;

CountDownLatch и CyclicBarrier

CountDownLatch — одноразовый гейт — N потоков отсчитывают вниз, другие ждут; не сбрасывается. CyclicBarrier переиспользуемый — потоки ждут друг друга в точке рандеву, с опциональным действием при прибытии всех. Phaser самый гибкий: переменные стороны, несколько фаз, древовидная структура. Используйте latch для координации запуска, barrier для параллельных многофазных алгоритмов, phaser для динамического количества участников.

java
import java.util.concurrent.*;
import java.util.*;

// CountDownLatch: one-shot gate, N threads must arrive before proceeding
CountDownLatch ready = new CountDownLatch(3);
List<String> results = Collections.synchronizedList(new ArrayList<>());

for (int i = 0; i < 3; i++) {
    final int id = i;
    new Thread(() -> {
        try { Thread.sleep(id * 100); } catch (InterruptedException e) {}
        results.add("worker-" + id);
        ready.countDown();  // signal done
    }).start();
}
ready.await();  // main thread blocks until count reaches 0
System.out.println("All done: " + results);

// CyclicBarrier: reusable barrier, threads wait for each other
CyclicBarrier barrier = new CyclicBarrier(3, () ->
    System.out.println("--- phase complete ---"));

Runnable worker = () -> {
    try {
        System.out.println(Thread.currentThread().getName() + " phase 1");
        barrier.await();  // wait for all 3
        System.out.println(Thread.currentThread().getName() + " phase 2");
        barrier.await();  // reusable for next phase
    } catch (Exception e) {}
};
for (int i = 0; i < 3; i++) new Thread(worker).start();

// Phaser: more flexible (variable parties, multiple phases)
Phaser phaser = new Phaser(3);
phaser.register();  // dynamically add a party
phaser.arriveAndAwaitAdvance();

Semaphore и Exchanger

Semaphore контролирует доступ к N разрешениям — acquire блокируется до доступности, release возвращает. Используйте для rate limiting, пулов соединений или любого сценария ограниченных ресурсов. tryAcquire предлагает non-blocking и timed варианты. Exchanger позволяет двум потокам обменяться значениями в точке рандеву — нишево, но элегантно для pipeline-дизайнов, где два потока торгуются буферами. Оба в java.util.concurrent и ниже по уровню, чем locks для некоторых паттернов координации.

java
import java.util.concurrent.*;

// Semaphore: limit concurrent access to N permits
Semaphore pool = new Semaphore(5);  // 5 concurrent allowed

pool.acquire();  // blocks until a permit is available
try {
    // critical section (at most 5 threads here at once)
    System.out.println("Working, permits left: " + pool.availablePermits());
} finally {
    pool.release();  // return the permit
}

// Try-acquire (non-blocking)
if (pool.tryAcquire()) {
    try { /* work */ } finally { pool.release(); }
} else {
    System.out.println("Too busy, try later");
}

// Timed acquire
if (pool.tryAcquire(1, TimeUnit.SECONDS)) {
    try { /* work */ } finally { pool.release(); }
}

// Use case: rate limiting, connection pools, parking lots

// Exchanger: two threads swap values
Exchanger<String> ex = new Exchanger<>();
new Thread(() -> {
    try {
        String got = ex.exchange("from-A");  // gives "from-A", receives "from-B"
        System.out.println("A got: " + got);
    } catch (InterruptedException e) {}
}).start();
String got = ex.exchange("from-B");  // gives "from-B", receives "from-A"
System.out.println("B got: " + got);

import java.util.concurrent.TimeUnit;

CompletableFuture Advanced

CompletableFuture — Promise Java — компонуйте async-работу с thenApply (map), thenCompose (flatMap), thenCombine (zip two). allOf ждёт все, anyOf — первого. exceptionally восстанавливается от ошибок; handle покрывает оба. orTimeout (Java 9+) отменяет, если слишком долго. Всегда передавайте явный executor — дефолтный commonPool может голодать под блокирующей работой. Это основа reactive-style async-кода в Java.

java
import java.util.concurrent.*;
import java.util.*;

ExecutorService pool = Executors.newFixedThreadPool(4);

// Async composition (like JS Promises)
CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> "hello", pool)            // async supplier
    .thenApply(String::toUpperCase)              // transform
    .thenCompose(s -> CompletableFuture.supplyAsync(  // flatMap
        () -> s + " world"))
    .thenApply(s -> s + "!");

System.out.println(future.join());  // "HELLO world!"

// Combine two independent futures
CompletableFuture<Integer> a = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> b = CompletableFuture.supplyAsync(() -> 20);
CompletableFuture<Integer> sum = a.thenCombine(b, Integer::sum);
System.out.println(sum.join());  // 30

// Run multiple and wait for all (or any)
List<CompletableFuture<Integer>> futures = List.of(
    CompletableFuture.supplyAsync(() -> 1, pool),
    CompletableFuture.supplyAsync(() -> 2, pool),
    CompletableFuture.supplyAsync(() -> 3, pool)
);
CompletableFuture<Void> all = CompletableFuture.allOf(
    futures.toArray(new CompletableFuture[0]));
all.join();
List<Integer> results = futures.stream().map(CompletableFuture::join).toList();

// Error handling
CompletableFuture<String> safe = CompletableFuture
    .supplyAsync(() -> { throw new RuntimeException("boom"); })
    .exceptionally(ex -> "fallback: " + ex.getMessage());
System.out.println(safe.join());  // "fallback: boom"

// handle: both success and failure
CompletableFuture<String> handled = CompletableFuture
    .supplyAsync(() -> "ok")
    .handle((val, ex) -> ex == null ? val : "error");

// Timeout (Java 9+)
CompletableFuture<String> timed = CompletableFuture
    .supplyAsync(() -> { Thread.sleep(5000); return "slow"; })
    .orTimeout(1, TimeUnit.SECONDS)
    .exceptionally(ex -> "timed out");

Виртуальные потоки (Java 21)

Виртуальные потоки (Java 21) — легковесные потоки, планируемые JVM на маленьком пуле carrier-потоков (платформенных). Когда виртуальный поток блокируется на I/O, он приостанавливается и carrier запускает другой — поэтому можно иметь миллионы одновременных блокирующих операций. Это позволяет писать простой блокирующий код вместо сложных reactive/async-цепочек. Используйте для I/O-bound workloads (HTTP-обработчики, DB-вызовы); для CPU-bound работы платформенные потоки или parallelStream всё ещё подходят.

java
import java.util.concurrent.*;
import java.util.*;

// Virtual threads: lightweight, cheap, millions possible
// Java 21 LTS feature — "Project Loom"

// Start a virtual thread
Thread vt = Thread.ofVirtual().start(() -> {
    System.out.println("Running on: " + Thread);
});

// Builder pattern
Thread vt2 = Thread.ofVirtual().name("worker-1").start(() -> {
    // blocking I/O here is fine — virtual thread yields, not the OS thread
});

// Per-thread factory
ThreadFactory factory = Thread.ofVirtual().factory();

// ExecutorService for virtual threads (Java 21+)
try (ExecutorService es = Executors.newVirtualThreadPerTaskExecutor()) {
    // Submit a million tasks — each gets its own virtual thread
    List<Future<String>> futures = new ArrayList<>();
    for (int i = 0; i < 1_000_000; i++) {
        final int id = i;
        futures.add(es.submit(() -> {
            Thread.sleep(100);  // blocking call is cheap on virtual threads
            return "done-" + id;
        }));
    }
    // virtual threads yield when blocking, so 1M concurrent is feasible
}

// Why virtual threads?
// - Platform threads (OS threads) are heavy (~1MB stack, kernel scheduling)
// - Virtual threads are user-mode, ~KB, scheduled by JVM on a small carrier pool
// - Blocking I/O on a virtual thread doesn't block a platform thread
// - Lets you write straightforward blocking code at scale (no reactive complexity)

// Best practice: use virtual threads for I/O-bound work,
// NOT for CPU-bound work (use platform threads / parallelStream).
20

Углублённое изучение Collections Framework

Comparator'ы и сортировка

Comparator.comparing(keyExtractor) строит comparator'ы из ключевой функции — намного чище, чем писать raw compare-логику. thenComparing цепочит вторичные ключи сортировки. nullsFirst/nullsLast безопасно обрабатывают null. Используйте comparingInt/comparingLong/comparingDouble для избежания autoboxing. List.sort() сортирует на месте (только изменяемые списки); Stream.sorted() возвращает новый отсортированный stream. Comparator'ы питают сортировку, порядок TreeSet/TreeMap и stream-операции.

java
import java.util.*;
import java.util.stream.*;

record Person(String name, int age) {}

List<Person> people = List.of(
    new Person("Alice", 30),
    new Person("Bob", 25),
    new Person("Carol", 30),
    new Person("Dave", 25)
);

// Comparator.comparing (key extractor)
people.stream().sorted(Comparator.comparing(Person::name)).toList();
people.stream().sorted(Comparator.comparingInt(Person::age)).toList();

// Reversed
people.stream().sorted(Comparator.comparing(Person::age).reversed()).toList();

// Chained (thenComparing): sort by age, then by name
List<Person> sorted = people.stream().sorted(
    Comparator.comparingInt(Person::age)
              .thenComparing(Person::name)
).toList();
// [Bob(25), Dave(25), Alice(30), Carol(30)]

// Nulls handling
Comparator<String> cmp = Comparator.nullsFirst(Comparator.naturalOrder());
List.of("b", null, "a").stream().sorted(cmp).toList();  // [null, a, b]

// Custom comparator
Comparator<Person> byNameLen = (a, b) -> a.name().length() - b.name().length();

// Mutable list sort
List<String> names = new ArrayList<>(List.of("charlie", "alice", "bob"));
names.sort(Comparator.naturalOrder());
// names = [alice, bob, charlie]

// Comparing with primitive specializations avoids boxing
Comparator<Person> byAge = Comparator.comparingInt(Person::age);

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

List.of/Set.of/Map.of (Java 9+) создают真正 неизменяемые коллекции — без null, без мутаций. Collections.unmodifiableX создаёт read-only view, который всё ещё отражает изменения backing-коллекции. List.copyOf (Java 10+) делает независимую неизменяемую копию. Arrays.asList — fixed-size view массива (set работает, add/remove — нет). Выбирайте по необходимости: immutable-фабрики для констант, unmodifiable views для безопасного раскрытия internals, copyOf для защитных копий.

java
import java.util.*;

// Java 9+ immutable factories (List.of, Set.of, Map.of)
List<String> immutable = List.of("a", "b", "c");
Set<Integer> set = Set.of(1, 2, 3);
Map<String, Integer> map = Map.of("a", 1, "b", 2);
Map<String, Integer> bigMap = Map.ofEntries(
    Map.entry("x", 1), Map.entry("y", 2), Map.entry("z", 3)
);
// immutable.add("d");  // throws UnsupportedOperationException
// immutable.set(0, "z");  // throws
// nulls not allowed in these immutable collections

// Unmodifiable view (wraps an existing collection)
List<String> mutable = new ArrayList<>(List.of("a", "b"));
List<String> view = Collections.unmodifiableList(mutable);
// view.add("c");  // throws
mutable.add("c");  // but changes to backing list ARE visible in view
System.out.println(view);  // [a, b, c]

// CopyOf (Java 10+): creates immutable copy
List<String> copy = List.copyOf(mutable);  // independent immutable copy

// Arrays.asList: fixed-size view of an array
String[] arr = {"a", "b"};
List<String> fixed = Arrays.asList(arr);
fixed.set(0, "x");  // OK (writes through to array)
// fixed.add("c");  // throws (size fixed)

// To make a truly mutable copy:
List<String> mut = new ArrayList<>(Arrays.asList(arr));

Реализации Queue и Deque

ArrayDeque — предпочтительная реализация стека и очереди — быстрее устаревшего Stack (который synchronized) и LinkedList. PriorityQueue упорядочивает элементы по Comparator (min-heap по умолчанию) — используйте для планирования, top-K проблем. Deque поддерживает оба конца; используйте addFirst/removeFirst для семантики стека, addLast/removeFirst для семантики очереди. Для concurrent-очередей используйте реализации java.util.concurrent (LinkedBlockingQueue и т.д.).

java
import java.util.*;

// Queue: FIFO (offer/poll/peek)
Queue<String> queue = new LinkedList<>();
queue.offer("a"); queue.offer("b");
System.out.println(queue.peek());  // "a" (head)
System.out.println(queue.poll());  // "a" (remove head)

// Deque: double-ended (add/remove at both ends)
Deque<String> deque = new ArrayDeque<>();
deque.addFirst("a"); deque.addLast("b");
System.out.println(deque.peekFirst());  // "a"
System.out.println(deque.peekLast());   // "b"
deque.removeFirst(); deque.removeLast();

// ArrayDeque as a stack (push/pop/peek)
Deque<String> stack = new ArrayDeque<>();
stack.push("first");  // addFirst
stack.push("second");
System.out.println(stack.pop());  // "second" (LIFO)
System.out.println(stack.peek()); // "first"

// PriorityQueue: orders by Comparator (not insertion order)
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5); pq.offer(1); pq.offer(3);
System.out.println(pq.poll());  // 1 (smallest first)
System.out.println(pq.poll());  // 3

// With custom comparator
PriorityQueue<String> byLen = new PriorityQueue<>(Comparator.comparingInt(String::length));
byLen.offer("aaa"); byLen.offer("a"); byLen.offer("aa");
System.out.println(byLen.poll());  // "a"

// BlockingQueue implementations (concurrency): see concurrency-deep section
// ArrayDeque is faster than Stack/LinkedList for stack/queue use

Map merge, compute, getOrDefault

Эти методы Map делают распространённые паттерны атомарными и лаконичными. merge — идиома подсчёта слов — комбинирует существующее и новое значение, удаляя запись, если функция возвращает null. computeIfAbsent — паттерн lazy-cache (мемоизация). getOrDefault избегает проверок на null. replaceAll преобразует все значения. Это намного чище, чем check-then-act get/put-танец, и это строительные блоки для атомарных обновлений ConcurrentHashMap в concurrent-коде.

java
import java.util.*;

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

// getOrDefault: safe read with default
int n = counts.getOrDefault("missing", 0);  // 0, no null

// putIfAbsent: only set if not present
counts.putIfAbsent("a", 1);  // sets to 1
counts.putIfAbsent("a", 2);  // no change (already present)

// compute: recompute value for a key
counts.compute("a", (k, v) -> v == null ? 1 : v + 1);  // increment

// computeIfAbsent: lazy initialization (cache pattern)
Map<String, List<String>> groups = new HashMap<>();
groups.computeIfAbsent("key", k -> new ArrayList<>()).add("item");
// creates list if absent, then adds — atomic, no race in single thread

// computeIfPresent: update only if present
counts.computeIfPresent("a", (k, v) -> v > 0 ? v - 1 : null);  // decrement, remove at 0

// merge: combine existing and new value (great for counting)
counts.merge("word", 1, Integer::sum);  // word count pattern
counts.merge("word", 1, Integer::sum);  // now 2
counts.merge("word", 1, Integer::sum);  // now 3

// merge with removal: function returns null -> entry removed
counts.merge("word", 1, (old, v) -> old > 1 ? old - 1 : null);

// replaceAll: transform all values
Map<String, Integer> doubled = new HashMap<>(counts);
doubled.replaceAll((k, v) -> v * 2);

// Word frequency counter (idiomatic)
String text = "the cat the dog the bird";
Map<String, Integer> freq = new HashMap<>();
for (String w : text.split(" ")) {
    freq.merge(w, 1, Integer::sum);
}

Utility-методы Collections

Collections.* предоставляет классические утилиты: sort, binarySearch (требует отсортированный вход), shuffle, reverse, frequency, min/max. singleton/empty-фабрики возвращают неизменяемые single-element или пустые коллекции — предпочитайте emptyList() возврату null. nCopies memory-efficient (один элемент разделяется). Synchronized-обёртки существуют для legacy-кода, но предпочитайте коллекции java.util.concurrent. Checked-обёртки отлавливают generic-нарушения типов в runtime, полезно для interop с raw типами.

java
import java.util.*;
import java.util.stream.*;

List<Integer> nums = new ArrayList<>(List.of(3, 1, 4, 1, 5, 9, 2, 6));

// Sorting
Collections.sort(nums);                    // in-place, natural order
Collections.sort(nums, Comparator.reverseOrder());
nums.sort(Comparator.naturalOrder());      // List.sort (preferred)

// Searching (list must be sorted first)
int idx = Collections.binarySearch(nums, 4);  // index, or negative if absent

// Shuffling & reversing
Collections.shuffle(nums);
Collections.reverse(nums);

// Frequency & disjoint
int freq = Collections.frequency(nums, 1);  // count occurrences
boolean dis = Collections.disjoint(List.of(1, 2), List.of(3, 4));  // true

// Min/max
int min = Collections.min(nums);
int max = Collections.max(nums, Comparator.reverseOrder());

// Singleton collections (immutable, single element)
Set<String> one = Collections.singleton("only");
List<Integer> oneList = Collections.singletonList(42);
Map<String, Integer> oneMap = Collections.singletonMap("k", 1);

// Empty collections (prefer over returning null)
List<Object> empty = Collections.emptyList();
Set<Object> emptySet = Collections.emptySet();

// nCopies (immutable list of n copies)
List<String> padding = Collections.nCopies(5, "x");  // [x,x,x,x,x]

// Synchronized wrappers (legacy — prefer concurrent collections)
List<String> sync = Collections.synchronizedList(new ArrayList<>());

// Checked wrappers (catch heap pollution at runtime)
List<String> checked = Collections.checkedList(new ArrayList<>(), String.class);
// checked.add(123);  // throws ClassCastException at the add site
21

Тестирование (JUnit 5 и Mockito)

Основы JUnit 5

Аннотации JUnit 5 (Jupiter): @Test помечает тест; @BeforeEach/@AfterEach запускаются вокруг каждого теста; @BeforeAll/@AfterAll запускаются один раз для класса (должны быть static). @DisplayName кастомизирует имена тестов. @Disabled пропускает тесты. assertThrows проверяет исключения. Тесты должны быть независимыми — используйте @BeforeEach для сброса состояния, не static-поля. JUnit 5 живёт в org.junit.jupiter.api (отличается от org.junit JUnit 4).

java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {
    private Calculator calc;

    @BeforeAll
    static void setUpOnce() {
        // runs once before all tests (must be static)
        System.out.println("Starting CalculatorTest");
    }

    @BeforeEach
    void setUp() {
        // runs before each test
        calc = new Calculator();
    }

    @Test
    @DisplayName("2 + 2 should equal 4")
    void addsTwoNumbers() {
        assertEquals(4, calc.add(2, 2));
    }

    @Test
    void dividesByZeroThrows() {
        ArithmeticException ex = assertThrows(
            ArithmeticException.class,
            () -> calc.divide(1, 0)
        );
        assertEquals("Division by zero", ex.getMessage());
    }

    @AfterEach
    void tearDown() {
        // runs after each test (cleanup)
        calc = null;
    }

    @AfterAll
    static void tearDownOnce() {
        // runs once after all tests
    }

    @Disabled("until bug #42 is fixed")
    @Test
    void skippedTest() {}
}

class Calculator {
    int add(int a, int b) { return a + b; }
    int divide(int a, int b) {
        if (b == 0) throw new ArithmeticException("Division by zero");
        return a / b;
    }
}

Утверждения

Утверждения JUnit 5: assertEquals/assertNotEquals, assertTrue/False, assertNull/NotNull, assertSame (identity). assertAll группирует проверки, поэтому все запускаются, даже если некоторые падают. assertTimeout фейлит медленные тесты. Сообщения могут быть строками или Supplier'ами (ленивые — строятся только при неудаче, избегая конкатенации строк при проходе тестов). assertIterableEquals сравнивает упорядоченные коллекции. Они из org.junit.jupiter.api.Assertions.

java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class AssertionsTest {

    @Test
    void equality() {
        assertEquals(4, 2 + 2);
        assertEquals(4, 2 + 2, "math is broken");
        assertNotEquals(5, 2 + 2);
    }

    @Test
    void booleans() {
        assertTrue(5 > 3);
        assertFalse(5 < 3, "5 should not be less than 3");
    }

    @Test
    void nullness() {
        assertNull(null);
        assertNotNull(new Object());
    }

    @Test
    void sameInstance() {
        String a = "x";
        assertSame(a, a);          // ==
        assertNotSame(a, new String("x"));
    }

    @Test
    void collections() {
        assertIterableEquals(List.of(1, 2, 3), List.of(1, 2, 3));
        assertLinesMatch(List.of("a.*", "b"), List.of("abc", "b"));
    }

    @Test
    void groupedAssertions() {
        // All run even if one fails — reports all failures
        assertAll("person",
            () -> assertEquals("Alice", "Alice"),
            () -> assertEquals(30, 30),
            () -> assertNotNull("x")
        );
    }

    @Test
    void timeout() {
        // Fails if it takes longer than 100ms
        assertTimeout(Duration.ofMillis(100), () -> {
            Thread.sleep(10);
        });
        // assertTimeoutPreemptively: stops the task early (in another thread)
    }

    @Test
    void customMessage() {
        int result = 5;
        assertEquals(4, result, () -> "expected 4 but got " + result);
        // Supplier<String> — message built lazily only on failure
    }

    import java.time.Duration;
    import java.util.List;
}

Параметризованные тесты

Параметризованные тесты запускают одну логику теста с несколькими входами. @ValueSource предоставляет single-arg массив. @CsvSource отображает CSV-строки в несколько параметров. @MethodSource (самый гибкий) использует static Stream<Arguments>. @EnumSource итерирует значения enum. @NullAndEmptySource добавляет null/empty случаи. Это устраняет copy-paste тестовые методы и делает data-driven тестирование чистым. Display names авто-включают параметры для лёгкой диагностики.

java
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;

class ParameterizedTests {

    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3, 4, 5})
    void positiveNumbersArePositive(int n) {
        assertTrue(n > 0);
    }

    @ParameterizedTest
    @ValueSource(strings = {"", "  ", "\t"})
    void blankStrings(String s) {
        assertTrue(s.isBlank());
    }

    @ParameterizedTest
    @NullAndEmptySource
    @ValueSource(strings = {"  ", "\t"})
    void blankOrNull(String s) {
        assertTrue(s == null || s.isBlank());
    }

    @ParameterizedTest
    @CsvSource({
        "1, 1, 2",
        "2, 3, 5",
        "10, -5, 5"
    })
    void addition(int a, int b, int expected) {
        assertEquals(expected, a + b);
    }

    @ParameterizedTest
    @CsvFileSource(resources = "/testdata.csv", numLinesToSkip = 1)
    void fromCsv(int a, int b, int expected) {
        assertEquals(expected, a + b);
    }

    @ParameterizedTest
    @MethodSource("additionProvider")
    void fromMethod(int a, int b, int expected) {
        assertEquals(expected, a + b);
    }
    static java.util.stream.Stream<Arguments> additionProvider() {
        return java.util.stream.Stream.of(
            Arguments.of(1, 1, 2),
            Arguments.of(2, 3, 5)
        );
    }

    @ParameterizedTest
    @EnumSource(TimeUnit.class)
    void allEnums(TimeUnit unit) {
        assertNotNull(unit);
    }
}

Lifecycle, Nested и Conditional

@Nested создаёт внутренние тестовые классы, разделяющие lifecycle — отлично для BDD-стиля 'when X then Y' структур, где внешняя setup применяется к внутренним тестам. Conditional-аннотации (@EnabledOnOs, @EnabledIfSystemProperty, @EnabledIfEnvironmentVariable) пропускают тесты по окружению. @Tag группирует тесты для селективного выполнения (например, fast vs slow, unit vs integration). Nested-классы не могут иметь @BeforeAll (они non-static). Эти функции делают организацию тестов выразительной.

java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.condition.*;

class LifecycleTest {

    @Test
    void topLevel() {}

    @Nested
    @DisplayName("when stack is empty")
    class WhenEmpty {
        @BeforeEach
        void setUp() { /* stack is empty here */ }

        @Test
        void throwsOnPop() {
            assertThrows(Exception.class, () -> {});
        }

        @Nested
        @DisplayName("after pushing one element")
        class AfterPush {
            @BeforeEach
            void push() { /* push one */ }

            @Test
            void popReturnsElement() {
                // ...
            }
        }
    }

    // Conditional execution
    @Test
    @EnabledOnOs(OS.LINUX)
    void onlyOnLinux() {}

    @Test
    @EnabledIfSystemProperty(named = "env", matches = "ci")
    void onlyInCi() {}

    @Test
    @EnabledIfEnvironmentVariable(named = "DATABASE", matches = "postgres")
    void onlyWithPostgres() {}

    @Test
    @DisabledIf("customCondition")
    void conditional() {}
    static boolean customCondition() { return java.time.LocalTime.now().getHour() < 9; }

    // Tagging for selective runs
    @Test
    @Tag("slow")
    void slowIntegrationTest() {}

    @Test
    @Tag("fast")
    void fastUnitTest() {}

    // Run only fast: mvn test -Dgroups=fast
    import org.junit.jupiter.api.condition.OS;
    import java.util.concurrent.TimeUnit;

Мокирование Mockito

Mockito создаёт test doubles для зависимостей. @Mock создаёт мок; @InjectMocks строит реальный объект с внедрёнными моками. when(...).thenReturn(...) стабит возвращаемые значения; verify(...) проверяет взаимодействия. Argument matchers (any(), eq(), argThat()) гибко сопоставляют вызовы. Spies оборачивают реальные объекты (partial mocking). Паттерн Arrange-Act-Assert держит тесты читаемыми. Мокирование изолирует тестируемый модуль от его зависимостей (БД, сеть, время).

java
import org.junit.jupiter.api.*;
import org.mockito.*;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

class UserServiceTest {

    @Mock
    UserRepository repo;  // mock dependency

    @InjectMocks
    UserService service;  // real service with mocks injected

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void returnsUserWhenFound() {
        // Arrange: stub the mock
        User alice = new User(1, "Alice");
        when(repo.findById(1)).thenReturn(alice);

        // Act
        String name = service.getUserName(1);

        // Assert
        assertEquals("Alice", name);
        verify(repo).findById(1);       // called once
        verify(repo, never()).findById(2);
    }

    @Test
    void throwsWhenNotFound() {
        when(repo.findById(99)).thenReturn(null);
        // or: when(repo.findById(99)).thenThrow(new RuntimeException());

        assertThrows(RuntimeException.class, () -> service.getUserName(99));
    }

    @Test
    void argumentMatchers() {
        when(repo.findById(anyInt())).thenReturn(new User(0, "default"));
        // matchers: eq(), any(), anyInt(), contains(), argThat()

        service.getUserName(42);
        verify(repo).findById(intThat(n -> n > 0));
    }

    @Test
    void verifyInteractionDetails() {
        service.getUserName(1);
        verify(repo, times(1)).findById(1);
        verify(repo, atLeastOnce()).findById(anyInt());
        verifyNoMoreInteractions(repo);
    }

    @Test
    void spy_partialMock() {
        List<String> spy = spy(new ArrayList<>());
        spy.add("real");
        when(spy.size()).thenReturn(100);  // stub one method
        assertEquals(100, spy.size());     // stubbed
        assertEquals(1, spy.size());       // wait, this would be 100 too
    }
}

interface UserRepository { User findById(int id); }
record User(int id, String name) {}
class UserService {
    UserRepository repo;
    UserService(UserRepository r) { repo = r; }
    String getUserName(int id) {
        User u = repo.findById(id);
        if (u == null) throw new RuntimeException("not found");
        return u.name();
    }
}
22

Тестирование JUnit

Базовый тест

JUnit 5 использует @Test из org.junit.jupiter.api. assertEquals проверяет, что expected равно actual. Другие утверждения: assertTrue, assertThrows, assertAll.

java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalcTest {
    @Test
    void testAdd() {
        assertEquals(5, calc.add(2, 3));
    }
}

Параметризованные тесты

Параметризованные тесты запускают один и тот же тест с разными входными данными. @ValueSource предоставляет одиночные аргументы. @CsvSource предоставляет несколько аргументов. Снижает дублирование тестов.

java
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4})
void testPositive(int n) { assertTrue(n > 0); }

@ParameterizedTest
@CsvSource({"1,2,3", "4,5,9"})
void testAdd(int a, int b, int expected) {
    assertEquals(expected, calc.add(a, b));
}

Методы жизненного цикла

@BeforeAll/@AfterAll выполняются один раз для класса (должны быть static). @BeforeEach/@AfterEach выполняются вокруг каждого теста. Используйте для подключения к базе данных и настройки моков.

java
class DbTest {
    @BeforeAll static void setupAll() { /* once before all */ }
    @AfterAll static void tearDownAll() { /* once after all */ }
    @BeforeEach void setup() { /* before each test */ }
    @AfterEach void tearDown() { /* after each test */ }
}

Утверждения

assertAll запускает все утверждения, даже если некоторые завершаются неудачей. assertThrows проверяет, что код выбрасывает определённое исключение. Используйте assertTimeout для тестов с ограничением по времени.

java
@Test
void testAll() {
    assertAll("person",
        () -> assertEquals("Alice", p.getName()),
        () -> assertEquals(30, p.getAge())
    );
}
@Test
void testException() {
    assertThrows(IllegalArgumentException.class, () -> service.process(-1));
}

Mockito

@Mock создаёт мок-объекты, @InjectMocks внедряет их. when().thenReturn() заглушает вызовы. verify() проверяет, что метод был вызван. Mockito — стандартный фреймворк для создания моков.

java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock UserRepository repo;
    @InjectMocks UserService service;
    @Test
    void testFind() {
        when(repo.findById(1)).thenReturn(Optional.of(new User("Alice")));
        assertEquals("Alice", service.findUser(1).getName());
        verify(repo).findById(1);
    }
}
23

Maven/Gradle

Maven POM

Maven использует pom.xml. groupId/artifactId/version идентифицируют проект. Зависимости имеют scope (compile, test, provided). Maven требует стандартную структуру каталогов.

java
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>myapp</artifactId>
  <version>1.0.0</version>
  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.10.0</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Команды Maven

Жизненный цикл Maven: clean, compile, test, package, install, deploy. Каждая фаза запускает предшествующие фазы. Используйте -DskipTests для пропуска тестов.

java
mvn clean          # Remove target/
mvn compile        # Compile sources
mvn test           # Run tests
mvn package        # Build JAR
mvn install        # Install to local repo
mvn dependency:tree  # Show dependency tree

Сборка Gradle

Gradle использует build.gradle (Groovy) или build.gradle.kts (Kotlin). implementation для зависимостей компиляции, testImplementation для тестов. Gradle быстрее, чем Maven.

java
plugins { id 'java'; id 'application' }
repositories { mavenCentral() }
dependencies {
    implementation 'com.google.guava:guava:32.1.3-jre'
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}
application { mainClass = 'com.example.App' }

Команды Gradle

Задачи Gradle: build, test, run, clean. Обёртка (gradlew) обеспечивает согласованные версии. Используйте --parallel для параллельной сборки модулей.

java
gradle clean       # Clean build
gradle build       # Build + test
gradle test        # Run tests
gradle run         # Run application
gradle bootRun     # Run Spring Boot
gradle dependencies  # Show dependency tree

Многомодульный проект

Многомодульные проекты разделяют большие приложения. settings.gradle перечисляет модули. project(:core) создаёт зависимости между модулями. Каждый модуль имеет свой собственный build.gradle.

java
// settings.gradle
include 'core', 'web', 'api'
// build.gradle (root)
subprojects {
    apply plugin: 'java'
    repositories { mavenCentral() }
}
// In web/build.gradle
dependencies { implementation project(':core') }
24

Основы Spring

Приложение Spring Boot

@SpringBootApplication включает автоконфигурацию, сканирование компонентов и конфигурацию. SpringApplication.run запускает встроенный сервер. Устраняет XML-конфигурацию.

java
@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

REST Controller

@RestController объединяет @Controller и @ResponseBody. @GetMapping, @PostMapping — ярлыки. @PathVariable извлекает параметры URL, @RequestBody связывает JSON.

java
@RestController
@RequestMapping("/api/users")
public class UserController {
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
    @PostMapping
    public User create(@RequestBody User user) {
        return userService.save(user);
    }
}

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

@Autowired внедряет зависимости. Внедрение через конструктор рекомендуется (тестируемое, неизменяемое). @Service, @Repository, @Component — стереотипы для внедрения.

java
@Service
public class UserService {
    private final UserRepository repo;
    @Autowired  // Constructor injection (recommended)
    public UserService(UserRepository repo) {
        this.repo = repo;
    }
}

Конфигурация

@Configuration помечает конфигурационные классы. @Bean объявляет бины, управляемые Spring. @Primary делает бин предпочтительным. Используйте для сторонних классов.

java
@Configuration
public class AppConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    @Bean @Primary
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }
}

Свойства приложения

application.properties настраивает Spring Boot. Профили включают конфигурацию для конкретной среды. Активируйте с помощью spring.profiles.active=dev. Используйте @Value или @ConfigurationProperties.

java
# application.properties
server.port=8080
spring.datasource.url=jdbc:postgresql://localhost/mydb
spring.jpa.hibernate.ddl-auto=update
# Profile-specific
# application-dev.properties
server.port=9090
25

Углублённое изучение JDBC

Connection и Statement

DriverManager.getConnection устанавливает соединение. Statement выполняет статический SQL. ResultSet перебирает результаты. Всегда закрывайте ресурсы или используйте try-with-resources.

java
Connection conn = DriverManager.getConnection(url, user, pass);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {
    System.out.println(rs.getString("name"));
}
rs.close(); stmt.close(); conn.close();

PreparedStatement

PreparedStatement предотвращает SQL-инъекции путём параметризации запросов. Устанавливайте значения по индексу (с 1). try-with-resources автоматически закрывает. Повышает производительность за счёт предкомпиляции.

java
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setString(1, "Alice");
    ps.setString(2, "[email protected]");
    ps.executeUpdate();
}

Управление транзакциями

setAutoCommit(false) начинает транзакцию. commit сохраняет, rollback отменяет. Если любой оператор завершается неудачей, выполните rollback для сохранения целостности. @Transactional обрабатывает это в Spring.

java
conn.setAutoCommit(false);
try {
    stmt.executeUpdate("UPDATE accounts SET bal = bal - 100 WHERE id = 1");
    stmt.executeUpdate("UPDATE accounts SET bal = bal + 100 WHERE id = 2");
    conn.commit();
} catch (SQLException e) {
    conn.rollback();
}

Пул соединений

Пул соединений повторно использует соединения. HikariCP — самый быстрый пул. maximumPoolSize ограничивает параллельные соединения. Всегда закрывайте (возвращается в пул). Spring Boot настраивает HikariCP автоматически.

java
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setMaximumPoolSize(10);
HikariDataSource ds = new HikariDataSource(config);
Connection conn = ds.getConnection();

Метаданные ResultSet

ResultSetMetaData описывает структуру результата: имена столбцов, типы, свойства. Полезно для универсального доступа к данным. Индексы столбцов начинаются с 1.

java
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
ResultSetMetaData meta = rs.getMetaData();
int cols = meta.getColumnCount();
for (int i = 1; i <= cols; i++)
    System.out.println(meta.getColumnName(i) + ": " + meta.getColumnTypeName(i));
26

Утилиты конкурентности

ExecutorService

ExecutorService управляет пулами потоков. submit возвращает Future для асинхронных результатов. get блокирует до завершения (с тайм-аутом). Всегда выключайте executor.

java
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<String> future = executor.submit(() -> {
    Thread.sleep(1000);
    return "Result";
});
String result = future.get(5, TimeUnit.SECONDS);
executor.shutdown();

CompletableFuture

CompletableFuture включает функциональное асинхронное программирование. supplyAsync запускается в ForkJoinPool. thenApply преобразует, thenAccept потребляет, exceptionally обрабатывает ошибки.

java
CompletableFuture.supplyAsync(() -> fetchData())
    .thenApply(data -> process(data))
    .thenAccept(result -> System.out.println(result))
    .exceptionally(ex -> { ex.printStackTrace(); return null; });

Конкурентные коллекции

ConcurrentHashMap потокобезопасен без полной блокировки. CopyOnWriteArrayList копирует при записи (для частого чтения). BlockingQueue поддерживает шаблоны producer-consumer.

java
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.computeIfAbsent("b", k -> k.length());
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);

CountDownLatch и CyclicBarrier

CountDownLatch ожидает N потоков (одноразовый). CyclicBarrier ожидает N потоков, затем сбрасывается (многоразовый). Используйте latch для запуска, barrier для поэтапных вычислений.

java
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++)
    new Thread(() -> { work(); latch.countDown(); }).start();
latch.await();  // Wait for all

CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All ready"));

Атомарные переменные

Атомарные переменные обеспечивают потокобезопасные операции без блокировок. compareAndSet включает оптимистическую блокировку. LongAdder быстрее, чем AtomicLong для счётчиков с высокой конкуренцией.

java
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.compareAndSet(0, 1);
LongAdder adder = new LongAdder();
adder.increment();
27

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

Области памяти

Память JVM: Heap (объекты, управляется GC), Stack (вызовы методов, на поток), Metaspace (метаданные классов). Young Gen использует копирующий GC, Old Gen использует mark-sweep-compact.

java
// Heap: objects and arrays (shared)
// - Young Gen: Eden, S0, S1
// - Old Gen: long-lived objects
// Stack: method frames (per thread)
// Metaspace: class metadata (Java 8+)
// JVM flags: -Xms512m -Xmx2g

Загрузка классов

Загрузка классов ленивая. Bootstrap загружает core Java, Extension загружает расширения, Application загружает classpath. Статические инициализаторы выполняются один раз. Пользовательские загрузчики классов позволяют горячую перезагрузку.

java
// Bootstrap -> Extension -> Application classloaders
class MyClass {
    static { System.out.println("Static init"); }
}
// Class.forName("MyClass") triggers loading
// -verbose:class shows class loading

Байт-код

Java компилируется в байт-код (на основе стека). javap -c дизассемблирует файлы классов. Каждая инструкция помещает/извлекает значения в стек операндов. Java-агенты могут изменять байт-код во время загрузки.

java
// javap -c MyClass.class
// Method int add(int, int):
//   iload_1      // Load local var 1
//   iload_2      // Load local var 2
//   iadd         // Add
//   ireturn      // Return int

JIT-компиляция

JIT компилирует часто выполняемый байт-код в машинный код. Многоуровневая компиляция балансирует запуск и пиковую производительность. Горячие методы инлайнятся и оптимизируются.

java
// JIT compiles hot methods to native code
// -XX:+PrintCompilation  // Show JIT activity
// -XX:CompileThreshold=10000  // Method call count
// Tiered: Interpreter -> C1 -> C2

Дамп потоков

Дампы потоков показывают все состояния потоков и трассировки стека. Необходимы для отладки взаимных блокировок и зависаний. jstack — инструмент командной строки. Ищите потоки в состоянии BLOCKED и WAITING.

java
// Get thread dump
jstack <pid>
// Or: kill -3 <pid>
// Deadlock detection
jstack -l <pid> | grep -A 20 "Found deadlock"
28

Сборка мусора

Алгоритмы GC

Serial GC для небольших приложений. Parallel GC максимизирует пропускную способность. G1 GC балансирует пропускную способность и задержки (по умолчанию). ZGC обеспечивает задержки менее миллисекунды для больших куч.

java
# Serial GC (single-threaded)
-XX:+UseSerialGC
# Parallel GC (throughput)
-XX:+UseParallelGC
# G1 GC (balanced, default in Java 9+)
-XX:+UseG1GC
# ZGC (low-latency)
-XX:+UseZGC

Настройка G1 GC

G1 делит кучу на регионы. MaxGCPauseMillis устанавливает мягкую цель паузы. G1 приоритизирует регионы с наибольшим количеством мусора. Используйте GCViewer или GCEasy для анализа логов.

java
# Set heap
-Xms4g -Xmx4g
# Max GC pause target
-XX:MaxGCPauseMillis=200
# Region size (1-32MB)
-XX:G1HeapRegionSize=16m
# Enable GC logging
-Xlog:gc*:file=gc.log:time,uptime

Утечки памяти

Утечки памяти вызываются непреднамеренным удержанием объектов. Статические коллекции, незакрытые ресурсы и регистрация слушателей — распространённые причины. jmap показывает количество объектов. Анализируйте hprof с помощью MAT.

java
// Common leak: static collections
static Map<String, Object> cache = new HashMap<>();
// Objects never removed -> leak
// Detect with:
jmap -histo <pid> | head -20
// Heap dump:
jmap -dump:format=b,file=heap.hprof <pid>

Слабые ссылки

WeakReference разрешает GC, когда нет сильных ссылок. SoftReference существует до давления памяти. Ключи WeakHashMap слабые. Используйте для кэшей, которые не должны препятствовать GC.

java
WeakReference<Object> weakRef = new WeakReference<>(new Object());
SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024]);
WeakHashMap<Object, String> map = new WeakHashMap<>();
map.put(key, "value");  // Entry removed when key is GC'd

Финализация

finalize() устарел (непредсказуемый, медленный). Cleaner API (Java 9+) обеспечивает лучшую очистку. try-with-resources предпочтителен для детерминированной очистки.

java
// Cleaner API (Java 9+)
class Resource implements AutoCloseable {
    private final Cleaner.Cleanable cleanable;
    Resource() {
        cleanable = Cleaner.create().register(this, () -> cleanup());
    }
    public void close() { cleanable.clean(); }
}
29

Коллекторы Stream

Группировка (Grouping By)

groupingBy разделяет элементы по классификатору. Второй аргумент — нижестоящий коллектор для агрегации. counting, averaging, summing — распространённые нижестоящие коллекторы.

java
Map<String, List<Person>> byCity =
    people.stream().collect(Collectors.groupingBy(Person::getCity));
Map<String, Long> countByCity =
    people.stream().collect(Collectors.groupingBy(
        Person::getCity, Collectors.counting()));

Разделение (Partitioning)

partitioningBy разделяет на две группы (true/false). Эффективнее, чем groupingBy для логических ключей. Результат всегда имеет оба ключа. Нижестоящие коллекторы агрегируют каждый раздел.

java
Map<Boolean, List<Person>> partition =
    people.stream().collect(Collectors.partitioningBy(p -> p.getAge() >= 18));
// {false=[minors], true=[adults]}
Map<Boolean, Long> count =
    people.stream().collect(Collectors.partitioningBy(
        p -> p.getAge() >= 18, Collectors.counting()));

Объединение (Joining)

joining объединяет строки с опциональным разделителем, префиксом и суффиксом. Элементы должны быть строками; сначала используйте map. Внутренне использует StringBuilder.

java
String names = people.stream()
    .map(Person::getName)
    .collect(Collectors.joining(", "));
// "Alice, Bob, Charlie"
String csv = people.stream()
    .map(p -> p.getName() + "=" + p.getAge())
    .collect(Collectors.joining("\n", "[", "]"));

Сокращение (Reducing)

reducing выполняет операцию свёртки. Версия с тремя аргументами принимает identity, mapper, reducer. Версия с двумя аргументами возвращает Optional. Используйте, когда стандартных коллекторов недостаточно.

java
int totalAge = people.stream()
    .collect(Collectors.reducing(0, Person::getAge, Integer::sum));
Optional<Person> oldest = people.stream()
    .collect(Collectors.reducing((p1, p2) ->
        p1.getAge() > p2.getAge() ? p1 : p2));

Пользовательский коллектор

Collector.of создаёт пользовательские коллекторы: supplier, accumulator, combiner, finisher. Combiner объединяет частичные результаты для параллельных потоков. Полезно для специализированных форматов вывода.

java
Collector<Person, ?, String> toJson = Collector.of(
    StringBuilder::new,
    (sb, p) -> sb.append(`{"name":"${p.getName()}"}`),
    StringBuilder::append,
    StringBuilder::toString
);
30

Распространённые подводные камни

Кэширование Integer

Java кэширует значения Integer от -128 до 127. == сравнивает ссылки, а не значения. Для Integer вне диапазона кэша == возвращает false. Всегда используйте .equals() для Integer.

java
Integer a = 127; Integer b = 127;
System.out.println(a == b);  // true (cached)
Integer c = 128; Integer d = 128;
System.out.println(c == d);  // false (not cached)
System.out.println(c.equals(d));  // true

Неизменяемость строк

Строки неизменяемы: методы вроде concat возвращают новые строки. Забыть присвоить результат — распространённая ошибка. Используйте StringBuilder для повторяющейся конкатенации.

java
String s = "Hello";
s.concat(" World");  // Returns new string, s unchanged
System.out.println(s);  // "Hello"
// Use StringBuilder for mutation
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World");  // Mutates in place

Equals и HashCode

Равные объекты должны иметь равные хеш-коды. HashMap и HashSet используют hashCode для корзин и equals для сравнения. Если вы переопределяете equals, вы ДОЛЖНЫ переопределить hashCode.

java
class Person {
    String name;
    public boolean equals(Object o) {
        if (!(o instanceof Person)) return false;
        return name.equals(((Person)o).name);
    }
    public int hashCode() { return name.hashCode(); }
}

Проверяемые и непроверяемые

Проверяемые исключения должны быть объявлены или перехвачены (IOException, SQLException). Непроверяемые (RuntimeException) не требуют обработки. Избегайте перехвата широкого Exception.

java
// Checked: must catch or declare
try { Thread.sleep(1000); }
catch (InterruptedException e) { /* handle */ }
// Unchecked: RuntimeException
throw new IllegalArgumentException("bad input");

Утечки ресурсов

Ресурсы должны быть закрыты. try-with-resources (Java 7+) автоматически закрывает AutoCloseable. Без этого исключения вызывают утечки. Никогда не оставляйте ресурс незакрытым.

java
// BAD: resource leak
FileInputStream fis = new FileInputStream("file.txt");
// If exception here, fis never closed
// GOOD: try-with-resources
try (FileInputStream fis = new FileInputStream("file.txt")) {
    // Use resource
}  // Auto-closed even on exception
31

Паттерны проектирования

Singleton

Двойная проверка блокировки с volatile обеспечивает потокобезопасную ленивую инициализацию. volatile предотвращает переупорядочивание инструкций. Enum singleton проще: public enum Singleton { INSTANCE; }.

java
public class Singleton {
    private static volatile Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) instance = new Singleton();
            }
        }
        return instance;
    }
}

Builder

Паттерн Builder обрабатывает объекты со многими опциональными параметрами. Избегает телескопических конструкторов. Внутренний Builder собирает параметры текуче. build() создаёт неизменяемый объект.

java
public class Pizza {
    private final String size;
    private final boolean cheese;
    private Pizza(Builder b) { size = b.size; cheese = b.cheese; }
    public static class Builder {
        private String size; private boolean cheese;
        public Builder size(String s) { size = s; return this; }
        public Builder cheese(boolean c) { cheese = c; return this; }
        public Pizza build() { return new Pizza(this); }
    }
}

Strategy

Паттерн Strategy инкапсулирует взаимозаменяемые алгоритмы. Контекст делегирует интерфейсу стратегии. Избегает больших цепочек if-else. Следует принципу open-closed.

java
interface PaymentStrategy { void pay(double amount); }
class CreditCard implements PaymentStrategy {
    public void pay(double amount) { System.out.println("Card: " + amount); }
}
class Cart { private PaymentStrategy strategy; void checkout() { strategy.pay(100); } }

Observer

Паттерн Observer определяет зависимость один-ко-многим. Когда субъект изменяется, все наблюдатели уведомляются. Используется в MVC, событийных системах и реактивном программировании.

java
interface Observer { void update(String event); }
class Subject {
    private List<Observer> observers = new ArrayList<>();
    void subscribe(Observer o) { observers.add(o); }
    void notify(String event) { observers.forEach(o -> o.update(event)); }
}

Factory Method

Factory Method определяет интерфейс для создания объектов, но позволяет подклассам решать, какой класс инстанцировать. Отделяет клиентский код от конкретных классов.

java
abstract class Document { abstract void open(); }
class PDF extends Document { void open() { /* ... */ } }
abstract class DocFactory { abstract Document create(); }
class PDFFactory extends DocFactory { Document create() { return new PDF(); } }

Связанные сниппеты Java

Copy-paste ready code for common tasks.

Строковые операции и StringBuilder

Манипуляции со строками с помощью split, join, substring и StringBuilder в Java.

Stream API

Обработка коллекций с помощью Stream API.

Лямбда-выражения

Упрощение кода с помощью лямбда-выражений.

Optional

Элегантная обработка null-значений.

Операции с коллекциями

Распространённые операции с List, Set и Map.

Обработка исключений

try-catch-finally и пользовательские исключения.

Файловый ввод-вывод

Операции чтения и записи файлов.

Потоки

Создание и управление потоками.

Утилиты конкурентности

Распространённые утилиты в пакете конкурентности.

Аннотации

Пользовательские аннотации и использование.

Дженерики

Дженерик-классы и методы.

Рефлексия

Получение информации о классе во время выполнения.

Сериализация

Сериализация и десериализация объектов.

Дата и время

Date-Time API Java 8+.

Регулярные выражения

Pattern и Matcher.

JDBC

Подключение к базе данных и операции.

HTTP-клиент

HTTP-клиент Java 11+.

Record

Классы record Java 14+.

Сопоставление образцов

Сопоставление образцов instanceof.

Запечатанные классы

Запечатанные классы Java 17+.

Текстовые блоки

Многострочные строки Java 15+.

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.