您的位置:首页 > 其它

ArrayList中的实现原理以及实现线程安全

2017-11-01 15:35 459 查看
一,ArrayList概述:

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

ArrayList不是线程安全的,只能在单线程环境下,多线程环境下可以考虑使用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用并发并发包下的CopyOnWriteArrayList类。

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

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

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

二,ArrayList的实现:

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

1)私有属性:

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

/ * *
*存储ArrayList元素的数组缓冲区。
* ArrayList的容量是此数组缓冲区的长度。
* /
private transient Object [] elementData;

/ * *
* ArrayList的大小(包含的元素数)。
*
* @serial
* /
private  int size;


很容易理解,elementData中存储的ArrayList内的元素,大小表示它包含的元素的数量。

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

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

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

public  class UserInfo implements Serializable {
private  static final long serialVersionUID = 996890129747019948L ;
私有字符串名称;
私人瞬态字符串psw;

public UserInfo(String name,String psw){
this .name = name;
这个 .psw = psw
}

public String toString(){
return  “ name = ” + name + “ ,psw = ” + psw;
}
}

public  class TestTransient {
public  static  void main(String [] args){
UserInfo userInfo = new UserInfo(“ 张三”,“ 123456 ” );
系统。out .println(userInfo);
尝试{
// 序列化,被设置为transient的属性没有被序列化
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(
“ UserInfo.out ” ));
o.writeObject(USERINFO);
o.close();
} catch (Exception e){
// TODO:handle exception
e.printStackTrace();
}
尝试{
// 重新读取内容
ObjectInputStream in = new ObjectInputStream(new FileInputStream(
“ UserInfo.out ” ));
UserInfo readUserInfo =(UserInfo)in .readObject();
// 读取后psw的内容为null
系统。out .println(readUserInfo.toString());
} catch (Exception e){
// TODO:handle exception
e.printStackTrace();
}
}
}


被标记为短暂的属性在对象被序列化的时候不会被保存。

接着回到ArrayList中的分析中......

2)构造方法:

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

// ArrayList的带容量大小的构造函数。
公共的ArrayList(INT 参数:initialCapacity){
超();
if(initialCapacity < 0 )
throw  new IllegalArgumentException(“ Illegal Capacity:” +
参数:initialCapacity);
// 新建一个数组
这个 .elementData = new Object [initialCapacity];
}

// ArrayList无参构造函数。默认容量是10.
public ArrayList(){
this(10 );
}

// 创建一个包含集合的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     返回oldValue;
27 }
28 // 。将指定的元素添加到此列表的  尾部
29 公共布尔添加(E E){
30     的ensureCapacity(大小+ 1 );
31     elementData [size ++] = e;
32    返回 真;
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);  // Increment modCount!
41     // 将elementData中从索引位置开始,长度为size-index的元素,
42     // 拷贝到从下标为索引+ 1位置开始的新的elementData
数组中   。43     // 即将当前位于该位置的元素以及所有后续元素右移一个位置
。44     System.arraycopy(elementData,index,elementData,index + 1,size- index);
45     elementData [index] = element;
46     尺寸++ ;
47 }
48 // 按照指定集合的迭代器所返回的元素顺序,将该集合中的所有元素添加到此列表的尾部。
49 公共布尔的addAll(集合<?延伸E> C){
50    Object [] a = c.toArray();
51     int numNew = a.length ;
52     ensureCapacity(size + numNew);  // Increment modCount
53     System.arraycopy(a,0 ,elementData,size,numNew);
54     size + = numNew;
55     return numNew!= 0 ;
56 }
57 // 从指定的位置开始,将指定集合中的所有元素插入到此列表中。
58 公共布尔的addAll(INT索引,集合<?延伸E> C){
59     如果(指数>尺寸||索引<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);  // Increment 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是基于数组实现的,属性中也看到了数组,具体是怎么实现的呢?比如就这个添加元素的方法,如果数组大,则在将某个位置的值设置为指定元素即可,如果数组容量不够了呢?

看到add(E e)中先调用了ensureCapacity(size + 1)方法,之后将元素的索引赋予elementData [size],而后size size增加例如初次添加时间,size为0,add将elementData [0 ]赋值为E,然后大小设置为1(类似执行以下两条语句elementData中[0]
= E;大小= 1)。?将元素的索引赋给elementData中[大小]不是会出现数组越界的情况吗这里关键就在的ensureCapacity(大小+ 1)中了。

4)元素读取:

