Java 集合笔记(List、Queue、Set 和 Map)

下图展示了 Java 集合的整体框架。其中黄色框代表接口,绿色框代表抽象类,蓝色框代表具体类。实线代表继承关系,虚线代表实现关系。抽象类AbstractCollection在图中出现了两次,这是为了方便连线,看起来关系要清晰一些。

Java 集合框架

从上图中可以看出,Java 中的集合分为两大类:Collection 和 Map。Collection 是单值集合,实例对象以一个独立个体存储在集合中。Map 是对值集合,一对实例对象以键值对的组合形式存储在 Map 中。Collection 中包含三类集合:List、Queue 和 Set。下面分别说说 List、Queue、Set 和 Map 四类集合的具体实现类。

1. List

List 用于存储的元素是有顺序的,而且允许有重复元素。List 的具体实现类有三个:ArrayList、Vector 和 LinkedList。

1.1 ArrayList、Vector 和 LinkedList之间的区别:

  1. ArrayList 内部实际上是在维护一个数组,当容量不足是它会进行自动扩容,即把原数组整体复制到一个更大的数组中去。所以它拥有和数组相同的优点和缺点。优点就是快速随机访问,缺点就是元素(对象引用或者基本类型数值)之间不能有物理间隔。因此对 ArrayList 进行随机查找或遍历操作时很高效的,但是对其在非末尾位置作插入或者删除操作时代价较高,需要对元素进行复制和移动[1]。
  2. Vector 内部同样是通过数组实现的,和 ArrayList 不同的是它支持线程同步,可以保证增删改查元素过程的线程安全。但线程同步是耗时操作,因此它比 ArrayList 低效。
  3. LinkedList 内部使用链表结构实现,所以它适合动态插入和删除,但访问和遍历速度比较慢,因为随机访问元素是需要从头开始遍历。

其实 Vector 类还有一个子类 Stack,它是对栈结构的实现类,但是官方提供的这个栈的实现类被认为是不合理的[2],通常不被使用。代替的方式是直接使用 LinkedList,它实现了 Deque 接口(Queue 的子接口),所以提供有用于操作表头和表尾元素的方法,这让我们可以把一个 LinkedList 对象当作堆栈、队列或者双向队列使用,在下文描述 Deque 时还会提到这一点。

1.2 ArrayList 和 Vector 的扩容方式

  1. 当不指定初始容量时,ArrayList 对象默认创建的是一个空数组,即初始容量为 0。源码如下:

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

    其中的DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空数组。

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    

    当第一次添加元素时,如果所需的最小容量minCapacity小于默认容量DEFAULT_CAPACITY = 10,则将容量扩展为 10,否则直接扩容到所需的最小容量minCapacity。之后当需要扩容时,默认的新容量newCapacity为在当前容量oldCapacity之上增加 50%(源码中通过右移一位计算当前容量的 50%),如果所需的最小容量minCapacity超出了默认的新容量newCapacity,则直接扩容到所需的最小容量minCapacity。具体处理逻辑在 ArrayList 的 newCapacity 方法中,源码如下:

    /**
    * Returns a capacity at least as large as the given minimum capacity.
    * Returns the current capacity increased by 50% if that suffices.
    * Will not return a capacity greater than MAX_ARRAY_SIZE unless
    * the given minimum capacity is greater than MAX_ARRAY_SIZE.
    *
    * @param minCapacity the desired minimum capacity
    * @throws OutOfMemoryError if minCapacity is less than zero
    */
    private int newCapacity(int minCapacity) {
    int oldCapacity = elementData.length;
    // 通过右移一位计得到当前容量的 50%
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity <= 0) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
    return Math.max(DEFAULT_CAPACITY, minCapacity);
    if (minCapacity < 0) // 数值溢出(超过 int 类型的最大范围)
    throw new OutOfMemoryError();
    return minCapacity;
    }
    // 以下代码用于保证数组长度不会超出 int 类型的最大值 Integer.MAX_VALUE
    return (newCapacity - MAX_ARRAY_SIZE <= 0)
    ? newCapacity
    : hugeCapacity(minCapacity);
    }
    
  2. 当不指定初始容量时,Vector 对象默认创建的数组容量为 10,请看源码:

    public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        // 创建数组并指定初始容量    
        this.elementData = new Object[initialCapacity]; 
        this.capacityIncrement = capacityIncrement;
       }
    public Vector(int initialCapacity) { 
        this(initialCapacity, 0);
    }
    public Vector() {
        this(10); // 数组初始容量为 10
    }
    

    注意两参构造器中有一个capacityIncrement参数,它用于指定数组扩容的增量this.capacityIncrement,若不指定,那么它的默认值为 0。当需要扩容时,会先判断增量capacityIncrement值是否大于 0,如果是,默认的新容量newCapacity就是在当前容量oldCapacity之上增加一个capacityIncrement值,否则直接在当前容量oldCapacity之上增加 1 倍。如果所需的最小容量minCapacity超出了默认的新容量newCapacity,则直接扩容到所需的最小容量minCapacity。具体处理逻辑在 Vector 的 newCapacity 方法中,源码如下:

    /**
     * Returns a capacity at least as large as the given minimum capacity.
     * Will not return a capacity greater than MAX_ARRAY_SIZE unless
     * the given minimum capacity is greater than MAX_ARRAY_SIZE.
     *
     * @param minCapacity the desired minimum capacity
     * @throws OutOfMemoryError if minCapacity is less than zero
     */
    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity <= 0) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        // 以下代码用于保证数组长度不会超出 int 类型的最大值 Integer.MAX_VALUE
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }
    

