集合之ArrayList

ArrayList: 

内部结构就是对象数组 transient Object[] elementData,默认的大小是10。这边为什么要用transient? 在序列化时,不需要把整个数组都序列化,因为数组中可能包含很多空元素,序列化这些空元素是浪费空间和时间。在序列化时通过writeObject方法中把元素都写出去。在反序列化时通过readObject把二进制流重新组成数组。

 

构造器

1.  ArrayList(int initialCapacity)  构建一个initialCapacity大小的数组列表。

if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
}

2. ArrayList(), 构建出一个空的数组列表。在新增元素操作时会判断数组大小,如果空间不足,会自动扩充数组大小。如果是第一次扩充,会判断新增元素大小和默认大小10比较,取大者,即初始化数组最小为10. 以后扩展会以实际数组大小的1.5倍大小和实际需要的大小取大者进行扩展。

public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

 

3. ArrayList(Collection c):  把集合c 转换成 ArrayList。

 

常用APIs:

1. size():int, 获取List的元素数量。

2. isEmpty(): boolean, 判断集合是否为空。

3. toArray(): Object[], List转换成Array.

4. get(int index): E, 获取List中位置是index的值,本质就是取数组的index下标的值。

5. set(int index, E element): E 设置List中位置是index的值,本质就是设置数组是index的元素值。返回数组中原来index位置上的值。

6. add(E e) : boolean, 先检查size + 1是否要扩容,如果要扩容则按上面的规则进行扩容然后在size位置上赋值。

add(int index, E e), 在index位置添加元素 E, 需要把数组index及后面的元素都往后移位。再在index位置上添加值。

7. remove(int index):E, 从list的index位置上移除改元素。需要把index+1后面的元素都往前移一位,数组的最后元素设置为null. 返回删除的元素值。

remove(object o):boolean, 从list中删除元素o.

8. clear(), 清空数组,设置数组每个元素值为null. size 变0。

9. addAll(Collection c) : boolean, 添加集合c到当前集合的末尾。

addAll(int index, Collection c) : boolean, 添加集合到指定的位置index.

10. removeAll(Collection c):  删除集合中有c中的元素的item.

11. trimToSize(): 把数组的大小缩减到和集合元素一样大,以节省空间。

12. iterator(): Iterator, 获取迭代器。

13. removeIf(Predicate filter):boolean, 如果条件满足就删除该元素。

14. sort(Comparator c),  用比较器进行排序。

15. subList(int from, int to) 获取从位置from 到 to的子集合。

16. replaceAll(UnaryOperator<E> operator), 替换所有的操作。

 

1. ArrayList 简介:

 

ArrayList集合是Collection和List接口的实现类。底层的数据结构是数组。数据结构特点 : 增删慢,查询 快。线程不安全的集合!

许多程序员开发的时候,使用集合基本上无脑选取ArrayList!不建议这种用法。

ArrayList的特点:

  • 单列集合 : 对应与Map集合来说【双列集合】
  • 有序性 : 存入的元素和取出的元素是顺序是一样的
  • 元素可以重复 : 可以存入两个相同的元素
  • 含带索引的方法 : 数组与生俱来含有索引【下角标】

 

 2. ArrayList原理分析

2.1 ArrayList的数据结构源码分析

//空的对象数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认容量空对象数组,通过空的构造参数生成ArrayList对象实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//ArrayList对象的实际对象数组!
transient Object[] elementData; // non-private to simplify nested class access
//1、为什么是Object类型呢?利用面向对象的多态特性,当前ArrayList的可以存储任意引用数据//2、ArrayList有一个问题,不能存储基本数据类型!就是数组的类型是Object类型

 

2.2 ArrayList默认容量&最大容量 

//默认的初始化容量是10
private static final int DEFAULT_CAPACITY = 10;
//最大容量 : 2^31 - 1 - 8 = 21 4748 3639【21亿】
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

 

为什么最大容量要-8呢?

有的VM需要存储ArrayList集合的基本信息,比如list集合的最大容量等!

 

2.3 为什么ArrayList查询快,增删慢? 

ArrayList的底层数据结构就是一个Object数组,一个可变的数组,对于其的所有操作都是通过数组来实现的。

  • 数组是一种,查询快、增删慢!
  • 查询数据是通过索引定位,查询任意数据耗时均相同O(1)。查询效率贼高!
  • 删除数据时,要将原始数据删除,同时后面的每个数据往前迁移。删除效率就比较低!
  • 新增数据,在数组该位置及后面位置的数据都要后移一位!然后在数组该位置添加元素。添加效率极低! 

 

