Программирование:Экзамен Сем2: различия между версиями
WT EMIT (обсуждение | вклад) Нет описания правки Метка: визуальный редактор отключён |
Ivabus (обсуждение | вклад) м Ivabus переименовал страницу Программирование. Экзамен Сем2 в Программирование:Экзамен Сем2 |
(нет различий)
|
Текущая версия от 08:20, 27 июня 2025
Основы Java и Коллекции
1. Обобщенные и параметризованные типы. Создание параметризованных классов.
Обобщенные типы (Generics) — это механизм в Java, который позволяет создавать классы, интерфейсы и методы, работающие с различными типами данных, при этом сохраняя строгую проверку типов на этапе компиляции. Основная цель — повысить безопасность типов и избежать необходимости явного приведения типов (casting).
Параметризованный класс — это класс, который может быть параметризован одним или несколькими типами. Эти типы указываются в угловых скобках <>
.
Создание параметризованного класса: Для создания такого класса мы используем параметр типа (например, T
, E
, K
, V
) в его объявлении. Этот параметр затем можно использовать внутри класса как обычный тип для полей, методов и локальных переменных.
Пример: Создадим простой класс Box
, который может хранить объект любого типа.
// Объявление параметризованного класса Box с параметром типа T
public class Box<T> {
// Поле типа T
private T value;
// Конструктор
public Box(T value) {
this.value = value;
}
// Геттер, возвращающий значение типа T
public T getValue() {
return value;
}
// Сеттер, принимающий значение типа T
public void setValue(T value) {
this.value = value;
}
public void showType() {
if (value != null) {
System.out.println("Тип T: " + value.getClass().getName());
} else {
System.out.println("Значение не установлено.");
}
}
}
// Использование класса Box
public class Main {
public static void main(String[] args) {
// Создаем Box для Integer
Box<Integer> integerBox = new Box<>(123);
System.out.println("Значение в integerBox: " + integerBox.getValue());
integerBox.showType(); // Тип T: java.lang.Integer
// Создаем Box для String
Box<String> stringBox = new Box<>("Привет, мир!");
System.out.println("Значение в stringBox: " + stringBox.getValue());
stringBox.showType(); // Тип T: java.lang.String
// Ошибка компиляции! Попытка положить String в Box<Integer>
// integerBox.setValue("не число");
}
}
Преимущества Generics: 1. Безопасность типов: Компилятор проверяет, что в коллекцию или класс помещаются объекты только совместимого типа. 2. Устранение приведения типов: Не нужно вручную приводить типы при извлечении объектов, что делает код чище и безопаснее. 3. Переиспользование кода: Один и тот же класс можно использовать для работы с разными типами данных.
2. Работа с параметризованными методами. Ограничение типа сверху или снизу.
Параметризованный метод (Generic Method) — это метод, который имеет собственный параметр типа. Этот параметр указывается в угловых скобках перед возвращаемым типом метода. Он полезен, когда обобщенная логика нужна только для одного метода, а не для всего класса.
public class Util {
// <T> объявляет, что это параметризованный метод
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
}
Ограничение типа (Bounded Wildcards) используется для того, чтобы ограничить типы, которые можно использовать в качестве аргументов для обобщенных типов. Это делается с помощью ключевых слов extends
и super
.
1. Ограничение сверху (Upper Bounded Wildcard) — <? extends Type>
- Означает “любой тип, который является Type
или его подтипом”. - Используется, когда вы хотите читать данные из структуры (Producer). Вы не можете добавлять элементы в такую коллекцию (кроме null
), потому что компилятор не знает точного типа. - Принцип PECS: Producer Extends.
Пример: Метод, который вычисляет сумму чисел в списке, где элементы могут быть Integer
, Double
, Float
и т.д. Все они наследуются от Number
.
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number n : list) {
sum += n.doubleValue(); // Мы можем безопасно читать, т.к. любой элемент - это Number
}
// list.add(123); // Ошибка компиляции! Нельзя добавлять.
return sum;
}
2. Ограничение снизу (Lower Bounded Wildcard) — <? super Type>
- Означает “любой тип, который является Type
или его супертипом”. - Используется, когда вы хотите записывать данные в структуру (Consumer). Вы можете безопасно добавлять объекты типа Type
или его подтипов. При чтении вы получите только Object
. - Принцип PECS: Consumer Super.
Пример: Метод, который добавляет несколько целых чисел в список. Список может быть List<Integer>
, List<Number>
или List<Object>
.
public static void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
// Object o = list.get(0); // Читать можно, но получим только Object
}
public static void main(String[] args) {
List<Number> numberList = new ArrayList<>();
addIntegers(numberList);
System.out.println(numberList); // [1, 2]
}
3. Класс Number. Классы-обертки (Wrappers). Автоупаковка и автораспаковка.
Класс Number
- java.lang.Number
— это абстрактный класс, который является суперклассом для всех стандартных числовых классов-оберток в Java: Byte
, Short
, Integer
, Long
, Float
, Double
. - Он предоставляет методы для преобразования значения, хранящегося в объекте-обертке, в любой из примитивных числовых типов: intValue()
, longValue()
, floatValue()
, doubleValue()
, byteValue()
, shortValue()
.
Классы-обертки (Wrappers) - Это классы, которые “оборачивают” примитивные типы данных в объекты. Это необходимо, потому что коллекции и многие другие механизмы Java (например, Generics) работают только с объектами. - Каждому примитивному типу соответствует свой класс-обертка: - byte
-> Byte
- short
-> Short
- int
-> Integer
- long
-> Long
- float
-> Float
- double
-> Double
- char
-> Character
- boolean
-> Boolean
Автоупаковка (Autoboxing) - Это автоматическое преобразование примитивного типа в соответствующий ему класс-обертку. Компилятор Java делает это неявно.
// Раньше (до Java 5):
Integer iObject = Integer.valueOf(100);
// Сейчас (с автоупаковкой):
Integer iObject = 100; // int 100 автоматически преобразуется в Integer
Автораспаковка (Unboxing) - Это обратный процесс: автоматическое преобразование объекта класса-обертки в соответствующий примитивный тип.
// Раньше:
int iPrimitive = iObject.intValue();
// Сейчас (с автораспаковкой):
int iPrimitive = iObject; // Integer iObject автоматически преобразуется в int
Важный момент: Автораспаковка может привести к NullPointerException
, если объект-обертка равен null
.
Integer n = null;
int num = n; // Бросит NullPointerException во время выполнения
4. Коллекции. Виды коллекций. Интерфейсы List, Queue, Deque, Set, Map.
Java Collections Framework — это набор классов и интерфейсов для хранения и обработки групп объектов. Он предоставляет готовые структуры данных и алгоритмы.
Основные интерфейсы коллекций:
Collection<E>
— корневой интерфейс иерархии (кромеMap
). Определяет базовые операции: добавление, удаление, проверка размера, проверка на наличие элемента.List<E>
— упорядоченная коллекция (последовательность), в которой элементы могут дублироваться. Элементы имеют индекс, доступ к ним осуществляется по этому индексу.- Основные реализации:
ArrayList
,LinkedList
.
- Основные реализации:
Set<E>
— коллекция, которая не хранит дублирующихся элементов. Порядок элементов, как правило, не гарантируется (зависит от реализации).- Основные реализации:
HashSet
(неупорядоченный),LinkedHashSet
(в порядке добавления),TreeSet
(отсортированный).
- Основные реализации:
Queue<E>
— коллекция, предназначенная для хранения элементов перед их обработкой. Обычно работает по принципу FIFO (First-In-First-Out, “первым пришел — первым ушел”).- Методы для добавления:
add()
,offer()
. - Методы для извлечения:
remove()
,poll()
. - Методы для просмотра:
element()
,peek()
. - Основные реализации:
LinkedList
,PriorityQueue
.
- Методы для добавления:
Deque<E>
(Double-Ended Queue) — двунаправленная очередь. Позволяет добавлять и удалять элементы с обоих концов. Может использоваться как очередь (FIFO) или как стек (LIFO - Last-In-First-Out).- Основные реализации:
ArrayDeque
,LinkedList
.
- Основные реализации:
Интерфейс Map<K, V>
- Стоит особняком от иерархии Collection
. - Хранит данные в виде пар “ключ-значение” (K
-V
). - Каждый ключ в Map
должен быть уникальным. Одному ключу соответствует одно значение. - Не является “коллекцией” в строгом смысле, но входит в Collections Framework. - Основные реализации: HashMap
(неупорядоченный), LinkedHashMap
(в порядке добавления), TreeMap
(ключи отсортированы).
5. Обход элементов коллекции. Интерфейсы Iterable, Iterator и ListIterator
Обход элементов коллекции — это процесс последовательного доступа к каждому элементу коллекции для выполнения каких-либо действий.
1. Интерфейс Iterable<T>
- Реализуется всеми классами коллекций из java.util.Collection
. - Означает, что по объектам этого класса можно “проитерироваться”. - Содержит один метод iterator()
, который возвращает объект типа Iterator
. - Именно благодаря этому интерфейсу мы можем использовать цикл for-each
.
List<String> names = List.of("Alice", "Bob", "Charlie");
// Цикл for-each работает, потому что List реализует Iterable
for (String name : names) {
System.out.println(name);
}
2. Интерфейс Iterator<E>
- Основной механизм для обхода элементов любой коллекции. - Позволяет безопасно удалять элементы из коллекции во время итерации. - Основные методы: - boolean hasNext()
: возвращает true
, если в коллекции есть еще элементы. - E next()
: возвращает следующий элемент в коллекции и сдвигает “курсор”. - void remove()
: удаляет последний элемент, возвращенный методом next()
. Может быть вызван только один раз после next()
.
Пример: ```java List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6)); Iterator<Integer> iterator = numbers.iterator();
while (iterator.hasNext()) { Integer number = iterator.next(); if (number % 2 == 0) { iterator.remove(); // Безопасное удаление четных чисел } } System.out.println(numbers); // [1, 3, 5] ``*Пытаться удалить элемент напрямую из коллекции во время итерации (кроме как через
iterator.remove()) приведет к
ConcurrentModificationException`.*
3. Интерфейс ListIterator<E>
- Расширяет Iterator
и предназначен специально для List
. - Позволяет двигаться по списку в обоих направлениях. - Позволяет изменять и добавлять элементы в список во время итерации. - Дополнительные методы: - boolean hasPrevious()
: проверяет, есть ли предыдущий элемент. - E previous()
: возвращает предыдущий элемент. - int nextIndex()
/ int previousIndex()
: возвращают индексы. - void set(E e)
: заменяет последний элемент, возвращенный next()
или previous()
. - void add(E e)
: вставляет новый элемент перед “курсором”.
6. Сортировка элементов коллекций. Интерфейсы Comparable и Comparator.
Для сортировки объектов в Java используются два основных интерфейса: Comparable
и Comparator
.
1. Интерфейс Comparable<T>
- Определяет “естественный порядок” сортировки для объектов класса. - Находится в пакете java.lang
. - Класс, который вы хотите сортировать, должен реализовать этот интерфейс. - Содержит один метод: int compareTo(T o)
. - Возвращает отрицательное число, если текущий объект (this
) меньше o
. - Возвращает ноль, если объекты равны. - Возвращает положительное число, если текущий объект больше o
.
Пример: ```java public class Person implements Comparable<Person> { private String name; private int age;
// конструкторы, геттеры... @Override public int compareTo(Person other) { // Сортировка по возрасту return Integer.compare(this.age, other.age); }
}
List<Person> people = new ArrayList<>(); // … добавляем людей Collections.sort(people); // Сортирует по “естественному порядку” (по возрасту) ```
2. Интерфейс Comparator<T>
- Определяет внешний, настраиваемый порядок сортировки. - Находится в пакете java.util
. - Используется, когда: - Класс не реализует Comparable
. - Нужно определить несколько разных способов сортировки. - Вы не можете изменить исходный код класса. - Содержит основной метод: int compare(T o1, T o2)
. - Работает аналогично compareTo
: возвращает <0, 0, >0.
Пример: ```java // Компаратор для сортировки Person по имени public class PersonNameComparator implements Comparator<Person> { @Override public int compare(Person p1, Person p2) { return p1.getName().compareTo(p2.getName()); } }
List<Person> people = new ArrayList<>(); // … добавляем людей // Используем компаратор для сортировки people.sort(new PersonNameComparator());
// С Java 8 можно использовать лямбда-выражения: people.sort(Comparator.comparing(Person::getName)); ```
Методы сортировки: - Collections.sort(List<T> list)
: сортирует список, используя естественный порядок (Comparable
). - Collections.sort(List<T> list, Comparator<? super T> c)
: сортирует с использованием заданного Comparator
. - list.sort(Comparator<? super T> c)
: метод самого списка, появился в Java 8.
7. Интерфейсы Set и SortedSet, их реализации. Классы HashSet и TreeSet.
Интерфейс Set<E>
- Представляет коллекцию, которая не содержит дубликатов. - Если попытаться добавить элемент, который уже есть в Set
(проверка через equals()
), операция будет проигнорирована. - Не гарантирует порядок элементов (за исключением LinkedHashSet
).
Основные реализации Set
:
1. HashSet<E>
- Самая распространенная реализация Set
. - Основана на хэш-таблице (внутренне использует HashMap
). - Не гарантирует порядок элементов. Порядок может меняться со временем. - Обеспечивает очень высокую производительность (в среднем O(1)) для операций add
, remove
, contains
, size
. - Требует, чтобы у добавляемых объектов были корректно реализованы методы hashCode()
и equals()
. - Допускает хранение одного null
элемента.
2. TreeSet<E>
- Реализация, которая хранит элементы в отсортированном порядке. - Основана на сбалансированном бинарном дереве (красно-черное дерево). - Элементы сортируются либо в “естественном порядке” (класс должен реализовывать Comparable
), либо с помощью Comparator
, переданного в конструктор TreeSet
. - Производительность операций add
, remove
, contains
составляет O(log n). - Не допускает хранения null
элементов (вызовет NullPointerException
).
Интерфейс SortedSet<E>
- Расширяет Set
и добавляет гарантию того, что итератор будет обходить элементы в отсортированном порядке. - TreeSet
является основной реализацией этого интерфейса. - Предоставляет дополнительные методы для работы с отсортированной коллекцией: - Comparator<? super E> comparator()
: возвращает компаратор, используемый для сортировки. - SortedSet<E> subSet(E fromElement, E toElement)
: возвращает подмножество элементов. - SortedSet<E> headSet(E toElement)
: возвращает “голову” множества (элементы < toElement
). - SortedSet<E> tailSet(E fromElement)
: возвращает “хвост” множества (элементы >= fromElement
). - E first()
: возвращает первый (минимальный) элемент. - E last()
: возвращает последний (максимальный) элемент.
Сравнение HashSet
и TreeSet
: | Характеристика | HashSet
| TreeSet
| | —————— | ———————————————– | ——————————————————– | | Порядок | Неупорядоченный | Отсортированный | | Производительность | O(1) в среднем | O(log n) | | Требования к элементам | equals()
и hashCode()
| Comparable
или Comparator
| | null
элементы | Разрешен один null
| Не разрешены | | Основа | Хэш-таблица (HashMap
) | Красно-черное дерево |
8. Интерфейс List и его реализации. Классы ArrayList и LinkedList.
Интерфейс List<E>
- Представляет собой упорядоченную коллекцию (последовательность). - Позволяет хранить дублирующиеся элементы. - Доступ к элементам осуществляется по целочисленному индексу (начиная с 0). - Позволяет контролировать позицию вставки каждого элемента.
Основные реализации List
:
1. ArrayList<E>
- Реализация List
на основе динамического массива. - Хранит элементы в непрерывном блоке памяти. - Преимущества: - Быстрый доступ по индексу (операция get(int index)
выполняется за O(1)), так как позиция элемента вычисляется по формуле. - Недостатки: - Медленные операции вставки и удаления элементов из середины списка (O(n)), так как требуется сдвигать все последующие элементы. - При переполнении внутреннего массива происходит его копирование в новый массив большего размера, что является затратной операцией. - Когда использовать: Когда вам нужен частый доступ к элементам по индексу и редкие операции вставки/удаления.
2. LinkedList<E>
- Реализация List
и Deque
на основе двусвязного списка. - Каждый элемент (узел) хранит ссылку на предыдущий и следующий элементы. - Преимущества: - Быстрые операции вставки и удаления элементов в начало, конец и середину списка (O(1), если у вас есть ссылка на узел; O(n) для поиска узла по индексу). Нужно лишь изменить ссылки у соседних элементов. - Недостатки: - Медленный доступ по индексу (операция get(int index)
выполняется за O(n)), так как требуется пройти по списку от начала или конца до нужного элемента. - Занимает больше памяти, чем ArrayList
, из-за хранения ссылок. - Когда использовать: Когда вам нужны частые операции вставки/удаления элементов. Также хорошо подходит для реализации очередей и стеков.
Сравнение ArrayList
и LinkedList
: | Операция | ArrayList
| LinkedList
| | ———————— | —————————————– | —————————————– | | Доступ (get
) | O(1) - очень быстро | O(n) - медленно | | Вставка/удаление (конец) | O(1) в среднем (иногда O(n) из-за resizing) | O(1) - очень быстро | | Вставка/удаление (середина) | O(n) - медленно | O(1) (если итератор на месте) | | Поиск (contains
) | O(n) | O(n) | | Использование памяти | Меньше | Больше (из-за ссылок) |
9. Интерфейсы Map и SortedMap, их реализации. Классы HashMap и TreeMap.
Интерфейс Map<K, V>
- Определяет структуру данных для хранения пар “ключ-значение”. - Ключи (K
) в Map
должны быть уникальными. - Каждому ключу соответствует ровно одно значение (V
). - Не является наследником интерфейса Collection
.
Основные реализации Map
:
1. HashMap<K, V>
- Самая популярная реализация Map
. - Основана на хэш-таблице. - Не гарантирует порядок элементов. - Обеспечивает очень высокую производительность (в среднем O(1)) для операций put
, get
, remove
, containsKey
. - Требует, чтобы у ключей были корректно реализованы методы hashCode()
и equals()
. - Допускает один null
ключ и множество null
значений.
2. TreeMap<K, V>
- Реализация, которая хранит записи отсортированными по ключам. - Основана на сбалансированном бинарном дереве (красно-черное дерево). - Ключи сортируются либо в “естественном порядке” (класс ключа должен реализовывать Comparable
), либо с помощью Comparator
, переданного в конструктор TreeMap
. - Производительность операций put
, get
, remove
составляет O(log n). - Не допускает null
ключей (вызовет NullPointerException
).
Интерфейс SortedMap<K, V>
- Расширяет Map
и гарантирует, что итерация по ключам будет происходить в отсортированном порядке. - TreeMap
является основной реализацией этого интерфейса. - Предоставляет дополнительные методы, аналогичные SortedSet
: - Comparator<? super K> comparator()
- SortedMap<K,V> subMap(K fromKey, K toKey)
- SortedMap<K,V> headMap(K toKey)
- SortedMap<K,V> tailMap(K fromKey)
- K firstKey()
- K lastKey()
Сравнение HashMap
и TreeMap
: | Характеристика | HashMap
| TreeMap
| | —————— | ———————————————– | ——————————————————– | | Порядок | Неупорядоченный | Отсортированный по ключам | | Производительность | O(1) в среднем | O(log n) | | Требования к ключам | equals()
и hashCode()
| Comparable
или Comparator
| | null
ключ | Разрешен один | Не разрешен | | Основа | Хэш-таблица | Красно-черное дерево |
10. Интерфейсы Queue и Deque. Классы PriorityQueue и ArrayDeque.
Интерфейс Queue<E>
(Очередь) - Коллекция, работающая по принципу FIFO (First-In-First-Out, “первым пришел — первым ушел”). - Элементы добавляются в “хвост” очереди, а извлекаются из “головы”. - Методы Queue
существуют в двух вариантах: - Один бросает исключение при ошибке (add
, remove
, element
). - Другой возвращает специальное значение (offer
-> false
, poll
-> null
, peek
-> null
).
Интерфейс Deque<E>
(Двунаправленная очередь) - Расширяет Queue
и позволяет добавлять/удалять элементы с обоих концов. - Может использоваться как очередь (FIFO) и как стек (LIFO - Last-In-First-Out). - Как стек: push
(addFirst), pop
(removeFirst), peek
(peekFirst).
Реализации:
1. PriorityQueue<E>
- Особый вид очереди, где элементы упорядочены не по времени добавления, а по приоритету. - При извлечении (poll
) всегда возвращается элемент с наивысшим приоритетом (по умолчанию — наименьший элемент). - Порядок определяется либо “естественным порядком” (Comparable
), либо с помощью Comparator
. - Основана на структуре данных “куча” (heap). - Не допускает null
элементов. - Производительность offer
и poll
— O(log n). peek
— O(1). - Применение: алгоритмы поиска кратчайшего пути (Дейкстры), планировщики задач.
2. ArrayDeque<E>
- Реализация Deque
на основе циклического массива. - Очень эффективна для добавления и удаления элементов с обоих концов (O(1) в среднем). - Не имеет ограничений по емкости (растет по мере необходимости). - Не допускает null
элементов. - Является предпочтительной реализацией для стеков и очередей, когда не требуется сортировка по приоритету. Она быстрее, чем LinkedList
.
Пример использования ArrayDeque
как стека:
Deque<String> stack = new ArrayDeque<>();
stack.push("A"); // Добавляем в начало
stack.push("B");
stack.push("C");
System.out.println(stack.pop()); // C (извлекаем из начала)
System.out.println(stack.peek()); // B (смотрим на начало)
Пример использования ArrayDeque
как очереди:
Queue<String> queue = new ArrayDeque<>();
queue.offer("A"); // Добавляем в конец
queue.offer("B");
queue.offer("C");
System.out.println(queue.poll()); // A (извлекаем из начала)
System.out.println(queue.peek()); // B (смотрим на начало)
11. Интерфейсы SequencedCollection, SequencedSet. Классы HashSet и LinkedHashSet.
Эти интерфейсы были добавлены в Java 21 для формализации коллекций с определенным порядком следования элементов.
Интерфейс SequencedCollection<E>
- Представляет коллекцию, элементы которой имеют определенный, предсказуемый порядок следования (encounter order). - Объединяет общие возможности List
и Deque
. - Предоставляет методы для доступа, добавления и удаления элементов с обоих концов: - addFirst(E)
, addLast(E)
- getFirst()
, getLast()
- removeFirst()
, removeLast()
- List
, Deque
и LinkedHashSet
реализуют этот интерфейс.
Интерфейс SequencedSet<E>
- Расширяет SequencedCollection
и Set
. - Представляет собой Set
, элементы которого имеют определенный порядок. - Основной реализацией является LinkedHashSet
. SortedSet
(и TreeSet
) также его реализует.
Сравнение HashSet
и LinkedHashSet
HashSet<E>
- Основан на хэш-таблице. - Не гарантирует порядок элементов. Порядок итерации может меняться после добавления или удаления элементов. - Производительность: O(1) для add
, remove
, contains
.
LinkedHashSet<E>
- Наследуется от HashSet
. - Основан на хэш-таблице, но дополнительно поддерживает двусвязный список, который проходит через все его элементы. - Этот список поддерживает порядок вставки элементов. Итерация по LinkedHashSet
происходит в том порядке, в котором элементы были добавлены. - Производительность практически такая же, как у HashSet
(O(1)), но с небольшими накладными расходами на поддержку списка. - Когда использовать: Когда нужна производительность HashSet
, но при этом важен порядок итерации.
Пример:
Set<String> hashSet = new HashSet<>();
hashSet.add("Charlie");
hashSet.add("Alice");
hashSet.add("Bob");
System.out.println("HashSet: " + hashSet); // Вывод может быть любым, например [Alice, Bob, Charlie]
Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("Charlie");
linkedHashSet.add("Alice");
linkedHashSet.add("Bob");
System.out.println("LinkedHashSet: " + linkedHashSet); // Вывод всегда [Charlie, Alice, Bob]
12. Классы Collections, Arrays и Objects, методы для работы с коллекциями и массивами.
Это три утилитарных класса, предоставляющих статические методы для выполнения общих операций.
1. java.util.Collections
- Содержит статические методы для работы с коллекциями (List
, Set
, Map
). - Основные методы: - Сортировка: sort(List<T> list)
, sort(List<T> list, Comparator<? super T> c)
- Поиск: binarySearch(List<? extends Comparable> list, T key)
- Перемешивание: shuffle(List<?> list)
- Изменение порядка: reverse(List<?> list)
, rotate(List<?> list, int distance)
- Копирование и заполнение: copy(List dest, List src)
, fill(List list, T obj)
- Поиск экстремумов: min(Collection coll)
, max(Collection coll)
- Создание неизменяемых (immutable) коллекций: unmodifiableList(List l)
, unmodifiableSet(Set s)
, unmodifiableMap(Map m)
- Создание потокобезопасных (thread-safe) оберток: synchronizedList(List l)
, synchronizedSet(Set s)
, synchronizedMap(Map m)
- Создание пустых коллекций: emptyList()
, emptySet()
, emptyMap()
2. java.util.Arrays
- Содержит статические методы для работы с массивами. - Основные методы: - Сортировка: sort(int[] a)
, sort(T[] a, Comparator<? super T> c)
- Поиск: binarySearch(int[] a, int key)
- Сравнение: equals(int[] a, int[] a2)
, compare(int[] a, int[] a2)
- Заполнение: fill(int[] a, int val)
- Копирование: copyOf(T[] original, int newLength)
, copyOfRange(...)
- Преобразование в строку: toString(int[] a)
- Преобразование в Stream: stream(T[] array)
- Создание списка из массива: asList(T... a)
(возвращает список фиксированного размера, обернутый вокруг массива).
3. java.util.Objects
- Содержит статические утилитарные методы для работы с объектами, особенно полезны для обработки null
. Появился в Java 7. - Основные методы: - Сравнение: equals(Object a, Object b)
- безопасно сравнивает два объекта, даже если один из них null
. - Хэш-код: hash(Object... values)
- вычисляет хэш-код на основе переданных значений, удобен для реализации hashCode()
. hashCode(Object o)
- возвращает хэш-код или 0, если объект null
. - Проверка на null
: requireNonNull(T obj)
- проверяет, что объект не null
, и если это так, бросает NullPointerException
. requireNonNull(T obj, String message)
- то же самое с пользовательским сообщением. - Значение по умолчанию: requireNonNullElse(T obj, T defaultObj)
- возвращает obj
, если он не null
, иначе defaultObj
. - Преобразование в строку: toString(Object o)
, toString(Object o, String nullDefault)
- возвращает строковое представление или значение по умолчанию, если объект null
.
Потоки ввода-вывода (I/O) и Сериализация
13. Байтовые и символьные потоки ввода-вывода. Базовые классы и их потомки.
Система ввода-вывода (I/O) в Java основана на потоках. Поток — это абстракция, представляющая собой последовательность данных.
Два основных типа потоков:
1. Байтовые потоки (Byte Streams) - Работают с необработанными двоичными данными (байтами). - Подходят для чтения и записи любых типов файлов: изображений, аудио, видео, исполняемых файлов. - Базовые абстрактные классы: - java.io.InputStream
: для чтения байтов. - Основные потомки: FileInputStream
, ByteArrayInputStream
, ObjectInputStream
. - java.io.OutputStream
: для записи байтов. - Основные потомки: FileOutputStream
, ByteArrayOutputStream
, ObjectOutputStream
.
2. Символьные потоки (Character Streams) - Работают с символами (Unicode, 16-бит). - Автоматически обрабатывают преобразование между байтами и символами с использованием указанной кодировки. - Подходят для работы с текстовыми данными. - Базовые абстрактные классы: - java.io.Reader
: для чтения символов. - Основные потомки: FileReader
, StringReader
, InputStreamReader
. - java.io.Writer
: для записи символов. - Основные потомки: FileWriter
, StringWriter
, OutputStreamWriter
.
Иерархия и взаимодействие: - Символьные потоки являются “обертками” над байтовыми потоками. - InputStreamReader
— это мост от байтовых потоков к символьным: он читает байты и декодирует их в символы с использованием указанной кодировки. - OutputStreamWriter
— мост в обратную сторону: он принимает символы, кодирует их в байты и записывает в базовый OutputStream
.
Пример:
// Байтовый поток
try (FileInputStream fis = new FileInputStream("data.bin");
FileOutputStream fos = new FileOutputStream("copy.bin")) {
int byteData;
while ((byteData = fis.read()) != -1) {
fos.write(byteData);
}
}
// Символьный поток
try (FileReader reader = new FileReader("text.txt", StandardCharsets.UTF_8);
FileWriter writer = new FileWriter("copy.txt", StandardCharsets.UTF_8)) {
int charData;
while ((charData = reader.read()) != -1) {
writer.write(charData);
}
}
14. Потоки-фильтры, BufferedReader, InputStreamReader, PrintStream.
Потоки-фильтры (Filter Streams) - Это потоки, которые “оборачивают” другие потоки (байтовые или символьные) для добавления новой функциональности. Они работают по принципу “декоратора”. - Примеры байтовых фильтров: BufferedInputStream
, DataInputStream
, ObjectInputStream
. - Примеры символьных фильтров: BufferedReader
, PrintWriter
.
InputStreamReader
- Как упоминалось выше, это мост от байтовых потоков к символьным. - Он читает байты из InputStream
и преобразует их в символы, используя заданную кодировку. - Это ключевой класс для правильной работы с текстовыми файлами, особенно если их кодировка отличается от системной по умолчанию. java // Чтение текстового файла в кодировке UTF-8 FileInputStream fis = new FileInputStream("file.txt"); InputStreamReader reader = new InputStreamReader(fis, "UTF-8");
BufferedReader
- Символьный поток-фильтр, который добавляет буферизацию к другому Reader
. - Буферизация — это чтение данных из источника большими кусками (в буфер) за один раз. Последующие вызовы read()
берут данные из буфера, что гораздо эффективнее, чем многократные обращения к физическому устройству. - Предоставляет очень удобный метод readLine()
, который читает целую строку текста за раз. java try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) { String line; while ((line = br.readLine()) != null) { System.out.println(line); } }
PrintStream
- Байтовый поток-фильтр, который добавляет функциональность для вывода форматированного представления различных типов данных (int
, double
, String
и т.д.). - Он преобразует эти данные в байты и записывает в базовый OutputStream
. - В отличие от других потоков, PrintStream
никогда не бросает IOException
. Вместо этого он устанавливает внутренний флаг ошибки, который можно проверить методом checkError()
. - Он может автоматически сбрасывать буфер (auto-flush) после каждой новой строки. - System.out
, System.err
являются объектами типа PrintStream
.
15. Стандартный ввод и вывод. Поля System.in, .out, .err, класс Scanner.
Стандартные потоки — это три предопределенных потока, доступных в любой Java-программе. Они управляются операционной системой.
System.out
— стандартный поток вывода.- Тип:
PrintStream
. - По умолчанию направлен на консоль.
- Используется для вывода обычной информации (
System.out.println("Hello");
).
- Тип:
System.err
— стандартный поток ошибок.- Тип:
PrintStream
. - По умолчанию также направлен на консоль.
- Используется для вывода сообщений об ошибках. ОС и IDE часто отображают этот поток отдельно (например, красным цветом), и его вывод не буферизуется так, как
System.out
, чтобы ошибки появлялись немедленно.
- Тип:
System.in
— стандартный поток ввода.- Тип:
InputStream
. - По умолчанию связан с клавиатурой.
- Представляет собой “сырой” байтовый поток, поэтому для удобного чтения данных (особенно текста) его нужно оборачивать.
- Тип:
Класс java.util.Scanner
- Scanner
— это утилитарный класс, который упрощает разбор (парсинг) примитивных типов и строк из потока ввода. - Он может “сканировать” данные из любого источника, который реализует Readable
(например, InputStream
, File
, String
). - Он разбивает входные данные на “токены” с использованием разделителя (по умолчанию — пробельные символы). - Предоставляет удобные методы nextInt()
, nextDouble()
, nextLine()
, next()
и т.д.
Использование Scanner
для чтения с консоли:
// Оборачиваем System.in в Scanner для удобного чтения
Scanner scanner = new Scanner(System.in);
System.out.print("Введите ваше имя: ");
String name = scanner.nextLine(); // Читает всю строку до нажатия Enter
System.out.print("Введите ваш возраст: ");
int age = scanner.nextInt(); // Читает целое число
System.out.println("Привет, " + name + "! Вам " + age + " лет.");
scanner.close(); // Важно закрывать сканер, чтобы освободить ресурсы
Проблема с nextInt()
и nextLine()
: Метод nextInt()
считывает только число, а символ новой строки (\n
) остается в буфере. Следующий вызов nextLine()
сразу же считывает этот пустой остаток. Решение: либо вызвать scanner.nextLine()
после nextInt()
, чтобы “съесть” остаток, либо считывать все как строки и парсить вручную (Integer.parseInt(scanner.nextLine())
).
16. Сериализация объектов. Интерфейс Serializable. Модификатор transient.
Сериализация — это процесс преобразования состояния объекта в последовательность байтов, которую можно сохранить в файл, передать по сети или сохранить в базу данных. Десериализация — это обратный процесс восстановления объекта из этой последовательности байтов.
Интерфейс java.io.Serializable
- Это интерфейс-маркер. Он не содержит никаких методов. - Класс, объекты которого вы хотите сериализовать, должен реализовывать этот интерфейс. Это служит сигналом для JVM, что объект можно безопасно сериализовать. - Если класс реализует Serializable
, все его нестатические и нетранзиентные поля автоматически сериализуются. Если поле является объектом, то его класс также должен быть Serializable
.
Процесс сериализации: - Используется ObjectOutputStream
, который оборачивает любой OutputStream
. - Метод writeObject(Object obj)
выполняет сериализацию.
Процесс десериализации: - Используется ObjectInputStream
, который оборачивает любой InputStream
. - Метод readObject()
выполняет десериализацию и возвращает Object
, который нужно привести к нужному типу.
Модификатор transient
- Ключевое слово transient
используется для пометки полей, которые не должны быть сериализованы. - Это полезно для: - Полей, которые содержат конфиденциальную информацию (пароли, ключи). - Полей, состояние которых можно легко вычислить или восстановить после десериализации. - Полей, которые ссылаются на несериализуемые ресурсы (например, сетевое соединение). - При десериализации transient
поля инициализируются значением по умолчанию для их типа (0
, false
, null
).
Пример:
import java.io.*;
// Класс должен реализовывать Serializable
class User implements Serializable {
private static final long serialVersionUID = 1L; // Важно для контроля версий
private String login;
private transient String password; // Это поле не будет сохранено
public User(String login, String password) {
this.login = login;
this.password = password;
}
@Override
public String toString() {
return "User{login='" + login + "', password='" + password + "'}";
}
}
public class SerializationDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
User user = new User("admin", "12345");
// Сериализация
FileOutputStream fos = new FileOutputStream("user.dat");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(user);
oos.close();
// Десериализация
FileInputStream fis = new FileInputStream("user.dat");
ObjectInputStream ois = new ObjectInputStream(fis);
User restoredUser = (User) ois.readObject();
ois.close();
System.out.println("Original: " + user); // User{login='admin', password='12345'}
System.out.println("Restored: " + restoredUser); // User{login='admin', password='null'}
}
}
17. Работа с файлами в Java. Интерфейс Path. Классы File, Files, Paths.
Эта группа классов и интерфейсов относится к пакету NIO.2 (New I/O), который был представлен в Java 7 как более современная и мощная альтернатива классу java.io.File
.
1. Класс java.io.File
(старый подход) - Представляет собой абстракцию пути к файлу или каталогу в файловой системе. - Имеет недостатки: не предоставляет информацию о причине сбоя операций (возвращает boolean
), плохо работает с символическими ссылками, имеет методы с платформозависимым поведением. - Хотя он все еще используется, предпочтительнее использовать NIO.2.
2. Интерфейс java.nio.file.Path
- Центральная абстракция в NIO.2 для работы с путями. - Представляет путь в файловой системе, но не обязательно указывает на существующий файл. - Является платформонезависимым. - Предоставляет множество полезных методов для манипуляции путями: getFileName()
, getParent()
, getRoot()
, resolve()
, relativize()
.
3. Класс java.nio.file.Paths
- Утилитарный класс с одним статическим методом get(String first, String... more)
, который создает объект Path
из строки или последовательности строк. - Это основной способ получения экземпляра Path
.
Path p1 = Paths.get("C:\\Users\\John\\Documents\\file.txt");
Path p2 = Paths.get("/home/user/data", "report.csv");
4. Класс java.nio.file.Files
- Утилитарный класс, который содержит статические методы для выполнения операций с файлами и каталогами, используя объекты Path
. - Это “рабочая лошадка” NIO.2. - Основные методы: - Проверка: exists(Path)
, isDirectory(Path)
, isRegularFile(Path)
, isReadable(Path)
- Создание: createFile(Path)
, createDirectory(Path)
, createDirectories(Path)
- Удаление: delete(Path)
, deleteIfExists(Path)
- Копирование/перемещение: copy(Path src, Path dest, CopyOption...)
, move(Path src, Path dest, CopyOption...)
- Чтение/запись: - byte[] readAllBytes(Path)
: читает весь файл в массив байтов (для небольших файлов). - List<String> readAllLines(Path)
: читает все строки в список. - write(Path, byte[])
: записывает массив байтов в файл. - write(Path, Iterable<? extends CharSequence>)
: записывает строки в файл. - Работа с потоками: newInputStream(Path)
, newOutputStream(Path)
, newBufferedReader(Path)
, newBufferedWriter(Path)
. - Работа с атрибутами: size(Path)
, getLastModifiedTime(Path)
, setAttribute(...)
. - Обход дерева каталогов: walkFileTree(Path, FileVisitor)
.
Пример:
Path path = Paths.get("data.txt");
// Запись в файл
List<String> lines = List.of("Строка 1", "Строка 2");
Files.write(path, lines);
// Чтение из файла
if (Files.exists(path)) {
List<String> readLines = Files.readAllLines(path);
System.out.println(readLines);
}
// Копирование
Path copyPath = Paths.get("data_copy.txt");
Files.copy(path, copyPath, StandardCopyOption.REPLACE_EXISTING);
// Удаление
Files.delete(copyPath);
18. Новый пакет ввода-вывода. Буферы и каналы. Класс FileChannel.
NIO (New I/O) — это альтернативная система ввода-вывода в Java, появившаяся в версии 1.4. Она предоставляет более гибкую и производительную модель, основанную на каналах и буферах.
Ключевые концепции NIO:
1. Буферы (Buffers) - java.nio.Buffer
и его потомки (ByteBuffer
, CharBuffer
, IntBuffer
и т.д.). - Буфер — это блок памяти фиксированного размера, в который записываются данные перед передачей по каналу, или из которого читаются данные, полученные по каналу. - Основные свойства буфера: - capacity
: общая вместимость буфера (неизменна). - limit
: индекс первого элемента, который не следует читать или записывать. Ограничивает “активную” область буфера. - position
: индекс следующего элемента для чтения или записи. - mark
: сохраненная позиция. - Основные операции: - put()
: запись данных в буфер. - get()
: чтение данных из буфера. - flip()
: переключает буфер из режима записи в режим чтения. limit
устанавливается на текущую position
, а position
сбрасывается в 0. - clear()
: переключает буфер из режима чтения в режим записи. position
сбрасывается в 0, limit
устанавливается на capacity
. Данные не стираются, они будут перезаписаны. - rewind()
: сбрасывает position
в 0, чтобы перечитать данные из буфера.
2. Каналы (Channels) - java.nio.channels.Channel
и его потомки. - Канал — это абстракция, представляющая собой “трубу” для передачи данных между источником (файл, сокет) и буфером. - Каналы работают двунаправленно (если позволяет источник), в отличие от потоков I/O, которые однонаправленные (InputStream
/OutputStream
). - Основные операции: read(ByteBuffer)
и write(ByteBuffer)
.
FileChannel
- Это реализация канала для работы с файлами. - Позволяет читать и записывать данные в файл. - Получить FileChannel
можно из FileInputStream
, FileOutputStream
или RandomAccessFile
.
FileInputStream fis = new FileInputStream("file.txt");
FileChannel fileChannel = fis.getChannel();
Пример чтения файла с помощью FileChannel
и ByteBuffer
:
try (RandomAccessFile file = new RandomAccessFile("data.txt", "rw");
FileChannel channel = file.getChannel()) {
// 1. Запись в файл
String data = "Hello, NIO!";
ByteBuffer writeBuffer = ByteBuffer.allocate(48);
writeBuffer.put(data.getBytes());
writeBuffer.flip(); // Готовим буфер к записи из него в канал
while (writeBuffer.hasRemaining()) {
channel.write(writeBuffer);
}
// Перемещаем позицию в начало файла для чтения
channel.position(0);
// 2. Чтение из файла
ByteBuffer readBuffer = ByteBuffer.allocate(48);
int bytesRead = channel.read(readBuffer); // Читаем из канала в буфер
while (bytesRead != -1) {
readBuffer.flip(); // Готовим буфер к чтению из него
while (readBuffer.hasRemaining()) {
System.out.print((char) readBuffer.get());
}
readBuffer.clear(); // Готовим буфер к новой записи из канала
bytesRead = channel.read(readBuffer);
}
} catch (IOException e) {
e.printStackTrace();
}
Преимущества NIO: - Неблокирующий ввод-вывод: Позволяет одному потоку управлять несколькими каналами (с помощью Selector
), что критично для высокопроизводительных серверов. - Прямые буферы: Возможность выделять память вне Java-кучи, что позволяет ОС выполнять I/O операции напрямую, минуя копирование данных. - Memory-mapped files: Отображение файла или его части в память для очень быстрого доступа.
JDBC и Работа с Базами Данных
19. Взаимодействие с базами данных. Протокол JDBC. Основные элементы.
JDBC (Java Database Connectivity) — это стандартный Java API для взаимодействия с реляционными базами данных. Важно понимать, что JDBC — это не протокол, а спецификация (API), которая определяет набор классов и интерфейсов. Производители баз данных (PostgreSQL, Oracle, MySQL и др.) предоставляют конкретные реализации этого API в виде JDBC-драйверов.
Цель JDBC — предоставить универсальный, платформо- и СУБД-независимый способ для Java-приложений выполнять SQL-запросы к базе данных.
Основные элементы (компоненты) JDBC:
Driver
: Интерфейс, который должен быть реализован производителем СУБД. Драйвер — это “переводчик” между вызовами JDBC API и специфическим протоколом конкретной базы данных. Обычно драйвер регистрируется автоматически (с помощью механизма ServiceLoader) или вручную (Class.forName("org.postgresql.Driver")
- устаревший способ).DriverManager
: Класс, который управляет набором доступных JDBC-драйверов. Его основная задача — установить соединение с базой данных на основе URL-адреса подключения. Он находит подходящий драйвер для указанного URL и использует его для создания соединения.Connection
: Интерфейс, представляющий сессию (соединение) с базой данных. Все SQL-запросы выполняются в контекстеConnection
. Через него мы создаем объекты для выполнения запросов.Statement
: Интерфейс, используемый для выполнения статичных SQL-запросов.PreparedStatement
: РасширениеStatement
, используемое для выполнения параметризованных (заранее скомпилированных) запросов. Это предпочтительный способ, так как он более производительный и защищает от SQL-инъекций.ResultSet
: Интерфейс, представляющий результат выполнения SQL-запроса (SELECT
). Он содержит набор строк, полученных из базы данных, и предоставляет методы для навигации по этим строкам и извлечения данных из колонок.
Схема взаимодействия: Приложение -> DriverManager
-> JDBC Driver -> База данных
20. Создание соединения с базой данных. Класс DriverManager. Интерфейс Connection.
Класс DriverManager
- Это “точка входа” в JDBC. Он отвечает за управление списком зарегистрированных драйверов и установку соединения с базой данных. - Основной метод — getConnection()
.
Создание соединения: Соединение устанавливается с помощью статического метода DriverManager.getConnection()
. Он имеет три основные перегруженные версии: - getConnection(String url)
- getConnection(String url, String user, String password)
- getConnection(String url, Properties info)
JDBC URL — это строка, которая уникально идентифицирует базу данных. Ее формат: jdbc:<subprotocol>:<subname>
- jdbc:
— стандартный префикс. - <subprotocol>
— название драйвера (например, postgresql
, mysql
). - <subname>
— информация для подключения, специфичная для драйвера (адрес сервера, порт, имя БД).
Пример для PostgreSQL: jdbc:postgresql://localhost:5432/mydatabase
Интерфейс Connection
- Представляет собой активное соединение с базой данных. В рамках одного соединения можно выполнять несколько запросов и управлять транзакциями. - Ключевые методы: - createStatement()
: Создает объект Statement
. - prepareStatement(String sql)
: Создает объект PreparedStatement
. - prepareCall(String sql)
: Создает объект CallableStatement
(для вызова хранимых процедур). - close()
: Закрывает соединение и освобождает ресурсы. Крайне важно всегда закрывать соединение! - Управление транзакциями: - setAutoCommit(boolean autoCommit)
: Включает/выключает режим автокоммита. По умолчанию true
(каждый запрос — отдельная транзакция). Для ручного управления транзакциями устанавливают в false
. - commit()
: Фиксирует все изменения, сделанные в текущей транзакции. - rollback()
: Откатывает все изменения в текущей транзакции.
Использование try-with-resources
(предпочтительный способ): Этот синтаксис гарантирует, что Connection
будет автоматически закрыт, даже если произойдет исключение.
String url = "jdbc:postgresql://localhost:5432/mydatabase";
String user = "user";
String password = "password";
try (Connection connection = DriverManager.getConnection(url, user, password)) {
// Работа с базой данных...
System.out.println("Соединение установлено!");
} catch (SQLException e) {
System.err.println("Ошибка подключения: " + e.getMessage());
}
21. Создание запросов. Интерфейсы Statement, PreparedStatement, CallableStatement.
После получения объекта Connection
, мы используем его для создания объектов-запросов.
1. Statement
- Используется для выполнения простых SQL-запросов без параметров. - Запрос передается в виде строки непосредственно в метод выполнения (executeQuery
, executeUpdate
). - Основной недостаток: Уязвим для SQL-инъекций, так как строковые переменные конкатенируются с SQL-кодом. Использовать с большой осторожностью!
try (Statement stmt = connection.createStatement()) {
String unsafeInput = "'admin' OR '1'='1'";
// УЯЗВИМЫЙ КОД!
ResultSet rs = stmt.executeQuery("SELECT * FROM users WHERE name = " + unsafeInput);
}
2. PreparedStatement
- Расширяет Statement
и является предпочтительным способом выполнения запросов. - SQL-запрос предварительно компилируется базой данных с плейсхолдерами (?
) вместо конкретных значений. - Значения для параметров устанавливаются с помощью set*()
методов (setString()
, setInt()
, setDate()
и т.д.). - Преимущества: - Защита от SQL-инъекций: Драйвер сам экранирует передаваемые значения, и они никогда не интерпретируются как часть SQL-кода. - Высокая производительность: Если один и тот же запрос выполняется многократно с разными параметрами, СУБД может кэшировать план выполнения запроса.
String sql = "SELECT * FROM users WHERE name = ? AND age > ?";
try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
pstmt.setString(1, "John"); // Устанавливаем значение для первого '?'
pstmt.setInt(2, 30); // Устанавливаем значение для второго '?'
ResultSet rs = pstmt.executeQuery();
// ... обработка rs
}
3. CallableStatement
- Расширяет PreparedStatement
. - Предназначен специально для вызова хранимых процедур в базе данных. - Поддерживает не только входные (IN
), но и выходные (OUT
) и комбинированные (INOUT
) параметры. - Синтаксис вызова: {call procedure_name(?, ?, ?)}
.
String sql = "{call get_user_by_id(?, ?)}";
try (CallableStatement cstmt = connection.prepareCall(sql)) {
cstmt.setInt(1, 123); // Входной параметр (IN)
cstmt.registerOutParameter(2, Types.VARCHAR); // Регистрируем выходной параметр (OUT)
cstmt.execute();
String userName = cstmt.getString(2); // Получаем результат из OUT-параметра
System.out.println("Имя пользователя: " + userName);
}
22. Выполнение запросов. Методы execute, executeQuery, executeUpdate.
У интерфейсов Statement
и PreparedStatement
есть три основных метода для выполнения SQL-кода.
1. ResultSet executeQuery(String sql)
(для Statement
) или ResultSet executeQuery()
(для PreparedStatement
) - Используется исключительно для SELECT
-запросов, то есть для запросов, которые возвращают данные. - Возвращает объект ResultSet
, содержащий результаты запроса. - Если попытаться выполнить этим методом INSERT
или UPDATE
, будет брошено исключение SQLException
.
PreparedStatement pstmt = connection.prepareStatement("SELECT name, email FROM clients");
ResultSet rs = pstmt.executeQuery();
2. int executeUpdate(String sql)
(для Statement
) или int executeUpdate()
(для PreparedStatement
) - Используется для выполнения DML-операторов (Data Manipulation Language): INSERT
, UPDATE
, DELETE
. - Также используется для DDL-операторов (Data Definition Language): CREATE TABLE
, ALTER TABLE
, DROP TABLE
. - Возвращает int
: - Для DML-операторов — количество измененных строк. - Для DDL-операторов — 0.
PreparedStatement pstmt = connection.prepareStatement("UPDATE products SET price = ? WHERE id = ?");
pstmt.setDouble(1, 99.99);
pstmt.setInt(2, 5);
int rowsAffected = pstmt.executeUpdate(); // вернет 1, если продукт с id=5 существует
System.out.println("Обновлено строк: " + rowsAffected);
3. boolean execute(String sql)
(для Statement
) или boolean execute()
(для PreparedStatement
) - Наиболее универсальный метод, который может выполнить любой тип SQL-запроса. - Используется, когда тип запроса заранее неизвестен или когда хранимая процедура может возвращать несколько результатов (и наборы данных, и количество обновленных строк). - Возвращает boolean
: - true
— если первый результат является ResultSet
. - false
— если первый результат является количеством обновленных строк или результатов нет. - После вызова execute()
нужно использовать методы getResultSet()
или getUpdateCount()
для получения конкретного результата.
boolean isResultSet = statement.execute(anySqlString);
if (isResultSet) {
ResultSet rs = statement.getResultSet();
// ... обработка ResultSet
} else {
int updateCount = statement.getUpdateCount();
// ... обработка количества измененных строк
}
23. Обработка результатов запроса. Интерфейс ResultSet, получение значений.
Интерфейс ResultSet
- Представляет собой таблицу данных, сгенерированную SELECT
-запросом. - ResultSet
поддерживает курсор, который указывает на текущую строку данных. Изначально курсор находится перед первой строкой.
Основной цикл обработки ResultSet
: - Метод boolean next()
сдвигает курсор на следующую строку. Он возвращает true
, если следующая строка существует, и false
, если все строки пройдены. - Стандартный паттерн обработки — это цикл while
.
try (Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery("SELECT id, name, price FROM products")) {
// Цикл по всем строкам результата
while (rs.next()) {
// Извлечение данных из текущей строки
int id = rs.getInt("id");
String name = rs.getString("name");
double price = rs.getDouble("price");
System.out.printf("ID: %d, Name: %s, Price: %.2f%n", id, name, price);
}
} // rs и stmt будут автоматически закрыты здесь
Получение значений из колонок: - Для каждого типа данных существует свой get*()
метод: getInt()
, getString()
, getDouble()
, getDate()
, getBoolean()
и т.д. - Обратиться к колонке можно двумя способами: 1. По индексу колонки (начиная с 1): rs.getInt(1)
. Быстро, но хрупко: если порядок колонок в SELECT
изменится, код сломается. 2. По имени колонки (регистронезависимо): rs.getInt("id")
. Предпочтительный способ, так как он более читаемый и устойчив к изменениям порядка колонок.
Важные моменты: - ResultSet
, Statement
, Connection
— это ресурсы, которые необходимо закрывать. Использование try-with-resources
— лучший способ гарантировать их закрытие. - Вызывать get*()
методы можно только для текущей строки, на которую указывает курсор (т.е. после успешного вызова next()
). - По умолчанию ResultSet
является TYPE_FORWARD_ONLY
, то есть по нему можно двигаться только вперед. Существуют и другие типы (TYPE_SCROLL_INSENSITIVE
, TYPE_SCROLL_SENSITIVE
), которые позволяют двигаться в обе стороны, но они используются реже и не всегда поддерживаются драйверами.
Многопоточность (Concurrency)
24. Многопоточность. Класс Thread и интерфейс Runnable. Состояния потока.
Многопоточность (Multithreading) — это способность программы выполнять несколько задач (потоков) одновременно в рамках одного процесса. Это позволяет более эффективно использовать ресурсы процессора, особенно на многоядерных системах, и создавать отзывчивые пользовательские интерфейсы.
Способы создания потока:
- Наследование от класса
Thread
:- Создается класс, который наследуется от
java.lang.Thread
. - Переопределяется метод
run()
, в котором содержится код, который должен выполняться в новом потоке. - Недостаток: Java не поддерживает множественное наследование, поэтому если ваш класс уже от чего-то наследуется, этот способ не подойдет.
class MyThread extends Thread { public void run() { System.out.println("Поток, созданный наследованием, запущен."); } } // Запуск: MyThread t = new MyThread(); t.start(); // !! Вызывать нужно start(), а не run()
- Создается класс, который наследуется от
- Реализация интерфейса
Runnable
(предпочтительный способ):- Создается класс, который реализует интерфейс
java.lang.Runnable
. - Интерфейс содержит один метод
run()
. - Экземпляр этого класса передается в конструктор класса
Thread
. - Преимущество: Позволяет отделить задачу (что делать) от механизма ее выполнения (поток). Более гибкий подход.
class MyRunnable implements Runnable { public void run() { System.out.println("Поток, созданный через Runnable, запущен."); } } // Запуск: Thread t = new Thread(new MyRunnable()); t.start();
- Создается класс, который реализует интерфейс
Состояния (жизненный цикл) потока:
NEW
: Поток создан (new Thread()
), но методstart()
еще не вызван.RUNNABLE
: Поток готов к выполнению и ожидает, когда планировщик потоков выделит ему процессорное время. Это состояние включает как “готовый к запуску”, так и “выполняющийся”.BLOCKED
: Поток заблокирован и ожидает освобождения монитора (замка), который захвачен другим потоком (например, при входе вsynchronized
блок).WAITING
: Поток находится в состоянии ожидания на неопределенный срок. Он ждет, пока другой поток его “разбудит” с помощьюnotify()
илиnotifyAll()
. Также в это состояние поток переходит после вызоваjoin()
без таймаута.TIMED_WAITING
: Поток находится в состоянии ожидания в течение определенного времени. Это происходит после вызововThread.sleep(long)
,wait(long)
,join(long)
.TERMINATED
: Методrun()
завершил свое выполнение (нормально или из-за исключения), и поток прекратил свою работу.
25. Гонки. Синхронизация потоков. Мониторы. Модификатор synchronized.
Состояние гонки (Race Condition) - Это ошибка проектирования многопоточной системы, при которой результат работы программы зависит от того, в какой последовательности выполняются потоки. - Возникает, когда два или более потока одновременно обращаются к общему изменяемому ресурсу (переменной, объекту, файлу), и хотя бы один из них этот ресурс изменяет. - Классический пример — инкремент счетчика (counter++
), который не является атомарной операцией (чтение-изменение-запись).
Синхронизация потоков - Это механизм, который гарантирует, что только один поток в определенный момент времени может получить доступ к критической секции (участку кода, работающему с общим ресурсом). - Основная цель — обеспечить целостность данных и избежать состояния гонки.
Монитор (Monitor) - Высокоуровневый механизм синхронизации. В Java каждый объект имеет связанный с ним монитор (также называемый intrinsic lock или mutex). - Монитор позволяет только одному потоку “владеть” им в любой момент времени. Если поток хочет выполнить код, защищенный монитором, он должен сначала “захватить” монитор. Если монитор уже занят другим потоком, текущий поток блокируется (BLOCKED
) и ждет его освобождения.
Модификатор synchronized
- Это основной способ использования мониторов в Java. Он может применяться к методам или блокам кода.
- Синхронизированный метод:
- Когда
synchronized
применяется к методу экземпляра, поток должен захватить монитор объекта (this
), чтобы выполнить этот метод. - Когда применяется к статическому методу, поток должен захватить монитор объекта
Class
, соответствующего этому классу.
public class Counter { private int count = 0; // Только один поток может выполнять этот метод для данного экземпляра Counter public synchronized void increment() { count++; } }
- Когда
- Синхронизированный блок:
- Позволяет синхронизировать доступ к любому объекту, а не только к
this
. - Обеспечивает более гранулярный контроль, позволяя защищать только критическую часть кода, а не весь метод.
public class SomeClass { private final Object lock = new Object(); // Специальный объект для блокировки public void doWork() { // ... какой-то код, не требующий синхронизации synchronized (lock) { // Захват монитора объекта 'lock' // Критическая секция: код, который должен выполняться атомарно } // ... другой код } }
- Позволяет синхронизировать доступ к любому объекту, а не только к
26. Многопоточность. Интерфейсы Executor, ExecutorService, Callable, Future
Эти интерфейсы являются частью Executor Framework (java.util.concurrent
), который предоставляет высокоуровневый и гибкий подход к управлению потоками, абстрагируя создание и управление потоками от самих задач.
Executor
- Простой интерфейс с одним методом: void execute(Runnable command)
. - Основная идея — отделить задачу (что сделать, Runnable
) от исполнителя (как и где это сделать). - Вы просто передаете задачу исполнителю, а он уже решает, как ее запустить: в новом потоке, в существующем потоке из пула и т.д.
ExecutorService
- Расширяет Executor
и добавляет важные функции для управления жизненным циклом исполнителя и обработки задач, возвращающих результат. - Основные методы: - Future<?> submit(Runnable task)
: Отправляет Runnable
на выполнение и возвращает Future
для отслеживания. - Future<T> submit(Callable<T> task)
: Отправляет Callable
на выполнение. - void shutdown()
: “Мягкое” завершение. ExecutorService
перестает принимать новые задачи, но выполняет уже отправленные. - List<Runnable> shutdownNow()
: “Жесткое” завершение. Пытается остановить все активные задачи и возвращает список невыполненных.
Callable<V>
- Аналог Runnable
, но предназначен для задач, которые возвращают результат и могут бросать проверяемое исключение. - Имеет один метод V call() throws Exception
. - Параметр V
— это тип возвращаемого значения.
Callable<Integer> task = () -> {
TimeUnit.SECONDS.sleep(1);
return 123; // Возвращаем результат
};
Future<V>
- Представляет результат асинхронного вычисления. - Когда вы отправляете Callable
в ExecutorService
, вы немедленно получаете Future
в ответ. Основной поток может продолжать работать, пока задача выполняется в другом потоке. - Основные методы: - V get()
: Блокирующий метод. Ожидает завершения задачи и возвращает ее результат. Если задача бросила исключение, get()
перебросит его как ExecutionException
. - V get(long timeout, TimeUnit unit)
: То же, что и get()
, но с таймаутом. - boolean isDone()
: Проверяет, завершена ли задача. - boolean cancel(boolean mayInterruptIfRunning)
: Пытается отменить выполнение задачи.
Пример:
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<String> callableTask = () -> {
System.out.println("Задача выполняется...");
Thread.sleep(2000);
return "Результат задачи";
};
Future<String> future = executor.submit(callableTask);
System.out.println("Задача отправлена, основной поток продолжает работу.");
// ... можно делать другую работу ...
try {
// Блокируемся и ждем результат
String result = future.get();
System.out.println("Получен результат: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
executor.shutdown();
27. Класс Executors и интерфейс ExecutorService. Пулы потоков.
Пул потоков (Thread Pool) — это набор заранее созданных, готовых к работе потоков, которыми управляет ExecutorService
. Использование пула потоков является предпочтительным подходом в многопоточном программировании по сравнению с созданием нового потока для каждой задачи (new Thread(...)
).
Преимущества пула потоков: 1. Снижение накладных расходов: Создание и уничтожение потоков — ресурсоемкие операции. Пул позволяет переиспользовать существующие потоки для новых задач. 2. Управление ресурсами: Пул ограничивает максимальное количество одновременно работающих потоков, предотвращая исчерпание системных ресурсов (памяти, процессорного времени). 3. Упрощение управления: Предоставляет готовые механизмы для запуска, контроля и завершения задач.
Класс Executors
- Это фабричный класс, который предоставляет статические методы для создания различных типов ExecutorService
(и, соответственно, пулов потоков).
Основные типы пулов, создаваемых через Executors
:
newFixedThreadPool(int nThreads)
- Создает пул с фиксированным количеством потоков.
- Если все потоки заняты, новые задачи помещаются в неограниченную очередь и ждут, пока какой-нибудь поток освободится.
- Применение: Когда нужно ограничить количество одновременных задач для контроля нагрузки (например, по числу ядер процессора).
newCachedThreadPool()
- Создает гибкий пул, который создает новые потоки по мере необходимости и переиспользует ранее созданные.
- Если поток простаивает 60 секунд, он удаляется из пула.
- Потенциальная проблема: Если задачи поступают очень быстро, пул может неограниченно расти, что приведет к нехватке ресурсов.
- Применение: Для большого количества короткоживущих задач.
newSingleThreadExecutor()
- Создает исполнителя с одним-единственным потоком.
- Гарантирует, что все задачи будут выполняться последовательно, в том порядке, в котором они были отправлены.
- Применение: Когда требуется гарантировать последовательное выполнение асинхронных задач.
newScheduledThreadPool(int corePoolSize)
- Создает пул, который может выполнять задачи по расписанию: с задержкой или периодически.
- Возвращает
ScheduledExecutorService
, который расширяетExecutorService
методамиschedule()
,scheduleAtFixedRate()
,scheduleWithFixedDelay()
.
Жизненный цикл ExecutorService
: 1. Создание: ExecutorService executor = Executors.newFixedThreadPool(4);
2. Отправка задач: executor.submit(task);
3. Завершение: - executor.shutdown();
— инициирует graceful shutdown. Новые задачи не принимаются, старые дорабатываются. - executor.awaitTermination(long timeout, TimeUnit unit);
— блокирует текущий поток до тех пор, пока все задачи не завершатся после вызова shutdown
, или пока не истечет таймаут.
// Правильный шаблон завершения работы
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow(); // Отменяем ожидающие задачи
}
} catch (InterruptedException ex) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
28. Модель памяти, кэширование. Модификатор volatile и условие “happens-before”.
Модель Памяти Java (Java Memory Model, JMM) - JMM — это часть спецификации Java, которая определяет, как потоки взаимодействуют через общую память. Она описывает правила, по которым изменения переменных, сделанные одним потоком, становятся видимыми для других потоков.
Проблема кэширования: - Для повышения производительности современные процессоры имеют несколько уровней кэш-памяти. Каждый поток, выполняющийся на своем ядре, может кэшировать значения переменных из основной памяти. - Это создает проблему видимости: один поток может изменить значение переменной в своем кэше, но это изменение не будет сразу же записано в основную память и, следовательно, не будет видно другим потокам.
Модификатор volatile
- volatile
— это ключевое слово, которое можно применить к полю. Оно предоставляет две важные гарантии:
- Видимость (Visibility):
- Любая запись в
volatile
переменную немедленно сбрасывается из кэша в основную память. - Любое чтение
volatile
переменной всегда происходит напрямую из основной памяти, а не из локального кэша. - Это гарантирует, что все потоки всегда видят самое актуальное значение
volatile
переменной.
- Любая запись в
- Упорядочивание (Ordering) / Отношение “happens-before”:
- JMM запрещает переупорядочивание инструкций компилятором и процессором, которое могло бы нарушить “happens-before” отношение, установленное
volatile
. - Запись в
volatile
переменную устанавливает отношение happens-before со всеми последующими чтениями этой же переменной. Это означает, что все изменения памяти, которые были видны потоку до записи вvolatile
поле, станут видны любому другому потоку после того, как он прочитает это поле.
- JMM запрещает переупорядочивание инструкций компилятором и процессором, которое могло бы нарушить “happens-before” отношение, установленное
Важно: volatile
не обеспечивает атомарность для сложных операций. Например, операция count++
для volatile int count
не является атомарной. Она состоит из трех шагов (чтение, изменение, запись), и между ними может вклиниться другой поток. Для атомарных операций нужно использовать synchronized
или классы из java.util.concurrent.atomic
.
Отношение “happens-before” - Это фундаментальная концепция в JMM, которая определяет строгий частичный порядок всех действий в программе. Если действие A happens-before действие B, то результат действия A гарантированно виден и предшествует действию B. - Основные правила happens-before: - Действия в одном потоке упорядочены в порядке их написания в коде. - Освобождение монитора (unlock
) happens-before последующего захвата (lock
) того же монитора. - Запись в volatile
переменную happens-before каждого последующего чтения этой же переменной. - Вызов start()
на потоке happens-before любого действия в этом новом потоке. - Все действия в потоке happens-before успешного возврата из join()
на этом потоке.
29. Взаимодействие потоков. Ожидание и нотификация. Методы wait(), notify(), notifyAll().
Эти три метода, определенные в классе Object
, являются низкоуровневым механизмом для координации потоков. Они позволяют одному потоку приостановить свое выполнение (wait
) до тех пор, пока другой поток не уведомит его о том, что некоторое условие изменилось (notify
/notifyAll
).
Важное правило: Методы wait()
, notify()
, notifyAll()
могут быть вызваны только из synchronized
блока или метода на том объекте, чей монитор захвачен текущим потоком. В противном случае будет брошено IllegalMonitorStateException
.
void wait()
- Вызвавший этот метод поток освобождает монитор объекта. - Переходит в состояние WAITING
(или TIMED_WAITING
для wait(long timeout)
). - Поток “засыпает” и ждет, пока другой поток вызовет notify()
или notifyAll()
на этом же объекте. - Когда поток “просыпается”, он не продолжает выполнение немедленно, а снова пытается захватить монитор. Он будет ждать, пока монитор освободится, и только потом продолжит работу.
void notify()
- “Будит” один случайный поток, который ожидает на мониторе этого объекта (находится в WAITING
состоянии после вызова wait()
). - Выбор потока зависит от реализации JVM и не является предсказуемым.
void notifyAll()
- “Будит” все потоки, которые ожидают на мониторе этого объекта. - Они все переходят из WAITING
в BLOCKED
и начинают соревноваться за захват монитора.
Шаблон “Guarded Blocks” (Защищенные блоки) - Из-за проблемы “спонтанного пробуждения” (spurious wakeup
), когда поток может проснуться без вызова notify
, wait()
всегда должен вызываться внутри цикла while
, который проверяет условие ожидания.
Классический пример: Производитель-Потребитель (Producer-Consumer)
public class SharedQueue {
private final Queue<Integer> queue = new LinkedList<>();
private final int CAPACITY = 5;
private final Object lock = new Object();
public void produce(int item) throws InterruptedException {
synchronized (lock) {
// Ждем, пока в очереди есть место
while (queue.size() == CAPACITY) {
System.out.println("Очередь полна, производитель ждет.");
lock.wait(); // Освобождаем монитор и ждем
}
queue.add(item);
System.out.println("Производитель добавил: " + item);
lock.notifyAll(); // Уведомляем потребителей, что появился товар
}
}
public int consume() throws InterruptedException {
synchronized (lock) {
// Ждем, пока в очереди появится товар
while (queue.isEmpty()) {
System.out.println("Очередь пуста, потребитель ждет.");
lock.wait(); // Освобождаем монитор и ждем
}
int item = queue.poll();
System.out.println("Потребитель забрал: " + item);
lock.notifyAll(); // Уведомляем производителей, что появилось место
return item;
}
}
}
Использование notifyAll()
является более безопасным, чем notify()
, так как предотвращает ситуации, когда “не тот” поток просыпается и система входит в дедлок.
30. Интерфейсы Lock, ReadWriteLock, Condition и реализующие их классы.
Пакет java.util.concurrent.locks
предоставляет более гибкие и мощные механизмы блокировки, чем встроенный synchronized
.
Интерфейс Lock
- Представляет собой объект блокировки. Основная реализация — java.util.concurrent.locks.ReentrantLock
. - Основные методы: - void lock()
: Захватывает блокировку. Если она занята, текущий поток блокируется до ее освобождения. - void unlock()
: Освобождает блокировку. - boolean tryLock()
: Пытается захватить блокировку. Возвращает true
, если удалось, и false
, если она уже занята (не блокируется). - boolean tryLock(long time, TimeUnit unit)
: Пытается захватить блокировку в течение указанного времени. - Ключевой паттерн использования: unlock()
всегда должен вызываться в блоке finally
, чтобы гарантировать освобождение блокировки даже при возникновении исключения.
Lock lock = new ReentrantLock();
lock.lock();
try {
// ... критическая секция ...
} finally {
lock.unlock(); // Гарантированное освобождение
}
Преимущества ReentrantLock
над synchronized
: - Возможность прерывания: Поток, ожидающий блокировку, может быть прерван (lockInterruptibly()
). - Попытка захвата с таймаутом: tryLock()
позволяет избежать дедлоков. - Честность (Fairness): Конструктор ReentrantLock(boolean fair)
позволяет создать “честную” блокировку, которая отдает доступ самому долго ждущему потоку (снижает производительность). - Возможность использования нескольких Condition
.
Интерфейс ReadWriteLock
- Основная реализация — ReentrantReadWriteLock
. - Предоставляет пару блокировок: одну для чтения (readLock()
) и одну для записи (writeLock()
). - Правила работы: - Несколько потоков могут одновременно удерживать блокировку на чтение, если нет писателей. - Блокировка на запись является эксклюзивной: пока она удерживается, ни один другой поток (ни читатель, ни писатель) не может получить доступ. - Применение: Идеально подходит для структур данных, которые читаются намного чаще, чем изменяются. Это значительно повышает параллелизм.
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
// Чтение
readLock.lock();
try { /* ... чтение данных ... */ } finally { readLock.unlock(); }
// Запись
writeLock.lock();
try { /* ... изменение данных ... */ } finally { writeLock.unlock(); }
Интерфейс Condition
- Представляет собой “условие”, связанное с Lock
. Это более гибкая и мощная замена механизму wait
/notify
/notifyAll
. - Один Lock
может иметь несколько объектов Condition
. - Объект Condition
получается вызовом lock.newCondition()
. - Основные методы: - await()
: аналог wait()
. - signal()
: аналог notify()
. - signalAll()
: аналог notifyAll()
. - Это позволяет создавать более сложные сценарии координации, например, в задаче “Производитель-Потребитель” можно создать два условия: notFull
(для производителей) и notEmpty
(для потребителей).
31. Атомарные операции. Пакет java.util.concurrent.atomic. Класс AtomicInteger.
Атомарная операция — это операция, которая выполняется как единое, неделимое целое. Никакой другой поток не может “вклиниться” в середину атомарной операции и увидеть промежуточное состояние.
Операция i++
не является атомарной. Она состоит из трех шагов: 1. Прочитать текущее значение i
. 2. Увеличить его на 1. 3. Записать новое значение обратно в i
.
Пакет java.util.concurrent.atomic
предоставляет классы, которые поддерживают атомарные операции над примитивами и ссылками без использования блокировок.
Механизм Compare-And-Swap (CAS) - В основе атомарных классов лежит низкоуровневая, аппаратно-поддерживаемая инструкция CAS. - CAS — это операция, которая принимает три аргумента: адрес в памяти (V), ожидаемое старое значение (A) и новое значение (B). Она атомарно обновляет значение в V на B, только если текущее значение в V равно A. В случае успеха она возвращает true
. - Атомики используют CAS в цикле: они читают значение, вычисляют новое, а затем пытаются обновить его с помощью CAS. Если CAS не удался (потому что другой поток успел изменить значение), операция повторяется с новым прочитанным значением. Это называется lock-free или non-blocking алгоритмом.
Класс AtomicInteger
- Предоставляет int
значение, которое можно изменять атомарно. - Основные методы: - get()
: получает текущее значение. - set(int newValue)
: устанавливает новое значение. - getAndIncrement()
: атомарно увеличивает значение на 1 и возвращает старое значение (аналог i++
). - incrementAndGet()
: атомарно увеличивает значение на 1 и возвращает новое значение (аналог ++i
). - addAndGet(int delta)
: атомарно добавляет delta
и возвращает новое значение. - boolean compareAndSet(int expect, int update)
: основной метод CAS.
Пример использования:
// Вместо synchronized
public class UnsafeCounter {
private int count = 0;
public void increment() { count++; }
}
// Используем атомик
public class SafeCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int get() {
return count.get();
}
}
Преимущества атомиков: - В условиях низкой и средней конкуренции они работают значительно быстрее, чем блокировки, так как не приводят к блокировке и приостановке потоков.
Другие атомарные классы: AtomicLong
, AtomicBoolean
, AtomicReference
(для атомарного обновления объектных ссылок), AtomicIntegerArray
и др.
32. Потокобезопасные коллекции. Synchronized- и Concurrent-коллекции.
Стандартные коллекции (ArrayList
, HashMap
и т.д.) не являются потокобезопасными. Их использование в многопоточной среде без внешней синхронизации может привести к повреждению данных или ConcurrentModificationException
.
Существует два основных подхода к созданию потокобезопасных коллекций:
1. Синхронизированные обертки (Synchronized Collections) - Создаются с помощью статических методов класса Collections
: - Collections.synchronizedList(new ArrayList<>())
- Collections.synchronizedMap(new HashMap<>())
- Collections.synchronizedSet(new HashSet<>())
- Как работают: Каждый метод обертки просто делегирует вызов оригинальной коллекции, заключая его в synchronized (this)
блок. - Проблемы: - Низкая производительность: Весь доступ к коллекции (чтение или запись) сериализуется, так как блокируется вся коллекция. При высокой конкуренции это создает “бутылочное горлышко”. - Составные операции не атомарны: Операции типа “проверить и добавить” (if (!list.contains(x)) { list.add(x); }
) требуют дополнительной внешней синхронизации. - Итерация требует ручной блокировки: Итерация по такой коллекции должна быть обернута в synchronized
блок, иначе возможна ConcurrentModificationException
.
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// ...
// Правильная итерация
synchronized (syncList) {
for (String s : syncList) {
System.out.println(s);
}
}
2. Конкурентные коллекции (Concurrent Collections) - Находятся в пакете java.util.concurrent
. - Разработаны с нуля для эффективной работы в многопоточной среде. Они используют более продвинутые техники, чем простая блокировка всей коллекции. - Предпочтительны для использования в многопоточных приложениях. - Основные классы: - ConcurrentHashMap
: Высокопроизводительная, потокобезопасная реализация Map
. Использует сегментацию (lock-striping) — блокирует не всю карту, а только ее часть (сегмент), что позволяет нескольким потокам одновременно изменять разные части карты. Итераторы являются “слабо согласованными” (weakly consistent
) и не бросают ConcurrentModificationException
. - CopyOnWriteArrayList
: Потокобезопасная реализация List
. Любая операция модификации (add
, set
, remove
) создает полную копию базового массива. - Плюсы: Чтение очень быстрое и не требует блокировок. Идеально для ситуаций, где чтений на порядки больше, чем записей. - Минусы: Запись очень медленная и затратная по памяти. - BlockingQueue<E>
(и ее реализации LinkedBlockingQueue
, ArrayBlockingQueue
, PriorityBlockingQueue
): Потокобезопасные очереди, которые блокируют поток при попытке добавить элемент в полную очередь (put
) или забрать элемент из пустой очереди (take
). Незаменимы для реализации шаблона “Производитель-Потребитель”.
Функциональное программирование и Stream API
33. Функциональные интерфейсы и λ-выражения. Интерфейсы пакета java.util.function.
Функциональный интерфейс — это интерфейс, который содержит ровно один абстрактный метод. Он может содержать любое количество default
или static
методов. - Аннотация @FunctionalInterface
не является обязательной, но рекомендуется. Она заставляет компилятор проверять, что интерфейс действительно является функциональным.
Лямбда-выражение (λ-выражение) - Это краткая, анонимная реализация функционального интерфейса. - Позволяет рассматривать функцию как объект: передавать ее в качестве аргумента в метод, возвращать из метода, хранить в переменной. - Синтаксис: (параметры) -> { тело_выражения }
- ()
: если параметров нет. - (p1, p2)
: если несколько параметров. - p
: если один параметр, скобки можно опустить. - Если тело состоит из одного выражения, фигурные скобки и return
можно опустить.
Пример:
// Функциональный интерфейс
@FunctionalInterface
interface MyOperation {
int operate(int a, int b);
}
// Реализация через анонимный класс (старый способ)
MyOperation additionOld = new MyOperation() {
@Override
public int operate(int a, int b) {
return a + b;
}
};
// Реализация через лямбда-выражение
MyOperation additionNew = (a, b) -> a + b;
System.out.println(additionNew.operate(5, 3)); // 8
Пакет java.util.function
- Содержит набор стандартных, предопределенных функциональных интерфейсов, которые покрывают большинство типичных сценариев. Их следует использовать вместо создания своих собственных. - Основные интерфейсы: - Predicate<T>
: - Метод: boolean test(T t)
- Принимает один аргумент, возвращает boolean
. (Проверяет условие). - Пример: s -> s.length() > 5
- Function<T, R>
: - Метод: R apply(T t)
- Принимает аргумент типа T
, возвращает результат типа R
. (Преобразование). - Пример: s -> s.length()
- Consumer<T>
: - Метод: void accept(T t)
- Принимает аргумент, ничего не возвращает. (Выполняет действие). - Пример: s -> System.out.println(s)
- Supplier<T>
: - Метод: T get()
- Не принимает аргументов, возвращает результат. (Поставщик). - Пример: () -> new ArrayList<>()
- UnaryOperator<T>
: - Расширяет Function<T, T>
. - Принимает и возвращает значение одного типа. (Унарная операция). - Пример: n -> n * 2
- BinaryOperator<T>
: - Расширяет BiFunction<T, T, T>
. - Принимает два аргумента одного типа и возвращает результат того же типа. (Бинарная операция). - Пример: (a, b) -> a + b
Функциональное программирование, Stream API и другие темы
34. Пакет java.time. Классы для представления даты и времени.
Пакет java.time
, представленный в Java 8, является современной и значительно улучшенной заменой старых классов java.util.Date
и java.util.Calendar
.
Ключевые принципы java.time
API: - Неизменяемость (Immutability): Все классы в java.time
являются неизменяемыми. Любая операция модификации (например, добавление дня) возвращает новый объект, а не изменяет существующий. Это делает их потокобезопасными и предсказуемыми. - Четкое разделение концепций: API разделяет машинное время ( Instant
) и человеческое время (LocalDate
, LocalTime
, LocalDateTime
, ZonedDateTime
). - Понятный и гибкий API: Методы имеют интуитивно понятные имена (plusDays
, minusHours
), а API является “текучим” (fluent), позволяя выстраивать цепочки вызовов.
Основные классы:
LocalDate
: Представляет дату без времени и временной зоны (например,2023-10-27
).LocalDate today = LocalDate.now(); LocalDate specificDate = LocalDate.of(2025, 5, 20); // 20 мая 2025
LocalTime
: Представляет время без даты и временной зоны (например,15:30:55.123
).LocalTime now = LocalTime.now(); LocalTime specificTime = LocalTime.of(18, 0); // 18:00
LocalDateTime
: Комбинация даты и времени без временной зоны (например,2023-10-27T15:30:55
). Наиболее часто используемый класс для представления конкретного момента времени, когда часовой пояс не важен.LocalDateTime now = LocalDateTime.now(); LocalDateTime specificDateTime = LocalDateTime.of(2025, 5, 20, 18, 0);
Instant
: Представляет одну точку на временной шкале (количество наносекунд с начала эпохи Unix, 1 января 1970 UTC). Это машинное время, идеально подходит для логирования, хранения временных меток в базе данных.Instant instantNow = Instant.now();
ZonedDateTime
:LocalDateTime
с привязкой к конкретной временной зоне (ZoneId
). Это полное представление даты-времени для конкретного региона.ZoneId parisZone = ZoneId.of("Europe/Paris"); ZonedDateTime parisTime = ZonedDateTime.of(specificDateTime, parisZone);
Period
: Представляет количество времени в днях, месяцах и годах. Используется для человеческих понятий длительности.Period twoMonths = Period.ofMonths(2); LocalDate futureDate = today.plus(twoMonths);
Duration
: Представляет количество времени в секундах и наносекундах. Используется для точного, машинного измерения времени.LocalTime start = LocalTime.now(); // ... какая-то операция LocalTime end = LocalTime.now(); Duration duration = Duration.between(start, end); System.out.println("Операция заняла: " + duration.toMillis() + " мс");
DateTimeFormatter
: Класс для форматирования (преобразования объекта в строку) и парсинга (преобразования строки в объект).LocalDateTime now = LocalDateTime.now(); // Использование предопределенного формата String formatted = now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); // Использование своего шаблона DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss"); String customFormatted = now.format(formatter); // "27.10.2023 15:45:00" // Парсинг LocalDateTime parsedDateTime = LocalDateTime.parse("20.05.2025 18:00:00", formatter);
35. Конвейеры и Stream API. Источники конвейера. Особенности обработки данных.
Stream API — это мощный инструмент, добавленный в Java 8, для обработки последовательностей данных в декларативном, функциональном стиле.
Что такое Stream (Поток)? - Это не структура данных, а конвейер (pipeline) для данных. Он не хранит элементы, а получает их из источника, обрабатывает и передает дальше. - Представляет собой последовательность элементов, над которой можно выполнять различные операции.
Ключевые особенности (особенности обработки данных):
- Декларативность: Вы описываете что вы хотите сделать с данными, а не как (в отличие от императивных циклов
for
,while
). - Ленивые вычисления (Lazy Evaluation): Промежуточные операции (например,
filter
,map
) не выполняются немедленно. Они лишь строят план конвейера. Вычисления начинаются только тогда, когда вызывается терминальная операция (collect
,forEach
). Это позволяет оптимизировать производительность, например, обрабатывать только необходимое количество элементов. - Одноразовость (Consumable): Поток можно использовать только один раз. После вызова терминальной операции поток считается “потребленным”, и повторное его использование вызовет
IllegalStateException
. - Возможность распараллеливания: Любой поток можно легко превратить в параллельный, вызвав метод
.parallel()
. Фреймворк сам позаботится о разделении работы между ядрами процессора.
Структура конвейера (Pipeline): Любая работа со Stream API состоит из трех частей: 1. Источник (Source): Откуда берутся данные. 2. Промежуточные операции (Intermediate Operations): 0 или более операций, которые преобразуют поток в другой поток. Они ленивы. 3. Терминальная операция (Terminal Operation): 1 операция, которая запускает весь конвейер и производит результат или побочный эффект.
Источники конвейера (Stream Sources):
Из коллекций: Самый частый способ.
List<String> list = List.of("a", "b", "c"); Stream<String> streamFromList = list.stream();
Из массивов:
String[] array = {"a", "b", "c"}; Stream<String> streamFromArray = Arrays.stream(array);
Из набора значений:
Stream<String> streamOfValues = Stream.of("a", "b", "c");
Из числовых диапазонов (для примитивных стримов):
IntStream intStream = IntStream.range(1, 10); // от 1 до 9 LongStream longStream = LongStream.rangeClosed(1, 10); // от 1 до 10 включительно
Из файла (через
Files.lines
):try (Stream<String> streamFromFile = Files.lines(Paths.get("file.txt"))) { // ... работа со строками файла }
Бесконечные стримы (с помощью
limit
):Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6... Stream<Double> randomStream = Stream.generate(Math::random);
36. Промежуточные и терминальные операции в конвейере. Коллекторы.
Промежуточные операции (Intermediate Operations) - Принимают поток и возвращают новый поток. Они всегда ленивы. - Основные операции: - filter(Predicate<T> predicate)
: Отфильтровывает элементы, не удовлетворяющие условию. - map(Function<T, R> mapper)
: Преобразует каждый элемент потока в другой элемент (возможно, другого типа). - flatMap(Function<T, Stream<R>> mapper)
: Преобразует каждый элемент в поток других элементов и “сплющивает” все эти потоки в один. Используется для работы со вложенными структурами. - sorted()
/ sorted(Comparator<T> comparator)
: Сортирует элементы. - distinct()
: Оставляет только уникальные элементы (на основе equals()
). - limit(long maxSize)
: Ограничивает количество элементов в потоке. - skip(long n)
: Пропускает первые n
элементов. - peek(Consumer<T> action)
: Выполняет действие над каждым элементом. В основном используется для отладки, чтобы посмотреть, какие элементы проходят через определенный этап конвейера.
Терминальные операции (Terminal Operations) - Запускают обработку потока и производят конечный результат. - Основные операции: - forEach(Consumer<T> action)
: Применяет действие к каждому элементу (побочный эффект). - collect(Collector<T, A, R> collector)
: Собирает элементы потока в какую-либо структуру данных (например, List
, Map
). Самая мощная и гибкая терминальная операция. - reduce(BinaryOperator<T> accumulator)
: “Сворачивает” все элементы потока в одно значение (например, сумма, конкатенация строк). - count()
: Возвращает количество элементов в потоке. - min(Comparator)
/ max(Comparator)
: Находит минимальный/максимальный элемент. - anyMatch(Predicate)
, allMatch(Predicate)
, noneMatch(Predicate)
: Проверяют, удовлетворяет ли какой-либо/все/ни один элемент условию. - findFirst()
/ findAny()
: Возвращают первый/любой элемент потока в виде Optional
.
Коллекторы (java.util.stream.Collectors
)
Collector
— это объект, который описывает, как аккумулировать элементы потока в конечный результат. Используется с терминальной операцией collect()
. Класс Collectors
предоставляет множество готовых фабричных методов для создания коллекторов.
- Сборка в коллекции:
toList()
: собирает элементы вList
.toSet()
: собирает элементы вSet
.toMap(keyMapper, valueMapper)
: собирает вMap
. Важно обрабатывать возможные дубликаты ключей, например:toMap(keyMapper, valueMapper, (oldValue, newValue) -> newValue)
.toCollection(Supplier<C> collectionFactory)
: собирает в указанный тип коллекции.
- Сборка в строку:
joining()
/joining(delimiter)
: объединяет элементы в одну строку.
- Агрегирующие/арифметические:
counting()
: подсчитывает количество элементов.summingInt()
,summingLong()
,summingDouble()
: находит сумму.averagingInt()
,averagingLong()
,averagingDouble()
: находит среднее.summarizingInt()
, etc.: собирает всю статистику (count, sum, min, max, average) в один объектIntSummaryStatistics
.
- Группировка и разделение:
groupingBy(Function<T, K> classifier)
: Группирует элементы по какому-либо признаку вMap<K, List<T>>
.partitioningBy(Predicate<T> predicate)
: Разделяет элементы на две группы (true и false) вMap<Boolean, List<T>>
.
Пример:
List<String> words = List.of("apple", "banana", "apricot", "cherry", "blueberry", "avocado");
Map<Character, List<String>> groupedByFirstLetter = words.stream()
.filter(s -> s.length() > 5) // Промежуточная: фильтруем по длине
.sorted() // Промежуточная: сортируем
.collect(Collectors.groupingBy(s -> s.charAt(0))); // Терминальная: группируем
// Результат: {a=[apricot, avocado], b=[banana, blueberry], c=[cherry]}
System.out.println(groupedByFirstLetter);
37. Рекурсия и ее использование.
Рекурсия — это метод решения задачи, при котором функция вызывает саму себя для решения подзадачи меньшего размера.
Структура рекурсивной функции: 1. Базовый случай (Base Case): Условие, при котором функция прекращает вызывать саму себя и возвращает конкретное значение. Это точка выхода из рекурсии, предотвращающая бесконечный цикл. 2. Рекурсивный шаг (Recursive Step): Часть функции, где она вызывает саму себя, но с измененными параметрами, которые приближают задачу к базовому случаю.
Пример: вычисление факториала Факториал числа n
(n!
) — это произведение всех натуральных чисел от 1 до n
. n! = n * (n-1) * (n-2) * ... * 1
Рекурсивное определение: n! = n * (n-1)!
public class RecursionExample {
public static int factorial(int n) {
// 1. Базовый случай
if (n <= 1) {
return 1;
}
// 2. Рекурсивный шаг
else {
return n * factorial(n - 1);
}
}
public static void main(String[] args) {
// factorial(4) -> 4 * factorial(3)
// -> 4 * (3 * factorial(2))
// -> 4 * (3 * (2 * factorial(1)))
// -> 4 * (3 * (2 * 1)) = 24
System.out.println("Факториал 4 = " + factorial(4));
}
}
Преимущества рекурсии: - Элегантность и читаемость: Для некоторых задач (например, обход деревьев, фракталы) рекурсивное решение выглядит гораздо проще и естественнее, чем итеративное. - Сокращение кода: Позволяет избежать сложных циклов и вспомогательных структур данных.
Недостатки рекурсии: - Риск переполнения стека (StackOverflowError
): Каждый рекурсивный вызов помещает новый фрейм в стек вызовов. При слишком большой глубине рекурсии стек может переполниться. - Производительность: Рекурсивные вызовы могут быть медленнее итеративных из-за накладных расходов на вызов функций. - Потребление памяти: Каждый вызов требует памяти в стеке.
Использование: Рекурсия широко используется для задач, которые имеют рекурсивную природу: - Работа с древовидными структурами данных (обход, поиск). - Алгоритмы “разделяй и властвуй” (быстрая сортировка, сортировка слиянием). - Парсинг математических выражений. - Переборные задачи (поиск путей в лабиринте).
38. Регулярные выражения, Классы Pattern и Matcher.
Регулярное выражение (Regex) — это последовательность символов, которая определяет шаблон поиска. Они используются для проверки соответствия строки шаблону, поиска и замены подстрок.
В Java для работы с регулярными выражениями используются два основных класса из пакета java.util.regex
:
Pattern
- Представляет собой скомпилированное регулярное выражение.
- Объекты
Pattern
создаются с помощью статического методаPattern.compile(String regex)
. - Компиляция — это затратный процесс, поэтому рекомендуется создавать объект
Pattern
один раз и переиспользовать его, если шаблон используется многократно. - Объекты
Pattern
являются неизменяемыми и потокобезопасными.
Matcher
- Это “движок”, который выполняет операции сопоставления, используя скомпилированный
Pattern
и входную строку. - Объект
Matcher
создается вызовом методаpattern.matcher(CharSequence input)
. - Объекты
Matcher
являются изменяемыми (хранят состояние поиска) и не потокобезопасны.
- Это “движок”, который выполняет операции сопоставления, используя скомпилированный
Основные методы Matcher
:
boolean matches()
: Проверяет, соответствует ли вся входная строка шаблону.boolean find()
: Ищет следующее совпадение в строке. Обычно используется в циклеwhile
.boolean lookingAt()
: Проверяет, соответствует ли начало строки шаблону.String group()
: Возвращает подстроку, которая совпала с последним вызовомfind()
.String group(int group)
: Возвращает подстроку, захваченную скобочной группой(...)
в шаблоне.String replaceAll(String replacement)
: Заменяет все совпадения в строке.
Пример: Найти все числа в тексте.
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexDemo {
public static void main(String[] args) {
String text = "Заказ #123 на сумму 45.67 будет доставлен 28-10-2023.";
// Шаблон: одна или более цифр, возможно с точкой
String regex = "\\d+(\\.\\d+)?";
// 1. Компилируем шаблон
Pattern pattern = Pattern.compile(regex);
// 2. Создаем Matcher для нашего текста
Matcher matcher = pattern.matcher(text);
System.out.println("Найденные числа:");
// 3. Ищем все совпадения в цикле
while (matcher.find()) {
// 4. Получаем найденное совпадение
String foundNumber = matcher.group();
System.out.println(foundNumber);
}
}
}
// Вывод:
// Найденные числа:
// 123
// 45.67
// 28
// 10
// 2023
Также существуют удобные методы в классе String
, такие как matches()
, split()
, replaceAll()
. Однако при многократном использовании в цикле они менее эффективны, так как каждый раз заново компилируют шаблон.
39. Паттерны проектирования. Порождающие паттерны. Примеры.
Паттерны проектирования (Design Patterns) — это проверенные, переиспользуемые решения типичных проблем проектирования программного обеспечения. Они не являются готовым кодом, а скорее шаблонами или описаниями того, как взаимодействуют объекты и классы для решения определенной задачи.
Порождающие паттерны (Creational Patterns) - Назначение: Отвечают за процесс создания объектов, делая систему независимой от того, как объекты создаются, компонуются и представляются. Они инкапсулируют знание о конкретных классах, которые использует система, и скрывают детали того, как эти экземпляры создаются и собираются.
Примеры:
- Singleton (Одиночка)
- Цель: Гарантировать, что у класса есть только один экземпляр, и предоставить глобальную точку доступа к этому экземпляру.
- Применение: Логгеры, драйверы устройств, пулы потоков, кэши.
- Реализация (потокобезопасная через Initialization-on-demand holder idiom):
public class Singleton { private Singleton() {} // Приватный конструктор private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
- Factory Method (Фабричный метод)
- Цель: Определить интерфейс для создания объекта, но позволить подклассам решать, какой класс инстанцировать. Фабричный метод делегирует создание экземпляров подклассам.
- Применение: Когда класс не может заранее знать, объекты каких классов ему нужно создавать.
- Структура:
// Продукт interface Document { void open(); } class TextDocument implements Document { public void open() { /*...*/ } } class PdfDocument implements Document { public void open() { /*...*/ } } // Создатель (Creator) abstract class Application { // Фабричный метод public abstract Document createDocument(); public void newDocument() { Document doc = createDocument(); doc.open(); } } // Конкретные создатели class TextApplication extends Application { @Override public Document createDocument() { return new TextDocument(); } } class PdfApplication extends Application { @Override public Document createDocument() { return new PdfDocument(); } }
- Builder (Строитель)
- Цель: Отделить конструирование сложного объекта от его представления, так что один и тот же процесс конструирования может создавать разные представления.
- Применение: Когда нужно создать объект с большим количеством необязательных полей или сложной логикой конструирования. Позволяет избежать “телескопических” конструкторов.
- Реализация:
public class User { private final String firstName; // required private final String lastName; // required private final int age; // optional private final String phone; // optional private User(UserBuilder builder) { this.firstName = builder.firstName; this.lastName = builder.lastName; this.age = builder.age; this.phone = builder.phone; } public static class UserBuilder { private final String firstName; private final String lastName; private int age = 0; private String phone = ""; public UserBuilder(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public UserBuilder age(int age) { this.age = age; return this; } public UserBuilder phone(String phone) { this.phone = phone; return this; } public User build() { return new User(this); } } } // Использование: // User user = new User.UserBuilder("John", "Doe").age(30).phone("123-45-67").build();
40. Паттерны проектирования. Поведенческие паттерны. Примеры.
Поведенческие паттерны (Behavioral Patterns) - Назначение: Определяют алгоритмы и способы взаимодействия и распределения ответственности между объектами. Они описывают не только структуру классов или объектов, но и шаблоны их коммуникации.
Примеры:
- Strategy (Стратегия)
- Цель: Определить семейство алгоритмов, инкапсулировать каждый из них и сделать их взаимозаменяемыми. Стратегия позволяет изменять алгоритмы независимо от клиентов, которые их используют.
- Применение: Когда есть несколько способов выполнить действие, и нужно выбирать один из них во время выполнения. Например, разные способы оплаты, сортировки, сжатия файлов.
- Структура:
// Интерфейс стратегии interface PaymentStrategy { void pay(int amount); } // Конкретные стратегии class CreditCardStrategy implements PaymentStrategy { /*...*/ } class PayPalStrategy implements PaymentStrategy { /*...*/ } // Контекст class ShoppingCart { private PaymentStrategy paymentStrategy; public void setPaymentStrategy(PaymentStrategy strategy) { this.paymentStrategy = strategy; } public void checkout(int amount) { paymentStrategy.pay(amount); } } // Использование: // cart.setPaymentStrategy(new CreditCardStrategy()); // cart.checkout(100);
- Observer (Наблюдатель)
- Цель: Определить зависимость “один-ко-многим” между объектами таким образом, что при изменении состояния одного объекта все зависящие от него объекты автоматически уведомляются и обновляются.
- Применение: Системы, управляемые событиями (Event-driven systems), GUI-компоненты (слушатели событий), реализация подписок.
- Структура:
// Наблюдатель interface Observer { void update(String news); } class Subscriber implements Observer { /*...*/ } // Субъект (наблюдаемый) interface Subject { void addObserver(Observer o); void removeObserver(Observer o); void notifyObservers(); } class NewsAgency implements Subject { private List<Observer> observers = new ArrayList<>(); private String news; public void setNews(String news) { this.news = news; notifyObservers(); } @Override public void notifyObservers() { for (Observer o : observers) { o.update(news); } } // ... impl add/remove }
- Template Method (Шаблонный метод)
- Цель: Определить “скелет” алгоритма в операции, перекладывая определение некоторых шагов на подклассы. Позволяет подклассам переопределять определенные шаги алгоритма, не меняя его структуру в целом.
- Применение: Когда есть общий алгоритм с небольшими вариациями в шагах.
- Структура:
abstract class HouseBuilder { // Шаблонный метод public final void buildHouse() { buildFoundation(); buildWalls(); buildRoof(); paintHouse(); // hook - необязательный шаг } // Абстрактные шаги, которые должны реализовать подклассы protected abstract void buildWalls(); protected abstract void buildRoof(); // Общие шаги private void buildFoundation() { System.out.println("Building foundation"); } // Необязательный шаг (hook) с реализацией по умолчанию protected void paintHouse() {} } class WoodenHouse extends HouseBuilder { @Override protected void buildWalls() { System.out.println("Building wooden walls"); } @Override protected void buildRoof() { System.out.println("Building wooden roof"); } }
41. Паттерны проектирования. Структурные паттерны. Примеры.
Структурные паттерны (Structural Patterns) - Назначение: Объясняют, как из классов и объектов образовывать более крупные структуры. Они упрощают проектирование, определяя простые способы реализации отношений между сущностями.
Примеры:
- Decorator (Декоратор)
- Цель: Динамически добавлять объекту новые обязанности. Декораторы предоставляют гибкую альтернативу наследованию для расширения функциональности.
- Применение: Когда нужно добавлять функциональность к объектам во время выполнения, не затрагивая другие объекты того же класса. Классический пример — потоки ввода-вывода в Java (
BufferedInputStream
декорируетFileInputStream
). - Структура:
// Компонент interface Coffee { String getDescription(); double getCost(); } // Конкретный компонент class SimpleCoffee implements Coffee { /*...*/ } // Абстрактный декоратор abstract class CoffeeDecorator implements Coffee { protected Coffee decoratedCoffee; public CoffeeDecorator(Coffee coffee) { this.decoratedCoffee = coffee; } public String getDescription() { return decoratedCoffee.getDescription(); } public double getCost() { return decoratedCoffee.getCost(); } } // Конкретные декораторы class WithMilk extends CoffeeDecorator { /* ... добавляет " with milk" и +cost */ } class WithSugar extends CoffeeDecorator { /* ... добавляет " with sugar" и +cost */ } // Использование: // Coffee myCoffee = new WithSugar(new WithMilk(new SimpleCoffee()));
- Adapter (Адаптер)
- Цель: Преобразовать интерфейс одного класса в интерфейс другого, который ожидают клиенты. Адаптер обеспечивает совместную работу классов, которые иначе не могли бы работать вместе из-за несовместимости их интерфейсов.
- Применение: Когда нужно использовать существующий класс, но его интерфейс не соответствует вашим требованиям. Пример из JDK:
Arrays.asList()
адаптирует массив к интерфейсуList
. - Структура:
// "Неудобный" класс class OldSystem { public void specificRequest() { /*...*/ } } // Целевой интерфейс interface NewSystem { void request(); } // Адаптер class Adapter implements NewSystem { private OldSystem oldSystem; public Adapter(OldSystem oldSystem) { this.oldSystem = oldSystem; } @Override public void request() { oldSystem.specificRequest(); } }
- Facade (Фасад)
- Цель: Предоставить единый, упрощенный интерфейс к сложной подсистеме. Фасад определяет интерфейс более высокого уровня, который облегчает использование подсистемы.
- Применение: Когда нужно предоставить простой интерфейс для сложной системы или библиотеки. Фасад уменьшает количество зависимостей между клиентом и подсистемой.
- Структура:
// Сложная подсистема class CPU { public void processData() {} } class Memory { public void load() {} } class HardDrive { public void readData() {} } // Фасад class ComputerFacade { private CPU cpu = new CPU(); private Memory memory = new Memory(); private HardDrive hardDrive = new HardDrive(); public void start() { hardDrive.readData(); memory.load(); cpu.processData(); System.out.println("Computer started."); } } // Клиенту теперь нужно работать только с ComputerFacade // ComputerFacade facade = new ComputerFacade(); // facade.start();
42. Интернационализация. Локализация. Хранение локализованных ресурсов.
Интернационализация (Internationalization, i18n) — это процесс проектирования и разработки приложения таким образом, чтобы оно могло быть легко адаптировано для различных языков, регионов и культур без внесения изменений в исходный код. Основная задача i18n — вынести все специфичные для локали данные (текст, даты, числа, валюты) за пределы кода.
Локализация (Localization, L10n) — это процесс адаптации уже интернационализированного приложения для конкретного региона или языка. Это включает в себя перевод текстов, адаптацию форматов дат и чисел, изображений и других культурно-зависимых элементов.
Ключевые классы в Java:
java.util.Locale
:Объект
Locale
представляет определенный географический, политический или культурный регион.Он не является контейнером для локализованных данных, а служит идентификатором для них.
Состоит из:
- Языка (language code, ISO 639, например, “en”, “fr”).
- Страны (country code, ISO 3166, например, “US”, “FR”).
- Варианта (необязательный, специфичный для поставщика).
Примеры создания:
Locale usLocale = Locale.US; // pre-defined Locale frLocale = new Locale("fr", "FR"); // французский (Франция) Locale ruLocale = new Locale("ru"); // русский
Хранение локализованных ресурсов: ResourceBundle
java.util.ResourceBundle
— это абстрактный класс, который управляет локализованными ресурсами. Он позволяет загружать наборы пар “ключ-значение” в зависимости от выбранной Locale
.
Способы хранения:
- На основе
.properties
файлов (PropertyResourceBundle
): Самый распространенный способ.- Создаются текстовые файлы с расширением
.properties
для каждой локали. - Файлы именуются по схеме:
basename_language_country.properties
. basename
— базовое имя для группы ресурсов.
Пример:
Файл
messages.properties
(по умолчанию, если ничего не найдено):greeting = Hello farewell = Goodbye
Файл
messages_fr_FR.properties
(для французской локали):greeting = Bonjour farewell = Au revoir
Файл
messages_de.properties
(для немецкой локали):greeting = Hallo farewell = Auf Wiedersehen
- Создаются текстовые файлы с расширением
- На основе Java-классов (
ListResourceBundle
): Более гибкий, позволяет хранить не только строки, но и любые объекты.
Механизм загрузки: ResourceBundle.getBundle("messages", locale)
выполняет поиск файлов в следующем порядке (для new Locale("fr", "FR")
): 1. messages_fr_FR.properties
2. messages_fr.properties
3. messages.properties
(базовый, по умолчанию)
Пример использования:
public class I18nDemo {
public static void main(String[] args) {
// Устанавливаем локаль
Locale locale = new Locale("fr", "FR");
// Загружаем бандл
ResourceBundle messages = ResourceBundle.getBundle("messages", locale);
// Получаем строки по ключу
System.out.println(messages.getString("greeting")); // Выведет: Bonjour
System.out.println(messages.getString("farewell")); // Выведет: Au revoir
}
}
43. Форматирование локализованных числовых данных, текста, даты и времени.
После того как приложение интернационализировано, необходимо корректно отображать данные в соответствии с правилами выбранной Locale
.
1. Форматирование чисел и валют: java.text.NumberFormat
- Этот класс позволяет форматировать числа, валюты и проценты с учетом локальных особенностей (разделители тысяч, десятичный разделитель, символ валюты). - Экземпляры создаются с помощью фабричных методов, принимающих Locale
: - getInstance(locale)
: для обычных чисел. - getCurrencyInstance(locale)
: для денежных сумм. - getPercentInstance(locale)
: для процентов.
double number = 123456.789;
Locale usLocale = Locale.US;
Locale deLocale = Locale.GERMANY;
NumberFormat usFormatter = NumberFormat.getInstance(usLocale);
NumberFormat deFormatter = NumberFormat.getInstance(deLocale);
System.out.println("US: " + usFormatter.format(number)); // 123,456.789
System.out.println("DE: " + deFormatter.format(number)); // 123.456,789
NumberFormat usCurrency = NumberFormat.getCurrencyInstance(usLocale);
NumberFormat deCurrency = NumberFormat.getCurrencyInstance(deLocale);
System.out.println("US Currency: " + usCurrency.format(number)); // $123,456.79
System.out.println("DE Currency: " + deCurrency.format(number)); // 123.456,79 €
2. Форматирование даты и времени: java.time.format.DateTimeFormatter
- Современный java.time
API предоставляет лучший способ для форматирования дат и времени с учетом локали. - Используются предопределенные стили (FormatStyle.FULL
, SHORT
, MEDIUM
, LONG
).
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
LocalDateTime now = LocalDateTime.now();
Locale usLocale = Locale.US;
Locale itLocale = Locale.ITALY;
DateTimeFormatter usFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(usLocale);
DateTimeFormatter itFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(itLocale);
System.out.println("US Date/Time: " + usFormatter.format(now)); // Oct 27, 2023, 4:30:00 PM
System.out.println("IT Date/Time: " + itFormatter.format(now)); // 27 ott 2023, 16:30:00
3. Форматирование сложных текстовых сообщений: java.text.MessageFormat
- Этот класс позволяет создавать составные сообщения из шаблона. Шаблон может содержать плейсхолдеры {}
для подстановки аргументов. - Особенно полезно, когда порядок слов в предложении меняется в зависимости от языка. - Плейсхолдеры могут включать тип и стиль форматирования: {index,type,style}
.
Пример: - Файл report_en.properties
: report.pattern = On {0,date,long}, there was an event {1} at {2,time,short}.
- Файл report_ru.properties
: report.pattern = Событие «{1}» произошло {0,date,long} в {2,time,short}.
Locale ruLocale = new Locale("ru");
ResourceBundle bundle = ResourceBundle.getBundle("report", ruLocale);
String pattern = bundle.getString("report.pattern");
Object[] arguments = {new Date(), "Конференция", new Date()};
String formattedMessage = MessageFormat.format(pattern, arguments);
// Вывод для русской локали:
// Событие «Конференция» произошло 27 октября 2023 г. в 16:35.
System.out.println(formattedMessage);
Графические интерфейсы (GUI)
44. Компоненты графического интерфейса. Классы Component, JComponent, Node.
В Java существует несколько библиотек для создания GUI. Этот вопрос охватывает базовые строительные блоки из трех основных фреймворков: AWT, Swing и JavaFX.
1. java.awt.Component
(AWT) - AWT (Abstract Window Toolkit) — самая первая библиотека GUI в Java. - Component
— это абстрактный базовый класс для всех визуальных компонентов AWT. Он представляет собой любой объект, который имеет графическое представление и может взаимодействовать с пользователем. - Особенности: - Тяжеловесные компоненты (Heavyweight): Каждый компонент AWT (Button
, Label
) имеет соответствующий ему нативный компонент (peer) в операционной системе. Это означает, что внешний вид и поведение компонентов AWT полностью определяются ОС. - Плюсы: Нативное поведение, потенциально высокая производительность. - Минусы: Ограниченный набор компонентов, зависимость от платформы (Look and Feel не меняется), проблемы с совместимостью. - Основные потомки: Button
, TextField
, Label
, Container
.
2. javax.swing.JComponent
(Swing) - Swing — это более современная и гибкая библиотека, построенная поверх AWT. - JComponent
— это абстрактный базовый класс для всех легковесных компонентов Swing. Он наследуется от java.awt.Container
(который, в свою очередь, наследуется от Component
). - Особенности: - Легковесные компоненты (Lightweight): Большинство компонентов Swing (JButton
, JLabel
) нарисованы полностью на Java, без использования нативных аналогов ОС (за исключением окон верхнего уровня, таких как JFrame
). - Плюсы: - Pluggable Look and Feel (PLAF): Внешний вид приложения можно менять программно, независимо от ОС (например, Metal, Nimbus, Windows, GTK+). - Богатый набор компонентов (JTable
, JTree
, JProgressBar
). - Более гибкая и мощная архитектура (например, поддержка иконок, всплывающих подсказок). - JComponent
добавляет функциональность, которой нет в Component
, например, поддержку границ (Borders
), всплывающих подсказок (Tooltips
), сочетаний клавиш.
3. javafx.scene.Node
(JavaFX) - JavaFX — самая современная из трех библиотек, предназначенная для создания насыщенных клиентских приложений (Rich Client Applications). - Node
— это базовый класс для всех элементов в графе сцены (scene graph) JavaFX. - Особенности: - Граф сцены: Весь интерфейс JavaFX представлен в виде иерархического дерева узлов. Это позволяет легко применять трансформации (масштабирование, вращение), эффекты (тень, размытие) и анимации ко всему поддереву узлов. - Декларативное описание UI: Интерфейс можно определять не только в коде, но и с помощью XML-подобного языка FXML, что разделяет логику и представление. - Стилизация через CSS: Внешний вид компонентов легко настраивается с помощью CSS-файлов, аналогично веб-разработке. - Поддержка мультимедиа, 3D-графики, веб-компонентов (WebView
). - Иерархия: Node
-> Parent
-> Region
-> Control
(кнопки, метки) или Pane
(контейнеры).
Характеристика | java.awt.Component (AWT)
|
javax.swing.JComponent (Swing)
|
javafx.scene.Node (JavaFX)
|
---|---|---|---|
Тип | Тяжеловесный (Native Peer) | Легковесный (Нарисован в Java) | Легковесный (Нарисован в Java) |
Look & Feel | Зависит от ОС | Настраиваемый (PLAF) | Настраиваемый через CSS |
Архитектура | Простая иерархия | Расширение AWT, модель MVC | Граф сцены, FXML |
Возможности | Базовые | Богатый набор компонентов, границы, подсказки | Анимации, эффекты, 3D, CSS, FXML |
Современность | Устаревший | Широко используется, но вытесняется | Современный стандарт |
45. Контейнеры. Классы Container, JPanel, Parent, Region, Scene.
Контейнер — это специальный компонент, основная задача которого — содержать и организовывать другие компоненты.
1. java.awt.Container
(AWT) - Наследуется от Component
. Является базовым классом для всех контейнеров AWT. - Отвечает за: - Добавление и удаление дочерних компонентов (add()
, remove()
). - Управление их расположением с помощью менеджера компоновки (LayoutManager
). - Основные потомки: Panel
(простой контейнер), Window
, Frame
.
2. javax.swing.JPanel
(Swing) - Основной контейнер общего назначения в Swing. Наследуется от JComponent
. - Это легковесный аналог java.awt.Panel
. - Использование: - Группировка компонентов для более сложной компоновки (например, можно создать JPanel
с BorderLayout
и поместить его в другой JPanel
с GridLayout
). - Используется в качестве “холста” для рисования. - По умолчанию использует FlowLayout
.
3. javafx.scene.Parent
и javafx.scene.layout.Region
(JavaFX) - Parent
: Базовый абстрактный класс для всех узлов, которые могут иметь дочерние узлы в графе сцены. Он управляет списком дочерних узлов. - Region
: Базовый класс для всех контейнеров компоновки (Layout Panes) и элементов управления (Controls) в JavaFX. Он наследуется от Parent
и добавляет возможность стилизации через CSS, а также настройки размеров (min/pref/max width/height). - Основные потомки Region
: - Panes (контейнеры компоновки): HBox
, VBox
, BorderPane
, GridPane
, StackPane
. - Controls (элементы управления): Button
, Label
, TextField
.
4. javafx.scene.Scene
(JavaFX) - Scene
(Сцена) — это корневой контейнер для всего содержимого в окне JavaFX (Stage
). - Она содержит граф сцены (дерево узлов) и определяет его размеры. - В одном окне (Stage
) в один момент времени может быть только одна Scene
. - Можно думать о связке Stage
и Scene
как о театре: Stage
— это сама сцена (окно), а Scene
— это декорации и актеры на ней (содержимое).
// JavaFX пример
// Создаем корневой узел (контейнер)
VBox root = new VBox();
root.getChildren().addAll(new Label("Имя:"), new TextField());
// Создаем сцену с корневым узлом и задаем ее размеры
Scene scene = new Scene(root, 300, 200);
// Устанавливаем сцену в окно (Stage)
primaryStage.setScene(scene);
primaryStage.show();
46. Размещение компонентов в контейнерах. Менеджеры компоновки.
Менеджер компоновки (Layout Manager) — это объект, который определяет правила расположения и изменения размеров дочерних компонентов внутри контейнера. Использование менеджеров компоновки позволяет создавать гибкие интерфейсы, которые корректно выглядят на разных операционных системах и при изменении размеров окна.
AWT и Swing используют одну и ту же модель менеджеров компоновки. Менеджер устанавливается для контейнера методом setLayout()
.
Основные менеджеры компоновки AWT/Swing:
FlowLayout
(по умолчанию дляJPanel
)- Располагает компоненты в ряд, один за другим, слева направо.
- Когда ряд заполняется, переходит на следующую строку.
- Учитывает предпочтительный размер (
preferred size
) компонентов.
BorderLayout
(по умолчанию дляJFrame
)- Делит контейнер на пять областей:
NORTH
,SOUTH
,WEST
,EAST
,CENTER
. - Компоненты в
NORTH
иSOUTH
растягиваются по горизонтали. - Компоненты в
WEST
иEAST
растягиваются по вертикали. - Компонент в
CENTER
занимает все оставшееся место.
- Делит контейнер на пять областей:
GridLayout
- Размещает компоненты в виде прямоугольной сетки с одинаковыми по размеру ячейками.
- Все компоненты растягиваются, чтобы заполнить свою ячейку.
GridBagLayout
- Самый мощный и сложный менеджер.
- Размещает компоненты в сетке, но позволяет им занимать несколько ячеек.
- Для каждого компонента настраиваются “ограничения” (
GridBagConstraints
): положение (gridx
,gridy
), размер (gridwidth
,gridheight
), вес (weightx
,weighty
), выравнивание (anchor
) и т.д.
JavaFX использует другую модель. Здесь нет отдельного объекта-менеджера. Вместо этого используются специальные классы-контейнеры (Layout Panes), которые сами реализуют логику компоновки.
Основные контейнеры компоновки JavaFX:
HBox
: Располагает дочерние узлы в один горизонтальный ряд.VBox
: Располагает дочерние узлы в один вертикальный столбец.BorderPane
: АналогBorderLayout
в Swing. Области:top
,bottom
,left
,right
,center
.GridPane
: АналогGridBagLayout
, но гораздо проще в использовании. Компоненты размещаются в ячейках сетки по индексам строки и столбца.StackPane
: Располагает все дочерние узлы друг на друге, “стопкой”.FlowPane
: АналогFlowLayout
.AnchorPane
: Позволяет “привязывать” дочерние узлы к краям панели. Полезно для создания интерфейсов, которые масштабируются вместе с окном.
47. Контейнеры верхнего уровня. Классы JFrame, SwingUtilities, Stage, Application.
Контейнеры верхнего уровня — это “корни” иерархии GUI-компонентов. Они не могут быть помещены в другие контейнеры и напрямую взаимодействуют с оконной системой ОС.
Swing:
JFrame
- Основной класс для создания главного окна приложения в Swing. - Наследуется от java.awt.Frame
и является тяжеловесным компонентом (имеет нативный peer). - Содержит панель содержимого (Content Pane), куда и добавляются все остальные легковесные компоненты. Доступ к ней осуществляется через frame.getContentPane()
. - Ключевые методы: - setTitle(String title)
: Устанавливает заголовок окна. - setSize(int width, int height)
: Задает размер. - pack()
: Автоматически подбирает размер окна под предпочтительные размеры его компонентов. - setDefaultCloseOperation(int operation)
: Определяет, что делать при закрытии окна (EXIT_ON_CLOSE
, DISPOSE_ON_CLOSE
и т.д.). - setVisible(true)
: Делает окно видимым.
SwingUtilities
- Это утилитарный класс с набором статических методов для работы со Swing. - Самый важный метод: invokeLater(Runnable doRun)
. - Правило Swing: Все операции, изменяющие GUI (создание, обновление компонентов), должны выполняться в специальном потоке — Event Dispatch Thread (EDT). - invokeLater
помещает переданный Runnable
в очередь событий EDT и немедленно возвращает управление. Код будет выполнен, когда EDT до него доберется. Это гарантирует потокобезопасность при работе с GUI.
```java public static void main(String[] args) { SwingUtilities.invokeLater(() -> { JFrame frame = new JFrame("My App"); // ... настройка frame ... frame.setVisible(true); }); } ```
JavaFX:
Application
- Абстрактный класс, который является точкой входа для любого JavaFX-приложения. - Чтобы создать приложение, нужно унаследоваться от Application
и переопределить метод start()
. - Жизненный цикл: 1. Вызывается Application.launch(MyApplication.class, args)
. 2. Создается экземпляр класса MyApplication
(вызывается конструктор). 3. Вызывается метод init()
(можно переопределить для инициализации). 4. Вызывается метод start(Stage primaryStage)
. Это основной метод, где строится GUI. 5. Приложение работает, пока не будет закрыто. 6. Вызывается метод stop()
(можно переопределить для освобождения ресурсов).
Stage
- Аналог JFrame
в JavaFX. Представляет собой окно верхнего уровня (главное окно, диалоговое окно). - Основной Stage
(primary stage) передается в метод start()
. - На Stage
устанавливается Scene
, которая содержит весь UI. - Основные методы: setTitle()
, setScene()
, show()
.
public class MyFxApp extends Application {
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("My FX App");
// ... создание Scene ...
// primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
48. Обработка событий графического интерфейса. События и слушатели.
GUI-приложения являются событийно-ориентированными (event-driven). Они реагируют на действия пользователя (клик мыши, нажатие клавиши) или системы (изменение размера окна).
Модель делегирования событий (Delegation Event Model):
- Источник события (Event Source): Компонент, на котором произошло событие (например,
JButton
). - Объект события (Event Object): Объект, который инкапсулирует информацию о событии (например,
ActionEvent
,MouseEvent
). Наследуется отjava.util.EventObject
. - Слушатель события (Event Listener): Объект, который реализует специальный интерфейс и содержит код для обработки события. Слушатель должен быть зарегистрирован на источнике.
Процесс: 1. Пользователь нажимает кнопку (JButton
). 2. JButton
(источник) создает объект ActionEvent
и передает его всем зарегистрированным слушателям. 3. Слушатель (объект, реализующий ActionListener
) получает ActionEvent
и выполняет код, определенный в его методе actionPerformed()
.
Примеры в Swing:
- Событие:
ActionEvent
(клик по кнопке, выбор пункта меню). - Слушатель:
ActionListener
(методactionPerformed(ActionEvent e)
). - Регистрация:
button.addActionListener(listener)
. - Событие:
MouseEvent
(движение, клик, вход/выход курсора). - Слушатель:
MouseListener
(методыmousePressed
,mouseReleased
,mouseClicked
, etc.) иMouseMotionListener
(mouseMoved
,mouseDragged
). - Регистрация:
component.addMouseListener(listener)
.
Пример в Swing:
JButton button = new JButton("Нажми меня");
// Регистрация слушателя через анонимный класс
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Кнопка нажата!");
}
});
// С Java 8 можно использовать лямбду, так как ActionListener - функциональный интерфейс
button.addActionListener(e -> System.out.println("Кнопка нажата!"));
Обработка событий в JavaFX: Модель очень похожа, но более современна и гибка. - События: javafx.event.Event
и его подклассы (ActionEvent
, MouseEvent
). - Слушатели: javafx.event.EventHandler<T extends Event>
. Это функциональный интерфейс с одним методом handle(T event)
. - Регистрация: - node.addEventHandler(EventType, eventHandler)
: универсальный способ. - node.setOnAction(eventHandler)
: удобный метод для конкретных событий (например, у Button
).
Пример в JavaFX:
Button button = new Button("Нажми меня");
// Использование лямбда-выражения
button.setOnAction(event -> {
System.out.println("Кнопка нажата!");
});
49. Библиотеки графического интерфейса - AWT, Swing, JavaFX, их особенности.
Характеристика | AWT (Abstract Window Toolkit) | Swing | JavaFX |
---|---|---|---|
Год выпуска | Java 1.0 (1996) | Java 1.2 (1998) | JavaFX 1.0 (2008), в составе JDK с Java 8 |
Основные классы | Component , Container , Frame
|
JComponent , JPanel , JFrame
|
Node , Parent , Region , Stage , Scene , Application
|
Тип компонентов | Тяжеловесные (Heavyweight). Используют нативные компоненты ОС. | Легковесные (Lightweight). Нарисованы в Java. Исключение - окна верхнего уровня. | Легковесные. Полностью нарисованы в Java. Используют аппаратное ускорение (DirectX, OpenGL). |
Look and Feel | Зависит от ОС. Приложение выглядит по-разному на Windows, macOS, Linux. | Pluggable (PLAF). Можно программно менять внешний вид, независимо от ОС. | Стилизация через CSS. Полный контроль над внешним видом, как в веб-разработке. |
Компоновка | LayoutManager (FlowLayout , BorderLayout , etc.)
|
Такие же, как в AWT. GridBagLayout - самый мощный.
|
Layout Panes (VBox , HBox , GridPane , etc.). Более интуитивные и гибкие.
|
Потоковая модель | Единый поток для AWT. | Event Dispatch Thread (EDT). Все изменения GUI должны быть в этом потоке. SwingUtilities.invokeLater .
|
JavaFX Application Thread. Все изменения GUI должны быть в этом потоке. Platform.runLater .
|
Особенности | - Самая старая и простая. - Ограниченный набор компонентов. |
- Построена поверх AWT. - Богатый набор компонентов ( JTable , JTree ).- Архитектура Model-View-Controller (MVC). |
- Граф сцены (Scene Graph). - Декларативное описание UI через FXML. - Поддержка анимаций, эффектов, 3D. - Встроенный браузер ( WebView ).- Поддержка привязки свойств (Properties and Binding). |
Современное состояние | Устарела. Не рекомендуется для новых проектов. Используется как основа для Swing. | Все еще широко используется во многих корпоративных и настольных приложениях. Считается стабильной и зрелой, но вытесняется JavaFX. | Современный стандарт для создания настольных приложений на Java. Активно развивается. С JDK 11 вынесена в отдельный модуль. |
50. Рефлексия. Класс Class и пакет java.lang.reflect.
Рефлексия (Reflection) — это механизм, который позволяет исследовать и изменять структуру и поведение программы во время ее выполнения (runtime). С помощью рефлексии можно получать информацию о классах, их полях, методах, конструкторах, а также создавать экземпляры классов, вызывать методы и изменять значения полей, даже если они приватные.
Класс java.lang.Class
- Это “точка входа” в API рефлексии. - Экземпляр класса Class
представляет собой мета-информацию о любом типе в Java (классе, интерфейсе, перечислении, примитивном типе). - Способы получения объекта Class
: 1. Через литерал .class
: String.class
, int.class
. Самый простой и эффективный способ, если тип известен на этапе компиляции. 2. Через метод getClass()
у объекта: String s = "hello"; Class<?> c = s.getClass();
. 3. Через статический метод Class.forName(String className)
: Class<?> c = Class.forName("java.lang.String");
. Используется, когда имя класса известно только во время выполнения в виде строки. Может бросить ClassNotFoundException
.
Пакет java.lang.reflect
- Содержит основные классы для работы с рефлексией: - Field
: Представляет поле класса. Позволяет читать и изменять его значение (get()
, set()
). - Method
: Представляет метод класса. Позволяет вызывать его (invoke()
). - Constructor
: Представляет конструктор класса. Позволяет создавать новые экземпляры (newInstance()
). - Modifier
: Утилитарный класс для декодирования модификаторов доступа (public, private, static, final).
Основные операции: - getFields()
/ getDeclaredFields()
: возвращают все public поля / все поля, объявленные в этом классе. - getMethods()
/ getDeclaredMethods()
: аналогично для методов. - getConstructors()
/ getDeclaredConstructors()
: аналогично для конструкторов.
Работа с private
членами: - По умолчанию доступ к private
членам запрещен. - Чтобы обойти это ограничение, нужно вызвать метод setAccessible(true)
на объекте Field
, Method
или Constructor
. Это отключает проверку доступа для данного конкретного объекта рефлексии.
Пример:
class MyClass {
private String privateField = "secret";
private void privateMethod(String message) {
System.out.println("Private method called with: " + message);
}
}
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
MyClass obj = new MyClass();
Class<?> clazz = obj.getClass();
// Доступ к приватному полю
Field field = clazz.getDeclaredField("privateField");
field.setAccessible(true); // <<<<<<<<<<<<< ВАЖНО!
String value = (String) field.get(obj);
System.out.println("Value of privateField: " + value);
field.set(obj, "new secret");
System.out.println("New value: " + (String) field.get(obj));
// Вызов приватного метода
Method method = clazz.getDeclaredMethod("privateMethod", String.class);
method.setAccessible(true); // <<<<<<<<<<<<< ВАЖНО!
method.invoke(obj, "Hello from Reflection!");
}
}
Плюсы рефлексии: - Позволяет создавать очень гибкие фреймворки и библиотеки (например, Spring, Hibernate, JUnit). - Полезна для инструментов анализа кода, сериализации.
Минусы рефлексии: - Низкая производительность: Вызовы через рефлексию гораздо медленнее прямых вызовов. - Нарушение инкапсуляции: Позволяет обходить модификаторы доступа, что может разрушить архитектуру и логику класса. - Небезопасность типов: Проверки типов происходят во время выполнения, а не компиляции, что может привести к ошибкам (например, ClassCastException
). - Усложнение кода: Код, использующий рефлексию, сложнее читать и поддерживать.
51. Аннотации и их использование.
Аннотация — это форма метаданных, которую можно добавить в исходный код Java. Аннотации не влияют напрямую на выполнение кода, но они могут быть прочитаны и использованы компилятором, инструментами сборки, фреймворками или во время выполнения через рефлексию.
Назначение аннотаций: - Информация для компилятора: Аннотации могут использоваться для обнаружения ошибок или подавления предупреждений (например, @Override
, @SuppressWarnings
). - Обработка во время компиляции: Инструменты могут обрабатывать аннотации для генерации кода, XML-файлов и т.д. (например, Lombok, MapStruct). - Обработка во время выполнения: Аннотации могут быть прочитаны через рефлексию для изменения поведения программы (например, JUnit использует @Test
, Spring использует @Component
, @Autowired
).
Типы аннотаций (определяются мета-аннотациями):
@Target
: Указывает, к каким элементам программы можно применять аннотацию (TYPE
- класс,METHOD
- метод,FIELD
- поле, и т.д.).@Retention
: Указывает, до какого этапа жизненного цикла будет сохранена аннотация:RetentionPolicy.SOURCE
: Аннотация доступна только в исходном коде и отбрасывается компилятором.RetentionPolicy.CLASS
: Аннотация сохраняется в.class
файле, но не доступна во время выполнения через рефлексию (по умолчанию).RetentionPolicy.RUNTIME
: Аннотация сохраняется в.class
файле и доступна во время выполнения через рефлексию. Это необходимо для большинства фреймворков.
Создание своей аннотации: - Используется ключевое слово @interface
. - Внутри можно объявлять “элементы” аннотации, которые выглядят как методы без тела. Они определяют параметры, которые можно передать в аннотацию. - Можно задавать значения по умолчанию с помощью ключевого слова default
.
Пример:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
// 1. Определение аннотации
@Retention(RetentionPolicy.RUNTIME) // Доступна в рантайме
@Target(ElementType.METHOD) // Применяется к методам
public @interface MyTest {
String info() default "default info";
}
// 2. Применение аннотации
class MyTestClass {
@MyTest(info = "This is test method 1")
public void testMethod1() {
// ...
}
public void regularMethod() {
// ...
}
@MyTest
public void testMethod2() {
// ...
}
}
// 3. Обработка аннотации через рефлексию
public class TestRunner {
public static void main(String[] args) {
Class<MyTestClass> clazz = MyTestClass.class;
for (Method method : clazz.getDeclaredMethods()) {
// Проверяем, есть ли у метода наша аннотация
if (method.isAnnotationPresent(MyTest.class)) {
// Получаем объект аннотации
MyTest annotation = method.getAnnotation(MyTest.class);
System.out.printf("Running test: %s, Info: %s%n",
method.getName(), annotation.info());
// Здесь мог бы быть код для вызова тестового метода
// method.invoke(...)
}
}
}
}
// Вывод:
// Running test: testMethod2, Info: default info
// Running test: testMethod1, Info: This is test method 1
Безопасность и Модули
52. Безопасное хранение паролей. Предотвращение SQL-инъекций.
Этот вопрос затрагивает два критически важных аспекта безопасности приложений.
Часть 1: Безопасное хранение паролей
Никогда нельзя хранить пароли в открытом виде. Хранить нужно только их хэш.
Хэширование — это одностороннее преобразование данных произвольной длины в строку фиксированной длины (хэш). Восстановить исходные данные из хэша невозможно.
Процесс аутентификации с хэшами: 1. Регистрация: Пользователь вводит пароль. Приложение вычисляет хэш от этого пароля и сохраняет хэш в базу данных. 2. Вход: Пользователь снова вводит пароль. Приложение вычисляет хэш от введенного пароля и сравнивает его с хэшем, хранящимся в базе. Если хэши совпадают, аутентификация успешна.
Требования к современным алгоритмам хэширования паролей:
- Использование “соли” (Salt): Соль — это случайная строка, уникальная для каждого пользователя. Она добавляется к паролю перед хэшированием. Это защищает от атак по радужным таблицам (pre-computed rainbow tables), так как даже у пользователей с одинаковыми паролями будут разные хэши. Соль хранится в базе данных вместе с хэшем.
- Использование адаптивных (медленных) функций: Алгоритмы типа MD5 или SHA-256 очень быстрые. Это хорошо для проверки целостности файлов, но плохо для паролей, так как позволяет злоумышленникам очень быстро перебирать варианты (brute-force). Нужно использовать алгоритмы, которые можно намеренно “замедлить”, увеличив вычислительную сложность.
Рекомендуемые алгоритмы: - BCrypt: Широко используемый, надежный и проверенный временем. - SCrypt: Разработан как более устойчивый к атакам с использованием специализированного оборудования (GPU/FPGA), чем BCrypt, за счет большего потребления памяти. - Argon2: Победитель конкурса Password Hashing Competition (2015), считается самым современным и надежным стандартом.
Пример с использованием BCrypt (библиотека Spring Security):
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class PasswordSecurity {
public static void main(String[] args) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String rawPassword = "my_super_secret_password123";
// 1. Хэширование при регистрации
String hashedPassword = passwordEncoder.encode(rawPassword);
System.out.println("Hashed Password: " + hashedPassword);
// Пример вывода: $2a$10$N9pQ3K2h8h.QyZ.YwXf8Iu9K6n1t2w.sD3eF.G5h.H7j.I8i.M4a
// 2. Проверка при входе
System.out.println("Does raw password match? " +
passwordEncoder.matches(rawPassword, hashedPassword)); // true
System.out.println("Does wrong password match? " +
passwordEncoder.matches("wrongpassword", hashedPassword)); // false
}
}
Часть 2: Предотвращение SQL-инъекций
SQL-инъекция — это один из самых распространенных и опасных типов атак на веб-приложения. Атака заключается во внедрении в SQL-запрос произвольного SQL-кода, что позволяет злоумышленнику обойти аутентификацию, получить несанкционированный доступ к данным, изменить или удалить их.
Уязвимый код (пример):
String userName = request.getParameter("username"); // Получаем данные от пользователя
String sql = "SELECT * FROM users WHERE username = '" + userName + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);
Если пользователь введет admin' --
, то итоговый запрос станет: SELECT * FROM users WHERE username = 'admin' --'
Символы --
в SQL означают начало комментария, и оставшаяся часть запроса (например, проверка пароля) будет проигнорирована.
Основной и самый надежный способ предотвращения SQL-инъекций — использование параметризованных запросов (Prepared Statements).
PreparedStatement
- При использовании PreparedStatement
SQL-запрос и данные передаются на сервер СУБД раздельно. - Сначала отправляется шаблон запроса с плейсхолдерами (?
). Сервер компилирует этот шаблон. - Затем отправляются данные для плейсхолдеров. Драйвер базы данных и сервер гарантируют, что эти данные будут интерпретированы только как данные, а не как часть исполняемого SQL-кода. Они автоматически экранируются.
Безопасный код:
String userName = request.getParameter("username");
String sql = "SELECT * FROM users WHERE username = ?"; // Используем плейсхолдер
try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
pstmt.setString(1, userName); // Безопасно устанавливаем параметр
ResultSet rs = pstmt.executeQuery();
// ... обработка результата
}
Другие меры защиты: - Валидация ввода: Всегда проверяйте данные, полученные от пользователя. Например, если ожидается число, убедитесь, что это число. - Принцип наименьших привилегий: Пользователь базы данных, от имени которого работает приложение, должен иметь только те права, которые ему абсолютно необходимы. Например, ему не нужны права на DROP TABLE
. - Использование ORM (Object-Relational Mapping) фреймворков: Фреймворки типа Hibernate или JPA по умолчанию используют параметризованные запросы, что снижает риск ошибки. - Экранирование спецсимволов: В крайнем случае, если использование PreparedStatement
невозможно (например, при динамическом формировании имен таблиц), необходимо вручную экранировать все спецсимволы, но этот подход сложен и подвержен ошибкам. Всегда предпочитайте PreparedStatement
.
53. Система модулей в Java. Провайдеры служб.
Система модулей Java (Java Platform Module System, JPMS), также известная как Project Jigsaw, была введена в Java 9. Ее основная цель — решить проблемы, связанные с “адом JAR-файлов” (JAR hell
), и улучшить структуру, безопасность и производительность больших приложений.
Что такое модуль? - Модуль — это новый уровень агрегации кода, выше чем пакет. Это набор связанных пакетов и ресурсов, снабженный файлом-дескриптором module-info.java
. - Модуль явно объявляет: - requires
: от каких других модулей он зависит. - exports
: какие из своих пакетов он делает доступными для других модулей.
Основные цели и преимущества JPMS:
- Надежная конфигурация (Reliable Configuration): Модульная система проверяет зависимости на этапе запуска. Если модуль
A
требует модульB
, аB
отсутствует, приложение не запустится. Это лучше, чем получитьClassNotFoundException
в середине работы. - Строгая инкапсуляция (Strong Encapsulation): По умолчанию все пакеты внутри модуля являются приватными для него. Только те пакеты, которые явно помечены как
exports
, видны другим модулям. Это предотвращает использование внутренних, не-API классов (например,sun.misc.Unsafe
, доступ к которому теперь ограничен). - Масштабируемая платформа Java (Scalable Java Platform): Сама JDK была разделена на модули (
java.base
,java.sql
,java.xml
и т.д.). Это позволяет создавать кастомные сборки JRE, включающие только необходимые модули, что значительно уменьшает размер развертываемого приложения (актуально для микросервисов и IoT).
Файл module-info.java
:
module com.myapp.main {
// 1. Зависимости
requires java.sql; // Зависимость от модуля java.sql
requires com.external.lib; // Зависимость от другого модуля
// 2. Экспорт пакетов
exports com.myapp.main.api; // Делаем пакет api видимым для других модулей
// Пакет com.myapp.main.internal останется скрытым
// 3. Провайдеры и потребители служб
uses com.myapp.services.SomeService; // Этот модуль является потребителем службы
provides com.myapp.services.SomeService with com.myapp.main.internal.SomeServiceImpl; // Этот модуль предоставляет реализацию службы
}
Провайдеры служб (Service Providers)
Это механизм, который позволяет достичь слабой связанности (loose coupling) между компонентами. Он существовал и до Java 9, но был полностью интегрирован в модульную систему.
Концепция: 1. Служба (Service): Это набор интерфейсов и классов, определяющих некоторую функциональность (например, java.sql.Driver
, javax.imageio.spi.ImageReaderSpi
). 2. Провайдер службы (Service Provider): Это конкретная реализация службы (например, JDBC-драйвер для PostgreSQL, плагин для чтения PNG-изображений). 3. Потребитель службы (Service Consumer): Код, которому нужна эта служба. Он не знает о конкретных реализациях, а работает только с интерфейсом.
Механизм java.util.ServiceLoader
: - ServiceLoader
— это класс, который позволяет находить и загружать все доступные реализации (провайдеры) данной службы в classpath или modulepath.
Как это работает в модульной системе: - Интерфейс службы определяется в одном модуле. - Потребитель в своем module-info.java
указывает uses com.example.MyService;
. - Провайдер в своем module-info.java
указывает provides com.example.MyService with com.example.internal.MyServiceImpl;
. - Потребитель использует ServiceLoader
для получения всех доступных реализаций.
// Потребительский код
ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
for (MyService service : loader) {
// Используем найденную реализацию
service.doSomething();
}
ServiceLoader
автоматически найдет все модули, которые “предоставляют” MyService
, и создаст их экземпляры. Это позволяет легко добавлять новую функциональность (например, поддержку нового формата файлов, новой СУБД) простым добавлением нового JAR-модуля в modulepath
, не меняя код потребителя.
54. Фреймворк fork/join. Класс ForkJoinPool и его использование.
Фреймворк Fork/Join — это реализация шаблона “разделяй и властвуй”, предназначенная для эффективного распараллеливания задач на многоядерных процессорах. Он был добавлен в Java 7 и является основой для параллельных стримов (parallelStream()
) в Java 8.
Основная идея: 1. Разделение (Fork): Если задача слишком велика для выполнения в одном потоке, она рекурсивно разбивается на более мелкие подзадачи. 2. Выполнение (Join): Эти подзадачи выполняются параллельно. После завершения выполнения подзадачи ее результат объединяется с результатами других подзадач.
Ключевые классы:
ForkJoinPool
:- Это специальный
ExecutorService
, оптимизированный для работы с задачамиForkJoinTask
. - По умолчанию создается общий пул (
common pool
) для всего приложения, который и используют параллельные стримы. Его можно получить черезForkJoinPool.commonPool()
. - Главная особенность — “воровство работы” (work-stealing): Если поток в пуле завершил все свои задачи, он не простаивает, а “ворует” задачу из очереди другого, более загруженного потока. Это позволяет максимально эффективно загрузить все ядра процессора.
- Это специальный
ForkJoinTask<V>
:- Абстрактный класс для задач, выполняемых в
ForkJoinPool
. АналогFuture
. - Две основные реализации:
RecursiveTask<V>
: для задач, которые возвращают результат.RecursiveAction
: для задач, которые не возвращают результат (аналогRunnable
).
- Абстрактный класс для задач, выполняемых в
Процесс создания задачи:
- Создать класс, наследующийся от
RecursiveTask<V>
илиRecursiveAction
. - Переопределить его абстрактный метод
compute()
. - Внутри
compute()
:- Проверить базовый случай: Если задача достаточно мала, выполнить ее напрямую (не рекурсивно). Это называется порогом (threshold).
- Рекурсивный шаг: Если задача велика:
- Разбить ее на две (или более) подзадачи.
- Вызвать метод
fork()
для одной из подзадач. Это асинхронно ставит ее в очередь на выполнение. - Выполнить вторую подзадачу напрямую в текущем потоке, вызвав ее
compute()
. - Дождаться результата первой подзадачи, вызвав метод
join()
на ней. - Объединить результаты.
Пример: параллельное суммирование элементов массива
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class ArraySumTask extends RecursiveTask<Long> {
private final int[] array;
private final int start;
private final int end;
private static final int THRESHOLD = 10000; // Порог для прямого выполнения
public ArraySumTask(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
int length = end - start;
if (length <= THRESHOLD) {
// Задача достаточно мала - считаем напрямую
long sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
return sum;
} else {
// Задача велика - разделяем
int middle = start + length / 2;
ArraySumTask leftTask = new ArraySumTask(array, start, middle);
ArraySumTask rightTask = new ArraySumTask(array, middle, end);
// Асинхронно запускаем левую часть
leftTask.fork();
// Синхронно выполняем правую часть в текущем потоке
Long rightResult = rightTask.compute();
// Ждем и получаем результат левой части
Long leftResult = leftTask.join();
// Объединяем результаты
return leftResult + rightResult;
}
}
public static void main(String[] args) {
int[] data = new int[1_000_000];
for (int i = 0; i < data.length; i++) {
data[i] = i + 1;
}
ForkJoinPool pool = ForkJoinPool.commonPool();
ArraySumTask task = new ArraySumTask(data, 0, data.length);
Long result = pool.invoke(task); // Запустить и дождаться
System.out.println("Сумма: " + result);
}
}
Сетевое взаимодействие
55. Сетевое взаимодействие. Клиент и сервер. Протоколы TCP, UDP, их свойства.
Сетевое взаимодействие — это процесс обмена данными между двумя или более компьютерами (узлами) по сети. В большинстве случаев используется клиент-серверная архитектура.
- Сервер (Server): Программа, которая ожидает входящие запросы от клиентов, обрабатывает их и предоставляет какой-либо ресурс или услугу (например, веб-страницы, данные из базы).
- Клиент (Client): Программа, которая инициирует соединение с сервером, отправляет ему запрос и получает ответ.
Транспортные протоколы
Для передачи данных по сети поверх IP-протокола используются два основных транспортных протокола: TCP и UDP.
1. TCP (Transmission Control Protocol — Протокол управления передачей)
- Ориентированный на соединение (Connection-Oriented): Перед обменом данными между клиентом и сервером устанавливается виртуальное соединение через процедуру “тройного рукопожатия” (three-way handshake).
- Надежный (Reliable): TCP гарантирует, что данные будут доставлены без потерь, без ошибок и в правильном порядке. Это достигается за счет:
- Подтверждений (ACK): Получатель отправляет подтверждение о получении каждого сегмента данных.
- Повторной передачи (Retransmission): Если отправитель не получает подтверждение в течение определенного времени, он отправляет данные повторно.
- Нумерации сегментов: Каждый сегмент данных имеет порядковый номер, что позволяет получателю собрать их в правильной последовательности.
- Управление потоком (Flow Control): Получатель может “попросить” отправителя замедлить передачу, если не успевает обрабатывать данные.
- Тяжеловесный: Установка соединения и механизмы обеспечения надежности создают дополнительные накладные расходы.
Применение: HTTP/HTTPS (веб), FTP (передача файлов), SMTP (электронная почта), SSH, базы данных. Везде, где критически важна целостность и порядок данных.
2. UDP (User Datagram Protocol — Протокол пользовательских датаграмм)
- Без установления соединения (Connectionless): Данные (датаграммы) отправляются получателю без предварительного “рукопожатия”.
- Ненадежный (Unreliable): UDP не дает никаких гарантий:
- Датаграммы могут быть потеряны.
- Они могут прийти в неправильном порядке.
- Они могут быть дублированы.
- Быстрый и легковесный: Отсутствие механизмов надежности делает UDP очень быстрым и с минимальными накладными расходами.
- Ориентированный на сообщения (Message-Oriented): Сохраняет границы сообщений. Если вы отправили 100 байт, получатель получит либо 100 байт за одно чтение, либо ничего. TCP же является потоковым (stream-oriented).
Применение: VoIP (голосовая связь), онлайн-игры, потоковое видео (стриминг), DNS. Везде, где скорость важнее надежности, и где потеря небольшого количества данных не критична.
Свойство | TCP | UDP |
---|---|---|
Соединение | Устанавливается | Не устанавливается |
Надежность | Гарантирована (порядок, целостность) | Не гарантирована |
Скорость | Медленнее | Быстрее |
Накладные расходы | Выше (заголовки, подтверждения) | Ниже |
Модель передачи | Поток байт (Stream) | Датаграммы (сообщения) |
Примеры | HTTP, FTP, SMTP | DNS, VoIP, онлайн-игры |
56. Неблокирующий сетевой обмен. Селекторы.
Блокирующий ввод-вывод (Blocking I/O) - Традиционная модель, используемая в java.net
(Socket
, ServerSocket
). - Когда поток вызывает метод read()
или accept()
, он блокируется и ждет, пока данные не поступят или пока не установится новое соединение. - Проблема: Чтобы обслуживать множество клиентов одновременно, нужно создавать отдельный поток для каждого клиента. Это плохо масштабируется и приводит к большому потреблению ресурсов (память на стеки, переключение контекста).
Неблокирующий ввод-вывод (Non-Blocking I/O) - Модель, предоставляемая пакетом java.nio
(New I/O). - Каналы (SocketChannel
, ServerSocketChannel
) можно перевести в неблокирующий режим (channel.configureBlocking(false)
). - Вызовы read()
и accept()
в этом режиме возвращаются немедленно, даже если данных или соединений нет (они вернут 0 или null
).
Проблема неблокирующего I/O: Как узнать, когда можно читать/писать? Постоянно опрашивать все каналы в цикле (busy-waiting
) — это крайне неэффективно и загружает процессор.
Решение: Селекторы (java.nio.channels.Selector
)
Селектор — это объект, который позволяет одному потоку эффективно отслеживать состояние множества каналов и определять, какие из них готовы к выполнению операций ввода-вывода.
Как это работает: 1. Создается Selector
. 2. Каждый канал (Channel
), который мы хотим отслеживать, регистрируется на селекторе. При регистрации указывается, какие именно события нас интересуют. Эти события представлены константами в классе SelectionKey
: - OP_READ
: Канал готов к чтению. - OP_WRITE
: Канал готов к записи. - OP_CONNECT
: Завершилось установление TCP-соединения (для клиента). - OP_ACCEPT
: Серверный канал готов принять новое соединение (для сервера). 3. Основной поток входит в цикл и вызывает блокирующий метод selector.select()
. Этот метод “засыпает” до тех пор, пока хотя бы на одном из зарегистрированных каналов не произойдет интересующее нас событие. 4. Когда select()
возвращает управление, он возвращает количество “готовых” каналов. 5. Мы получаем набор “ключей” (SelectionKey
) для готовых каналов через selector.selectedKeys()
. 6. Мы итерируемся по этому набору ключей, для каждого ключа определяем тип события (key.isReadable()
, key.isAcceptable()
, etc.) и выполняем соответствующую операцию (читаем данные, принимаем соединение). 7. Важно: После обработки ключ необходимо удалить из набора selectedKeys
вручную (iterator.remove()
).
Преимущества: - Масштабируемость: Один поток может обслуживать тысячи одновременных соединений. - Эффективность: Нет необходимости создавать поток на каждого клиента, что экономит ресурсы. - Эта модель лежит в основе многих высокопроизводительных сетевых фреймворков, таких как Netty.
57. Протокол ТСР. Классы Socket и ServerSocket.
Эти классы из пакета java.net
представляют собой классическую, блокирующую реализацию TCP-клиента и сервера.
ServerSocket
(Сервер) - Представляет собой серверный сокет, который “слушает” определенный порт на предмет входящих клиентских соединений. - Основной жизненный цикл: 1. Создание: ServerSocket serverSocket = new ServerSocket(int port);
— создает сокет и привязывает его к указанному порту. 2. Ожидание соединения: Socket clientSocket = serverSocket.accept();
— это блокирующий вызов. Поток останавливается и ждет, пока какой-нибудь клиент не подключится. Когда соединение установлено, метод возвращает объект Socket
для общения с этим конкретным клиентом. 3. Общение: Получение потоков ввода/вывода из clientSocket
(getInputStream
, getOutputStream
) для обмена данными с клиентом. 4. Закрытие: clientSocket.close()
, serverSocket.close()
.
Пример простого эхо-сервера:
public class EchoServer {
public static void main(String[] args) throws IOException {
try (ServerSocket serverSocket = new ServerSocket(8189)) {
System.out.println("Сервер запущен, ожидает клиентов...");
while (true) {
// Блокирующий вызов - ждем клиента
Socket clientSocket = serverSocket.accept();
System.out.println("Клиент подключился: " + clientSocket.getInetAddress());
// Для обслуживания нескольких клиентов нужен новый поток
new Thread(() -> {
try (InputStream in = clientSocket.getInputStream();
OutputStream out = clientSocket.getOutputStream();
Scanner scanner = new Scanner(in);
PrintWriter writer = new PrintWriter(out, true)) {
writer.println("Привет! Введите 'bye' для выхода.");
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
if ("bye".equalsIgnoreCase(line)) {
break;
}
writer.println("Эхо: " + line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
clientSocket.close();
} catch (IOException e) { e.printStackTrace(); }
System.out.println("Клиент отключился.");
}
}).start();
}
}
}
}
Socket
(Клиент) - Представляет собой конечную точку соединения между двумя машинами. - Основной жизненный цикл: 1. Создание и подключение: Socket socket = new Socket(String host, int port);
— создает сокет и пытается установить соединение с сервером по указанному адресу и порту. Этот вызов также является блокирующим. 2. Общение: Получение потоков ввода/вывода (getInputStream
, getOutputStream
) для отправки запросов и получения ответов. 3. Закрытие: socket.close()
.
58. Протокол UDP. Класс DatagramChannel.
Этот класс из пакета java.nio.channels
является частью неблокирующего API для работы с UDP. DatagramChannel
может быть как клиентом, так и сервером.
Основные особенности: - Неориентирован на соединение: Методы send()
и receive()
работают с датаграммами. - Может быть в блокирующем или неблокирующем режиме. - DatagramChannel
не разделяется на клиентский и серверный. Один и тот же канал может и отправлять, и получать данные.
Привязка (Binding) и Подключение (Connecting): - bind(SocketAddress local)
: Привязывает канал к локальному адресу и порту, чтобы он мог получать датаграммы. Это типичный шаг для “сервера”. - connect(SocketAddress remote)
: “Подключает” канал к удаленному адресу. Это не устанавливает TCP-соединение, а лишь фиксирует адрес по умолчанию для отправки и получения. После connect()
можно использовать методы read()
и write()
(как в TCP), а не send()
и receive()
. Это также позволяет получать датаграммы только от указанного адреса.
Отправка и получение данных: - Данные отправляются и получаются через ByteBuffer
. - Отправка: channel.send(ByteBuffer src, SocketAddress target)
- Получение: SocketAddress sourceAddress = channel.receive(ByteBuffer dst)
— это блокирующий вызов (в блокирующем режиме), который возвращает адрес отправителя.
Пример UDP-сервера на DatagramChannel
:
public class UdpServerNio {
public static void main(String[] args) throws IOException {
try (DatagramChannel channel = DatagramChannel.open()) {
channel.socket().bind(new InetSocketAddress(8189));
System.out.println("UDP Сервер запущен...");
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
// Блокирующее получение данных
SocketAddress clientAddress = channel.receive(buffer);
buffer.flip();
String message = StandardCharsets.UTF_8.decode(buffer).toString().trim();
System.out.printf("Получено '%s' от %s%n", message, clientAddress);
// Отправляем эхо-ответ
String response = "Эхо: " + message;
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8));
channel.send(responseBuffer, clientAddress);
}
}
}
}
59. Протокол ТСР. Классы SocketChannel и ServerSocketChannel.
Эти классы из java.nio.channels
представляют неблокирующий API для работы с TCP. Они являются современной альтернативой Socket
и ServerSocket
.
ServerSocketChannel
(Сервер) - Аналог ServerSocket
, но для NIO. - Работа в неблокирующем режиме: 1. ServerSocketChannel ssc = ServerSocketChannel.open();
2. ssc.bind(new InetSocketAddress(port));
3. ssc.configureBlocking(false);
— переключение в неблокирующий режим. 4. SocketChannel clientChannel = ssc.accept();
— в неблокирующем режиме этот вызов возвращается немедленно. Если нет новых клиентов, он вернет null
. - Обычно используется вместе с Selector
для эффективной обработки множества соединений.
SocketChannel
(Клиент и серверное представление клиента) - Аналог Socket
. - Представляет TCP-соединение. - Операции read()
и write()
работают с ByteBuffer
. - Клиентская сторона: - SocketChannel sc = SocketChannel.open();
- sc.configureBlocking(false);
- sc.connect(new InetSocketAddress(host, port));
— в неблокирующем режиме этот метод инициирует подключение и возвращает управление. Завершение подключения нужно отслеживать через Selector
с OP_CONNECT
или в цикле вызывать finishConnect()
. - Серверная сторона: Экземпляр SocketChannel
возвращается методом ServerSocketChannel.accept()
.
Эти каналы — основа для построения высокопроизводительных TCP-серверов с использованием селекторов, как описано в вопросе 56.
60. Протокол UDP. Классы DatagramSocket и DatagramPacket.
Эти классы из пакета java.net
представляют собой классическую, блокирующую реализацию UDP-клиента и сервера.
DatagramPacket
- Это контейнер для UDP-данных. Он инкапсулирует: - Массив байтов с данными (byte[]
). - Длину данных. - IP-адрес и порт отправителя/получателя. - Используется как для отправки, так и для получения данных.
DatagramSocket
- Представляет собой сокет для отправки и приема UDP-датаграмм. - Для “сервера” (получателя): - DatagramSocket socket = new DatagramSocket(int port);
— создает сокет и привязывает его к порту для прослушивания. - Для “клиента” (отправителя): - DatagramSocket socket = new DatagramSocket();
— создает сокет на любом свободном порту.
Основные методы: - void send(DatagramPacket p)
: Отправляет датаграмму. - void receive(DatagramPacket p)
: Блокирующий вызов. Ждет получения датаграммы и заполняет ею переданный объект DatagramPacket
.
Пример UDP-клиента на DatagramSocket
:
public class UdpClient {
public static void main(String[] args) throws IOException {
try (DatagramSocket socket = new DatagramSocket()) {
InetAddress address = InetAddress.getByName("localhost");
String message = "Привет, UDP сервер!";
byte[] buffer = message.getBytes(StandardCharsets.UTF_8);
// 1. Создаем пакет для отправки
DatagramPacket sendPacket = new DatagramPacket(buffer, buffer.length, address, 8189);
socket.send(sendPacket);
System.out.println("Сообщение отправлено.");
// 2. Готовим пакет для получения ответа
byte[] receiveBuffer = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(receiveBuffer, receiveBuffer.length);
// 3. Блокируемся и ждем ответа
socket.receive(receivePacket);
String response = new String(receivePacket.getData(), 0, receivePacket.getLength(), StandardCharsets.UTF_8);
System.out.println("Получен ответ: " + response);
}
}
}