transient HashMap使用目的分析

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://linuxstyle.blog.csdn.net/article/details/88954824

看HashSet源码有这么一句:

private transient HashMap<E,Object> map;

再看HashSet的Add方法:

实际上HashSet是复用HashMap了。

而我们去看看HashMap也会发现一样使用了transient

而不管是HashSet还是HashMapdou都要求是Serializable的:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable

而transient实际上是不序列化的,这就好像有点“矛盾”,再回答这个问题之前先看看transient。

测试代码如下:

import java.io.Serializable;

public class EmployeeTransient implements Serializable {

    public String getConfidentialInfo() {
        return confidentialInfo;
    }

    public void setConfidentialInfo(String confidentialInfo) {
        this.confidentialInfo = confidentialInfo;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    private transient String confidentialInfo;
    private String firstName;
    private String lastName;
}

 现在让我们先序列化再反序列化回java对象,并验证是否保存了“ confidentialInfo ”?

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class TransientTest {
    public static void main(String args[]) {

        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("empInfo.ser"));
            EmployeeTransient emp = new EmployeeTransient();
            emp.setFirstName("Lokesh");
            emp.setLastName("Gupta");
            emp.setConfidentialInfo("password");
            //Serialize the object
            oos.writeObject(emp);
            oos.close();
        } catch (Exception e) {
            System.out.println(e);
        }

        try {
            ObjectInputStream ooi = new ObjectInputStream(new FileInputStream("empInfo.ser"));
            //Read the object back
            EmployeeTransient readEmpInfo = (EmployeeTransient) 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);
        }
    }
}

很明显,“ confidentialInfo ”在序列化时没有保存到持久状态,这正是我们在java中使用“ transient ”关键字的原因。 

什么时候应该在java中使用transient关键字?

现在我们对“ transient  ”关键字非常了解。让我们通过确定您需要使用transient关键字的情况来扩展理解。

  1. 第一个也是非常合乎逻辑的情况是,您可能拥有类实例中的其他字段派生/计算的字段。应该每次都以编程方式计算它们,而不是通过序列化来保持状态。一个例子可以是基于时间戳的价值; 例如人的年龄或时间戳和当前时间戳之间的持续时间。在这两种情况下,您将根据当前系统时间而不是序列化实例来计算变量的值。
  2. 第二个逻辑示例可以是任何不应以任何形式泄漏到JVM外部的安全信息(在数据库或字节流中)。
  3. 另一个例子可能是在JDK或应用程序代码中未标记为“Serializable”的字段。未实现Serializable接口并在任何可序列化类中引用的类无法序列化; 并将抛出“java.io.NotSerializableException”异常。在序列化主类之前,应将这些不可序列化的引用标记为“transient ”。
  4. 最后,有时候序列化某些字段根本没有意义。期。例如,在任何类中,如果添加了记录器引用,那么序列化该记录器实例的用途是什么。绝对没用。您可以逻辑地序列化表示实例状态的信息。Loggers永远不要分享实例的状态。它们只是用于编程/调试目的的实用程序。类似的例子可以是一个Thread类的引用。线程表示任何给定时间点的进程状态,并且没有用于将线程状态存储到您的实例中; 仅仅因为它们不构成你班级实例的状态。

 

//final field 1
public final transient String confidentialInfo = "password";

 现在当我再次运行序列化(写/读)时,会输出出password。

原因是,只要任何最终字段/引用被评估为“ 常量表达式 ”,它就会被JVM序列化,忽略transient关键字的存在。

如果要保持非可序列化字段的状态,请使用readObject()和writeObject()方法。writeObject()/ readObject()通常在内部链接到序列化/反序列化机制,因此自动调用。 阅读更多:java中的SerialVersionUID及相关

 

HashMap如何使用transient关键字?

