Java集合之ArrayList与LinkList

注:示例基于JDK1.8版本node

参考资料:Java知音公众号 算法

本文超长,也是搬运的干货,但愿小伙伴耐心看完。数组

Collection集合体系安全

 

 

 

List、Set、Map是集合体系的三个接口。数据结构

其中List和Set继承了Collection接口。app

List有序且元素能够重复,默认大小为10;ArrayList、LinkedList和Vector是三个主要的实现类。源码分析

Set元素不能够重复,HashSet和TreeSet是两个主要的实现类。性能

Map也属于集合系统,但和Collection接口不一样。Map是key-value键值对形式的集合,key值不能重复,value能够重复;HashMap、TreeMap和Hashtable是三个主要的实现类。测试

--------------------------------------------------------------------------------------------------------------------优化

1、ArrayList

ArrayList基层是以数组实现的,能够存储任何类型的数据,但数据容量有限制,超出限制时会扩增50%容量,查找元素效率高。

ArrayList是一个简单的数据结构,因超出容量会自动扩容,可认为它是常说的动态数组。

源码分析

A、属性分析

/**
 * 默认初始化容量
 */
private static final int DEFAULT_CAPACITY = 10;

/**
 * 若是自定义容量为0,则会默认用它来初始化ArrayList。或者用于空数组替换。
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * 若是没有自定义容量,则会使用它来初始化ArrayList。或者用于空数组比对。
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * 这就是ArrayList底层用到的数组

 * 非私有,以简化嵌套类访问
 * transient 在已经实现序列化的类中,不容许某变量序列化
 */
transient Object[] elementData;

/**
 * 实际ArrayList集合大小
 */
private int size;

/**
 * 可分配的最大容量
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

B、构造方法分析

一、不带参数初始化,默认容量为10

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

二、根据initialCapacity初始化一个空数组,若是值为0,则初始化一个空数组

/**
 * 根据initialCapacity 初始化一个空数组
 */
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);
    }
}

三、经过集合作参数的形式初始化,若是集合为空,则初始化为空数组

/**
 * 经过集合作参数的形式初始化
 */
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;
    }
}

C、主要方法

一、trimToSize()方法:

用来最小实例化存储,将容器大小调整为当前元素所占用的容量大小。

/**
 * 这个方法用来最小化实例存储。
 */
public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}

判断size的值,若为0,则将elementData置为空集合,若大于0,则将一份数组容量大小的集合复制给elementData。

二、clone()方法

克隆一个新数组。

public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

经过调用Objectclone()方法来获得一个新的ArrayList对象,而后将elementData复制给该对象并返回。

三、add(E e)

在ArrayList末尾添加元素。

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

该方法首先调用了ensureCapacityInternal()方法,注意参数是size+1(数组已有参数个数+1个新参数),先来看看ensureCapacityInternal的源码:

private void ensureCapacityInternal(int minCapacity) {
   if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
       minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
   }
   ensureExplicitCapacity(minCapacity);
}

方法说明:计算容量+确保容量(上上代码)

计算容量:若elementData为空,则minCapacity值为默认容量和size+1(minCapacity)的最大值;若elementData不为空,minCapacity(size+1)不用进行操做

确保容量:若是size+1 > elementData.length证实数组已经放满,则增长容量,调用grow()

private void ensureExplicitCapacity(int minCapacity) {
   modCount++;//计算修改次数

   // overflow-conscious code
   if (minCapacity - elementData.length > 0)
       grow(minCapacity);
}

private void grow(int minCapacity) {
   // overflow-conscious code
   int oldCapacity = elementData.length;

   // 扩展为原来的1.5倍
   int newCapacity = oldCapacity + (oldCapacity >> 1);

   // 若是扩为1.5倍还不知足需求,直接扩为需求值
   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);
}

