Java集合框架源码分析:ArrayList

发布于:2024-06-24 ⋅ 阅读:(42) ⋅ 点赞:(0)

一、ArrayList特性

特性 描述
是否允许为null 允许
是否允许数据重复 允许
是否有序 有序
是否线程安全 非线程安全

二、ArrayList底层数据结构

底层的存储结构为数组,并且可以动态的调整数组的大小。

数组的特性

  • 增删慢:每次删除元素,都需要更改数组长度、拷贝以及移动元素位置。
  • 查询快:由于数组在内存中是一块连续空间,因此可以根据地址 + 索引的方式快速获取对应位置上的元素。

三、ArrayList继承关系

在这里插入图片描述

1、Serializable标记性接口

类的序列化由实现java.io.Serializable接口的类启用。不实现此接口的类将不会使用任何状态序列化和反序列化。可序列化类的所有子类型都是可序列化的。序列化接口没有方法和字段,仅用于标识可串行化的语义。

  • 序列化:将对象的数据写入到文件(写对象)。
  • 反序列化:将文件中对象的数据读取出来(读对象)。
public interface Serializable {
}

2、Cloneable标记性接口

一个类实现Cloneable接口指示Object.clone()方法,该方法对于该类的实例进行字段的复制是合法的。在不实现Cloneable接口的实例上调用对象的克隆方法会导致异常CloneNotSupportedException被抛出。简而言之:克隆就是依据已经有的数据,创造一份新的完全一样的数据拷贝。
源码

public interface Cloneable {
}

克隆的前提条件

  • 被克隆对象所在的类必须实现Cloneable接口。
  • 必须重写clone方法。

3、RandomAccess标记性接口

标记接口由List实现使用,以表名它们支持快速(通常为恒定时间)随机访问。此接口的主要目的是允许通用算法更改其行为,以便在应用于随机访问列表或顺序访问列表时提供良好的性能。

  • ArrayList:随机访问时间消耗小于顺序访问。
  • LinkList:随机访问的时间消耗大于顺序访问。

4、AbstractList抽象接口

四、ArrayList源码分析

1、构造方法

Constructor Constructor描述
ArrayList() 构造一个初始容量为十的空列表
ArrayList(int initialCapacity) 构造具有指定初始容量的空列表
ArrayList(Collection<? extends E> c) 构造一个包含指定集合的元素列表,按照它们由集合的迭代器返回的顺序。

2、添加方法

方法名 描述
public boolean add(E e) 将指定的元素追加到此列表的末尾。
public void add(int index, E element) 在此列表中的指定位置插入指定的元素。
public booean addAll(Collection<? extends E> c) 按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。
public boolean addAll(int index, Collection<? extends E> c) 将指定集合中的所有元素插入到此列表中,从指定的位置开始。

当未指定初始化大小的时候,ArrayList先是初始化为一个空数组;但在首次添加元素时,ArrayList才会初始化为一个容量为10的数组。这么做的原因是节省内存,一些场景可能只是单纯的定义了数组并没有真实的使用,直接初始化容量为10的数组会造成不必要的浪费。