HashMap用于存储键值对,我们都知道。并且我们还知道内部密钥的位置HashMap是基于例如密钥获得的哈希码来计算的。现在当我们序列化一个HashMap意味着内部的所有键HashMap和键的各个值也将被序列化。序列化后,当我们反序列化HashMap实例时,所有键实例也将被反序列化。我们知道在这个序列化/反序列化过程中,可能会丢失信息(用于计算哈希码),最重要的是它本身就是一个新的实例。

在java中,任何两个实例(即使是同一个类)都不能具有相同的哈希码。这是一个很大的问题,因为根据新的哈希码应该放置键的位置不在正确的位置。检索键的值时,您将在此新HashMap中引用错误的索引。

阅读更多:在java中使用hashCode和equals方法

因此,当序列化哈希映射时,这意味着哈希索引,因此表的顺序不再有效,不应保留。这是问题陈述。

HashMap类使用writeObject()readObject()方法,如下所示

/**
     * Save the state of the <tt>HashMap</tt> instance to a stream (i.e.,
     * serialize it).
     *
     * @serialData The <i>capacity</i> of the HashMap (the length of the
     *             bucket array) is emitted (int), followed by the
     *             <i>size</i> (an int, the number of key-value
     *             mappings), followed by the key (Object) and value (Object)
     *             for each key-value mapping.  The key-value mappings are
     *             emitted in no particular order.
     */
    private void writeObject(java.io.ObjectOutputStream s)
        throws IOException {
        int buckets = capacity();
        // Write out the threshold, loadfactor, and any hidden stuff
        s.defaultWriteObject();
        s.writeInt(buckets);
        s.writeInt(size);
        internalWriteEntries(s);
    }
// Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }

    // Called only from writeObject, to ensure compatible ordering.
    void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
        Node<K,V>[] tab;
        if (size > 0 && (tab = table) != null) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    s.writeObject(e.key);
                    s.writeObject(e.value);
                }
            }
        }
    }

下面是HashSet中的实现,思路都是一样的。都是把key,value都写到序列化里去,

/**
     * Save the state of this <tt>HashSet</tt> instance to a stream (that is,
     * serialize it).
     *
     * @serialData The capacity of the backing <tt>HashMap</tt> instance
     *             (int), and its load factor (float) are emitted, followed by
     *             the size of the set (the number of elements it contains)
     *             (int), followed by all of its elements (each an Object) in
     *             no particular order.
     */
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        // Write out any hidden serialization magic
        s.defaultWriteObject();

        // Write out HashMap capacity and load factor
        s.writeInt(map.capacity());
        s.writeFloat(map.loadFactor());

        // Write out size
        s.writeInt(map.size());

        // Write out all elements in the proper order.
        for (E e : map.keySet())
            s.writeObject(e);
    }

然后反序列化的时候就是重新hash一次的过程

下面是HashMap的readObject:

下面是HashSet的readObject:

使用上面的代码,HashMap仍然可以像通常那样处理非transient字段,但是它们一个接一个地在字节数组的末尾写入存储的键值对。在反序列化时,它会通过默认的反序列化过程处理非transient变量,然后逐个读取键值对。对于每个键,哈希和索引再次计算并插入到表中的正确位置,以便可以再次检索它而不会出现任何错误。 

参考:Java transient关键字示例 

扩展阅读:

为什么HashMap的哈希表标记为transient,尽管该类是可序列化的

图解集合4:HashMap 

展开阅读全文

HashMap分析 ----JDK1.8

04-26

