概述

在Java中,一个类只要实现Serializable接口,这个类的对象就可以被序列化,这种序列化模式为开发者提供了很多便利,我们可以不必关心具体序列化的过程,只要这个类实现了Serializable接口,这个类的所有属性都会自动序列化。但是有时我们需要让类的某些属性不被序列化,如密码这类信息,为了安全起见,不希望在网络操作中被传输或者持久化到本地。只要在相应的属性前加上transient关键字,就可以实现部分属性不被序列化,该属性的生命周期仅存于调用者的内存中而不会写入到磁盘持久化。

transient的使用

  1. public class TransientTest {
  2. public static void main(String[] args) {
  3. User user = new User();
  4. user.setUsername("Github");
  5. user.setPassword("123456");
  6. System.out.println("read before Serializable: ");
  7. System.out.println("username: " + user.getUsername());
  8. System.err.println("password: " + user.getPassword());
  9. try {
  10. ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("user.txt"));
  11. os.writeObject(user); // 将User对象写进文件
  12. os.flush();
  13. os.close();
  14. } catch (IOException e) {
  15. e.printStackTrace();
  16. }
  17. try {
  18. ObjectInputStream is = new ObjectInputStream(new FileInputStream("user.txt"));
  19. user = (User) is.readObject(); // 从流中读取User的数据
  20. is.close();
  21. System.out.println("\nread after Serializable: ");
  22. System.out.println("username: " + user.getUsername());
  23. System.err.println("password: " + user.getPassword());
  24. } catch (IOException | ClassNotFoundException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. }
  29. public class User implements Serializable {
  30. private static final long serialVersionUID = 1234567890L;
  31. private String username;
  32. private transient String password;
  33. public String getUsername() {
  34. return username;
  35. }
  36. public void setUsername(String username) {
  37. this.username = username;
  38. }
  39. public String getPassword() {
  40. return password;
  41. }
  42. public void setPassword(String password) {
  43. this.password = password;
  44. }
  45. }

运行结果
image.png

transient修饰静态变量

  1. public class TransientTest {
  2. public static void main(String[] args) {
  3. User user = new User();
  4. user.setUsername("Github");
  5. user.setPassword("123456");
  6. System.out.println("read before Serializable: ");
  7. System.out.println("username: " + user.getUsername());
  8. System.err.println("password: " + user.getPassword());
  9. try {
  10. ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("user.txt"));
  11. os.writeObject(user); // 将User对象写进文件
  12. os.flush();
  13. os.close();
  14. } catch (IOException e) {
  15. e.printStackTrace();
  16. }
  17. try {
  18. // 在反序列化前盖板username的值
  19. user.setUsername("Tom");
  20. ObjectInputStream is = new ObjectInputStream(new FileInputStream("user.txt"));
  21. user = (User) is.readObject(); // 从流中读取User的数据
  22. is.close();
  23. System.out.println("\nread after Serializable: ");
  24. System.out.println("username: " + user.getUsername());
  25. System.err.println("password: " + user.getPassword());
  26. } catch (IOException | ClassNotFoundException e) {
  27. e.printStackTrace();
  28. }
  29. }
  30. }
  31. public class User implements Serializable {
  32. private static final long serialVersionUID = 1234567890L;
  33. private static String username;
  34. private transient String password;
  35. public String getUsername() {
  36. return username;
  37. }
  38. public void setUsername(String username) {
  39. this.username = username;
  40. }
  41. public String getPassword() {
  42. return password;
  43. }
  44. public void setPassword(String password) {
  45. this.password = password;
  46. }
  47. }

运行结果
image.png

案例研究:HashMap如何使用transient关键字?

