Привет! В сегодняшней статье мы рассмотрим модификатор transient в Java. Поговорим о том, зачем данный модификатор нужен и как его правильно использовать. Поехали! Что скрывает модификатор transient в Java - 1

Вспомним сериализацию

Модификатор transient используется в процессе сериализации и десериализации объектов. Поэтому для начала кратко поговорим об этом.Что скрывает модификатор transient в Java - 2Предположим, у нас есть некоторый объект, а у него — поля, каждое из которых имеет какое-то значение. Все это называется состоянием объекта. Сериализация — это конвертация состояния объекта в последовательность байт. Данные байты сохраняются, как правило, в каком-либо файле. Десериализация — это обратный процесс. Представим, что мы сериализовали объект в байты и сохранили данный набор байтов в некотором файле. При десериализации программе нужно:
  1. Считать набор байтов из файла.
  2. Сконструировать из данного набора байтов исходный объект и задать каждому полю значение, которое было у объекта на момент сериализации.
Когда это может быть полезным? Например, когда мы хотим, чтобы при завершении работы программа сохраняла свое состояние, и при следующем включении восстанавливала его. Когда вы завершаете работу IntelliJ IDEA, при следующем включении, скорее всего, у вас открыты те же вкладки и классы

Вспомним сериализацию на практике

Что же, теперь рассмотрим сериализацию на практике. Если хочется получше разобраться в теме, советуем почитать материал Сериализация и десериализация в Java. Ну а в этой статье мы пройдемся по верхам и перейдем сразу к примерам. Предположим, у нас есть класс User с набором некоторых полей, геттерами и сеттерами, а также методом toString:

public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    private String firstName;
    private String lastName;
    private String email;
    private LocalDate birthDate;
    private String login;
    private String password;

    public User() {}

    public User(String firstName, String lastName, String email, LocalDate birthDate, String login, String password) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
        this.birthDate = birthDate;
        this.login = login;
        this.password = password;
    }

    /*
        Геттеры, Сеттеры
     */

    @Override
    public String toString() {
        return "User{" +
                "firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", email='" + email + '\'' +
                ", birthDate=" + birthDate +
                ", login='" + login + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}
