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

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高,但是也有一定概率问到的,还是那句话,小心使得万年船





在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_48254340/article/details/108370791