// 返回此列表中指定位置上的元素
public E get(int index){
RangeCheck(索引);

return (E)elementData [index];
}


5)元素删除:

ArrayList中提供了根据下标或者指定对象两种方式的删除功能如下:

romove(int index):

1  // 移除此列表中指定位置上的元素
2   public E remove(int index){
3      RangeCheck(index);
4
5      modCount ++ ;
6      E oldValue = (E)elementData [index];
7
8      int numMoved = size  -  index  - 1 ;
9      if(numMoved> 0 )
10          System.arraycopy(elementData,index + 1 ,elementData,index,numMoved);
11      elementData [ -  size] = null ; // 让gc做它的工作
12
13      return oldValue;
14   }


首先是检查范围,修改modCount的,保留将要被移除的元素,将移除位置之后的元素向前挪动一个位置,将列表末尾元素置空(NULL),返回被移除的元素。

remove(Object o)

1   // 移除此列表中首次出现的指定元素(如果存在)。这是应为ArrayList中允许存放重复的元素
2   public boolean remove(Object o){
3      // 由于ArrayList中允许存放null,因此下面通过两种情况来分别处理。
4      如果(O == 空){
5          为(INT索引= 0 ;索引<大小;索引++ )
6              如果(elementData中[指数] == 空){
7                  // 类似除去( int index),移除列表中指定位置上的元素
8                  fastRemove(index);
9                  返回 真;
10              }
11      } else {
12          for(int index = 0 ; index <size; index ++ )
13              if (o.equals(elementData [index])){
14                  fastRemove(index);
15                  回归 真实;
16              }
17          }
18          返回 假;
19      }
20 }


首先通过代码可以看到,当移除成功后返回true,否则返回false.remove(Object o)中通过遍历元素寻找是否存在传入对象,一旦找到就调用fastRemove移除对象。为什么找到了元素就知道了索引,不通过删除(索引)来移除元素呢?因为fastRemove跳过了判断边界的处理,因为找到元素就相当于确定了指数不会超过边界,而且fastRemove并不返回被移除的元素。下面是fastRemove的代码,基本和除去(指数)一致。

1  private  void fastRemove(int index){
2           modCount ++ ;
3           int numMoved = size  -  index  - 1 ;
4           if(numMoved> 0 )
5               System.arraycopy(elementData,index + 1 ,elementData,index,
6                                numMoved);
7           elementData [ -  size] = null ; // 让gc做它的工作
8   }


removeRange(int fromIndex,int toIndex)

1  protected  void removeRange(int fromIndex,int toIndex){
2       modCount ++ ;
3       int numMoved = size  - toIndex;
4           System.arraycopy(elementData,toIndex,elementData,fromIndex,
5                            numMoved);
6
7       // 让gc做它的工作
8       int newSize = size  - (toIndex- fromIndex);
9       while(size!= newSize)
10           elementData [ -  size] = null ;
11 }


执行过程是将elementData中从toIndex位置开始的元素向前移动到的fromIndex,然后将toIndex位置之后的元素全部置空顺便修改大小。

这个方法是受保护的,及受保护的方法,为什么这个方法被定义为保护呢?

这是一个解释,但是可能不容易看明白.http://stackoverflow.com/questions/2289183/why-is-javas-abstractlists-removerange-method-protected

先看下面这个例子

的ArrayList <Integer>的整数= 新的ArrayList <Integer>的(Arrays.asList(0,1,2 ,
3,4,5,6 ));
// 的fromIndex低点(包括)subList的
// 子列表的toIndex高点(不包括)
ints.subList(2,4 ).clear();
系统。out .println(ints);


输出结果是[0,1,4,5,6],结果是不是像调用了removeRange(int fromIndex,int toIndex)!哈哈哈,就是这样的。但是为什么效果相同呢?是不是调用了removeRange fromIndex,int toIndex)呢?

6)调整数组容量ensureCapacity:

从上面介绍的向ArrayList的中存储元素的代码中,我们看到,每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过一个公开的方法ensureCapacity(int minCapacity)来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。

public  void ensureCapacity(int minCapacity){
modCount ++ ;
int oldCapacity = elementData.length;
if(minCapacity> oldCapacity){
Object oldData [] = elementData;
int newCapacity =(oldCapacity * 3)/ 2 + 1 ;  // 增加50%+ 1
if(newCapacity < minCapacity)
newCapacity = minCapacity;
// minCapacity通常接近大小,所以这是一个win:
elementData = Arrays.copyOf(elementData,newCapacity);
}
}


