精通ArrayList,关于ArrayList你想知道的一切

ArrayList 数据结构 扩容 序列化 线程安全


前言

在做Java开发中,ArrayList是最常用的数据结构之一,我们用它来存储一个数据列表。初始化一个ArrayList对象之后,我们可以使用它提供的诸多的方法:插入,指定位置插入,批量插入,获取,删除,非空判断,存量获取等。

虽然我们都熟练使用,但是否有过这样的疑问:ArrayList是怎么保存我add()进去的数据的呢?当我new 一个ArrayList对象的时候,他有多大容量?我初始化了一个容量为10的ArrayList,却能插入11个元素,它是怎么扩容的呢?……下面将会从ArrayList源码来看这些问题。

ArrayList 内部结构,和常用方法实现

ArrayList是基于数组存储。打开ArrayList源码发现其中有个变量表明:

    /**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData;

变量的注释是说,这个数组是用来存储ArrayList元素的,数组的长度即是ArrayList的容量。一个空ArrayList中的elementData是一个空数组,当第一次添加数据的时候,容量会扩充到DEFAULT_CAPACITY(也就是10)。

实例化方法

ArrayList有两个实例化方法,也称构造函数。无参实例化方法代码如下:

	private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

这个实例化方法很简单,其实就是给存储元素的数组elementData赋值——一个空的Object数组。

有参的实例化方法如下:

private static final Object[] EMPTY_ELEMENTDATA = {};
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);
}
}

方法接收参数initialCapacity做为初始化elementData的长度,如果这个数小于0抛异常,如果等于0 结果和无参构造函数一样。

添加元素 add()方法

添加方法有两个,一个是普通插入,一个是指定位置插入。普通方法代码如下:

    public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

方法第一行是和容量相关的,下面详细分析。这里主要看第二行,添加元素其实就是将目标元素e放入数组elementData的size下标处。同时让size加1。下面在看指定位置插入:

public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,size - index);
elementData[index] = element;
size++;
}

方法接收两个参数:index--插入位置,element--目标元素。第一行检查插入位置是否合理(0 < index <size),第二行统一是容量方面的。我们需要注意第三行,调用System.arraycopy方法来做“元素移位”,位移后再赋值elementData[index] = element。

假设list里边存储了A,B,C,D,E5个字母,现在调用add(3,"F"),将F插入。则调用System.arraycopy位移后示意图如下:

A B C D E
A B C D E

然后执行elementData[3]=“F”;整体过程

原始 A B C D E
位移 A B C D E
插入 A B C F D E

get()方法

    public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}

get方法第一行检查index是否合法,例如你肯定不能get(-1)。然后取出elementData数组中的index下标处的元素。

移除元素

    public E remove(int index) {
rangeCheck(index); modCount++;
E oldValue = 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;
}

remove方法移除elementData中index处的元素,并将这个元素返回。numMoved为需要发生位移的元素的个数。然后调用位移。然后移除最后一个元素。

怎么扩容的

上面我们看到add()方法中有个ensureCapacityInternal()方法,这个方法实际上完成了扩容操作。扩容操作分为两部分,1、确定最小容量的值(为了插入当前元素,容量所要达到的值)

这个最小容量值就是minCapacity变量。如果当前elementData数组为空,minCapacity=10。否则minCapacity=size+1;

如果minCapacity小于10,则取10。如果minCapacity大于elementData的长度,则调用grow()方法扩容。

2、调用grow()方法,grow()方法如下:

    private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5倍
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);
}

这个方法里确定elementData数组的新容量,并调用Arrays.copyOf完成扩容。确定新容量基于这三个数值:

  • 最小容量(size+1) minCapacity
  • 当前容量的1.5倍 elementData.lengt 1.5倍
  • 允许的最大容量

当调用new ArrayList();初始化的时候,elementData为空。第一次调用add()方法的时候,扩容至10。添加第11个元素的时候就需要扩容了,扩容后的值是15。10+(10>>1)=15。当添加第16个元素的时候,扩容至15+(15>>1)=22。

所以,可以粗略的理解成每次需要扩容时会扩大至原来的1.5倍,最大不超过Integer.MAX_VALUE。

序列化的问题

前面我们提到,ArrayList是基于数组的,它存储数据就是放在其数组类型成员变量上,也就是这个:

	transient Object[] elementData;

这个变量是用transient修饰的。transient关键字的作用是这么定义的:

如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。换句话来说就是,用transient关键字标记的成员变量不参与序列化过程。

莫非数组元素不参与序列化?那我辛辛苦苦add添加的数据岂不是没了。——然而,实际的情况不是这样。

ArrayList实现了writeObject()和readObject()方法,相当于定制了序列化和反序列化。尽管elementData变量是用transient修饰的。但是实际上elementData中的元素在序列化的时候被写入了。方法如下:

    private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject(); // Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size); // Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
} if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}

为什么要这么做呢?我们前面提到,ArrayList是动态扩容的,所以,当一个arrayList具有10个容量的时候,实际上可能只存放了一个元素。即size=1,而elementData.length=10。这个时候序列化elementData干啥呢,后面都是空值。

线程安全问题

ArrayList是线程不安全的。所以,多线程环境下容易出错。下面例子中启动20个线程,每个线程向共享的ArrayList中插入20个元素,最终输出ArrayList的长度。如果ArrayList是线程安全的,那么最终的结果应该是200.

public class Test {

    public static void main(String[] args)throws Exception {
ArrayList<Integer> list = new ArrayList<>();
final CyclicBarrier cb=new CyclicBarrier(20);
final CountDownLatch latch=new CountDownLatch(20);
for(int i=0;i<20;i++){
Thread t1 = new Thread(()->{
try{
long cur = System.nanoTime();
Thread.sleep(100);
System.out.println(cur+"准备好了");
cb.await();
for(int j=0;j<20;j++){
list.add(j);
}
System.out.println(cur+"执行完了");
latch.countDown();
}catch (InterruptedException |BrokenBarrierException e){
e.printStackTrace();
}
});
t1.start();
}
latch.await();
System.out.println("数组大小:"+list.size());
}
}

我随便执行一次,得到如下结果:

33927850979931准备好了

33927850899613准备好了

33927850816171准备好了

33927850686322准备好了

33927850595294准备好了

33927850751470准备好了

33927851168680准备好了

33927851239628准备好了

33927851821046准备好了

33927851959373准备好了

33927851351182准备好了

33927851887532准备好了

33927851070067准备好了

33927852038799准备好了

33927851433286准备好了

33927852370336准备好了

33927852448424准备好了

33927852126703准备好了

33927852215500准备好了

33927852281986准备好了

33927852281986执行完了

33927850979931执行完了

33927850899613执行完了

33927850816171执行完了

33927850686322执行完了

33927850595294执行完了

33927850751470执行完了

33927851168680执行完了

33927851239628执行完了

33927851821046执行完了

33927852370336执行完了

33927851433286执行完了

33927852038799执行完了

33927851959373执行完了

33927851070067执行完了

33927851887532执行完了

33927852215500执行完了

33927851351182执行完了

33927852126703执行完了

33927852448424执行完了

数组大小:397

和明显结果不对,再执行几次,还会发现,报数组越界。所以,ArrayList是线程不安全的。

精通ArrayList,关于ArrayList你想知道的一切的更多相关文章

  1. java中把list列表转为arrayList以及arraylist数组截取的简单方法

    java中把list列表转为arrayList以及arraylist数组截取的简单方法 package xiaobai; import java.util.ArrayList; import java ...

  2. AJPFX实例集合嵌套之ArrayList嵌套ArrayList

    案例:import java.util.ArrayList;import java.util.Iterator;import com.heima.bean.Person;public class De ...

  3. 代码实现集合嵌套之ArrayList嵌套ArrayList

    package com.loaderman.list; import java.util.ArrayList; import com.loaderman.bean.Person; public cla ...

  4. Java学习笔记51:数组转ArrayList和ArrayList转数组技巧

    ArrayList转数组: public class Test { public static void main(String[] args) { List<String> list = ...

  5. List list = new ArrayList();和ArrayList list=new ArrayList();的区别

    List是一个接口,而ArrayList 是一个类. ArrayList 继承并实现了List.List list = new ArrayList();这句创建了一个ArrayList的对象后把上溯到 ...

  6. 【MongoDB】从入门到精通mongdb系列学习宝典,想学mongodb小伙伴请进来

    最近一段时间在学习MongoDB,在学习过程中总共编写了四十余篇博客.从mongodb软件下载到分片集群的搭建. 从理论讲解到实例练习.现在把所有博客的内容做个简单目录,方便阅读的小伙伴查询. 一. ...

  7. ArrayList源码深度剖析,从最基本的扩容原理,到魔幻的迭代器和fast-fail机制,你想要的这都有!!!

    ArrayList源码深度剖析 本篇文章主要跟大家分析一下ArrayList的源代码.阅读本文你首先得对ArrayList有一些基本的了解,至少使用过它.如果你对ArrayList的一些基本使用还不太 ...

  8. 面试官:你说你精通源码,那你知道ArrayList 源码的设计思路吗?

    Arraylist源码分析 ArrayList 我们几乎每天都会使用到,但是通常情况下我们只是知道如何去使用,至于其内部是怎么实现的我们不关心,但是有些时候面试官就喜欢问与ArrayList 的源码相 ...

  9. ArrayList LinkedList源码解析

    在java中,集合这一数据结构应用广泛,应用最多的莫过于List接口下面的ArrayList和LinkedList; 我们先说List, public interface List<E> ...

随机推荐

  1. Oracle篇 之 查询行及概念

    Oracle: s_emp   s_dept  s_region 行:Row(tuple) 列:Column(attribute) conn:改变用户 Drop:删除用户  drop user bri ...

  2. Django之ContentType组件

    一.理想表结构设计 1.初始构建 1. 场景刚过去的双12,很多电商平台都会对他们的商品进行打折促销活动的,那么我们如果要实现这样的一个场景,改如何设计我们的表? 2. 初始表设计 注释很重要,看看吧 ...

  3. linux 安装所有软件可以使用这个网站搜索RPM包

    #很方便很实用  强烈推荐 https://pkgs.org/

  4. P4610 [COCI2011-2012#7] KAMPANJA

    题目背景 临近选举,总统要在城市1和城市2举行演讲.他乘汽车完成巡回演讲,从1出发,途中要经过城市2,最后必须回到城市1.特勤局对总统要经过的所有城市监控.为了使得费用最小,必须使得监控的城市最少.求 ...

  5. python 高阶函数之 reduce

    1.正常写法 >>> from functools import reduce >>> def fn(x, y): ... return x * 10 + y .. ...

  6. [WC2018]通道

    题目描述 http://uoj.ac/problem/347 题解 解法1 求三棵树的直径,看起来非常不可做,但是所有边权都是正的,可以让我们想到爬山. 所以我们可以按照BFS求树的直径的方法,随机一 ...

  7. 我眼中的支持向量机(SVM)

    看吴恩达支持向量机的学习视频,看了好几遍,才有一点的理解,梳理一下相关知识. (1)优化目标: 支持向量机也是属于监督学习算法,先从优化目标开始.   优化目标是从Logistics regressi ...

  8. django和celery结合应用

    django+celery项目结构 - project_name - app01 - __init__.py - admin.py - views.py - modes.py - tasks.py # ...

  9. DirectX11 With Windows SDK--19 模型加载:obj格式的读取及使用二进制文件提升读取效率

    前言 一个模型通常是由三个部分组成:网格.纹理.材质.在一开始的时候,我们是通过Geometry类来生成简单几何体的网格.但现在我们需要寻找合适的方式去表述一个复杂的网格,而且包含网格的文件类型多种多 ...

  10. TCP/IP教程

    一.TCP/IP 简介 TCP/IP 是用于因特网的通信协议. 通信协议是对计算机必须遵守的规则的描述,只有遵守这些规则,计算机之间才能进行通信. 什么是 TCP/IP? TCP/IP 是供已连接因特 ...