增长容量grow():默认1.5倍扩容。

  1. 获取当前数组长度=>oldCapacity

  2. oldCapacity>>1 表示将oldCapacity右移一位(位运算),至关于除2。再加上1,至关于新容量扩容1.5倍。

  3. 若是newCapacity<mincapacity`,则`newcapacity mincapacity="size+1=2" elementdata="1" newcapacity="1+1""">&gt;1=1,1&lt;2因此若是不处理该状况,扩容将不能正确完成。

  4. 若是新容量比最大值还要大,则将新容量赋值为VM要求最大值。

  5. 将elementData拷贝到一个新的容量中。

也就是说,当增长数据的时候,若是ArrayList的大小已经不知足需求时,那么就将数组变为原长度的1.5倍,以后的操做就是把老的数组拷到新的数组里面。

例如,默认的数组大小是10,也就是说当咱们add10个元素以后,再进行一次add时,就会发生自动扩容,数组长度由10变为了15具体状况以下所示:

四、add(int index, E element)方法

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

rangeCheckForAdd()是越界异常检测方法。

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

System.arraycopy方法:

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的数组的长度

 五、set(int index, E element)方法:

用指定元素替换此列表中指定位置的元素。

public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

E elementData(int index) {
    return (E) elementData[index];
}

六、indexOf(Object o):

 返回数组中第一个与参数相等的值的索引,容许null。

public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

七、get(int index)方法:

返回指定下标处的元素的值。

public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

rangeCheck(index)会检测index值是否合法,若是合法则返回索引对应的值。

八、remove(int index):

删除指定下标的元素。

public E remove(int index) {
    // 检测index是否合法
    rangeCheck(index);
    // 数据结构修改次数
    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;
}

ArrayList优缺点

优势:

一、由于其底层是数组,因此修改和查询效率高。

二、自动扩容(1.5倍)。

缺点:

一、插入和删除效率不高。(文末对比LinkedList)

二、线程不安全。

2、LinkedList

LinkList以双向链表实现,链表无容量限制,但双向链表自己使用了更多空间,每插入一个元素都要构造一个额外的Node对象,也须要额外的链表指针操做。容许元素为null,线程不安全。

源码分析

一、变量

/**
 * 集合元素数量
 **/
transient int size = 0;

/**
 * 指向第一个节点的指针
 * Invariant: (first == null && last == null) ||
 *            (first.prev == null && first.item != null)
 */
transient Node<E> first;

/**
 * 指向最后一个节点的指针
 * Invariant: (first == null && last == null) ||
 *            (last.next == null && last.item != null)
 */
transient Node<E> last;

二、构造方法

/**
 * 无参构造方法
 */
public LinkedList() {
}

/**
 * 将集合c全部元素插入链表中
 */
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

三、Node节点

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

Node既有prev也有next,因此证实它是一个双向链表。

四、添加元素

a、addAll(Collection c)方法

将集合c添加到链表,若是不传index,则默认是添加到尾部。若是调用addAll(int index, Collection<? extends E> c)方法,则添加到index后面。

/**
 * 将集合添加到链尾
 */
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

/** 
 * 
 */
public boolean addAll(int index, Collection<? extends E> c) {
    checkPositionIndex(index);

    // 拿到目标集合数组
    Object[] a = c.toArray();
    //新增元素的数量
    int numNew = a.length;
    //若是新增元素数量为0,则不增长,并返回false
    if (numNew == 0)
        return false;

    //定义index节点的前置节点,后置节点
    Node<E> pred, succ;
    // 判断是不是链表尾部,若是是:在链表尾部追加数据
    //尾部的后置节点必定是null,前置节点是队尾
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        // 若是不在链表末端(而在中间部位)
        // 取出index节点,并做为后继节点
        succ = node(index);
        // index节点的前节点 做为前驱节点
        pred = succ.prev;
    }

    // 链表批量增长,是靠for循环遍历原数组,依次执行插入节点操做
    for (Object o : a) {
        @SuppressWarnings("unchecked") 
        // 类型转换
        E e = (E) o;
        // 前置节点为pred,后置节点为null,当前节点值为e的节点newNode
        Node<E> newNode = new Node<>(pred, e, null);
        // 若是前置节点为空, 则newNode为头节点,不然为pred的next节点
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        pred = newNode;
    }

    // 循环结束后,若是后置节点是null,说明此时是在队尾追加的
    if (succ == null) {
        // 设置尾节点
        last = pred;
    } else {
    //不然是在队中插入的节点 ,更新前置节点 后置节点
        pred.next = succ;
        succ.prev = pred;
    }

    // 修改数量size
    size += numNew;
    //修改modCount
    modCount++;
    return true;
}