从上述代码中可以看出,数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造的ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用的ensureCapacity方法来手动增加的ArrayList实例的容量。

对象oldData [] = elementData; //为什么要用到oldData []

乍一看来后面并没有用到关于oldData,这句话显得多这一举!但是这是一个牵涉到内存管理的类,所以要了解内部的问题。而且为什么这一句还在if的内部,这跟elementData
= Arrays.copyOf(elementData,newCapacity); 这句是有关系的,下面这句Arrays.copyOf的实现时新创建了newCapacity大小的内存,然后把老的elementData中放入。好像也没有用到OLDDATA,有什么问题呢。问题就在于旧的内存的引用是elementData,elementData指向了新的内存块,如果有一个局部变量oldData变量引用旧的内存块的话,在复制的过程中就会比较安全,因为这样证明这块老的内存依然有引用,分配内存的时候就不会被侵占掉,然后复制完成后这个局部变量的生命期也过去了,然后释放才是安全的。不然在副本的的时候万一新的内存或其他线程的分配内存侵占了这块老的内存,而复制还没有结束,这将是个严重的事情。

关于ArrayList的和矢量区别如下:

ArrayList在内存不够时间默认是扩展50%+ 1个,Vector是默认扩展1倍。
Vector提供indexOf(obj,start)接口,ArrayList没有。
矢量属于线程安全级别的,但是大多数情况下不使用矢量,因为线程安全需要更大的系统开销。

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

127 public  void trimToSize(){
128     modCount ++ ;
129     int oldCapacity = elementData.length;
130     if(size < oldCapacity){
131         elementData = Arrays.copyOf(elementData,size);
132     }
}


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

7)转为静态数组指定者

4,注意的ArrayList的两个转化为静态数组的指定者方法。

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

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


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

public [T] T [] toArray(T [] a){
if(a.length < size)
//创建一个新的数组的运行时类型,但我的内容:
return (T [])Arrays.copyOf(elementData, size,a.getClass());
System.arraycopy(elementData,0,a,0 ,size);
if(a.length> size)
a [size] = null ;
返回一个;
}


Fail-Fast机制:

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

线程安全:

列表list
= Collections.synchronizedList(new ArrayList());

总结:

关于ArrayList中的源码,给出几点比较重要的总结:

1,注意其三个不同的构造方法。无参构造方法构造的ArrayList中的容量默认为10,带有收集参数的构造方法,将收集转化为数组赋给ArrayList中的实现数组elementData中。

2,注意扩充容量的方法ensureCapacity.ArrayList在每次增加元素(可能是1个,也可能是一组)时,都要调用该方法来确保足够的容量。当容量不足以容纳当前的元素个数时,就设置新的容量为旧的容量的1.5倍加1,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量),而后用Arrays.copyof(
)方法将元素拷贝到新的数组(详见下面的第3点)。从中可以看出,当容量不够时,每次增加元素,都要将原来的元素拷贝到一个新的数组中,非常之耗时,也因此建议在事先能确定元素数量的情况下,才使用ArrayList中,否则建议使用链表。

3,ArrayList中的实现中大量地调用了Arrays.copyof()和System.arraycopy()方法。我们有必要对这两个方法的实现做下深入的了解。

首先来看Arrays.copyof()方法它有很多个重载的方法,但实现思路都是一样的,我们来看泛型版本的源码。:

public  static <T> T [] copyOf(T [] original,int newLength){
return (T [])copyOf(original,newLength,original.getClass());
}


很明显调用了另一个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));
返回副本;
}


这里可以很明显地看出,该方法实际上是在其内部又创建了一个长度为newlength的数组,调用System.arraycopy()方法,将原来数组中的元素复制到了新的数组中。

下面来看System.arraycopy()方法。该方法被标记了天然的,调用了系统的C / C ++代码,在JDK中是看不到的,但在OpenJDK的中可以看到其源码。该函数实际上最终调用了ç语言的的memmove()函数,因此它可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多,很适合用来批量处理数组的.java强烈推荐在复制大量数组元素时用该方法,以取得更高的效率。

4,ArrayList的基于数组实现,可以通过下标索引直接查找到指定位置的元素,因此查找效率高,但每次插入或删除元素,就要大量地移动元素,插入删除元素的效率低。

5,在查找给定元素索引值等的方法中,源码都将该元素的值分为空和不为空两种情况处理,ArrayList的中允许元素为空。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: