ArrayList可以说是Java开发中最常用的集合容器了,今天就来分析一下ArrayList的源码和注意点,可以更加深入的理解ArrayList实现原理。

文章目录

  • 概述
  • 常用方法
  • 源码分析
  • ArrayList相关
  • 删除问题
    • 使用Iterator的remove()方法
    • 使用for循环正序遍历
  • 本文小结

概述

arrayList的继承类图

本文基于JDK1.8


常用方法

arrayList中的方法比较多,下面给出比较常用的一些方法

boolean add(E e)

将指定的元素添加到此列表的尾部。

void add(int index, E element)

将指定的元素插入此列表中的指定位置。

boolean addAll(Collection c)

按照指定 collection 的迭代器所返回的元素顺序,将该 collection 中的所有元素添加到此列表的尾部。

boolean addAll(int index, Collection c)

从指定的位置开始,将指定 collection 中的所有元素插入到此列表中。

void clear()

移除此列表中的所有元素。

Object clone()

返回此 ArrayList 实例的浅表副本。

boolean contains(Object o)

如果此列表中包含指定的元素,则返回 true。

void ensureCapacity(int minCapacity)

如有必要,增加此 ArrayList 实例的容量,以确保它至少能够容纳最小容量参数所指定的元素数。

E get(int index)

返回此列表中指定位置上的元素。

int indexOf(Object o)

返回此列表中首次出现的指定元素的索引,或如果此列表不包含元素,则返回 -1。

boolean isEmpty()

如果此列表中没有元素,则返回 true

int lastIndexOf(Object o)

返回此列表中最后一次出现的指定元素的索引,或如果此列表不包含索引,则返回 -1。

E remove(int index)

移除此列表中指定位置上的元素。

boolean remove(Object o)

移除此列表中首次出现的指定元素(如果存在)。

protected void removeRange(int fromIndex, int toIndex)

移除列表中索引在 fromIndex(包括)和 toIndex(不包括)之间的所有元素。

E set(int index, E element)

用指定的元素替代此列表中指定位置上的元素。

int size()

返回此列表中的元素数。

Object[] toArray()

按适当顺序(从第一个到最后一个元素)返回包含此列表中所有元素的数组。

T[] toArray(T[] a)

按适当顺序(从第一个到最后一个元素)返回包含此列表中所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。

void trimToSize()

将此 ArrayList 实例的容量调整为列表的当前大小。


源码分析

成员变量

// 初始容量:10private static final int DEFAULT_CAPACITY = 10;
// 空数组,没有元素数据
private static final Object[] EMPTY_ELEMENTDATA = {};
// 空数组,默认容量为空,没有元素数据
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 数组,用来存储ArrayList的元素transient Object[] elementData;
// size为ArrayList的大小,在elementData不为空数组的情况下,size是小于elementData.length的private int size;

根据elementData也能看出来,ArrayList的内部是通过数组来实现的,ArrayList对元素的增删改查实际上都是对数组的操作。

ArrayList的构造函数

public ArrayList(int initialCapacity) {if (initialCapacity > 0) {this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {this.elementData = EMPTY_ELEMENTDATA;} else {throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);}
}public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}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;}
}

构造ArrayList时,可以指定容器的初始容量initialCapacity,构造一个给定初始大小的数组作为数据集;使用无参构造时,则默认容量为空的数组作为初始数据集;也可以使用其他任意的集合Collection作为构造参数,可以看到,源码中就是直接将集合c转换数组来作为数据集(如果数据集是非Object数组,比如多维数组,则将元素拷贝到数据集数组中)。ArrayList的构造实际上就是对其内部数组的初始化。

add方法

public boolean add(E e) {// 确保当前数据集数组能够放得下新加入的元素ensureCapacityInternal(size + 1);  // Increments modCount!!// 将列表大小size自增1,并在数据集数组中放入元素eelementData[size++] = e;return true;
}public void add(int index, E element) {// 检查新加入的位置index是否越界rangeCheckForAdd(index);// 确保当前数据集数组能够放得下新加入的元素,如果需要扩容的话就扩容ensureCapacityInternal(size + 1);  // Increments modCount!!// 将index位置及后面的元素都向后移动一位System.arraycopy(elementData, index, elementData, index + 1,size - index);// 将index位置的元素设置为新建如的elementelementData[index] = element;size++;
}

在添加元素时,如果不指定加入的位置,会添加到内部数组中已有元素的最后一位,也就是添加到了ArrayList的末尾。如果指定了添加位置index,判断index是否越界,是否需要扩容,最后移动index位置后的元素,并将index位置设置为新添加的元素。

需要注意的是,添加的元素并没有判空,所以ArrayList中的元素是可以为null的。

在add方法中,都调用了ensureCapacityInternal(int minCapacity)这个方法来确保数据集数组能够放得下新的元素:

private void ensureCapacityInternal(int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);}ensureExplicitCapacity(minCapacity);
}private void ensureExplicitCapacity(int minCapacity) {modCount++;// overflow-conscious code// 如果添加新元素需要的最小容量大于数组的长度,就需要扩容if (minCapacity - elementData.length > 0)grow(minCapacity);
}

看下扩容的方法grow(int minCapacity)

private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length;// 扩展至新的容量newCapacity为旧的容量的1.5倍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);
}

数组扩容的过程,实际上是新建了一个需要扩容的长度的数组,然后将原素组中的元素拷贝到这个新建的数组中,新的数组指定为ArrayList内部数据集数组。

总结:ArrayList在添加元素时,首先会判断添加的位置是否在内部数组中越界,如果越界,抛出异常;如果没有越界,则判断数组能否放得下新添加的元素,如果放得下,则直接存放到数组中;如果放不下,则将数组扩容,扩容后再存放到数组中

remove方法:

public E remove(int index) {// 检查越界rangeCheck(index);modCount++;// 需要移除的元素E oldValue = elementData(index);// 需要移动位置的元素的数量int numMoved = size - index - 1;// 将需要移除元素的位置后的所有元素复制到index位置开始后的numMoved个位置if (numMoved > 0)System.arraycopy(elementData, index+1, elementData, index,numMoved);// size减1,并将之前的最后一个位置元素置空elementData[--size] = null; // clear to let GC do its workreturn oldValue;
}

remove还有一个重载的方法,是移除给定的元素,它的实现就是遍历数组,找到元素的索引值,然后调用remove(int index)方法,根据索引值去删除。

总结:ArrayList在删除元素时,根据删除的索引值判断是否越界,如果越界,抛出异常;如果没有越界,取出要删除的元素,然后将这个元素后面所有的元素向前移动一位。

set方法

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

set方法即修改列表中指定位置的元素值。它的实现非常简单:直接修改数组指定位置的值。

get方法

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

get方法实现非常简单只需要堆获取位置判断是否越界,然后直接从数组中取值即可。

从上面分析的源码中可以看出,ArrayList的实现就是对数组的操作,在添加和删除元素的时候,会涉及到数组的扩容和数组元素位置的移动,相对查询和修改元素要复杂一些,所以ArrayList适合用在查询和修改比较频繁,而添加和删除较少的情况下。


ArrayList相关

ArrayList和LinkedList的区别

通过阅读util包里面的源码可以很容易的看出两者的大致区别:
ArrayList是一种具有动态扩容特性且基于数组基础的数据结构,而LinkedList则是一种基于链表的数据结构。

在进行元素查找的时候适合用ArrayList进行操作,在进行元素的添加和删除的时候适合用LinkedList。由于ArrayList是采用数组作为数据结构的,因此在进行查找的时候只需要根据下标的索引进行判断即可。

LinkedList数据结构则是采用链表的结构进行设计的,因此在查找的时候需要进行逐一比较,所以效率会比较慢(并非是全链查询,而是采用了折半搜索的方式来进行优化)。在添加或者删除元素的时候,由于ArrayList需要进行元素的位置移动,而链表的移动和删除只需要将链表节点的头尾指针进行修改即可。

ArrayList的动态扩容有什么特点

当我们在进行ArrayList的插入元素时候,相应的元素会被插入到动态数组里面,但是由于数组本身所能存储的数据量是有限制的,因此在插入数据的时候,需要进行相应的动态扩容,在看源码的时候,可以看到相应的代码部分:

add操作源码

/**2     * Appends the specified element to the end of this list.3     *4     * @param e element to be appended to this list5     * @return <tt>true</tt> (as specified by {@link Collection#add})6     */7    public boolean add(E e) {8        ensureCapacityInternal(size + 1);  // Increments modCount!!9        elementData[size++] = e;
10        return true;
11    }

核心扩容部分的实现

private void ensureCapacityInternal(int minCapacity) {2        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {3            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);4        }56        ensureExplicitCapacity(minCapacity);7    }89    private void ensureExplicitCapacity(int minCapacity) {10        modCount++;
11
12        // overflow-conscious code
13        if (minCapacity - elementData.length > 0)
14            grow(minCapacity);
15    }

ArrayList默认数组的大小为10,扩容的时候采用的是采用移位运算

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

这里也可以看出ArrayList的扩容因子为1.5。(4>>1 就相当于4除以2 即缩小了一半)。

什么时候会选择使用ArrayList

这又是一个大多数面试者都会困惑的问题。多数情况下,当遇到访问元素比插入或者是删除元素更加频繁的时候,应该使用ArrayList。另外一方面,当在某个特别的索引中,插入或者是删除元素更加频繁,或者压根就不需要访问元素的时候,不妨考虑选择LinkedList。

这里的主要原因是,在ArrayList中访问元素的最糟糕的时间复杂度是O(1),而在LinkedList中可能就是O(n)了。在ArrayList中增加或者删除某个元素时候,如果触发到了扩容机制,那么底层就会调用到System.arraycopy方法,如果有兴趣深入挖掘jdk源码的话,会发现这是一个本地调用方法,被native修饰,该方法会直接通过内存复制,省去了大量的数组寻址访问等时间,但是相比于LinkedList而言,在频繁的修改元素的情况下,选用LinkedList的性能会更加好一点。

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {2        @SuppressWarnings("unchecked")3        T[] copy = ((Object)newType == (Object)Object[].class)4            ? (T[]) new Object[newLength]5            : (T[]) Array.newInstance(newType.getComponentType(), newLength);6        //复制集合7        System.arraycopy(original, 0, copy, 0,8                         Math.min(original.length, newLength));9        return copy;
10    }

如果读者有去学习过jvm的话,应该会对“内存碎片“这个名词比较熟悉。基于数组结构的数据在存储信息的时候都需要有连续的内存空间,所以如果当内存碎片化情况较为严重的时候,可能在使用ArrayList的时候会有OOM的异常抛出。

如何复制某个ArrayList到另一个ArrayList中去

1.使用clone()方法,比如ArrayList newArray = oldArray.clone();
2.使用ArrayList构造方法,比如:ArrayList myObject = new ArrayList(myTempObject);
3.使用Collection的copy方法。

关于list集合的拷贝问题在面试中可能还会引申出深拷贝和浅拷贝的对比。

请说说ArrayList、Vector和LinkedList的区别

相同点

这三者都是单列集合Collection下List集合的实现类,所以他们的共同点,元素有序,允许重复元素 。

不同点

1.ArrayList和Vector底层都是数组实现,这样的实现注定查找快、增删慢 。
2.Vector支持线程同步,是线程访问安全的,ArrayList线程不安全 。
3.LinkedList底层是链表结构,查找元素慢、增删元素速度快,线程不安全。

modCount参数的意义

在ArrayList设计的时候,其实还包含有了一个modCount参数,这个参数需要和expectedModCount 参数一起使用,expectedModCount参数在进行修改的时候会被modCount进行赋值操作,当多个线程同时对该集合中的某个元素进行修改之前都会进行expectedModCount 和modCount的比较操作,只有当二者相同的时候才会进行修改,两者不同的时候则会抛出异常。这个机制也被称之为fail-fast机制

COW容器

jdk1.5之前,由于常用的ArrayList并不具有线程安全的特性,因此在1.5之后的并发包里面出现了CopyOnWrite容器,简称为COW。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

并发包juc里面的CopyOnWriteArrayList中,核心原理主要是通过加入了ReentrantLock来保证线程安全性,从而解决了ArrayList的线程安全隐患问题。

相应的add操作源码如下

/**2     * Appends the specified element to the end of this list.3     *4     * @param e element to be appended to this list5     * @return {@code true} (as specified by {@link Collection#add})6     */7    public boolean add(E e) {8        final ReentrantLock lock = this.lock;9        lock.lock();
10        try {11            Object[] elements = getArray();
12            int len = elements.length;
13            Object[] newElements = Arrays.copyOf(elements, len + 1);
14            newElements[len] = e;
15            setArray(newElements);
16            return true;
17        } finally {18            lock.unlock();
19        }
20    }

删除问题

使用过ArrayList的人一般都知道,在执行for循环的时候一般情况是不会去执行remove的操作的,因为remove的操作会改变这个集合的大小, 所以会有可能出现数组角标越界异常,我们可以试一下

看代码

package cn.wideth.util.other;import java.util.ArrayList;
import java.util.List;public class Main {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("A");list.add("B");list.add("C");list.add("D");list.add("E");list.add("F");for(String s : list){if(s.equals("D")){list.remove(s);}}System.out.println(list);}
}

程序结果


使用Iterator的remove()方法

看代码

package cn.wideth.util.other;import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;public class Main {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("A");list.add("B");list.add("C");list.add("D");list.add("E");list.add("F");Iterator<String> iterator = list.iterator();while (iterator.hasNext()) {String platform = iterator.next();if (platform.equals("D")) {iterator.remove();}}System.out.println(list);}
}

程序结果


使用for循环正序遍历

看代码

package cn.wideth.util.other;import java.util.ArrayList;
import java.util.List;public class Main {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("A");list.add("B");list.add("C");list.add("D");list.add("E");list.add("F");for (int i = 0; i < list.size(); i++) {String item = list.get(i);if (item.equals("D")) {list.remove(i);i = i - 1;}}System.out.println(list);}
}

程序结果


本文小结

本文详细介绍了arraylist相关的知识点。

深入理解ArrayList相关推荐

  1. java arraylist 实现原理_Java进阶--深入理解ArrayList实现原理

    编辑推荐: 文章主要介绍ArrayList的继承关系,ArrayList的方法使用和源码解析,它提供了动态的增加和减少元素,实现了Collection和List接口,可以灵活的设置数组的大小,希望对您 ...

  2. 深入理解ArrayList 和 LinkedList 区别

    ArrayList的内部实现是基于数组,因此,它使用get方法访问列表中的任意一个元素时,它的速度要比LinkedList快.LinkedList的内部实现是基于链表的,LinkedList中的get ...

  3. 深入理解ArrayList中 toArray(),toArray(T[])方法

    在List类的具体实现类 ArrayList类中,有一个toArray()方法,该方法的作用是将ArrayList类型的对象转换为数组. 该类型有两个方法:1.toArray()  和 2. toAr ...

  4. java 自定义arraylist_Java 中模仿源码自定义ArrayList

    Java 中模仿源码自定义ArrayList 最近看了下ArrayList的源码,抽空根据ArrayList的底层结构写了一个功能简单无泛型的自定义ArrayLsit,帮助自己更好理解ArrayLis ...

  5. arraylist插入数据_集合系列 List(二):ArrayList

    ArrayList 是 List 集合的列表经典实现,其底层采用定长数组实现,可以根据集合大小进行自动扩容. public class ArrayList extends AbstractList i ...

  6. 【集合】源码级深入理解LinkedList,点开即食

    全面解析LinkedList 由ArrayList引发的思考 在讲之前来对比一下ArrayList和LinkedList 首先看看LinkedList的继承实现关系 类内部的成员变量 构造方法 最难的 ...

  7. 数据结构中ArrayList实现杨辉三角

    杨辉三角在数据结构是较为常见的一个模型,对我们理解ArrayList有很大的帮助. 杨辉三角是一个非常特殊的结构,他的第一行是1,每一行的首尾都是1,中间的每一位是上一行的前一位加上一行的这一位. 这 ...

  8. 深入理解 Vector

    文章目录 什么是Vector? Vector与ArrayList有什么区别? 相同点 不同点 Vector中的一些成员变量 Vector中的构造方法有哪些? 一.Vector(int initialC ...

  9. 《阿里巴巴JAVA编码规范》阅读理解

    https://github.com/alibaba/p3c/ https://github.com/singgel?tab=repositories 前言 2017 年阿里云栖大会,阿里发布了针对 ...

最新文章

  1. python中编写无参数decorator
  2. [YTU]_2637(编程题:类---矩形类)
  3. 【小程序】刘一哥课堂随机点名提问神器V1.0(附源程序)
  4. Java 8 StampedLocks与ReadWriteLocks和同步
  5. python中的随机函数random
  6. 简单说说驱动程序设计的入门
  7. Qt文档阅读笔记-编写应用脚本解析与实例
  8. Python 字符串与二进制串的相互转换
  9. 设计软件哪里找?图片素材哪里找?
  10. PyCharm中文指南2.0
  11. 刚创建了蕝薱嚣张IT部落
  12. 再见 FTP/SFTP!是时候拥抱下一代文件传输利器 Croc 了!
  13. catia中尺子没了怎么调出来,【答疑】草图大师sketchup的尺子快捷键是什么呀? - 羽兔网问答...
  14. 【提高组NOIP2007】矩阵取数游戏
  15. FFmpeg开发(1)从mp4中提取aac音频
  16. GitHub---团队合作
  17. web html5音乐播放器设计与实现,基于HTML5技术的音乐播放器的设计与实现
  18. 智能客服选型产品选型比较:晓多、奇智、春松客服
  19. mysql死锁解决方法_mysql出现死锁的原因及解决方案
  20. 如何理解广义线性回归分析Logistic输出的OR值?

热门文章

  1. curl抓取页面每次生成新的session问题
  2. .NET基础架构方法—DataTableToExcel通用方法
  3. Mysql 声明变量
  4. 深入浅出MFC学习笔记 消息
  5. Docker pull镜像报错问题
  6. react-native icon使用方式
  7. Lock-Free 编程
  8. 工信部副部长怀进鹏:信息产业呈现四大发展特点
  9. 【HDU 1150】Machine Schedule(二分图匹配)
  10. Spring MVC遭遇checkbox的问题解决方案