IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> Arraylist源码分析 -> 正文阅读

[数据结构与算法]Arraylist源码分析

一个大腹便便的身穿格子衫的秃头老,缓缓的想你走来,心想看到这头秃的,就几根了,怎么也得顶级架构师吧!但是咱也是看过源码的人,不虚他!那么我们就开始今天的ArrayList旅程把!

小伙子,今天我就来考考你ArryList,看看你到底是骡子是🐎!

好嘞,那今天就看我的表现吧!(尼玛,你才是骡子,老子是🐎)!


秃头老:ArrayList知道吧,工作中经常用的集合,你来简单的介绍一下

我:首先ArrayList底层是用Object[]数组来实现的,可以实现动态增长;其次通过看源码我们可以得知它继承了AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable,其中List代表他是一个集合,RandomAccess是支持快速随机访问,Cloneable覆盖了clone方法,能被克隆,Serializable代表可以进行序列化传输。


秃头老:那你知道ArrayList和Vector的区别吗?(哎呦小伙子,还看过源码呀,来来接着靠你,不考到你不会,今天就不结束!)

我:ArrayList和Vector都是List的实现类,底层都是Object[]数组实现的,区别就是ArrrayList是线程不安全的,而Vector是线程安全的!

(这才哪跟哪,给我整点难的!)


秃头老:那你再说说ArrayList和LinkedList的区别!

我:(劳资给你分点说,让你看看有多细!)

1、从底层实现上来说,A(ArrayList)是通过Object[]存储的,L(LinkedList)是通过双向链表存储的(JDK1.6是循环链表,JDK1.7就变成了双向链表

2、从线程安全性来说,A和L都是线程不安全的!

3、从快速随机访问来说,A是支持快速随机访问的,L不支持,相当于遍历数组

4、从插入删除是否受位置影响来说,A如果在末尾插入删除时间复杂度为O(1),如果在第i个位置插入删除,则需要移动元素,时间复杂度为O(n-i),L在末尾插入和删除都是O(1),如果**指定位置的话是O(n)**需要先查找在插入删除!

5、从内存空间占用来说,A浪费主要在结尾都有一部分的预留空间,L主要是每个元素都比A占的空间大,因为需要直接前驱和直接后继!


秃头老:小伙子,看来确实是做过一些功课呀,那既然你看过源码,说说ArrayList的构造函数吧(老子要给挖坑了,准备给我跳进去!)

我:(TM的想问扩容机制,我早看透了!)

构造函数有两种,一种是无参构造,一种是有参构造。无参构造的话默认是赋值一个空对象,只有添加第一个元素的时候才会初始化容量为10的数组;有参构造有两个,一种是指定数组大小的,根据数组大小来初始化A,一种是传一个集合,把集合添加到数组中!

哝,来瞅瞅源码!

	private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 
	/**
     * 无参构造,默认是空数组,当添加第一个元素,初始化容量为10的数组
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }


	/**
     * 
     * 创建的时候指定集合大小
     * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    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中
     *
     * @param c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     */
    public ArrayList(Collection<? extends E> c) {
        // 把集合转换成数组
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

秃头老:既然你知道默认无参构造只有添加元素的时候才会初始化容量为10的数组,那它是什么实现的那?

我:(C)这个就说来话长了,那我们首先需要知道三大方法ensureCapacityInternal(int minCapacity),ensureExplicitCapacity(int minCapacity),grow(int minCapacity),我们从add(E e)方法说起

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

很简单,先调用了ensureCapacityInternal(size + 1),当我们第一次添加元素的时候参数为size+1=1。

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 计算数组所需最小容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

通过上面不难看出,因为第一次添加元素的时候elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA为true,就会把DEFAULT_CAPACITY(10)和minCapacity(1)来比较,从而返回10,这就把数组大小定了。

// 判断数组是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

此时minCapcity为10,elementData.length为0(空list,第一次添加),符合条件则进行扩容,我们在想当第二次添加的时候minCapacity为2,所以不会进行扩容,知道添加第11个元素的时候才会扩容!

// 数组扩容
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private void grow(int minCapacity) {
    // overflow-conscious code 老数组长度
    int oldCapacity = elementData.length;
    // 扩容1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 扩容后的大小 小于 所需最小容量,则扩容大小为 所需最小容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 扩容后大小 大于 最大长度
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 数组扩容
    elementData = Arrays.copyOf(elementData, newCapacity);
}

通过grow我们看出每次扩容1.5倍,当第一次添加的时候,oldCapacity = 0,newCapacity = 0,newCapacity - minCapacity < 0(0 -10)成立,所以newCapacity为10。

补充:

// 最大扩容大小
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
}

简单的来说,就是当添加第一个元素的时候,先调用ensureCapacityInternal判断是不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,是的话则把minCapacity该为10,调用ensureExplicitCapacity来进行grow,如果第一次扩容则会把扩容大小为minCapacity(10)结束!


秃头老:还不错哦,小伙子,那你有没有发现源码中有引起你注意的方法?(小崽子有点东西哦!)

我:(MD,还问,扩容机制都给你讲的明明白白的了!)

确实有哦,源码中经常用到System.arraycopy()Arrays.copyOf() 方法,这两个都是复制数组,区别就是后者会返回一个新的数组,前者复制到目标数组当中,我们看一下源码

// native 方法
// src:原数组 srcPos:原数组开始复制位置
// dest:目标数组 destPos:开始复制的位置
// length:复制的长度
public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);



public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    // 把原始数组的值从0开始,复制到copy数组当中,从0开始,长度为最小长度
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;// 返回一个新的数组
}

我们再来看看使用场景(指定位置添加元素):

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;
    
   	// 数组大小+1
    size++;
}

秃头老:(这小伙子肚子还是有点东西的!)那你有没有发现ensureCapacity()这个方法没有被内部调用,知道这个方法有什么用嘛?

我:(老东西,还TMD的问,没玩了,你给我等着哦!)

发现了哦,这个方法主要是为了当有大量数据插入的时候,先调用这个方法进行扩容,因为大量数据插入会递增式的调用扩容,这个是事先给我们扩容一个比较适合的大小,节省了大量时间!

// 根据最小扩容,来判断是否需要扩容
public void ensureCapacity(int minCapacity) {
    int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
        // any size if not default element table
        ? 0
        // larger than default for default empty table. It's already
        // supposed to be at default size.
        : DEFAULT_CAPACITY;

    if (minCapacity > minExpand) {
        ensureExplicitCapacity(minCapacity);
    }
}

举例说明:

ArrayList<Object> arrayList = new ArrayList<>();
final int N = 1000000;
long startTime = System.currentTimeMillis();

for (int i = 0; i < N; i++) {
    arrayList.add(i);
}

long endTime = System.currentTimeMillis();
System.out.println("不使用EnsureCapacity的时间:" + (endTime - startTime));// 3015



ArrayList<Object> arrayList = new ArrayList<>();
final int N = 10000000;
long startTime = System.currentTimeMillis();
arrayList.ensureCapacity(N);
for (int i = 0; i < N; i++) {
    arrayList.add(i);
}

long endTime = System.currentTimeMillis();
System.out.println("使用EnsureCapacity的时间:" + (endTime - startTime));// 2690

参考文章:Guide哥的Java学习

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2021-12-15 18:33:00  更:2021-12-15 18:33:09 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 16:25:46-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码