JDK1.8源码学习-ArrayList

  JDK1.8源码学习-ArrayList

目录

一、ArrayList简介

为了弥补普通数组无法自动扩容的不足,Java提供了集合类,其中ArrayList对数组进行了封装,使其可以自动的扩容或缩小长度,相当于动态数组。

ArrayList封装了一个动态的可以重新分配的Object[]数组,其中每一个类的对象都有一个capacity属性,表示了它们所封装的Object[]数组的长度,当向ArrayList中添加元素的时候,该属性会自动的添加。如果想要添加大量元素的时候,可以使用ensureCapacity方法一次性增加capacity,减少数组的重新分配次数,提高效率。

二、ArrayList工作原理和数据结构

ArrayList 是基于数组来实现的

对于数组,这里就不进行过多叙述了。 想要说明的是ArrayList集合中底层数组元素的类型为Objec类型,即可以存放所有类型的数据。我们对ArrayList类的实例的操作,也都是基于数组的。

三、ArrayList源码分析

3.1、继承关系分析

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList继承自AbstractList,实现了List、RandomAccess、Cloneable、Serializable接口,其中List中定义了一些基本的增删查改的功能,实现RandomAccess接口则具有随机读写的功能,实现了Cloneable接口,可以被克隆,实现了Serializable可以使ArrayList实现序列化。

3.2、成员变量分析

  //定义的序列化id,主要是为了标识不同版本的兼容性
  private static final long serialVersionUID = 8683452581122892189L;
  //默认的数组存储容量为 10
  private static final int DEFAULT_CAPACITY = 10;
  //当指定数组的容量为0的时候使用这个变量赋值,空对象数组
  private static final Object[] EMPTY_ELEMENTDATA = {};
  //默认的实例化的时候使用此变量赋值,空对象数组
  private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  //真正存放数据的对象数组,并不被序列化 (前面加有transient关键字)
  transient Object[] elementData;
  //数组中的真实元素个数它小于或等于elementData.length
  private int size;
  //数组中最大存放元素的个数
  private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;  

3.3、构造函数分析

3.3.1无参构造函数

ArrayList无参构造函数,默认初始化容量是10。

 /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;  //默认值:10
    }

在这里注释上说明构造一个容量为10的空的list,但是并没有看到数组的容量变为10,而是一个空的数组,实际上当有元素被加入(add方法)的时候,才会被初始化为10的数组。

3.3.2传入初始化容量构造函数

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

创建一个initialCapacity大小的数组。

3.3.3传入初始化元素的构造函数

 public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            //判断引用的数组类型,并将引用转换成Object数组引用
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            //替换为空数组
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

传入一个集合类(Collection的子类即可),转为list。

总结:可以看出ArrayList的内部存储结构就是一个Object类型的数组,因此它可以放任意类型的元素。在构造ArrayList的时候,如果传入初始大小,那么它将新建一个指定容量的Object数组,如果不设置初始大小,那么它将不会分配内存空间而是使用对象数组,在实际要放入元素的时候再进行内存分配。

3.4、add()方法分析

ArrayList中提供了四种方式的添加:1.直接添加;2.指定位置添加;3.添加全部;4.在指定位置添加全部。

1.直接添加

public boolean add(E e) {
  //添加前先检查是否需要拓展数组, 此时数组长度最小为size+1
  ensureCapacityInternal(size + 1);
  //将元素添加到数组末尾
  elementData[size++] = e;
  return true;
}

add(E e)调用涉及的方法:

1.private void ensureCapacityInternal(int minCapacity) {
     //判断数组是不是一个长度为0的空数组
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //如果是的话则给容量赋值为默认的容量大小10
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }

2. private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        //判断当前容量是否大于数组当前的长度
        if (minCapacity - elementData.length > 0)
            //如果数组容量过小则进行扩容
            grow(minCapacity);
    }

3.private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        //扩容的长度是增加了原来数组的一半大小
        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);
    }

4.private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0)
            throw new OutOfMemoryError();
        //如果minCapacity还大于MAX_ARRAY_SIZE,那么就将Integer.MAX_VALUE返回,反之将MAX_ARRAY_SIZE返回。
        //因为maxCapacity是三倍的minCapacity,可能扩充的太大了,就用minCapacity来判断了。
        return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
    }

2.指定位置添加

public void add(int index, E element) {
  //插入位置范围检查
  rangeCheckForAdd(index);
  //检查是否需要扩容
  ensureCapacityInternal(size + 1);
  //挪动插入位置后面的元素(将elementData中的元素从index开始,拷贝到index+1的位置,拷贝size-index个元素)
  System.arraycopy(elementData, index, elementData, index + 1, size - index);
  //在要插入的位置赋上新值
  elementData[index] = element;
  size++;
}

在指定位置进行添加,必然会影响到该位置的元素以及后续元素,因为是数组结构,所以只能进行元素的后移了,同时要首先检查入参的元素的位置(index)是否在范围内。

其中System.arraycopy(Object src,int srcPos,Object dest,int destPos,int length)方法中的参数的含义:src(原数组)、srcPos(从原数组的起始位置开始)、dest(目标数组)、destPos(目标数组的起始位置)、length(要copy的数组的长度)

3.添加一个集合中的全部元素

 public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        //检查是否需要扩容
        ensureCapacityInternal(size + numNew);
        //进行元素拷贝
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

4.在指定位置上添加一个集合中的全部元素

 public boolean addAll(int index, Collection<? extends E> c) {
        //插入位置范围检查
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        //检查是否需要扩容
        ensureCapacityInternal(size + numNew);
        //复制元素的个数
        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                    numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

3.5、set()方法分析

修改的方法比较简单,就是给数组的下标重新赋值。

public E set(int index, E element) {
  //index不能大于size
  rangeCheck(index);
  E oldValue = elementData(index);
  //替换成新元素
  elementData[index] = element;
  return oldValue;
}

确保set的位置(index)小于当前数组的长度(size)并且大于0,获取指定位置(index)元素,然后放到oldValue中存放,将需要设置的元素放到指定位置(index)上。

3.6、remove()方法分析

1. E remove(int index):根据下标进行删除

public E remove(int index) {
   // 检查下标是否越界
    rangeCheck(index); 

    modCount++;
    E oldValue = elementData(index);

    //最后 -1 是为了数组下标不越界
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; //置空引用,让GC进行回收

    return oldValue;
}

2.boolean remove(Object o):根据传入对象进行删除

// 删除成功返回true, 失败false
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;
}

// 快速删除指定下标的元素, 无越界检查
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; //置空引用,让GC进行回收
}

3. boolean removeAll(Collection<?> c):根据传入集合进行删除

public boolean removeAll(Collection<?> c) {
    Objects.requireNonNull(c);
    return batchRemove(c, false);
}

private boolean batchRemove(Collection<?> c, boolean complement) {
    final Object[] elementData = this.elementData;
    int r = 0, w = 0;
    boolean modified = false;
    try {
        for (; r < size; r++)
            // 如果complement为false, 则c集合包含本集合中的元素, 则不进行操作, 这就是保留不属于c集合中的所有元素.
            if (c.contains(elementData[r]) == complement)
                elementData[w++] = elementData[r];
    } finally {
        // Preserve behavioral compatibility with AbstractCollection,
        // even if c.contains() throws.

        if (r != size) {
            System.arraycopy(elementData, r,
                             elementData, w,
                             size - r);
            w += size - r;
        }
        if (w != size) {
            // clear to let GC do its work
            for (int i = w; i < size; i++)
                elementData[i] = null;
            modCount += size - w;
            size = w;
            modified = true;
        }
    }
    return modified;
}

如果传入的集合c中包含本集合中的元素,则对元素进行处理,把匹配的元素(c.contains(elementData[r])==complement)进行保留,不需要保留的下标置为null,其中利用complement属性,来决定含有的元素是删除还是保留。

3.7、get()方法分析

  public E get(int index) {
        //检查是否越界
        rangeCheck(index);
        //返回指定位置上的元素
        return elementData(index);
    }  

四、ArrayList总结

1、ArrayList底层实现是基于数组的,因此对指定下标的查找和修改比较快,但是删除和插入操作比较慢。

2、构造ArrayList的时候尽量指定容量,减少扩容所带来的数组复制操作,如果不知道大小可以采用默认值10。

3、每次添加元素之前会检查是否需要扩容,每次扩容都是增加原有容量的一半。

