时间:2021-05-20
本篇分析ArrayList的源码,在分析之前先跟大家谈一谈数组。数组可能是我们最早接触到的数据结构之一,它是在内存中划分出一块连续的地址空间用来进行元素的存储,由于它直接操作内存,所以数组的性能要比集合类更好一些,这是使用数组的一大优势。但是我们知道数组存在致命的缺陷,就是在初始化时必须指定数组大小,并且在后续操作中不能再更改数组的大小。在实际情况中我们遇到更多的是一开始并不知道要存放多少元素,而是希望容器能够自动的扩展它自身的容量以便能够存放更多的元素。ArrayList就能够很好的满足这样的需求,它能够自动扩展大小以适应存储元素的不断增加。它的底层是基于数组实现的,因此它具有数组的一些特点,例如查找修改快而插入删除慢。本篇我们将深入源码看看它是怎样对数组进行封装的。首先看看它的成员变量和三个主要的构造器。
//默认初始化容量private static final int DEFAULT_CAPACITY = 10;//空对象数组private static final Object[] EMPTY_ELEMENTDATA = {};//对象数组private transient Object[] elementData;//集合元素个数private int size;//传入初始容量的构造方法public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } //新建指定容量的Object类型数组 this.elementData = new Object[initialCapacity];}//不带参数的构造方法public ArrayList() { super(); //将空的数组实例传给elementData this.elementData = EMPTY_ELEMENTDATA;}//传入外部集合的构造方法public ArrayList(Collection<? extends E> c) { //持有传入集合的内部数组的引用 elementData = c.toArray(); //更新集合元素个数大小 size = elementData.length; //判断引用的数组类型, 并将引用转换成Object数组引用 if (elementData.getClass() != Object[].class) { elementData = Arrays.copyOf(elementData, size, Object[].class); }}可以看到ArrayList的内部存储结构就是一个Object类型的数组,因此它可以存放任意类型的元素。在构造ArrayList的时候,如果传入初始大小那么它将新建一个指定容量的Object数组,如果不设置初始大小那么它将不会分配内存空间而是使用空的对象数组,在实际要放入元素时再进行内存分配。下面再看看它的增删改查方法。
//增(添加)public boolean add(E e) { //添加前先检查是否需要拓展数组, 此时数组长度最小为size+1 ensureCapacityInternal(size + 1); //将元素添加到数组末尾 elementData[size++] = e; return true;}//增(插入)public void add(int index, E element) { //插入位置范围检查 rangeCheckForAdd(index); //检查是否需要扩容 ensureCapacityInternal(size + 1); //挪动插入位置后面的元素 System.arraycopy(elementData, index, elementData, index + 1, size - index); //在要插入的位置赋上新值 elementData[index] = element; size++;}//删public E remove(int index) { //index不能大于size rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) { //将index后面的元素向前挪动一位 System.arraycopy(elementData, index+1, elementData, index, numMoved); } //置空引用 elementData[--size] = null; return oldValue;}//改public E set(int index, E element) { //index不能大于size rangeCheck(index); E oldValue = elementData(index); //替换成新元素 elementData[index] = element; return oldValue;}//查public E get(int index) { //index不能大于size rangeCheck(index); //返回指定位置元素 return elementData(index);}每次添加一个元素到集合中都会先检查容量是否足够,否则就进行扩容,扩容的细节下面会讲到。我们先看具体增删改查要注意的地方。
增(添加):仅是将这个元素添加到末尾。操作快速。
增(插入):由于需要移动插入位置后面的元素,并且涉及数组的复制,所以操作较慢。
删:由于需要将删除位置后面的元素向前挪动,也会设计数组复制,所以操作较慢。
改:直接对指定位置元素进行修改,不涉及元素挪动和数组复制,操作快速。
查:直接返回指定下标的数组元素,操作快速。
通过源码看到,由于查找和修改直接定位到数组下标,不涉及元素挪动和数组复制所以较快,而插入删除由于要挪动元素,涉及到数组复制,操作较慢。并且每次添加操作还可能进行数组扩容,也会影响到性能。下面我们看看ArrayList是怎样动态扩容的。
每次添加元素前会调用ensureCapacityInternal这个方法进行集合容量检查。在这个方法内部会检查当前集合的内部数组是否还是个空数组,如果是就新建默认大小为10的Object数组。如果不是则证明当前集合已经被初始化过,那么就调用ensureExplicitCapacity方法检查当前数组的容量是否满足这个最小所需容量,不满足的话就调用grow方法进行扩容。在grow方法内部可以看到,每次扩容都是增加原来数组长度的一半,扩容实际上是新建一个容量更大的数组,将原先数组的元素全部复制到新的数组上,然后再抛弃原先的数组转而使用新的数组。至此,我们对ArrayList中比较常用的方法做了分析,其中有些值得注意的要点:
1. ArrayList底层实现是基于数组的,因此对指定下标的查找和修改比较快,但是删除和插入操作比较慢。
2. 构造ArrayList时尽量指定容量,减少扩容时带来的数组复制操作,如果不知道大小可以赋值为默认容量10。
3. 每次添加元素之前会检查是否需要扩容,每次扩容都是增加原有容量的一半。
4. 每次对下标的操作都会进行安全性检查,如果出现数组越界就立即抛出异常。
5. ArrayList的所有方法都没有进行同步,因此它不是线程安全的。
6. 以上分析基于JDK1.7,其他版本会有些出入,因此不能一概而论。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。
本文基于jdk1.8进行分析。LinkedList和ArrayList都是常用的java集合。ArrayList是数组,Linkedlist是链表,是双向链表。
Java中模仿源码自定义ArrayList最近看了下ArrayList的源码,抽空根据ArrayList的底层结构写了一个功能简单无泛型的自定义ArrayLsi
List是Java中比较常用的集合类,关于List接口有很多实现类,本文就来简单介绍下其中几个重点的实现ArrayList、LinkedList和Vector之
1.ArrayList源码和多线程安全问题分析在分析ArrayList线程安全问题之前,我们线对此类的源码进行分析,找出可能出现线程安全问题的地方,然后代码进行
Java集合删除元素ArrayList实例详解AbstractCollection集合类中有一个remove方法,该方法为了适配多种不同的集合,允许删除空的元素