Skip to content

Instantly share code, notes, and snippets.

@Vova-SH
Last active September 26, 2026 09:39
Show Gist options
  • Select an option

  • Save Vova-SH/8104d9382c0af80fd693c79de49252cc to your computer and use it in GitHub Desktop.

Select an option

Save Vova-SH/8104d9382c0af80fd693c79de49252cc to your computer and use it in GitHub Desktop.
Разработка на Java

Программирование на Java

Методические указания и задания к лабораторным работам

Формат курса — сквозной проект. Все шесть лабораторных развивают одно приложение: от модели данных до распределённого клиент-серверного решения. Вариант выдаётся один раз и закрепляется за бригадой на все лабораторные работы.

Бригада может состоять не более чем из 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) по каждой лабораторной:

  1. Титульный лист.
  2. Содержание/оглавление.
  3. Цель, вариант, ТЗ.
  4. Ход работы: ФРАГМЕНТЫ кода и пояснения с описанием ключевых решений.
  5. Демонстрация работы: скриншоты реализации и примеры показывающие работоспособность программы.
  6. Выводы/заключение.

Приложение с вставкой исходного кода не нужно. При сдаче отчёта с таким приложением это приведёт к снижению полученных баллов на 20%.

Защита — демонстрация работающего приложения и ответы на вопросы. Копирование кода между студентами — смена варианта и обнуления всех существующих работ.

Типичные анти-паттерны (снижают оценку)

  • Вся логика в контроллере GUI / в контроллере REST («god object»).
  • Проглатывание исключений (catch (Exception e) {}).
  • Магические числа и строки вместо констант/enum.
  • Тесты, проверяющие «всё и сразу» вместо одного поведения.
  • Блокировка GUI-потока любой операцией дольше 100 мс.

Варианты

1. Электронная библиотека

Основная сущность Книга

  • 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]

📖 Википедия — AVL-дерево, VisuAlgo — AVL

Дополнительные операции

  • Ближайшее занятие группы

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]

📖 Википедия — B-дерево, Визуализатор USFCA

Дополнительные операции

  • Диапазонный скан по артикулам
  • Статистика заполненности узлов

4. Справочник контактов

Основная сущность Контакт

  • имя
  • телефон
  • e-mail
  • организация

Дополнительные сущности:

  • Аварийный контакт (read-only)
  • Корпоративный контакт
    • должность
    • внутренний номер

Структура данных

Префиксное дерево — Trie (по имени)

Details

Каждый узел — символ; слово читается по пути от корня. Автодополнение — спуск по префиксу + сбор всех поддеревьев ниже найденного узла.

        (root)
        /    \
       а      м
      / \      \
    н    р      а
    |    |      |
    н    я      р
    |           |
    а           и
    |           |
  [НАТА]      [МАРИЯ]        [] — терминальный узел

автодополнение "ма": спустились по м→а → собираем [МАРИЯ]

📖 Википедия — Префиксное дерево, Визуализатор USFCA

Дополнительные операции

  • Автодополнение по префиксу
  • Подсчёт контактов в поддереве

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, все элементы переселятся

📖 Википедия — Хеш-таблица, Визуализатор USFCA

Дополнительные операции

  • 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) → конец

📖 Википедия — Двоичная куча, VisuAlgo — Binary Heap

Дополнительные операции

  • K самых срочных
  • Изменение приоритета
  • Слияние двух очередей

7. Социальная сеть

Основная сущность Профиль

  • id
  • имя
  • город
  • год рождения
  • связь «дружба»: пара id, «сила» связи

Дополнительные сущности:

  • Удалённый профиль (read-only)
  • Сообщество
    • администратор

Структура данных

Граф: вершины — профили, рёбра — дружба. Списки смежности разрешены, если узел списка — собственная структура (не java.util-список)

Details

Вершины + рёбра; соседи хранятся в списках смежности. BFS (обход «в ширину» по слоям) даёт кратчайшую цепочку знакомств; Дейкстра учитывает вес ребра — «силу» связи.

        Аня ──── Борис ──── Вера
         │        │          │
        Глеб ────√          Дима

   adjacency:
     Аня   → [Борис, Глеб]
     Борис → [Аня, Вера, Глеб]
     Вера  → [Борис, Дима]
     Глеб  → [Аня, Борис]
     Дима  → [Вера]

BFS Аня→Дима: Аня → Борис → Вера → Дима   (цепочка длины 3)

📖 Википедия — Представление графа, Визуализатор USFCA

Дополнительные операции

  • Кратчайшая цепочка знакомств (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) │
  └───────────────────────┘

📖 Википедия — k-d дерево

Дополнительные операции

  • Все точки в прямоугольной области

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 → левое пропускаем целиком

