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源码分析

Java-ArrayList源码分析

[Java源码分析]ArrayList源码分析

Java中arraylist和linkedlist源码分析与性能比较

JAVA——底层源码阅读——集合ArrayList的实现底层源码分析

JAVA集合之ArrayList源码分析