4、ArrayList中的所有的方法都没有进行同步,因此它不是线程安全的。

5、在查找给定元素索引值等方法中,源码都将该元素的值分为null和不为null两种情况进行处理,ArrayList中允许元素为null。

原文地址:https://www.cnblogs.com/liudblog/p/10764099.html

时间: 2024-10-16 12:31:31

JDK1.8源码学习-ArrayList的相关文章

JDK源码学习----ArrayList

                                                                         JDK源码学习----ArrayList 1.ArrayList简介 ArrayList是基于Object[] 数组的,也就是我们常说的动态数组.它能很方便的实现数组的增加删除等操作. public class ArrayList<E> extends AbstractList<E> implements List<E>, R

JDK1.8源码学习之 HashMap.java

///JDK1.8源码学习之HashMap.java package java.util; import java.io.IOException; import java.io.InvalidObjectException; import java.io.Serializable; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.function.BiConsu

由JDK源码学习ArrayList

ArrayList是实现了List接口的动态数组.与java中的数组相比,它的容量能动态增长.ArrayList的三大特点: ① 底层采用数组结构 ② 有序 ③ 非同步 下面我们从ArrayList的增加元素.获取元素.删除元素三个方面来学习ArrayList. ArrayList添加元素 因为ArrayList是采用数组实现的,其源代码比较简单.首先我们来看ArrayList的add(E e).以下代码版本是jdk7. public boolean add(E e) { // 检查数组容量 e

JDK1.8源码学习-Object

JDK1.8源码学习-Object 目录 一.方法简介 1.一个本地方法,主要作用是将本地方法注册到虚拟机中. private static native void registerNatives(); static { registerNatives(); } 2.获取类的字节码对象 public final native Class<?> getClass(); 3.返回当前对象的hash值 public native int hashCode(); 4.比较党当前对象的引用是否和要比较的

JDK1.8源码学习-String

JDK1.8源码学习-String 目录 一.String简介 String类是Java中最常用的类之一,所有字符串的字面量都是String类的实例,字符串是常量,在定义之后不能被改变. 二.定义 public final class String implements java.io.Serializable, Comparable<String>, CharSequence{} 1.String类是由final修饰的,表明String类不能被继承,并且String类中的成员方法都默认是fi

JDK1.8源码之ArrayList

主要从以下几个方面介绍: ArrayList扩容机制 ArrayList迭代器 Spliterator多线程遍历 一.ArrayList扩容机制 ArrayList实现List接口,底层为一个可以调节长度的数组,主要成员变量如下. 1 //默认初始容量 2 private static final int DEFAULT_CAPACITY = 10; 3 4 //空数组的实例,供需要空数组的地方调用,有实意 5 private static final Object[] EMPTY_ELEMEN

从JDK源码学习Arraylist

从今天开始从源码去学习一些Java的常用数据结构,打好基础:) Arraylist源码阅读: jdk版本:1.8.0 首先看其构造方法: 构造方法一: 第一种支持初始化容量大小,其中声明一个对象数组,赋值给this.elementdata 构造方法二: 第二种无参构造函数,即不指定初始容量大小,则默认赋值this.elementdata为一个空的对象数组,但是由注释可以看到其无参构造实际上初始容量为10 在elementData的注释中也说了该变量是实际存储Arrylist数据的存储结构,任何空

JDK1.8源码学习-String-hashCode方法为什么选择数字31作为乘子

1. 背景 某天,我在写代码的时候,无意中点开了 String hashCode 方法.然后大致看了一下 hashCode 的实现,发现并不是很复杂.但是我从源码中发现了一个奇怪的数字,也就是本文的主角31.这个数字居然不是用常量声明的,所以没法从字面意思上推断这个数字的用途.后来带着疑问和好奇心,到网上去找资料查询一下.在看完资料后,默默的感叹了一句,原来是这样啊.那么到底是哪样呢?在接下来章节里,请大家带着好奇心和我揭开数字31的用途之谜. 2. 选择数字31的原因 在详细说明 String

JDK1.8源码学习之Map.java

package java.util; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Function; import java.io.Serializable; /** * 一个可以将关键字映射为值的对象; * 一个map可以包含重复的关键字keys * 但一个关键字(key)最多只能映射到一个值(value) * *这个接口取代了Dict