Files
Lab8-Prog/docs stuff/java_answers.md
T
2026-06-12 17:42:16 +03:00

65 KiB
Raw Blame History

Ответы на экзаменационные вопросы по 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

class App : Application() {
    override fun start(stage: Stage) {
        AuthView(stage) {           // Stage = окно ОС
            val mainStage = Stage()
            MainView(mainStage).show()
        }.show()
    }
}

Из проекта: MainView.kt — дерево компонентов

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:

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:

// 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:

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 для формы

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

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:

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<T extends Event>:

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

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

// ListChangeListener — JavaFX-аналог Observer на ObservableList
private val collectionListener = ListChangeListener<HumanBeingProperty> { _ ->
    reapplyFiltersAndSort()  // пересчитать при любом изменении
}

fun start() {
    Session.collection.addListener(collectionListener)  // подписка
}

fun stop() {
    Session.collection.removeListener(collectionListener)  // отписка
}

Из проекта: слушатель изменения текста в MainView.kt

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<T> — обёртка над значением, которая уведомляет слушателей при изменении.

// HumanBeingProperty.kt — каждое поле объекта — Property
val name = SimpleStringProperty(humanBeing.name)
val realHero = SimpleBooleanProperty(humanBeing.realHero)
val coordinateX = SimpleObjectProperty<Long>(humanBeing.coordinates.x)

Это позволяет привязывать (bind) поля UI прямо к данным:

// TableColumn автоматически отображает значение Property
tableCol.cellValueFactory = Callback { features ->
    SimpleObjectProperty(extractor(features.value))
}

JavaFX Application Thread

Весь UI живёт в одном потоке — JavaFX Application Thread. Если нужно обновить UI из фонового потока — используют Platform.runLater { }.

Из проекта: Platform.runLater в AuthController.kt

fun authenticate(..., onResult: (Boolean, String) -> Unit) {
    scope.launch {                           // фоновый поток
        val response = provider.auth(...)   // сетевой запрос
        
        // Обновление UI — только через Platform.runLater!
        Platform.runLater { onResult(true, "") }
    }
}

Из проекта: AnimationTimer в CollectionCanvas.kt

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

object Session {
    // ObservableList — специальный список JavaFX:
    // уведомляет слушателей при добавлении/удалении элементов
    val collection: ObservableList<HumanBeingProperty> = 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:

auth.title=Вход / Регистрация
button.add=Добавить
error.not.owner=Вы не владелец этого объекта

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

Locale locale = new Locale("ru");
ResourceBundle bundle = ResourceBundle.getBundle("messages", locale);
String title = bundle.getString("auth.title");  // → "Вход / Регистрация"

Объект Locale

java.util.Locale описывает язык+регион:

Locale ruLocale = new Locale("ru");           // русский
Locale enAuLocale = new Locale("en", "AU");   // английский (Австралия)
Locale.getDefault()                            // локаль системы

Подход в проекте: Map-based AppLocale.kt

В проекте локализация реализована через HashMap прямо в коде (без .properties файлов). Строки хранятся в Map<Locale, Map<String, String>>:

object AppLocale {
    val currentLocale: ObjectProperty<Locale> = SimpleObjectProperty(Locale("ru"))

    private val strings: Map<Locale, Map<String, String>> = 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:

// Привязка текста кнопки к ключу локализации
fun bind(property: Property<String>, key: String) {
    property.value = get(key)
    currentLocale.addListener { _, _, _ ->
        property.value = get(key)  // автообновление при смене языка
    }
}

fun bindLabel(labeled: Labeled, key: String) {
    bind(labeled.textProperty(), key)
}

В MainView.kt при смене локали:

localeCombo.setOnAction {
    val selected = localeCombo.selectionModel.selectedItem ?: return@setOnAction
    AppLocale.currentLocale.set(selected.locale)  // → все bind()-ы обновятся
    updateLocale()
}

7. Форматирование локализованных данных. NumberFormat, DateFormat, MessageFormat, ChoiceFormat

java.text.NumberFormat

Форматирует числа в зависимости от локали:

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

Форматирует даты по локали:

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}):

String template = "Пользователь {0} добавил {1} объект(ов)";
MessageFormat mf = new MessageFormat(template, new Locale("ru"));
String result = mf.format(new Object[]{"Alice", 3});
// → "Пользователь Alice добавил 3 объект(ов)"

Можно задавать форматирование прямо в шаблоне:

// {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

Выбирает строку в зависимости от числового значения (для правильного склонения):

// Паттерн: "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:

String pattern = "У вас {0,choice,0#нет сообщений|1#одно сообщение|2<{0} сообщения}";
MessageFormat.format(pattern, 2); // → "У вас 2 сообщения"

В проекте: хранение локализованных дат

В HumanBeingProperty.kt поле даты — LocalDate. При отображении в таблице оно конвертируется в строку через toString(). В продакшн-коде лучше использовать DateTimeFormatter:

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

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Выполняется в потоке: " + Thread.currentThread().getName());
    }
}
MyThread t = new MyThread();
t.start();  // запуск (не run()!)

