ArrayList集合深度解析

一. 手写高仿ArrayList集合

基本原理思想

Arraylist集合底层使用动态数组实现,随机查询效率非常快,插入和删除需要移动整个数组、效率低。

1. 高仿ArrayList集合

public interface MyList<E> {
    /**
     * 集合的大小(长度)
     * @return
     */
    int size();
    /**
     * 往集合中添加我们的元素
     * @param e
     * @return
     */
    boolean add(E e);
    /**
     * 使用下标查询到我们的集合元素
     * @param index
     * @return
     */
    E get(int index);
    /**
     * 使用下标位置删除我们的元素
     * @param index
     * @return
     */
    public E remove(int index);
}
public class MyArraylist<E> implements MyList<E> {
    /**
     * elementData数据存放我们Arraylist所有的数据 transient作用不能序列化
     */
    transient Object[] elementData;
    /**
     * 给我们的数组容量赋值为空
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    /*
     * 数组的容量默认大小为0
     */
    private int size;
    /**
     * 数组默认容量大小
     */
    private static final int DEFAULT_CAPACITY = 10;
    /**
     * 2的31次方-1 -8
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    public MayiktArraylist() {
        // 给我们的数组容量赋值为空
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public boolean add(E e) {
        // 对我们的数组实现【扩容】
        ensureCapacityInternal(size + 1);
        // 对我们的数据元素【赋值】
        elementData[size++] = e;
        return true;
    }

    @Override
    public E get(int index) {
        rangeCheck(index);
        // 根据下标从数组中查询到数据
        return (E) elementData[index];
    }

    @Override
    public E remove(int index) {
        // 检查我们的下标位置是否越界
        rangeCheck(index);
        // 获取要删除的对象
        E oldValue = get(index);
        //计算移动的位置
        int numMoved = size - index - 1;
        // 判断如果删除数据的时候 不是最后一个的情况下,将删除后面的数据往前移动一位
        if (numMoved > 0) {
            System.arraycopy(elementData, index + 1, elementData, index,
                    numMoved);
        }
        // 如果numMoved 为0的情况下,说明后面不需要往前移动,直接将最后一条数据赋值为null
        elementData[--size] = null; // clear to let GC do its work
        return oldValue ;
    }

    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException("下标位置越界啦index:" + index);
    }

    private void ensureCapacityInternal(int minCapacity) {
        // 添加元素的时候 如果我们数组是为空的情况下
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //DEFAULT_CAPACITY =10;   minCapacity=0+1  DEFAULT_CAPACITY=10 10>1
            //minCapacity=10;
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }

    // modCount++; 增删改的时候  modCount++
    private void ensureExplicitCapacity(int minCapacity) {
        // 10- 数组长度 (0) 10-0 >0  作用:判断我们数组中是否需要继续扩容
        if (minCapacity - elementData.length > 0) {
            // 对我们的数组实现扩容
            grow(minCapacity);
        }
    }

    // length 和size区别
    private void grow(int minCapacity) {
        // 获取我们的数组的长度 old原来的 new新的 原来数组容量 0;
        int oldCapacity = elementData.length;
        // 新的容量 = 原来的容量 + 原来的容量/2 = 0
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 新的容量(0)-最小的容量(10) < 0  -10
        if (newCapacity - minCapacity < 0) {
            // 新的容量=10 作用:第一次对我们数组做初始化容量操作
            newCapacity = minCapacity;
        }
        // 判断我们扩容长度大于Integer 21 最大值的情况下
        // 限制我们数组扩容最大值
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 开始对我们的数组实现扩容 对我们的数据实现扩容(newCapacity) 将旧的数组数据复制到新的数组中,并可以指定长度
        elementData = Arrays.copyOf(elementData, newCapacity);//这里无非就是把相同的数据复制了一份,并重新指定了数组的length
    }

    /**
     * 判断我们最小的初始化容量
     * @param minCapacity minCapacity==最小的容量>Integer 21 最大值的情况下  Integer.MAX_VALUE
     *                    minCapacity 相当于当前添加元素的下标位置
     * @return
     */
    private static int hugeCapacity(int minCapacity) {
        /*
            Integer.MAX_VALUE : 2147483647
            Integer.MAX_VALUE + 1 :-2147483648
            只要大于IntegerMAX_VALUE,就会变为负数,所以这里判断是否小于0
         */
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
    }
}
public class Test {
    public static void main(String[] args) {

        MyArraylist<String> list= new MyArraylist<String>();

        for (int i = 0; i < 10; i++) {
            list.add("元素" + i);
        }
        System.out.println(list.size());

        list.add("元素11");

        System.out.println("删除数据之前:");
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
//        // 下一次数组扩容是在什么时候呢?
        list.remove(2);
        System.out.println("删除数据之后:");
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
//        // 数组的下标为0 开始 删除第一条数据 10=11
//        System.out.println(list.get(10));

    }
}

