再也不怕被面试官问到ArrayList啦!!!

tech2022-08-01  146

ArrayList

我们先了解一下数组Array的概念

数组是一种顺序储存的线性表,所有的元素的内容地址都是连续的

数组有个致命缺点,就是无法动态的修改容量

ArrayList 的底层是基于数组实现的,其容量大小可动态变化

Arraylist 与 LinkedList 区别

1.是否保证线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全

2.底层数据结构: Arraylist 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构

3.是否支持快速随机访问:** LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。

4.内存空间占用: ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间浪费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。

ArrayList和Vector的区别

这两个类都实现了List接口,他们都是有序集合,即存储在这两个集合中的元素的位置都是有顺序的

(1)同步性:

Vector是线程安全的,它的方法之间是线程同步的。ArrayList是线程序不安全的,它的方法之间是线程不同步的。在不考虑线程安全的情况下,使用ArrayList,因为它效率高;如果考虑线程安全的安全,使用Vector

(2)数据增长:

ArrayList与Vector都有一个初始的容量大小,10。

当添加元素超过集合的长度时,会对原数组进行扩容

Vector扩容的长度为原来数组 ✖ 2 ,ArrayList扩容的长度为原来数组 ✖ 1.5

ArrayList 常用属性

// ArrayList默认初始容量 10 private static final int DEFAULT_CAPACITY = 10; // 记录ArrayList集合中元素个数 private int size; // 定义一个Object[]类型的空数组 private static final Object[] EMPTY_ELEMENTDATA = {}; // ArrayList集合用来储存对象元素的数组 transient Object[] elementData;

ArrayList 构造器

// 无参构造器 public ArrayList() { // 空参创建空的ArrayList数组 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } public ArrayList(Collection<? extends E> c) { // 把传入的集合转换为数组 给Object类型的数组elementData elementData = c.toArray(); // if ((size = elementData.length) != 0) { // 当c.toArray转换的数组类型不等object[]类型时 if (elementData.getClass() != Object[].class) // 使用Arrays.copyOf(original,newLength,newType)方法 // @param original 要复制的数组 // @param newLength 新数组的长度 // @param newType 新数组的类型 elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // 否则弄个空的数组给他 this.elementData = 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); } }

ArrayList的扩容机制

当ArrayList调用 add() 方法时,当 size == elementData.length 的时候,会调用 grow() 方法对ArrayList进行扩容

// 数组扩容方法 // minCapacity = size + 1 private int newCapacity(int minCapacity) { // 当前数组长度 int oldCapacity = elementData.length; // 新的数组容量 = 旧数组长度 + 旧数组长度 / 2 // oldCapacity = 10 oldCapacity >> 1 --- 5 // 例如10的二进制为 : 0000 1010 >> 1 -----> 0000 0101 = 5 int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity <= 0) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) // 如果一开始没有定义初始容量这时newCapacity=0,返回默认容量10 // 可以得出当无参new 一个ArrayList()时候,这个ArrayList()为空集合,size为0 return Math.max(DEFAULT_CAPACITY, minCapacity); if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return minCapacity; } return (newCapacity - MAX_ARRAY_SIZE <= 0) ? newCapacity // 这里返回的长度为原数组的1.5倍 : hugeCapacity(minCapacity); }

由此可得出结论

通过无参构造器实例化的ArrayList初始容量为 0

在首次调用add()方法之后,返回一个容量为10的数组,后面每次扩容后新数组的长度为原数组长度的 1.5 倍

可以简单理解为:首次扩容为10 ,后面每次扩容为原数组的1.5倍

ArrayList频繁扩容导致添加性能极具下降,该如何处理

解决方案:通过有参构造器 ,提前设置好容量, 空间换时间

