Serializable
справлялся со своей работой, да и автоматическая реализация всего процесса не может не радовать. Примеры, которые мы рассмотрели, тоже не были сложными. Так в чем же дело? Зачем еще один интерфейс для, по сути, той же задачи?
Дело в том, что Serializable
обладает рядом недостатков. Перечислим некоторые их них:
Производительность. У интерфейса
Serializable
много плюсов, но высокая производительность явно не из их числа.
Во-первых, внутренний механизм Serializable
во время работы генерирует большой объем служебной информации и разного рода временных данных.
Во-вторых (в это можешь сейчас не углубляться и почитать на досуге, если интересно), работа Serializable
основана на использовании Reflection API. Эта штуковина позволяет делать, казалось бы, невозможные в Java вещи: например, менять значения приватных полей. На JavaRush есть отличная статья про Reflection API, можешь почитать о ней здесь.
Гибкость.Мы вообще не управляем процессом сериализации-десериализации при использовании интерфейса
Serializable
.С одной стороны, это очень удобно, ведь если нас не особо волнует производительность, возможность не писать код кажется удобной. Но что если нам действительно необходимо добавить какие-то свои фичи (пример одной из них будет ниже) в логику сериализации?
По сути, все что у нас есть для управления процессом, — это ключевое слово
transient
для исключения каких-либо данных, и все. Такой себе «инструментарий» :/Безопасность.Этот пункт частично вытекает из предыдущего.
Мы раньше особо над этим не задумывались, но что делать, если какая-то информация в твоем классе не предназначена для «чужих ушей» (точнее, глаз)? Простой пример — пароль или другие персональные данные пользователя, которые в современном мире регулируются кучей законов.
Используя
Serializable
, мы по факту ничего с этим сделать не можем. Сериализуем все как есть.А ведь, по-хорошему, такого рода данные мы должны зашифровать перед записью в файл или передачей по сети. Но
Serializable
этой возможности не дает.
Externalizable
.
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class UserInfo implements Externalizable {
private String firstName;
private String lastName;
private String superSecretInformation;
private static final long SERIAL_VERSION_UID = 1L;
//...конструктор, геттеры, сеттеры, toString()...
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
}
Как видишь, у нас появились существенные изменения!
Главное из них очевидно: при имплементации интерфейса Externalizable
ты должен реализовать два обязательных метода — writeExternal()
и readExternal()
.
Как мы и говорили ранее, вся ответственность за сериализацию и десериализацию будет лежать на программисте.
Однако теперь ты можешь решить проблему отсутствия контроля над этим процессом! Весь процесс программируется напрямую тобой, что, конечно, создает гораздо более гибкий механизм.
Кроме того, решается проблема и c безопасностью. Как видишь, у нас в классе есть поле: персональные данные, которые нельзя хранить в незашифрованном виде.
Теперь мы легко можем написать код, соответствующий этому ограничению.
К примеру, добавить в наш класс два простых приватных метода для шифрования и дешифрования секретных данных. Записывать их в файл и вычитывать из файла мы будем именно в зашифрованном виде. А остальные данные будем записывать и считывать как есть :)
В результате наш класс будет выглядеть примерно так:
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Base64;
public class UserInfo implements Externalizable {
private String firstName;
private String lastName;
private String superSecretInformation;
private static final long serialVersionUID = 1L;
public UserInfo() {
}
public UserInfo(String firstName, String lastName, String superSecretInformation) {
this.firstName = firstName;
this.lastName = lastName;
this.superSecretInformation = superSecretInformation;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(this.getFirstName());
out.writeObject(this.getLastName());
out.writeObject(this.encryptString(this.getSuperSecretInformation()));
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
firstName = (String) in.readObject();
lastName = (String) in.readObject();
superSecretInformation = this.decryptString((String) in.readObject());
}
private String encryptString(String data) {
String encryptedData = Base64.getEncoder().encodeToString(data.getBytes());
System.out.println(encryptedData);
return encryptedData;
}
private String decryptString(String data) {
String decrypted = new String(Base64.getDecoder().decode(data));
System.out.println(decrypted);
return decrypted;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getSuperSecretInformation() {
return superSecretInformation;
}
}
Мы реализовали два метода, которые в качестве параметров используют те же ObjectOutput out
и ObjectInput
, с которыми мы уже встречались в лекции о Serializable
.
В нужный момент мы шифруем или расшифровываем необходимые данные, и в таком виде используем их для сериализации нашего объекта.
Посмотрим, как это будет выглядеть на практике:
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
UserInfo userInfo = new UserInfo("Ivan", "Ivanov", "Ivan Ivanov's passport data");
objectOutputStream.writeObject(userInfo);
objectOutputStream.close();
}
}
В методах encryptString()
и decryptString()
мы специально добавили вывод в консоль, чтобы проверить, в каком виде будут записаны и прочитаны секретные данные.
Код выше вывел в консоль строку:
SXZhbiBJdmFub3YncyBwYXNzcG9ydCBkYXRh
Шифрование удалось!
Полное содержание файла выглядит так:
¬н sr UserInfoГ!}ҐџC‚ћ xpt Ivant Ivanovt $SXZhbiBJdmFub3YncyBwYXNzcG9ydCBkYXRhx
Теперь попробуем использовать написанную нами логику десериализации.
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
UserInfo userInfo = (UserInfo) objectInputStream.readObject();
System.out.println(userInfo);
objectInputStream.close();
}
}
Ну, вроде ничего сложного тут нет, должно работать!
Запускаем…
Exception in thread "main" java.io.InvalidClassException: UserInfo; no valid constructor
Упс :(
Все оказалось не так просто! Механизм десериализации выбросил исключение и потребовал от нас создать конструктор по умолчанию. Интересно, зачем? В Serializable
мы обходились и без него… :/
Здесь мы подошли к еще одному важному нюансу. Разница между Serializable
и Externalizable
заключается не только в «расширенном» доступе для программиста и возможность более гибко управлять процессом, но и в самом процессе. Прежде всего, в механизме десериализации.
При использовании Serializable
под объект просто выделяется память, после чего из потока считываются значения, которыми заполняются все его поля.
Если мы используем Serializable
, конструктор объекта не вызывается! Вся работа производится через рефлексию (Reflection API, который мы мельком упоминали в прошлой лекции).
В случае с Externalizable
механизм десериализации будет иным. В начале вызывается конструктор по умолчанию. И только потом у созданного объекта UserInfo
вызывается метод readExternal()
, который и отвечает за заполнение полей объекта.
Именно поэтому любой класс, имплементирующий интерфейс Externalizable
, обязан иметь конструктор по умолчанию.
Добавим его в наш класс UserInfo
и перезапустим код:
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
UserInfo userInfo = (UserInfo) objectInputStream.readObject();
System.out.println(userInfo);
objectInputStream.close();
}
}
Вывод в консоль:
Ivan Ivanov's passport data
UserInfo{firstName='Ivan', lastName='Ivanov', superSecretInformation='Ivan Ivanov's passport data'}
Совсем другое дело! В консоль сначала была выведена дешифрованная строка с секретными данными, а после — наш восстановленный из файла объект в строковом формате!
Вот так мы успешно разрешили все проблемы :)
Тема сериализации и десериализации, казалось бы, несложная, но лекции у нас получились, как видишь, большими.
И это далеко не все! Есть еще множество тонкостей при использовании каждого из этих интерфейсов, но, чтобы сейчас у тебя не взрывался мозг от объема новой информации, я кратко перечислю еще несколько важных моментов и дам ссылки на дополнительное чтение.
Итак, что еще тебе нужно знать?
Во-первых, при сериализации (неважно, используешь ты Serializable
или Externalizable
) обращай внимание на переменные static
.
При использовании Serializable
эти поля вообще не сериализуются (и, соответственно, их значение не меняется, т.к. static
поля принадлежат классу, а не объекту). А вот при использовании Externalizable
ты управляешь процессом сам, поэтому технически сделать это можно. Но не рекомендуется, так как это чревато трудноуловимыми ошибками.
Во-вторых, внимание стоит также обратить на переменные с модификатором final
. При использовании Serializable
они сериализуются и десериализуются как обычно, а вот при использовании Externalizable
десериализовать final
-переменную невозможно!
Причина проста: все final
-поля инициализируются при вызове конструктора по умолчанию, и после этого их значение уже невозможно изменить. Поэтому для сериализации объектов, содержащих final
-поля, используй стандартную сериализацию через Serializable
.
В-третьих, при использовании наследования, все классы-наследники, происходящие от какого-то Externalizable
-класса, тоже должны иметь конструкторы по умолчанию.
Вот несколько ссылок на хорошие статьи про механизмы сериализации:
До встречи! :)