从源码看集合ArrayList
可能大家都知道,java中的ArrayList类,是一个泛型集合类,可以存储指定类型的数据集合,也知道可以使用get(index)方法通过索引来获取数据,或者使用for each 遍历输出集合中的内容,但是大家可能对其中的具体的方法是怎么实现的不大了解,本篇就将从jdk源码的角度看看什么是动态扩容数组(毕竟我们不应该停留在会用的层面上)。本篇主要从以下几个角度看看ArrayList:
- add及其重载方法是如何实现的
- remove及其重载方法是如何实现的
- 迭代器的本质及实现的基本原理
一、add方法添加元素到集合中
实际上ArrayList内部是用的 transient Object[] elementData;这么一条语句定义的一个Object类型的数组,因为我们知道数组一旦被初始化长度就不能再发生改变,那我们的ArrayList是怎么做到可以不断的添加元素到集合中的呢?其实就是通过创建新的数组,将原来的数组中的内容转移到新的数组中来,实现动态扩容。具体的我们看源码:
public static void main(String[] args){
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1);
}
/*这是最简单的add方法*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
当调用此add方法时,将指定了类型的数据传入(变量e接受),首先执行第一条语句:ensureCapacityInternal(size + 1);,这条语句实际上就是用来判断size+1之后是否会导致原数组长度溢出,如果会就扩充数组容量,如果没有就什么也不做。我们看看ensureCapacityInternal方法内部源码:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
/*ensureExplicitCapacity*/
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
首先判断当前数组是否为空,默认数组长度为DEFAULT_CAPACITY=10,minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);表示:如果数组还未初始化即刚刚声明并未做任何操作,就取10作为数据容量值,然后调用方法ensureExplicitCapacity(minCapacity);设置数组长度。
接受过传入的数据容量值,执行modCount++;增加修改次数(后文会说为什么有这个计数器),判断数据容量值是否比现数组长度大,如果数据容量值超过现有数组长度(需要扩容),执行:grow(minCapacity);,我们可以看看他是怎么进行扩容的。
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);
}
这条语句 int newCapacity = oldCapacity + (oldCapacity >> 1);通过位右移将新数组容量扩充为原来的1.5倍。数组的每次扩容都是,扩充为原来的1.5倍。下面是一系列的判断,最终确定新数组的长度,调用Arrays.copyOf方法,新建数组并且转移原数组内容。再往下,就不深究了。。
最后小结一下整个过程,调用add 方法首先调用ensureCapacityInternal方法,如果原数组是空的就将10作为数据容量值,然后判断数据容量值是否大于当前数组长度(如果当前数组是空数组的话,自然长度为0),然后进行扩充数组容量,创建新数组返回。如果原数组非空,将判断数据容量值是否大于现数组长度,否说明添加此新元素之后数据量长度仍然小于数组长度(数组长度足够),是就会调用grow方法创建新数组赋值elementData数组。
add的另一个比较麻烦的方法是,addAll方法,其他的重载方法类似,本篇不再赘述。下面我们一起看看addAll方法原理。
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;
}
addAll()方法的动态扩容和添加数值都和add 类似,我们主要理解一下,他的这个参数是什么意思,也顺便复习一下泛型相关内容。
大家知道Collection<? extends E>作为类型,有哪些类型可以作为形参传入?假如E是Number类型,Collection<Integer>,Collection<Float>,Collection<Double>,都是可以作为形参传入的。而所有继承Collection接口的类也可以作为形参传入,例如:List<Integer>,Set<Integer>,List<Double>,ArrayList<Integer>,等等,但在本方法中是需要调用toArray这个具体的方法的,所以只能使具体类作为形参传入,这样就保证,形参是可以是任意类型的集合(前提是此类型必须继承与我们指定的E)。
二、Remove方法的实现原理
既然集合是可以添加元素的,自然也是可以删除元素的,接下来我们一起看看ArrayList的Remove方法。
/*根据集合索引删除任意位置的元素*/
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;
}
第一行代码很简单,rangeCheck(index);,检查指定索引是否越界,如果越界抛出异常。然后计算出,移除后的数据容量,因为经过判断index是<size的,也就是说numMoved >=0。判断是否大于0,如果等于0表示原来就一个数据,直接将其赋值null交给GC回收即可。如果大于0,执行System.arraycopy方法,因为此方法为native方法,我们不得而知它是如何实现的,但是我们可以大致猜出他是这样实现的:以索引位置开始,索引位置后面的数组元素向前覆盖。例如:index=3;elementData[3]=elementData[4],elementData[4]=elementData[5]等等。最后将最后位置的元素赋值为null。
以上便是remove方法的简单原理,至于其他重载与上述类似。接下来,我们看看重要的迭代器。
三、迭代器
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
我们把接口 Iterator<E>叫做迭代器接口。通过反复调用next方法可以访问到所有的元素,当访问到最后一个元素的下一的位置时,就会抛出异常,所以我们常常在调用next方法之前调用hasNext方法判断是否还有下一个元素,remove方法表示删除元素(一个要求,调用remove方法之前一定要先调用next方法,这一点下文说)
了解完 Iterator<E>,我们看看另一个和它相关的接口,Iterable<E>:
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}
这个接口 Iterable<E>表示可迭代,强调了可以迭代的这种能力。声明一个方法 iterator();返回 Iterable<E> 迭代器接口,所有实现了 Iterable<E>接口的类都是可以使用for each 循环遍历集合中元素的。当我们的类实现 Iterable<E>接口时,可以使用for each 循环集合,其实内部还是,通过调用方法 iterator()实现当前集合和迭代器的一种类似于绑定的过程,最终返回迭代器接口,实际上for each 语法还是调用的是 迭代器接口中声明的方法 类似:
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
Iterator<Integer> i = list.interator();
while(i.hasNext()){
System.out.println(i.next);
}
/*for each 语句的本质其实就是这样*/
下面要说的关于迭代器的一个重要的特性,迭代器的结构不可破坏性。就是说,在进行迭代的过程中,是不允许改变原集合的结构性的,集合的结构性就是指:对集合进行添加(add),删除(remove)。对集合的修改操作不属于破坏集合的结构性。例如:
for(Integer a : list){
if(a == 3){
list.remove(a); //throw exception
}
}
//破坏了集合的结构性,不允许的。
要想解决这个问题就要看看ArrayList中是怎么实现迭代器的。实际上是通过内部类来实现迭代器接口的。
public Iterator<E> iterator() {
return new Itr();
}
//内部类,我们只看其中remove方法
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
//remove方法
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
我们之所以在for each循环中不能破坏结构性,是因为for each每次调用next方法时,都会检查是否破坏了结构性,而这种检查就是依靠modCount 这个变量,通过对比前后的修改次数得出是否破坏了结构性,在我们的remove方法中,调用了外部类remove方法删除元素,并且 expectedModCount = modCount; 更新了修改次数变量,使得下次检查时,不会出现结构性破坏。
Iterator<Integer> it = list.interator();
while(it.hasNext()){
if(it.next().equals(1)){
it.remove();
}
}
最后想要强调一点的是,迭代器中调用remove方法之前一定要,调用next方法,例如:
Iterator<Integer> it = list.interator();
while(it.hasNext()){
it.remove();
}//报错
现在大家能够想明白为什么在调用remove方法之前一定要调用next方法了吧,因为next方法为lastRet和cursor重置数值,如果没有next方法,lastRet为 -1 自然是不能用作删除的。
本篇就此结束,如果文中有博主说的不清楚的地方,望大家指出!
从源码看集合ArrayList的更多相关文章
- 从源码看Java集合之ArrayList
Java集合之ArrayList - 吃透增删查改 从源码看初始化以及增删查改,学习ArrayList. 先来看下ArrayList定义的几个属性: private static final int ...
- 集合框架源码学习之ArrayList
目录: 0-0-1. 前言 0-0-2. 集合框架知识回顾 0-0-3. ArrayList简介 0-0-4. ArrayList核心源码 0-0-5. ArrayList源码剖析 0-0-6. Ar ...
- 【集合框架】JDK1.8源码分析之ArrayList详解(一)
[集合框架]JDK1.8源码分析之ArrayList详解(一) 一. 从ArrayList字表面推测 ArrayList类的命名是由Array和List单词组合而成,Array的中文意思是数组,Lis ...
- 集合源码分析[3]-ArrayList 源码分析
历史文章: Collection 源码分析 AbstractList 源码分析 介绍 ArrayList是一个数组队列,相当于动态数组,与Java的数组对比,他的容量可以动态改变. 继承关系 Arra ...
- java读源码 之 list源码分析(ArrayList)---JDK1.8
java基础 之 list源码分析(ArrayList) ArrayList: 继承关系分析: public class ArrayList<E> extends AbstractList ...
- 【源码解析】- ArrayList源码解析,绝对详细
ArrayList源码解析 简介 ArrayList是Java集合框架中非常常用的一种数据结构.继承自AbstractList,实现了List接口.底层基于数组来实现动态容量大小的控制,允许null值 ...
- 读 Zepto 源码之集合元素查找
这篇依然是跟 dom 相关的方法,侧重点是跟集合元素查找相关的方法. 读Zepto源码系列文章已经放到了github上,欢迎star: reading-zepto 源码版本 本文阅读的源码为 zept ...
- Java源码阅读之ArrayList
基于jdk1.8的ArrayList源码分析. 实现List接口最常见的大概就四种,ArrayList, LinkedList, Vector, Stack实现,今天就着重看一下ArrayList的源 ...
- JUC源码分析-集合篇(九)SynchronousQueue
JUC源码分析-集合篇(九)SynchronousQueue SynchronousQueue 是一个同步阻塞队列,它的每个插入操作都要等待其他线程相应的移除操作,反之亦然.SynchronousQu ...
随机推荐
- Java JDBC连接SQL Server2005错误:通过端口 1433 连接到主机 localhost 的 TCP/IP 连接失败 及sql2008外围服务器
转载:Java JDBC连接SQL Server2005错误:通过端口 1433 连接到主机 localhost 的 TCP/IP 连接失败 错误原因如下: Exception in thread & ...
- PHP图片处理之图片背景、画布操作
像验证码或根据动态数据生成统计图标,以及前面介绍的一些GD库操作等都属于动态绘制图像.而在web开发中,也会经常去处理服务器中已存在的图片.例如,根据一些需求对图片进行缩放.加水印.裁剪.翻转和旋转等 ...
- js原生设计模式——12装饰者模式
1.面向对象模式装饰者 <!DOCTYPE html><html lang="en"><head> <meta charset=&q ...
- TForm类
显示给用户的窗体有两种:有模式和无模式的.具体使用哪一种窗体,取决于是否希望用户能够同时与这个窗体和其他窗体交互. 1.当打开一个模式窗体后,用户无法与应用程序的其他部分交互,知道用户关闭了这个窗体. ...
- Unable to resolve target 'android-XX'解决办法
在搭建好安卓编译环境后,我用Eclipse导入冲git上下载的安卓源码编译时,会提示 Unable to resolve target 'android-17' 等 “Unable to resolv ...
- validform表单验证插件最终版
做个笔记,以后直接用吧. 报名界面: <%@ page language="java" pageEncoding="UTF-8" contentType= ...
- Java学习之旅基础知识篇:数组及引用类型内存分配
在上一篇中,我们已经了解了数组,它是一种引用类型,本篇将详细介绍数组的内存分配等知识点.数组用来存储同一种数据类型的数据,一旦初始化完成,即所占的空间就已固定下来,即使某个元素被清空,但其所在空间仍然 ...
- WebRTC VoiceEngine使用简单Demo
Google收购的GIPS公司的音频处理技术是很牛的,现在开源了,这么好的技术应该拿来用的,这里就简单的介绍一下怎样使用VoiceEngine,欢迎大家拍砖指导. WebRTC相关的VideoEngi ...
- HTML5 & CSS3初学者指南(3) – HTML5新特性
介绍 本文介绍了 HTML5 的一些新特性.主要包含以下几个方面: Web 存储 地理位置 拖放 服务器发送事件 Web存储 HTML5 Web 存储的设计与构想是一个更好的机制来存储客户端的网络数据 ...
- 这个发现是否会是RSA算法的BUG、或者可能存在的破解方式?
笔者从事各种数据加解密算法相关的工作若干年,今天要说的是基于大数分解难题的RSA算法,可能有些啰嗦. 事情的起因是这样的,我最近针对一款芯片进行RSA CRT解密的性能优化.因为期望值是1024bit ...