小结:第一次add的时候,会扩容/初始化 数组length为10,下一次扩容是在什么时候?

答案:添加第11条数据的时候【原理见ensureExplicitCapacity()方法】

添加第11条数据的时候,length会扩容到多少呢?
答案:15 【原理见grow()方法】
                 // 新的容量 = 原来的容量 + 原来的容量/2 = 10 + 10/2 = 15
                int newCapacity = oldCapacity + (oldCapacity >> 1);
以此类推,添加第16条的时候,会扩容到16+16/2=24

数组最大容量就是Integer.MAX_VALUE

2. 仔细看ArrayList的源码,不难发现,每次add和remove的时候,都会调用全局变量modCount++

为什么这么做呢,此时我们先引入一个理论:Fail-Fast机制原理

Fail-Fast是我们Java集合框架为了解决集合中结构发生改变的时候,快速失败的机制。

举例说明:

package com.example;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class Test004 {

    // 定义一个全局的集合存放 存在在高并发的情况下线程安全的问题
    private List<String> strings = new ArrayList<String>();
//    private List<String> strings = new CopyOnWriteArrayList<>();

    public static void main(String[] args) {
        new Test004().startRun();
    }

    public void startRun() {
        new Thread(new ThreadOne()).start();
        new Thread(new ThreadTWo()).start();
    }

    // 打印我们的数据
    private void print() {
        strings.forEach((t) -> System.out.println("t:" + t));
    }

    class ThreadOne implements Runnable {
        @Override
        public void run() {
            // 存储数据
            for (int i = 0; i < 10; i++) {
                strings.add("i:" + i);
                print(); // 打印数据
            }
        }
    }

    class ThreadTWo implements Runnable {
        @Override
        public void run() {
            for (int i = 10; i < 20; i++) {
                strings.add("i:" + i);
                print();
            }
        }
    }
}

执行main方法,会报错:ConcurrentModificationException即并发修改异常

那么我们定位到ArrayList的第1252行:

即我们调用下图的时候,会走到上图ArrayList源码(用for each的时候,底层还是用for循环去遍历)

for each原理:会把modCount赋值给一个临时变量/期望变量expectedModCount,for循环的时候,判断modCount的值是否发生变化,如果没发生变化,才会进行打印值等操作,如果发生变化,则会抛出并发修改异常。

为什么会抛出异常:modCount是全局的,可能会被add,remove进行更改,而我们的临时变量expectedModCount是局部的,不会更改。线程一和线程二可能会同时调用add方法,比如线程一调用完add,在打印之前,线程二调用add方法(modCount++),此时会导致modCount和expectedModCount不一致,则抛出并发修改异常。

解决上面Fail-Fast场景,可以使用CopyOnWriteArrayList,该集合是,添加和修改的时候都加了Lock锁。

