# Ответы на экзаменационные вопросы по Java / Kotlin > Все примеры кода взяты из реального учебного проекта — клиент-серверного приложения с GUI на JavaFX. --- ## Часть I: GUI, JavaFX, Локализация --- ## 1. Компоненты пользовательского интерфейса. Иерархия компонентов ### Что такое компонент? **Компонент** (Component) — любой визуальный элемент GUI: кнопка, текстовое поле, таблица, окно. В Java/Kotlin GUI компоненты организованы в **иерархию наследования**. ### Иерархия в AWT/Swing ``` java.lang.Object └── java.awt.Component ← базовый визуальный элемент ├── java.awt.Container ← может содержать другие компоненты │ ├── java.awt.Window │ │ ├── java.awt.Frame │ │ └── java.awt.Dialog │ └── javax.swing.JComponent ← база всех Swing-виджетов │ ├── JPanel │ ├── JButton │ ├── JLabel │ ├── JTextField │ ├── JTable │ └── ... (все J-компоненты) └── java.awt.Button (AWT) ``` ### Иерархия в JavaFX (используется в проекте) В JavaFX нет AWT/Swing — другое дерево: ``` javafx.scene.Node ← базовый узел сцены ├── javafx.scene.Parent ← может содержать дочерние узлы │ ├── javafx.scene.layout.Pane ← менеджер компоновки │ │ ├── VBox, HBox, GridPane, StackPane, BorderPane... │ └── javafx.scene.control.Control ← интерактивные элементы │ ├── Button, Label, TextField, PasswordField │ ├── ComboBox, CheckBox, ToggleButton │ ├── TableView, MenuButton, MenuItem │ └── ToolBar, SplitPane, Separator └── javafx.scene.canvas.Canvas ← поверхность для рисования ``` ### Концепция Stage → Scene → Node ``` Stage (окно ОС) └── Scene (контент окна) └── Node (корневой элемент — обычно Pane) └── ... (дерево виджетов) ``` ### Из проекта: Main.kt ```kotlin class App : Application() { override fun start(stage: Stage) { AuthView(stage) { // Stage = окно ОС val mainStage = Stage() MainView(mainStage).show() }.show() } } ``` ### Из проекта: MainView.kt — дерево компонентов ```kotlin private fun buildUI(): BorderPane { val root = BorderPane() // корневой контейнер root.top = buildToolbar() // ToolBar сверху root.center = buildContent() // SplitPane посередине root.bottom = buildCommandBar() // HBox снизу return root } ``` `SplitPane` внутри делится на `TableView` (список объектов) и `Canvas` (визуализация). --- ## 2. Базовые классы Component, Container, JComponent ### `java.awt.Component` Самый базовый класс AWT. Описывает **любой визуальный элемент**: имеет позицию (x, y), размер, цвет, шрифт. Ключевые методы: | Метод | Описание | |---|---| | `setVisible(boolean)` | показать / скрыть | | `setEnabled(boolean)` | включить / выключить | | `setSize(int, int)` | задать размер | | `repaint()` | перерисовать | | `addMouseListener(...)` | подписаться на мышь | ### `java.awt.Container` Расширяет `Component`. Может **содержать другие компоненты** (дочерние). Ключевые методы: | Метод | Описание | |---|---| | `add(Component)` | добавить дочерний компонент | | `remove(Component)` | удалить | | `setLayout(LayoutManager)` | назначить менеджер компоновки | | `getComponents()` | получить массив дочерних | ### `javax.swing.JComponent` Расширяет `Container`. База **всех Swing-виджетов** (кнопок, полей, таблиц). Добавляет: - поддержку **двойной буферизации** (нет мерцания) - механизм **бордеров** (`setBorder(...)`) - поддержку **ToolTip** (`setToolTipText(...)`) - систему **рисования через paintComponent(Graphics g)** **Пример переопределения рисования в Swing:** ```java class MyPanel extends JPanel { @Override protected void paintComponent(Graphics g) { super.paintComponent(g); g.setColor(Color.RED); g.fillOval(10, 10, 100, 100); } } ``` ### JavaFX-аналог в проекте: Canvas В JavaFX рисование — через `Canvas` + `GraphicsContext`: ```kotlin // CollectionCanvas.kt class CollectionCanvas : Canvas() { private val gc: GraphicsContext = graphicsContext2D // аналог Graphics private fun drawItem(item: HumanBeingProperty, glowIntensity: Double) { val px = worldToScreenX(item.coordinateX.value.toDouble()) val py = worldToScreenY(item.coordinateY.value.toDouble()) val radius = mapImpactSpeed(item.impactSpeed.value) val color = ownerColor(item.ownerId.value) gc.fill = color gc.fillOval(px - radius, py - radius, radius * 2, radius * 2) gc.stroke = Color.WHITE gc.strokeOval(px - radius, py - radius, radius * 2, radius * 2) } } ``` --- ## 3. Менеджеры компоновки **Менеджер компоновки (Layout Manager)** — объект, который автоматически расставляет компоненты внутри контейнера. Без него пришлось бы вручную задавать координаты каждого элемента. ### Основные менеджеры в Swing (AWT) | Менеджер | Поведение | |---|---| | `FlowLayout` | компоненты идут слева направо, как слова в тексте | | `BorderLayout` | 5 зон: NORTH, SOUTH, EAST, WEST, CENTER | | `GridLayout` | сетка одинаковых ячеек N×M | | `GridBagLayout` | гибкая сетка с весами и отступами | | `BoxLayout` | горизонтальная или вертикальная линия | | `CardLayout` | стопка карточек, показывается одна | | `null` | абсолютное позиционирование (setBounds) | **Пример Swing:** ```java JPanel panel = new JPanel(new BorderLayout()); panel.add(new JButton("Сверху"), BorderLayout.NORTH); panel.add(new JTextField(), BorderLayout.CENTER); panel.add(new JButton("Снизу"), BorderLayout.SOUTH); ``` ### Менеджеры компоновки в JavaFX (используются в проекте) JavaFX встраивает менеджер прямо в класс контейнера: | Класс | Аналог Swing | Описание | |---|---|---| | `VBox` | BoxLayout вертикальный | дети вертикально | | `HBox` | BoxLayout горизонтальный | дети горизонтально | | `GridPane` | GridBagLayout | гибкая сетка по строкам/столбцам | | `BorderPane` | BorderLayout | 5 зон: top, bottom, left, right, center | | `StackPane` | LayeredPane | компоненты стопкой (z-порядок) | | `SplitPane` | JSplitPane | делится на части с разделителем | | `ToolBar` | JToolBar | панель инструментов | ### Из проекта: EditDialog.kt — GridPane для формы ```kotlin private fun buildUI(): VBox { val form = GridPane() form.hgap = 8.0 // горизонтальный отступ между ячейками form.vgap = 8.0 // вертикальный отступ form.padding = Insets(16.0) form.alignment = Pos.CENTER_LEFT var row = 0 form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.name") }, nameField) row++ form.addRow(row, Label().also { AppLocale.bindLabel(it, "table.coordinates.x") }, coordXField) row++ // ... return VBox(8.0, form, buttonBar) // VBox: форма + кнопки } ``` ### Из проекта: MainView.kt — BorderPane + SplitPane ```kotlin private fun buildUI(): BorderPane { val root = BorderPane() root.top = buildToolbar() // ToolBar: вверху root.center = buildContent() // SplitPane: по центру root.bottom = buildCommandBar() // HBox: внизу return root } private fun buildContent(): Node { val splitPane = SplitPane() splitPane.orientation = Orientation.HORIZONTAL splitPane.items.addAll(buildTablePane(), buildCanvasPane()) splitPane.setDividerPositions(0.55) // 55% левой части return splitPane } ``` --- ## 4. Модель обработки событий. Класс-слушатель и класс-событие ### Суть модели событий В Java GUI используется **паттерн Observer (наблюдатель)**: 1. **Источник события** (компонент) — кнопка, поле и т.д. 2. **Событие** (Event) — объект, описывающий что произошло. 3. **Слушатель** (Listener) — объект с методом-обработчиком, подписанный на источник. ### Класс-событие Каждое событие — объект класса, наследующего `java.util.EventObject`: ``` java.util.EventObject └── java.awt.AWTEvent ├── java.awt.event.ActionEvent (нажатие кнопки) ├── java.awt.event.MouseEvent (мышь) ├── java.awt.event.KeyEvent (клавиатура) ├── java.awt.event.WindowEvent (окно) └── java.awt.event.ItemEvent (выбор в списке) ``` Событие содержит: источник (`getSource()`), тип события, дополнительные данные (координаты мыши, код клавиши и т.д.). ### Класс-слушатель (интерфейс Listener) Каждый тип события — интерфейс с методом-обработчиком: | Интерфейс | Методы | Событие | |---|---|---| | `ActionListener` | `actionPerformed(ActionEvent)` | клик кнопки | | `MouseListener` | `mouseClicked/Pressed/Released/Entered/Exited` | мышь | | `KeyListener` | `keyPressed/Released/Typed` | клавиши | | `WindowListener` | `windowOpened/Closed/...` | окно | | `ChangeListener` | `stateChanged(ChangeEvent)` | изменение состояния | **Пример в Swing:** ```java JButton btn = new JButton("Нажми"); btn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("Кнопка нажата! Источник: " + e.getSource()); } }); ``` ### JavaFX: EventHandler вместо Listener В JavaFX используется generic-интерфейс `EventHandler`: ```kotlin button.setOnAction { event: ActionEvent -> println("Нажата кнопка") } ``` Иерархия событий JavaFX: ``` javafx.event.Event ├── javafx.event.ActionEvent (кнопки, меню) ├── javafx.scene.input.MouseEvent ├── javafx.scene.input.KeyEvent └── javafx.collections.ListChangeListener.Change (коллекции) ``` ### Из проекта: обработка кликов мыши в CollectionCanvas.kt ```kotlin init { // Добавляем слушатель событий мыши addEventHandler(MouseEvent.MOUSE_CLICKED, this::onClick) } private fun onClick(event: MouseEvent) { val mx = event.x // координаты клика val my = event.y val items = Session.collection.toList() for (item in items) { val px = worldToScreenX(item.coordinateX.value.toDouble()) val py = worldToScreenY(item.coordinateY.value.toDouble()) val r = mapImpactSpeed(item.impactSpeed.value) val dx = mx - px val dy = my - py if (dx * dx + dy * dy <= (r + hitRadius) * (r + hitRadius)) { showItemInfo(item) // показать информацию об объекте return } } } ``` ### Из проекта: слушатель изменения коллекции в MainController.kt ```kotlin // ListChangeListener — JavaFX-аналог Observer на ObservableList private val collectionListener = ListChangeListener { _ -> reapplyFiltersAndSort() // пересчитать при любом изменении } fun start() { Session.collection.addListener(collectionListener) // подписка } fun stop() { Session.collection.removeListener(collectionListener) // отписка } ``` ### Из проекта: слушатель изменения текста в MainView.kt ```kotlin val filterField = TextField() filterField.textProperty().addListener { _, _, _ -> // _ = observable, _ = oldValue, _ = newValue applyFilters() } ``` --- ## 5. Технология JavaFX. Особенности архитектуры, отличия от AWT/Swing ### Ключевые отличия | Характеристика | AWT | Swing | JavaFX | |---|---|---|---| | Год появления | 1995 | 1997 | 2008 (JavaFX 2.0 в 2011) | | Рендеринг | нативные виджеты ОС | Java 2D (программный) | Hardware-ускорение (Prism) | | UI-описание | Java-код | Java-код | FXML (XML) + Java/Kotlin | | CSS-стилизация | нет | ограничено | полная поддержка CSS | | Анимация | нет | Timer | Animation API (Timeline, AnimationTimer) | | Связывание данных | нет | нет | Property/Binding API | | Потокобезопасность | EDT (Event Dispatch Thread) | EDT | JavaFX Application Thread | | 3D-поддержка | нет | нет | да (Scene3D) | | WebView | нет | нет | да (встроенный браузер) | ### Архитектура JavaFX ``` Application ├── Stage (окно) │ └── Scene │ └── Scene Graph (дерево Node) ├── JavaFX Application Thread (поток UI) └── Prism (рендеринг через GPU: OpenGL / DirectX / Metal) ``` **Scene Graph** — это граф объектов (`Node`). Каждый узел имеет трансформации, эффекты, CSS-класс. Это основное отличие от AWT/Swing. ### Property и Binding JavaFX вводит **реактивное связывание**: `Property` — обёртка над значением, которая уведомляет слушателей при изменении. ```kotlin // HumanBeingProperty.kt — каждое поле объекта — Property val name = SimpleStringProperty(humanBeing.name) val realHero = SimpleBooleanProperty(humanBeing.realHero) val coordinateX = SimpleObjectProperty(humanBeing.coordinates.x) ``` Это позволяет привязывать (bind) поля UI прямо к данным: ```kotlin // TableColumn автоматически отображает значение Property tableCol.cellValueFactory = Callback { features -> SimpleObjectProperty(extractor(features.value)) } ``` ### JavaFX Application Thread Весь UI живёт в одном потоке — **JavaFX Application Thread**. Если нужно обновить UI из фонового потока — используют `Platform.runLater { }`. ### Из проекта: Platform.runLater в AuthController.kt ```kotlin fun authenticate(..., onResult: (Boolean, String) -> Unit) { scope.launch { // фоновый поток val response = provider.auth(...) // сетевой запрос // Обновление UI — только через Platform.runLater! Platform.runLater { onResult(true, "") } } } ``` ### Из проекта: AnimationTimer в CollectionCanvas.kt ```kotlin private val renderTimer: AnimationTimer = object : AnimationTimer() { override fun handle(now: Long) { animationTime = now / 1_000_000_000.0 // время в секундах render() // перерисовка каждый кадр (~60 FPS) } } ``` `AnimationTimer` — JavaFX-механизм игрового цикла, вызывающий `handle()` перед каждым кадром. Аналогов в Swing нет. ### Из проекта: ObservableList в Session.kt ```kotlin object Session { // ObservableList — специальный список JavaFX: // уведомляет слушателей при добавлении/удалении элементов val collection: ObservableList = FXCollections.observableArrayList() } ``` `TableView` напрямую привязан к `ObservableList` — при изменении коллекции таблица обновляется автоматически. --- ## 6. Интернационализация. Локализация. Хранение локализованных ресурсов ### Интернационализация (i18n) vs Локализация (l10n) - **Интернационализация (i18n)** — проектирование программы так, чтобы её можно было адаптировать под разные языки/регионы *без* изменения кода. - **Локализация (l10n)** — конкретная адаптация под конкретный язык/регион (перевод, форматы дат и чисел). ### Стандартный Java-подход: ResourceBundle + .properties файлы **Структура файлов:** ``` src/main/resources/ messages.properties ← fallback (английский) messages_ru.properties ← русский messages_de.properties ← немецкий messages_fr_FR.properties ← французский (Франция) ``` **Содержимое messages_ru.properties:** ```properties auth.title=Вход / Регистрация button.add=Добавить error.not.owner=Вы не владелец этого объекта ``` **Использование в коде:** ```java Locale locale = new Locale("ru"); ResourceBundle bundle = ResourceBundle.getBundle("messages", locale); String title = bundle.getString("auth.title"); // → "Вход / Регистрация" ``` ### Объект `Locale` `java.util.Locale` описывает язык+регион: ```java Locale ruLocale = new Locale("ru"); // русский Locale enAuLocale = new Locale("en", "AU"); // английский (Австралия) Locale.getDefault() // локаль системы ``` ### Подход в проекте: Map-based AppLocale.kt В проекте локализация реализована через **HashMap** прямо в коде (без .properties файлов). Строки хранятся в `Map>`: ```kotlin object AppLocale { val currentLocale: ObjectProperty = SimpleObjectProperty(Locale("ru")) private val strings: Map> = mapOf( Locale("ru") to mapOf( "auth.title" to "Вход / Регистрация", "button.add" to "Добавить", "error.not.owner" to "Вы не владелец этого объекта", // ... ), Locale("nl") to mapOf( "auth.title" to "Inloggen / Registreren", "button.add" to "Toevoegen", "error.not.owner" to "U bent niet de eigenaar van dit object", ), Locale("hu") to mapOf( "auth.title" to "Bejelentkezés / Regisztráció", // ... ), Locale("en", "AU") to mapOf( "auth.title" to "Sign In / Sign Up", "auth.error.empty" to "Fill in all fields, mate", // ... ), ) fun get(key: String): String = strings[currentLocale.value]?.get(key) ?: key } ``` ### Динамическое переключение языка Ключевая особенность — **реактивная смена языка в реальном времени** через `Property` + `bind`: ```kotlin // Привязка текста кнопки к ключу локализации fun bind(property: Property, key: String) { property.value = get(key) currentLocale.addListener { _, _, _ -> property.value = get(key) // автообновление при смене языка } } fun bindLabel(labeled: Labeled, key: String) { bind(labeled.textProperty(), key) } ``` В `MainView.kt` при смене локали: ```kotlin localeCombo.setOnAction { val selected = localeCombo.selectionModel.selectedItem ?: return@setOnAction AppLocale.currentLocale.set(selected.locale) // → все bind()-ы обновятся updateLocale() } ``` --- ## 7. Форматирование локализованных данных. NumberFormat, DateFormat, MessageFormat, ChoiceFormat ### `java.text.NumberFormat` Форматирует числа в зависимости от локали: ```java NumberFormat nf = NumberFormat.getInstance(Locale.GERMANY); nf.format(1234567.89); // → "1.234.567,89" (точка — разделитель тысяч, запятая — дробная) NumberFormat nf2 = NumberFormat.getInstance(Locale.US); nf2.format(1234567.89); // → "1,234,567.89" // Валюта NumberFormat cf = NumberFormat.getCurrencyInstance(new Locale("ru")); cf.format(1500.00); // → "1 500,00 ₽" // Проценты NumberFormat pf = NumberFormat.getPercentInstance(Locale.US); pf.format(0.75); // → "75%" ``` ### `java.text.DateFormat` / `SimpleDateFormat` Форматирует даты по локали: ```java DateFormat df = DateFormat.getDateInstance(DateFormat.FULL, new Locale("ru")); df.format(new Date()); // → "пятница, 12 июня 2026 г." DateFormat dfEn = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US); dfEn.format(new Date()); // → "6/12/26" // SimpleDateFormat — задаём паттерн вручную SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy HH:mm", new Locale("ru")); sdf.format(new Date()); // → "12.06.2026 16:00" ``` Паттерны: `d` = день, `M` = месяц, `y` = год, `H` = час (24ч), `m` = минута, `s` = секунда. ### `java.text.MessageFormat` Форматирует строки с параметрами (шаблоны с `{0}`, `{1}`): ```java String template = "Пользователь {0} добавил {1} объект(ов)"; MessageFormat mf = new MessageFormat(template, new Locale("ru")); String result = mf.format(new Object[]{"Alice", 3}); // → "Пользователь Alice добавил 3 объект(ов)" ``` Можно задавать форматирование прямо в шаблоне: ```java // {1,number,#.##} — число с 2 знаками после запятой String tpl = "Скорость: {0,number,#.##} м/с"; MessageFormat.format(tpl, 123.456789); // → "Скорость: 123.46 м/с" // {1,date,short} — короткая дата String tpl2 = "Создан: {0,date,short}"; MessageFormat.format(tpl2, new Date()); // → "Создан: 12.06.26" ``` ### `java.text.ChoiceFormat` Выбирает строку в зависимости от числового значения (для правильного склонения): ```java // Паттерн: "0#нет файлов|1#один файл|2#два файла|5#пять файлов" // Числа задают границы диапазонов double[] limits = {0, 1, 2, 5}; String[] formats = {"нет файлов", "один файл", "два файла", "много файлов"}; ChoiceFormat cf = new ChoiceFormat(limits, formats); cf.format(0); // → "нет файлов" cf.format(1); // → "один файл" cf.format(3); // → "два файла" cf.format(10); // → "много файлов" ``` `ChoiceFormat` часто используют внутри `MessageFormat`: ```java String pattern = "У вас {0,choice,0#нет сообщений|1#одно сообщение|2<{0} сообщения}"; MessageFormat.format(pattern, 2); // → "У вас 2 сообщения" ``` ### В проекте: хранение локализованных дат В `HumanBeingProperty.kt` поле даты — `LocalDate`. При отображении в таблице оно конвертируется в строку через `toString()`. В продакшн-коде лучше использовать `DateTimeFormatter`: ```kotlin val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) .withLocale(AppLocale.currentLocale.value) creationDate.value.format(formatter) // → "12.06.26" для ru, "6/12/26" для en_US ``` --- ## Часть II: Многопоточность и JDBC --- ## 1. Многопоточность. Класс Thread, интерфейс Runnable, модификатор synchronized ### Поток (Thread) Поток — независимый поток выполнения кода в рамках одного JVM-процесса. Все потоки делят общую кучу (heap), но имеют свой стек. ### Создание потока: два способа **Способ 1: наследование от Thread** ```java class MyThread extends Thread { @Override public void run() { System.out.println("Выполняется в потоке: " + Thread.currentThread().getName()); } } MyThread t = new MyThread(); t.start(); // запуск (не run()!) ``` **Способ 2: реализация Runnable (предпочтительно)** ```java Runnable task = () -> System.out.println("Работаю в потоке"); Thread t = new Thread(task); t.start(); ``` `Runnable` — функциональный интерфейс с одним методом `void run()`. Предпочтителен, т.к. не ограничивает наследование. ### Жизненный цикл потока ``` NEW → RUNNABLE → (BLOCKED/WAITING/TIMED_WAITING) → TERMINATED ``` ### Модификатор `synchronized` Проблема: при одновременном доступе нескольких потоков к общим данным возникают **состояния гонки** (race conditions). `synchronized` гарантирует, что только один поток в момент времени выполняет защищённый блок. Работает через **монитор объекта** (встроенный мьютекс). ```java // 1. Синхронизированный метод (монитор = this) public synchronized void increment() { counter++; } // 2. Синхронизированный блок (явное указание монитора) public void increment() { synchronized (this) { counter++; } } // 3. Статический synchronized (монитор = Class объект) public static synchronized void staticMethod() { ... } ``` ### Из проекта: CollectionManager.kt — синхронизация коллекции ```kotlin class CollectionManager { // Collections.synchronizedSortedSet — потокобезопасная обёртка private val collection: MutableSet = Collections.synchronizedSortedSet(TreeSet()) fun update(id: UUID, updated: HumanBeing): Boolean { synchronized(collection) { // явный synchronized-блок val old = getById(id) ?: return false collection.remove(old) val replaced = updated.copy(id = old.id, creationDate = old.creationDate) return collection.add(replaced) } } fun getAll(): TreeSet { synchronized(collection) { return TreeSet(collection) // снимок коллекции } } } ``` ### Из проекта: CacheInvalidator.kt — фоновый поток ```kotlin class CacheInvalidator( private val dataSource: DataSource, private val onInvalidated: () -> Unit ) { @Volatile private var running = false private var thread: Thread? = null fun start() { running = true thread = Thread { // создание потока через Runnable (лямбда) while (running) { // слушаем PostgreSQL NOTIFY Thread.sleep(500) // пауза 500мс между проверками } }.also { it.isDaemon = true } // демон-поток: не мешает остановке JVM .apply { name = "pg-notify-listener" start() // запуск } } fun stop() { running = false thread?.interrupt() // прерываем сон через interrupt() } } ``` --- ## 2. Методы wait(), notify(), Lock и Condition ### wait() и notify() из класса Object Эти методы работают **только внутри synchronized-блока**. Они реализуют схему "ожидание условия": - `wait()` — текущий поток **освобождает монитор** и засыпает, пока другой поток не вызовет `notify()`. - `notify()` — будит **один** ожидающий поток. - `notifyAll()` — будит **все** ожидающие потоки. **Классический пример: очередь производитель-потребитель** ```java class Buffer { private final Queue queue = new LinkedList<>(); private final int capacity = 10; public synchronized void produce(int item) throws InterruptedException { while (queue.size() == capacity) { wait(); // ждём, пока не освободится место } queue.add(item); notify(); // будим потребителя } public synchronized int consume() throws InterruptedException { while (queue.isEmpty()) { wait(); // ждём, пока не появится элемент } int item = queue.poll(); notify(); // будим производителя return item; } } ``` ### Интерфейс `Lock` (java.util.concurrent.locks) `Lock` — более гибкая альтернатива `synchronized`. Основная реализация — `ReentrantLock`. | Возможность | synchronized | ReentrantLock | |---|---|---| | Явное получение/освобождение | нет | `lock()` / `unlock()` | | Попытка без блокировки | нет | `tryLock()` | | Попытка с таймаутом | нет | `tryLock(time, unit)` | | Прерываемое ожидание | нет | `lockInterruptibly()` | | Справедливость (FIFO) | нет | `new ReentrantLock(true)` | ```java Lock lock = new ReentrantLock(); try { lock.lock(); // захват монитора // критическая секция } finally { lock.unlock(); // ВСЕГДА в finally! } // tryLock — не блокируется if (lock.tryLock()) { try { /* работа */ } finally { lock.unlock(); } } else { System.out.println("Ресурс занят"); } ``` ### Интерфейс `Condition` `Condition` — аналог `wait()/notify()` для `Lock`. Создаётся из объекта `Lock`: ```java Lock lock = new ReentrantLock(); Condition notEmpty = lock.newCondition(); Condition notFull = lock.newCondition(); // Производитель: lock.lock(); try { while (queue.size() == capacity) { notFull.await(); // ждём "не полон" } queue.add(item); notEmpty.signal(); // сигналим "не пуст" } finally { lock.unlock(); } // Потребитель: lock.lock(); try { while (queue.isEmpty()) { notEmpty.await(); // ждём "не пуст" } int item = queue.poll(); notFull.signal(); // сигналим "не полон" } finally { lock.unlock(); } ``` --- ## 3. Классы-синхронизаторы из пакета java.util.concurrent `java.util.concurrent` предоставляет готовые синхронизаторы для типовых сценариев. ### Основные синхронизаторы | Класс | Назначение | |---|---| | `Semaphore` | ограничение числа одновременных доступов | | `CountDownLatch` | ожидание выполнения N задач | | `CyclicBarrier` | синхронизация N потоков в точке | | `Exchanger` | обмен объектами между двумя потоками | | `Phaser` | гибкий многофазный барьер | ### `Semaphore` — контролируем число одновременных доступов ```java // Не более 3 потоков одновременно могут работать с ресурсом Semaphore semaphore = new Semaphore(3); void accessResource() throws InterruptedException { semaphore.acquire(); // захватить "разрешение" try { // работа с ограниченным ресурсом } finally { semaphore.release(); // вернуть "разрешение" } } ``` ### `CountDownLatch` — ждём завершения N задач ```java CountDownLatch latch = new CountDownLatch(3); // ждём 3 задачи for (int i = 0; i < 3; i++) { new Thread(() -> { // выполняем задачу latch.countDown(); // уменьшаем счётчик }).start(); } latch.await(); // главный поток ждёт, пока счётчик не = 0 System.out.println("Все 3 задачи выполнены!"); ``` ### `CyclicBarrier` — все потоки ждут друг друга в точке ```java CyclicBarrier barrier = new CyclicBarrier(3, () -> { System.out.println("Все потоки достигли барьера!"); }); Runnable task = () -> { // часть работы... barrier.await(); // ждём остальных // продолжаем... }; ``` ### В проекте: готовые пулы потоков как синхронизированное решение В `NetworkManager.kt` сервера используются пулы потоков вместо ручной синхронизации: ```kotlin private val readerPool = Executors.newFixedThreadPool(4) // 4 потока-читателя private val processingPool = ForkJoinPool( // пул для обработки Runtime.getRuntime().availableProcessors() ) private val senderPool = ForkJoinPool( // пул для отправки Runtime.getRuntime().availableProcessors() ) ``` --- ## 4. Модификатор volatile. Атомарные типы данных и операции ### Проблема видимости (visibility) Каждый поток может кешировать значения переменных в **регистрах CPU или L1/L2 кеше**. Изменение переменной одним потоком может быть невидимо другим. ### `volatile` `volatile` гарантирует: 1. **Видимость**: запись всегда идёт в main memory, чтение — всегда из main memory (без кеша). 2. **Запрет переупорядочивания** (happens-before на запись/чтение). ```java volatile boolean running = true; // без volatile второй поток может не увидеть false // Поток 1: running = false; // Поток 2: while (running) { // без volatile этот цикл может никогда не закончиться! } ``` **Ограничение**: `volatile` НЕ делает операцию `i++` атомарной, т.к. это три операции: read → increment → write. ### Из проекта: `@Volatile` в CacheInvalidator.kt ```kotlin @Volatile // Kotlin-аналог volatile private var running = false fun start() { running = true thread = Thread { while (running) { // читаем актуальное значение каждый раз try { // ... Thread.sleep(500) } catch (e: Exception) { if (!running) break // проверяем после пробуждения } } } } fun stop() { running = false // запись видна другому потоку сразу thread?.interrupt() } ``` ### Атомарные типы: `java.util.concurrent.atomic` Атомарные типы позволяют делать составные операции (read-modify-write) **атомарно** без `synchronized`. Реализованы через CPU-инструкции **CAS (Compare-And-Swap)**. | Класс | Описание | |---|---| | `AtomicInteger` | атомарный `int` | | `AtomicLong` | атомарный `long` | | `AtomicBoolean` | атомарный `boolean` | | `AtomicReference` | атомарная ссылка | | `AtomicIntegerArray` | атомарный массив int | ```java AtomicInteger counter = new AtomicInteger(0); counter.incrementAndGet(); // атомарный i++, возвращает новое counter.getAndIncrement(); // атомарный i++, возвращает старое counter.addAndGet(5); // атомарный i += 5 counter.compareAndSet(5, 10); // если == 5, то установить 10 (CAS) ``` ### Из проекта: AtomicInteger в GatewayManager.kt ```kotlin // Атомарный счётчик для round-robin балансировки нагрузки private val roundRobinCounter = AtomicInteger(0) private fun pickBackend(): BackendServer? { val alive = backends.filter { it.isAlive } if (alive.isEmpty()) return null // getAndIncrement() — атомарно: read + increment + write return alive[roundRobinCounter.getAndIncrement() % alive.size] } ``` --- ## 5. Коллекции из пакета java.util.concurrent Обычные коллекции (`ArrayList`, `HashMap`, `TreeSet`) **не потокобезопасны**. `Collections.synchronizedXxx()` делает их потокобезопасными, но с глобальной блокировкой (плохая производительность). `java.util.concurrent` предоставляет высокопроизводительные потокобезопасные коллекции. ### Основные коллекции | Класс | Описание | |---|---| | `ConcurrentHashMap` | HashMap с сегментной блокировкой (высокая пропускная способность) | | `CopyOnWriteArrayList` | список: запись = копирование, чтение без блокировки | | `CopyOnWriteArraySet` | то же, но Set | | `ConcurrentLinkedQueue` | неблокирующая очередь (CAS) | | `ConcurrentLinkedDeque` | неблокирующая двусторонняя очередь | | `LinkedBlockingQueue` | блокирующая очередь (производитель-потребитель) | | `PriorityBlockingQueue` | блокирующая очередь с приоритетом | | `ArrayBlockingQueue` | ограниченная блокирующая очередь | ### `ConcurrentHashMap` ```java ConcurrentHashMap map = new ConcurrentHashMap<>(); map.put("key", 1); map.putIfAbsent("key", 2); // атомарная операция map.computeIfAbsent("key", k -> 42); // атомарное вычисление ``` ### `CopyOnWriteArrayList` При каждой записи создаётся **копия массива**. Читатели не блокируются. Оптимален для редких записей и частых чтений. ```java List list = new CopyOnWriteArrayList<>(); list.add("a"); // копирует массив, добавляет, заменяет ссылку // Итерация безопасна даже при параллельных записях: for (String s : list) { /* ... */ } ``` ### `LinkedBlockingQueue` — производитель-потребитель ```java BlockingQueue queue = new LinkedBlockingQueue<>(100); // Производитель: queue.put(task); // блокируется если очередь полна // Потребитель: Task t = queue.take(); // блокируется если очередь пуста ``` ### Из проекта: CopyOnWriteArrayList в GatewayManager.kt ```kotlin // CopyOnWriteArrayList: список серверов читается часто (при каждом запросе), // пишется редко (при обнаружении/удалении серверов) private val backends = CopyOnWriteArrayList() // Безопасное чтение без блокировки: private fun pickBackend(): BackendServer? { val alive = backends.filter { it.isAlive } // итерация без блокировки // ... } // Запись (создаёт копию): backends.addAllAbsent(newBackends) backends.removeAll { ... } ``` --- ## 6. Интерфейсы Executor, ExecutorService, Callable, Future ### Проблема прямого создания потоков Создавать `new Thread()` для каждой задачи — дорого. Лучше **переиспользовать потоки** через пулы. ### `Executor` Самый простой интерфейс: принимает `Runnable` на выполнение: ```java interface Executor { void execute(Runnable command); } ``` ### `ExecutorService` Расширяет `Executor`. Добавляет управление жизненным циклом и возможность получать результаты: ```java ExecutorService es = Executors.newFixedThreadPool(4); // submit Runnable (нет результата): es.submit(() -> System.out.println("задача")); // submit Callable (с результатом): Future future = es.submit(() -> 2 + 2); int result = future.get(); // блокирует до получения результата // Завершение: es.shutdown(); // ждёт завершения задач es.shutdownNow(); // прерывает запущенные задачи ``` ### `Callable` Как `Runnable`, но **возвращает результат** и **может бросить checked-исключения**: ```java interface Callable { V call() throws Exception; } Callable task = () -> { // долгая операция return "результат"; }; ``` ### `Future` Представляет **будущий результат** асинхронной задачи: ```java Future future = executorService.submit(task); future.isDone(); // завершена ли задача? future.isCancelled(); // отменена? future.cancel(true); // попытка отмены (true = прервать поток) future.get(); // блокирует до готовности future.get(5, TimeUnit.SECONDS); // с таймаутом ``` ### Фабричные методы `Executors` ```java Executors.newFixedThreadPool(4) // пул из 4 потоков Executors.newCachedThreadPool() // динамический пул (рождаются по требованию) Executors.newSingleThreadExecutor() // один поток, очередь задач Executors.newScheduledThreadPool(2) // для отложенных/периодических задач ``` ### Из проекта: ExecutorService в NetworkManager.kt ```kotlin // Сервер использует несколько пулов для изоляции задач: private val readerPool = Executors.newFixedThreadPool(4) // читают сокет private val processingPool = ForkJoinPool(...) // обрабатывают запросы private val senderPool = ForkJoinPool(...) // отправляют ответы fun start() { repeat(4) { readerPool.submit(::readLoop) // запуск 4 задач чтения } } fun stop() { socket.close() readerPool.shutdownNow() // прерываем чтение processingPool.shutdown() // ждём обработки senderPool.shutdown() // ждём отправки } ``` ### Из проекта: ScheduledExecutorService в GatewayManager.kt ```kotlin private val healthPool = Executors.newScheduledThreadPool(2) private fun startHealthChecks() { // Запускать проверку каждые 5 секунд healthPool.scheduleAtFixedRate( { backends.forEach { probeServer(it) } }, healthCheckIntervalMs, // начальная задержка healthCheckIntervalMs, // интервал TimeUnit.MILLISECONDS ) } ``` --- ## 7. Пулы потоков **Пул потоков** — набор заранее созданных потоков, которые **переиспользуются** для выполнения задач. ### Зачем нужны пулы - Создание потока — дорогая операция (~1мс, ~1МБ памяти на стек). - Бесконтрольное создание потоков перегрузит систему. - Пул: потоки создаются один раз, задачи ставятся в очередь. ### Типы пулов | Метод `Executors` | Поведение | |---|---| | `newFixedThreadPool(n)` | ровно n потоков; очередь задач не ограничена | | `newCachedThreadPool()` | потоки создаются при необходимости, удаляются через 60с простоя | | `newSingleThreadExecutor()` | 1 поток, задачи выполняются последовательно | | `newScheduledThreadPool(n)` | n потоков, поддержка задержек и периодических задач | | `ForkJoinPool(n)` | work-stealing: поток может "украсть" задачу у другого | ### `ForkJoinPool` и рекурсивные задачи Оптимален для **рекурсивных задач** (divide & conquer). Использует **work-stealing**: незанятый поток берёт задачу из очереди другого потока. ```java // RecursiveTask — возвращает результат class SumTask extends RecursiveTask { @Override protected Long compute() { if (размер_маленький) { return посчитать_напрямую(); } SumTask left = new SumTask(левая_половина); SumTask right = new SumTask(правая_половина); left.fork(); // запустить левую асинхронно return right.compute() + left.join(); // правую здесь + ждать левую } } ForkJoinPool pool = new ForkJoinPool(); long result = pool.invoke(new SumTask(data)); ``` ### Из проекта: ForkJoinPool для обработки запросов (NetworkManager.kt) ```kotlin // RecursiveTask для обработки запроса inner class ProcessRequestTask( private val json: String, private val address: InetAddress, private val clientPort: Int ) : RecursiveTask() { override fun compute() { val response = invoker.execute(request) // выполнить команду senderPool.submit(SendResponseTask(response, address, clientPort)) // отправить } } // RecursiveAction для отправки (нет возвращаемого значения) inner class SendResponseTask(...) : RecursiveAction() { override fun compute() { val bytes = AppJson.encodeToString(...).toByteArray() val packet = DatagramPacket(bytes, bytes.size, address, clientPort) synchronized(socket) { socket.send(packet) } } } ``` Аналогично устроен `GatewayManager.kt` с `ProxyTask` и `SendTask`. --- ## 8. JDBC. Порядок взаимодействия с базой данных. DriverManager. Connection ### Что такое JDBC **JDBC (Java Database Connectivity)** — стандартный API для работы с реляционными БД из Java. Предоставляет единый интерфейс независимо от типа БД (PostgreSQL, MySQL, SQLite...). ### Порядок взаимодействия с БД ``` 1. Загрузить драйвер 2. Получить соединение (Connection) через DriverManager 3. Создать Statement или PreparedStatement 4. Выполнить запрос 5. Обработать ResultSet 6. Закрыть ресурсы (ResultSet → Statement → Connection) ``` ### `DriverManager` — получение соединения ```java // Строка подключения: jdbc:<субпротокол>://<хост>:<порт>/<база> String url = "jdbc:postgresql://localhost:5432/mydb"; String user = "postgres"; String pass = "secret"; Connection conn = DriverManager.getConnection(url, user, pass); ``` `DriverManager` автоматически находит нужный драйвер (если он в classpath). При подключении к PostgreSQL нужен артефакт `org.postgresql:postgresql`. ### `Connection` — соединение с БД `java.sql.Connection` — главный объект, представляющий открытое соединение. ```java // Управление транзакциями: conn.setAutoCommit(false); // отключить авто-коммит // ... выполнить запросы ... conn.commit(); // подтвердить транзакцию conn.rollback(); // откатить при ошибке // Закрытие: conn.close(); // ОБЯЗАТЕЛЬНО закрывать! ``` Лучшая практика — try-with-resources: ```java try (Connection conn = DriverManager.getConnection(url, user, pass)) { // работа с conn } // автоматически закрывается ``` ### Connection Pool: HikariCP в проекте В проекте используется **HikariCP** — пул соединений. Соединения дорого создавать, поэтому их переиспользуют: ```kotlin // DatabaseManager.kt fun start() { val config = HikariConfig().apply { jdbcUrl = System.getenv("DB_URL") ?: "jdbc:postgresql://localhost:5432/prog7_db" driverClassName = "org.postgresql.Driver" username = System.getenv("DB_USER") ?: error("DB_USER not set") password = System.getenv("DB_PASS") ?: error("DB_PASS not set") maximumPoolSize = 11 // максимум 11 соединений в пуле isAutoCommit = false transactionIsolation = "TRANSACTION_REPEATABLE_READ" } dataSource = HikariDataSource(config) Database.connect(dataSource) // Exposed ORM использует этот пул } ``` --- ## 9. Интерфейсы Statement, PreparedStatement, ResultSet, RowSet ### `Statement` — простой запрос Выполняет SQL-запрос без параметров. Уязвим к **SQL-инъекциям** — никогда не используйте для запросов с пользовательским вводом! ```java Statement stmt = conn.createStatement(); // DML (изменение данных): int affected = stmt.executeUpdate("DELETE FROM users WHERE id = 1"); // DQL (выборка): ResultSet rs = stmt.executeQuery("SELECT * FROM users"); // DDL (CREATE, DROP, ALTER): stmt.execute("CREATE TABLE test (id INT)"); stmt.close(); ``` ### `PreparedStatement` — параметризованный запрос Компилируется один раз, выполняется много раз с разными параметрами. Защищает от SQL-инъекций. ```java PreparedStatement pstmt = conn.prepareStatement( "INSERT INTO users (username, password) VALUES (?, ?)" ); pstmt.setString(1, "alice"); // параметр 1 pstmt.setString(2, "hashed"); // параметр 2 pstmt.executeUpdate(); // выполнить // Переиспользование: pstmt.setString(1, "bob"); pstmt.setString(2, "hash2"); pstmt.executeUpdate(); pstmt.close(); ``` **Batch-выполнение:** ```java PreparedStatement pstmt = conn.prepareStatement("INSERT INTO t VALUES (?)"); for (int i = 0; i < 1000; i++) { pstmt.setInt(1, i); pstmt.addBatch(); // добавить в пакет } pstmt.executeBatch(); // выполнить весь пакет одним запросом к БД ``` ### `ResultSet` — результат запроса Курсор для итерации по строкам результата: ```java PreparedStatement pstmt = conn.prepareStatement( "SELECT id, name, speed FROM humanBeing WHERE owner_id = ?" ); pstmt.setString(1, ownerId.toString()); ResultSet rs = pstmt.executeQuery(); while (rs.next()) { // перейти к следующей строке UUID id = UUID.fromString(rs.getString("id")); String name = rs.getString("name"); double speed = rs.getDouble("speed"); System.out.println(id + ": " + name + " speed=" + speed); } rs.close(); ``` Методы получения значений: `getString(col)`, `getInt(col)`, `getDouble(col)`, `getBoolean(col)`, `getDate(col)`, `getObject(col)`. ### `RowSet` — автономный ResultSet `RowSet` — расширение `ResultSet`, которое: - может существовать **без открытого соединения** (`CachedRowSet`) - поддерживает **сериализацию** и **передачу по сети** - сам управляет соединением ```java // CachedRowSet — загружает данные в память, соединение закрывается CachedRowSet crs = RowSetProvider.newFactory().createCachedRowSet(); crs.setUrl("jdbc:postgresql://localhost:5432/db"); crs.setUsername("user"); crs.setPassword("pass"); crs.setCommand("SELECT * FROM users"); crs.execute(); // загрузить данные (соединение закроется) while (crs.next()) { System.out.println(crs.getString("username")); } // Соединение к БД уже закрыто, данные в памяти ``` ### В проекте: Exposed ORM поверх JDBC (Repository.kt) Проект использует **Exposed** (Kotlin ORM) как обёртку над JDBC. Exposed генерирует `PreparedStatement` автоматически: ```kotlin // Repository.kt — под капотом это PreparedStatement fun login(username: String, password: String): Pair? { var id: String? = null transaction { // Exposed генерирует: SELECT id FROM "user" WHERE username=? AND password=? id = UserTable.select(UserTable.id) .where { (UserTable.username eq username) and (UserTable.password eq password) } .singleOrNull() ?.get(UserTable.id)?.toString() } // ... } fun write(item: HumanBeing): UUID? { transaction { // INSERT INTO coordinates (x, y) VALUES (?, ?) val cords = CoordinatesTable.insertAndGetId { it[x] = item.coordinates.x it[y] = item.coordinates.y } // INSERT INTO humanBeing (...) VALUES (...) humanId = HumanBeingTable.insertAndGetId { it[owner_id] = item.ownerId it[name] = item.name it[coordinates] = cords // ... }.value exec("NOTIFY cache_changed") // PostgreSQL NOTIFY } } ``` --- ## 10. Шаблоны проектирования **Шаблон проектирования (Design Pattern)** — проверенное решение типовой проблемы в проектировании ПО. Делятся на три группы: - **Порождающие** (Creational) — создание объектов - **Структурные** (Structural) — организация классов - **Поведенческие** (Behavioral) — взаимодействие объектов --- ### Команда (Command) — поведенческий **Проблема**: нужно инкапсулировать запрос как объект, поддерживать отмену операций, логировать команды. **Решение**: каждое действие — отдельный класс, реализующий интерфейс `Command`. ```kotlin // Интерфейс команды (Command.kt) interface Command { val name: String val description: String val args: List val type: CommandType fun execute(args: List, humanBeing: HumanBeing? = null, userId: String = ""): Response } ``` ```kotlin // Конкретная команда (AddCommand.kt) class AddCommand( private val manager: CollectionManager, private val repository: Repository ) : Command { override val name = "add" override val description = "Добавить объект в коллекцию" override val type = CommandType.MODEL override val args = listOf(CommandArgType.NONE) override fun execute(args: List, humanBeing: HumanBeing?, userId: String): Response { val id = repository.write(humanBeing!!) ?: return Response(false, "Ошибка БД") val saved = humanBeing.copy(id = id) manager.add(saved) return Response(true, "Объект добавлен: $id") } } ``` ```kotlin // Инвокер (CommandInvoker.kt) — хранит и запускает команды class CommandInvoker(private val jwtManager: JWTManager) { private val commands = mutableMapOf() private val history = ArrayDeque() fun register(command: Command) { commands[command.name] = command } fun execute(request: Request): Response { val command = commands[request.commandName] ?: return Response(false, "Неизвестная команда") addToHistory(request.commandName) return command.execute(request.args, request.humanBeing, userId) } } ``` Сервер регистрирует все команды в `Main.kt`: ```kotlin fun registerServerCommands(invoker: CommandInvoker, manager: CollectionManager, repository: Repository) { listOf( InfoCommand(manager), ShowCommand(manager), AddCommand(manager, repository), UpdateCommand(manager, repository), RemoveByIdCommand(manager, repository), // ... ).forEach { invoker.register(it) } } ``` --- ### Observer (Наблюдатель) — поведенческий **Проблема**: объект должен уведомлять другие объекты об изменениях, не зная, кто именно слушает. В JavaFX это реализовано через `ObservableList` и `ListChangeListener`: ```kotlin // Session.kt — Observable (субъект) val collection: ObservableList = FXCollections.observableArrayList() // CollectionCanvas.kt — Observer (наблюдатель) Session.collection.addListener { _: ListChangeListener.Change? -> scheduleRender() // перерисовать при любом изменении коллекции } // MainController.kt — ещё один Observer private val collectionListener = ListChangeListener { _ -> reapplyFiltersAndSort() } fun start() { Session.collection.addListener(collectionListener) } ``` --- ### Singleton — порождающий **Проблема**: объект должен существовать в единственном экземпляре. В проекте это `Session` и `AppLocale` через Kotlin-объекты (`object`): ```kotlin // Session.kt — единственный экземпляр сессии object Session { var client: ClientProvider? = null var username: String = "" val collection: ObservableList = FXCollections.observableArrayList() } // AppLocale.kt — единственный менеджер локализации object AppLocale { val currentLocale: ObjectProperty = SimpleObjectProperty(Locale("ru")) fun get(key: String): String = strings[currentLocale.value]?.get(key) ?: key } ``` --- ### MVC (Model-View-Controller) — архитектурный Проект использует паттерн **MVC**: | Слой | Классы в проекте | Описание | |---|---|---| | **Model** | `HumanBeing`, `Session`, `CollectionManager` | данные и бизнес-логика | | **View** | `MainView`, `AuthView`, `EditDialog`, `CollectionCanvas` | UI, отображение | | **Controller** | `MainController`, `AuthController` | связь View и Model | ```kotlin // View (MainView.kt) создаёт Controller и вызывает его методы: class MainView(private val stage: Stage) { private val controller = MainController() private fun onAdd() { val dialog = EditDialog(stage.scene.window) val humanBeing = dialog.showAndWait() ?: return controller.executeCommand(name = "add", humanBeing = humanBeing) { success, message -> if (success) controller.requestRefresh() else showError(message) } } } // Controller (MainController.kt) координирует логику: class MainController { fun executeCommand(name: String, ..., callback: (Boolean, String) -> Unit) { scope.launch { // фоновый поток val response = Session.client?.execute(name, args, humanBeing) Platform.runLater { callback(success, message) } // обновление View } } } ``` --- ### Strategy (Стратегия) — поведенческий **Проблема**: нужно иметь несколько алгоритмов и переключаться между ними в runtime. В проекте реализована через `Comparator` при сортировке: ```kotlin // MainView.kt — динамическое создание стратегии сортировки private fun toggleSort(key: String) { val comparator: Comparator = Comparator { a, b -> val va = extractSortValue(a, key) val vb = extractSortValue(b, key) if (sortAscending) va.compareTo(vb) else vb.compareTo(va) } controller.updateComparators(listOf(comparator)) } // StreamUtils.kt — применение стратегии fun applyFiltersAndSort(..., comparators: List>): ObservableList<...> { if (comparators.isNotEmpty()) { val combined = comparators.reduce { a, b -> a.thenComparing(b) } stream = stream.sorted(combined) // применяем выбранную стратегию } } ``` --- ### Proxy — структурный **Проблема**: нужен объект-посредник для контроля доступа к другому объекту. В проекте — `GatewayManager` выступает прокси-сервером: ```kotlin // GatewayManager.kt — принимает запросы клиентов и перенаправляет на backend inner class ProxyTask(...) : RecursiveTask() { override fun compute() { val backend = pickBackend() // выбрать живой сервер (round-robin) // Прозрачно пересылаем запрос на backend: DatagramSocket().use { bs -> bs.send(DatagramPacket(data, data.size, InetAddress.getByName(backend.host), backend.port)) bs.receive(respPacket) senderPool.submit(SendTask(respPacket.data.copyOf(respPacket.length), clientAddress, clientPort)) } } } ```