• Java集合框架里面的的transient关键字


    转自:https://blog.csdn.net/u010454030/article/details/79416996

    在分析HashMap和ArrayList的源码时,我们会发现里面存储数据的数组都是用transient关键字修饰的,如下:

    HashMap里面的:

    transient Node<K,V>[] table;

    ArrayList里面的:

    transient Object[] elementData

    既然用transient修饰,那就说明这个数组是不会被序列化的,那么同时我们发现了这两个集合都自定义了独自的序列化方式:

    先看HashMap自定义的序列化的代码:

    //1
        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);
        }
    //2    
     public 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);
                    }
                }
            }
        }
    

     再看HashMap自定义的反序列化的代码:

    //1
       private void readObject(java.io.ObjectInputStream s)
            throws IOException, ClassNotFoundException {
            // Read in the threshold (ignored), loadfactor, and any hidden stuff
            s.defaultReadObject();
            reinitialize();
            if (loadFactor <= 0 || Float.isNaN(loadFactor))
                throw new InvalidObjectException("Illegal load factor: " +
                                                 loadFactor);
            s.readInt();                // Read and ignore number of buckets
            int mappings = s.readInt(); // Read number of mappings (size)
            if (mappings < 0)
                throw new InvalidObjectException("Illegal mappings count: " +
                                                 mappings);
            else if (mappings > 0) { // (if zero, use defaults)
                // Size the table using given load factor only if within
                // range of 0.25...4.0
                float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
                float fc = (float)mappings / lf + 1.0f;
                int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                           DEFAULT_INITIAL_CAPACITY :
                           (fc >= MAXIMUM_CAPACITY) ?
                           MAXIMUM_CAPACITY :
                           tableSizeFor((int)fc));
                float ft = (float)cap * lf;
                threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                             (int)ft : Integer.MAX_VALUE);
                @SuppressWarnings({"rawtypes","unchecked"})
                    Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
                table = tab;
     
                // Read the keys and values, and put the mappings in the HashMap
                for (int i = 0; i < mappings; i++) {
                    @SuppressWarnings("unchecked")
                        K key = (K) s.readObject();
                    @SuppressWarnings("unchecked")
                        V value = (V) s.readObject();
                    putVal(hash(key), key, value, false, false);
                }
            }
        }
    

    这里面我们看到HashMap的源码里面自定义了序列化和反序列化的方法,序列化方法主要是把当前HashMap的buckets数量,size和里面的k,v对一一给写到了对象输出流里面,然后在反序列化的时候,再从流里面一一的解析出来,然后又重新恢复出了HashMap的整个数据结构。

    接着我们看ArrayList里面自定义的序列化的实现:

    private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException{
            // Write out element count, and any hidden stuff
            int expectedModCount = modCount;
            s.defaultWriteObject();
     
            // Write out size as capacity for behavioural compatibility with clone()
            s.writeInt(size);
     
            // Write out all elements in the proper order.
            for (int i=0; i<size; i++) {
                s.writeObject(elementData[i]);
            }
     
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
        }
    
    private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            elementData = EMPTY_ELEMENTDATA;
     
            // Read in size, and any hidden stuff
            s.defaultReadObject();
     
            // Read in capacity
            s.readInt(); // ignored
     
            if (size > 0) {
                // be like clone(), allocate array based upon size not capacity
                ensureCapacityInternal(size);
     
                Object[] a = elementData;
                // Read in all elements in the proper order.
                for (int i=0; i<size; i++) {
                    a[i] = s.readObject();
                }
            }
        }
    

    ArrayList里面也是把其size和里面不为null的数据给写到流里面,然后在反序列化的时候重新使用数据把数据结构恢复出来。

    那么问题来了,为什么他们明明都实现了Serializable接口,已经具备了自动序列化的功能,为啥还要重新实现序列化和反序列化的方法呢?

    (1)HashMap中实现序列化和反序列化的原因:

    在HashMap要定义自己的序列化和反序列化实现,有一个重要的因素是因为hashCode方法是用native修饰符修饰的,也就是用它跟jvm的运行环境有关,Object类中的hashCode源码如下:

    public native int hashCode();

    也就是说不同的jvm虚拟机对于同一个key产生的hashCode可能是不一样的,所以数据的内存分布可能不相等了,举个例子,现在有两个jvm虚拟机分别是A和B,他们对同一个字符串x产生的hashCode不一样:

    所以导致:

    在A的jvm中它的通过hashCode计算它在table数组中的位置是3

    在B的jvm中它的通过hashCode计算它在table数组中的位置是5

    这个时候如果我们在A的jvm中按照默认的序列化方式,那么位置属性3就会被写入到字节流里面,然后通过B的jvm来反序列化,同样会把这条数据放在table数组中3的位置,然后我们在B的jvm中get数据,由于它对key的hashCode和A不一样,所以它会从5的位置取值,这样以来就会读取不到数据。

    如何解决这个问题,首先导致上面问题的主要原因在于因为hashCode的不一样从而可能导致内存分布不一样,所以只要在序列化的时候把跟hashCode有关的因素比如上面的位置属性给排除掉,就可以解决这个问题。

    最简单的办法就是在A的jvm把数据给序列化进字节流,而不是一刀切把数组给序列化,之后在B的jvm中反序列化时根据数据重新生成table的内存分布,这样就来就完美解决了这个问题。

    (2)ArrayList中实现序列化和反序列化的原因:

    在ArrayList中,我们知道数组的长度会随着数据的插入而不断的动态扩容,每次扩容都需要增加原数组一半的长度,这而一半的长度极端情况下都是null值,所以在序列化的时候可以把这部分数据排除出去,从而节省时间和空间:

    1. for (int i=0; i<size; i++) {
    2. s.writeObject(elementData[i]);
    3. }

    注意ArrayList在序列化的时候用的size来遍历原数组中的元素,而并不是elementData.length也就是数组的长度,而size的大小就是数组里面非null元素的个数,所以这里才采用了自定义序列化的方式。

    到这里细心的朋友可能有个疑问:HashMap中也就是采用的动态数组扩容为什么它在序列化的时候用的是table.length而不是size呢,这其实很容易回答在HashMap中table.length必须是2的n次方,而且这个值会决定了好几个参数的值,所以如果也把null值给去掉,那么必须要重新的估算table.length的值,有可能造成所有数据的重新分布,所以最好的办法就是保持原样。

    注意上面的null值,指的是table里面Node元素是null,而并不是HashMap里面的key等于null,而key是Node里面的一个字段。

    总结:

    本文主要介绍了在HashMap和ArrayList中其核心的数据结构字段为什么用transient修饰并分别介绍了其原因,所以使用序列化时,应该谨记effective java中的一句话:当一个对象的物理表示方法与它的逻辑数据内容有实质性差别时,使用默认序列化形式有N种缺陷,所以应该尽可能的根据实际情况重写序列化方法。

  • 相关阅读:
    WCF中神秘的“8731"端口和“Design_Time_Addresses”
    wcf分布式构架集群案例解决方案
    asp.net odjectdatasource的用法
    ClientBase<T>中对ChannelFactory<T>的缓存机制
    asp.net datatable中行的复制
    c#连接各种数据库的方法
    wcf系列学习5天速成——事务的使用
    Atitit 软件设计中的各种图纸 uml 之道 1. 常见设计成果与图纸 2 1.1. 总图 2 1.2. ui原型图与html 2 1.3. 业务逻辑 伪代码 各种uml图 2 1.4. 总体
    Atitit 综合原则 软件与项目开发中的理念信念 目录 1.1. 建议组合使用扬长避短 1 1.2. 常见数据库 mysql oracle mssql mongodb postgre sqlit
    Atitit it理论不可能三角“:“安全方便廉价 目录 1. 不可能三角 1 2. 三元悖论 1 2.1. 培恩洛兹三角形 2 2.2. 彭罗斯三角(Penrose triangle) 2 2.
  • 原文地址:https://www.cnblogs.com/Genesisx/p/9487118.html
Copyright © 2020-2023  润新知