一文讲透Java的List集合

List是Java中比较常用的集合类,关于List接口有很多实现类,本文就来简单介绍下其中几个重点的实现ArrayList、LinkedList和Vector之间的关系和区别。Vector和ArrayList一样,都是通过数组实现的,其中的很多方法都通过同步(synchronized)处理来保证线程安全,所以我们重点讲解ArrayList和LinkedList这两种数据结构的区别。

集合和接口的关系

Collection接口是 (java.util.Collection)是Java集合类的顶级接口之一,整个集合框架就围绕一组标准接口而设计,Collection 接口有 3 种子类型集合: List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类。

数据结构

  • ArrayList:ArrayList底层是基于数组实现的,可以认为ArrayList是一个可变动大小的数组,随着越来越多的元素被添加到ArrayList中,其规模是动态增加的,内存分配是一块儿连续的内存

     
  • LinkedList:LinkList底层是基于双向链表实现的,每一个元素Node除了记录本身的值之外还需要保存前、后数据节点的引用


源码解析

ArrayList核心源码解读

构造方法及属性值:


/**
* 默认初始容量大小
*/
private static final int DEFAULT_CAPACITY = 10

/**
* 空数组(用于空实例)
*/
private static final Object[] EMPTY_ELEMENTDATA = {}

/**
* 用于默认大小空实例的共享空数组实例
* 从EMPTY_ELEMENTDATA数组中区分出来,在使用默认构造函数的时候赋值(不指定初始容量)
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}

/**
* 存储数据的对象,是一个Object数组
*/
transient Object[] elementData;

/**
* 数组包含的数据个数
*/
private int size

/**
* 数组最大容量
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
* 带初始容量的构造函数,可以指定初始化数据容量大小
*如果容量大于0就创建指定容量大小的数据,如果等于0就赋值空数组对象
*/
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);
     }   
}

/**
* 默认无参构造函数,不指定初始容量,数组对象赋值DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* 初始容量为0,当第一次添加元素时,容量变成10
*/
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
 * 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。
 */