📖 Wikipedia — Interval tree

Дополнительные операции

  • Брони, пересекающиеся с интервалом
  • Проверка конфликта комнаты

Примечания.

  • Семантика read-only-типов — «архивные/закрытые» записи: они видны в GUI, но редактирование запрещено.
  • Синтетические производные типы из списка «Дополнительные сущности» можно дополнять своими, если сохраняются обязательные три типа: базовый, read-only, редактируемый с новыми полями.

Лабораторная работа № 1. «Разработка модели данных»

Задание

  1. Реализовать сущности согласно варианту в ООП паттернах. Минимум три типа из «Дополнительных сущностей» (базовый, read-only, редактируемый с дополнительными полями).
  2. Добавить интерфейс Editable. Маркерный или с методами. Кнопка «Изменить» в GUI доступна только для объектов, реализующих Editable. Через «Добавить» можно создать только редактируемые типы; read-only-типы появляются только из файла.
  3. Загрузка данных из файла CSV. Битые строки пропускаются.
  4. Реализация 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");
    }
}

📖 Metanit — Первая программа

Шаг 2. Класс данных. Опишите сущность варианта обычным классом (поля, конструктор, геттеры/сеттеры, toString) и создайте пару объектов в main, выведите их на консоль.

📖 Metanit — Классы и объекты

Шаг 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.

Лабораторная работа № 2. «Собственные структуры данных, асинхронность»

Задание

  1. Реализовать собственную структуру данных согласно варианту. Реализация самостоятельно на массивах и собственных узлах, без использования java.util и подобных.
  2. Реализация дополнительных операций по варианту, а также асимптотика каждой операции (таблица в отчёте: операция, O(...), обоснование).
  3. Визуализация работы структуры данных. Панель, отображающая внутреннее устройство структуры. Перерисовка после каждой операции; подсветка узлов, посещённых при поиске/вставке.
  4. Добавление возможности генерировать набор данных (с указанием числа генерации вхождений).

Ограничения

  • Начиная с лабораторной № 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-снимок).

Лабораторная работа № 3. «Сериализация/десериализация и тестирование»

Задание

  1. Реализовать возможность сериализации данных в структуре через общий интерфейс. Минимальный набор — два формата: бинарный (Serializable или собственный) и JSON (Jackson).
  2. Доработать GUI. Добавление экспорт/импорт через FileChooser. После импорта обновляются таблица и визуализация структуры.
  3. Сравнение форматов в отчёте: размер файла на 10⁴ записей; время сохранения/загрузки (среднее по 5 прогонам); читаемость «глазами» (фрагмент файла каждого формата).
  4. Необходимо написать 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 штук в сумме.

Лабораторная работа № 4. «Версионирование формата и миграции данных»

Задание

  1. Реализация миграции при обогащении данными сущности. В сущность добавляется новое поле (семантика по варианту: рейтинг, теги, заметка, дата изменения и т. п.). Для записей из старых файлов поле заполняется значением по умолчанию (не null).
  2. Добавление заголовка версии во всех форматах:
    • JSON/XML — поле/атрибут schema_version;
    • proto — поле schema_version;
    • собственный бинарный формат — версия в заголовке;
    • Serializable — кастомные writeObject/readObject с версией.
  3. Реализовать тесты на мигрирование данных.
  4. GUI: при открытии файла старой версии — диалог «Файл версии 1. Мигрировать до 2?».
  5. Выполнение сохранения с обратной совместимостью (на любую предыдущую версию).

Детали реализации

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 — предупреждение о потере).
  • Обработка различных ошибок при миграции, например, файл из будущего, битый файл, неверно заполненное поле (обнаруженное в ходе миграции).

Лабораторная работа № 5. «Spring Boot: сервер, REST API»

Цель: вынести предметную логику в серверное приложение на Spring Boot и предоставить REST API; GUI работает в том же процессе и обращается к хранилищу напрямую, минуя HTTP.

Задание

  1. Необходимо подключить библиотеку Spring Boot и реализовать HTTP запросы для изменения структуры данных.
  2. Запуск GUI в том же процессе. Для обновления и получения актуальных данных можно добавить кнопку обновления либо выполнить подписку на изменения через слушатель.
  3. Реализовать следующие 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 как сервис.
  4. Ошибки. Единый формат 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 — сериализация хотя бы в один тип.

Максимум

  • Автоматическое обновление GUI при вызове API.

Лабораторная работа № 6. «REST-клиент»

Задание

  1. Разделить клиент и сервер. Теперь клиент находится в отдельном приложении и взаимодействует с сервером через 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 через сеть.

Максимум

  • Обработка сетевых ошибок.

    Курсовая работа. «Создание Android приложения»

    Задание

    Портируйте GUI на Android

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment