• ArrayList使用及原理


    一、前言

    集合类是面试中经常会被问到,今天带大家分析一下最常用的集合类之一ArrayList类,希望对大家有所帮助。

    ArrayList属于Collection集合类大家族的一员,是分支List中的主力军之一。ArrayList使用非常广泛,无论是在数据库表中查询,还是网络信息爬取都需要使用,所以了解ArrayList的原理就十分重要了(本文ArrayList版本基于JDK 1.8)。

    二、ArrrayList的继承关系

    通过IDEA生成ArrayList的继承关系图,可以清晰的看出ArrayList的继承关系。入下图。

     三、定义ArrayList

    ArrayList有三个构造方法:

    1. 无参构造方法
    2. 参数为整数的构造方法
    3. 参数为集合的构造方法

    3.1 ArrayList的属性

    首先,我们先看一下ArrayList类定义的几个属性。

    /**
         * Default initial capacity.
         */
        private static final int DEFAULT_CAPACITY = 10;
    
        /**
         * Shared empty array instance used for empty instances.
         */
        private static final Object[] EMPTY_ELEMENTDATA = {};
    
        /**
         * Shared empty array instance used for default sized empty instances. We
         * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
         * first element is added.
         */
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
        /**
         * The array buffer into which the elements of the ArrayList are stored.
         * The capacity of the ArrayList is the length of this array buffer. Any
         * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
         * will be expanded to DEFAULT_CAPACITY when the first element is added.
         */
        transient Object[] elementData; // non-private to simplify nested class access
    
        /**
         * The size of the ArrayList (the number of elements it contains).
         *
         * @serial
         */
        private int size;
    
       
    View Code

     可以看到,DEFAULT_CAPACIT属性定义ArrayList的默认容量是10。

    ArrayList定义了两个空实例 EMPTY_ELEMENTDATA 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,EMPTY_ELEMENTDATA 用于空实例的共享空数组实例。DEFAULTCAPACITY_EMPTY_ELEMENTDATA用于默认大小的空实例。 我们将此与EMPTY_ELEMENTDATA区别开来,用于添加第一个元素的时候知道扩容多少。

    elementData属性是一个Object数组,用于存储添加的元素。transient关键字标识elementData不能被序列化。

    size属性标识,当前Arraylist的长度。

    3.2 ArrayList的构造方法

    1、参数为整数的构造方法

     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     }

    很容易可以看出,当参数为正数时,初始化一个长度为传入参数的数组;参数为0时,初始化一个长度为默认长度的数组,否则就抛出一个非法参数异常。

    2、无参构造方法

    public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    无参构造方法只是将elementData指向了一个空数组。

    当然,我们能够调用ArrayList提供的扩容方法来扩充ArrayList的容量

    public void ensureCapacity(int minCapacity) {
            int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                // any size if not default element table
                ? 0
                // larger than default for default empty table. It's already
                // supposed to be at default size.
                : DEFAULT_CAPACITY;
    
            if (minCapacity > minExpand) {
                ensureExplicitCapacity(minCapacity);
            }
    }

    可以看出,若当前数组是空时,最小扩容为10,否则扩容传入的正整数。然后调用ensureExplicitCapacity方法

    private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
    
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
    }

    该方法主要调用grow方法进行扩容

    private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
    }
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
    throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
    Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
    }
     

    该方法首先算出当前容量的1.5倍小于传入的容量,如果是则将传入的参数作为扩容大小,否则,扩容到当前容量的1.5倍。如果参数大于数组最大值,则扩容到ArrayList最大值,否则扩容到数组最大值。实际上MAX_ARRAY_SIZE与Integer.MAX_VALUE相差8。再来看一下是怎么扩容的

    public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
            @SuppressWarnings("unchecked")
            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));
            return copy;
     }

    看到了吧,就是实例化了一个对应类的数组而已。

    3、参数为集合的构造方法

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

    该构造函数首先将如参转化为数组赋值给elementData,将elementData指向新copy的一个数组对象,copyof方法就是上文描述的。

    四、ArrayList的使用

    前面我们知道了改怎么实例化一个ArrayList对象,接下来我们讲讲该怎么使用使用ArrayList了。ArrayList给我们提供了很多方法,经常使用的有add,addAll,set,get,remove,size,isEmpty等;

    接下来我们举个例子来说明一下这些方法的使用。

    4.1 add方法

    首先我们先添加几种我最爱吃的几种水果

    public void testArrayList() {
            ArrayList<String> list = new ArrayList<>();
            list.add("苹果");
            list.add("香蕉");
            list.add("草莓");
            list.add("水蜜桃");
            list.add("菠萝");
            list.add("葡萄");
     }

    好奇add方法做了什么吗,那么接着往下看

    public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
    }

    可以从代码中看出首先执行了ensureCapacityInternal方法,然后想elementData里面添加一个值,也就是,我们添加的值都被放在了elementData数组里,这跟之前所说的一致。接下来再看看ensureCapacityInternal方法。

    private void ensureCapacityInternal(int minCapacity) {
            ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            }
            return minCapacity;
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
    
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }

    我将这几个相关的方法复制过来,首先看一下calculateCapacity方法,通过上面的构造函数我们发现if判断为true,取一个最大值。我们这个时候最大值就应该是默认值了,也就是10。然后调用ensureExplicitCapacity,是不是很眼熟呀,这不就是之前所将的扩容吗。初始化了一个长度为10的数组。其实,之前版本ArrayList的new ArrayList()是会初始化一个长度为10的数组的,之所以移除,可能是考虑到节省空间。目前的设计体现了一种懒加载的思想,当用的时候再去分配空间。

    那么,如果我们再向list里添加水果名称会发生什么呢?当我们再添加时 ensureExplicitCapacity 方法的if条件是false,不会再分配空间。但,当我们填第10个的时候,我们当前的对象就装满,怎么办呢,当然要换个大一点的来装呀。这时执行grow方法进行扩容。通过上面的grow方法我们知道,grow会准备一个长度为15的对象来装我们的水果,这样就可以继续装了。为什么会分配当前长度的1.5倍的容量呢?考虑一下,如果每次只分配比当前长度多一个会发生什么呢?对了,以后再添加就会继续分配空间,扩容可不是一个快的操作,会减慢add的执行速度。所以我就多分配点给你用,避免反复扩容。但也不能分配的太大,造成空间浪费,因此才制定了这个游戏规则。

    我们还能够通过add(index, element)方法在index前添加一个元素,如下

    public void testArrayList() {
            ArrayList<String> list = new ArrayList<>();
            list.add("苹果");
            list.add("香蕉");
            list.add("草莓");
            list.add("水蜜桃");
            list.add("菠萝");
            list.add("葡萄");
            list.add("香蕉");
            list.add(0, "香蕉");
            System.out.println(list.toString());
    }

    输出:[香蕉, 苹果, 香蕉, 草莓, 水蜜桃, 菠萝, 葡萄, 香蕉]

    这里会有一个大家很容易出现的错误,如果我们执行下面代码会发生什么呢?ArrayList还会自动分配空间吗?

    public void testArrayList() {
            ArrayList<String> list = new ArrayList<>();
            list.add(1, "葡萄");
    }

    答案是不会了这样做会抛出一个数组越界的异常。那我们自己分配空间呢,如下代码,设置一个长为10的数组。

    public void testArrayList() {
            ArrayList<String> list = new ArrayList<>(10);
            list.add(1, "葡萄");
    }

    结果会怎么样呢?运行一下,竟然还会抛出异常。让我们看看怎么回事吧

    public void add(int index, E element) {
            rangeCheckForAdd(index);
    
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            System.arraycopy(elementData, index, elementData, index + 1,
                             size - index);
            elementData[index] = element;
            size++;
    }
    
    
    private void rangeCheckForAdd(int index) {
            if (index > size || index < 0)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    结果显而易见,你的异常就是这么抛出来的。虽然ArrayList是对数组的封装,但是这和数组的用法上还是有点区别的。

    4.2、set方法

    set方法能够修改指定位置的值,测试代码如下:

    public void testArrayList() {
            ArrayList<String> list = new ArrayList<>(10);
            list.add("葡萄");
            list.add("苹果");
            list.set(0, "香蕉");
            System.out.println(list.toString());
     }

    返回结果为:[香蕉, 苹果],再来看一下set()方法

    public E set(int index, E element) {
            rangeCheck(index);
    
            E oldValue = elementData(index);
            elementData[index] = element;
            return oldValue;
    }
    
    private void rangeCheck(int index) {
            if (index >= size)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    我们可以看到set方法就是将某个位置的元素换成传入的值,并将原来的值返回。

    4.3 remove(int index)和remove(Object o)

    再来看看移除元素的代码

    public E remove(int index) {
            rangeCheck(index);
    
            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;
    }

    其中System.arraycopy(elementData, index+1, elementData, index, numMoved);是用来将后面的元素前移

    五、结语

    本文主要对ArrayList原理进行介绍,着重介绍了ArrayList的增加、扩容机制、获取元素、修改元素、删除时元素移动的方式。

    如果本文对你的学习有帮助,请给一个赞吧,这会是我最大的动力。

     

    参考资料:
    Java集合 ArrayList原理及使用
    https://www.cnblogs.com/LiaHon/p/11089988.html

     

     

     

     

     

     

  • 相关阅读:
    异常作业
    多态作业
    封装和继承作业
    类和对象作业
    多重循环、方法作业
    选择语句+循环语句作业
    数据类型和运算符作业
    初识Java作业
    C 数据结构堆
    C基础 旋转数组查找题目
  • 原文地址:https://www.cnblogs.com/ChenBingJie123/p/12626557.html
Copyright © 2020-2023  润新知