摘要:概述为了弥补普通数组无法自动扩容的不足提供了集合类其中就对数组进行了封装使其可以自动的扩容或缩小长度因为是对数据进行了封装所以底层存储结构是数组结构可以想象的到数组长度的自动变化必须需要开辟新内存然后进行数组元素的拷贝因为数组所以也就具有数
[TOC]
1. 概述为了弥补普通数组无法自动扩容的不足, Java提供了集合类, 其中ArrayList就对数组进行了封装, 使其可以自动的扩容或缩小长度.
因为是对数据进行了封装, 所以底层存储结构是数组结构. 可以想象的到, 数组长度的自动变化必须需要开辟新内存, 然后进行数组元素的拷贝.
因为数组, 所以ArrayList也就具有数组的一些性, 如支持随机访问.
2. 存储结构第一节已经说了, 它是一种自动数组, 内部存储结构就是数组了.
Object[] elementData;3. 构造方法
先从构造方法开始分析.
3-1. 无参构造方法/** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
注释说, 构造一个容量为10的空的list.
但是这里我们并没有看到数组的容量变为10, 而是一个空的数组 DEFAULTCAPACITY_EMPTY_ELEMENTDATA.
那么什么时候会被初始化为10的数组呢?答案是有元素被加入时(add方法).
3-2. 带有初始化容量的构造方法/** * Constructs an empty list with the specified initial capacity. * * @param initialCapacity the initial capacity of the list * @throws IllegalArgumentException if the specified initial capacity * is negative */ 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); } }
这个就比无参构造方法清晰多了, 直接建立一个initialCapacity大小的数组.
3-3. 带有初始化元素的构造方法/** * Constructs a list containing the elements of the specified * collection, in the order they are returned by the collection"s * iterator. * * @param c the collection whose elements are to be placed into this list * @throws NullPointerException if the specified collection is null */ 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; } }
这里的意思传入一个集合类(Collection的子类即可), 转为list.
Collection有一个子抽象类, 默认实现了toArray();, 如果子类比较特殊需要, 进行重写即可.
4. 集合的操作集合类的重要功能就是进行交互(元素的存储、修改、删除、遍历等).
4-1. 添加内部有四种方式的添加:
直接添加
指定位置添加
添加全部
在指定位置添加全部
4-1-1. 普通添加(在尾端添加元素)因为是动态数组, 所以元素添加时需要校验数组空间是否足够, 如果不足, 需要进行数组的扩容(关于扩容看第5节).
源码如下:
/** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return true (as specified by {@link Collection#add}) */ public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }4-1-2. 指定位置添加
在指定位置添加, 必然会影响到该位置的元素以及后续元素, 对于数组这种数据结构, 只能进行元素的后移了.
这就体现出数组这种数据结构的不足了: 对元素的修改不太擅长.
同普通添加元素一样, 需要校验数组容量数组足够, 不过在校验之前还要检查一下入参的元素位置(index)是否在范围内.
然后进行数组元素的移动.
源码如下:
/** * Inserts the specified element at the specified position in this * list. Shifts the element currently at that position (if any) and * any subsequent elements to the right (adds one to their indices). * * @param index index at which the specified element is to be inserted * @param element element to be inserted * @throws IndexOutOfBoundsException {@inheritDoc} */ 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++; }4-1-3. 添加一个集合中的全部元素
与构造方法类似, 这里也使用了toArray();
只不过需要进行数组大小的校验扩容, 然后进行元素拷贝.
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; }4-1-4. 在指定位置添加一个集合中的全部元素
通过上面的说明, 这个方法就很容易懂了.
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; }4-2. 修改
修改操作比较简单, 就是给数组的某个下标重新赋值.
只有一个方法: E set(int index, E element)
将指定位置上的元素进行更新, 会对index进行越界检查.
4-3. 删除删除操作也意味着数组中会有元素移动, 除非删除的是最后一个元素.
删除方法有一下几个:
E remove(int index)
boolean remove(Object o)
boolean removeAll(Collection> c)
boolean retainAll(Collection> c)
4-3-1. 通过下标进行删除删除指定位置上的元素, 如果删除的不是最后一个元素, 则要进行元素的移动.
public E remove(int index) { rangeCheck(index); // 检查下标是否越界 modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; // 最后 -1 是为了数组下标不越界 if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work return oldValue; }4-3-2. 通过对象进行删除
删除数组中的某个元素, 会分为两种情况: null OR !null.
找到元素之后会使用fastRemove(index)进行删除.
源码如下:
// 删除成功返回true, 失败false 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 }4-3-3. 删除集合中的所有元素
传入一个集合c, 如果集合c中包含本集合中的元素, 则对改元素进行处理, 这里利用了一个complement属性, 来决定含有的元素是删除还是保留.
简单说一下批量删除/保留操作, 把匹配的元素(c.contains(elementData[r]) == complement)进行保留.
然后对不需要保留的下标赋予null值.
public boolean removeAll(Collection> c) { Objects.requireNonNull(c); return batchRemove(c, false); } private boolean batchRemove(Collection> c, boolean complement) { final Object[] elementData = this.elementData; int r = 0, w = 0; boolean modified = false; try { for (; r < size; r++) // 如果complement为false, 则c集合包含本集合中的元素, 则不进行操作, 这就是保留不属于c集合中的所有元素. // 这就是 4-3-4. boolean retainAll(Collection> c) if (c.contains(elementData[r]) == complement) elementData[w++] = elementData[r]; } finally { // Preserve behavioral compatibility with AbstractCollection, // even if c.contains() throws. if (r != size) { System.arraycopy(elementData, r, elementData, w, size - r); w += size - r; } if (w != size) { // clear to let GC do its work for (int i = w; i < size; i++) elementData[i] = null; modCount += size - w; size = w; modified = true; } } return modified; }4-4. 查找
这部分就不贴源码了, 很容易理解.
查找可以分为两种情况:
通过下标直接定位元素
通过元素进行定位下标
包含的方法:
boolean contains(Object o)
int indexOf(Object o)
int lastIndexOf(Object o)
E elementData(int index)
E get(int index)
contains(Object o)方法使用了indexOf(Object o)作为底层实现, 我们需要了解indexOf(Object o)的底层实现.
indexOf(Object o)是从数组的头开始查找, 查找到相同元素时, 进行返回, 而lastIndexOf(Object o)正好相反, 从数组的尾开始查找, 查找到相同元素时进行返回.
可以使用indexOf和lastIndexOf进行返回值的比较, 如果相等, 说明该元素在数组中唯一(前提是返回非-1).
elementData(int index)则是内部方法, 获取指定下标处的元素.
而get(int index)内部调用了element(int index), 调用之前会进行下标越界的校验.
4-5. 遍历遍历分为三种方式:
普通for
foreach
iterator
至于三种方式的性能, 自己测试吧, 哈哈哈
重点: 只有某种情况下的普通for和iterator可以在循环的同事进行元素的删除.
例如:
普通for// 该情况下不会发生异常 for (int i = 0; i < list.size(); i++) { String s = list.get(i); // do something... list.remove(i); } // 这种情况会出现越界问题 int size = list.size(); for (int i = 0; i < size; i++) { String s = list.get(i); // do something... list.remove(i); }
第一种情况下, for循环的终止条件会随着数组元素的移除不断的变化.
第二种情况下, for循环的种植条件不会变化.
for (String s : list) { // do something... list.remove(s); }
这种方式会发生ConcurrentModificationException, 因为ArrayList内部有一个属性为modCount, 每当数组中的元素发生变化是, 该数值会增1, 而foreach形式的循环编译后会变为
Iterator var2 = list.iterator(); while(var2.hasNext()) { String s = (String)var2.next(); list.remove(s); }
这种形式. 因为ArrayList内部的Iterator也维护了一个属性expectedModCount来标识数组元素的变化, 初始化为modCount, 如果modCount与expectedModCount不一致的话, 就会抛出这个异常.
iteratorIteratoriterator = list.iterator(); while (iterator.hasNext()) { // do something... iterator.remove(); }
foreach时我们直接调用了list.remove(s);方法, 该方法只会改变modCount的值, 并不会改变expectedModCount的值, 相反, 使用Iterator提供的remove方法则会对两个值一起修改.
5. 扩容方式首先记住一点: 每次会扩容原数组的 1.5倍(正常情况下).
非正常情况当前是两端的情况:
初始化时第一次扩容会扩容到初始化容量(DEFAULT_CAPACITY)
当容量达到最大值时不会遵循这个规律
扩容方式: 在每次添加新元素时, 会调用扩容代码, 扩容方式还是比较简单的. 只是进行了一些防止溢出的判断.
// 进行扩容调用 private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } // 计算是使用 DEFAULT_CAPACITY 还是传入的 minCapacity private static int calculateCapacity(Object[] elementData, int minCapacity) { // 当使用 new ArrayList() 创建实例时, 数组的默认容量为10就是在这里产生的. if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } // 进行一次modCount的修改, 表示数组元素发生了变动 private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code // 防止元素溢出 if (minCapacity - elementData.length > 0) grow(minCapacity); } /** * The maximum size of array to allocate. * Some VMs reserve some header words in an array. * Attempts to allocate larger arrays may result in * OutOfMemoryError: Requested array size exceeds VM limit */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; /** * Increases the capacity to ensure that it can hold at least the * number of elements specified by the minimum capacity argument. * * @param minCapacity the desired minimum capacity */ private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; // 获取旧数组长度 int newCapacity = oldCapacity + (oldCapacity >> 1); // 计算出新数组长度 oldCapacity + oldCapacity / 2. if (newCapacity - minCapacity < 0) // 如果计算出的长度比传入的长度小, 则使用传入的长度作为新数组长度. newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) // 如果新数组长度比MAX_ARRAY_SIZE, 进行溢出的校验 newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: // 进行数组的拷贝 elementData = Arrays.copyOf(elementData, newCapacity); } // 如果minCapacity比MAX_ARRAY_SIZE大, 就取Integer.MAX_VALUE的值作为新数组长度, 否则使用MAX_ARRAY_SIZE // 也就是传入的长度, 而非通过原数组计算出来的长度. private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }6. 总结
ArrayList就是一个动态数组.
ArrayList的扩容操作必然会进行内存的申请以及数组元素的拷贝.
实例化时尽可能的确定好数组中元素的数量, 避免发生扩容.
使用List
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/72676.html
摘要:需要注意的是,通过构造函数定义初始量是动态数组的实际大小。带容量的构造函数新建一个容量为的数组默认构造函数,默认为空构造一个包含指定元素的第一个构造方法使用提供的来初始化数组的大小。 前言 今天介绍经常使用的一个Java集合类——ArrayList(基于JDK1.8.0_121)。ArrayList在工作和日常面试中经常被使用或者提到。总的来说,工作中使用ArrayList主要是因为动...
摘要:源码和多线程安全问题分析在分析线程安全问题之前,我们线对此类的源码进行分析,找出可能出现线程安全问题的地方,然后代码进行验证和分析。即当多线程调用方法的时候会出现元素覆盖的问题。 1.ArrayList源码和多线程安全问题分析 在分析ArrayList线程安全问题之前,我们线对此类的源码进行分析,找出可能出现线程安全问题的地方,然后代码进行验证和分析。 1.1 数据结构 ArrayLi...
摘要:部分源码分析小记底层数据结构底层的数据结构就是数组,数组元素类型为类型,即可以存放所有类型数据。初始容量大于初始化元素数组新建一个数组初始容量为为空对象数组初始容量小于,抛出异常无参构造函数。 JDK1.8 ArrayList部分源码分析小记 底层数据结构 底层的数据结构就是数组,数组元素类型为Object类型,即可以存放所有类型数据。我们对ArrayList类的实例的所有的操作底层都...
摘要:表明该类具有序列化功能。关键属性默认初始容量大小指定该容量为时,返回该空数组。构造一个包含指定的元素的列表,这些元素是按照该的迭代器返回它们的顺序排列的。对扩容后的容量进行判断,如果大于允许的最大容量,则将容量再次调整为。 总览 showImg(https://segmentfault.com/img/bVbsm9v?w=1232&h=643); 底层:ArrayList底层是一个数...
摘要:源码分析类的实现接口及继承父类和和都实现了接口。这个接口的作用是实现它能够支持快速随机访问。在取出值的时候利用范型转为声明的类型。如果等于则初始化为空如果小于则抛出异常。并且设置为传入的大小。常用方法解析的元素数方法很简单直接返回值的大小。 ArrayList源码分析 类的实现接口及继承父类 public class ArrayList extends AbstractList. i...
摘要:同步众所周知,是同步的而不是,在一些必要的方法上都加了关键字,但是这也会加大系统开销。中有一个方法用来返回一个,以匿名内部类的方式实现的接口和类似,都用作于对集合进行迭代,不过没有删除功能,已经被取代。还有是的,但不是,这一点很重要。 在上篇文章ArrayList源码浅析中分析了一下 ArrayList的源码和一些重要方法,现在对比 ArrayList,总结一下 Vector和 Arr...
阅读 3040·2021-10-12 10:12
阅读 1547·2021-09-09 11:39
阅读 1817·2019-08-30 15:44
阅读 2271·2019-08-29 15:23
阅读 2881·2019-08-29 15:18
阅读 2909·2019-08-29 13:02
阅读 2668·2019-08-26 18:36
阅读 711·2019-08-26 12:08