Способ 2: реализация Runnable (предпочтительно)

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 гарантирует, что только один поток в момент времени выполняет защищённый блок. Работает через монитор объекта (встроенный мьютекс).

// 1. Синхронизированный метод (монитор = this)
public synchronized void increment() {
    counter++;
}

// 2. Синхронизированный блок (явное указание монитора)
public void increment() {
    synchronized (this) {
        counter++;
    }
}

// 3. Статический synchronized (монитор = Class объект)
public static synchronized void staticMethod() { ... }

Из проекта: CollectionManager.kt — синхронизация коллекции

class CollectionManager {
    // Collections.synchronizedSortedSet — потокобезопасная обёртка
    private val collection: MutableSet<HumanBeing> =
        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<HumanBeing> {
        synchronized(collection) {
            return TreeSet(collection)       // снимок коллекции
        }
    }
}

Из проекта: CacheInvalidator.kt — фоновый поток

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() — будит все ожидающие потоки.

Классический пример: очередь производитель-потребитель

class Buffer {
    private final Queue<Integer> 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)
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:

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 — контролируем число одновременных доступов

// Не более 3 потоков одновременно могут работать с ресурсом
Semaphore semaphore = new Semaphore(3);

void accessResource() throws InterruptedException {
    semaphore.acquire();  // захватить "разрешение"
    try {
        // работа с ограниченным ресурсом
    } finally {
        semaphore.release();  // вернуть "разрешение"
    }
}

CountDownLatch — ждём завершения N задач

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 — все потоки ждут друг друга в точке

CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("Все потоки достигли барьера!");
});

Runnable task = () -> {
    // часть работы...
    barrier.await();  // ждём остальных
    // продолжаем...
};

В проекте: готовые пулы потоков как синхронизированное решение

В NetworkManager.kt сервера используются пулы потоков вместо ручной синхронизации:

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 на запись/чтение).
volatile boolean running = true;  // без volatile второй поток может не увидеть false

// Поток 1:
running = false;

// Поток 2:
while (running) {
    // без volatile этот цикл может никогда не закончиться!
}

Ограничение: volatile НЕ делает операцию i++ атомарной, т.к. это три операции: read → increment → write.

Из проекта: @Volatile в CacheInvalidator.kt

@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<V> атомарная ссылка
AtomicIntegerArray атомарный массив int
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

// Атомарный счётчик для 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<K,V> HashMap с сегментной блокировкой (высокая пропускная способность)
CopyOnWriteArrayList<T> список: запись = копирование, чтение без блокировки
CopyOnWriteArraySet<T> то же, но Set
ConcurrentLinkedQueue<T> неблокирующая очередь (CAS)
ConcurrentLinkedDeque<T> неблокирующая двусторонняя очередь
LinkedBlockingQueue<T> блокирующая очередь (производитель-потребитель)
PriorityBlockingQueue<T> блокирующая очередь с приоритетом
ArrayBlockingQueue<T> ограниченная блокирующая очередь

ConcurrentHashMap

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
map.putIfAbsent("key", 2);              // атомарная операция
map.computeIfAbsent("key", k -> 42);   // атомарное вычисление

CopyOnWriteArrayList

При каждой записи создаётся копия массива. Читатели не блокируются. Оптимален для редких записей и частых чтений.

List<String> list = new CopyOnWriteArrayList<>();
list.add("a");  // копирует массив, добавляет, заменяет ссылку
// Итерация безопасна даже при параллельных записях:
for (String s : list) { /* ... */ }

LinkedBlockingQueue — производитель-потребитель

BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);

// Производитель:
queue.put(task);   // блокируется если очередь полна

// Потребитель:
Task t = queue.take();  // блокируется если очередь пуста

Из проекта: CopyOnWriteArrayList в GatewayManager.kt

// CopyOnWriteArrayList: список серверов читается часто (при каждом запросе),
// пишется редко (при обнаружении/удалении серверов)
private val backends = CopyOnWriteArrayList<BackendServer>()

// Безопасное чтение без блокировки:
private fun pickBackend(): BackendServer? {
    val alive = backends.filter { it.isAlive }  // итерация без блокировки
    // ...
}

// Запись (создаёт копию):
backends.addAllAbsent(newBackends)
backends.removeAll { ... }

6. Интерфейсы Executor, ExecutorService, Callable, Future

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

Создавать new Thread() для каждой задачи — дорого. Лучше переиспользовать потоки через пулы.

Executor

Самый простой интерфейс: принимает Runnable на выполнение:

interface Executor {
    void execute(Runnable command);
}

ExecutorService

Расширяет Executor. Добавляет управление жизненным циклом и возможность получать результаты:

ExecutorService es = Executors.newFixedThreadPool(4);

// submit Runnable (нет результата):
es.submit(() -> System.out.println("задача"));

// submit Callable (с результатом):
Future<Integer> future = es.submit(() -> 2 + 2);
int result = future.get();  // блокирует до получения результата

// Завершение:
es.shutdown();              // ждёт завершения задач
es.shutdownNow();           // прерывает запущенные задачи

Callable<V>

Как Runnable, но возвращает результат и может бросить checked-исключения:

interface Callable<V> {
    V call() throws Exception;
}

