java集合源码分析(三)--ArrayList源码

吐槽

周末啊,冷啊啊啊啊,然后怕自己周六中午睡起来都晚上了,就不睡午觉了去实验室看下ArrayList的源码。
之前自己学集合只是简单的看了下用法,写项目的时候虽然用这块但是也没仔细看下这块到底咋实现的。

ArrayList的基本功能

首先这个货是个数组
数组就是存放东西的一个仓库
但是这个和普通的普通的数组还是有区别的
它的特殊的地方就是可以动态的添加或者减少这个数组里面的元素emmmmm//当然也是有限制的不可能无限放东西

我们今天要看的就是ArrayList的几个问题

  • Arraylist动态添加或者减少怎么样实现的?
  • 它的线程的安全性?
  • 它的最大的容量是多少?
  • java中Array和ArrayList区别?

ArrayList类的介绍

首先去文档上看下这块的继承关系
《java集合源码分析(三)--ArrayList源码》
《java集合源码分析(三)--ArrayList源码》
发现这个货继承了AbstractList然后实现了四个个接口List, RandomAccess, Cloneable, Serializable
根据我们看他继承的类和试下的接口看到他有一下的能力

  • ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能
  • ArrayList 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。
  • ArrayList 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
  • ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。

我们还是去看下这块的源码吧233也不是很长,我们一部分一部分看

类的成员变量介绍

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
    //可序列化的版本号
    private static final long serialVersionUID = 8683452581122892189L;
    //默认的数组大小为10 重点
    private static final int DEFAULT_CAPACITY = 10;
    //实例化一个空的数组 当用户指定的ArrayList为0的时候 返回这个
    private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
    //一个空数组实例 当用户没有指定 ArrayList 的容量时(即调用无参构造函数),返回的是该数组==>刚创建一个 ArrayList 时,其内数据量为 0。
//当用户第一次添加元素时,该数组将会扩容,变成默认容量为 10(DEFAULT_CAPACITY) 的一个数组===>通过 ensureCapacityInternal() 实现
//它与 EMPTY_ELEMENTDATA 的区别就是:该数组是默认返回的,而后者是在用户指定容量为 0 时返回
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
    //存放List元素的数组 保存了添加到ArrayList中的元素。实际上,elementData是个动态数组 ArrayList基于数组实现,用该数组保存数据, ArrayList 的容量就是该数组的长度
//该值为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,当第一次添加元素进入 ArrayList 中时,数组将扩容值 DEFAULT_CAPACITY(10)

    transient Object[] elementData;
    //List中元素的数量 存放List元素的数组长度可能相等,也可能不相等
    private int size;
    //这个数字就是最大存放的大小emmmmmm
    private static final int MAX_ARRAY_SIZE = 2147483639;

里面也蛮清楚的有两个总要的对象:
elementData 数组 “Object[]类型的数组,后面的初始化,其他方面有很重要的用处
size 这个是动态数组的实际大小

构造方法

有三个