简单来说,在默认情况且扩容时所需的最小容量不超过默认的新容量时:

ArrayList 初始容量为 0,第一次扩容默认大小为 10,扩容时在原本容量之上增加 50%;

Vector 初始容量为 10,扩容时在原本容量之上增加 1 倍。

2. Queue

Queue 是关于队列结构的接口。队列中存储的元素同样是有顺序的,且允许有重复元素。但 Queue 不允许使用索引进行随机访问,它只允许操作头尾元素,即从尾部加入新元素,从头部取得最先加入的元素(先进先出,FIFO)。

Queue 有一个子接口 Deque,它是关于双向队列结构的接口。双向队列的两头都可以进行元素的添加或者访问,因此实现了 Deque 的实体类对象可以被当作队列或者双向队列使用,还可以被当成栈来使用。实现 Deque 的两个常用实体类是 ArrayDeque 和 LinkedList。从名字也可以看出 ArrayDeque 是通过数组实现的,LinkedList 则是链表结构实现的。

从前面的集合框架图中可以看出,纯粹实现了 Queue 接口的实体类是 PriorityQueue。它并不是一个遵循 FIFO 标准的队列实现,而是一个具有优先级顺序的队列实现。PriorityQueue 内部按照元素的优先级顺序维护一个平衡二叉堆,元素存储在堆中,所以需要保证其中的每个元素之间都是可以比较的(对于基本数据类型按照自然顺序,对于引用类型要么需要实现 Comparable 接口,要么指定一个 Comparator 提供比较规则)。PriorityQueue 的头部是优先级顺序最小的元素。每次访问队列时,它返回头部元素。

3. Set

Set 接口对应于数学中的集合概念。Set 不允许存储重复元素,且只允许存储最多一个 null 元素。实现 Set 接口的常用具体实现类有:HashSet、LinkedHashSet 和 TreeSet。它们的实现方式其实是在类中封装了对应的 Map 实现类对象,即 HashSet 中封装了一个 HashMap 对象,LinkedHashSet 中封装了一个 LinkedHashMap 对象,TreeSet 中封装了一个TreeMap 对象。Set 是单值集合,所以只关心对应 Map 对象中的键元素,而值元素的位置全部使用一个 Object 对象PRESENT填充。

// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

因此,这三种 Set 实现类的特点和其对应的 Map 实现类是一致的。简单叙述如下[3]:

  1. HashSet 存储的元素无序,访问元素的时间接近常数级别。因此当不需要维持元素顺序时选择 HashSet 最合适。
  2. LinkedHashSet 以元素存入的顺序存储,遍历时按插入顺序进行访问。因此当需要保持存入元素时的顺序时适合使用 LinkedHashSet 。
  3. TreeSet 会对存入的元素进行排序,所以默认要求元素实现 Comparable(默认按照升序排序),也可以指定 Comparator 对象提供比较规则。因此如果需要按照自然顺序或自定义顺序遍历元素时适合用 TreeSet。

关于内部数据结构的实现方式在下面的 Map 部分说。

4. Map

整理中...

参考

  1. Java中Vector和ArrayList的区别

  2. On Java 8 by Bruce Eckel

  3. HashMap,LinkedHashMap,TreeMap的区别(转)

posted @ 2021-08-27 19:14  alterwl  阅读(243)  评论(0编辑  收藏  举报