栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

ArrayList 源码分析

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

ArrayList 源码分析

ArrayList集合类,相信大家都不陌生,并且可以说是非常熟悉,因为在日常开发中,需要用到集合的时候,大多数情况都是使用的这个类,那么为什么大家都使用这个类呢?它到底好在哪里?那我们通过源码分析来一探究竟。

一、初始化过程

首先来看一下 ArrayList 内部声明的变量都有哪些:

// 默认的容量大小为 10
private static final int DEFAULT_CAPACITY = 10;

// 声明了一个空的数组
private static final Object[] EMPTY_ELEMENTDATA = {};

// 又声明了一个默认容量的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//存储 ArrayList 元素的数组缓冲区
transient Object[] elementData;

// 集合的元素个数,默认是 0
private int size;

// 扩容时用来比较容量大小的,这个值已经非常的大了
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
 1. 无参构造 初始化

        当我们使用无参构造函数List list = new ArrayList()时,我们分析一下,它做了什么:

// 将 变量 elementData 赋值了一个默认容量大小为 10 的空数组
public ArrayList() {
      this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

        通过这一步,我们可以看到,ArrayList 存储数据,其实也是用的数组来完成的;

2. 有参构造 初始化

        看一下有参构造做了什么:

// 根据传入的大小,创建一个同等大小容量的数组
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+                           initialCapacity);
    }
}
 二、add method

        初始化过程了解了,我们看一下添加元素方法:

