源码分析-java-ArrayList-基本方法及实现
来源:互联网 发布:mui框架 js自动加载 编辑:程序博客网 时间:2024/05/16 05:43
ArrayList
API文档
大小的可变数组的List接口的实现。实现了所有的List可选操作,允许所有(包括null)在内的元素。为了实现这List接口,这个类提供了操作数组(这个数组用于内部储存)大小的方法。(这个类类似于Vector,只是这个类不是同步的)
size、isEmpty、get、set、Iterator和ListIterator以固定的时间运行。add操作的摊还时间为添加n个操作需要O(n)时间。所有其他操作均运行在线性时间(大约的情形)。常数因子略低于LinkedList的实现
每个ArrayList实例都有一个capacity。这个能力表示数组能储存的元素的大小。它至少大于size。随着元素加入到ArrayList。它的capacity自动增长。增长策略的细节基于这样的细节,一个元素有常数的均摊时间代价。
应用程序应该在大量添加元素之前,使用ensureCapacity操作增加ArrayList的capacity。这可能会降低请求内存的次数。
需要注意的是这个实现不是同步的。如果多线程同时访问ArrayList实例,而且至少有一个线程结构性的更改了list,它必须要由额外的同步策略。通常来说通过同步一些代码块实现。如果没有这样的对象存在,则list应该被Collections.synchronizedList方法包装起来。为了防止意外的异步访问这个list,包装最好在创建的时候包装。
List List = Collections.synchronized(new ArrayList)
通过这个类的Iterator方法和listIterator方法返回的迭代器是fail-fast的。如果在迭代器创建后,不同此迭代器而list实现了结构性调整,这个迭代器会抛出一个ConcurrentModificationException异常,因此在面对同时修改的可能时,迭代器迅速失效,而不是增加风险的随机性、及未来不可预估的行为和时间。
需要说明的是,fail-fast行为的迭代器通常来说不能强制性的保证不发生异步的同时修改。fail-fast迭代器竟可能保证快速抛出异常。因此依赖fail-fast迭代器的抛出异常行为的正确性写出代码是不可靠的。迭代器的fail-fast行为应该只用于debug。
构造器及相关域
域
private static final long serialVersionUID = 8683452581122892189L;private static final int DEFAULT_CAPACITY = 10;private static final Object[] EMPTY_ELEMENTDATA = {};private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};transient Object[] elementData; // non-private to simplify nested class accessprivate int size;
首先是四个静态域:
- serialVersionUID :没什么好说的。
DEFAULT_CAPACITY 表示默认的初始化数组大小。 - EMPTY_ELEMENTDATA: 是一个静态的空数组,意思是当一个ArrayList在创建时先将其指向这里(除非是传入是集合)。直到ArrayList添加了第一个元素之前一直都指向这里。而不是真的创建一个数组。
- DEFAULTCAPACITY_EMPTY_ELEMENTDATA: 作用同EMPTY_ELEMENTDATA 一样。区别是这里表明创建这个类的时候没有指定capacity而是使用默认的DEFAULT_CAPACITY 。
- elementData:是真实的存放元素的地方。有意思的是这里居然是使用默认的访问权限,理由是方便内部类的访问。但是按照道理来说即使是private内部类也可以访问。不知道为何这里要使用默认的访问权限。此外这个类是transient的,这里也很有意思。
构造器
public ArrayList()public ArrayList(int initialCapacity)public ArrayList(Collection<? extends E> c)
ArrayList():直接将elementData指向了DEFAULTCAPACITY_EMPTY_ELEMENTDATA
ArrayList(int initialCapacity):检查了initialCapacity的合法性,然后将elementData指向了
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; } }
首先将原集合转成了数组。然后判断是否是空,如果是空则将elementData指向EMPTY_ELEMENTDATA空数组。
在不是空的情况下这里做了一些额外的判断。
首先如注释所说这里有一些java的bug。在有些情况下toArray返回的并不是Object[]类的数组。如果toArray返回的不是Object数组,而是一个Object子类的数组,那后期如果向其添加非该子类或者其子类的元素会导致错误,更为关键的是这样的错误应用程序相对来说难以排查。所以这里判断了elementData的类型是不是Object[]类型,如果不是则调用Arrays的copyOf静态方法将elementData复制到一个同样大小的Object[]中。
这里补充下关于java的bug的问题:
java对于bug建立了一个bug库,这里的注释 see 6260652就是bug的编号。网上搜一下就很容易搜到。
比如这里的bug其实说的是Arrays.asList(String[])方法返回的并不是Object类而是一个String相关的类型。这样当你在存入Object类的时候(或者只要存入类型不是返回类型或者其的子类)的时候会出现错误。类似的还有Arrays.ArrayList.toArray()
bug出现的概率是很低的。目前只有这一种情况。且在java9中已经修复。在这里设计人员并没有尝试解决问题而是采取了绕过的方式。这样可能是一种更快速更安全的方法。
bug 6260652的数据库
关于此bug的更多解释
capacity相关方法及域
在看源码之前我一直以为ArrayList有一个名为capacity的域,而API文档的解释似乎也引导程序员有这样的想法。但是实际上在ArrayList中并没有这个域,相反ArrayList并不是通过 设定一个这样的值来实现管理capacity的。而是通过一系列方法。换句话说其实所谓的capacity更像是一种一次性的优化手段。主要目的仅仅在于创建数组的时候可以节省空间加快速度。因此只需要一次运行即可,如果只是运行一次就设定一个域显然有些浪费。
这里看一下有关的函数:
public void trimToSize()public void ensureCapacity(int minCapacity)private void ensureCapacityInternal(int minCapacity)private void ensureExplicitCapacity(int minCapacity)private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;private void grow(int minCapacity)private static int hugeCapacity(int minCapacity)
接下来依次看看这几个函数:
截取
public void trimToSize() { modCount++; if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } }
这个函数其实跟capacity并没有生关系。只是根据size的大小来截断数组,创建一个更小的数组。同样这里做了一些稍许的优化如果size为0的时候指向EMPTY_ELEMENTDATA。
在ArrayListList中大量的运用了三目运算。主要原因是ArrayList的构造是否指定了capacity以及元素数组是否为空。当然可以使用if等条件语句来执行,但是三目运算一个最大的好处是有返回值。使用三目运算使得程序显得更加紧凑。
ensureCapacity
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); } } 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); }
这三个方法很有意思。其实完成的是同一项功能但是却分了三段。其实最终完成确认工作的是ensureExplicitCapacity方法。前面两个方法相当于是两个适配器。分别对应于public的方法和private的方法。下面我来尝试分析一下为何是这样做,但内容仅仅是我的个人的猜测。
- 编程人员希望将public和private的调用分离。因为public的确认和private的确认目的不一样的。
- 对于public的确认,首先java为程序员提供了一种指定capacity的权利。但是由于java提供了默认的capacity的大小。这虽然有时提供了方便但是未必一定是每时每刻都合理的,比如说有时用户并不需要这么大的数组,所以他使用ensureCapacity来指定一自己的大小。这时如果直接将其与DEFAULT_CAPACITY比较则是赋予编程人员指定capacity的权利就没有意义了。就会出现最小都创建DEFAULT_CAPACITY大小的数组,会造成空间上的浪费。这个方法的目的是给程序员更大的自由性,以及用于初始化的工作。
- 对于private的情况,这种情况是适用于在增加元素或者序列化的时候。你可以看到ensureCapacityInternal实际上通常只在add类型的方法中调用。这个函数的目的是当ArrayList在使用默认的capacity的时候需要判断一下需要增加的值和默认的capacity的值DEFAULT_CAPACITY哪个大。否则指定DEFAULT_CAPACITY就没有意义。这个方法的目的是为了在默认情况下发挥DEFAULT_CAPACITY的功效。
- 设计人员希望将ensureExplicitCapacity作为一种增加modCount的方法。其实我对有一些疑问。这里仅仅抛出一些看法,以后可能需要更多的思考。
- 首先为何将这个方法作为一种增加modCount的方法我个人认为是欠妥的。因为我理解的modCount方法是结构性修改的情况下做出的。但是在这里未必进了结构性的修改,也许放入grow中是一种更加合理的选择。从下文中每次使用ensureCapacityInternal都注释了// Increments modCount来提示程序员也说明这里其实是略微欠妥的。
- 我猜想这里之所以放入modCount可能是为保证编程的确定性。因为如果每次只在grow这样的结构性修改中才能增加modCount那编程人员无法判断modCount什么时候增加,所以从这个角度上说这里可能是为了增加编程的确定性,而故意曲解了结构性调整的概念。
- 另一种可能的原因是结构性的调整不仅仅是变化了数组,由1.的观点可以看出其实ensureExplicitCapacity是用于add的方法的所有增加元素。而增加元素显然是属于结构性调整的。因此将modCount放到这里而在add方法中不再增加modCount,这样从结果上说依然严格遵守了结构性调整的概念,所以这样来看也是非常合理的,虽然这只是通过不调用ensureExplicitCapacity来实现的。
- ensureExplicitCapacity是最终需要完成的任务。目的是判断是否需要对数组进扩展,而且每完成一次这样的查询则需要增加一次modCount而无论是否真的需要扩展。对于private的情形自然是每次完成都需要拓展。对于public的情形则需要根据其是否设定了capacity来给编程人员最大的自由度的同时提高便利性。
扩展数组
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 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;
扩展数组的部分和之前的LinkedList的方法并没有流程上的区别,只是这里使用Arrays.copyOf来实现。
实现上也是通过每次增加当前数组的一半。对于minCapacity过大的情况需要实现满足miniCapacity大小的数组。对于miniCapacity过大的情况需要根据虚拟机的限制做出合理的设定。仅此。
增删改查
增删改查基本上没有特别需要注意的。但是这里的语法写得非常简洁,短小精干。还是有很多可以借鉴的地方。大量的应用了System.arraycopy方法。其余需要注意的也只有类型转换了移动数值的问题了。具体的程序细节看下源码就好。这里只集中看下其中几类的方法。
1.remove方法
public E remove(int index)public boolean remove(Object o)private void fastRemove(int index)
其实跟上面的ensureCapacity类似。最终做删除的是fastRemove,前两个都是不同情况下的适配器。而fastRemove是被抽取出来执行删除的公共方法。fastRemove不做检测也不返回值。而是直接删除。
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; } 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 }
2.retainAll及batchRemove
另一类有意思的方法是batchRemove,这个方法实际上是retainAll调用的。
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++) 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; }
使用complement来判断当前元素是否包含集合内的元素。加入为true则只保留包含在Collection内的元素,反之易证。
创建两个变量一个指向执行前的变量pos,一个指向执行后的pos,这样可以做一个原地的算法不需要创建新数组。这里需要考虑的是抛出异常的情况。
如果r不等于size,则说明在运算的过程中抛出了异常,因此将未完成的运算即r之后的变量复制到当前运算的结果w之后,并且改变w的值,现在w的之后的值是多余的
如果w为size,则说明原数组没有被改动modified依然为false,如果w不为size,则说明modified为true,此外还说明数组w之后的元素为多余元素,将他们全部置为null,以方便GC回收。
3.clear
另外关于clear。并没有将指针指向空数组,而只是将原先所有的元素改为nulll。可能是设计人员认为程序员执行clear后任然会继续使用这个数组。如果指向空数组会在下次创建的时候浪费很多资源。
public void clear() { modCount++; // clear to let GC do its work for (int i = 0; i < size; i++) elementData[i] = null; size = 0; }
copyOf和arraycopy的区别及分析
在最后我想说明一下两个常用的方法的区别,这是在上文中没有提到的:
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType)public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
首先第一个方法,copyOf是属于Arrays类的方法。从原型可以看出来这是一个将一个类型数组,而这里指定一个新的类型,这里还需要指定长度。方法会根据这个条件进行截断和补null。通常来说什么情况下会使用copyOf。是需要构造新的类型数组或者需要将一个数组类型转换的时候,比如ArrayList的构造器,toArray等方法,clone以及grow的时候。因为这个时候可能会涉及到类型变化。
而arraycopy是一个native方法,这也是我第一次见到一个实体的native方法。根据网上的资料native方法指的是依赖于非java实现的,而是通过java调用其他语言实现的。常见的是c及c++编写车dll之类的库,然后java调用这些库来实现的。从java的源码中我们看不出来如何实现的。API文档解释的比较多,简单的说这里就是将src从srcPos复制到dest的destPos处,复制长度为length。如果类型不匹配,超出索引范围,或者src或dest为空都会抛出异常。有意思的是这里虽然是一个数组却使用的是Object。这个方法通常用来复制数组的一部分到另一个数组的一部分,或者数组内部位移使用。你可以看到add和remove等方法使用到了这个方法。
接下来看下第一个方法copyOf的实现:
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; }
由以上内容可以看到copyOf方法是需要调用。
关于这个方法的讨论有两个点:
- 为何要使用(Object)newType == (Object)Object[].class)
之所以要进行类型转换是因为Class<Object>.class
和Class<? extends T[]>
不是可以互相比较的类型,这句话甚至都不会被编译,如果需要相互比较则需要使得一方是另一方的子类才可以。而java的通配符使得这个问题更加的复杂。因为虽然Object[]是其他所有类型数组的超类(这里有一定疑问stackoverflow原文是这样说的,暂时没查到。),但Class<Object[]>
并不是Class<? extends T[]>
的超类。所有需要将其都转换成为Object来判断。另外一种可以比较的方法是(newType == (Class<? extends Object[]>)Object[].class)
判断是否可以将其转换成为Object。 - 为何不可以都是用反射的方法创建数组,一个理由是反射比较慢,而用普通的方法比较快,因此这三目运算实际上是为了做一些微小的优化。
更多的解释请参看原文Arrays.copyOf实现的讨论
toArray
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; }
对于输入的数组小于size的需要重新构造一个新的T[]符合size大小的数组,而输入数组大于size的需要将目前的elementData复制到指定的size中,如果a有多余的大小,则将下一个元素赋为null。
需要说明的是不可以想当然的将elementData给出去,这样会导致外部引用到的没有访问权限的数据的引用。
一个问题是这里Arrays.copyOf应该是已经返回的已经是T[]类型的了为何需要加(T[])?一种可能的原因是java本身泛型的限制。用a.getClass得到类并不是调用者希望的,这里得到的a的类型可能会更具体,因此这里添加一个转换,转换成T的类型(可能是更普遍更上层的类型)。
SubList
ArrayList可以通过subList的方法返回一个SubList的内部类,它同ArrayList一样也是继承自AbstractList并且实现了RandomAccess的。
ArrayList有几个内部私有final域:
- private final AbstractList parent;
- private final int parentOffset;
- private final int offset;
- int size;
和ArrayList的区别就是带有一个AbstractList接口的内部类,并且再ArrayList调用subList的时候将这个ArrayList付给parent域,并根据设定的起始目录和终止目录设定另外三个域。
SubList(AbstractList<E> parent, int offset, int fromIndex, int toIndex) { this.parent = parent; this.parentOffset = fromIndex; this.offset = offset + fromIndex; this.size = toIndex - fromIndex; this.modCount = ArrayList.this.modCount; }
SubList的其他方法并没有什么特别要说的,基本是ArrayList的翻版,只是需要注意考虑偏置。
需要考虑的SubList所用的iterator时,构造了一个新的内部匿名类,因为原先的类没办法实现偏置的调整。
- 源码分析-java-ArrayList-基本方法及实现
- java 集合ArrayList及LinkList源码分析
- java 集合ArrayList及LinkList源码分析
- java 集合ArrayList及LinkList源码分析
- java 集合ArrayList及LinkList源码分析
- Java集合-ArrayList源码分析及注意事项
- Java ArrayList源码分析
- Java ArrayList 源码分析
- 《JAVA源码分析》:ArrayList
- Java ArrayList源码分析
- java ArrayList源码分析
- Java源码分析-ArrayList
- Java源码分析-ArrayList
- 《JAVA源码分析》:ArrayList
- Java ArrayList源码分析
- Java ArrayList源码分析
- Java----- ArrayList构造、add、remove、clear方法实现原理源码分析
- 顺序线性表 ---- ArrayList 源码解析及实现原理分析
- 【solr 基础篇三】SolrJ的入门使用
- iOS网络请求工具oc版,swift版基于AFNetworking的简单封装
- 浅谈WebService SOAP、Restful、HTTP(post/get)请求
- 【那些遇到的坑】—hadoop完全分布式集群搭建启动一直处于starting namenode
- Password authentication failed, Please verify that the username and password are correct
- 源码分析-java-ArrayList-基本方法及实现
- HDU 1237 简单计算器 (栈)
- jsp数据库基础之---jsp与MySQL数据库的连接
- mysql建立外键约束
- IOS设备的三种分辨率
- Block 浅析
- 鼠标移上边框动画练习
- 【QT小贴士】删除QListWidget中的某项
- jQuery $(document).ready()和JavaScript onload事件