Java 集合笔记(List、Queue、Set 和 Map)
下图展示了 Java 集合的整体框架。其中黄色框代表接口,绿色框代表抽象类,蓝色框代表具体类。实线代表继承关系,虚线代表实现关系。抽象类AbstractCollection
在图中出现了两次,这是为了方便连线,看起来关系要清晰一些。
从上图中可以看出,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之间的区别:
- ArrayList 内部实际上是在维护一个数组,当容量不足是它会进行自动扩容,即把原数组整体复制到一个更大的数组中去。所以它拥有和数组相同的优点和缺点。优点就是快速随机访问,缺点就是元素(对象引用或者基本类型数值)之间不能有物理间隔。因此对 ArrayList 进行随机查找或遍历操作时很高效的,但是对其在非末尾位置作插入或者删除操作时代价较高,需要对元素进行复制和移动[1]。
- Vector 内部同样是通过数组实现的,和 ArrayList 不同的是它支持线程同步,可以保证增删改查元素过程的线程安全。但线程同步是耗时操作,因此它比 ArrayList 低效。
- LinkedList 内部使用链表结构实现,所以它适合动态插入和删除,但访问和遍历速度比较慢,因为随机访问元素是需要从头开始遍历。
其实 Vector 类还有一个子类 Stack,它是对栈结构的实现类,但是官方提供的这个栈的实现类被认为是不合理的[2],通常不被使用。代替的方式是直接使用 LinkedList,它实现了 Deque 接口(Queue 的子接口),所以提供有用于操作表头和表尾元素的方法,这让我们可以把一个 LinkedList 对象当作堆栈、队列或者双向队列使用,在下文描述 Deque 时还会提到这一点。
1.2 ArrayList 和 Vector 的扩容方式
-
当不指定初始容量时,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); }
-
当不指定初始容量时,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]:
- HashSet 存储的元素无序,访问元素的时间接近常数级别。因此当不需要维持元素顺序时选择 HashSet 最合适。
- LinkedHashSet 以元素存入的顺序存储,遍历时按插入顺序进行访问。因此当需要保持存入元素时的顺序时适合使用 LinkedHashSet 。
- TreeSet 会对存入的元素进行排序,所以默认要求元素实现 Comparable(默认按照升序排序),也可以指定 Comparator 对象提供比较规则。因此如果需要按照自然顺序或自定义顺序遍历元素时适合用 TreeSet。
关于内部数据结构的实现方式在下面的 Map 部分说。
4. Map
整理中...