实例 HashMap 如何使用transient 关键字?
到目前为止,我们一直在讨论与“暂时”关键字相关的概念,这些概念在本质上大多是理论性的。让我们理解“transient”的正确用法,它在“HashMap”类中使用得非常有逻辑。它将使您更好地了解java中transient关键字的实际用法。
在理解使用transient创建的解决方案之前,让我们首先确定问题本身。
HashMap
用于存储键值对,我们都知道。
并且我们也知道HashMap
中key的位置是根据key的实例得到的hash码计算出来的。
现在,当我们序列化一个 HashMap
时,这意味着 HashMap
中的所有键和对应于键的所有值也将被序列化。
序列化后,当我们反序列化 HashMap
实例时,所有键实例也将被反序列化。
我们知道在这个序列化/反序列化过程中,可能会丢失信息(用于计算哈希码),最重要的是它本身就是一个新实例。
在java中,任何两个实例(即使是同一个类)不能有相同的 hashcode 。
这是一个大问题,因为根据新的哈希码应该放置键的位置不在正确的位置。
在检索键的值时,我们将在这个新的 HashMap 中引用错误的索引。
因此,当哈希映射被序列化时,这意味着哈希索引以及表的排序不再有效,不应保留。
这是问题语句。
现在看看它是如何在 HashMap 类中解决的。
如果查看 HashMap.java 的源代码,我们会发现以下声明:
transient Entry table[]; transient int size; transient int modCount; transient int hashSeed; private transient Set entrySet;
所有重要的字段都被标记为“transient”(它们都是在运行时实际计算/更改的),因此它们不是序列化的HashMap
实例的一部分。
为了再次填充这个重要信息,HashMap
类使用 writeObject() 和 readObject() 方法,如下所示:
private void writeObject(ObjectOutputStream objectoutputstream) throws IOException { objectoutputstream.defaultWriteObject(); if (table == EMPTY_TABLE) objectoutputstream.writeInt(roundUpToPowerOf2(threshold)); else objectoutputstream.writeInt(table.length); objectoutputstream.writeInt(size); if (size > 0) { Map.Entry entry; for (Iterator iterator = entrySet0().iterator(); iterator.hasNext(); objectoutputstream.writeObject(entry.getValue())) { entry = (Map.Entry) iterator.next(); objectoutputstream.writeObject(entry.getKey()); } } } private void readObject(ObjectInputStream objectinputstream) throws IOException, ClassNotFoundException { objectinputstream.defaultReadObject(); if (loadFactor <= 0.0F || Float.isNaN(loadFactor)) throw new InvalidObjectException((new StringBuilder()) .append("Illegal load factor: ").append(loadFactor).toString()); table = (Entry[]) EMPTY_TABLE; objectinputstream.readInt(); int i = objectinputstream.readInt(); if (i < 0) throw new InvalidObjectException((new StringBuilder()).append("Illegal mappings count: ").append(i).toString()); int j = (int) Math.min((float) i * Math.min(1.0F / loadFactor, 4F), 1.073742E+009F); if (i > 0) inflateTable(j); else threshold = j; init(); for (int k = 0; k < i; k++) { Object obj = objectinputstream.readObject(); Object obj1 = objectinputstream.readObject(); putForCreate(obj, obj1); } }
有了上面的代码,HashMap
仍然让非瞬态字段像往常一样被处理,但是他们将存储的键值对一个接一个地写在字节数组的末尾。
在反序列化时,它让非瞬态变量由默认反序列化过程处理,然后一一读取键值对。
对于每个键,再次计算散列和索引并将其插入到表中的正确位置,以便可以再次检索而不会出现任何错误。
1.什么是Javatransient关键字
java 中的修饰符瞬态可以应用于类的字段成员以关闭这些字段成员的序列化。
每个标记为瞬态的字段都不会被序列化。
我们使用transient 关键字向java 虚拟机指示transient 变量不是对象持久状态的一部分。
让我们写一个非常基本的例子来理解上面的类比到底是什么意思。
我将创建一个 Employee
类并定义 3 个属性,例如:firstName、lastName 和 secretInfo。
我们不想出于某种目的存储/保存“confidentialInfo”,因此我们将该字段标记为“transient”。
class Employee implements Serializable { private String firstName; private String lastName; private transient String confidentialInfo; //Setters and Getters }
现在让我们序列化一个 Employee
类的实例。
try { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("empInfo.ser")); Employee emp = new Employee(); emp.setFirstName("Jamez"); emp.setLastName("Gupta"); emp.setConfidentialInfo("password"); //Serialize the object oos.writeObject(emp); oos.close(); } catch (Exception e) { System.out.println(e); }
现在让我们反序列化回 java object ,并验证是否保存了“confidentialInfo”?
try { ObjectInputStream ooi = new ObjectInputStream(new FileInputStream("empInfo.ser")); //Read the object back Employee readEmpInfo = (Employee) ooi.readObject(); System.out.println(readEmpInfo.getFirstName()); System.out.println(readEmpInfo.getLastName()); System.out.println(readEmpInfo.getConfidentialInfo()); ooi.close(); } catch (Exception e) { System.out.println(e); }
输出:
Jamez Gupta null
显然,“confidentialInfo”在序列化时没有保存到持久状态,这正是我们在 java 中使用“transient”关键字的原因。
Java transient关键字用于类属性/变量,以指示此类类的序列化过程应忽略此类变量,同时为该类的任何实例创建持久字节流。
瞬态变量(transient)是不能序列化的变量。
根据 Java 语言规范 [jls-8.3.1.3] “变量可能被标记为瞬态以表明它们不是对象持久状态的一部分.”
2. java中什么时候应该使用transient关键字?
- 第一个也是非常合乎逻辑的情况是,我们可能拥有从类实例中的其他字段派生/计算的字段。它们应该每次都以编程方式计算,而不是通过序列化来持久化状态。一个例子可能是基于时间戳的值;例如一个人的年龄或者时间戳和当前时间戳之间的持续时间。在这两种情况下,我们都将根据当前系统时间而不是实例序列化时间来计算变量的值。
- 第二个逻辑示例可以是任何不应以任何形式(在数据库或者字节流中)泄漏到 JVM 之外的安全信息。
- 另一个示例可能是在 JDK 或者应用程序代码中未标记为“可序列化”的字段。未实现 Serializable 接口并在任何可序列化类中引用的类,不能被序列化;并且会抛出“java.io.NotSerializableException”异常。在序列化主类之前,这些不可序列化的引用应该被标记为“transient”。
- 最后,有时序列化某些字段根本没有意义。时期。例如,在任何类中,如果我们添加了记录器引用,那么序列化该记录器实例有什么用。绝对没用。我们在逻辑上序列化代表实例状态的信息。
Loggers
从不共享实例的状态。它们只是用于编程/调试目的的实用程序。类似的例子可以是一个Thread
类的引用。线程代表进程在任何给定时间点的状态,并且在实例中存储线程状态没有用;仅仅因为它们不构成类实例的状态。
以上四个用例是我们应该将关键字“transient”与引用变量一起使用的情况。
3. 瞬态(transient)与最终(final)
transient和final 一起使用时,在不同情况下的行为不同,而 Java 中的其他关键字通常不会出现这种情况。
为了使这个概念实用,我修改了 Employee 类,如下所示:
private String firstName; private String lastName; //final field 1 public final transient String confidentialInfo = "password"; //final field 2 public final transient Logger logger = Logger.getLogger("demo");
现在,当我再次运行序列化(写入/读取)时,输出如下:
输出:
Jamez Gupta password null
这很奇怪。
我们已将“confidentialInfo”标记为transient;并且该字段仍然被序列化。
对于类似的声明,logger 没有被序列化。
为什么?
原因是,每当任何最终字段/引用被评估为“常量表达式”时,JVM 都会对其进行序列化,而忽略瞬态关键字的存在。
在上面的例子中,值 password
是一个常量表达式,并且 logger demo
的实例是引用。
因此,根据规则,confidentialInfo 被持久化,而 logger 则没有。
你在想,如果我从两个字段中删除 transient
会怎样?
那么,实现“Serializable”引用的字段将持续存在,否则不会。
所以,如果你在上面的代码中删除了transient,String(实现了Serializable)将被持久化;因为 Logger(它没有实现 Serializable)不会被持久化,并且会抛出“java.io.NotSerializableException”。
如果要保留不可序列化字段的状态,请使用 readObject() 和 writeObject() 方法。
writeObject()/readObject() 通常在内部链接到序列化/反序列化机制中,从而自动调用。