2.4 ArrayList初始化容量

ArrayList底层是数组,动态数组!底层是Object对象数组,数组存储的数据类型是Object,数组名字为elementData。

transient Object[] elementData;

 

1、创建ArrayList对象分析:无参数

创建ArrayList的之后,ArrayList容量是多少呢?回答10是错误的!回答0是正确【限定条件,在 JDK1.8及之后】, 这也是一种懒初始化,要添加元素时在初始化容量。

构造方法:

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//空数组!
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

 

在执行add()方法的时候初始化!【懒加载】

判断当前数组的容量是否有存储空间,如果没有初始化一个10的容量。

//想数组中,添加一个元素
public boolean add(E e) {
  //确保有容量,如果第一次添加,会初始化一个容量为10的list
  //size当前集合元素的个数,随着添加的元素递增
  ensureCapacityInternal(size + 1); // Increments modCount!!
  //添加元素
  elementData[size++] = e;
  return true;
}
//ensureCapacityInternal确保有容量,如果第一次添加,会初始化一个容量为10的list
private void ensureCapacityInternal(int minCapacity) {
    //两个方法
  ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// calculateCapacity(elementData, minCapacity) 拿着当前ArrayList的数组,与当前数组中的元素个数。计算容量
private static int calculateCapacity(Object[] elementData, int minCapacity)
{
  //ArrayList的数组 与默认的数组进行比较。、
  //{} == {}
  if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {/true
    //DEFAULT_CAPACITY = 10
    //minCapacity 1
    //1和10比谁大 10
    return Math.max(DEFAULT_CAPACITY, minCapacity);//计算之后,返回的初始化容量是10
  }
  return minCapacity;
}
// ensureExplicitCapacity() 确保不会超过数组的真实容量
private void ensureExplicitCapacity(int minCapacity) {
  //minCapacity 当前计算后容量 10
  modCount++;//对当前数组操作计数器
  // overflow-conscious code
  //最小的容量 : 10 - 当前数组的容量{} 0
  if (minCapacity - elementData.length > 0)
    grow(minCapacity);//做了扩容
}

 

 2. 创建ArrayList对象分析:带有初始化容量构造方法

//创建ArrayList集合,并且设置固定的集合容量
public ArrayList(int initialCapacity) {
    //initialCapacity 手动设置的初始化容量
    if (initialCapacity > 0) {//判断容量是否大于0,如果大于0
      //创建一个对象数组位指定容量大小,并且交给ArrayList对象
      this.elementData = new Object[initialCapacity];
      //如果设置的容量为0,设置默认数组
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;//默认的元素数据数组{}
    } else {
      //如果不是0,也不是大于0的数,会抛出非法参数异常!
      throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
    }
}       

 

注意 : 使用ArrayList的集合,建议如果知道集合的大小,最好提前设置。提示集合的使用效率! 

 

2.5 ArrayList扩容原理

add方法先要确保数组的容量足够,防止数组已经填满还往里面添加数据造成数组越界:

1. 如果数组空间足够,直接将数据添加到数组中

2. 如果数组空间不够了,则进行扩容。扩容1.5倍扩容。

3. 扩容 : 原始数组copy新数组中,同时向新数组后面加入数据

 

注意 : new的ArrayList的对象如果没有容量的,在第一次添加的add,会进行第一次扩容。0 -> 10!

 

//grow扩容数组
private void grow(int minCapacity) {
  //minCapacity 当前数组的最小容量,存储了多少个元素
  // overflow-conscious code
  //获取当前存储数据数组的长度
  int oldCapacity = elementData.length;
  //新的容量 = 旧的容量 + 扩容的容量【旧容量/2 = 0.5旧容量】
  //扩容1.5倍扩容
  int newCapacity = oldCapacity + (oldCapacity >> 1);
  //极端情况过滤 : 新的容量 - 旧的容量小于0【int值移除】
  if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;//不扩容了
    //新的容量,比ArrayList的最大值,还要打
  if (newCapacity - MAX_ARRAY_SIZE > 0)
    //设置新的容量为ArrayList的最大值,以ArrayList最大值为当前容量
    newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

 

 

总结:

1. 扩容的规则并不是翻倍,是原来容量的1.5倍

2. ArrayList的数组最大值Integer.MAX_VALUE - 8。不允许超过这个最大值

3. 新增元素时,没有严格的数据值的检查。所以可用添加null 。

 

 

2.6 ArrayList线程安全问题及解决方案

1. 导致ArrayList线程不安全的源码分析

ArrayList成员变量 

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    //ArrayList的Object的数组存所有元素。
    transient Object[] elementData; // non-private to simplify nested     class access
   //size变量保存当前数组中元素个数。
   private int size;
   //...
}

 

ArrayList的Object的数组存所有元素。

size变量保存当前数组中元素个数。

 

出现线程不安全源码之一 : add()方法 

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

 

add添加元素,实际做了两个大的步骤:

1. 判断elementData数组容量是否满足需求

2. 在elementData对应位置上设置值 

 

我们用多线程来复现问题:

/**
* 目标 : 线程安全问题复现
*/
public class Demo04 {
    //全局线程共享集合ArrayList
    protected static ArrayList<Object> arrayList = new             
    ArrayList<>();
  public static void main(String[] args) {
    //1.创建线程数组【500】
    Thread[] threads = new Thread[500];
    //2.遍历数组,想线程中添加500线程对象
    for (int i = 0; i < threads.length; i++) {
      threads[i] = new MyThread();
      threads[i].start();//启动线程
    }
    //3.遍历线程,等待线程执行完毕【等待所有线程执行完毕】
    for (int i = 0; i < threads.length; i++) {
      try {
        threads[i].join();//等待线程执行完毕
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    //线程执行内容 : 向集合中添加自己的线程名称
    //4.遍历list集合,获取所有线程的名称
    for (Object threadName : arrayList) {
      System.out.println("threadName = " + threadName);
    }
  }
}
//线程执行内容,是想集合中添加自己的线程名称
class MyThread extends Thread {
  @Override
  public void run() {
    try {
      //线程休眠1000
      Thread.sleep(1000);
      //向集合中添加自己的线程名称【操作共享内容,会出现线程安全问题】
      Demo04.arrayList.add(Thread.currentThread().getName());
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}    

 

运行代码结果可知,会出现以下几种情况:

  1. 打印null
  2. 某些线程并未打印
  3. 数组角标越界异常

 

 

我们来剖析一下上面三个问题出现的原因。

 

1. 数组索引越界

如果当前ArrayList的容量为10, 已经存储了9个元素,size = 9. 当两个线程A,B同时到达ensureCapacityInternal(size + 1)时,发现容量够用。然后线程A执行elementData[size++] = e; size 变成 10。线程B也执行elementData[size++] = e. size = 10 已经数组越界了,所以抛出异常。

 

 

2. 导致 null 和 某些线程并未打印

 当两个线程A,B同时到达ensureCapacityInternal(size + 1)时,发现容量够用然后线程A, B同时执行elementData[size++] = e, 此时在size这个位置上同时改值,后者把前者覆盖。然后两个线程都执行size++, size变成了2. size=1的位置就没有值,所以是null.

 

 

 

 

 

2. 解决方案

1. 使用Collections.synchronizedList。它会自动将我们的list方法进行改变,最后返回给我们 一个加锁了List

//线程安全问题解决方案1
//将集合改为同步集合
protected static List<Object> synList = Collections.synchronizedList(arrayList);

 

2. 使用JUC中的CopyOnWriteArrayList类进行替换。

//线程安全问题解决方案2 JUC 【最佳选择】
protected static CopyOnWriteArrayList<Object> copyOnWriteArrayList = new
CopyOnWriteArrayList<>();

 

2.7 ArrayList的Fail-Fast机制深入理解

什么是Fail-Fast机制?

"快速失败"即Fail-Fast机制,它是Java中一种错误检测机制!

当多钱程对集合进行结构上的改变,或者在迭代元素时直接调用自身方法改变集合结构而没有通知迭代器时,有可能会触发Fail-Fast机制并抛出异常【ConcurrentModificationException】。注意,是有可能 触发Fail-Fast,而不是肯定!

触发时机 : 在迭代过程中,集合的结构发生改变,而此时迭代器并不知情,或者还没来得及反应,便会 产生Fail-Fast事件.

 

再次强调,迭代器的快速失败行为无法得到保证!一般来说,不可能对是否出现不同步并发修改,或者自身修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。

Java.util包中的所有集合类都是快速失败的,而java.util.concurrent包中的集合类都是安全失败的;快速失败的迭代器抛出ConcurrentModificationException,而安全失败的迭代器从不抛出这个异常。

 

2.8 常见面试题

1、ArrayList的JDK1.8之前与之后的实现区别?

JDK1.6 : ArrayList像饿汉式,直接创建一个初始化容量为10的数组。缺点就是如果没使用之前就浪费空间。

 

 

JDK1.7之后 : ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时再创 建一个初始容量为10 的数组 。

private static final Object[] EMPTY_ELEMENTDATA = {};
public ArrayList() {
  this.elementData = EMPTY_ELEMENTDATA;
}

 

 2、List 和 Map 区别?

 

 

Map集合

  • 双列集合 : 一次存一对 key是不允许重复的,value可以重复
  • 一个key只能对应一个值value
  • Map集合三兄弟 : HashMap【无序集合】、LinkedHashMap【有序集合】、TreeMap【有序集 合,自带排序能力】

 

List集合

  • 单列集合 : 一次存一个
  • 有序集合
  • 元素可以重复
  • 带索引
  • List集合主要有两个实现类 : ArrayList和LinkedList 

 

3、Array 和 ArrayList 有何区别?什么时候更适合用 Array?

 

区别:

  • Array可以容纳基本类型和对象,而 ArrayList 只能容纳对象【底层是一个对象数组】。
  • Array指定大小的固定不变,而ArrayList大小是动态的,可自动扩容。
  • Array没有ArrayList 方法多。

 

尽管 ArrayList 明显是更好的选择,但也有些时候 Array 比较好用,比如下面的情况。

  • 1、如果列表的大小已经指定,大部分情况下是存储和遍历它们
  • 2、基本数据类型使用Array更合适。 

 

4. ArrayList 与 LinkedList 区别?

ArrayList

  • 优点:ArrayList 是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操 作效率会比较高(在内存里是连着放的),查询快。
  • 缺点:因为地址连续,ArrayList 要移动数据,所以插入和删除操作效率比较低

 

LinkedList 

  • 优点:LinkedList 基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址。对于新增和删除操作 add 和 remove ,LinedList 比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景。
  • 缺点:因为 LinkedList 要移动指针,所以查询操作性能比较低。查询慢,增删快

 

适用场景分析:

  • 当需要对数据进行对随机访问的情况下,选用 ArrayList 。
  • 当需要对数据进行多次增加删除修改时,采用 LinkedList 。
  • 当然,绝大数业务的场景下,使用 ArrayList 就够了。主要是,注意 : 最好避免 ArrayList 扩容,以及非顺序的插入。 

 

5. ArrayList 是如何扩容的?

如果通过无参构造的话,初始数组容量为 0 (JDK1.7以后) ,当真正对数组进行添加时,才真正分配容量, 默认分配容量是10。每次按 照1.5 倍(位运算)的比率通过 copeOf 的方式扩容。

 

6、ArrayList 集合加入 10万条数据,应该怎么提高效率?

ArrayList 的默认初始容量为 10 ,要插入大量数据的时候需要不断扩容,而扩容是非常影响性能的。因 此,现在明确了 10 万条数据了,我们可以直接在初始化的时候就设置 ArrayList 的容量! 这样就可以提高效率了。

 

7、ArrayList 与 Vector 区别?

ArrayList 和 Vector 都是用数组实现的,主要有这么三个区别:

1、Vector 是多线程安全的,线程安全就是说多线程访问同一代码,不会产生不确定的结果,而 ArrayList 不是。这个可以从源码中看出,Vector 类中的方法很多有 synchronized 进行修饰,这 样就导致了 Vector 在效率上无法与 ArrayList 相比。 Vector 是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。

2、Vector 可以设置增长因子,而 ArrayList 不可以,ArrayList集合没有增长因子。

3、两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的增加方式是不同。 ArrayList是按1.5倍的空间增长的,Vector在没有设置增长因子时,是按2倍空间增长的,否则按增长因子增长。

 

适用场景分析: 1、Vector 是线程同步的,所以它也是线程安全的,而 ArrayList 是线程无需同步的,是不安全 的。如果不考虑到线程的安全因素,一般用 ArrayList 效率比较高。 实际场景下,如果需要多线程访问安全的数组,使用 CopyOnWriteArrayList 。

posted @ 2020-12-22 19:46  闪闪的星光  阅读(299)  评论(0)    收藏  举报