到目前为止,我们一直在讨论与transient关键字相关的概念,这些概念基本上都是理论性的。让我们了解一下在HashMap类中逻辑地使用transient的正确用法。它将使您更好地了解java中transient关键字的实际用法。
在理解使用transient创建的解决方案之前,让我们先确定问题本身。
HashMap用于存储键-值对,这一点我们都知道。我们还知道HashMap中键的位置是根据键实例的哈希码计算的。现在,当我们序列化一个HashMap时,这意味着HashMap中的所有键以及与键相关的所有值也将被序列化。序列化之后,当我们反序列化HashMap实例时,所有关键实例也将被反序列化。我们知道在这个序列化/反序列化过程中,可能会丢失信息(用于计算hashcode),最重要的是它本身是一个新实例。
在java中,任何两个实例(甚至是相同类的实例)都不能有相同的hashcode。这是一个大问题,因为应该根据新的hashcode放置键的位置不正确。当检索键的值时,您将在这个新的HashMap中引用错误的索引。
因此,当一个哈希表被序列化时,它意味着哈希索引,和表的顺序不再有效,不应该被保留。这是问题陈述。
现在看看如何在HashMap类中解决这个问题。如果通过HashMap.java的源代码。你会发现下面的声明:

  1. transient Node<K,V>[] table;
  2. transient Set<Map.Entry<K,V>> entrySet;
  3. transient int size;
  4. transient int modCount;

所有重要字段都标记为transient(所有字段实际上都是在运行时计算/更改的),因此它们不是序列化HashMap实例的一部分。为了再次填充这个重要的信息,HashMap类使用writeObject()和readObject()方法,如下所示:

  1. private void writeObject(java.io.ObjectOutputStream s)
  2. throws IOException {
  3. int buckets = capacity();
  4. // Write out the threshold, loadfactor, and any hidden stuff
  5. s.defaultWriteObject();
  6. s.writeInt(buckets);
  7. s.writeInt(size);
  8. internalWriteEntries(s);
  9. }
  10. void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
  11. Node<K,V>[] tab;
  12. if (size > 0 && (tab = table) != null) {
  13. for (Node<K,V> e : tab) {
  14. for (; e != null; e = e.next) {
  15. s.writeObject(e.key);
  16. s.writeObject(e.value);
  17. }
  18. }
  19. }
  20. }
  1. private void readObject(java.io.ObjectInputStream s)
  2. throws IOException, ClassNotFoundException {
  3. // Read in the threshold (ignored), loadfactor, and any hidden stuff
  4. s.defaultReadObject();
  5. reinitialize();
  6. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  7. throw new InvalidObjectException("Illegal load factor: " +
  8. loadFactor);
  9. s.readInt(); // Read and ignore number of buckets
  10. int mappings = s.readInt(); // Read number of mappings (size)
  11. if (mappings < 0)
  12. throw new InvalidObjectException("Illegal mappings count: " +
  13. mappings);
  14. else if (mappings > 0) { // (if zero, use defaults)
  15. // Size the table using given load factor only if within
  16. // range of 0.25...4.0
  17. float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
  18. float fc = (float)mappings / lf + 1.0f;
  19. int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
  20. DEFAULT_INITIAL_CAPACITY :
  21. (fc >= MAXIMUM_CAPACITY) ?
  22. MAXIMUM_CAPACITY :
  23. tableSizeFor((int)fc));
  24. float ft = (float)cap * lf;
  25. threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
  26. (int)ft : Integer.MAX_VALUE);
  27. // Check Map.Entry[].class since it's the nearest public type to
  28. // what we're actually creating.
  29. SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Map.Entry[].class, cap);
  30. @SuppressWarnings({"rawtypes","unchecked"})
  31. Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
  32. table = tab;
  33. // Read the keys and values, and put the mappings in the HashMap
  34. for (int i = 0; i < mappings; i++) {
  35. @SuppressWarnings("unchecked")
  36. K key = (K) s.readObject();
  37. @SuppressWarnings("unchecked")
  38. V value = (V) s.readObject();
  39. putVal(hash(key), key, value, false, false);
  40. }
  41. }
  42. }

使用上面的代码,HashMap仍然允许像通常那样处理非transient字段,但是它们在字节数组的末尾一个接一个写存储的键-值对。在反序列化时,它允许默认情况下处理的非transient变量,然后逐个读取键-值对。对于每个键,哈希值和索引将被再次计算,并被插入到表中的正确位置,以便再次检索不会出现任何错误
上面使用transient关键字就是一个很好的例子。您应该记住它,并在下一次java面试问题中提到它。

总结

  1. 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
  2. transient关键字只能修饰变量,而不能修饰方法和类。本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
  3. 一个静态变量不管是否被transient修饰,均不能被序列化。
  4. 无论何时将任何final字段/引用计算为“常量表达式”,JVM都会对其进行序列化,忽略transient关键字的存在。

    参考链接

    https://zhuanlan.zhihu.com/p/51980884
    https://www.jianshu.com/p/2911e5946d5c