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("Сообщение отправлено!");
}
}
}
}
Как это работает:
- Сначала запускается Server (он ждёт подключения).
- Потом запускается Client (он подключается к "localhost:5000").
- Клиент сериализует объект Message и отправляет его через сокет.
- Сервер получает поток байтов, десериализует его и печатает текст.
Здесь мы используем сокеты (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: изменения структуры класса после сериализации.
Если вы сохранили объект, а потом добавили или удалили поле в классе, при попытке десериализации возникнет ошибка или появятся «странные» значения. Подробнее — в следующих лекциях.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