public static void main(String[] args) { // 创建集合对象 ArrayList<String> list = new ArrayList<>(); // 添加元素 list.add("A"); list.add("B"); list.add("C"); Long startTime = System.currentTimeMillis(); // 需求:需要添加100W跳数据 for (int i = 0; i < 1000000; i++) { list.add(i+""); } Long endTime = System.currentTimeMillis(); // 添加100W条数据用时:129 System.out.println("添加100W条数据用时:" + (endTime - startTime)); // ArrayList(initialCapacity) 通过有参构造器指定ArrayList集合容量 ArrayList<String> list1 = new ArrayList<>(1000000); startTime = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { list1.add(i+""); } endTime = System.currentTimeMillis(); // 提前声明数组长度后,添加100W条数据用时:51 System.out.println("提前声明数组长度后,添加100W条数据用时:" + (endTime - startTime)); }

ArrayLis插入或删除元素一定比LinkedList慢么

不一定

先看下ArrayList删除功能的源码 public E remove(int index) { // 范围校验 Objects.checkIndex(index, size); // 将源数组赋给es final Object[] es = elementData; // 将index对应的元素赋值给 oldValue E oldValue = (E) es[index]; // 调用fastRemove方法 fastRemove(es, index); return oldValue; } private void fastRemove(Object[] es, int i) { // 修改次数+1 modCount++; // newSize新数组的长度 final int newSize; if ((newSize = size - 1) > i) // 如果新数组长度newSize 大于 源数组size所要删除元素的索引 // 使用arraycopy方法进行拷贝 //注意 :这里源数组跟目标数组都为es,说明没有创建新数组 System.arraycopy(es, i + 1, es, i, newSize - i); // 将源数组最后一个元素设置为null,今早让垃圾回收机制对其进行回收 es[size = newSize] = null; }

通过源码分析,我们得出一个结论

ArrayList删除元素的时候,并没有创建一个新的数组

而是把数组中要删除的元素从后往前给覆盖了,然后把最后一个元素设为值null

在对比一下LinkedList删除功能的源码 public E remove(int index) { // 校验索引 checkElementIndex(index); /* if(return index >= 0 && index < size){ throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } */ return unlink(node(index)); } 看下unlink()方法里的node()方法 Node<E> node(int index) { // 如果删除索引index小于LinkedList集合长度的一半 if (index < (size >> 1)) { // 如果小于,那么把第一个节点赋值给x Node<E> x = first; // 从头开始往后找 for (int i = 0; i < index; i++) x = x.next; // 返回找到的节点 return x; } else { // 把最后一个节点赋值给x Node<E> x = last; // 从最后一个位置往前找 for (int i = size - 1; i > index; i--) x = x.prev; // 返回找到的节点 return x; } }

根据源码得出结论,LinkedList每次删除元素的时候都要逐个检索元素

如果LinkedList容量很大,删除的元素索引处于中间部位

这时LinkedList删除效率并不见的比ArrayList效率快

如何赋值某个ArrayList到另一个ArrayList中去

1. 使用clone()方法

这里是浅拷贝操作,不是添加操作 public Object clone() { try { // 把源集合中的成员信息拷贝到 V ArrayList<?> v = (ArrayList<?>) super.clone(); // 把源集合中的数组元素拷贝到v的数组元素中 v.elementData = Arrays.copyOf(elementData, size); // 把v中的修改次数变量重置为0 v.modCount = 0; return v; } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(e); } }

2. 使用ArrayList构造方法

这里回顾之前ArrayList的有参构造方法 public ArrayList(Collection<? extends E> c) { // ArrayList的toArray()返回结果是object类型的数组 elementData = c.toArray(); if ((size = elementData.length) != 0) { // 当c.toArray转换的数组类型不是object[]类型时 // 即c可能为其他类型的toArray()方法返回的不是object类型 if (elementData.getClass() != Object[].class) // 这时就使用Arrays.copyOf(original,newLength,newType)方法 // 通过Arrays.copyOf()方法把 c 转换为object类型的数组 (对应ArrayList的数组类型) // @param original 要复制的数组 // @param newLength 新数组的长度 // @param newType 新数组的类型 elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // 否则弄个空的数组给他 this.elementData = EMPTY_ELEMENTDATA; } }

3. 使用addAll用法

这里不是拷贝操作,而是将新集合添加到ArrayList集合的后面 public boolean addAll(Collection<? extends E> c) { // 把传入的集合转换为数组 给Object类型的数组elementData Object[] a = c.toArray(); modCount++; int numNew = a.length; if (numNew == 0) return false; Object[] elementData; final int s; // 这里判断,如果新集合长度加上源集合的元素个数 大于源集合容量时,进行grow()扩容 if (numNew > (elementData = this.elementData).length - (s = size)) elementData = grow(s + numNew); // 调用 System.arraycopy ()方法进行数组的拷贝 System.arraycopy(a, 0, elementData, s, numNew); size = s + numNew; return true; }

ArrayList(int initialCapacity)会不会初始化数组大小?

会初始化数组大小

但是ArrayList大小没有变,即 size = 0

因为在ArrayList的有参构造函数中,没有把int initialCapacity赋值给size

ArrayList中获得元素个数 size()方法

这里我们猜想一下,当调用set,remove方法时,肯定会有索引校验的方法

如果这个时候调用set()方法,就会出现索引越界异常

这是Java Bug里面的一个经典问题了,大家仔细品读一下。

结尾

可能大家会觉得 那也只能硬啃,慢慢品 ArrayList在面试里面问的频率没HashMap,ConcurrentHashMap高,但是也有一定概率问到的,还是那句话,小心使得万年船

最新回复(0)