Callable<String> task = () -> {
    // долгая операция
    return "результат";
};

Future<V>

Представляет будущий результат асинхронной задачи:

Future<String> future = executorService.submit(task);

future.isDone();     // завершена ли задача?
future.isCancelled(); // отменена?
future.cancel(true); // попытка отмены (true = прервать поток)
future.get();        // блокирует до готовности
future.get(5, TimeUnit.SECONDS); // с таймаутом

Фабричные методы Executors

Executors.newFixedThreadPool(4)      // пул из 4 потоков
Executors.newCachedThreadPool()      // динамический пул (рождаются по требованию)
Executors.newSingleThreadExecutor()  // один поток, очередь задач
Executors.newScheduledThreadPool(2)  // для отложенных/периодических задач

Из проекта: ExecutorService в NetworkManager.kt

// Сервер использует несколько пулов для изоляции задач:
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

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: незанятый поток берёт задачу из очереди другого потока.

// RecursiveTask — возвращает результат
class SumTask extends RecursiveTask<Long> {
    @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)

// RecursiveTask для обработки запроса
inner class ProcessRequestTask(
    private val json: String,
    private val address: InetAddress,
    private val clientPort: Int
) : RecursiveTask<Unit>() {

    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 — получение соединения

// Строка подключения: 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 — главный объект, представляющий открытое соединение.

// Управление транзакциями:
conn.setAutoCommit(false);  // отключить авто-коммит
// ... выполнить запросы ...
conn.commit();              // подтвердить транзакцию
conn.rollback();            // откатить при ошибке

// Закрытие:
conn.close();               // ОБЯЗАТЕЛЬНО закрывать!

Лучшая практика — try-with-resources:

try (Connection conn = DriverManager.getConnection(url, user, pass)) {
    // работа с conn
} // автоматически закрывается

Connection Pool: HikariCP в проекте

В проекте используется HikariCP — пул соединений. Соединения дорого создавать, поэтому их переиспользуют:

// 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-инъекциям — никогда не используйте для запросов с пользовательским вводом!

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-инъекций.

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-выполнение:

PreparedStatement pstmt = conn.prepareStatement("INSERT INTO t VALUES (?)");
for (int i = 0; i < 1000; i++) {
    pstmt.setInt(1, i);
    pstmt.addBatch();    // добавить в пакет
}
pstmt.executeBatch();   // выполнить весь пакет одним запросом к БД

ResultSet — результат запроса

Курсор для итерации по строкам результата:

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)
  • поддерживает сериализацию и передачу по сети
  • сам управляет соединением
// 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 автоматически:

// Repository.kt — под капотом это PreparedStatement
fun login(username: String, password: String): Pair<String, String>? {
    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.

// Интерфейс команды (Command.kt)
interface Command {
    val name: String
    val description: String
    val args: List<CommandArgType>
    val type: CommandType

    fun execute(args: List<String>, humanBeing: HumanBeing? = null, userId: String = ""): Response
}
// Конкретная команда (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<String>, 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")
    }
}
// Инвокер (CommandInvoker.kt) — хранит и запускает команды
class CommandInvoker(private val jwtManager: JWTManager) {
    private val commands = mutableMapOf<String, Command>()
    private val history = ArrayDeque<String>()

    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:

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:

// Session.kt — Observable (субъект)
val collection: ObservableList<HumanBeingProperty> = FXCollections.observableArrayList()

// CollectionCanvas.kt — Observer (наблюдатель)
Session.collection.addListener { _: ListChangeListener.Change<out HumanBeingProperty>? ->
    scheduleRender()  // перерисовать при любом изменении коллекции
}

// MainController.kt — ещё один Observer
private val collectionListener = ListChangeListener<HumanBeingProperty> { _ ->
    reapplyFiltersAndSort()
}
fun start() {
    Session.collection.addListener(collectionListener)
}

Singleton — порождающий

Проблема: объект должен существовать в единственном экземпляре.

В проекте это Session и AppLocale через Kotlin-объекты (object):

// Session.kt — единственный экземпляр сессии
object Session {
    var client: ClientProvider? = null
    var username: String = ""
    val collection: ObservableList<HumanBeingProperty> = FXCollections.observableArrayList()
}

// AppLocale.kt — единственный менеджер локализации
object AppLocale {
    val currentLocale: ObjectProperty<Locale> = 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
// 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 при сортировке:

// MainView.kt — динамическое создание стратегии сортировки
private fun toggleSort(key: String) {
    val comparator: Comparator<HumanBeingProperty> = 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<Comparator<HumanBeingProperty>>): ObservableList<...> {
    if (comparators.isNotEmpty()) {
        val combined = comparators.reduce { a, b -> a.thenComparing(b) }
        stream = stream.sorted(combined)  // применяем выбранную стратегию
    }
}

Proxy — структурный

Проблема: нужен объект-посредник для контроля доступа к другому объекту.

В проекте — GatewayManager выступает прокси-сервером:

// GatewayManager.kt — принимает запросы клиентов и перенаправляет на backend
inner class ProxyTask(...) : RecursiveTask<Unit>() {
    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))
        }
    }
}