JavaRush /Курси /JAVA 25 SELF /Вступ до серіалізації об’єктів: навіщо вона потрібна

Вступ до серіалізації об’єктів: навіщо вона потрібна

JAVA 25 SELF
Рівень 42 , Лекція 0
Відкрита

1. Навіщо потрібна серіалізація

Уявіть, що ваш об’єкт — це речі, які ви берете з собою у відпустку. Серіалізація — це запакування всього вмісту валізи в спеціальний контейнер, який можна покласти в багаж або надіслати поштою. Десеріалізація, відповідно, — це розпакування цього контейнера й отримання речей у початковому вигляді.

По суті, серіалізація перетворює об’єкт на потік байтів, який можна зберегти у файл, передати мережею або просто тримати в пам’яті. Десеріалізація робить зворотне: відновлює об’єкт із цього потоку. Якщо зовсім спростити, серіалізація — це як «заморозка» об’єкта, щоб потім «розморозити» його й отримати назад у тому ж стані.

Збереження стану об’єктів між запусками програми

Один із найчастіших сценаріїв — це збереження стану програми. Наприклад, у вас є список користувачів, результати гри або налаштування застосунку. Усе це зручно зберігати безпосередньо у вигляді об’єктів. Щоб дані не губилися між запусками, їх серіалізують у файл, а під час наступного запуску — десеріалізують.

Добрий приклад — звичайний сейв у грі. Коли гравець проходить рівень, його прогрес «заморожується» і записується у файл за допомогою серіалізації. Наступного дня він запускає гру — і прогрес «розморожується»: дані з файлу перетворюються назад на об’єкти, і гравець продовжує з того місця, де зупинився.

Створімо такий простий сейв:

import java.io.*;

// Клас гравця має бути Serializable
class Player implements Serializable {
    String name;
    int score;

    Player(String name, int score) {
        this.name = name;
        this.score = score;
    }
}

public class GameSaveExample {
    public static void main(String[] args) throws Exception {
        // Створюємо об'єкт гравця
        Player player = new Player("Ihor", 1500);

        // --- Збереження (серіалізація) ---
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("save.dat"))) {
            out.writeObject(player);
            System.out.println("Прогрес збережено!");
        }

        // --- Завантаження (десеріалізація) ---
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("save.dat"))) {
            Player loaded = (Player) in.readObject();
            System.out.println("Прогрес завантажено: " + loaded.name + " з очками " + loaded.score);
        }
    }
}

Зверніть увагу: щоб цей код працював, клас Player має реалізовувати інтерфейс Serializable. Детальніше про нього — у наступній лекції!

  • Player — звичайний клас із полями ім’я та очки, який реалізує інтерфейс Serializable (implements Serializable).
  • ObjectOutputStream записує об’єкт у файл "save.dat".
  • ObjectInputStream зчитує цей самий об’єкт назад.
  • У результаті маємо справжній сейв: під час наступного запуску програма завантажить об’єкт гравця з тим самим станом.

Передавання об’єктів мережею та між JVM

У розподілених системах часто потрібно передавати об’єкти між різними програмами або навіть різними машинами. Наприклад, у вас є клієнт і сервер, які мають обмінюватися повідомленнями. Серіалізація дає змогу «запакувати» об’єкт на одному боці, надіслати його мережею та «розпакувати» на іншому.

Приклад: Клієнт надсилає серверу об’єкт замовлення (Order), сервер його отримує, десеріалізує та обробляє.

Використання в технологіях Java

  • RMI (Remote Method Invocation): дає змогу викликати методи віддалених об’єктів — серіалізація потрібна для передавання аргументів і значень, що повертаються.
  • HTTP‑сесії: у сервлетах об’єкти в сесії серіалізуються під час перезапуску контейнера.
  • JMS (Java Message Service): повідомлення між компонентами можуть бути серіалізовані.
  • Кешування: об’єкти можуть серіалізуватися для зберігання в кеші (на диск або в розподілене сховище).

Кешування та переносимість

Якщо ви хочете швидко зберігати проміжні результати (наприклад, для кешування), серіалізація — чудовий інструмент. Ви серіалізуєте об’єкт, зберігаєте його на диск або в пам’ять, а потім швидко відновлюєте без повторних обчислень.

2. Приклади сценаріїв використання серіалізації

Збереження колекції користувачів у файл

Припустімо, у вас є клас User:

public class User {
    String name;
    int age;
    // ... інші поля
}

І у вас є список користувачів:

List<User> users = new ArrayList<>();
users.add(new User("Василь", 25));
users.add(new User("Марія", 30));
// ... і так далі

Щоб зберегти цей список у файл, ви серіалізуєте його. Коли потрібно — десеріалізуєте й отримуєте той самий список із тими самими користувачами. Нагадаємо, що клас User (і всі його поля) має підтримувати серіалізацію, тобто реалізовувати Serializable.

Передавання повідомлення між клієнтом і сервером

Класичний приклад — чат. Користувач пише повідомлення, об’єкт Message серіалізується та надсилається мережею. Сервер отримує потік байтів, десеріалізує об’єкт, обробляє його й, можливо, пересилає далі.

import java.io.*;
import java.net.*;

// Повідомлення має бути Serializable
class Message implements Serializable {
    String text;

    Message(String text) {
        this.text = text;
    }
}

