List集合源码解析

java集合之List集合源码解析

1.为什么会有集合的概念

​ 数组在声明的时候就需要固定大小,集合不是可以扩容。Java集合就像一个容器,可以存储任何类型的数据,也可以结合泛型来存储具体的类型对象。在程序运行时,Java集合可以动态的进行扩展,随着元素的增加而扩大。

​ List集合可以存储有序且可重复的数据。有序指的是:集合保存了元素的插入顺序,可重复表示的是:两个元素的hashCode值是否相同。List集合存储元素的时候并不是根据元素的hashCode值来确定位置的(Set是)。

2.ArrayList的数据结构

​ ArrayList是基于数组实现的集合。

1.属性

​ ArrayList是基于数组实现的集合。

//默认的数组容量大小 如果没有指定大小的话就使用默认大小
private static final int DEFAULT_CAPACITY = 10;
//当ArrayList的构造方法中显示指出ArrayList的数组长度为0时,类内部将EMPTY_ELEMENTDATA 这个空对象数组赋给elemetData数组。
private static final Object[] EMPTY_ELEMENTDATA = {};
//当ArrayList的构造方法中没有显示指出ArrayList的数组长度时,类内部使用默认缺省时对象数组为DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 
//实际存储数据的数组
transient Object[] elementData; 
//ArrayList实际存放元素的个数
private int size;

2.构造方法

//指定大小
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
            //如果指定大小为0 将EMPTY_ELEMENTDATA赋值给elementData
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
//不指定大小 将DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给elementData
 public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

3.add方法

//首先判断是否需要扩容 元素存放的位置应该是当前容量大小+1的位置 也就是说数组的最小容量应该是size+1
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
//计算容量大小 如果elementData是默认的空对象,那么就返回默认数组容量大小和最小容量大小中的最大值否则就返回最小容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
//确认容量大小  如果计算出来的最小容量比当前数组的容量要大,就需要进行扩容。
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
//扩容   首先将数组的容量赋值给oldCapacity  newCapacity是oldCapacity的1.5倍。 如果newCapacity比最小容量小  就将最小容量赋值给newCapacity,如果newCapacity大于数组最大容量MAX_ARRAY_SIZE,newCapacity = hugeCapacity(minCapacity);
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
//数组存放的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//如果最小容量大于数组存放的最大容量返回Integer.MAX_VALUE 否则返回 MAX_ARRAY_SIZE;
private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

在这里插入图片描述

4.Integer.MAX_VALUE - 8

​ Inter.Max_Value标识的int类型的最大值2^31-1,为什么要减去8呢?数组在java中是一种特殊类型,数组对象中有一个额外的元数据(存放了一些数组的信息,如size、flag、lock)有的回答说这个8字节是用来存储Inter.Max_Value的,因为本身Inter.Max_Value需要8字节的容量。但是这么说似乎不对,如果只用来存储size,那么其他元数据存储在何处?是否也需要空间来存储呢。

源码中这样写道:

/**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */

​ 所以个人觉得,这个-8只是为了减少出错的几率 避免OOM

3.LinkedList的数据结构

​ 在JDK1.6及其之前LinkedList是基于双向循环链表实现的。在JDK1.7及其之后LinkedList是基于双向链表实现的

1 JDK1.7

​ LinkedList是通过headerEntry实现的一个双向循环链表的。先初始化一个空的Entry,用来做header,然后首尾相连,形成一个循环链表:

private transient Entry<E> header = new Entry<E>(null, null, null);
private transient int size = 0;

private static class Entry<E> {
       E element;  // 当前存储元素
       Entry<E> next;  // 下一个元素节点
       Entry<E> previous;  // 上一个元素节点
 
       Entry(E element, Entry<E> next, Entry<E> previous) {
           this.element = element;
           this.next = next;
           this.previous = previous;
       }
}

//构造函数
    public LinkedList() {
        //将header节点的前一节点和后一节点都设置为自身
        header.next = header. previous = header ;
    }
 

​ LinkedList中提供了上面两个属性,其中size和ArrayList中一样用来计数,表示list的元素数量,而header则是链表的头结点,Entry则是链表的节点对象。

增加方法

 //添加首节点,将元素加到header节点后面
public void addFirst(E e) {
       addBefore(e, header.next);
 }
//添加尾节点
 public void addLast(E e){
    addbefore(e,header);
 }

private Entry<E> addBefore(E e,Entry<E> entry){
    Entry<E> newEntry =new Entry<E>(e,entrey,entry.previous);
    newEntry.previous.next=newEntry;
    newEntry.next.previous=newEntry;
    size++;
    modCount++;
}

​ 循环链表中没有null指针(但是头节点的内容始终是null)。涉及遍历操作时,其终止条件就不再是像非循环链表那样判别p或p->next是否为空,而是判别它们是否等于某一指定指针,如头指针或尾指针等

1 JDK1.8

​ 在jdk1.7中创建了两个节点对象代替了 header,first头节点的引用,last尾节点的引用

transient  int size=0;
transient Node<E> first;
transient Node<E> last;

//初始化不做任何操作
public LinkedList() {}

private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
}

LinkedList的构造函数没有做任何操作 那么first、last对象是如何初始化的?为什么要这样做?

​ 因为LinkedList底层是通过链表实现的,每当有元素添加进来的时候,都是通过链接新的节点实现的(first、last也在此时初始化),也就是说他的容量是随着节点的增加而变化的。和ArrayList不同它底层是通过数组实现的,会默认设置一个初始容量。

