ArrayList 源码解析 及其扩展(jdk1.7)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yyyCHyzzzz/article/details/72229043

概述

ArrayList 基于数组实现,是一个动态数组,其容量能自然增长(1.5倍增长)。

不是线程安全,你可以使用Collection.synchronizedList方法将该列表包装起来,以防止意外对列表进行不同步的访问。也可以使用concurrent并发包下的CopyOnWriteArrayList类。

java 1.6API对其解释
返回指定列表支持的同步(线程安全的)列表。为了保证按顺序访问,必须通过返回的列表完成所有对底层实现列表的访问。
在返回的列表上进行迭代时,用户必须手工在返回的列表上进行同步: 

  List list = Collections.synchronizedList(new ArrayList());
      ...
  synchronized(list) {
      Iterator i = list.iterator(); // Must be in synchronized block
      while (i.hasNext())
          foo(i.next());
  }
 不遵从此建议将导致无法确定的行为。 
如果指定列表是可序列化的,则返回的列表也将是可序列化的。

ArrayList实现了 List,RandomAccess,Cloneable,Serialiable 接口

RandomAccess接口,支持随机访问,实际上就是通过下标序号进行快速访问。实际上,实现此接口的List使用 for (int i=0, n=list.size(); i < n; i++) 这种方式迭代的速度会比用for(int i : list)会快一点

ArrayList实现(JDK1.7)

ArrayList中定义了四个私有属性:

private static final int DEFAULT_CAPACITY = 10;     //默认容量
private static final Object[] EMPTY_ELEMENTDATA = {};     //一个空数组,当用户指定了0为容量时,返回该空数组
private transient Object[] elementData; //实际存放数据的数组
private int size;    //实际存放数据的大小

构造方法:

public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        //构造一个新数组,指定容量
        this.elementData = new Object[initialCapacity];
    }
    
    
    public ArrayList() {
        super();
        //默认等于空的数组
        this.elementData = EMPTY_ELEMENTDATA;
    }
    
    
    
    //此方法的Collection是指集合类只要实现了Collection接口 都能重新转换为ArrayList(List接口已经继承了Collection接口)
    public ArrayList(Collection<? extends E> c) {
        //转换为数组
        elementData = c.toArray();
        size = elementData.length;
        if (elementData.getClass() != Object[].class)
            //调用native方法 快速构造数组
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    }

ArrayList增加操作

'''
    //将当前容量调整为实际个数
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = Arrays.copyOf(elementData, size);
        }
    }
    
    
    public boolean add(E e) {
        //此方法的关键是grow函数,增加元素是一个一个添加 所以size+1传入进去
        ensureCapacityInternal(size + 1);  
        elementData[size++] = e;
        return true;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        //默认增加为原大小的1.5倍    oldCapacity>>1是把数转换为二进制 并向右移动一位 效果相当于 oldCapacity/2
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //判断增加后的大小够不够,够了就直接使用newCapacity创建新数组
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)//如果增加后的大小比规定的最大size还大 则调用hugeCapacity方法
            newCapacity = hugeCapacity(minCapacity);
        //正常情况下 新的数组大小都为以前的1.5倍
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) 
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

容量扩大,调用的是Arrays.copyOf(..,..);这个方法,虽然这个方法是native方法,源码里面是创建一个新的数组,然后将旧数组上的数组copy到新数组,这是一个很大的消耗。如果放在程序中,我们最好能够预计其大小,避免重复申请内存。


注意:

1.在jdk1.6中

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);
     }
}

很明显 这里的数组扩容没有使用位运算,而是直接使用除法和乘法,从效率上来看,jdk1.7 的ArrayList 会比 jdk1.6快一点(位运算更接近系统底层,有兴趣的同学 可以百度)

2.jdk1.6中没有定义MAX_ARRAY_SIZE的大小,所以无法做判断,这也是1.7中改进的地方。


例子

我们在15 16行打上断点

image

到达15行 还未运行完15行时 list中只有4个数据 所以size为4 因为刚开始定义了5为list的初始大小,则下标为4的为null

image

当运行完15行 到达16行时,很明显list已经满了

image

运行完16行后,list扩容为7 (5*1.5 省略小数点后面的数) 所以下标为6的为null

image

最后输出list.size()的大小,因为是返回list实际的大小 和elementData大小无关

image


说完了最复杂的增加操作,我们说删,改,查。

删除操作

//移除指定位置的数据
 public E remove(int index) {
        //检查是否下标越界
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            //elementData从第index+1下标开始 复制到原elementData的index下标开始 numMoved是复制的长度
            //numMoved已经定义好了 是从要移除的下标号后面还剩余的数组长度
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //数组最后一个数就为null了
        elementData[--size] = null; 
        //返回移除后的数据
        return oldValue;
    }
    
private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    
    
    
    
//移除指定数据
public boolean remove(Object o) {
    //判断对象是否为空
    if (o == null) {
        //迭代数组查找目标
        for (int index = 0; index < size; index++)
            //迭代数组里面为空数据的索引号,快速删除
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            //和节点比较
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}
//其实这个方法和remove(int index) 里面的方法如出一辙
//可能开发人员没有重复把这个方法运用到remove(int index)里,
private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; 
    }

以上这些 就很好的说明了 为什么ArrayList增和删的效率不高了 都是要重新给数组赋值,或者新new一个数组接收更改后的旧数组,这样对内存的消耗会很大。

修改和删除

public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }
 public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

这个应该很简单了吧 我就不给注释了。

ArrayList扩展

我们在eclipse中 ctrl+t 查看实现list接口的类有哪些 image

这其中我们所了解的恐怕只有LinkedList和Vector了

这里大致说一下这两个集合类

LinkedList 底层是链表结构,而且是双链表,也可以当作堆栈,队列或双端队列进行操作。 特点是:查询效率低,增删效率高

Vector 底层也是用数组实现的,里面大部分方法都被声明了synchronized关键字,所以说是线程安全的集合,就是因为synchronized关键字的存在,这个集合本身就是很重量级,几乎很少用到。

关于线程安全和不安全的问题,因为我们是程序员嘛,当然会有办法外部控制这个操作,所以没必要纠结用哪个集合。但是如果有现成的类使用,就不要重复造轮子了。

猜你喜欢

转载自blog.csdn.net/yyyCHyzzzz/article/details/72229043