public ArrayList(int var1) {
    if (var1 > 0) {
    //创建一样大的elementData数组
        this.elementData = new Object[var1];
    } else {
        if (var1 != 0) {
        //传入的参数为负数时候 报错
            throw new IllegalArgumentException("Illegal Capacity: " + var1);
        }
        //初始化这个为空的数组
        this.elementData = EMPTY_ELEMENTDATA;
    }

}
//构造方法 无参 数组缓冲区 elementData = {}, 长度为 0
//当元素第一次被加入时,扩容至默认容量 10
public ArrayList() {
      //初始化这个为空的数组
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//构造方法,参数为集合元素
public ArrayList(Collection<? extends E> var1) {
//将集合元素转换为数组,然后给elementData数组
    this.elementData = var1.toArray();
    if ((this.size = this.elementData.length) != 0) {
    //如果不是object类型的数组,转换成object类型的数组
        if (this.elementData.getClass() != Object[].class) {
            this.elementData = Arrays.copyOf(this.elementData, this.size, Object[].class);
        }
    } else {
        this.elementData = EMPTY_ELEMENTDATA;
    }

}

构造方法里面这块就发现elementData数组真的是作用很大啊
我们发现这块它被修饰的时候是用transient修饰的

transient是干嘛的啊

当对象被序列化时(写入字节序列到目标文件)时,transient阻止实例中那些用此关键字声明的变量持久化;当对象被反序列化时(从源文件读取字节序列进行重构),这样的实例变量值不会被持久化和恢复。例如,当反序列化对象——数据流(例如,文件)可能不存在时,原因是你的对象中存在类型为java.io.InputStream的变量,序列化时这些变量引用的输入流无法被打开。

那么为什么ArrayList里面的elementData为什么要用transient来修饰?

因为ArrayList不能序列化和反序列化吗?肯定不是,是因为elementData里面不是所有的元素都有数据,因为容量的问题,elementData里面有一些元素是空的,这种是没有必要序列化的。ArrayList的序列化和反序列化依赖writeObject和readObject方法来实现。可以避免序列化空的元素。
序列化的

存放元素和改变容量的方法

//改变数组的长度,使长度和List的size相等。
//集合中元素个数的size和表示集合容量的elementData.length可能不同,在不太需要增加//集合元素的情况下容量有浪费,可以使用trimToSize方法减小elementData的大小
public void trimToSize() {
    ++this.modCount;//继承自AbstractList中的字段,表示数组修改的次数,数组每修改一次,就要增加modCount
    if (this.size < this.elementData.length) {
        this.elementData = this.size == 0 ? EMPTY_ELEMENTDATA : Arrays.copyOf(this.elementData, this.size);
    }

}
//确定ArrayList的容量
//判断当前elementData是否是EMPTY_ELEMENTDATA,若是设置长度为10
public void ensureCapacity(int var1) {
    int var2 = this.elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA ? 0 : 10;
    if (var1 > var2) {
        //需要扩充
        this.ensureExplicitCapacity(var1);
    }

}
//最小扩充容量,默认是 10
//如果elementData为空的时候 看下长度和10比较的结果,找最大的
//判断是不是空的ArrayList,如果是的最小扩充容量10,否则最小扩充量为0
//上面无参构造函数创建后,当元素第一次被加入时,扩容至默认容量 10,就是靠这句代码
private void ensureCapacityInternal(int var1) {
  // 若用户指定的最小容量 > 最小扩充容量,则以用户指定的为准,否则还是 10
    if (this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        var1 = Math.max(10, var1);
    }
    //扩容
    this.ensureExplicitCapacity(var1);
}
//minCapacity和默认大小(10)比较,如果需要扩大容量,继续调用
private void ensureExplicitCapacity(int var1) {
    ++this.modCount;
    if (var1 - this.elementData.length > 0) {
        this.grow(var1);
    }

}
//扩容的操作
private void grow(int var1) {
 // 防止溢出代码
    int var2 = this.elementData.length;
    //容量扩容为之前的1.5倍
    int var3 = var2 + (var2 >> 1);
    //对新的容量进行判断
    // 若 newCapacity 依旧小于 minCapacity
    if (var3 - var1 < 0) {
        var3 = var1;
    }
    // 若 newCapacity 大于最大存储容量,则进行大容量分配
    if (var3 - 2147483639 > 0) {
        var3 = hugeCapacity(var1);
    }
    //复制旧元素到新的数组上面
    this.elementData = Arrays.copyOf(this.elementData, var3);
}
//这个就是判断大小的
private static int hugeCapacity(int var0) {
    if (var0 < 0) {
    //如果传入的小于0的话 报错
        throw new OutOfMemoryError();
    } else {
    //大于0的话 如果大于 ((2^31)-1) = 2147483647-8 = 2147483639
        return var0 > 2147483639 ? 2147483647 : 2147483639;
    }
}

我们看到这块的代码
扩容的时候利用位运算把容量扩充到之前的1.5倍
比如说用默认的构造方法,初始容量被设置为10。当ArrayList中的元素超过10个以后,会重新分配内存空间,使数组的大小增长到16。

可以通过调试看到动态增长的数量变化:10->16->25->38->58->88->…

将ArrayList的默认容量设置为4。当ArrayList中的元素超过4个以后,会重新分配内存空间,使数组的大小增长到7。

可以通过调试看到动态增长的数量变化:4->7->11->17->26->…

公式就是 新的容量 = (旧的容量*3)/2 +1;

然后还要检测下大小,不能超出最大的范围

那那那这个Java中ArrayList最大容量是多少啊?
大约是8G
看下源码发现是2147483639 这个数字emmmmmmmmm
看下别人的源码是private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE – 8;好像是我看的源码版本问题

Integer.MAX_VALUE 是 2的31次方减一
为什么要减8,,,原因好像是因为只是为了避免一些机器内存溢出, -8 是为了减少出错的几率,虚拟机在数组中保留了一些头信息。避免内存溢出。

增加

//这个是 不指定位置的话 添加到数组末尾
public boolean add(E var1) {
    this.ensureCapacityInternal(this.size + 1);
    this.elementData[this.size++] = var1;
    return true;
}
//指定位置的情况 
public void add(int var1, E var2) {
    //检验下是否插入的位置在数组容量范围内
    this.rangeCheckForAdd(var1);
    //检查是否需要扩容
    this.ensureCapacityInternal(this.size + 1);
    System.arraycopy(this.elementData, var1, this.elementData, var1 + 1, this.size - var1);
    //腾出新空间添加元素
    this.elementData[var1] = var2;
    //修改数组内元素的数量
    ++this.size;
}
private void rangeCheckForAdd(int var1) {
    if (var1 > this.size || var1 < 0) {
        throw new IndexOutOfBoundsException(this.outOfBoundsMsg(var1));
    }
}
//检验下是否插入的位置在数组容量范围内
private void rangeCheckForAdd(int var1) {
    if (var1 > this.size || var1 < 0) {
        throw new IndexOutOfBoundsException(this.outOfBoundsMsg(var1));
    }
}
//检测是否要扩容
private void ensureCapacityInternal(int var1) {
    if (this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        var1 = Math.max(10, var1);
    }

    this.ensureExplicitCapacity(var1);
}

private void ensureExplicitCapacity(int var1) {
    ++this.modCount;
    if (var1 - this.elementData.length > 0) {
        this.grow(var1);
    }

}

查找

它这个有两个查找
普通查找
逆序查找 思路也蛮清楚的

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;
}
public int lastIndexOf(Object o) {
    if (o == null) {
        for (int i = size-1; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = size-1; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

删除

这个和数组的删除类似的emmmmmmmmm
都是把指定位置的元素删除后,它后面的元素统一向前移动一位

public E remove(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    modCount++;
    E oldValue = (E) elementData[index];
    //要移动的长度
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 将最后一个元素置空
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

 //移除指定的第一个元素
// 如果list中不包含这个元素,这个list不会改变
// 如果包含这个元素,index 之后的所有元素依次左移一位

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}
//快速删除下标第index的元素
private void fastRemove(int index) {
        modCount++;//这个地方改变了modCount的值了
        int numMoved = size - index - 1;//移动的个数
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                    numMoved);
        elementData[--size] = null; //将最后一个元素清除
    }

ArrayList线程不安全的原因

虽然之前一直背的是ArrayList的线程不安全,但是还是不知道是为什么,操作系统课上对这块的理解就是当两个线程请求数据的时候,可能一个线程不小心把另一个线程的里面的东西改了,这块又和计算机的存储有关了
一般来说,,,遇到 变量++这种的,,,很容易遇到线程不安全的问题

我们来看下这块的线程不安全的原因
在之前的成员变量里面我们说过里面有两个货特别重要
transient Object[] elementData;
private int size;

elementData是个动态数组 ArrayList基于数组实现,用该数组保存数据, ArrayList 的容量就是该数组的长度
个size变量用来保存当前数组中已经添加了多少元素

外面再进行add()操作时候的一系列源代码

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

之前上面分析了这几个函数了的 这块就不讲了
主体思路就是我要添加一个元素,先进行判断一下,看下里面空间够不够,不够的话我就扩容什么的,然后再把这个元素加进去。里面其实就是两部操作
1判断elementData数组容量是否满足需求
2在elementData对应位置上设置值

所以,这块就是出现线程不安全的第一个地方,在多个线程进行add操作时可能会导致elementData数组越界。具体逻辑如下:

  1. 列表大小为9,即size=9
  2. 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断
  3. 线程B此时也进入add方法,它获取到size的值也为9,也开始调用ensureCapacityInternal方法。 线程B此时也进入add方法,它获取到size的值也为9,也开始调用ensureCapacityInternal方法
  4. 线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回
  5. 线程B也发现需求大小为10,也可以容纳,返回。 线程A开始进行设置值操作, elementData[size++] = e线程B也发现需求大小为10,也可以容纳,返回
  6. 线程A开始进行设置值操作, elementData[size++] = e线程A开始进行设置值操作, elementData[size++] = e操作。此时size变为10。
  7. 线程B也开始进行设置值操作,它尝试设置elementData[10] =线程B也开始进行设置值操作,它尝试设置elementData[10] =e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException.

然后第二个线程不安全的地方就是 elementData[size++] = e 设置值的操作同样会导致线程不安全。这块不是原子操作,所以会脏数据,他由两步操作构成
1elementData[size] = e;
2size = size + 1;

所以这块也就是在多线程的时候执行的话出现问题了

  1. 列表大小为0,即size=0
  2. 线程A开始添加一个元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。线程A开始添加一个元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。
  3. 接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到size的值依然为0,于是它将B也放在了elementData下标为0的位置上。
  4. 接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到size的值依然为0,于是它将B也放在了elementData下标为0的位置上。接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到size的值依然为0,于是它将B也放在了elementData下标为0的位置上。
  5. 线程A开始将size的值增加为1线程A开始将size的值增加为1
  6. 线程B开始将size的值增加为2线程B开始将size的值增加为2
    这样线程AB执行完毕后,理想中情况为size为2,elementData下标0的位置为A,下标1的位置为B。而实际情况变成了size为2,elementData下标为0的位置变成了B,下标1的位置上什么都没有。并且后续除非使用set方法修改此位置的值,否则将一直为null,因为size为2,添加元素时会从下标为2的位置上开始。

如何使ArrayList线程安全

  • 继承Arraylist,然后重写或按需求编写自己的方法,这些方法要写成synchronized,在这些synchronized的方法中调用ArrayList的方法。
  • List list = Collections.synchronizedList(new ArrayList());

当多线程访问这些容器类时,可能会出现数据同步导致的问题,java的工具类java.util.Collections提供了将非同步对象转换为同步对象的方法
当多线程访问这些容器类时,可能会出现数据同步导致的问题,java的工具类java.util.Collections提供了将非同步对象转换为同步对象的方法

自己对ArrayList的语言总结

因为昨天学长给我们模拟面试,然后我发现昨天早上才把源码看了,但是自己还是讲东西给面试官还是很卡顿。所以我觉的把每次学的东西自己组织语言过一遍,然后再给别人讲就会好一点。

问:简单介绍下ArrayList
答:ArrayList是以数组实现,可以自动扩容的动态数组,当超出限制的时候会增加50%的容量,用ystem.arraycopy()复制到新的数组,因此最好能给出数组大小的预估值。默认第一次插入元素时创建大小为10的数组。arrayList的性能很高效,不论是查询和取值很迅速,但是插入和删除性能较差,该集合线程不安全。

问:ArrayList的自动扩容怎么样实现的
关键字:elementData size ensureCapacityInternal
答:每次在add()一个元素时,arraylist都需要对这个list的容量进行一个判断。如果容量够,直接添加,否则需要进行扩容。在1.8 arraylist这个类中,扩容调用的是grow()方法。
在核心grow方法里面,首先获取数组原来的长度,然后新增加容量为之前的1.5倍。随后,如果新容量还是不满足需求量,直接把新容量改为需求量,然后再进行最大化判断。
通过grow()方法中调用的Arrays.copyof()方法进行对原数组的复制,在通过调用System.arraycopy()方法进行复制,达到扩容的目的。

问:ArrayList的构造方法过程答:ArrayList里面有三种构造方法,第一种:无参的构造方法 先将数组为空,第一次加入的时候 然后扩充默认为10, 第二种是有参的构造方法 ,直接创建这个数组 第三种是传入集合元素,先将集合元素转换为数组,把不是object的数组转化为object数组。

问:ArrayList可以无限扩大吗?答:不能,大于是8G,因为在ArrayList扩容的时候,有个界限判断。 private static final int MAX_ARRAY_SIZE = 2147483639,2的31次方减一然后减8,-8 是为了减少出错的几率,虚拟机在数组中保留了一些头信息。避免内存溢出。

    原文作者:java集合源码分析
    原文地址: https://blog.csdn.net/sakurakider/article/details/83687077
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