Serializable
справлялся со своей работой, да и автоматическая реализация всего процесса не может не радовать. Примеры, которые мы рассмотрели, тоже не были сложными. Так в чем же дело? Зачем еще один интерфейс для, по сути, той же задачи? Дело в том, что Serializable
обладает рядом недостатков. Перечислим некоторые их них:
-
Производительность. У интерфейса
Serializable
много плюсов, но высокая производительность явно не из их числа.
Во-первых, внутренний механизм Serializable
во время работы генерирует большой объем служебной информации и разного рода временных данных.
Во-вторых (в это можешь сейчас не углубляться и почитать на досуге, если интересно), работа Serializable
основана на использовании Reflection API. Эта штуковина позволяет делать, казалось бы, невозможные в Java вещи: например, менять значения приватных полей. На JavaRush есть отличная статья про Reflection API, можешь почитать о ней здесь.
-
Гибкость.Мы вообще не управляем процессом сериализации-десериализации при использовании интерфейса
Serializable
.С одной стороны, это очень удобно, ведь если нас не особо волнует производительность, возможность не писать code кажется удобной. Но что если нам действительно необходимо добавить Howие-то свои фичи (пример одной из них будет ниже) в логику сериализации?
По сути, все что у нас есть для управления процессом, — это ключевое слово
transient
для исключения Howих-либо данных, и все. Такой себе «инструментарий» :/ -
Безопасность.Этот пункт частично вытекает из предыдущего.
Мы раньше особо над этим не задумывались, но что делать, если Howая-то информация в твоем классе не предназначена для «чужих ушей» (точнее, глаз)? Простой пример — пароль or другие персональные данные пользователя, которые в современном мире регулируются кучей законов.
Используя
Serializable
, мы по факту ничего с этим сделать не можем. Сериализуем все How есть.А ведь, по-хорошему, такого рода данные мы должны зашифровать перед записью в файл or передачей по сети. Но
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 {
}
}
Как видишь, у нас появorсь существенные изменения! Главное из них очевидно: при имплементации интерфейса Externalizable
ты должен реализовать два обязательных метода — writeExternal()
и readExternal()
. Как мы и говорor ранее, вся ответственность за сериализацию и десериализацию будет лежать на программисте. Однако теперь ты можешь решить проблему отсутствия контроля над этим процессом! Весь процесс программируется напрямую тобой, что, конечно, создает гораздо более гибкий механизм. Кроме того, решается проблема и c безопасностью. Как видишь, у нас в классе есть поле: персональные данные, которые нельзя хранить в незашифрованном виде. Теперь мы легко можем написать code, соответствующий этому ограничению. К примеру, добавить в наш класс два простых приватных метода для шифрования и дешифрования секретных данных. Записывать их в файл и вычитывать из file мы будем именно в зашифрованном виде. А остальные данные будем записывать и считывать How есть :) В результате наш класс будет выглядеть примерно так:
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
. В нужный момент мы шифруем or расшифровываем необходимые данные, и в таком виде используем их для сериализации нашего an object. Посмотрим, How это будет выглядеть на практике:
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()
мы специально добавor вывод в консоль, чтобы проверить, в Howом виде будут записаны и прочитаны секретные данные. Код выше вывел в консоль строку: SXZhbiBJdmFub3YncyBwYXNzcG9ydCBkYXRh Шифрование удалось! Полное содержание file выглядит так: ¬н 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
мы обходorсь и без него… :/ Здесь мы подошли к еще одному важному нюансу. Difference между Serializable
и Externalizable
заключается не только в «расширенном» доступе для программиста и возможность более гибко управлять процессом, но и в самом процессе. Прежде всего, в механизме десериализации. При использовании Serializable
под an object просто выделяется память, после чего из потока считываются значения, которыми заполняются все его поля. Если мы используем Serializable
, конструктор an object не вызывается! Вся работа производится через рефлексию (Reflection API, который мы мельком упоминали в прошлой лекции). В случае с Externalizable
механизм десериализации будет иным. В начале вызывается конструктор по умолчанию. И только потом у созданного an object UserInfo
вызывается метод readExternal()
, который и отвечает за заполнение полей an object. Именно поэтому любой класс, имплементирующий интерфейс Externalizable
, обязан иметь конструктор по умолчанию. Добавим его в наш класс UserInfo
и перезапустим code:
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'} Совсем другое дело! В консоль сначала была выведена дешифрованная строка с секретными данными, а после — наш восстановленный из file an object в строковом формате! Вот так мы успешно разрешor все проблемы :) Тема сериализации и десериализации, казалось бы, несложная, но лекции у нас получorсь, How видишь, большими. И это далеко не все! Есть еще множество тонкостей при использовании каждого из этих интерфейсов, но, чтобы сейчас у тебя не взрывался мозг от объема новой информации, я кратко перечислю еще несколько важных моментов и дам ссылки на дополнительное чтение. Итак, что еще тебе нужно знать? Во-первых, при сериализации (неважно, используешь ты Serializable
or Externalizable
) обращай внимание на переменные static
. При использовании Serializable
эти поля вообще не сериализуются (и, соответственно, их meaning не меняется, т.к. static
поля принадлежат классу, а не an objectу). А вот при использовании Externalizable
ты управляешь процессом сам, поэтому технически сделать это можно. Но не рекомендуется, так How это чревато трудноуловимыми ошибками. Во-вторых, внимание стоит также обратить на переменные с модификатором final
. При использовании Serializable
они сериализуются и десериализуются How обычно, а вот при использовании Externalizable
десериализовать final
-переменную невозможно! Причина проста: все final
-поля инициализируются при вызове конструктора по умолчанию, и после этого их meaning уже невозможно изменить. Поэтому для сериализации an objectов, содержащих final
-поля, используй стандартную сериализацию через Serializable
. В-третьих, при использовании наследования, все классы-наследники, происходящие от Howого-то Externalizable
-класса, тоже должны иметь конструкторы по умолчанию. Вот несколько ссылок на хорошие статьи про механизмы сериализации:
До встречи! :)
GO TO FULL VERSION