文档章节

ArrayList & Vector (transient关键字)

大白来袭
 大白来袭
发布于 2017/06/04 13:00
字数 3195
阅读 17
收藏 0

ArrayList & Vector (transient)

集合是Java中非常重要而且基础的内容,因为任何数据必不可少的就是数据的存储。集合的作用就是以一定的方式组织、存储数据。下面说说ArrayList,只捡干货聊。

ArrayList特点

1、ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存。

2、ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l),函数返回一个线程安全的ArrayList类,底层方法内使用synchronized同步块进行控制;也可以使用concurrent并发包下的CopyOnWriteArrayList类,底层数组直接使用关键字volatile。Vector则在操作数组的方法上加上了关键字synchronized。

3、ArrayList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问,实现了Cloneable接口,能被克隆。

4、每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素, 其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前, 应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。 

是否允许为空 允许
是否允许重复 允许
是否有序 有序
是否线程安全 非线程安全

注意,此实现不是同步的。如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。

ArrayList底层

对于ArrayList而言,它实现List接口、底层使用数组保存所有元素。其操作基本上是对数组的操作。下面我们来分析ArrayList的源代码:

   1) 私有属性:

   ArrayList定义只定义类两个私有属性:

     /** 
      * The array buffer into which the elements of the ArrayList are stored. 
      * The capacity of the ArrayList is the length of this array buffer. 
      */ 
private transient Object[] elementData;  
   
     /** 
      * The size of the ArrayList (the number of elements it contains). 
      * 
      * @serial 
      */ 
private int size;

 很容易理解,elementData存储ArrayList内的元素(应该是堆内存中元素的引用,而不是实际的元素 ),size表示它包含的元素的数量。

有个关键字需要解释:transient。  

Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。

有点抽象,看个例子应该能明白。

public class UserInfo implements Serializable {  
     private static final long serialVersionUID = 996890129747019948L;  
     private String name;  
     private transient String psw;  
   
     public UserInfo(String name, String psw) {  
         this.name = name;  
         this.psw = psw;  
     }  
   
     public String toString() {  
         return "name=" + name + ", psw=" + psw;  
     }  
 }  
   
 public class TestTransient {  
     public static void main(String[] args) {  
         UserInfo userInfo = new UserInfo("张三", "123456");  
         System.out.println(userInfo);  
         try {  
             // 序列化,被设置为transient的属性没有被序列化  
             ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(  
                     "UserInfo.out"));  
             o.writeObject(userInfo);  
             o.close();  
         } catch (Exception e) {  
             // TODO: handle exception  
             e.printStackTrace();  
         }  
         try {  
             // 重新读取内容  
             ObjectInputStream in = new ObjectInputStream(new FileInputStream(  
                     "UserInfo.out"));  
             UserInfo readUserInfo = (UserInfo) in.readObject();  
             //读取后psw的内容为null  
             System.out.println(readUserInfo.toString());  
         } catch (Exception e) {  
             // TODO: handle exception  
             e.printStackTrace();  
         }  
     }  
 }

被标记为transient的属性在对象被序列化的时候不会被保存。 ntData数组被序列化。这是为什么?因为序列化ArrayList的时候,ArrayList里面的elementData未必是满的,比方说elementData有10的大小,但是我只用了其中的3个,那么是否有必要序列化整个elementData呢?显然没有这个必要,因此ArrayList中重写了writeObject方法:

    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 array length
        s.writeInt(elementData.length);
        // 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();
            }
        }
     }

每次序列化的时候调用这个方法,先调用defaultWriteObject()方法序列化ArrayList中的非transient元素,elementData不去序列化它,然后遍历elementData,只序列化那些有的元素,这样:

  • 加快了序列化的速度
  • 减小了序列化之后的文件大小

这种做法也是值得学习、借鉴的一种思路。接着回到ArrayList的分析中......

2) 构造方法: 
   ArrayList提供了三种方式的构造器,可以构造一个默认初始容量为10的空列表、构造一个指定初始容量的空列表以及构造一个包含指定collection的元素的列表,这些元素按照该collection的迭代器返回它们的顺序排列的。

    // ArrayList带容量大小的构造函数。    
    public ArrayList(int initialCapacity) {    
        super();    
        if (initialCapacity < 0)    
            throw new IllegalArgumentException("Illegal Capacity: "+    
                                               initialCapacity);    
        // 新建一个数组    
        this.elementData = new Object[initialCapacity];    
    }    
   
    // ArrayList无参构造函数。默认容量是10。    
    public ArrayList() {    
        this(10);    
    }    
   
    // 创建一个包含collection的ArrayList    
    public ArrayList(Collection<? extends E> c) {    
        elementData = c.toArray();    
        size = elementData.length;    
        if (elementData.getClass() != Object[].class)    
            elementData = Arrays.copyOf(elementData, size, Object[].class);    
    }

3) 元素存储与扩容:

ArrayList 提供了set(int index, E element)、add(E e)、add(int index, E element)、 addAll(Collection<? extends E> c)、 addAll(int index, Collection<? extends E> c)这些添加元素的方法。