Мы хотим сериализовывать объекты данного класса в дальнейшем. Напишем метод, который принимает объект User и строку path — путь до файла, в котором мы сохраним байты:

    static void serialize(User user, String path) throws IOException {
        FileOutputStream outputStream = null;
        ObjectOutputStream objectOutputStream = null;
        try {
            //создаем 2 потока для сериализации объекта и сохранения его в файл
            outputStream = new FileOutputStream(path);
            objectOutputStream = new ObjectOutputStream(outputStream);

            // сохраняем объект в файл
            objectOutputStream.writeObject(user);
        } finally {
            // Закроем потоки в блоке finally
            if (objectOutputStream != null) {
                objectOutputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
Также напишем метод для десериализации. Метод принимает строку path (путь до файла из которого объект будет “загружен”) и возвращает объект типа User:

    static User deserialize(String path) throws IOException, ClassNotFoundException {
        FileInputStream fileInputStream = null;
        ObjectInputStream objectInputStream = null;

        try {

            //создаем 2 потока для десериализации объекта из файла
            fileInputStream = new FileInputStream(path);
            objectInputStream = new ObjectInputStream(fileInputStream);

            //загружаем объект из файла
            return  (User) objectInputStream.readObject();
        } finally {
            if (fileInputStream != null) {
                fileInputStream.close();
            }
            if (objectInputStream != null) {
                objectInputStream.close();
            }
        }
    }
Все инструменты готовы к работе. Пришло время расщеплять на атомы байты. Напишем метод main, в котором создадим объект класса User и сериализуем его. Затем загрузим и сравним с тем, что было изначально:

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // вставьте свой путь до файла
        final String path = "/home/zor/user.ser";

        //создаем наш объект
        User user = new User();
        user.setFirstName("Stefan");
        user.setLastName("Smith");
        user.setEmail("ssmith@email.com");
        user.setBirthDate(LocalDate.of(1991, 7, 16));
        user.setLogin("ssmith");
        user.setPassword("gemma_arterton_4ever_in_my_heart91");

        System.out.println("Initial user: " + user + "\r\n");


        serialize(user, path);
        User loadedUser = deserialize(path);
        System.out.println("Loaded user from file: " + loadedUser + "\r\n");
    }
Если запустить метод, мы увидим следующий вывод:

Initial user: User{firstName='Stefan', lastName='Smith', email='ssmith@email.com', birthDate=1991-07-16, login='ssmith', password='gemma_arterton_4ever_in_my_heart91'}

Loaded user from file: User{firstName='Stefan', lastName='Smith', email='ssmith@email.com', birthDate=1991-07-16, login='ssmith', password='gemma_arterton_4ever_in_my_heart91'}
Как видно из вывода, объекты идентичны. Но есть маленькое но… И это как раз то место, когда в игру вступает испанский стыд transient.

Модификатор (ну наконец-таки) transient

Никого не смутило, что мы пароль пользователя сохранили? Особенно такой пароль… Да-да, мы сами его придумали, но все же… Порой бывают ситуации, когда некоторые поля невозможно сериализовать, или лучше этого не делать. В примере выше хотелось бы сохранять все поля, за исключением пароля. Как это добиться? Ответ: использовать модификатор transient. transient — это модификатор, указываемый перед полем класса (подобно другим модификаторам, таким как public, final и т.д.) для обозначения того, что данное поле не должно быть сериализовано. Поля, помеченные ключевым словом transient, не сериализуются. Теперь отредактируем пример с нашим пользователем, чтобы исправить небольшой конфуз и не сохранять пароль пользователя. Для этого отметим соответствующее поле в классе ключевым словом transient:

public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    private String firstName;
    private String lastName;
    private String email;
    private LocalDate birthDate;
    private String login;
    private transient String password;
    
    /*
        Конструкторы, геттеры, сеттеры, toString...
     */
}
Если мы еще раз запустим метод main из примера выше, увидим, что пароль не сохранился:

Initial user: User{firstName='Stefan', lastName='Smith', email='ssmith@email.com', birthDate=1991-07-16, login='ssmith', password='gemma_arterton_4ever_in_my_heart91'}

Loaded user from file: User{firstName='Stefan', lastName='Smith', email='ssmith@email.com', birthDate=1991-07-16, login='ssmith', password='null'}
Отлично, мы добились поставленной цели и не сохраняем конфиденциальную информацию. Особенно такую информацию… (простите)

Когда использовать transient?

Пример с пользователем был нужен для того, чтобы погрузиться в контекст сериализации. Теперь поговорим предметнее о том, когда следует использовать модификатор transient.

  • Поля, которые вычисляются программно

В некоторых классах иногда бывают такие поля, которые вычисляются на основе других полей или же другой информации. Вычисляются, так сказать, на лету. Чтобы привести пример такого поля, представим себе заказ в интернет-магазине или же в каком-нибудь сервисе доставки еды. Каждый заказ, помимо прочей информации, состоит из списка товаров и итоговой стоимости. Она, в свою очередь, складывается из суммарной стоимости каждого товара. Выходит, что итоговую стоимость не стоит задавать “руками”: ее нужно вычислять программно, суммируя стоимость всех товаров. Подобные поля, которые следует вычислять программно, не нужно сериализовывать. Поэтому помечаем их модификатором transient.

class Order implements Serializable {

    private List items; 
    private transient BigDecimal totalAmount; //вычисляется на ходу

}

  • Поля с приватной информацией

Также бывают некоторые классы, которые хранят приватную информацию. Пример такого класса мы рассматривали в начале статьи. Не стоит допускать утечки такой информации за пределы JVM. Поэтому поля с подобными данными необходимо помечать модификатором transient, если вы собираетесь сериализовывать такой класс.

  • Поля, которые не реализуют интерфейс Serializable

Иногда класс содержит поля — объекты других классов, которые не реализуют интерфейс Serializable. Пример таких полей — логгеры, потоки ввода-вывода, объекты, которые хранят соединения с базой данных и прочие служебные классы. Если попытаться сериализовать объект, который содержит несериализуемые поля, возникнет ошибка java.io.NotSerializableException. Чтобы избежать этого, все поля, которые не реализуют интерфейс Serializable, необходимо помечать модификатором transient.

public class FileReader implements Serializable {
    // Первые 2 поля не реализуют Serializable
    // Помечаем их как transient поля
    private transient InputStream is; 
    private transient BufferedReader buf;
    private String fileName;

    // Constructors, Getters, Setters

    public String readFile() throws IOException {
        try {
            is = new FileInputStream(fileName);
            buf = new BufferedReader(new InputStreamReader(is));
            String line = buf.readLine();
            StringBuilder sb = new StringBuilder();
            while (line != null) {
                sb.append(line).append("\n");
                line = buf.readLine();
            }
            return sb.toString();
        } finally {
            if (buf != null) {
                buf.close();
            }
            if (is != null) {
                is.close();
            }
        }
    }
}

  • Поля с информацией о состоянии объекта

Ну и последнее. Не нужно сериализовывать поля, которые не являются частью информации о состоянии объекта. Примеры выше попадают под это правило. Но также сюда можно включить и все прочие поля, добавленные для дебага или для выполнения какой то служебной функции, которые не несут информации о состоянии объекта.

transient и final

Итоги

На этом все. Сегодня мы говорили о модификаторе transient:
  1. Вспомнили сериализацию в теории и на практике.
  2. Поняли, что для того, чтобы не сериализовать некоторые поля класса, их нужно помечать модификатором transient.
  3. Обсудили, в каких ситуациях следует использовать данный модификатор. Таких ситуаций оказалось четыре:
    1. поля, которые вычисляются программно;
    2. поля, которые содержат секретную информацию;
    3. поля, которые не реализуют интерфейс Serializable;
    4. поля, которые не являются частью состояния объекта.