精通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. 操作docker容器

    Docker容器时镜像的一个运行实例,而镜像是静态的只读文件,容器带有运行时需要的可写文件层.如果认为虚拟机是模拟运行的一整套操作系统(包括内核.应用运行的环境和其他系统环境)和跑在上面的应用,那么D ...

  2. PS中如何提高修改psd图片的效率(自动选择工具)

    在photoshop中制作图片的时候,一般要养成保留psd格式的习惯,纵然普通时候jpg,png格式常用,考虑到以后可能需要修改,也应该备份一下.如果考虑到以后需要修改,可每次成品保存成两个,一个ps ...

  3. Go语言中的Iota

    一.复习常量 提到Iota这个关键字,就必须要复习一下Go语言的常量. 1.Go语言的常量一般使用const声明 2.Go语言的常量只能是布尔型.数字型(整数型.浮点型和复数)和字符串型 3.Go语言 ...

  4. MySQL——修改一个表的自增值

    语句 alter table <table name> auto_increment=<value>; 示例 mysql; Query OK, rows affected (0 ...

  5. mysql表加锁、全表加锁、查看加锁、解锁

    单个表锁定: 格式: LOCK TABLES tbl_name {READ | WRITE},[ tbl_name {READ | WRITE},……] 例子: lock tables db_a.tb ...

  6. JS控制开灯关灯

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  7. C++回顾day03---<异常>

    一:传统错误处理机制(C中通过函数返回来处理) int CalcRes(int n, int m, char ch, int& res) { ; switch (ch) { case '+': ...

  8. oldboy s21day07(深浅拷贝及文件操作)

    #!/usr/bin/env python# -*- coding:utf-8 -*- # 1.看代码写结果'''v1 = [1, 2, 3, 4, 5]v2 = [v1, v1, v1]v1.app ...

  9. 学习python笔记 协程

    下面将一个经典的消费者和生产者的案例进行分析: import time def consumer(): r = '' while True: n = yield r if not n: return ...

  10. postfix - SPF 防发件人欺骗

    安装 perl 依赖: yum install perl-Mail-SPF perl-Sys-Hostname-Long 下载 SPF 插件工具: wget https://launchpad.net ...