public ArrayList(Collection<? extends E> c) {
    //将指定集合转换为数组
    elementData = c.toArray();
    //如果elementData数组的长度不为0
    if ((size = elementData.length) != 0) {
        // 如果elementData不是Object类型数据(c.toArray可能返回的不是Object类型的数组所以加上下面的语句用于判断)
        if (elementData.getClass() != Object[].class)
            //将原来不是Object类型的elementData数组的内容,赋值给新的Object类型的elementData数组
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 其他情况,用空数组代替
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

 核心方法解析:

/**
* 添加元素
*/
publicboolean add(E e) {
    //判断数据容量,如果空数组就初始化,如果容量小了就扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

/**
* 计算数据容量,如果是DEFAULTCAPACITY_EMPTY_ELEMENTDATA对象(默认无参构造函数创建)
* 容量设置为10,否则就是指定 初始化容量+1,这个也区分了EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_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);
}

/**
* 用于默认大小空实例的共享空数组实例
* 从EMPTY_ELEMENTDATA数组中区分出来,在使用默认构造函数的时候赋值(不指定初始容量)
*/
private void grow(int minCapacity) {
    //oldCapacity为旧容量,newCapacity为新容量
    int oldCapacity = elementData.length;
    //将oldCapacity 右移一位,其效果相当于oldCapacity /2,
    //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //再检查新容量是否超出了ArrayList所定义的最大容量
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        //如果超出了最大数组容量,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

/**
* 比较新容量和最大数组容量的大小,如果超出就设置Interger.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;
}
注:细心的同学可能会发现MAX_ARRAY_SIZE的值是Integer.MAX_VALUE-8,为什么会这样设置,而不是设置成
Integer.MAX_VALUE,这里涉及到Java对象结构,数组对象有一个属性_length用来记录数组的长度,2^31 = 2,147,483,648 
所以自身需要8byte来记录数组的长度,数组最大长度就是Integer.MAX_VALUE-8

/**
* 删除ArrayList指定位置元素
* 将待删除元素后面的所有元素向左边移动一位,然后删除最后一位数据
*/
public E remove(int index) {
    rangeCheck(index);

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

    int numMoved = size - index - 1;
    //计算移动的位置,将index之后所有元素左移一位
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    //最后一位删除                    
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}

 LinkedList核心源码解读

LinkedList是一个双向链表结构,所以它的操作主要就是链表的遍历、断开以及链接操作,来实现List本身的增删改查功能。


/**
* 链表节点对象Node,包括三个属性
* item:节点值  next:后节点引用  prev:前节点引用
*/
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;
    }
}

/**
* 添加元素
* 添加元素就是直接在链表尾端加入元素,所以主要调用linkLast方法实现
*/
public boolean add(E e) {
    linkLast(e);
    return true;
}

/**
* 链表尾端加入元素
*/
void linkLast(E e) {
    final Node<E> l = last;
    //创建节点对象,prev指向last,next为空
    final Node<E> newNode = new Node<>(l, e, null);
    //将last赋值为新节点
    last = newNode;
    if (l == null)
        //空链表,第一次插入首节点赋值为当前节点
        first = newNode;
    else
        //原尾节点的next指向新节点
        l.next = newNode;
    size++;
    modCount++;
}

/**
* 删除指定位置参数,主要操作是链表的断开,调用unlink方法
*/
public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

/**
* 断开删除节点的前后联系,链接删除节点的前后两个几点
*/
E unlink(Node<E> x) {
  // assert x != null;
  final E element = x.item;
  final Node<E> next = x.next;
  final Node<E> prev = x.prev;
    //前节点为空,当前节点是first节点
  if (prev == null) {
    //first节点引用指向后节点
    first = next;
  } else {
    //将前节点的next指向当前节点的next
    prev.next = next;
    //断开前节点的引用
    x.prev = null;
  }
  //后节点为空,当前节点是last节点
  if (next == null) {
    //last节点引用指向前节点
    last = prev;
  } else {
    //将后节点的prev指向当前节点的prev
    next.prev = prev;
    //断开后节点的引用
    x.next = null;
  }
  //断开了前后链接,值设置为空,等GC回收
  x.item = null;
  size--;
  modCount++;
  return element;
}

/**
* 比较新容量和最大数组容量的大小,如果超出就设置Interger.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;
}
注:细心的同学可能会发现MAX_ARRAY_SIZE的值是Integer.MAX_VALUE-8,为什么会这样设置,而不是设置成
Integer.MAX_VALUE,这里涉及到Java对象结构,数组对象有一个属性_length用来记录数组的长度,2^31 = 2,147,483,648 
所以自身需要8byte来记录数组的长度,数组最大长度就是Integer.MAX_VALUE-8

/**
* 删除ArrayList指定位置元素
* 将待删除元素后面的所有元素向左边移动一位,然后删除最后一位数据
*/
public E remove(int index) {
    rangeCheck(index);

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

    int numMoved = size - index - 1;
    //计算移动的位置,将index之后所有元素左移一位
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    //最后一位删除                    
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}

差异比较

前面我们从数据结构和源码两个维度对ArrayList和Linked进行了深层的剖析,两者的差异其实就是数组和双向链表的操作差异。数组是个连续的内存块,随机访问通过下标直接找到相应的值,查询快,但是插入或者删除都涉及到数据的移动,效率低。而链表刚好想法,随机访问需要遍历所有元素,速度慢,但是插入和删除只需要断开前后节点的链接关系就行,效率高。

时间复杂度

操作数组链表
随机访问O(1)O(N)
头部插入O(N)O(1)
头部删除O(N)O(1)
尾部插入O(1)O(1)
尾部删除O(1)O(1)

关注我的个人微信公众号:八阿哥技术圈,学习程序设计、系统架构知识,更多干货等你来哦!!!


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值