Формат курса — сквозной проект. Все шесть лабораторных развивают одно приложение: от модели данных до распределённого клиент-серверного решения. Вариант выдаётся один раз и закрепляется за бригадой на все лабораторные работы.
Бригада может состоять не более чем из 2 человек. Соответственно это либо 1 человек, либо 2 человека. Больше - нельзя.
Стек - Java (можно Kotlin) Рекомендуемые версии: Java 21 LTS; Java 25
| Компонент | Требование |
|---|---|
| Платформа | Java 21 LTS (минимум), рекомендуется Java 25 LTS. JDK — любой вендор (Temurin и т. п.) |
| IDE | IntelliJ IDEA (Community или Ultimate) / GigaIDE |
| Сборка | Gradle (Kotlin DSL) или Maven, toolchains; GUI подключается плагином/зависимостями OpenJFX — в составе JDK его нет |
| Тестирование | JUnit 5 |
| GUI | JavaFX (OpenJFX 21+) / Compose |
| Сервер | Spring Boot 3.x |
| Сериализация | java.io.Serializable и/или собственный бинарный формат; Protocol Buffers (proto3); Jackson JSON |
| Сеть | java.net.http.HttpClient / Ktor |
Каждый уровень предполагает выполненный каждый пункт задание качественно, в котором вы разбираетесь на 100% и готовы ответить на вопрос по коду самостоятельно в режиме реального времени.
| Уровень | Оценка | Смысл |
|---|---|---|
| Минимум | удовлетворительно | Базовый работающий функционал; без него лабораторная не сдаётся |
| Среднее | хорошо | Инженерное качество: валидация, обработка ошибок, полные тесты, замеры |
| Максимум | отлично | Продвинутые техники, автоматизация, исследования и сравнения в отчёте |
Уровни кумулятивны: среднее включает весь минимум, максимум — всё среднее.
Отчёт (DOCX) по каждой лабораторной:
- Титульный лист.
- Содержание/оглавление.
- Цель, вариант, ТЗ.
- Ход работы: ФРАГМЕНТЫ кода и пояснения с описанием ключевых решений.
- Демонстрация работы: скриншоты реализации и примеры показывающие работоспособность программы.
- Выводы/заключение.
Приложение с вставкой исходного кода не нужно. При сдаче отчёта с таким приложением это приведёт к снижению полученных баллов на 20%.
Защита — демонстрация работающего приложения и ответы на вопросы. Копирование кода между студентами — смена варианта и обнуления всех существующих работ.
- Вся логика в контроллере GUI / в контроллере REST («god object»).
- Проглатывание исключений (
catch (Exception e) {}). - Магические числа и строки вместо констант/
enum. - Тесты, проверяющие «всё и сразу» вместо одного поведения.
- Блокировка GUI-потока любой операцией дольше 100 мс.
Основная сущность Книга
- ISBN
- название
- автор(ы)
- год
- жанр
- признак «на руках»
Дополнительные сущности:
- Антикварная книга (read-only)
- Учебник
- курс
- тираж
Структура данных
Двоичное дерево поиска (ключ — название)
Details
Классическое дерево: слева ключи меньше, справа — больше. Поиск — спуск от корня (влево/вправо по сравнению).
[Мартин Иден]
/ \
[Война и мир] [Эффективная Java]
/ \ / \
[Алгоритмы] [Мартин И.] [Теория игр] [Язык Си]
Дополнительные операции
- Поиск по диапазону названий
- K последних по алфавиту
2. Расписание занятий университета
Основная сущность Занятие
- дата-время
- группа
- дисциплина
- аудитория
- преподаватель
Дополнительные сущности:
- Отменённое занятие (read-only)
- Лекция
- поток
- Практика
- подгруппа
Структура данных
AVL-дерево (ключ — дата-время)
Details
То же BST (см. вариант 1), но после каждой вставки/удаления проверяется баланс: высоты поддеревьев отличаются не более чем на 1. Нарушение лечится малыми поворотами (LL, RR, LR, RL).
вставка 10:10 в правое поддерево → дисбаланс у 10:00
[10:00] balance = -2
/ \
[09:30] [10:00]
\
[10:10]
малый левый поворот (RR):
[10:05]
/ \
[10:00] [10:10]
/
[09:30]
Дополнительные операции
- Ближайшее занятие группы
3. Каталог магазина
Основная сущность Товар
- артикул (число)
- название
- категория
- цена
- остаток
Дополнительные сущности:
- Товар «снят с продажи» (read-only)
- Товар с гарантией
- срок гарантии
Структура данных
B-дерево порядка 4 (ключ — артикул)
Details
Многоярусное дерево: каждый узел хранит до 3 ключей и до 4 детей; все листья на одном уровне — дерево растёт «вверх» через расщепление полных узлов.
[ 500 │ 900 ] ← корень (1–3 ключа)
/ │ \
[100│200│300] [600│700] [950│980│990] ← все листья на h=1
вставка 400 в [100│200│300] → узел полон → расщепление:
[ 300 │ 500 │ 900 ]
/ │ │ \
[100│200] [400] [600│700] [950│980│990]
Дополнительные операции
- Диапазонный скан по артикулам
- Статистика заполненности узлов
4. Справочник контактов
Основная сущность Контакт
- имя
- телефон
- организация
Дополнительные сущности:
- Аварийный контакт (read-only)
- Корпоративный контакт
- должность
- внутренний номер
Структура данных
Префиксное дерево — Trie (по имени)
Details
Каждый узел — символ; слово читается по пути от корня. Автодополнение — спуск по префиксу + сбор всех поддеревьев ниже найденного узла.
(root)
/ \
а м
/ \ \
н р а
| | |
н я р
| |
а и
| |
[НАТА] [МАРИЯ] [] — терминальный узел
автодополнение "ма": спустились по м→а → собираем [МАРИЯ]
Дополнительные операции
- Автодополнение по префиксу
- Подсчёт контактов в поддереве
5. Склад
Основная сущность Партия
- артикул
- название
- количество
- ячейка
- дата завоза
Дополнительные сущности:
- Архивная партия (read-only)
- Импортная партия
- страна
- таможенный код
Структура данных
Хеш-таблица с открытой адресацией (ключ — артикул, линейное пробирование)
Details
Массив ячеек; индекс = hash(key) % capacity. Если ячейка занята — сдвиг на шаг вправо, пока не найдётся свободная. При заполнении выше порога — рехеш: удвоение размера и переселение всех записей.
index: 0 1 2 3 4 5
┌────────┬────────┬────────┬────────┬────────┬────────┐
│ 91005 │ DELETED│ 91011 │ 91117 │ free │ 91002 │
└────────┴────────┴────────┴────────┴────────┴────────┘
put(91117): h(91117)=2 → занято(91011) → проба+1 → 3 свободно → пишем
^^ 2 пробы (статистика в отчёт)
load factor 0.75 → рехеш: capacity 6 → 12, все элементы переселятся
Дополнительные операции
- put/get/delete со статистикой проб
- Автоматический рехеш при заполнении
6. Электронная очередь поликлиники
Основная сущность Талон
- № карты
- ФИО
- кабинет
- срочность (0–3)
- время взятия
Дополнительные сущности:
- Закрытый талон (read-only)
- Талон на дому
- адрес
Структура данных
Двоичная min-куча (компаратор: срочность, затем время взятия)
Details
Полное дерево, хранящееся прямо в массиве: приоритет родителя ≤ приоритетов детей. Вставка — «всплытие» вверх, извлечение минимума — «погружение» вниз.
массив: [ A1 │ B3 │ A2 │ C5 │ B4 │ D3 │ C6 ]
индекс: 0 1 2 3 4 5 6
parent i → (i-1)/2; children i → 2i+1, 2i+2
дерево (срочность в узле):
(1,A1)
/ \
(3,B3) (2,A2)
/ \ / \
(5,C5) (4,B4) (3,D3) (6,C6)
вставка (0, C9): в конец → всплытие до корня
(0,C9) → (1,A1) → (3,B3) → (5,C5) → конец
Дополнительные операции
- K самых срочных
- Изменение приоритета
- Слияние двух очередей
7. Социальная сеть
Основная сущность Профиль
- id
- имя
- город
- год рождения
- связь «дружба»: пара id, «сила» связи
Дополнительные сущности:
- Удалённый профиль (read-only)
- Сообщество
- администратор
Структура данных
Граф: вершины — профили, рёбра — дружба. Списки смежности разрешены, если узел списка — собственная структура (не java.util-список)
Details
Вершины + рёбра; соседи хранятся в списках смежности. BFS (обход «в ширину» по слоям) даёт кратчайшую цепочку знакомств; Дейкстра учитывает вес ребра — «силу» связи.
Аня ──── Борис ──── Вера
│ │ │
Глеб ────√ Дима
adjacency:
Аня → [Борис, Глеб]
Борис → [Аня, Вера, Глеб]
Вера → [Борис, Дима]
Глеб → [Аня, Борис]
Дима → [Вера]
BFS Аня→Дима: Аня → Борис → Вера → Дима (цепочка длины 3)
Дополнительные операции
- Кратчайшая цепочка знакомств (BFS)
8. Карта мест (POI)
Основная сущность Место
- id
- название
- категория
- широта
- долгота
Дополнительные сущности:
- Место «на модерации» (read-only)
- Сетевое место
- сайт
- телефон
Структура данных
k-d дерево по (широта, долгота)
Details
Дерево, где сравнение идёт поочерёдно по осям: чётный уровень — широта, нечётный — долгота. Каждое разбиение «режет» плоскость — красивая визуализация областями. Поиск области — проверка пересечения прямоугольника поддерева с запросом.
корень — x: [ (50,30) ]
/ \
y-уровень: [ (20,60) ] [ (70,20) ]
/ \ \
x-уровень: [(10,40)] [(40,70)] [(80,50)]
плоскость (альтерн. разрезы):
┌───────────────────────┐
│ │ (50,30) │ вертикальный разрез по x=50
│ (20,60)──┼──(70,20) │ затем горизонтальные по y
│ │ │ │ │
│(10,40) (40,70) (80,50) │
└───────────────────────┘
Дополнительные операции
- Все точки в прямоугольной области
9. Таск-трекер
Основная сущность Задача
- id
- название
- исполнитель
- срок
- важность (1–10)
Дополнительные сущности:
- Задача в архиве (read-only)
- Срочная задача
- дежурный
Структура данных
Декартово дерево — treap (ключ — срок, приоритет кучи — важность)
Details
Гибрид BST и кучи: по ключу (срок) — дерево поиска, по приоритету (важность) — куча. Рекомендуемая реализация — через split/merge, а не вращения.
ключ(срок)=число, приоритет(важность)=буква (A — самый важный)
по сроку: слева меньше; по важности: родитель важнее (буква раньше)
(10 мар, A)
/ \
(05 мар, C) (20 мар, B)
/ \
(01 мар, D) (07 мар, E)
merge(A, B): всё с ключом < ключа(B) — в левую часть; корень = важнейший
split(T, дедлайн 08 мар): { 01, 05, 07 } | { 10, 20 }
📖 Хабр — Декартово дерево (секретная информация о декартовых деревьях), Wikipedia — Treap
Дополнительные операции
- Задачи в интервале сроков
- N самых важных
10. Бронирование переговорных
Основная сущность Бронь
- id
- комната
- сотрудник
- начало
- конец
Дополнительные сущности:
- Прошедшая бронь (read-only)
- Регулярная бронь
- дни недели
Структура данных
Дерево по началу брони (ключ — «начало»; пересечение проверяется при обходе диапазона)
Details
Обычное дерево поиска по началу брони, пересечения ищутся при обходе нужного диапазона. Дерево интервалов (на максимум) добавляет в узел аугментацию max_end — максимум концов поддерева, что позволяет отсекать ветви.
[09:00 ─ 10:30] max_end=13:00 ← в узле храним max концов поддерева
/ \
[08:00─09:30] [11:00 ─ 13:00] max_end=13:00
max_end=09:30
запрос: пересекается ли [10:45 ─ 11:30]?
корень: 10:45 < 10:30 ? нет → может пересекаться справа
правый: 11:00 ≤ 11:30 и 10:45 ≤ 13:00 → ПЕРЕСЕЧЕНИЕ ✔ (конфликт комнаты)
отсечение: если max_end(лев. поддерева) < 10:45 → левое пропускаем целиком
Дополнительные операции
- Брони, пересекающиеся с интервалом
- Проверка конфликта комнаты
Примечания.
- Семантика read-only-типов — «архивные/закрытые» записи: они видны в GUI, но редактирование запрещено.
- Синтетические производные типы из списка «Дополнительные сущности» можно дополнять своими, если сохраняются обязательные три типа: базовый, read-only, редактируемый с новыми полями.
- Реализовать сущности согласно варианту в ООП паттернах. Минимум три типа из «Дополнительных сущностей» (базовый, read-only, редактируемый с дополнительными полями).
- Добавить интерфейс
Editable. Маркерный или с методами. Кнопка «Изменить» в GUI доступна только для объектов, реализующихEditable. Через «Добавить» можно создать только редактируемые типы; read-only-типы появляются только из файла. - Загрузка данных из файла CSV. Битые строки пропускаются.
- Реализация GUI:
- отображение элемента(ов)
- загрузить из CSV
- сохранить в CSV
- редактирование сущности (не read-only)
- добавление сущности
- предусмотреть диалоги с ошибками
1. Интерфейс Editable — контракт «сущность можно менять». Может быть маркером (без методов) или нести методы, например проверку полей:
public interface Editable {
/** @return список ошибок; пустой список = данные корректны */
List<String> validate();
}Кнопка «Изменить» в GUI проверяет entity instanceof Editable и блокируется для read-only-типов. Решение «маркер или с методами» принимаете сами и обосновываете в отчёте.
📖 Metanit — Интерфейсы · Metanit — Классы Records (удобны для неизменяемых read-only-типов)
2. Наследование — базовая сущность, от которой наследуются производные с новыми полями:
public class Book { // базовый тип
protected String isbn;
protected String title;
// конструктор, геттеры/сеттеры
}
public class Textbook extends Book implements Editable { // редактируемый, + новые поля
private int course;
private int printRun;
@Override public List<String> validate() { /* проверка полей */ return List.of(); }
}Отличие наследования от реализации интерфейса: наследование (extends) переиспользует код и поля одного базового класса; интерфейс (implements) — только контракт без состояния, зато класс может реализовать несколько интерфейсов сразу. Editable — именно контракт, а не родитель.
📖 Metanit — Наследование · Metanit — Иерархия наследования и преобразование типов
Шаг 1. Первая программа. Создайте в IDEA проект (Gradle, JDK 21), класс с main, выведите Hello world в консоль и запустите зелёной стрелкой.
public class Main {
public static void main(String[] args) {
System.out.println("Hello world");
}
}Шаг 2. Класс данных. Опишите сущность варианта обычным классом (поля, конструктор, геттеры/сеттеры, toString) и создайте пару объектов в main, выведите их на консоль.
Шаг 3. Хранилище. Положите созданные объекты в список и пройдите по нему циклом:
List<Book> books = new ArrayList<>();
books.add(new Book("978-5-17", "Мартин Иден", "Лондон Дж.", 1909));
for (Book b : books) System.out.println(b);📖 Metanit — Типы коллекций. Интерфейс Collection
Шаг 4. Загрузка CSV. Скелет парсера — алгоритм разбора строки реализуйте сами:
public final class CsvLoader {
/** читает файл построчно, разбирает каждую строку и собирает сущности */
public List<Book> load(Path file) {
List<Book> result = new ArrayList<>();
// try (var reader = Files.newBufferedReader(file)) {
// reader.readLine(); // строка-заголовок
// String line;
// while ((line = reader.readLine()) != null) {
// String[] parts = line.split(";", -1);
// // разобрать parts в объект; битую строку — пропустить
// }
// } catch (IOException e) { /* показать диалог ошибки */ }
return result;
}
}📖 Metanit — Работа со строками, split · Metanit — BufferedReader/BufferedWriter · Metanit — Класс File, работа с файлами
Минимум
- Иерархия из трёх типов, интерфейс
Editable - GUI: таблица + три кнопки + диалог редактирования (без валидации полей). Заблокированная кнопка «Изменить» на read-only-объекте
Среднее
- Работа с корректным CSV-файлом
Максимум
- Собственные классы исключений загрузки с кодами ошибок (
BAD_NUMBER,WRONG_FIELD_COUNT, …) и обработка на уровне GUI.
- Реализовать собственную структуру данных согласно варианту. Реализация самостоятельно на массивах и собственных узлах, без использования
java.utilи подобных. - Реализация дополнительных операций по варианту, а также асимптотика каждой операции (таблица в отчёте: операция, O(...), обоснование).
- Визуализация работы структуры данных. Панель, отображающая внутреннее устройство структуры. Перерисовка после каждой операции; подсветка узлов, посещённых при поиске/вставке.
- Добавление возможности генерировать набор данных (с указанием числа генерации вхождений).
- Начиная с лабораторной № 2 списки запрещены как основное хранилище данных:
List,ArrayList,LinkedListи любые их аналоги. Основное хранилище — собственная структура по варианту. - Там, где по варианту требуется собственная структура, готовые реализации из
java.util(TreeMap, HashMap, PriorityQueue и т. п.) запрещены в основном коде. Разрешены вспомогательные коллекции (Set,Map) и массивы — по согласованию с преподавателем.
1. Начните с простого контракта, не дженерика. Опишите интерфейс хранилища под конкретную сущность и реализуйте его своей структурой:
public interface BookStore { // НЕ дженерик — конкретный тип
void add(Book book); // вставка
Book findByTitle(String title); // поиск по ключу
boolean removeByTitle(String title); // удаление
int size();
}
public class BstBookStore implements BookStore {
private static final class Node { // собственный узел, как в C++ struct
Book value;
Node left, right;
Node(Book value) { this.value = value; }
}
private Node root;
// add / findByTitle / removeByTitle — реализуете сами
}2. Пример: две функции односвязного списка (вставка в начало и обход) — ориентир того, что считается «реализовано самостоятельно»:
public class LinkedIntList {
private static final class Node {
int value; Node next;
Node(int value, Node next) { this.value = value; this.next = next; }
}
private Node head;
public void addFirst(int value) { head = new Node(value, head); }
public int size() {
int count = 0;
for (Node p = head; p != null; p = p.next) count++;
return count;
}
}3. Перевод на дженерики — когда конкретная версия работает, обобщение делается механически: тип сущности выносится в параметр <T>, ключ — в <K>.
public interface EntityStore<K, T> { // было BookStore
void add(T entity);
T findByKey(K key);
boolean removeByKey(K key);
int size();
}
public class BstStore<K extends Comparable<K>, T> implements EntityStore<K, T> { ... }Что такое дженерики (краткая теория)
Дженерики — параметризация класса/интерфейса типом: BstStore<String, Book> компилятор проверит так, что положить Integer в дерево книг не получится. Это аналог шаблонов C++, но проверка происходит на этапе компиляции, а примитивы (int) нужно упаковывать в обёртки (Integer). Ограничение K extends Comparable<K> требует от ключа умения сравнивать себя (compareTo) — вместо operator< из C++.
📖 Metanit — Обобщения (Generics) · Metanit — Наследование и обобщения
4. Гонка потоков и синхронизация. Если фоновый поток перестраивает структуру, пока GUI-поток её читает, данные могут повредиться («гонка»). Java даёт несколько инструментов: оператор synchronized (взаимное исключение), атомарные классы, неизменяемые снимки (copy-on-read). Для лабораторной достаточно понять и применить synchronized на критической секции.
public synchronized Book findByKey(String title) { return doFindByKey(title); }📖 Metanit — Многопоточность и класс Thread · Metanit — Создание и выполнение потоков · Metanit — Синхронизация, synchronized · Metanit — Завершение и прерывание потока
5. Canvas в JavaFX — панель визуализации: получаете графический контекст и рисуете примитивы (линии, круги, текст) в методе перерисовки:
GraphicsContext g = canvas.getGraphicsContext2D();
g.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); // очистить
g.strokeLine(x1, y1, x2, y2); // ребро дерева
g.fillOval(x - r, y - r, 2 * r, 2 * r); // узел
g.fillText("Мартин Иден", x - 20, y + 4); // подпись📖 Oracle — Canvas (javadoc) · Oracle — Working with Canvas · Metanit — Кнопки и метки JavaFX (базовые контролы панели)
Для фоновых операций используйте javafx.concurrent.Task: тяжёлая работа — в call() (фоновый поток), обновление GUI — в setOnSucceeded/Platform.runLater (FX-поток). 📖 OpenJFX — Task (javadoc)
Минимум
- Структура с операциями вставка/поиск/удаление.
- Визуализация: полная перерисовка панели после каждой операции.
- Генерация набора данных.
- Фоновая загрузка файла через
Taskс прогрессом.
Среднее
- Все дополнительные операции.
- Генерация набора данных в фоне, а также возможность отмены операции.
- Таблица замеров времени (вставка/поиск/удаление/дополнительные на 10⁴ и 10⁵).
Максимум
- Структура данных выполнена с использованием
generics - Сравнительный обзор: собственная структура против ближайшего аналога из
java.util(только для эксперимента, в основном коде аналога нет) — график/таблица в отчёте. - Конкурентный доступ: реализация многопотоковой записи в структуру и демонстрация гонки на незащищённой структуре и защита (синхронизация / immutable-снимок).
- Реализовать возможность сериализации данных в структуре через общий интерфейс. Минимальный набор — два формата: бинарный (
Serializableили собственный) и JSON (Jackson). - Доработать GUI. Добавление экспорт/импорт через
FileChooser. После импорта обновляются таблица и визуализация структуры. - Сравнение форматов в отчёте: размер файла на 10⁴ записей; время сохранения/загрузки (среднее по 5 прогонам); читаемость «глазами» (фрагмент файла каждого формата).
- Необходимо написать JUnit тесты с использованием заготовленных файлов на базовые операции.
1. Какой сериализации хотим: несколько реализаций одного контракта (интерфейс выше) — GUI и тесты работают через Serializer, не зная, что внутри: JSON, бинарный формат или что-то ещё. Пример реализации JSON-варианта на Jackson:
public final class JsonBookSerializer implements Serializer<BookData> {
@Override public String format() { return "json"; }
@Override public void save(BookData data, Path file) throws IOException {
new ObjectMapper().writeValue(file.toFile(), data);
}
@Override public BookData load(Path file) throws IOException {
return new ObjectMapper().readValue(file.toFile(), BookData.class);
}
}Roundtrip-контракт: load(save(x)) обязан вернуть данные, равные x — это главный тест сериализации.
2. Serializable и версионирование. Бинарный формат «из JDK»: класс реализует маркер-интерфейс java.io.Serializable, а JDK сама записывает объект в поток байтов. Поле serialVersionUID — «паспорт версии» класса: если при чтении версия класса не совпала с записанной, получится InvalidClassException. Именно поэтому serialVersionUID — первая, примитивная форма версионирования формата (пригодится в лаб. 4).
public class Book implements java.io.Serializable {
private static final long serialVersionUID = 1L; // версия схемы
private String isbn;
private String title;
}📖 JavaRush — Сериализация и десериализация в Java · JavaRush — Зачем нужен serialVersionUID · Metanit — Сериализация
3. Подключение библиотек. Внешние библиотеки (Jackson и другие) добавляются декларацией зависимости в сборку, jar вручную не скачиваются:
// build.gradle.kts
dependencies {
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0")
}📖 Jenkov — Jackson ObjectMapper (как Jackson маппит объекты ↔ JSON)
4. FileChooser в JavaFX — диалог выбора файла с фильтром формата:
FileChooser fc = new FileChooser();
fc.setTitle("Экспорт данных");
fc.getExtensionFilters().add(new FileChooser.ExtensionFilter("JSON", "*.json"));
File file = fc.showSaveDialog(stage); // showOpenDialog для импорта
if (file != null) serializer.save(data, file.toPath());📖 Oracle — FileChooser (javadoc)
5. JUnit. Тест — обычный метод с аннотацией @Test, проверяющий одно поведение одним утверждением:
Краткая теория
Тестовый метод готовит данные (given) → вызывает проверяемый код (when) → сравнивает факт с ожиданием (then). Основные утверждения: assertEquals (равенство значений), assertTrue/assertFalse, assertNull/assertNotNull, assertThrows (ожидаемое исключение). Тесты запускаются одной командой сборки (gradle test / mvn test) и падают с понятным сообщением, если фактическое значение не совпало.
Чтение файла в тесте: положите эталонные данные в src/test/resources/ и прочитайте из classpath — тогда тест не зависит от рабочей папки.
📖 JavaRush — JUnit: тестирование в Java · JUnit 5 User Guide
class CsvLoaderTest {
@Test
void loads_reference_file_and_matches_expected_line() throws Exception {
// эталонный файл лежит в src/test/resources/books.csv
try (var in = getClass().getResourceAsStream("/books.csv")) {
String expected = new String(in.readAllBytes(), StandardCharsets.UTF_8)
.lines().skip(1).findFirst().orElseThrow();
Book parsed = new CsvLoader().parseLine(expected);
assertEquals("978-5-17", parsed.getIsbn());
}
}
}Минимум
- Сериализация bin + JSON через общий интерфейс
Serializer, roundtrip-тесты обоих. - Доработать GUI.
- Сравнение форматов в отчёте.
Среднее
- JUnit тесты на позитивные сценарии (не менее 3х штук)
Максимум
- Указание версии при сериализации.
- Обработка исключительных ситуаций/битых файлов.
- JUnit тесты покрывают негативные сценарии. Не менее 7 штук в сумме.
- Реализация миграции при обогащении данными сущности. В сущность добавляется новое поле (семантика по варианту: рейтинг, теги, заметка, дата изменения и т. п.). Для записей из старых файлов поле заполняется значением по умолчанию (не
null). - Добавление заголовка версии во всех форматах:
- JSON/XML — поле/атрибут
schema_version; - proto — поле
schema_version; - собственный бинарный формат — версия в заголовке;
Serializable— кастомныеwriteObject/readObjectс версией.
- JSON/XML — поле/атрибут
- Реализовать тесты на мигрирование данных.
- GUI: при открытии файла старой версии — диалог «Файл версии 1. Мигрировать до 2?».
- Выполнение сохранения с обратной совместимостью (на любую предыдущую версию).
1. Что такое миграция. Миграция — автоматическое преобразование данных из старой версии формата в текущую при чтении файла. Приложение версии N умеет открывать файлы версий 1…N: смотрит schema_version в файле и применяет цепочку пошаговых преобразований. Каждый шаг — маленькая изолированная функция «данные версии k → данные версии k+1»: так шаги можно тестировать отдельно и переиспользовать (v1→v3 = v1→v2 + v2→v3).
2. Скелет шага миграции — новое поле с заполнением по умолчанию:
/** v1 → v2: добавили поле rating; для старых записей — значение по умолчанию */
public final class MigrationV1toV2 {
public static Map<String, Object> migrate(Map<String, Object> record) {
record.putIfAbsent("rating", 0.0); // НЕ null — конкретное значение
record.put("schema_version", 2);
return record;
}
}Представление записи как Map<String, Object> (или JSON-дерево Jackson) удобно: поля можно добавлять/переименовывать независимо от класса модели, а сам класс модели меняется один раз, в текущей версии. Реестр миграций — список шагов, применяемых по порядку, пока версия записи не сравняется с текущей.
3. Пример теста миграции — на фикстуре (эталонном файле версии 1 в src/test/resources/):
@Test
void migrates_v1_file_and_fills_default_rating() {
BookData v1 = loadFixture("books-v1.json"); // schema_version = 1
BookData v2 = new MigrationV1toV2().apply(v1);
assertEquals(2, v2.schemaVersion);
assertEquals(0.0, v2.books.get(0).rating); // дефолт, не null
assertEquals(3, v2.books.size()); // записи не потерялись
}📖 JavaRush — Сериализация и десериализация в Java · JavaRush — JUnit: тестирование в Java
Минимум
- Реализация миграции v1 → v2 во всех форматах.
- Доработка GUI.
Среднее
- Реализация поэтапной миграции v1 → v2 → v3 во всех форматах.
- Написание тестов. (6 штук)
- Обратная совместимость.
Максимум
- Обратная совместимость (downgrade): экспорт в схему v1/v2 с осознанной потерей новых полей (в GUI — предупреждение о потере).
- Обработка различных ошибок при миграции, например, файл из будущего, битый файл, неверно заполненное поле (обнаруженное в ходе миграции).
Цель: вынести предметную логику в серверное приложение на Spring Boot и предоставить REST API; GUI работает в том же процессе и обращается к хранилищу напрямую, минуя HTTP.
- Необходимо подключить библиотеку Spring Boot и реализовать HTTP запросы для изменения структуры данных.
- Запуск GUI в том же процессе. Для обновления и получения актуальных данных можно добавить кнопку обновления либо выполнить подписку на изменения через слушатель.
- Реализовать следующие endpoints (сетевые запросы) REST (имена сущностей — по варианту, например
/api/books):GET /api/{entities}— выборка с фильтрами по полям;GET /api/{entities}/{id};POST /api/{entities}→201+Location;PUT /api/{entities}/{id}— только дляEditable-типов, иначе422;DELETE /api/{entities}/{id};- дополнительные операции варианта (лаб. 2) как под-ресурсы:
/search?prefix=…,/range?from=…&to=…,/nearest?lat=…&lon=…,/path?from=…&to=…и т. п.; POST /api/store/swap?structure=…— переключение используемой структуры данных на лету (если у вас несколько реализаций структуры из лаб. 2; иначе ручку можно не делать — на оценку не влияет);POST /api/export?format=json|bin,POST /api/import— сериализация из лаб. 3 как сервис.
- Ошибки. Единый формат ProblemDetail (RFC 9457):
400— ошибка валидации,404— нет записи,409— дубликат ключа/конфликт,422— попытка изменить не-Editableзапись; доменные исключения (включаяVERSION_TOO_NEWиз лаб. 4) маппятся через@ExceptionHandler.
1. Запуск Spring и первый GET. Проще всего создать проект через Spring Initializr (зависимость Spring Web), затем объявить контроллер:
@RestController
public class BookController {
private final BookService service; // бин с бизнес-логикой
public BookController(BookService service) { this.service = service; }
@GetMapping("/api/books")
public List<BookDto> all() { return service.findAll(); }
@GetMapping("/api/books/{isbn}")
public BookDto one(@PathVariable String isbn) { return service.findByKey(isbn); }
}Запуск — ./gradlew bootRun (или кнопкой Run в IDEA на классе с @SpringBootApplication).
📖 Spring — Building an Application with Spring Boot · Spring — Building a RESTful Web Service
2. Проверка в браузере. GET-запрос открывается прямо в браузере: http://localhost:8080/api/books — увидите JSON-ответ. Это самый быстрый способ проверить ручку чтения.
3. POST и его проверка. POST меняет состояние, браузером его не отправить — используйте curl:
curl -X POST http://localhost:8080/api/books \
-H "Content-Type: application/json" \
-d '{"isbn":"978-5-17","title":"Мартин Иден","year":1909}' -iОжидаемый ответ: HTTP/1.1 201 Created и заголовок Location: /api/books/978-5-17. Повторный GET по этому адресу возвращает созданную запись.
4. GUI в том же процессе. JavaFX-приложение может поднять Spring-контекст в Application#init() (этот поток без UI, Spring там разрешён), а в start() достать бины getBean и вызывать Service напрямую, минуя HTTP. Для актуализации данных достаточно кнопки «Обновить» (перечитать данные из сервиса) — либо слушателей изменений сервиса.
public class App extends Application {
private ConfigurableApplicationContext spring;
@Override public void init() {
spring = new SpringApplicationBuilder(ServerApp.class).run();
}
@Override public void start(Stage stage) {
var service = spring.getBean(BookService.class);
// контроллеры GUI читают/пишут через service; кнопка «Обновить» — service.findAll()
}
}5. Архитектура. Слои Spring-приложения: controller принимает HTTP и возвращает DTO (без бизнес-логики), service хранит валидацию и бизнес-операции, store — ваша структура данных как singleton-бин. Зависимости направлены вниз: контроллер знает сервис, сервис — хранилище, и никогда наоборот.
@Service
public class BookService {
private final EntityStore<String, Book> store; // бин-хранилище (ваша структура)
public BookService(EntityStore<String, Book> store) { this.store = store; }
public List<Book> findAll() { /* ... */ }
public Book add(BookDto dto) { /* валидация + store.add */ }
}Минимум
-
Выполнение операций:
GET /api/{entities}— выборка с фильтрами по полям;GET /api/{entities}/{id};POST /api/{entities}→201+Location;PUT /api/{entities}/{id}— только дляEditable-типов, иначе422;DELETE /api/{entities}/{id}
-
GUI с кнопкой обновить.
Среднее
- Выполнение операций:
- дополнительные операции варианта (лаб. 2) как под-ресурсы:
/search?prefix=…,/range?from=…&to=…,/nearest?lat=…&lon=…,/path?from=…&to=…и т. п.; POST /api/export?format=json|proto|xml|bin,POST /api/import— сериализация хотя бы в один тип.
- дополнительные операции варианта (лаб. 2) как под-ресурсы:
Максимум
- Автоматическое обновление GUI при вызове API.
- Разделить клиент и сервер. Теперь клиент находится в отдельном приложении и взаимодействует с сервером через API.
1. Базовый сетевой запрос. Стандартный java.net.http.HttpClient: клиент создаётся один раз, запрос собирается билдером, ответ читается как строка:
HttpClient http = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/api/books"))
.GET()
.build();
HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());
String json = response.body(); // 200 + JSON со списком книгPOST отличается телом и заголовком: .POST(HttpRequest.BodyPublishers.ofString(json)) + .header("Content-Type", "application/json").
📖 JavaRush — Руководство по HTTP-клиенту Java · Oracle — HttpClient (javadoc, Java SE 21)
2. Слой API-клиента и DTO. Сетевые вызовы прячутся за интерфейс, а GUI работает с объектами, а не с JSON-строками:
public interface ApiClient {
List<BookDto> findAll(); // GET /api/books
Optional<BookDto> findByKey(String isbn); // GET /api/books/{id}
BookDto create(BookDto dto); // POST /api/books
void delete(String isbn); // DELETE /api/books/{id}
}
public record BookDto(String isbn, String title, int year) { } // DTO: record для переноса данныхDTO (Data Transfer Object) — простой класс-носитель данных между клиентом и сервером: он повторяет формат сетевого ответа, но не содержит логики. JSON-ответ разбирается в DTO тем же Jackson, что и в лаб. 3. GUI зависит от ApiClient, а не от HTTP-деталей — так его можно тестировать без сервера.
📖 Martin Fowler — Data Transfer Object · freeCodeCamp — What are DTOs in Java · Jenkov — Jackson ObjectMapper
3. Визуализация по данным из экспорта. Структура целиком живёт на сервере, но клиент может восстановить её вид: держите на сервере ручку, отдающую содержимое структуры (например, GET /api/books/export — полный список или JSON-снимок структуры). Клиент по этому снимку отрисовывает панель: строит локальное «отражение» структуры (те же узлы и связи) и рисует её на Canvas так же, как в лаб. 2. Отражение — только для показа: операции всё равно выполняются через сеть.
Минимум
- Реализация базовых операций. Визуализация работы структуры может отсутствовать (не обязательна по сети)
Среднее
- Подключение дополнительных операции и export/import через сеть.
Максимум
-
Обработка сетевых ошибок.
Портируйте GUI на Android