CopyOnWriteArrayList特点:

  • 内部持有一个ReentrantLock lock = new ReentrantLock();
  • 底层是用volatile transient声明的数组 array
  • 读写分离,写时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给array

3. ArrayList删除原理

 这里我们重点分析System.arraycopy方法,首先贴出ArrayList的remove方法

System提供了一个静态方法arraycopy(),我们可以使用它来实现数组之间的复制,其函数原型是:

 public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length) ;参数解析如下:

src 源数组
srcPos 源数组要复制的起始位置
dest 目的数组
destPos 目的数组放置的起始位置
length 复制的长度

注意:src 和 dest都必须是同类型或者可以进行转换类型的数组。下面举例讲解:

package com.example;

public class Test003 {
    public static void main(String[] args) {

        Object[] objects = new Object[]{"0", "1", "2", "3"};

        // 要删除元素的索引,也就是目的数组放置的起始位置
        int index = 0;
        // 源数组要复制的起始位置
        int destPos = index + 1;
        // 要复制的长度
        int numMoved = objects.length - index - 1;

        System.arraycopy(objects, destPos, objects, index, numMoved);
        objects[objects.length - 1] = null;
        for (int i = 0; i < objects.length; i++) {
            System.out.println(objects[i]);
        }

    }
}

运行结果为:

那么疑问来了,我都删了,为什么还有四个元素?

答案:这是数组的长度为4,在实际ArrayList删除的时候,会发现,即集合的size会减1,但是数组的容量还是没变的,这点不要混淆了~  。如果要删除1,即前面的元素不会影响,只移动后面的,并把最后一个置为null。


下面贴出一段代码:

public class AAA {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        System.out.println(list.size());

        list.remove(3);
        System.out.println(list.size());
        System.out.println(list.get(4));
}

执行结果为:

说好的删了会置为null呢?原因是ArrayList源码中调用get()方法时有个下标监测判断~

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException("下标位置越界啦index:" + index);
    }
}

ArrayList的remove方法核心就是使用了System的静态方法arraycopy( );再次贴出我们手写的ArrayList源码:

可以发现,会先判断numMoved,如果numMoved大于0,证明要删除的不是最后一个元素,则执行System.arraycopy方法进行数组复制;复制完成后,会执行 elementData[--size] = null; 将最后一个元素置为null。并且size减1。(--size代表先执行size=size-1,然后再使用size的值,所以将数组的第size-1个元素,也就是最后一个元素置为null【注意索引从0开始】)

二. ArrayList,Vector,CopyOnWriteArrayList 对比

1. ArrayList与Vector的区别

说到这里,我们又会想到一个集合Vector,该集合是ArrayList的前身,相信有一定基础的小伙伴都了解过该集合。

这里我结合源码,列举一下ArrayList与Vector的区别:

相同点:底层都是采用数组实现

不同点:

① 默认初始化时候

Arraylist 默认 不会对我们数组做初始化

(第一次调用add方法的时候 才会初始化)

Vector 默认初始化的大小为10

② 扩容区别

ArrayList扩容:在原来数组的基础之上增加50%,即新容量是旧容量的1.5倍

Vector扩容:在原来数组基础之上再增加100%,即新容量是旧容量的2倍(如果没自定义容量capacityIncrement),所以新容量newCapacity = oldCapacity + oldCapacity;

那么如何自定义capacityIncrement,直接调用如下Vector的方法:

③ 线程是否安全(主要看增删,查的时候不存在线程安全问题)

ArrayList  默认的情况下 线程不安全的,因为没加线程同步,没加锁。

Vector 线程是安全的 效率是非常低的 查询 增加 、删除都加上了锁。

2. CopyOnWriteArrayList和Vector区别:

Vector:读,写,查都加上了synchronized同步锁(不建议使用)

CopyOnWriteArrayList:写,删的时候加了Lock锁,但读的时候没加锁;CopyOnWriteArrayList支持读多写少的并发情况

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章