ArrayList精讲(源码分析)---Java集合
Posted 写Bug的渣渣高
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ArrayList精讲(源码分析)---Java集合相关的知识,希望对你有一定的参考价值。
问题概览
1.ArrayList是什么
2.和Array有什么区别,效率怎么样
3.增删改查及其代码
4.源码剖析
1.ArrayList是什么
存储数据的一个容器,其底层是使用数组实现的。
为什么需要ArrayList?
- 可变容积。虽然其内部使用数组来存储元素,但是当元素个数到达一定程度时,可以创建新的数组并将元素拷贝过去,进而实现了可变容
积。 - 连续空间存储。同数组,地址是连续的,所以加入到ArrayList的元素是连续空间存储的。
2.与Array区别
Array:数组,定义时需要指定类型,即存储单一类型的数据。长度固定(正是因为数组长度固定,所以就需要一种数据结构,可以扩容用以存储更多数据。)
ArrayList:长度不固定,可以指定长度,当元素个数增加到或者减少到各自的临界值,会触发扩容和缩容机制。底层是数组,当剩余容量过小时,会触发扩容机制
创建ArrayList方式
ArrayList<String> list = new ArrayList<>();
//注意,创建时需要指定存储元素类型
源码分析
1.无参构造
关键点1.:无参,默认长度10
关键点2:elementData是存储
public class ArrayList<E>
/**
* 默认初始容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空数组
*/
private static final Object[] EMPTY_ELEMENTDATA = ;
/**
* 默认容量的空数组
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = ;
/**
* 集合真正存储数组元素的数组
*/
transient Object[] elementData;
/**
* 集合的大小
*/
private int size;
public ArrayList()
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
//若使用无参构造,则设定默认长度为10
2.有参构造
public class ArrayList<E>
public ArrayList(int initialCapacity) //initialCapacity = 5
//判断初始容量initialCapacity是否大于0
if (initialCapacity > 0)
//创建一个数组,且指定长度为initialCapacity
this.elementData = new Object[initialCapacity];
else if (initialCapacity == 0)
//如果initialCapacity容量为0,把EMPTY_ELEMENTDATA的地址赋值给elementData
this.elementData = EMPTY_ELEMENTDATA;
else
//以上两个条件都不满足报错
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
3.增删改查
增加
方法名 描述
public boolean add(E e) 将指定的元素追加到此列表的末尾。
public void add(int index, E element) 在此列表中的指定位置插入指定的元素。
public boolean addAll(Collection<?extends E> c):按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。
public boolean addAll(i nt index,Collection<? extends E> c)
将指定集合中的所有元素插入到此列表中,从指定的位置
开始。
1.添加元素
public class Test01
public static void main(String[] args)
ArrayList<String> list = new ArrayList<>();
//1.添加单个元素
list.add("ggzx");
//2.public void add(int index, E element) 在指定索引处添加元素
list.add(1, "长沙");
//3.public boolean addAll(Collection<? extends E> c) 将集合的所有元素一次性添加到集合
ArrayList<String> list1 = new ArrayList<>();
list1.addAll(list);
源码分析
1.添加单个元素
执行流程:add---->ensureCapacityinternal---->ensureExplicitCapacity
流程解析:ensureCapacityInternal确定添加元素后所需要的最小容积,ensureExplicitCapacity判断elementData长度是否大于最小容积,如果小于,就无法存放新元素,需要扩容
public boolean add(E e)
//调用方法对内部容量进行校验
ensureCapacityInternal(size + 1);//极限情况时,未加入该元素时,emelentData已经满了,所以说需要传入需要扩容时最小的扩容大小,如果数字长度小于这个最小长度,那么就需要扩容。
elementData[size++] = e;
return true;
private void ensureCapacityInternal(int minCapacity)
//判断集合存数据的数组是否等于空容量的数组,即第一次加入元素时才会执行此代码块。
//为什么需要这个代码块,因为假如指定了ArrayList长度为1,那么假如元素时会频繁触发扩容
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
//通过最小容量和默认容量 求出较大值 (用于第一次扩容)
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
//将if中计算出来的容量传递给下一个方法,继续校验
ensureExplicitCapacity(minCapacity);
private void ensureExplicitCapacity(int minCapacity)
//实际修改集合次数++ (在扩容的过程中没用,主要是用于迭代器中)
modCount++;
//判断最小容量 - 数组长度是否大于 0,注意注意注意:这里的长度指的是容积,不是元素个数
//意思是:当前数组长度小于最小所需要的长度,就执行扩容
if (minCapacity - elementData.length > 0)
//将第一次计算出来的容量传递给 核心扩容方法
grow(minCapacity);
private void grow(int minCapacity)
//记录数组的实际长度,此时由于木有存储元素,长度为0
int oldCapacity = elementData.length;
//核心扩容算法 原容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//判断新容量 - 最小容量 是否小于 0, 如果是第一次调用add方法必然小于
if (newCapacity - minCapacity < 0)
//还是将最小容量赋值给新容量
newCapacity = minCapacity;
//判断新容量-最大数组大小 是否>0,如果条件满足就计算出一个超大容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 调用数组工具类方法,创建一个新数组,将新数组的地址赋值给elementData
elementData = Arrays.copyOf(elementData, newCapacity);
2.在指定位置添加元素
思考一下,Array能不能在指定位置新增一个元素(要求,该位置新增元素,原有元素不不被覆盖)。
当我们尝试在合法索引内添加元素时候,例如a[10] = 1;
可以发现,此时元素实际上是覆盖原有值。但是我们可以使用移动元素的方法来实现指定位置新增一个元素,而ArrayList底层也是数组,其在某索引处新增元素也是采用这个办法
如何在指定位置添加一个元素
1.若容积足够,将该索引处后的所有元素后移
2.覆盖该索引处元素
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++;
删除
原理:因为ArrayLIst内部就是数组,可以直接使用索引的方式来删除元素,而且这个删除不能是仅仅覆盖原有元素,而且把移动该索引后的元素,覆盖掉需要删除的元素
public class Test01
public static void main(String[] args)
ArrayList<String> list = new ArrayList<>();
list.add("李逵");
list.add("宋江");
list.add("卢俊义");
//1.根据索引删除元素
String value = list.remove(1);
System.out.println("删除的元素为: "+value);
System.out.println("集合的元素: "+list);
//2.根据元素删除
boolean flag = list.remove("宋江");
根据<元素删除>源码分析:
关键点1:移除元素的核心原理是将移除元素往后的元素前移一位,具体看fastRemove中的System.arraycopy
public boolean remove(Object o)
//判断要删除的元素是否为null
if (o == null)
//遍历集合
for (int index = 0; index < size; index++)
//判断集合的元素是否为null
if (elementData[index] == null)
//如果相等,调用fastRemove方法快速删除
fastRemove(index);
return true;
else
//遍历集合
for (int index = 0; index < size; index++)
//用o对象的equals方法和集合每一个元素进行比较
if (o.equals(elementData[index]))
//如果相等,调用fastRemove方法快速删除
fastRemove(index);
return true;
//如果集合没有o该元素,那么就会返回false
return false;
private void fastRemove(int index)
//增量++
modCount++;
//计算集合需要移动元素的个数
int numMoved = size - index - 1;
//如果需要移动的个数大于0,调用arrayCopy方法进行拷贝
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);//让index+1往后的numMoved个元素依次覆盖索引index往后的元素,简单点来说,就是index+1及以后的元素前移一个,进而实现移除元素
//将集合最后一个元素置为null,尽早被释放
elementData[--size] = null;
public final class System
// 参数
// src - 源数组。
// srcPos - 源数组中的起始位置。
// dest - 目标数组。
// destPos - 目的地数据中的起始位置。
// length - 要复制的数组元素的数量。
public static void arraycopy(Object src,int srcPos,Object dest,int destPos,int length)
根据<索引删除>源码分析:
理解了上方的fastRemove这里就很好理解了,这里核心还是将index+1后的元素前移来实现“删除某个元素”。
关键点1:参数是索引,所以需要检查索引是否越界
public E remove(int index)
//范围校验
rangeCheck(index);
//增量++
modCount++;
//将index对应的元素赋值给 oldValue
E oldValue = elementData(index);
//计算集合需要移动元素个数
int numMoved = size - index - 1;
//如果需要移动元素个数大于0,就使用arrayCopy方法进行拷贝
//注意:数据源和数据目的就是elementData
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将源集合最后一个元素置为null,尽早让垃圾回收机制对其进行回收
elementData[--size] = null;
//返回被删除的元素
return oldValue;
修改方法
public class Test01
public static void main(String[] args)
ArrayList<String> list = new ArrayList<>();
list.add("李逵");
list.add("宋江");
list.add("卢俊义");
//根据索引修改集合元素
String value = list.set(2, "鲁智深");
System.out.println("set方法返回值: "+value);
System.out.println("集合的元素: "+list);
源码解析:
很简单,根据索引来修改elementData中的值即可
public E set(int index, E element)
//范围校验
rangeCheck(index);
//先取出index对应的元素,且赋值给oldValue
E oldValue = elementData(index);
//将element直接覆盖index对应的元素
elementData[index] = element;
//返回被覆盖的元素
return oldValue;
查询
调用get方法,传入索引即可返回元素
public class Test01
public static void main(String[] args)
ArrayList<String> list = new ArrayList<>();
list.add("山东大李逵");
list.add("天魁星宋江");
list.add("天罡星卢俊义");
//根据索引获取集合元素
String value = list.get(1);
System.out.println("get方法返回值: "+value);
System.out.println("集合的元素: "+list);
源码:
该部分源码很简单,因为ArrayList存储数据采用的是数组,所以是可以通过[i]索引的方式来直接获得元素。所以说只需要给get方法加上一个范围检测,防止越界即可。
public E get(int index)
//范围校验
rangeCheck(index);
//直接根据索引取出集合元素
return elementData(index);
private void rangeCheck(int index)
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
4.迭代器
代码演示:
public static void main(String[] args)
//创建集合对象
List<String> list = new ArrayList<String>();
//添加元素
list.add("hello");
list.add("Java");
list.add("php");
//获取迭代器
Iterator<String> it = list.iterator();
//遍历集合,加入还没有遍历完,就可以继续next()
while (it.hasNext())
String s = it.next();
System.out.println(s);
通过迭代器可以快速遍历ArrayLIst中的数组
Itr作为ArrayList的内部类,其继承了Iterator,我们主要来看next()以及hasNext()方法。
- size:存储了ArrayList的元素个数
- cursor: 下一个要返回元素的索引
haxNext():根据cursor和size来判断是否已经遍历到末尾了
next():准备移动索引,先判断是否越界,如果没有越界,那么就可以移动cursor索引,并且返回cursor索引指向的值
注意:Itr是ArrayList内部的迭代器,其内部还实现了remove等方法,因为数组的原因,这些源代码都是较为简单的,即先判断是否越界,然后操作索引的增删来改变。
public class ArrayList<E>
public Iterator<E> iterator()
return new Itr();
//ArrayList内部类
//一定要注意观察 Itr 类中的几个成员变量
private class Itr implements Iterator<E>
int cursor; // 下一个要返回元素的索引
int lastRet = -1; // 最后一个返回元素的索引
//将实际修改集合次数 赋值 给预期修改次数
//在迭代的过程中,只要实际修改次数和预期修改次数不一致就会产生并发修改异常
//由于expectedModCount是Itr的成员变量,那么只会被赋值一次!!!
//同时由于集合调用了三次add方法,那么实际修改集合次数就是 3,因此expectedModCount的值也是 3
int expectedModCount = modCount;
public boolean hasNext()
return cursor != size;
//获取元素的方法
public E next()
//每次获取元素,会先调用该方法校验 预期修改次数是否 == 实际修改次数
/*
tips:
if(s.equals("hello"))
list.remove("hello");
当if表达式的结果为true,那么集合就会调用remove方法
*/
checkForComodification();
//把下一个元素的索引赋值给i
int i = cursor;
//判断是否有元素
if (i >= size)
throw new NoSuchElementException();
//将集合底层存储数据的数组赋值给迭代器的局部变量 elementData
Object[] elementData = ArrayList.this.elementData;
//再次判断,如果下一个元素的索引大于集合底层存储元素的长度 并发修改异常
//注意,尽管会产生并发修改异常,但是这里显示不是我们要的结果
if (i >= elementData.length)
throw new ConcurrentModificationException();
//每次成功获取到元素,下一个元素的索引都是当前索引+1
cursor = i + 1;
//返回元素
return (E) elementData[lastRet = i];
final void checkForComodification()
//如果预期修改次数 和 实际修改次数不相等 就产生并发修改异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//集合的remove方法
public boolean remove(Object o)
if (o == null)
for (int index = 0; index < size; index++)
if (elementData[index] == null)
fastRemove(index);
return true;
// 案例三:已知集合:List list = new ArrayList();里面有三个元素:"hello"、"PHP"、"JavaSE",使用迭代//器
// 遍历集合看有没有"PHP"这个元素,如果有,就使用集合对象删除该元素
// 结果图
else
for (int index = 0; index < size; index++)
if (o.equals(elementData[index]))
fastRemove(index);
return true;
return false;
//快速删除方法
private void fastRemove(int index)
//最最最关键的一个操作,集合实际修改次数++,那么这个时候由原来的3变成4
//but迭代器的预期修改次数还是3!!!
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//还有一个很关键的操作,集合的长度也发生了改变
elementData[--size] = null;
以上是关于ArrayList精讲(源码分析)---Java集合的主要内容,如果未能解决你的问题,请参考以下文章
Java中arraylist和linkedlist源码分析与性能比较