zoukankan      html  css  js  c++  java
  • ArrayList源码学习

    前言

    不知道大家有没有同感,看书或者看别人博客又或者看源码的时候,看的时候都觉得自己都看懂了,过了一段时间呢又都想不起来了。

    以前学习的时候总认为看懂了就行了,没必要写下来,觉得网上那么多比自己写的好的多得是,看别人写的就行了。

    最近在极客时间APP上看左耳朵耗子的专栏,有一段是对我比较有感触的。自己总结了有几点:第一要有总结,你可能每部分都看懂了(比如:数组扩容,检查数据是否存在数组内),但是没有从全局去思考,有可能给自己造成假象觉得自己都懂了其实只是一知半解而已。第二要写下来,写的过程就能检测自己是不是真的懂了,如果没懂是写不出来的,还有就是写的过程中能锻炼自己的语言组织能力,所有不管写的好坏都要尽力写下来。第三就是能够把学到的讲给别人听,能够真正用自己的语言讲给别人并且能让别人听得懂那么就真的掌握了这个技术点。

    所以开始尝试把自己学习的东西记录下来,写的不对的地方欢迎各位指正,讨论。

    下面进入正题

    ArrayList源码学习

    JDK版本:JDK9

    ArrayList的原理其实就是维护了一个Object类型的数组,只不过内部实现了数组的扩容等一系列的操作

    接下来先看下ArrayList的属性有哪些

    一、属性

    private static final int DEFAULT_CAPACITY = 10;
    
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; // non-private to simplify nested class access
    private int size;

    1、DEFAULT_CAPACITY  默认数组容量大小 10

    2、EMPTY_ELEMENTDATA   , DEFAULTCAPACITY_EMPTY_ELEMENTDATA  空的数组对象,一开始看的时候没搞懂为什么会有两个对象,看到后面才明白,这个到添加对象的时候再讲

    3、elementData 这个就是我刚说的ArrayList背后维护的一个数组

    4、size 数组的长度

    二、构造函数

     1 public ArrayList(int initialCapacity) {
     2         if (initialCapacity > 0) {
     3             this.elementData = new Object[initialCapacity];
     4         } else if (initialCapacity == 0) {
     5             this.elementData = EMPTY_ELEMENTDATA;
     6         } else {
     7             throw new IllegalArgumentException("Illegal Capacity: "+
     8                                                initialCapacity);
     9         }
    10     }
    11 
    12     /**
    13      * Constructs an empty list with an initial capacity of ten.
    14      */
    15     public ArrayList() {
    16         this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    17     }
    18 
    19     /**
    20      * Constructs a list containing the elements of the specified
    21      * collection, in the order they are returned by the collection's
    22      * iterator.
    23      *
    24      * @param c the collection whose elements are to be placed into this list
    25      * @throws NullPointerException if the specified collection is null
    26      */
    27     public ArrayList(Collection<? extends E> c) {
    28         elementData = c.toArray();
    29         if ((size = elementData.length) != 0) {
    30             // defend against c.toArray (incorrectly) not returning Object[]
    31             // (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
    32             if (elementData.getClass() != Object[].class)
    33                 elementData = Arrays.copyOf(elementData, size, Object[].class);
    34         } else {
    35             // replace with empty array.
    36             this.elementData = EMPTY_ELEMENTDATA;
    37         }
    38     }
    View Code

     1、第一个是指定数组初始长度的构造函数。先说下使用场景然后大家就能知道为什么会有一个指定初始长度的构造函数了,在你能预测到大概会有多少数据要存储到这个数值里的时候就可以用指定数组长度的构造函数来创建对象了,这样就能避免数组多次的扩容,影响性能。

        着重关注下 initialCapacity == 0 的时候,elementData 的默认值是 EMPTY_ELEMENTDATA 

     2、第二个是不指定初始长度的构造函数。那么 elementData  的默认值是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA ,跟指定初始值为0的情况下的默认值是不一样的

     3、第三个就是构造一个包含指定元素的列表集合 

        着重看下 elementData.getClass() != Object[].class 的这个条件,是因为以前版本的JDK里getClass返回的类型不一定是Object[].class,所有做了一个判断,

      Arrays.copyOf 有不懂的先自行查找。稍后我会写一篇介绍 Arrays.copyOf和System.arraycopy 的文章

          另一个就是给定初始数据集合长度为0的时候 elementData 的默认值 EMPTY_ELEMENTDATA 跟指定数组长度为0的时候给的默认值一样

    三、增

     1 public boolean add(E e) {
     2         modCount++;
     3         add(e, elementData, size);
     4         return true;
     5     } 
     6 private void add(E e, Object[] elementData, int s) {
     7         if (s == elementData.length)
     8             elementData = grow();
     9         elementData[s] = e;
    10         size = s + 1;
    11     }
    12 
    13 private Object[] grow() {
    14         return grow(size + 1);
    15     }
    16 
    17 private Object[] grow(int minCapacity) {
    18         return elementData = Arrays.copyOf(elementData,
    19                                            newCapacity(minCapacity));
    20     }
    21 private int newCapacity(int minCapacity) {
    22         // overflow-conscious code
    23         int oldCapacity = elementData.length;
    24         int newCapacity = oldCapacity + (oldCapacity >> 1);
    25         if (newCapacity - minCapacity <= 0) {
    26             if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
    27                 return Math.max(DEFAULT_CAPACITY, minCapacity);
    28             if (minCapacity < 0) // overflow
    29                 throw new OutOfMemoryError();
    30             return minCapacity;
    31         }
    32         return (newCapacity - MAX_ARRAY_SIZE <= 0)
    33             ? newCapacity
    34             : hugeCapacity(minCapacity);
    35     }
    View Code

    1、这是一个最常用的新增操作,modCount是记录集合的修改次数。用迭代器操作netx的时候会先检查这个集合是否被外界操作,如果有操作那么modCount的值跟初始化迭代器时的值是不一样的代码如下:checkForComodification方法

    int expectedModCount = modCount;
    public E next() {
                checkForComodification();
                int i = cursor;
                if (i >= size)
                    throw new NoSuchElementException();
                Object[] elementData = ArrayList.this.elementData;
                if (i >= elementData.length)
                    throw new ConcurrentModificationException();
                cursor = i + 1;
                return (E) elementData[lastRet = i];
            }
     final void checkForComodification() {
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
            }
    View Code

    但是用迭代器remove集合值的时候为什么会不异常呢,是因为迭代器remove的时候更改了迭代器里expectedModCount 的值等于ArrayList的modCount的值,所有不会引发异常代码如下:

     expectedModCount = modCount;

    public void remove() {
                if (lastRet < 0)
                    throw new IllegalStateException();
                checkForComodification();
    
                try {
                    ArrayList.this.remove(lastRet);
                    cursor = lastRet;
                    lastRet = -1;
                    expectedModCount = modCount;
                } catch (IndexOutOfBoundsException ex) {
                    throw new ConcurrentModificationException();
                }
            }
    View Code

    2、新增操作前会检查集合是否需要扩容。有时候面试会问集合长度为10,问新增第几个的时候才会扩容,如果看过源码就会很清楚,简单说就是新增的时候不够了才会扩容,不会提前扩容的。

    s == elementData.length;就是判断是否要扩展操作

    扩容要扩多少能?下面代码就是要扩容的长度(>>代表 oldCapacity/2 )

    int newCapacity = oldCapacity + (oldCapacity >> 1); 

    这是默认情况下扩容的长度长度,为什么说是默认扩容规则呢,那么看下面代码就会知道另外一种情况下的扩容

    private int newCapacity(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            if (newCapacity - minCapacity <= 0) {
                if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                    return Math.max(DEFAULT_CAPACITY, minCapacity);
                if (minCapacity < 0) // overflow
                    throw new OutOfMemoryError();
                return minCapacity;
            }
            return (newCapacity - MAX_ARRAY_SIZE <= 0)
                ? newCapacity
                : hugeCapacity(minCapacity);
        }
    View Code

    一开始看的时候没看明白为什么会有 if (newCapacity - minCapacity <= 0)的判断,认为newCapacity始终>=minCapacity才对呀,后来看到add指定插入数组位置的时候恍然大悟,比如数组长度为5,但是指定插入的位置为8的时候就会存在 newCapacity - minCapacity <= 0 的情况。

    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) 这行代码不知道大家看明白没,这就是为啥一开始有两个空集合属性的原因,这个判断的意思是如果是通过默认无参构造函数创建的对象并且数组的长度小于10时那么默认数组的长度是初始为10

    下面有一句话代码不是太懂,不知道为什么要跟 Integer.MAX_VALUE - 8 做比较,希望那个小伙伴看懂了能交流下

    接下来就是指定位置插入数据了,这个就不贴源码了,简单说下这个跟新增的区别不大

    1、检查指定位置的合法性不能小于0

    2、扩容检查和扩容逻辑跟刚才的新增一样

    3、扩容后要移动数组位置,比如:索引2上面已经有数据了现在插入2位置数据那就要移动数组,代码如下。

     System.arraycopy(elementData, index,
                             elementData, index + 1,
                             s - index);
    View Code

    四、删除

    只介绍两种删除方式、第一种根据索引删除,第二种根据值删除数据

    代码如下:

     public E remove(int index) {
            Objects.checkIndex(index, size);
    
            modCount++;
            E oldValue = elementData(index);
    
            int numMoved = size - index - 1;
            if (numMoved > 0)
                System.arraycopy(elementData, index+1, elementData, index,
                                 numMoved);
            elementData[--size] = null; // clear to let GC do its work
    
            return oldValue;
        }
    
     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;
        }
    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; // clear to let GC do its work
        }
    View Code

    根据索引删除比较简单,就是移动数组位置,然后把最后一个索引上面的值设置为null,便于GC回收

    根据值删除,先判断值是否为null,因为null不能用equals判断,然后就是循环判断查找到要删除的值后根据值对应的索引然后移动数组,这个地方有个问题如果数据里有多个相同的值那么这种方式只能移除索引最靠前的那一个。

    五、查找

    查找功能大家应该都能看的懂,此处不再介绍。

    六、总结

    本文只要讲解了数组初始化不同方法的区别,数组扩容的逻辑,新增数据的原理,然后简单介绍了删除的逻辑。

    其实还有很多地方没有讲到的比如:iterator,toArray,removeAll,addAll,get,set,lastIndexOf等内容。

    由于本人第一次写作有哪些不正确和不足的地方还望指出,希望多多跟大家讨论共同学习进步。

  • 相关阅读:
    iOS数据持久化—FMDB框架的简单介绍
    iOS数据持久化—SQLite常用的函数
    多段图动态规划dp
    Cucumber测试驱动开发
    敏捷开发XP
    Android 小票打印USB
    TextInputLayout 用法
    Snackbar 提醒
    PermissionDispatcher 运行时权限框架
    Tinker 热修复
  • 原文地址:https://www.cnblogs.com/hank-chen/p/9500870.html
Copyright © 2011-2022 走看看