20 // 用指定的元素替代此列表中指定位置上的元素,并返回以前位于该位置上的元素。  
21 public E set(int index, E element) {  
22    RangeCheck(index);  
23 
24    E oldValue = (E) elementData[index];  
25    elementData[index] = element;  
26    return oldValue;  
27 }    
28 // 将指定的元素添加到此列表的尾部。  
29 public boolean add(E e) {  
30    ensureCapacity(size + 1);   
31    elementData[size++] = e;  
32    return true;  
33 }    
34 // 将指定的元素插入此列表中的指定位置。  
35 // 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加1)。  
36 public void add(int index, E element) {  
37    if (index > size || index < 0)  
38       throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);  
39    // 如果数组长度不足,将进行扩容。  
40    ensureCapacity(size+1); // Increments modCount!!  
41    // 将 elementData中从Index位置开始、长度为size-index的元素,  
42    // 拷贝到从下标为index+1位置开始的新的elementData数组中。  
43    // 即将当前位于该位置的元素以及所有后续元素右移一个位置。  
44    System.arraycopy(elementData, index, elementData, index + 1, size - index);  
45    elementData[index] = element;  
46    size++;  
47 }    
48 // 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部。  
49 public boolean addAll(Collection<? extends E> c) {  
50    Object[] a = c.toArray();  
51    int numNew = a.length;  
52    ensureCapacity(size + numNew); // Increments modCount  
53    System.arraycopy(a, 0, elementData, size, numNew);  
54    size += numNew;  
55    return numNew != 0;  
56 }    
57 // 从指定的位置开始,将指定collection中的所有元素插入到此列表中。  
58 public boolean addAll(int index, Collection<? extends E> c) {  
59    if (index > size || index < 0)  
60      throw new IndexOutOfBoundsException(  
61        "Index: " + index + ", Size: " + size);  
62 
63    Object[] a = c.toArray();  
64    int numNew = a.length;  
65    ensureCapacity(size + numNew); // Increments modCount  
66 
67    int numMoved = size - index;  
68    if (numMoved > 0)  
69      System.arraycopy(elementData, index, elementData, index + numNew, numMoved);  
70 
71    System.arraycopy(a, 0, elementData, index, numNew);  
72    size += numNew;  
73    return numNew != 0;  
   }  

底层数组的大小不够了怎么办? 答案就是扩容,这也就是为什么一直说ArrayList的底层是基于动态数组实现的原因,动态数组的意思就是指底层的数组大小并不是固定的,而是根据添加的元素大小进行一个判断,不够的话就动态扩容,扩容的代码就在ensureCapacity里面:

 public void ensureCapacity(int minCapacity) {
    modCount++;
    int oldCapacity = elementData.length;
    if (minCapacity > oldCapacity) {
        Object oldData[] = elementData;
        int newCapacity = (oldCapacity * 3)/2 + 1;
            if (newCapacity < minCapacity)
               newCapacity = minCapacity;
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
 }

看到扩容的时候把元素组大小先乘以3,再除以2,最后加1。可能有些人要问为什么?我们可以想:

  • 如果一次性扩容扩得太大,必然造成内存空间的浪费
  • 如果一次性扩容扩得不够,那么下一次扩容的操作必然比较快地会到来,这会降低程序运行效率,要知道扩容还是比价耗费性能的一个操作

所以扩容扩多少,是JDK开发人员在时间、空间上做的一个权衡,提供出来的一个比较合理的数值。最后调用到的是Arrays的copyOf方法,将元素组里面的内容复制到新的数组里面去:

    public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
           T[] copy = ((Object)newType == (Object)Object[].class)
               ? (T[]) new Object[newLength]
               : (T[]) Array.newInstance(newType.getComponentType(), newLength);
           System.arraycopy(original, 0, copy, 0,
                            Math.min(original.length, newLength));
           return copy;
    }

数组复制拷贝尽量使用System.arrayCopy或Arrays.copyof()方法,效率更高。

ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过trimToSize方法来实现。代码如下:

public void trimToSize() {  
    modCount++;  
    int oldCapacity = elementData.length;  
    if (size < oldCapacity) {  
        elementData = Arrays.copyOf(elementData, size);  
    }  
}

由于elementData的长度会被拓展,size标记的是其中包含的元素的个数。所以会出现size很小但elementData.length很大的情况,将出现空间的浪费。trimToSize将返回一个新的数组给elementData,元素内容保持不变,length和size相同,节省空间。

4) 元素读取:

// 返回此列表中指定位置上的元素。  
public E get(int index) {  
    RangeCheck(index);  
    return (E) elementData[index];  
}

5) 元素删除:

ArrayList提供了根据下标或者指定对象两种方式的删除功能。 对于ArrayList来说,这两种删除的方法差不多,都是调用的下面一段代码:

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                 numMoved);
    elementData[--size] = null; // Let gc do its work

把指定元素后面位置的所有元素,利用System.arraycopy方法整体向前移动一个位置,最后一个位置的元素指定为null,这样让gc可以去回收它。

