前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【Java】基础篇-ArrayList

【Java】基础篇-ArrayList

作者头像
haoming1100
发布2019-05-17 11:15:45
6800
发布2019-05-17 11:15:45
举报
文章被收录于专栏:步履前行

说到面试,高频的当属 Java 的集合类了,这是完全绕不开的一道大坎,而且里面包含了许多的数据结构。而在我们的平常使用中,ArrayList 几乎可以说是随处可见,尤其是对刚入行的朋友们来说,ArrayList 可以说是万金油了,今天我们就来好好的看看它 里面到底有些啥,平常我们的使用又该怎么注意。

注意,系列文章源码使用 Java 8 !


正文开始

在使用一个类的时候,我们首先是要实例化,那么我们先看 ArrayList 的构造方法。

构造函数:

代码语言:javascript
复制
// 指定初始容量的构造方法   
public ArrayList(int initialCapacity) {}

// 使用默认容量的构造方法  
public ArrayList() {}


//把 Collection 参数 当作参数初始化的构造方法,如果参数为空,则使用默认的容量初始化
public ArrayList(Collection<? extends E> c) {}

看完了构造方法,我们接下来看下主要的字段

代码语言:javascript
复制
    /**
     * 默认初始容量
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 用于空实例的共享空数组实例
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 用于默认大小的空实例的共享空数组实例。与EMPTY_ELEMENTDATA区分开来,以便在添加第一个元素时知道要扩容多少
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 主要存储结构。 ArrayList的容量是该数组的长度。
     * 添加第一个元素时,任何带有elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空 ArrayList都将扩展容量为 DEFAULT_CAPACITY,即默认容量 10
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * 这个 size 就是 上面数组的长度
     */
    private int size;

而我们对 ArrayList 的各种操作,其实就是在操作 elementData 数组。

接下来我们看我们最常用的 add 方法和 remove 以及 get 方法。


add 方法

代码语言:javascript
复制
    /**
     * 元素追加到此列表的末尾
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    /**
     * 将指定元素插入此*列表中的指定位置
     */
    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++;
    }

上述方法是添加一个元素的操作

代码语言:javascript
复制
    /**
     * 将指定集合中的所有元素追加到列表的末尾,按顺序返回一个指定的 collection 迭代器
     * (如果在操作正在进行时修改了指定的集合,则此操作的行为是 undefined)
     */
    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

    /**
     * 从指定位置开始,将指定集合中的所有元素插入到列表的末尾,如果当前位置有元素,则后移,增加索引
     */
    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        // 更新索引操作
        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

上述方法是添加一个集合的操作


1、 add()
代码语言:javascript
复制
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 数组赋值
        elementData[size++] = e;
        return true;
    }

我们来看第一个 add 方法,在该方法中,它首先调用 ensureCapacityInternal(size + 1) 来确保数组的容量是足够的,然后给指定索引下赋值,返回 true。我们来看下 ensureCapacityInternal 代码:

代码语言:javascript
复制
   /**
    * 维护 Object[] elementData 的容量大小
    */
    private void ensureCapacityInternal(int minCapacity) {
       ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
   }
   /**
   * 计算容量
   */
   private static int calculateCapacity(Object[] elementData, int minCapacity) {
       // 如果当前的 Object[] elementData 是 空的
       if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
           //那么容量就是 在传入的容量和默认初始容量中选择一个最大值
           // 所以 ArrayList 初始化后,其实只是把容量的大小确定了,而并没有去真正的实例化一个数组
           return Math.max(DEFAULT_CAPACITY, minCapacity);
       }
       return minCapacity;
   }

  /**
   * 通过拿到具体的容量大小后,这里才真正的保证容量的大小
   */
   private void ensureExplicitCapacity(int minCapacity) {
       // 这个是对当前数组的变更次数的更新,每次添加,删除都会对该变量进行更新,我们在下边会将到该字段的具体作用
       modCount++;

       // 如果容量小于当前数组的长度,那么进行扩容
       if (minCapacity - elementData.length > 0)
       // 具体的扩容规则
           grow(minCapacity);
   }   

   /**
    * 具体的扩容规则
    */
   private void grow(int minCapacity) {
       // 获取到当前数组的容量,这里可以看出,调用的是数组 length 方法计算长度
       int oldCapacity = elementData.length;
       // oldCapacity >> 1 代表是当前容量 除以2,位运算是高效操作
       // 所以扩容的长度,就是当前长度的 1.5倍
       int newCapacity = oldCapacity + (oldCapacity >> 1);
       // 如果计算出的新的长度还是小于需要扩容的长度,那么直接与扩容的长度为准
       if (newCapacity - minCapacity < 0)
           newCapacity = minCapacity;
       // 如果计算出的新的长度比 数组的最大长度都大了,那么进行最大长度的计算,(数组的最大长度为 Integer.MAX_VALUE - 8 , 即 0x7fffffff (2 的 31次 -1 ) - 8)
       if (newCapacity - MAX_ARRAY_SIZE > 0)
           // 最大容量的判断
           newCapacity = hugeCapacity(minCapacity);
       // 数组拷贝
       // 这里使用原数组和 newCapacity 这个最小容量 进行 copy, 因为最小容量更接近数组
       // 使用的 Arrays 类进行的 copy,其最后调用的 仍然是 System.arraycopy 。
       elementData = Arrays.copyOf(elementData, newCapacity);
   }
   
   /**
    * 最大长度计算,只是一个最大容量的判断
    */
   private static int hugeCapacity(int minCapacity) {
       if (minCapacity < 0) // overflow
           throw new OutOfMemoryError();
       // 最小容量大于数组最大容量,则用 Inter 的最大长度,反之,用最大容量
       return (minCapacity > MAX_ARRAY_SIZE) ?
           Integer.MAX_VALUE :
           MAX_ARRAY_SIZE;
   }

看完上面的 add 方法的具体实现,想必大家都很清晰了,我们再来总结下,添加一个元素 -> 确保容量 -> 数组 copy。很简单!

2、 add(int index, E element)
代码语言:javascript
复制
public void add(int index, E element) {
        // index 的验证
        rangeCheckForAdd(index);
        // 确保容量
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //系统的底层 拷贝
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

这个 add 方法和上面的唯一区别就是 index 的验证 和 系统底层 copy 的方法使用不同,其他一样。

3、 add(Collection<? extends E> c)
代码语言:javascript
复制
/**
 * 在列表的末尾追加指定的集合
 */
 public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        //容量的确保
        ensureCapacityInternal(size + numNew);  // Increments modCount
        // 底层 copy
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }
4、 add(int index, Collection<? extends E> c)
代码语言:javascript
复制
/**
 * 在指定位置追加集合
 */
 public boolean addAll(int index, Collection<? extends E> c) {
        //check 追加的索引
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        //q确保容量
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        //判断是否是数组中间进行插入
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

get 方法

代码语言:javascript
复制
    /**
     * 返回一个指定下标的元素
     */
    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }
    /**
     * 检查下标
     */
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

get 方法很简单,主要是检查下标,然后直接返回。


remove 方法

在 Java 8 中,remove 方法主要有以下4种,供我们使用:

代码语言:javascript
复制
public E remove(int index) {}

public boolean remove(Object o) {}

public boolean removeAll(Collection<?> c) {}

public boolean removeIf(Predicate<? super E> filter) {}

我们来看具体源码

代码语言:javascript
复制
    /**
     * 移除指定下标的元素
     * 移除后,指定位置的元素向左移一位
     */
    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);
        // index 下的元素 删除,即变更为null
        elementData[--size] = null; // clear to let GC do its work
    
        return oldValue;
    }

普通移除下标的方法很简单,主要是检查下标是否在范围内,然后调用系统底层函数进行数组的 copy,然后把传入的下标值设置为 null,等待 GC 即可。

所以 我们在移除一个下标元素的时候,如果下标越靠前,那么花费的时间越大,即最大时间为 O(n),而下标越靠后,则花费时间越少,最小时间为O(1)。

代码语言:javascript
复制
  /**
     * 移除第一个匹配到的元素,ArrayList 是可以插入 Null 的
     * 
     */
    public boolean remove(Object o) {
    //优先判断 Null 值
        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;
    }

移除指定元素的时候,由于 ArrayList 是可以插入Null 值的,所以需要先判断 Null 的情况;在移除指定元素的时候,调用了 fastRemove() ,如下:

代码语言:javascript
复制
/*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    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
    }

就是一个简单的调用底层数组拷贝,移动下标,然后把相关下标下的元素设为 null。

代码语言:javascript
复制
    /**
     * 删除集合中包含的所有元素
     */
     public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }
    
     /**
     * 批量删除 
     */
    private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData;
        //r 是记录整个数组下标的, w是记录有效元素索引的
        int r = 0, w = 0;
        boolean modified = false;
        try {
            //便利数组
            for (; r < size; r++)
                //如果complement为false  相当于是取c在elementData中的补集,集合 c 包含则不记录,集合 c不包含则记录
                //complement为true  相当于是取 c 和 elementData 的交集,c包含则记录,c不包含则不记录 
                if (c.contains(elementData[r]) == complement)
                //r是正在遍历的位置  w是用于记录有效元素的   在w之前的全是有效元素,w之后的会被"删除"
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            // 如果上面在遍历的过程中出错了,那么r肯定不等于size,于是源码就将出错位置r后面的元素全部放到w后面
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            //  如果w是不等于size,那么说明是需要删除元素的    否则不需要删除任何元素
            if (w != size) {
                // clear to let GC do its work
                // 将w之后的元素全部置空  因为这些已经没用了,置空方便GC回收
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                 // 记录当前有效元素
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }

在删除一个Collection集合的时候,从上面代码就可以看出,很长。其中我加了相关注释,方便大家阅读。其实它的主要操作就下面几点:

  • 集合 c 的判断
  • 调用内部的 batchRemove() 去删除本列表与 集合 c 的交集
  • 循环便利,记录 c 中没有的元素,放到数组的前面,这个时候的临界点下标就是w,w 前面是留下的数组,后面是要删除的数组
  • 在 第3 步出错的情况下,不删除元素,即跳到 finally 语句块
  • 最后一步就是把 w之后的元素 设置为null,然后把 size 大小设置为 w,修改modCount

其实这个操作看起来麻烦,说白了就是

  • 判断参数
  • 便利交集
  • 把无交集的下标元素,放到前面,然后这个下标位置设置为 w,之后的是有交集的,需要删除
  • 删除元素,修改相关size 和modCount
代码语言:javascript
复制
/**
     * 按照一定规则过滤集合中的元素
     * @Param filter ,过滤规则
     */
    @Override
    public boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        // figure out which elements are to be removed
        // any exception thrown from the filter predicate at this stage
        // will leave the collection unmodified
        int removeCount = 0;
        // 创建一个 与本List 大小相同的位集
        final BitSet removeSet = new BitSet(size);
        //预期的修改次数
        final int expectedModCount = modCount;
        final int size = this.size;
        //循环增加下标到位集中
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            @SuppressWarnings("unchecked")
            final E element = (E) elementData[i];
            //lambda 表达式进行判断
            if (filter.test(element)) {
            //增加下标到位集中
                removeSet.set(i);
                removeCount++;
            }
        }
        //这里判断如果修改次数不是预期次数,直接报并发修改异常
        //也就是防止在外界使用List的时候,如果for 循环或者是 while 循环删除元素,直接报错
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }

        // shift surviving elements left over the spaces left by removed elements
        //这里判断,如果传入的 Lambad 表达式过滤出结果,继续执行
        final boolean anyToRemove = removeCount > 0;
        if (anyToRemove) {
            final int newSize = size - removeCount;
            //这里的一个小算法就是把过滤出的元素,替换到后面
            for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
                i = removeSet.nextClearBit(i);
                elementData[j] = elementData[i];
            }
            //找到的元素,清空
            for (int k=newSize; k < size; k++) {
                elementData[k] = null;  // Let gc do its work
            }
            this.size = newSize;
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
            modCount++;
        }

        return anyToRemove;
    }

至于使用 Lambda 方法删除元素,我们在看了上面的 removeAll 方法后,就比较简单了:

  • 创建位集
  • 记录匹配的下标到位集中
  • 判断是否被外界函数修改
  • 判断是否有过滤结果
    • 把匹配到的元素放到数组后面
    • 进行移除
    • 判断是否被外界函数修改

我们在经历了上面的摧残后,再看下面的代码就很easy了:

clear 方法

代码语言:javascript
复制
public void clear() {
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

clear 方法就是循环设置为null。


set 方法

代码语言:javascript
复制
public E set(int index, E element) {
        //判断下标
        rangeCheck(index);
        //获取旧值
        E oldValue = elementData(index);
        //设置新值
        elementData[index] = element;
        return oldValue;
    }

ArrayList 中的其他方法

ArrayList 中还有一些其他方法,包括返回数组,返回只读集合,我们一起来看下:

返回数组 toArray
代码语言:javascript
复制
    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }
    
    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }
  • 第一个方法返回的是 Object 类型的数组
  • 第二个方法返回额是 对应类型的数组,如果参数数组长度够大,就用该数组,否则新建一个数组返回

优化数组大小的 trimToSize
代码语言:javascript
复制
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

这个方法会重新分配一个数组,以节省空间。


通过iterator()遍历 修改操作

首先我们看下概念:fail-fast

fail-fast 机制是java集合(Collection)中的一种错误机制,当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

这个fail-fast 体现在我们上面经常提到的一个变量 ==modCount== 。

==ConcurrentModificationException== :

当不允许进行此类修改时,检测到对象的并发修改的方法可能抛出此异常

代码语言:javascript
复制
    /**
     * ArrayList 内部的 Iterator
     */
    private class Itr implements Iterator<E> {
        //下一个元素的指引
        int cursor;
        //当前访问的最后一个元素的索引
        int lastRet = -1; 
        int expectedModCount = modCount;

        Itr() {}
        //判断是否有下一个元素
        public boolean hasNext() {
            return cursor != size;
        }

        //获取下一个元素
        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];
        }

        //移除元素
        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            //判断本列表是否被其他线程修改
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                //这里是移除的核心,要 expectedModCount 和  modCount 相等
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
        
        //遍历列表
        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }
        //在遍历过程中判断是否被其他线程修改
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

小结

ArrayList 内部是动态数组实现,所以:

  • 随机访问,效率很高, 为O(1)
  • 在非排序的情况下,效率很低,为 O(N)
  • 添加元素效率一般,虽然在数组末尾添加的效率为O(1),头部添加的效率为O(N),但是重新分配和复制数组的开销被平摊了
  • 插入和删除 为 O(N),毕竟要移动数组
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-05-14 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 正文开始
  • add 方法
    • 1、 add()
      • 2、 add(int index, E element)
        • 3、 add(Collection<? extends E> c)
          • 4、 add(int index, Collection<? extends E> c)
          • get 方法
          • remove 方法
          • clear 方法
          • set 方法
          • ArrayList 中的其他方法
            • 通过iterator()遍历 修改操作
            • 小结
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档