/**
  * 取出index节点
  */ 
Node<E> node(int index) {
    // assert isElementIndex(index);

    // 若是index 小于 size/2,则从头部开始找
    if (index < (size >> 1)) {
        // 把头节点赋值给x
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            // x=x的下一个节点
            x = x.next;
        return x;
    } else {
        // 若是index 大与等于 size/2,则从后面开始找
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}


// 检测index位置是否合法
private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

// 检测index位置是否合法
private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}  

假设咱们要在index=2处添加{1,2}到链表中,图解以下:

第一步:拿到index=2的前驱节点 prev=ele1

第二步:遍历集合prev.next=newNode,并实时更新prev节点以便下一次遍历:prev=newNode

第三步:将index=2的节点ele2接上:prev.next=ele2,ele2.prev=prev

 

注意node(index)方法:方法:寻找处于index的节点,有一个小优化,结点在前半段则从头开始遍历,在后半段则从尾开始遍历,这样就保证了只须要遍历最多一半结点就能够找到指定索引的结点。

b、addFirst(E e)方法

将e元素添加到链表并设置其为头节点(first)。

public void addFirst(E e) {
    linkFirst(e);
}

//将e连接成列表的第一个元素
private void linkFirst(E e) {

    final Node<E> f = first;
    // 前驱为空,值为e,后继为f
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    //若f为空,则代表列表中尚未元素,last也应该指向newNode
    if (f == null)
        last = newNode;
    else
    //不然,前first的前驱指向newNode
        f.prev = newNode;
    size++;
    modCount++;
}
  1. 拿到first节点命名为f

  2. 新建立一个节点newNode设置其next节点为f节点

  3. 将newNode赋值给first

  4. 若f为空,则代表列表中尚未元素,last也应该指向newNode;不然,前first的前驱指向newNode。

  5. 图解以下:

 

c、addLast(E e)方法

将e元素添加到链表并设置其为尾节点(last)。

public void addLast(E e) {
    linkLast(e);
}
/**
 * 将e连接成列表的last元素
 */
void linkLast(E e) {
    final Node<E> l = last;
    // 前驱为前last,值为e,后继为null
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    //最后一个节点为空,说明列表中无元素
    if (l == null)
        //first一样指向此节点
        first = newNode;
    else
        //不然,前last的后继指向当前节点
        l.next = newNode;
    size++;
    modCount++;
}

过程与linkFirst()方法相似,这里略过。

d、add(E e)方法

在尾部追加元素e。

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

void linkLast(E e) {
    final Node<E> l = last;
    // 前驱为前last,值为e,后继为null
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    //最后一个节点为空,说明列表中无元素
    if (l == null)
        //first一样指向此节点
        first = newNode;
    else
        //不然,前last的后继指向当前节点
        l.next = newNode;
    size++;
    modCount++;
}

e、add(int index, E element)方法

在链表的index处添加元素element.

public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}
/**
 * 在succ节点前增长元素e(succ不能为空)
 */
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    // 拿到succ的前驱
    final Node<E> pred = succ.prev;
    // 新new节点:前驱为pred,值为e,后继为succ
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 将succ的前驱指向当前节点
    succ.prev = newNode;
    // pred为空,说明此时succ为首节点
    if (pred == null)
        // 指向当前节点
        first = newNode;
    else
        // 不然,将succ以前的前驱的后继指向当前节点
        pred.next = newNode;
    size++;
    modCount++;
}

五、获取/查询元素

a、get(int index)

根据索引获取链表中的元素。

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

// 检测index合法性
private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

// 根据index 获取元素
Node<E> node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

b、getFirst()方法

获取头节点。

public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}

c、getLast()方法

获取尾节点。

public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}

六、删除元素