ArrayList的优缺点

  • 随机访问快。ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快
  • 顺序添加快。ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已
  • 删除元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能
  • 插入元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能

ArrayList和Vector区别

  • Vector属于线程安全级别的,但是大多数情况下不使用Vector,因为线程安全需要更大的系统开销。ArrayList需要使用Collections.synchronizedList方法变成一个线程安全的List。
        List<String> synchronizedList = Collections.synchronizedList(list);
        synchronizedList.add("aaa");
        synchronizedList.add("bbb");
        for (int i = 0; i < synchronizedList.size(); i++)
        {
            System.out.println(synchronizedList.get(i));
        }

    Vector是ArrayList的线程安全版本,其实现90%和ArrayList都完全一样

  • 增长因子不同。ArrayList在内存不够时默认是扩展50% + 1个,Vector是默认扩展1倍。
  • Vector提供indexOf(obj, start)接口,ArrayList没有。

ArrayList转静态数组toArray

有两个转化为静态数组的toArray方法。

第一个,调用Arrays.copyOf将返回一个数组,数组内容是size个elementData的元素,即拷贝elementData从0至size-1位置的元素到新数组并返回。

public Object[] toArray() {  
     return Arrays.copyOf(elementData, size);  
} 

第二个,如果传入数组的长度小于size,返回一个新的数组,大小为size,类型与传入数组相同。所传入数组长度与size相等,则将 elementData复制到传入数组中并返回传入的数组。若传入数组长度大于size,除了复制elementData外,还将把返回数组的第size个元素置为空。

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

 

Fail-Fast机制: 
ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。具体介绍请参考这篇文章HashMap中的Fail-Fast机制。

本文转载自:http://www.cnblogs.com/ITtangtang/p/3948555.html

大白来袭
粉丝 4
博文 41
码字总数 13667
作品 0
海淀
程序员
私信 提问
Java关键字 - transient

今天看和的代码时,发现一个细节不一样,它们实际用来存储元素的前面的修饰关键字不一样: 以前听到最多的就是说,是线程安全的,是非线程安全的。 在好奇心的驱使下,翻书找到了一段解释的话...

Jacktanger
05/19
32
0
jdk1.6的集合源码阅读之ArrayList

简述 ArrayList其实就是动态数组,是Array的复杂版本,动态扩容和缩容,灵活的设置数组的大小,等等。 其定义如下 而AbstractCollection实现了Collection的部分方法和AbstractList继承了该A...

双月通天
2016/08/23
36
0
Java 集合之 Collection

集合就是一组数的集合,就像是一个容器,但是我们应该清楚的是集合中存放的都是对象的引用,而不是真正的实体。而我们常说的集合中的对象其实指的就是对象的引用。 我们可以把集合理解为一个...

YJK923
2018/08/18
0
0
ArrayList中elementData为什么被transient修饰?

Java的ArrayList中,定义了一个数组elementData用来装载对象的,具体定义如下: transient用来表示一个域不是该对象序行化的一部分,当一个对象被序行化的时候,transient修饰的变量的值是不包...

群星纪元
04/06
14
0
java集合vector和ArrayList

ArrayList private transient Object[] elementData; Vector protected Object[] elementData; 两者都是数组实现的,为啥数组的声明不一样?为什么呢?...

敲代码猥琐男
2012/10/29
577
4

没有更多内容

加载失败,请刷新页面

加载更多

只需一步,在Spring Boot中统一Restful API返回值格式与统一处理异常

统一返回值 在前后端分离大行其道的今天,有一个统一的返回值格式不仅能使我们的接口看起来更漂亮,而且还可以使前端可以统一处理很多东西,避免很多问题的产生。 比较通用的返回值格式如下:...

晓月寒丶
昨天
59
0
区块链应用到供应链上的好处和实际案例

区块链可以解决供应链中的很多问题,例如记录以及追踪产品。那么使用区块链应用到各产品供应链上到底有什么好处?猎头悬赏平台解优人才网小编给大家做个简单的分享: 使用区块链的最突出的优...

猎头悬赏平台
昨天
28
0
全世界到底有多少软件开发人员?

埃文斯数据公司(Evans Data Corporation) 2019 最新的统计数据(原文)显示,2018 年全球共有 2300 万软件开发人员,预计到 2019 年底这个数字将达到 2640万,到 2023 年达到 2770万。 而来自...

红薯
昨天
65
0
Go 语言基础—— 通道(channel)

通过通信来共享内存(Java是通过共享内存来通信的) 定义 func service() string {time.Sleep(time.Millisecond * 50)return "Done"}func AsyncService() chan string {retCh := mak......

刘一草
昨天
58
0
Apache Flink 零基础入门(一):基础概念解析

Apache Flink 的定义、架构及原理 Apache Flink 是一个分布式大数据处理引擎,可对有限数据流和无限数据流进行有状态或无状态的计算,能够部署在各种集群环境,对各种规模大小的数据进行快速...

Vincent-Duan
昨天
60
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部