Java集合总结(一):列表和队列

java中的具体容器类都不是从头构建的,他们都继承了一些抽象容器类。这些抽象容器类,提供了容器接口的部分实现,方便具体容器类在抽象类的基础上做具体实现。容器类和接口的关系架构图如下:

虚线框表示接口,有Collection, List, Set, Queue, Deque和Map。

有六个抽象容器类:

  • AbstractCollection: 实现了Collection接口,被抽象类AbstractList, AbstractSet, AbstractQueue继承,ArrayDeque也继承自AbstractCollection (图中未画出)。
  • AbstractList:父类是AbstractCollection,实现了List接口,被ArrayList, AbstractSequentialList继承。
  • AbstractSequentialList:父类是AbstractList,被LinkedList继承。
  • AbstractMap:实现了Map接口,被TreeMap, HashMap, EnumMap继承。
  • AbstractSet:父类是AbstractCollection,实现了Set接口,被HashSet, TreeSet和EnumSet继承。
  • AbstractQueue:父类是AbstractCollection,实现了Queue接口,被PriorityQueue继承。

这些抽象类都提供了相应接口的基础实现,同时定义了一些子类必须重写的抽象方法 ,相当于定义了一个模板,这有点类似于一个设计模式——模板方法模式

 

下面主要总结一下上图中具体实现类的一些细节和特性:

ArrayList

内部使用Object数组实现、默认初始大小是10,可手动指定,扩容策略是 newCapacity = oldCapacity + (oldCapacity >> 1),
即每次增大到原来的1.5倍,但不会小于最小值,最大容量是Integer.MAX_VALUE,即0x7fffffff。扩容时将一个新建一个数组将原来数组的内容复制过去。

没用泛型数组E[]原因个人猜测是因为java中的泛型是在jdk1.5才开始支持的,在这之前都用的Object,为了保持向前的兼容性不得不这么做,jdk1.6中新增的ArrayDeque内部就是泛型数组E[]。

 

LinkedList
内部使用链表实现,可以用作队列(Queue),队列两端都可以操作,尾部添加,头部查看和删除。也可以用作双端队列(Deque)(双向链表)(双向遍历、可以实现栈的功能)。

内部只维护头和尾结点,构造方法不能指定大小,按需分配空间。

性能

  • 不可以随机访问,必须从头或尾开始查找,效率为O(N/2)(用了二分法)
  • 按照内容查找效率为O(N/2)
  • 两端添加删除效率为O(1)
  • 中间插入删除先定位 ,效率为O(n),但插入或者删除本身效率很高,为O(1)

 

ArrayDeque
这是jdk1.6新增的一个类。内部维护一个泛型数组E[],使用head和tail两个int索引表示链表头和尾(其中tail指向下一个空位),使得物理上简单的从头到尾的数组变成了逻辑上循环的数组,可以作为链表使用。默认初始数组大小是16,扩容策略是乘以2

  • 如果head和tail相同,则数组为空,长度为0。
  • 如果tail大于head,则第一个元素为elements[head],最后一个为elements[tail-1],长度为tail-head,元素索引从head到tail-1。
  • 如果tail小于head,且为0,则第一个元素为elements[head],最后一个为elements[elements.length-1],元素索引从head到elements.length-1。
  • 如果tail小于head,且大于0,则会形成循环,第一个元素为elements[head],最后一个是elements[tail-1],元素索引从head到elements.length-1,然后再从0到tail-1。

可以通过构造方法指定初始数组大小numElements,但是实际的大小是:

  • 如果numElements小于MIN_INITIAL_CAPACITY,则分配的数组长度就是MIN_INITIAL_CAPACITY,它是一个静态常量,值为8。
  • 在numElements大于等于8的情况下,分配的实际长度是严格大于numElements并且为2的整数次幂的最小数。比如,如果numElements为10,则实际分配16,如果numElements为32,则为64。

使用2的幂次方是为了保证

public void addLast(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[tail] = e;
        if ( (tail = (tail + 1) & (elements.length - 1)) == head)
            doubleCapacity();
    }

(tail + 1) & (elements.length - 1)能定位到正确下一个位置tail,比如说,如果elements.length为8,则(elements.length - 1)为7,二进制为0111,对于负数-1,与7相与,结果为7,对于正数8,与7相与,结果为0,都能达到循环数组中找下一个正确位置的目的。

扩容之后会重新排列数组,head是0,tail是当前元素数。

性能

  • 在两端添加、删除元素的效率很高,动态扩展需要的内存分配以及数组拷贝开销可以被平摊,具体来说,添加N个元素的效率为O(N)。
  • 根据元素内容查找和删除的效率比较低,为O(N)。

 

简单总结:

与ArrayList和LinkedList不同,ArrayDeque没有索引位置的概念,不能根据索引位置进行操作。
ArrayDeque和LinkedList都实现了Deque接口,应该用哪一个呢?如果只需要Deque接口,从两端进行操作,一般而言,ArrayDeque效率更高一些,应该被优先使用,不过,如果同时需要根据索引位置进行操作,或者经常需要在中间进行插入和删除,则应该选LinkedList。

posted @ 2018-08-04 17:43  沉默哥  阅读(579)  评论(0编辑  收藏  举报