数据结构java篇-ArrayList源码详解

前言

数据结构一直是一个优秀程序员必备的知识,有关数据结构的书籍也非常多,本系列我们通过研究JDK源码,观察源码的写法,来加深对数据结构的理解

JDK版本

本篇采用JDK版本为1.9

概述

ArrayList是一种可扩展数组结构,他的逻辑结构为集合(逻辑结构的分类介绍在本文中叙述一次,日后的博客中不再重复叙述),ArrayList继承了AbstractList类,实现了List,RandomAccess, Cloneable, Serializable接口,那么java是怎么来实现ArrayList这一数据结构的呢,他的数据结构的基本算法又是怎么样的呢,我们来看源码分析

逻辑结构

数据结构是相互之间存在一种或多种特定关系的数据元素的集合,而逻辑结构就是数据元素与数据元素的逻辑关系,实际上逻辑结构就是数据元素之间的逻辑关系,根据逻辑关系的不同,可以把数据结构分为以下4种基本结构

  • 集合
    这种结构的数据元素同属于一个集合,除此之外别无其他关系(实际上集合在数据结构中很少讨论)
  • 线性结构
    这种结构中的数据元素之间存在一对一的关系
  • 树形结构
    这种结构中的数据元素之间存在一对多的关系
  • 图形结构
    这种结构中的数据元素之间存在多对多的关系

ArrayList源码分析

成员变量

首先我们先来看看ArrayList有哪些成员变量,并且这些成员变量有什么用处,这些成员变量都是怎么声明的

  • private static final long serialVersionUID = 8683452581122892189L;
    这个自然不必多说,上文已经提到ArrayList实现了Serializable接口,所以定义了这么一个序列号
  • private static final int DEFAULT_CAPACITY = 10;
    即默认长度为10
  • transient Object[] elementData;
    这里可以看出ArrayList底层是Object数组,JDK1.7的版本中该成员变量是私有的,1.9改成了非私有来简化嵌套类的访问,前文我们说到,ArrayList实现了Serializable接口,所以该类的所有非静态属性和方法都自动被序列化,而为了安全,可以使用 transient关键字,被该关键字修饰的成员变量不会被序列化,即反序列号时该变量为null
  • private int size;
    用于记录ArrayList的长度

构造器

看完各个成员变量之后,我们来看看ArrayList的构造器,当你new一个ArrayList时,到底做了哪些事情呢?实际上ArrayList最重要的两个成员变量就是elementData(后文称之为底层数组)和size(ArrayList的长度),我们来观察采用各个构造器这两个成员变量会发生什么变化

无参构造

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

可以直观的看出,当我new ArrayList()时,会创建一个容量为10的空数组 size没有改变,因为他在成员变量的位置,所以还是默认值0.底层数组由null变为了长度为10的空数组

传一个容器

  public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // defend against c.toArray (incorrectly) not returning Object[]
            // (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

即传入一个容器(Collection的任意子类),将这个容器的所有元素传给底层数组,元素顺序为容器的迭代器,并且让ArrayList的长度为该容器的长度,如果容器长度为0,则让底层数组变成长度为10的空数组,如果容器为null,则抛出空指针异常

传长度

 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);
        }
    }

这种比较简单,即传一个int类型的长度,让底层数组变成指定长度的空数组,如果传入为0,则底层数组还是长度为10的空数组,如果传入负数,则抛出异常

方法

以上介绍完了,我们来看一看一个数据结构最基本的算法(即增删改查)是怎么实现的呢

源码肯定是要对于不同的情况作不同的处理的,所以经常会出现方法的重载的情况,那我们首先来看最简单的增加方法,即只传入要添加的元素值的方法

public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }

modCount是ArrayList的父类的一个成员变量,此处不做讨论,我们直接往下看,他会去寻找另一个重载方法(果然最简单…),然后直接返回true(所以源码认为并不可能出现添加失败的可能?)
那我们来看另一个重载方法吧

  private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }

这个成员方法是一个私有的成员方法,是不接受外部访问的,int s是上面传入的size,elementData即底层数组,首先判断s是不是等于底层数组的长度.如果等于,那么说明原数组长度不够,则需要扩容,很多人理不清这两者到底是什么关系,这里解释一下,size实际上可以理解成一个指针,指针指到的元素及以后的元素是空值,指针之前代表有值,比如当size为5,底层数组长度为10时,说明索引0-4是有值的,而索引5-9是null,所以当size等于底层数组时,相当于所有位置都已有值,则需要扩容,扩容的方法后文再讲,先看如果没有超过长度,或假设已经扩容后将指针指的位置添加进入元素,并将指针后移

我们再来看另外一种方法

public void add(int index, E element) {
        rangeCheckForAdd(index);
        modCount++;
        final int s;
        Object[] elementData;
        if ((s = size) == (elementData = this.elementData).length)
            elementData = grow();
        System.arraycopy(elementData, index,
                         elementData, index + 1,
                         s - index);
        elementData[index] = element;
        size = s + 1;
    }

传入一个索引位置,和一个要添加的元素,将要添加的元素添到指定索引位置,并把之后的元素后移,如何实现的呢,我们来分析代码,首先的rangeCheckForAdd方法是传入一个索引,判断这个索引有没有超过当前的size,或者小于0,如果有则抛出异常,很好理解,我们在继续看,modCount++,原因上文已经解释,略过,定义一个int s,定义一个新的底层数组,判断中首先将size赋值给新s,再把底层数组地址值给新的底层数组(即指向同一地址,只要地址值不变,操纵其中一个是另一个也会改变),然后判断两个是否相等,相等的话扩展,然后调用System类的方法,该方法的源码为