a、remove(Object o)

根据Object对象删除元素。

public boolean remove(Object o) {
    // 若是o是空
    if (o == null) {
        // 遍历链表查找 item==null 并执行unlink(x)方法删除
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

E unlink(Node<E> x) {
    // assert x != null;
    // 保存x的元素值
    final E element = x.item;
    //保存x的后继
    final Node<E> next = x.next;
    //保存x的前驱
    final Node<E> prev = x.prev;

    //若是前驱为null,说明x为首节点,first指向x的后继
    if (prev == null) {
        first = next;
    } else {
        //x的前驱的后继指向x的后继,即略过了x
        prev.next = next;
        // x.prev已无用处,置空引用
        x.prev = null;
    }

    // 后继为null,说明x为尾节点
    if (next == null) {
        // last指向x的前驱
        last = prev;
    } else {
        // x的后继的前驱指向x的前驱,即略过了x
        next.prev = prev;
        // x.next已无用处,置空引用
        x.next = null;
    }
    // 引用置空
    x.item = null;
    size--;
    modCount++;
    // 返回所删除的节点的元素值
    return element;
}
  1. 遍历链表查找 item==null 并执行unlink(x)方法删除

  2. 若是前驱为null,说明x为首节点,first指向x的后继,x的前驱的后继指向x的后继,即略过了x.

  3. 若是后继为null,说明x为尾节点,last指向x的前驱;不然x的后继的前驱指向x的前驱,即略过了x,置空x.next

  4. 引用置空:x.item = null

  5. 图解:

b、remove(int index)方法

根据链表的索引删除元素。

public E remove(int index) {
    checkElementIndex(index);
    //node(index)会返回index对应的元素
    return unlink(node(index));
}

c、removeLast()方法

删除尾节点。

public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}

private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    // 取出尾节点中的元素
    final E element = l.item;
    // 取出尾节点中的后继
    final Node<E> prev = l.prev;
    l.item = null;
    l.prev = null; // help GC
    // last指向前last的前驱,也就是列表中的倒数2号位
    last = prev;
    // 若是此时倒数2号位为空,那么列表中已无节点
    if (prev == null)
        // first指向null
        first = null;
    else
        // 尾节点无后继
        prev.next = null;
    size--;
    modCount++;
    // 返回尾节点保存的元素值
    return element;
}

七、修改元素

修改元素比较简单,先找到index对应节点,而后对值进行修改。

public E set(int index, E element) {
    checkElementIndex(index);
    // 获取到须要修改元素的节点
    Node<E> x = node(index);
    // 保存以前的值
    E oldVal = x.item;
    // 执行修改
    x.item = element;
    // 返回旧值
    return oldVal;
}

LinkedList优势:不须要扩容和预留空间,空间效率高。

3、ArrayList与LinkedList插入和查找消耗时间测试对比

参考连接:https://blog.csdn.net/dearKundy/article/details/84663512

在ArrayList和LinkedList的头部、尾部和中间三个位置插入与查找100000个元素所消耗的时间来进行对比测试,下面是测试结果:(时间单位ms)

  插入 查找
ArrayList尾部 26 12
ArrayList头部 859 7
ArrayList中间 1848 13
LinkedList尾部 28 9
LinkedList头部 15 11
LinkedList中间 15981 34928

 

 

 

 

 

 

 

 

 

测试结论:
ArrayList的查找性能绝对是一流的,不管查询的是哪一个位置的元素
ArrayList除了尾部插入的性能较好外(位置越靠后性能越好),其余位置性能就不如人意了
LinkedList在头尾查找、插入性能都是很棒的,可是在中间位置进行操做的话,性能就差很远了,并且跟ArrayList彻底不是一个量级的

根据源码分析所得结论:

  • 对于LinkedList来讲,头部插入和尾部插入时间复杂度都是O(1)
  • 可是对于ArrayList来讲,头部的每一次插入都须要移动size-1个元素,效率可想而知
  • 可是若是都是在最中间的位置插入的话,ArrayList速度比LinkedList的速度快将近10倍
相关文章
相关标签/搜索