JavaRush /Java 博客 /Random-ZH /Java中的transient修饰符隐藏了什么?
Анзор Кармов
第 31 级
Санкт-Петербург

Java中的transient修饰符隐藏了什么?

已在 Random-ZH 群组中发布
你好!在今天的文章中,我们将了解Java中的transient修饰符。我们来谈谈为什么需要这个修饰符以及如何正确使用它。去! Java中的transient修饰符隐藏了什么 - 1

让我们记住序列化

修饰符transient用于对象的序列化和反序列化过程中。那么我们先简单谈谈这一点。Java中的transient修饰符隐藏了什么——2假设我们有一些对象,并且它有字段,每个字段都有一些值。所有这些都称为对象的状态。序列化是将对象的状态转换为字节序列。这些字节通常存储在某个文件中。反序列化是相反的过程。假设我们将一个对象序列化为字节并将这组字节存储在某个文件中。反序列化时,程序需要:
  1. 从文件中读取一组字节。
  2. 从这组字节构造一个初始对象,并将每个字段设置为该对象在序列化时具有的值。
这什么时候有用?例如,当我们希望程序在关机时保存其状态并在下次开机时恢复它。当您关闭 IntelliJ IDEA 时,下次打开它时很可能会打开相同的选项卡和类

让我们记住实践中的序列化

好吧,现在让我们看看实践中的序列化。如果您想更好地理解该主题,我们建议您阅读材料Java 中的序列化和反序列化。好吧,在这篇文章中我们将超越顶部并直接进入示例。假设我们有一个类,User其中包含一组字段、getter 和 setter 以及一个方法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 {
        //create 2 threads to serialize the object and save it to a file
        outputStream = new FileOutputStream(path);
        objectOutputStream = new ObjectOutputStream(outputStream);

        // сохраняем an object в файл
        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 потока для десериализации an object из file
        fileInputStream = new FileInputStream(path);
        objectInputStream = new ObjectInputStream(fileInputStream);

        //загружаем an object из file
        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 {
    // вставьте свой путь до file
    final String path = "/home/zor/user.ser";

    // create our object
    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

有人对我们保存用户密码感到困惑吗?尤其是这样的密码……是的,是的,我们自己想出来的,但是还是……有时候会出现某些字段无法序列化的情况,或者最好不要这样做。在上面的示例中,我想保存除密码之外的所有字段。如何实现这一目标?答:使用修饰符transienttransient是放置在类字段之前的修饰符(类似于 等其他修饰符publicfinal,指示该字段不应被序列化。用关键字标记的字段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
class Order implements Serializable {

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

}

  • 包含私人信息的字段

还有一些类存储私有信息。我们在文章开头查看了此类的一个示例。您不应该允许此类信息泄漏到 JVM 之外。transient因此,如果要序列化这样的类,则必须用修饰符标记具有此类数据的字段。

  • 未实现接口的字段Serializable

有时,一个类包含字段 - 未实现该接口的其他类的对象Serializable。此类字段的示例包括记录器、I/O 流、存储数据库连接的对象和其他实用程序类。如果您尝试序列化包含不可序列化字段的对象,您将收到错误java.io.NotSerializableException。为了避免这种情况,所有未实现该接口的字段都Serializable必须使用修饰符进行标记transient
public class FileReader implements Serializable {
    // Первые 2 поля не реализуют Serializable
    // Помечаем их How 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();
            }
        }
    }
}

  • 包含有关对象状态信息的字段

好吧,最后一件事。无需序列化不属于对象状态信息的字段。上面的例子就属于这个规则。但您还可以在此处包含为调试或执行某种不携带有关对象状态信息的某种服务功能而添加的所有其他字段。

transientfinal

结果

就这样。今天我们讨论了修饰符transient
  1. 我们记住了理论和实践中的序列化。
  2. 我们意识到,为了不序列化类的某些字段,需要用修饰符来标记它们transient
  3. 我们讨论了在什么情况下应该使用这个修饰符。这样的情况有四种:
    1. 以编程方式计算的字段;
    2. 包含秘密信息的字段;
    3. 未实现接口的字段Serializable
    4. 不属于对象状态的字段。
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION