Android Gems - Java源码分析之List

Posted threepigs

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Gems - Java源码分析之List相关的知识,希望对你有一定的参考价值。

最近突发奇想,写个Java源码分析系列。开发过程中,总会使用各种类库,如ArrayList,LinkedList等,用得虽多,但对于其实现细节却了解甚少,所以专卖开辟个系列,从源码的角度分析一下各种Java类库的实现细节。就先从最简单的List入手。

List是给接口,其继承自Collection<?>,标准接口咱就不说了,List的实现有两个,ArrayList和LinkedList,前者是数组实现,后者是链表。

ArrayList只有两个成员变量array,size。array是个Object[] 表示List的元素数组,size是元素个数,这里可能有人要问,Object[]不是自带length吗,为什么还需要个size呢?这是为了提高ArrayList的add性能,数组的内存空间会比实际List的元素的个数大,这样在add的时候,不用每次都重新分配内存,并进行arraycopy。

   @Override 
    public boolean add(E object) 
        Object[] a = array;
        int s = size;
        if (s == a.length) 
            Object[] newArray = new Object[s +
                    (s < (MIN_CAPACITY_INCREMENT / 2) ?
                     MIN_CAPACITY_INCREMENT : s >> 1)];
            System.arraycopy(a, 0, newArray, 0, s);
            array = a = newArray;
        
        a[s] = object;
        size = s + 1;
        modCount++;
        return true;
    


以上就是ArrayList的add函数源码,如果当前的元素的个数已经达到数组内存的大小,那么当前这个待add的object就没有空间了,所以需要扩容,ArrayList的扩容的算法是当前的size 的 2倍,为了避免元素少的时候增长太慢,于是设置了一个最小的扩容值12(MIN_CAPACITY_INCREMENT),也就是说如果当前size如果是1的话,那么ArrayList add之后,array的内存大小就扩大到12,而不是1 × 2。这是add到最后的情况,那么如果是add到某个指定位置 add(int index, E object)呢?

   @Override 
    public void add(int index, E object) 
        Object[] a = array;
        int s = size;
        if (index > s || index < 0) 
            throwIndexOutOfBoundsException(index, s);
        

        if (s < a.length) 
            System.arraycopy(a, index, a, index + 1, s - index);
         else 
            // assert s == a.length;
            Object[] newArray = new Object[newCapacity(s)];
            System.arraycopy(a, 0, newArray, 0, index);
            System.arraycopy(a, index, newArray, index + 1, s - index);
            array = a = newArray;
        
        a[index] = object;
        size = s + 1;
        modCount++;
    


从上面的代码可以看出,如果当前元素的size还未达到内存的上限(s < a.length),那么要做的就是arraycopy,其时间复杂度可是O(n),越往前插越慢。而如果当前元素的size达到上限,那么先需要扩容,然后再挪内存。由此看来,如果你的需求有很多的往数组中间插入的操作,那么ArrayList的性能会比较差,应尽量避免这种操作。

这是add单个元素的情况,对于addAll添加一个数组的情况也是一样的逻辑,添加之后元素的个数是newSize = s + newPartSize,如果newSize没有超过内存上限,就只需要arrayCopy先挪内存元素,以便新的Collection的元素都能插入进来。否则先扩容,新的size为newCapacity函数给出,这时候的算法并不是翻倍,而是newSize的基础上增加50%,这主要是避免扩容扩太快。

    @Override
    public boolean addAll(int index, Collection<? extends E> collection) 
        int s = size;
        if (index > s || index < 0) 
            throwIndexOutOfBoundsException(index, s);
        
        Object[] newPart = collection.toArray();
        int newPartSize = newPart.length;
        if (newPartSize == 0) 
            return false;
        
        Object[] a = array;
        int newSize = s + newPartSize; // If add overflows, arraycopy will fail
        if (newSize <= a.length) 
             System.arraycopy(a, index, a, index + newPartSize, s - index);
         else 
            int newCapacity = newCapacity(newSize - 1);  // ~33% growth room
            Object[] newArray = new Object[newCapacity];
            System.arraycopy(a, 0, newArray, 0, index);
            System.arraycopy(a, index, newArray, index + newPartSize, s-index);
            array = a = newArray;
        
        System.arraycopy(newPart, 0, a, index, newPartSize);
        size = newSize;
        modCount++;
        return true;
    
    private static int newCapacity(int currentCapacity) 
        int increment = (currentCapacity < (MIN_CAPACITY_INCREMENT / 2) ?
                MIN_CAPACITY_INCREMENT : currentCapacity >> 1);
        return currentCapacity + increment;
    
可见ArrayList的容量是动态扩展的,有时为了避免扩容造成的性能损失,可以先调用ensureCapacity来提前分配内存,这种方法主要适用于对自己未来的内存占用量有个提前预估。

以上是add的情况,remove的操作则会更加简单一些,remove由于内存没有扩容,因此和add比,没有扩容部门,只有挪元素部分。

    @Override 
    public E remove(int index) 
        Object[] a = array;
        int s = size;
        if (index >= s) 
            throwIndexOutOfBoundsException(index, s);
        
        @SuppressWarnings("unchecked") E result = (E) a[index];
        System.arraycopy(a, index + 1, a, index, --s - index);
        a[s] = null;  // Prevent memory leak
        size = s;
        modCount++;
        return result;
    


先把[index + 1, s)的元素前移。a[s] = null的目的是避免hold住前移之前的最后一个元素的对象,从而后续造成内存泄漏。同理,ArrayList的clear函数,除了只把size置0之外,还需要把array的元素置空,从这个角度看,clear的时间复杂度其实是O(n)的。

以上便是ArrayList的实现细节,是不是很简单。


LinkedList是链表实现的,他也有两个成员变量,voidLink,size。voidLink是个Link对象,表示链表的表头,size则是元素大小,size的目的主要是优化LinkedList.size函数,不用每次都遍历链表来数个数。

LinkedList的Link是个双链表,有next也有previous,双链表比单链表好处是删除方便,单链表的删除需要找到他的previous,不好的地方就是多了一个previous对象,多占了内存。LinkedList的链表还是个循环列表,voidLink表头的previous是指向最后一个元素的,目的是了add操作,链表没法随机访问,如果不是循环链表的话,那么就需要遍历整个链表,找到最后一个节点,才能插入。

我们看一下LinkedList的几个基本函数:

    @Override
    public E get(int location) 
        if (location >= 0 && location < size) 
            Link<E> link = voidLink;
            if (location < (size / 2)) 
                for (int i = 0; i <= location; i++) 
                    link = link.next;
                
             else 
                for (int i = size; i > location; i--) 
                    link = link.previous;
                
            
            return link.data;
        
        throw new IndexOutOfBoundsException();
    
get函数是随机访问,这个不是链表的长处。如果是前半部分,那么就从表头开始往后找,如果是后半部分,就从表头往前找。所以如果你的使用场景是随机访问比较多的话,建议用ArrayList,而不是LinkedList,LinkedList适合那种插入操作比较多的场景。另外一个注意的地方就是,遍历的时候,LinkedList千万别用for + get实现,这样复杂度非常高,需要用iterator来实现。

    @Override
    public boolean add(E object) 
        return addLastImpl(object);
    

    private boolean addLastImpl(E object) 
        Link<E> oldLast = voidLink.previous;
        Link<E> newLink = new Link<E>(object, oldLast, voidLink);
        voidLink.previous = newLink;
        oldLast.next = newLink;
        size++;
        modCount++;
        return true;
    
这是LinkedList添加元素到最后的实现,链表很方便,O(1),同理addFirst也一样。往中间插就不一样了,如下,需要先找到location所在的节点。虽然都是O(n),但LinkedList这个复杂度比ArrayList的System.arraycopy要高,ArrayList挪内存用的arraycopy是native实现的,并且是连续内存,是有优化的。

    @Override
    public void add(int location, E object) 
        if (location >= 0 && location <= size) 
            Link<E> link = voidLink;
            if (location < (size / 2)) 
                for (int i = 0; i <= location; i++) 
                    link = link.next;
                
             else 
                for (int i = size; i > location; i--) 
                    link = link.previous;
                
            
            Link<E> previous = link.previous;
            Link<E> newLink = new Link<E>(object, previous, link);
            previous.next = newLink;
            link.previous = newLink;
            size++;
            modCount++;
         else 
            throw new IndexOutOfBoundsException();
        
    

LinkedList的remove需要先找到节点,比如remove(int location),需要先找到这个元素,然后删除。remove(Object object)也是一样的,两种case都是随机删除,这都不是LinkedList擅长的,如果如果你的使用场景,如果是这种随机插入、删除的场景特别多的话,那么LinkedList是不适合的。

    @Override
    public E remove(int location) 
        if (location >= 0 && location < size) 
            Link<E> link = voidLink;
            if (location < (size / 2)) 
                for (int i = 0; i <= location; i++) 
                    link = link.next;
                
             else 
                for (int i = size; i > location; i--) 
                    link = link.previous;
                
            
            Link<E> previous = link.previous;
            Link<E> next = link.next;
            previous.next = next;
            next.previous = previous;
            size--;
            modCount++;
            return link.data;
        
        throw new IndexOutOfBoundsException();
    


细心的读者可能发现了,不管是ArrayList还是LinkedList都有一个modCount字段,这个是用来干什么的呢?

LinkedList和ArrayList都不是线程安全的,modCount在每次元素有改动的时候都会++,这个就是为了检查是否有改动,而使用的地方就是在Iterator里,不管是ArrayList还是ArrayList都实现自己的Iterator,用来遍历数组的元素。在每次Interator实例化的时候,也就是准备遍历数组元素的时候,都会保存当前的modCount到迭代器的expectedModCount字段,然后在做遍历操作的时候,会检查expectedModCount和modCount的值,如果这个值不一样,就说明有其他线程在这次遍历期间,对List的元素做过改动,就会抛出ConcurrentModificationException异常,用来提示多线程竞争了。ConcurrentModificationException是不是很熟悉,如果你遇到这个异常,就说明在多个线程里对一个线程不安全的容器做了改动,很可能会造成数据的不一致,不止是ArrayList、LinkedList,HashMap、Hashtable都一样,都是在使用了迭代器的时候会碰到这个异常。


作者简介:

田力,网易彩票Android端创始人,小米视频创始人,现任roobo技术经理、视频云技术总监


欢迎关注微信公众号 磨剑石,定期推送技术心得以及源码分析等文章,谢谢





















以上是关于Android Gems - Java源码分析之List的主要内容,如果未能解决你的问题,请参考以下文章

Android Gems — Java源码分析之HashMap和SparseArray

Android Gems — Java源码分析之HashMap和SparseArray

Android Gems — Fragment本质之View管理

Android Gems — Fragment本质之View管理

Android Gems — AMS的Service生命周期管理

Android Gems — AMS的Service生命周期管理