public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
代码解释:
  Object src : 原数组
   int srcPos : 从原数据的起始位置开始
  Object dest : 目标数组
  int destPos : 目标数组的开始起始位置
  int length  : 要copy的数组的长度

即从原来数组的哪里开始,复制几个数据,复制到新的数组的哪里,这里的使用是底层数组的要插入位置开始,复制size-要插入的索引个数据,复制到底层数组的指定索引后一位,是不是看起来非常复杂呢,
我们来假设一下底层数组原来是{0,1,2,4,5},而且底层数组的最大容量是10,我们要插入一个3到索引3的位置,那么这个方法传参即为(底层数组,3,底层数组,4,2),那么复制就会把4,5(从索引3后面数两位),后移一位,数组变为{0,1,2,4,4,5},
注意只是我们主观认为后移一位,实际上原本位置的4并没有变,是复制不是剪贴哦,所以size-要插入的索引实际上就是指要插入索引位置的后面的所有元素,
然后将要插入位置的元素替换成要插入的元素,并使长度增加

扩展

上文增加方法中频繁出现的扩展,其实是ArrayList之所以长度不受限制的核心,我们来看下扩展的方法

private Object[] grow() {
        return grow(size + 1);
    }

…源码尿性,我们来看看他的方法

  private Object[] grow(int minCapacity) {
        return elementData = Arrays.copyOf(elementData,
                                           newCapacity(minCapacity));
    }

注意前面的传参是数组长度+1,调用的方法是传入一个数组和你想要的新数组的长度,返回一个全新地址值的数组,如果新数组长度超过原数组,则超过部分元素用默认值代替如

int[] is1 = {1,2,3,4}
int[] is2 = Arrays.copyOf(is,6);

那么is2为{1,2,3,4,0,0};
所以我们来看newCapacity方法就可知道每次扩容会扩容多少了

  private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity <= 0) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }

首先算出原长度增加百分之50的长度,然后与给定的最小长度(即原长度+1)比较,如果足够,则返回当前容量增加50%。不会返回大于最大数组大小的容量,除非给定的最小容量大于最大数组大小。如果容量小于0,则抛出异常

我们继续看删的方法,删有两种方法

public E remove(int index) {
        Objects.checkIndex(index, size);

        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;
    }

第一种:删除指定索引位置,首先调用的方法实际就是判断你给定的索引是否小于0,或者大于等于size,如果是则抛出异常,modCount++,下次看到就直接跳了,然后取出要删除的元素,接下来,同上面插入方法,这句实际上是让要删除的元素的左边集体左移,并让size自减,对应的元素为null,后面注释也明确说了"让GC(即垃圾回收机制)明确明白他的工作",最后返回删除的元素.

   public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

第二种,传入一个要删除的元素,然后删除该元素,若成功,返回true,若失败,返回false,这个功能比较简单,我们来看看源码是怎么做的,首先判断传入的是否为null,这在源码中经常看见,非空校验嘛,我们先来看如果是空,则遍历底层数组,找到第一个为null的元素,并调用方法删除,方法实际上和上面的方法逻辑基本一样,只是少了一些校验,这里不在叙述,删除后返回true,若循环结束还没找到,则返回false,若不是空,则仍然还是遍历数组,调用equals方法,并不是==,所以此处允许作弊(即重写hasecode和equals方法,比较值而不是地址值).然后删除,所以我们可以看出,如果一个集合中有多个相同元素,只会删除靠前的一个,删除成功返回true,删除失败返回false

我们来看看改的方法,这里只有一个方法

public E set(int index, E element) {
        Objects.checkIndex(index, size);
        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

即传入要修改的索引和想改成什么元素,返回老元素,我们来看看实现,第一步,同上,还是判断传入索引是否符合规定,只要有索引的位置都有这么一句判断,完美体现了把客户当睿智…然后取出旧元素,替换元素,返回旧元素,非常简单

我们来看看查的方法

public E get(int index) {
        Objects.checkIndex(index, size);
        return elementData(index);
    }

实际上,我认为没必要讲…传入想查的索引,返回指定索引的元素,判断了一下传入索引是否合法,完了.

总结

实际上ArrayList应用十分广泛,也非常简单,我认为ArrayList需要掌握的地方,就是在于他为什么是可扩展数组,是怎么扩展的,比如这一道常见题

ArrayList al = new ArrayList<>(20);
for(int i = 0 ; i < 20 ;i++){
	al.add(i);		
}

请问al集合一共扩容了几次
答案是没有扩容,因为他创建时传入了一个长度为20,所以底层数组默认大小就是20,不需要扩容,这题应该叫抖机灵了,我们看他的原形

ArrayList al = new ArrayList<>();
for(int i = 0 ; i < 20 ;i++){
	al.add(i);		
}

这里al扩容了几次呢
首先,是采用的无参构造方式,所以默认长度为10,当i=10时,会扩容一次,长度会变成101.5=15,然后当i=15时,又会扩容一次,长度变成151.5=22,向下取整,所以是2次
这是我的第一篇博客,原创,如有不对的地方,希望指正,欢迎各位交流技术,私聊我可以获得联系方式,后续将写一篇LinkedList的分析,再把ArrayList与之比对

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值