指定的元素添加

	private static final int DEFAULT_CAPACITY = 10;

    public boolean add(E e) {
    	// 对内部容量进行判断,详情见下方的被调方法
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

扩容方法

// 1.主体函数
private void ensureCapacityInternal(int minCapacity) {
    // 对容量进行判断
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 2.通过最小容量和默认容量求出较大值 (用于第一次扩容)
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

// 3.判断是否需要进行扩容
private void ensureExplicitCapacity(int minCapacity) {
    // 实际修改集合次数+1 (在扩容的过程中没用,主要是用于迭代器中)
    modCount++;

    // 判断当前最小容量是否大于数组长度
    if (minCapacity - elementData.length > 0)
        
        // 将计算出来的容量传递给核心扩容方法
        grow(minCapacity);
}

// 4.核心扩容方法
private void grow(int minCapacity) {
    // 记录数组的实际长度
    int oldCapacity = elementData.length;
    // 核心扩容算法,原容量的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    // 判断新容量是否小于当前最小容量(第一次调用add方法必然小于)
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    
    // 判断新容量是否大于最大数组长度,如果
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        
        // 条件满足就计算出一个超大容量
        newCapacity = hugeCapacity(minCapacity);

    // 调用数组工具类方法,创建一个新数组,将新数组的地址赋值给elementData
    elementData = Arrays.copyOf(elementData, newCapacity);
}

// 5.超大容量计算
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

3、删除方法

方法名 描述
public E remove(int index) 移除指定位置的元素,并返回该位置的原元素。
public boolean remove(Object o) 移除首个为 o 的元素,并返回是否移除成功。
protected void removeRange(int fromIndex, int toIndex) 批量移除 [fromIndex, toIndex)内的多个元素,这里需要特别注意: toIndex不包括
public boolean removeAll(Collection<?> c) 批量移除指定的多个元素。

根据索引删除

public E remove(int index) {
    // 检查索引是否在范围内
    rangeCheck(index);

	// 实际修改集合次数+1 (在扩容的过程中没用,主要是用于迭代器中)
    modCount++;

	// 获取当前索引原来的数据
    E oldValue = elementData(index);

    // 计算集合需要移动元素的个数
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);、

	// 将源集合最后一个元素置为null,尽早让垃圾回收机制对其进行回收
    elementData[--size] = null; 

	//返回当前索引原来的数据
    return oldValue;
}

4、修改方法

set(int index, E element) 方法,设置指定位置的元素。

public E set(int index, E element) {
    // 检查索引是否在范围内
    rangeCheck(index);
    
    // 获取当前索引原来的数据
    E oldValue = elementData(index);
    
    // 替换后返回旧数据
    elementData[index] = element;
    return oldValue;
}

5、获取方法

get(int index) 方法,获取指定位置的元素。

public E get(int index) {
    // 检查索引是否在范围内
    rangeCheck(index);

	// 获取该索引元素
    return elementData(index);
}

6、转换方法

  • public Object[] toArray() 将 ArrayList 转换为 Object[] 数组。
  • public T[] toArray(T[] a) 将 ArrayList 转换为指定 T 泛型的数组。
/**
 * 将ArrayList转换成Object类型数组
 * @return Object[]
 */
@Override
public Object[] toArray() {
    // 返回的是 Object[] 类型,需要注意:转换成数组就相当于是将 ArrayList 的底层elementData暴露出去
    return Arrays.copyOf(elementData, size);
}
 
 
public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

7、迭代器

	// 获取迭代器的方法
    public Iterator<E> iterator() {
    	// 创建了一个对象
        return new Itr();
    }
 private class Itr implements Iterator<E> {
 		// 光标,默认值就是0
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        // 将集合实际的修改次数赋值给预期修改次数
        int expectedModCount = modCount;
		
		// 判断集合是否有元素
        public boolean hasNext() {
        	// 光标是否不等于集合的size
            return cursor != size;
        }

        public E next() {
            checkForComodification();
            int i = cursor;
            // 判断,如果大于集合的size就说明没有元素了
            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();
        }
 }

迭代器

  • 集合每次调用add方法的时候,实际修改次数变量的值会自增一次。
  • 在获取迭代器的时候,集合只会执行一次将实际修改集合的次数赋值给预期修改集合的次数。
  • 集合在删除元素的时候也会针对实际修改次数的变量进行自增的操作。

迭代过程中删除元素注意点

  • 当要删除的元素在集合的倒数第二个位置的时候,不会产生并发修改异常。
    • 原因:是因为 在调用hasNext方法的时候,光标的值和集合的长度一样,那么就会返回false就不会再去调用next方法获取集合的元素,既然不会调用next方法那么底层就不会产生并发修改异常。

8、清空方法

从列表中删除所有元素。

    public void clear() {
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

9、包含方法

    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

10、Fail-Fast机制

ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。

五、面试题

1、ArrayList是如何扩容的?

第一次扩容10,以后每次都是原容量的1.5倍。

2、ArrayList频繁扩容导致添加性能急剧下降,如何处理?

在明确容量的情况下,创建ArrayList对象时指定初始化容量。

3、ArrayList插入或删除元素一定慢吗?

4、ArrayList是线程安全的吗?

5、如何复制某个ArrayList到另一个ArrayList中去?

6、已知成员变量集合存储N多用户名称,在多线程的环境下,使用迭代器在读取集合数据的同时如何保证可以正常写入数据到集合?

7、ArrayList和LinkList的区别

参考

  • https://blog.csdn.net/mrluo735/article/details/133926158
  • https://www.bilibili.com/video/BV1gE411A7H8?p=32&vd_source=cd03889ff27e1a185b3e97e3ed96d260

网站公告

今日签到

点亮在社区的每一天
去签到