JavaRush /Java блог /Random UA /Що приховує модифікатор transient у Java
Анзор Кармов
31 рівень
Санкт-Петербург

Що приховує модифікатор transient у Java

Стаття з групи Random UA
Вітання! У сьогоднішній статті ми розглянемо модифікатор 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. поля, які є частиною стану об'єкта.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