• 【Java集合】ArrayList精讲(源码分析)


    问题概览

    1.ArrayList是什么
    2.和Array有什么区别,效率怎么样
    3.增删改查及其代码
    4.源码剖析

    1.ArrayList是什么

    存储数据的一个容器,其底层是使用数组实现的。
    为什么需要ArrayList?

    • 可变容积。虽然其内部使用数组来存储元素,但是当元素个数到达一定程度时,可以创建新的数组并将元素拷贝过去,进而实现了可变容
      积。
    • 连续空间存储。同数组,地址是连续的,所以加入到ArrayList的元素是连续空间存储的。



    2.与Array区别

    Array:数组,定义时需要指定类型,即存储单一类型的数据。长度固定(正是因为数组长度固定,所以就需要一种数据结构,可以扩容用以存储更多数据。)
    ArrayList:长度不固定,可以指定长度,当元素个数增加到或者减少到各自的临界值,会触发扩容和缩容机制。底层是数组,当剩余容量过小时,会触发扩容机制


    创建ArrayList方式

    ArrayList<String> list = new ArrayList<>();
    //注意,创建时需要指定存储元素类型
    
    • 1
    • 2

    源码分析
    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
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    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);
    		}
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    源码分析
    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    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++;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    删除

    原理:因为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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    根据<元素删除>源码分析:
    关键点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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    根据<索引删除>源码分析:
    理解了上方的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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    修改方法

    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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    源码解析:
    很简单,根据索引来修改elementData中的值即可

    public E set(int index, E element) {
    	//范围校验
    	rangeCheck(index);
    	//先取出index对应的元素,且赋值给oldValue
    	E oldValue = elementData(index);
    	//将element直接覆盖index对应的元素
    	elementData[index] = element;
    	//返回被覆盖的元素
    	return oldValue;	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    查询

    调用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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    源码:
    该部分源码很简单,因为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));
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    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);
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    通过迭代器可以快速遍历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;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
  • 相关阅读:
    【联通】数据编排技术在联通的应用
    chmod文档权限
    DOM中text节点详解
    项目沟通管理案例题
    关于简单线性代数——矩阵乘法与行列式
    机器学习练习-决策树
    数据仓库 vs. 数据湖:解析两者的区别与优劣
    数据结构(持续更新)
    欧科云链研究院:仰传统机构之“鼻息”,RWA的关键不在于Web3技术
    程序设计与软件设计模式
  • 原文地址:https://blog.csdn.net/m0_52410356/article/details/125340795