1728 lines
65 KiB
Markdown
1728 lines
65 KiB
Markdown
# Ответы на экзаменационные вопросы по 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<T extends Event>`:
|
||
|
||
```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<HumanBeingProperty> { _ ->
|
||
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<T>` — обёртка над значением, которая уведомляет слушателей при изменении.
|
||
|
||
```kotlin
|
||
// HumanBeingProperty.kt — каждое поле объекта — Property
|
||
val name = SimpleStringProperty(humanBeing.name)
|
||
val realHero = SimpleBooleanProperty(humanBeing.realHero)
|
||
val coordinateX = SimpleObjectProperty<Long>(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<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:**
|
||
```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<Locale, Map<String, String>>`:
|
||
|
||
```kotlin
|
||
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`:
|
||
|
||
```kotlin
|
||
// Привязка текста кнопки к ключу локализации
|
||
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` при смене локали:
|
||
```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<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 — фоновый поток
|
||
|
||
```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<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)` |
|
||
|
||
```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<V>` | атомарная ссылка |
|
||
| `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<K,V>` | HashMap с сегментной блокировкой (высокая пропускная способность) |
|
||
| `CopyOnWriteArrayList<T>` | список: запись = копирование, чтение без блокировки |
|
||
| `CopyOnWriteArraySet<T>` | то же, но Set |
|
||
| `ConcurrentLinkedQueue<T>` | неблокирующая очередь (CAS) |
|
||
| `ConcurrentLinkedDeque<T>` | неблокирующая двусторонняя очередь |
|
||
| `LinkedBlockingQueue<T>` | блокирующая очередь (производитель-потребитель) |
|
||
| `PriorityBlockingQueue<T>` | блокирующая очередь с приоритетом |
|
||
| `ArrayBlockingQueue<T>` | ограниченная блокирующая очередь |
|
||
|
||
### `ConcurrentHashMap`
|
||
|
||
```java
|
||
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
|
||
map.put("key", 1);
|
||
map.putIfAbsent("key", 2); // атомарная операция
|
||
map.computeIfAbsent("key", k -> 42); // атомарное вычисление
|
||
```
|
||
|
||
### `CopyOnWriteArrayList`
|
||
|
||
При каждой записи создаётся **копия массива**. Читатели не блокируются. Оптимален для редких записей и частых чтений.
|
||
|
||
```java
|
||
List<String> list = new CopyOnWriteArrayList<>();
|
||
list.add("a"); // копирует массив, добавляет, заменяет ссылку
|
||
// Итерация безопасна даже при параллельных записях:
|
||
for (String s : list) { /* ... */ }
|
||
```
|
||
|
||
### `LinkedBlockingQueue` — производитель-потребитель
|
||
|
||
```java
|
||
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);
|
||
|
||
// Производитель:
|
||
queue.put(task); // блокируется если очередь полна
|
||
|
||
// Потребитель:
|
||
Task t = queue.take(); // блокируется если очередь пуста
|
||
```
|
||
|
||
### Из проекта: CopyOnWriteArrayList в GatewayManager.kt
|
||
|
||
```kotlin
|
||
// 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` на выполнение:
|
||
|
||
```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<Integer> future = es.submit(() -> 2 + 2);
|
||
int result = future.get(); // блокирует до получения результата
|
||
|
||
// Завершение:
|
||
es.shutdown(); // ждёт завершения задач
|
||
es.shutdownNow(); // прерывает запущенные задачи
|
||
```
|
||
|
||
### `Callable<V>`
|
||
|
||
Как `Runnable`, но **возвращает результат** и **может бросить checked-исключения**:
|
||
|
||
```java
|
||
interface Callable<V> {
|
||
V call() throws Exception;
|
||
}
|
||
|
||
Callable<String> task = () -> {
|
||
// долгая операция
|
||
return "результат";
|
||
};
|
||
```
|
||
|
||
### `Future<V>`
|
||
|
||
Представляет **будущий результат** асинхронной задачи:
|
||
|
||
```java
|
||
Future<String> 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<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)
|
||
|
||
```kotlin
|
||
// 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` — получение соединения
|
||
|
||
```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<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`.
|
||
|
||
```kotlin
|
||
// Интерфейс команды (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
|
||
}
|
||
```
|
||
|
||
```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<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")
|
||
}
|
||
}
|
||
```
|
||
|
||
```kotlin
|
||
// Инвокер (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`:
|
||
```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<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`):
|
||
|
||
```kotlin
|
||
// 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 |
|
||
|
||
```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<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` выступает прокси-сервером:
|
||
|
||
```kotlin
|
||
// 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))
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|