Java中Arraylist源码分析
Posted Yrion
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中Arraylist源码分析相关的知识,希望对你有一定的参考价值。
前言:ArrayList作为我们常用的一个集合数据类型,我们在代码中经常用它来装载数据,可谓是和HashMap一样常用的集合类型了。我们经常用它,那么就有必须知道它的内部工作原理,比如它是如何添加进去数据的,它内部的数据结构是怎样的,当我们做一个remove操作,它又做了哪些工作。了解这些内部工作的原理能够帮助我们更好的理解Arraylist,什么时候使用它和不使用它,如何提升它的效率,等等。那么本篇博文就来聚焦Arraylist,走进它的内部源码,来一探究竟吧。
本篇博客的目录
一:Arraylist简介
二:Arrsylist的构造方法
三:Arraylist的add和remove方法分析
四:Arraylist的常用方法源码分析
五:Arrraylist与linkedlist的区别
六:总结
一:Arraylist简介:
首先是Arraylist一种java的数据容器,作为数据容器,我们在程序中经常使用它,比如Map、Collection、HashMap、TreeSet等等。如它的名字一样,它的内部结构是数组,而这个数组是可以动态扩容的,就犹如hashMap一样。它的所有操作都是基于其内部的动态数组来进行操作,list接口的大小可变数组的实现、如果1-1所示(其中index表示的是数组的元素的位置,但注意其数组下标实际是从0开始的,而不是1),源码中多次涉及到index这个变量,在这里可以先理解一下。其次它本质上一个类,实现了list接口,继承了AbstractList,和Cloneable接口、Serializable表明其继承于抽象的list,并且可以进行浅复制和序列化写入到数据文件中。同时它在构建的时候就说明了其类型泛型E,所以Arraylist是一种在新建时就声明其包含类型的集合,并且可支持复制、序列化、克隆等特性。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
二:Arrsylist的成员变量和构造方法
我们先来看一看Arraylist的成员变量,从中可以看到Arraylist维护着的数组和一些基本的数值表示出它的大小和数据类型,之所以声明为Object类型的,是因为要定义一个适用于所有java数据类型的,这样它就可以向下转型为所有基本类型了。至于元素容量为什么是10,而不是11或者12,这个可能是jdk的作者经过计算、根据使用习惯计算出来的,有一定的科学依据。这些都是全局变量,也就是说下面的方法都可以访问这些变量,并修改它的值。
private static final long serialVersionUID = 8683452581122892189L; //序列版本号 private static final int DEFAULT_CAPACITY = 10;//默认数组元素的初始容量为10 private static final Object[] EMPTY_ELEMENTDATA = {};//空元素对象数组 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//默认容量的空数据数组 transient Object[] elementData; // 数组元素对象,list中的元素就存在这里 private int size;//添加的元素的数量
看完了成员变量,我们来看一下Arraylist的3个构造函数,其中我们用到的最多的也就是第二个无参的构造函数,平时代码里我们经常这样写:List<String> list= new Arraylist()<String>;这就是调用了其第二个构造函数,在构造函数里面,我们可以看到它对空数组进行了赋值,将维护着的数组的大小设为10个容量,然后我们就可以使用这个list了。接下来我们就讲一讲如何往这个list里面增添元素和移除元素,这是我们使用list最常用的两个方法。
public ArrayList(int initialCapacity) { //初始化容量 if (initialCapacity > 0) { //判断传入的参数值 this.elementData = new Object[initialCapacity];//把传入的容量作为数组的初始容量 } else if (initialCapacity == 0) {//如果传入的是0 this.elementData = EMPTY_ELEMENTDATA;//初始化为空数组 } else { throw new IllegalArgumentException("Illegal Capacity: "+ //传入的如果小于0,就抛出参数违规异常 initialCapacity); } } public ArrayList() { //空构造方法 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;//空对象数组初始化,其容量为10 } public ArrayList(Collection<? extends E> c) {//传入一个泛型的集合 elementData = c.toArray();//默认空数组 if ((size = elementData.length) != 0) { // // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } }
三:Arraylist的add和remove方法分析
1.add方法的解析:
我们先来看一下add方法,这其中又涉及到了rangeCheckForAdd方法和ensureCapacityInternal、System.arraycopy方法,我们来依次分析一下其中的源码,看看究竟在我们add一个元素的时候,Arraylist做了什么:
public void add(int index, E element) {//根据指定元素添加指定元素 rangeCheckForAdd(index);//通过位置进行范围检查 ensureCapacityInternal(size + 1); // System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element;//元素赋值给指定的位置 size++;//数组大小增加 }
private void rangeCheckForAdd(int index) { //专为add方法增加的返回检查 if (index < 0 || index > this.size)//如果传入的参数小于0或者大于数组已添加元素的个数 throw new IndexOutOfBoundsException(outOfBoundsMsg(index));//抛出异常 }
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;//定义数组最大的值为Integer最大值-8
private void ensureCapacityInternal(int minCapacity) {//确保容量 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //如果数组元素等于默认的空数组容量 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);//取默认的容量和指定的容量的较大值 } ensureExplicitCapacity(minCapacity);//传入 ensureExplicitCapacity方法确保扩展的容量 }
private void ensureExplicitCapacity(int minCapacity) {/确保扩展容量 modCount++;//修改次数+1 // overflow-conscious code if (minCapacity - elementData.length > 0)//如果指定的容量大于数组容量 grow(minCapacity);//调用gorw方法 }
private void grow(int minCapacity) { //传入指定的容量 // overflow-conscious code int oldCapacity = elementData.length;//取旧的数组元素的长度 int newCapacity = oldCapacity + (oldCapacity >> 1);//旧长度除以2+长度赋予新长度(相当于扩容1.5倍) if (newCapacity - minCapacity < 0)//如果新长度小于指定的容量 newCapacity = minCapacity;//把新计算的长度赋予指定的容量 if (newCapacity - MAX_ARRAY_SIZE > 0)//如果新容量>最大数组大小 newCapacity = hugeCapacity(minCapacity);//调用bugeCapcaity方法 elementData = Arrays.copyOf(elementData, newCapacity);//复制数组 }
private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // 如果指定容量小于0 throw new OutOfMemoryError();//抛出异常内存错误 return (minCapacity > MAX_ARRAY_SIZE) ?//如果指定容量大于最大数组大小返回int的最大值 Integer.MAX_VALUE : MAX_ARRAY_SIZE;//否则返回最小数组大小 }
通过源码我们可以看出:Arraylist在添加元素的时候,首先进行的是范围检查,防止其传入的参数小于0或者超过数组的size大小,再是和默认分配的大小值进行比较,如果大于默认大小就要进行扩容。扩容的时候首先把旧数组的大小提升1.5倍,成为新数组的大小值。同时也可以看到Arrylist的大小边界是Interger的最大值,这个数字是很大的:2147483647,也就是它的最大值是这么多,这个值足以我们程序员平常进行使用!在检查完毕和扩容完毕之后,就是要进行数组的拷贝了,我们开看一下System.arrayCopy()方法的源码:
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
其中可以看出,它是native修饰的,也就是说它是一个引用本地其他语言的代码库(windows就是.dll库)的方法,具体的源码我们不深究了,这里解释一下它的参数,src:你要从哪个数组复制,srcpos:要复制的位置起点 dest:要复制要哪个数组,destPos:要复制到的数组起始位置 lengh:复制的长度
System.arraycopy(elementData, index, elementData, index + 1, size - index);
代入add方法里,也就是说把elementData的从index开始的元素复制size-index长度到elementData中,从index+1开始。这是一个自我复制的过程,为了保证数据的完整。然后把指定的元素值放入到指定的位置中。我们来用图解释一下这个复制的过程:首先是图1-1表示的是一共有5个元素,那么这个数组的size就是5.我们现在要在index=2的位置上插入元素六。我们来看看通过arraycopy方法之后的数组是什么样的:如图1-2。然后调用lementData[index] = element方法,之后数组就会变成图1-3:可见我们要插入的元素六就这样插入到指定位置上了,从中也可以看出自我复制也就是数组的元素从指定的位置向后移动,这也间接说明了Arraylist插入数据的效率比较低,因为它要移动数据。
图 1-3
2:remove方法
我们看romove方法首先还是进行范围检查,然后用elementData()方法去找到指定数组元素中的值,再用size-index-1转为numMoved,接着进行再次自我复制。然后把size-1甚至设置为null。这样这个数组中元素就是null了。
public E remove(int index) {//根据指定位置移除元素 rangeCheck(index);//范围检查 modCount++;//修改次数+1 E oldValue = elementData(index);//根据数组元素的位置找到旧值 int numMoved = size - index - 1; if (numMoved > 0)//判断是否大于0 System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // return oldValue;//返回旧值 }
为了更好的理解remove方法,我们来用画图的方式理解一下:其中一二三到十使我们存放的值,如图2-1.那么这个数组的size=10,假如我们移除remove(3),则index=3,我们可以看到index=2对应的值是四,然后numMoved的值是7,大于0。然后进行自我复制。从elementData的index=3开始,复制之后的结果如图2-2,可以看出从index开始的位置所有的元素往前移动了一位,这就是arraycopy方法的根本目的,然后再把 elementData[--size] = null; 也就是最后一个元素十置为null,这样数组的大小就自然减-1,并且顺利的移出了指定位置的元素。这就是remove方法原理
图 2-1
图 2-2
四:Arraylist的常用方法源码分析
我们再来看一下Arraylist中一些常见方法的源码,其中包括返回其大小的方法,判断是否为空的方法。是否包含某个元素的方法等等,这些方法比较简单,我只列出了基本的注释。
public int size() {//取其大小 return size;//返回size的数组 } public boolean isEmpty() {//判断list是否为空 return size == 0;//用其size和0去比较 } public boolean contains(Object o) {//判断指定元素是否包含 return indexOf(o) >= 0;//返回indexof方法的返回结果是否大于0。大于0表示含有,如果小于0表示没有 } public int indexOf(Object o) {//检索指定元素是否存在 if (o == null) {//如果元素为null for (int i = 0; i < size; i++)//遍历循环整个数组 if (elementData[i]==null)//如果找到元素为null return i;//返回元素中的位置 } else {//如果不为null for (int i = 0; i < size; i++)//遍历循环数组 if (o.equals(elementData[i]))//如果指定元素和数组中的元素相同 return i;//返回数组中的位置 } return -1;//否则返回-1 } public int lastIndexOf(Object o) {//从末尾检索指定元素 if (o == null) {//如果元素为null for (int i = size-1; i >= 0; i--)//按照元素从大到小 if (elementData[i]==null)//如果元素为null return i;//返回数组中的位置 } else {//如果元素不为null for (int i = size-1; i >= 0; i--)//按照元素从大到小 if (o.equals(elementData[i]))//如果元素等于数组中的元素 return i;//返回数组中的位置 } return -1;//否则返回-1 }
public boolean remove(Object o) {//判断元素是否能够移除 if (o == null) {//如果元素为null for (int index = 0; index < size; index++)// if (elementData[index] == null) {//如果元素值为null fastRemove(index);//快速移除 return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } private void fastRemove(int index) {//根据指定的位置移除元素 modCount++;//修改次数+1 int numMoved = size - index - 1;//从指定元素的下一个开始 if (numMoved > 0)//如果元素的下一个大于0 System.arraycopy(elementData, index+1, elementData, index, numMoved);//拷贝数组 elementData[--size] = null; //数组对象的最后一个值赋值为null } public void clear() { //清除元素 modCount++;//修改次数+1 for (int i = 0; i < size; i++)//遍历循环整个数组 elementData[i] = null;//将其值设为null,便于GC回收 size = 0;//size等于0 }
五:Arraylist与linkedlist的区别
1.从本篇博文中可以看出ArrayList基于动态数组的数据结构,但是LinkedList基于链表的数据结构。
2:Arraylist是有序的,元素它是按照固定的顺序排列,每次都往后移动一位,并且很容易可以看出它是允许值为null。而linkedlist是无序的。
3.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。 我们在这里看一下get方法的源码,其中可以看出其只需要做个范围检查,然后就可以通过数组的位置计算 处其位置上的值,这也是数组的特点,获取数组很快。但是linkedlist是链表结构的,所谓结构决定功能,链表的获取元素时候需要一层层去节点里面遍历,这无疑增加了工作量,所以linked的随机 访 问要落后与Arraylist
public E get(int index) {//根据指定位置返回对应的值 rangeCheck(index);//调用范围检查的方法,防止其越界 return elementData(index);//把参数传入,调用elementData()方法 }
4.对于新增和删除操作add和remove,LinkedList有很大的优势,因为ArrayList要移动数据
我们通过比对发现add和remove方法,它们都要经过System.arrayCopy()方法,进行数据的移动,比较麻烦,而基于链表的结构的LinkedList则只需要进行值和位置的封装,然后放入链表即 可。
5:Arraylist是非线程安全的,其中我们可以看出它并没有同步机制的实现,也没有synchronize等关键字的修饰。所以在多线程的环境下慎用Arraylist。同时linkedList也并非线程安全的!
六:总结
本篇博文介绍了Arraylist的源码,从其中可以看出jdk设计者的巧妙,包括其add方法的校验,防止校验,还有动态扩容的特性,它作为一个我们在实际开发中常用的数据容器,按照从全局变量到构造方法再到其add、remove方法的学习,我们对arraylist也有了进一步的认知,同时与linkedlist进行了对比,以便于我们更好的掌握Arraylist!好了,本次博客就到这里结束了。
以上是关于Java中Arraylist源码分析的主要内容,如果未能解决你的问题,请参考以下文章