rnrn一、Map的结构rnrn Map是一种用来保存key-value的数据结构。Map接口提供了存取key和value的方法,并且提供了内部存储的数据结构接口Map/Entry.rnrn rnrninterface Entry rn /**rn * Returns the key corresponding to this entry.rn *rn * @return the key corresponding to this entryrn * @throws IllegalStateException implementations may, but are notrn * required to, throw this exception if the entry has beenrn * removed from the backing map.rn */rn K getKey();rnrnrnrnrn接口提供了各种方法,需要自己去实现。所有的Map都是基于如此。rnrnrnrnrn二、HashMap基本概念rnrn 在了解HashMap之前,可以去了解下Hash算法的概念,对理解HashMap很有帮助。rnrn 如果要设计保存key-value的结构,我们可以在每次put的时候,循环整个数组计算key是否重复,然后进行操作。get的时候也可以循环取出数据。这当然是可行的。但是如果数量很大的时候,自然会很慢,不是一种好的方法。rnrnrn HashMap提供了基于hash算法的快速存取的Map结构。实现了Map接口,继承于AbstractMap。HashMap通过一定的算法(通过Hashcode计算数组index,后面介绍),将key值计算为数组的索引位置,这样可以实现快速的存取。当然Hash算法可能会导致冲突。Hash值可能一样,计算出的索引位置也可能一样。这种情况我们称之为Hash冲突。rnrn这种情况下,HashMap通过链表来解决Hash冲突。当复数key出现在相同的index位置后面,将会形成在该位置形成一个链表结构来存储。具体后续讲解。rnrn三、数据结构rnrn HashMap中通过数组和指针来保存数据。当然几乎所有JAVA的类都可以如此使用。HashMap实际是一个链表散列。如下图(网上下载的图)rnrnrnrnrnrnrnrnrnrn HashMap中几个重要的个概念和结构。rnrn //对象存储的tablern transient Node[] table;rn //最大负载数量rn int threshold;rn //负载因子rn final float loadFactor;rnrn //HashMap的存储对象数据结构。rn static class Node implements Map.Entry rn final int hash;rn final K key;rn V value;rn Node next;rnrn Node(int hash, K key, V value, Node next) rn this.hash = hash;rn this.key = key;rn this.value = value;rn this.next = next;rn rn }rnrnrnrnrn loadFactor负载因子,threshold最大负载量。这是什么含义? 这两个参数是影响HashMap性能的重要参数,其中的最大负载量表示HashMap达到多满的程度进行扩容。而负载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。rn 这里我们有一个公式:threshold = table.length*loadFactor。最大负载量是根据table的容量和负载因子计算得出。rnrn rnrn来看看初始化方法。rnrn public HashMap(int initialCapacity, float loadFactor) rn if (initialCapacity < 0)rn throw new IllegalArgumentException("Illegal initial capacity: " +rn initialCapacity);rn if (initialCapacity > MAXIMUM_CAPACITY)rn initialCapacity = MAXIMUM_CAPACITY;rn if (loadFactor <= 0 || Float.isNaN(loadFactor))rn throw new IllegalArgumentException("Illegal load factor: " +rn loadFactor);rn this.loadFactor = loadFactor;rn this.threshold = tableSizeFor(initialCapacity);rn rnrnrnrnrn我们看到构造方法中并没有初始化数组的容量也没有初始化数组。threshold这里是最大的负载量,但是在put()的时候会初始化数组对象,并且这个值就是数组初始化的容量,而threshold的值会重新计算(是不是很绕,别被绕进去了,构造方法中计算出的是容量,只不过用最大负载字段来存起来,应为没有数组容量字段(该字段是数组的长度,是数组中的属性))。rnrn三个属性(负载因子,数组容量,和最大负载之间的概概念不要弄混了)。rnrn tableSizeFor(initialCapacity)rnrn /**rn * Returns a power of two size for the given target capacity.rn */rn static final int tableSizeFor(int cap) rn int n = cap - 1;rn n |= n >>> 1;rn n |= n >>> 2;rn n |= n >>> 4;rn n |= n >>> 8;rn n |= n >>> 16;rn return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;rn rnrnrnrnrn???先说结论,得到大于传入值cap的最小的那个2^n(2的N次方)的值。这个值作为数组容量。这个值对后面的计算key在数组中的位置有很大关系。而容量设置为2^n次方会提升计算速度。rnrn详细博客:http://blog.csdn.net/ls1firesoar/article/details/70751751rnrn 论坛

没有更多推荐了,返回首页