// Сервер
class Server {
    public static void main(String[] args) throws Exception {
        try (ServerSocket serverSocket = new ServerSocket(5000)) {
            System.out.println("Сервер чекає на підключення...");
            Socket socket = serverSocket.accept();
            System.out.println("Клієнт підключився!");

            try (ObjectInputStream in = new ObjectInputStream(socket.getInputStream())) {
                Message msg = (Message) in.readObject();
                System.out.println("Отримано повідомлення: " + msg.text);
            }
        }
    }
}

// Клієнт
class Client {
    public static void main(String[] args) throws Exception {
        try (Socket socket = new Socket("localhost", 5000)) {
            try (ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream())) {
                Message msg = new Message("Привіт, сервер!");
                out.writeObject(msg);
                System.out.println("Повідомлення надіслано!");
            }
        }
    }
}

Як це працює:

  1. Спочатку запускається Server (він чекає на підключення).
  2. Потім запускається Client (він підключається до "localhost:5000").
  3. Клієнт серіалізує об’єкт Message і надсилає його через сокет.
  4. Сервер отримує потік байтів, десеріалізує його та друкує текст.

Тут ми використовуємо сокети (ServerSocket, Socket) — це механізм мережевої взаємодії, який ви вивчатимете пізніше. Важливі зараз не деталі роботи мережі, а сама ідея: клієнт створює об’єкт Message, серіалізує його та надсилає; сервер отримує потік байтів, десеріалізує назад в об’єкт і друкує повідомлення. Таким чином, навіть якщо поки незрозуміло, що це за класи ServerSocket і Socket, приклад показує цінність серіалізації: завдяки їй можна «запакувати» об’єкт, передати його мережею, а на іншому боці розпакувати без зайвих перетворень.

Кешування об’єктів

У великих застосунках часто використовують кешування для прискорення роботи. Наприклад, результати складних обчислень серіалізуються та зберігаються в кеші (файл, база даних, розподілене сховище). За наступного запиту результат можна швидко відновити, десеріалізувавши об’єкт.

import java.io.*;

// Результат обчислень, який ми хочемо кешувати
class Result implements Serializable {
    int value;

    Result(int value) {
        this.value = value;
    }
}

public class CacheExample {
    private static final String CACHE_FILE = "cache.dat";

    public static void main(String[] args) throws Exception {
        Result result;

        // Перевіряємо, чи є кеш
        File file = new File(CACHE_FILE);
        if (file.exists()) {
            // Завантажуємо результат із кешу
            try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(file))) {
                result = (Result) in.readObject();
                System.out.println("Завантажено з кешу: " + result.value);
            }
        } else {
            // «Важке» обчислення (для прикладу просто квадрат числа)
            int x = 12345;
            System.out.println("Рахуємо... (це довго)");
            result = new Result(x * x);

            // Зберігаємо результат у кеш
            try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file))) {
                out.writeObject(result);
                System.out.println("Збережено в кеш: " + result.value);
            }
        }
    }
}

3. Обмеження та ризики серіалізації

Серіалізація — потужний інструмент, але не без підводних каменів. Обговорімо основні обмеження та ризики.

Не всі об’єкти можна серіалізувати

У Java не всі об’єкти можуть бути серіалізовані «з коробки». Наприклад, об’єкти, що пов’язані із зовнішніми ресурсами (файли, мережеві з’єднання, потоки введення‑виведення), не підлягають серіалізації. Це логічно: серіалізувати «відкритий файл» або «живе» мережеве з’єднання неможливо — їхній стан залежить від операційної системи та середовища виконання.

Приклад: Клас із полем типу FileInputStream не можна серіалізувати — під час спроби серіалізації виникне помилка.

Питання безпеки

Серіалізація — це потенційна діра у безпеці. Якщо ви десеріалізуєте дані, отримані з ненадійного джерела (наприклад, з інтернету), зловмисник може підкласти шкідливий потік байтів, що призведе до неочікуваної поведінки вашої програми, а інколи — навіть до виконання шкідливого коду.

Правило: Ніколи не десеріалізуйте дані з недовірених джерел! Це як приймати посилку від невідомого відправника — всередині може бути що завгодно.

Сумісність версій

Якщо ви змінили структуру класу (наприклад, додали або видалили поле), раніше серіалізовані об’єкти можуть стати несумісними з новою версією класу. Це може призвести до помилок під час десеріалізації. Детальніше це питання буде розглянуто в наступних лекціях.

Продуктивність

Бінарна серіалізація в Java досить швидка, але іноді не найкомпактніша й не завжди зручна для обміну з іншими мовами програмування. Для обміну із зовнішніми системами часто використовують текстові формати (JSON, XML).

4. Типові помилки під час першого знайомства з серіалізацією

Помилка № 1: спроба серіалізувати об’єкт, який не реалізує інтерфейс Serializable.
У результаті отримаєте виняток NotSerializableException. Не забувайте явно вказувати implements Serializable у класі й стежити, щоб усі поля також були серіалізованими!

Помилка № 2: серіалізація об’єктів із несеріалізованими полями.
Якщо ваш клас містить поле типу, який не підтримує серіалізацію (наприклад, потік або з’єднання з БД), серіалізація не спрацює. Рішення — позначити такі поля як transient (про це згодом).

Помилка № 3: десеріалізація даних із ненадійних джерел.
Це може призвести до вразливостей безпеки або навіть до виконання шкідливого коду. Довіряйте лише тим даним, які були серіалізовані вашою програмою!

Помилка № 4: зміни структури класу після серіалізації.
Якщо ви зберегли об’єкт, а потім додали або видалили поле в класі, під час спроби десеріалізації виникне помилка або з’являться «дивні» значення. Детальніше — в наступних лекціях.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