public boolean add(E e) {
    // 确保容量足够,先计算一下,当我们新增第一个元素的时候,size = 0
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

 可以看到,为了确保容量足够,先计算一下【ensureCapacityInternal(size + 1);】当我们新增第一个元素的时候,此时的size = 0,下面我们来看一下ensureCapacityInternal 方法干了啥:

// 第一次添加元素,此时 minCapacity = 1
private void ensureCapacityInternal(int minCapacity) {
    // 上一步,我们第一次添加元素的时候,size = 0, 此时这个方法的 minCapacity = 1
    // elementData 是在我们初始化的时候,赋值了一个容量大小为 10 的空数组
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

再来看一下 calculateCapacity 方法:

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 当我们使用无参构造时,进入判断,即 此时返回默认容量 10,因为传入的 minCapacity = 1
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

容量计算完了,我们回过头来,再看一下ensureExplicitCapacity:

// minCapacity = 10
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // elementData.length = 10,minCapacity = 10 ,当容量足够时,直接返回进行数组元素添加
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

分析到上面,我们发现,当使用无参构造初始化时,默认的数组容量是 10,在添加前10 个元素的时候即默认容量足够时,直接通过数组的下标进行添加元素:

public boolean add(E e) {
    // ...
    elementData[size++] = e;
    return true;
}

那么,当默认容量 10被我们使用完之后,当我们添加第 11 个元素时,会怎么样呢?小伙伴们可以继续通过上面的流程进行分析。我直接跳过前面几步,从计算容量这里开始:

// minCapacity = 11
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // elementData.length = 10,minCapacity = 11 ,容量不足,进入扩容方法
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

这里,我们继续分析 grow 方法【自动扩容】:

// 假设第 11 次添加元素,此时 minCapacity = 10
private void grow(int minCapacity) {
    // oldCapaccity = 10
    int oldCapacity = elementData.length;
    // 这里将 oldCapacity 向右偏移 1 位,即 1010 >> 1 = 0110 = 6
    // newCapacity = 10 + 6 = 16,自动扩容 1.6 倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 将原来的数组大小的容量扩大 1.6 倍后,通过复制元素,生成一个新的容量的数组,这里就完成了新增时的自动扩容
    elementData = Arrays.copyOf(elementData, newCapacity);
}

看到这里,相信大家肯定直呼 666,扩容原来这么简单,就是通过一个【位移运算+复制】就完成了自动扩容。在我们平时的开发工作中,如果遇到这种场景,也可以使用这种小技巧来完成。

三、get method

        集合中获取元素的方法,经常使用。

public E get(int index) {
    // 传入元素下标,首先要检查下标的位置是否超出集合的总容量大小
    rangeCheck(index);

    return elementData(index);
}
private void rangeCheck(int index) {
    // 当下标的大小 >= 集合的容量时,会报出我们经常看到的下标越界的异常,因为集合的下标是从 0 开始的
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E elementData(int index) {
	// 下标不越界,正常从数组中,通过下标获取元素
    return (E) elementData[index];
}
四、remove method

         集合中移除元素的方法,经常使用。

public E remove(int index) {
    // 同样,首先要检查下标是否越界
    rangeCheck(index);

    modCount++;
    // 取出要移除的元素
    E oldValue = elementData(index);
    // 算出要移动的下标位置
    int numMoved = size - index - 1;
    // 如果移除的是最后一个,则直接将最后一个元素置为 null
    if (numMoved > 0)
        // 通过计算出的下标的位置将后面的元素往前移动
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

代码比较简单,很容易理解,就不做一一描述。

ArrayList 中,还有其他的方法,不再逐一讲解,小伙伴们可以通过自行阅读源码进行学习。这里最主要的就是 add 添加元素的时候,自动扩容机制。

五、为什么不直接使用数组?

        我们通过分析 ArrayList 源码,得出结论,其实就是使用数组来进行存储数据的,那为什么不直接使用数组呢?为什么又要在数据的结构上进行封装呢?这个就要知道数组的特点,总结有以下几点:

    内存地址连续;可以通过下标访问成员,性能较高;增删操作带来更大的性能消耗(需要保证数据越界问题和动态扩容问题)

正是因为数组结构的优点和缺点,促使了 ArrayList 使其进行封装,使用 ArrayList 我们无须关心容量不够的问题,它自动帮我们完成了扩容,提供了更加方便的 API,使其操作元素更加便捷。

六、FailFast 机制【modCount++】

        快速失败的机制,Java 集合类为了应对并发访问在集合迭代过程中,内部结构发生变化的一种防护措施,这种错误检查的机制是为这种可能发生错误,通过抛出ConcurrentModificationException异常。

        我们在上面分析源码的过程中,会经常看到 modCount++;这一行代码,它的作用就是为了这个 FailFast 机制 而提供的;它是在父类 java.util.AbstractList.java 中声明的。

 protected transient int modCount = 0;

 

下面,我们来写一个DEMO,演示一下。

    新建一个ThreadListTest1.java
    public class ThreadListTest1 extends Thread {
    
    	private List list;
    
    	public ThreadListTest1(List list) {
    		this.list = list;
    	}
    
    	@Override
    	public void run() {
    		for (int i = 0; i < 10; i++) {
    			list.add(i);
    			System.out.println("list add :" + i);
    			try {
    				sleep(5);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    	}
    }
    再建一个ThreadListTest2.java
    public class ThreadListTest2 extends Thread {
    
    	private List list;
    
    	public ThreadListTest2(List list) {
    		this.list = list;
    	}
    
    	@Override
    	public void run() {
    		while (true) {
    			for (Iterator iterator = list.iterator();iterator.hasNext();) {
    				iterator.next();
    				try {
    					sleep(5);
    				} catch (InterruptedException e) {
    					e.printStackTrace();
    				}
    			}
    		}
    	}
    }
    测试一下
    public class ConcurrentListTest {
    
    	private static List list = new ArrayList();
    
    	public static void main(String[] args) {
    		ThreadListTest1 thread1 = new ThreadListTest1(list);
    		ThreadListTest2 thread2 = new ThreadListTest2(list);
    		thread1.start();
    		thread2.start();
    
    	}
    }

可以看到,在并发场景下,对同一个集合进行操作时,抛出了异常,这是为什么呢?我们通过源码可以看一下:

private class Itr implements Iterator {
    int cursor;
    int lastRet = -1;
    // 迭代的时候,首先将 当前的 modCount 赋值给了expectedModCount
    int expectedModCount = modCount;

    Itr() {}

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        // 执行 next 方法的时候,首先需要检查一下两个值是否相等
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

    // 省略其他源代码...
}
final void checkForComodification() {
	// 如果不相等的话,抛出异常
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

这就是 Java 集合在并发操作下的 控制方式,通过对比两个值是否相等来处理,有点类似于数据库的乐观锁。。

以上,是我个人的学习总结,仅供参考。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/778781.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号