add增加方法

public boolean add(E e) {
        linkLast(e);
        return true;
}

public Node<E> linkLast(E e){
    //新建一个节点l将尾节点储存起来,
    Node<E> l=last;
    //新建一个newNode节点,该节点是last节点 因此newNode.next=null, newNode.previous=last(未增加之前)
    Node<E> newNode=new Node<E>(l,e,null);
    //更新last节点的引用
    last=newNode;
    //若l为null表示该链表为空,故更新first节点的引用为newNode节点
    if(l==null)
        first=newNode;
    else
        //若l不为null表示该链表有数据 将l.next的引用更新为newNode
        l.next=newNode;
    size++;
    modCount++;
    return newNode;
}

4.ArrayList和LinkedList比较

1.随机访问

​ 对于ArrayList和LinkedList两种类型的集合的主要区别就是他们的数据结构不同,一个基于数组实现一个基于循环链表实现。首先比较数组和链表两种数据结构的实现方式。

数组:数组是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。(数组是具体的存储数据的方式,线性表是一种抽象概念。两者不是从属关系)

双向链表:链表的一种。和单链表一样,双链表也是由节点组成,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。

​ 这里的连续空间指的是数组在内存中的地址是连续的,而链表是通过指针来指向下一个节点的地址,因此链表在内存中节点的地址不一定是连续的。通过了解cpu读取数据的方式可以得出数组的效率比链表的高。

​ CPU有个预读机制,当CPU在内存读取数据的时候并不是只读取特定某个地址空间的数据,而是会将相邻地址的数据也一同读取出来并保存到CPU缓存中。等下次读取的时候会先从缓存中读取数据,若缓存中没有再从内存中读取。这样当你读取数组中的某个值的时候很大概率会将整个数组都保存在缓存中,这样数组的读取效率会大大提高。

源码实现

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    
 public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
     
     
public interface RandomAccess {
}

​ ArrayList实现了RandomAccess接口,该接口中没有任何方法,通过官方的API发现这是一个标志接口

public interface RandomAccess

Marker interface used by List implementations to indicate that they support fast (generally constant time) random access.

​ 这段话大概的意思就是说 RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。也就是说,实现了这个接口的集合是支持 快速随机访问 策略的。

​ 同时源码中有这段话:

 * for typical instances of the class, this loop:
 * <pre>
 *     for (int i=0, n=list.size(); i &lt; n; i++)
 *         list.get(i);
 * </pre>
 * runs faster than this loop:
 * <pre>
 *     for (Iterator i=list.iterator(); i.hasNext(); )
 *         i.next();
 * </pre>

​ 表示使用for循环的方式比使用迭代器的方式更快。

2.插入和删除

​ 对于数组来说,由于数组的数据在内存的地址是连续的,因此在数组中间插入一个数据,需要以后的数据都要移动地址。而链表不同只需要改变一下该位置的前一个节点的指针,将其指向插入数据的内存地址。因此对于插入和删除操作来说:数组的效率没有链表高。

时间复杂度数组链表
插入和删除O(n)O(1)
随机访问O(1)O(n)

综上:对于随机访问ArrayList优于LinkedList,插入和删除LinkedList优于ArrayList

5.遍历List集合

1.Iterator

​ 通过集合的继承关系知道List集合是继承自Collection接口,而Collection接口继承自Iterator接口。Iterator是一个接口,它是集合的迭代器。集合可以通过Iterator去遍历集合中的元素。

public interface Iterator<E> {
    boolean hasNext();//如果迭代器中有元素则返回true
    E next();//返回当前位置的元素并将游标移动到下一个位置
    default void remove() {//删除游标左边的元素,在执行玩next之后改操作只能执行一次
        throw new UnsupportedOperationException("remove");
    }
}

​ Collection有一个抽象方法iterator(),所有的Collection子类都实现了这个方法,返回一个Iterator对象。然后通过Iterator接口的三个方法来遍历集合。

List<T> list=new ArrayList<T>();
Iterator it=list.iterator();
while(it.hasNext()){
    //注意:不能再while循环中多次使用next()方法,每使用一次指针就会指向下一个元素的位置,会导致while(true)但是指针确无法在移动了。
    sout(it.next())
}
2.ListIterator

​ List接口中有另外一个方法listIterator(),该方法返回一个ListIterator对象。ListIterator是一个功能更加强大的, 它继承于Iterator接口,只能用于各种List类型的访问。可以通过调用listIterator()方法产生一个指向List开始处的ListIterator, 还可以调用listIterator(n)方法创建一个一开始就指向列表索引为n的元素处的ListIterator。

public interface ListIterator<E> extends Iterator<E> {
    boolean hasNext();//向前遍历列表是否还有元素
    E next();//返回当前位置的元素并将游标移动到下一个位置
    boolean hasPrevious();//反向遍历列表是否还有元素
    E previous();//返回当前位置的元素并将游标移动到上一个位置
    int nextIndex();//返回下
    int previousIndex();
    void remove();
    void set(E e);
    void add(E e);
}
public interface ListIterator<E> extends Iterator<E> {
    boolean hasNext();//向前遍历列表是否还有元素
    E next();//返回当前位置的元素并将游标移动到下一个位置
    boolean hasPrevious();//反向遍历列表是否还有元素
    E previous();//返回当前位置的元素并将游标移动到上一个位置
    int nextIndex();//返回下
    int previousIndex();
    void remove();
    void set(E e);
    void add(E e);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值