九. 初探集合
通常,程序总是根据运行时才知道的某些条件去创建新的对象,在此之前,无法知道所需对象的数量甚至确切类型。为了解决这个普遍的编程问题,需要在任意时刻和任意位置创建任意数量的对象。
Java有多种方式保存对象(确切地说,是对象的引用)。例如数组,是保存一组对象的最有效的方式,但是数组具有固定的大小尺寸,在很多时候可能需要一种更灵活的或支持更复杂的存储对象的方式。为此,java.util 库提供了一套相当完整的 集合类(又称容器类)来解决这个问题,其中基本的类型有 List 、 Set 、 Queue 和 Map。
Java集合类都可以自动地调整自己的大小。
1. 基本概念
- 集合(Collection) :一个独立元素的序列,这些元素都服从一条或多条规则。List 必须以插入的顺序保存元素, Set 不能包含重复元素, Queue 按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)。
- 映射(Map): 一组成对的“键值对”对象,允许使用键来查找值。 ArrayList 使用数字来查找对象,因此在某种意义上讲,它是将数字和对象关联在一起。 Map 允许我们使用一个对象来查找另一个对象,它也被称作关联数组(或者叫字典)。
在一些情况下,编写的大部分代码都在与这些接口打交道,并且唯一需要指定所使用的精确类型的地方就是在创建的时候。集合创建示例:
List<Apple> apples = new ArrayList<>();
ArrayList 已经被向上转型为了 List ,使用接口的目的是,如果想要改变具体实现,只需在创建时修改它就行了,就像下面这样:
List<Apple> apples = new LinkedList<>();
注意:这种方式并非总是有效的,因为某些具体类有额外的功能。例如, LinkedList 具有 List 接口中未包含的额外方法,而 TreeMap 也具有在 Map 接口中未包含的方法。如果需要使用这些方法,就不能将它们向上转型为更通用的接口。
Collection 接口概括了序列的概念——一种存放一组对象的方式。下面是用 Integer 对象填充了一个 Collection ,然后打印集合中的每个元素:
import java.util.*;
public class SimpleCollection {
public static void main(String[] args) {
Collection<Integer> c = new ArrayList<>();
for(int i = 0; i < 10; i++)
c.add(i); // 自动装箱
for(Integer i : c)
System.out.print(i + ", ");
}
}
输出结果:
2. 添加元素组
在 java.util 包中的 Arrays 和 Collections 类中都有很多实用的方法,可以在一个 Collection 中添加一组元素。
Arrays.asList() 方法接受一个数组或是逗号分隔的元素列表(使用可变参数),并将其转换为 List 对象。 Collections里的addAll() 方法接受一个 Collection 对象,以及一个数组或是一个逗号分隔的列表,将其中元素添加到 Collection 中。代码示例:
import java.util.*;
public class AddingGroups {
public static void main(String[] args) {
Collection<Integer> collection = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
Integer[] moreInts = { 6, 7, 8, 9, 10 };
collection.addAll(Arrays.asList(moreInts));
Collections.addAll(collection, 11, 12, 13, 14, 15);
Collections.addAll(collection, moreInts);
List<Integer> list = Arrays.asList(16,17,18,19,20);
list.set(1, 99);
}
}
Collection 的构造器可以接受另一个 Collection,用它来将自身初始化。因此,可以使用 Arrays.asList() 来为这个构造器产生输入。但是, Collections.addAll() 运行得更快,而且很容易构建一个不包含元素的 Collection ,因此这是首选方式。
然而,Collection.addAll() 方法只能接受另一个 Collection 作为参数,因此它没有 Arrays.asList() 或 Collections.addAll() 灵活。这两个方法都使用可变参数列表。
也可以直接使用 Arrays.asList() 的输出作为一个 List ,但是这里的底层实现是数组,没法调整大小。
public class Test {
public static void main(String[] args) {
List<Fruit> fruit = Arrays.<Fruit>asList(new Apple(), new Orange());
}
}
class Fruit {}
class Apple extends Fruit{}
class Orange extends Fruit{}
注意 Arrays.asList() 中间的“暗示”(即 ),告诉编译器 Arrays.asList() 生成的结果 List 类型的实际目标类型是什么,这称为显式类型参数说明。
3. 集合的打印
一般使用 Arrays.toString() 来生成数组的可打印形式。
4. 列表 List
List 将元素保存在特定的序列中,该接口在 Collection 的基础上添加了许多方法,允许在 List 的中间插入和删除元素。
有两种类型的 List :
- 基本的 ArrayList ,擅长随机访问元素,但在 List 中间插入和删除元素时速度较慢。
- LinkedList ,它通过代价较低的在 List 中间进行的插入和删除操作,提供了优化的顺序访问。 LinkedList 对于随机访问来说相对较慢,但它具有比 ArrayList 更大的特征集。
可以使用 contains() 方法确定对象是否在列表中。如果要删除一个对象,可以将该对象的引用传递给 remove() 方法。同样,如果有一个对象的引用,可以使用 indexOf() 在 List 中找到该对象所在位置的下标号。
当确定元素是否是属于某个 List ,寻找某个元素的索引,以及通过引用从 List 中删除元素时,都会用到 equals() 方法(根类 Object 的一个方法)。
还有其它许多 api ,直接去看源码。
5. 迭代器 Iterators
迭代器是一个对象,它在一个序列中移动并选择该序列中的每个对象,而客户端程序员不用关心该序列的底层结构(不必知晓集合的确切类型,迭代器统一了对集合的访问方式)。另外,迭代器通常被称为轻量级对象:创建它的代价小。Java 的 Iterator 只能单向移动,这个 Iterator 能用来:
- 使用 iterator() 方法要求集合返回一个 Iterator,Iterator 将准备好返回序列中的第一个元素。
- 使用 next() 方法获得序列中的下一个元素。
- 使用 hasNext() 方法检查序列中是否还有元素。
- 使用 remove() 方法将迭代器最近返回的那个元素删除。
代码示例:
public class Test {
public static void main(String[] args) {
List<Apple> apples = new ArrayList<>(10);
Iterator<Apple> appleIterator = apples.iterator();
while (appleIterator.hasNext()){
Apple a = appleIterator.next();
}
}
}
Iterator 可以删除由 next() 生成的最后一个元素,这意味着在调用 remove() 之前必须先调用 next() 。
ListIterator
ListIterator 是一个更强大的 Iterator 子类型,它只能由各种 List 类生成。 Iterator 只能向前移动,而 ListIterator 可以双向移动。
它可以生成迭代器在列表中指向位置的后一个和前一个元素的索引,并且可以使用 set() 方法替换它访问过的最近一个元素。
可以通过调用 listIterator() 方法来生成指向 List 开头处的 ListIterator ,还可以通过调用 listIterator(n) 创建一个一开始就指向列表索引号为 n 的元素处的 ListIterator 。
6. 链表 LinkedList
LinkedList 也像 ArrayList 一样实现了基本的 List 接口,但它在 List 中间执行插入和删除操作时比 ArrayList 更高效。然而,它在随机访问操作效率方面却要逊色一些。
LinkedList 还添加了一些方法,使其可以被用作栈、队列或双端队列 。在这些方法中,有些彼此之间可能只是名称有些差异,或者只存在些许差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是在 Queue 中)。例如:
- getFirst() 和 element() 是相同的,它们都返回列表的头部(第一个元素)而并不删除它,如果 List 为空,则抛出 NoSuchElementException 异常, peek() 方法与这两个方法只是稍有差异,它在列表为空时返回 null 。
- removeFirst() 和 remove() 也是相同的,它们删除并返回列表的头部元素,并在列表为空时抛出 NoSuchElementException 异常,poll() 稍有差异,它在列表为空时返回 null 。
- addFirst() 在列表的开头插入一个元素。
- offer() 与 add() 和 addLast() 相同,它们都在列表的尾部(末尾)添加一个元素。
- removeLast() 删除并返回列表的最后一个元素。
7. 堆栈 Stack
堆栈是“后进先出”集合,有时被称为叠加栈,因为最后“压入”栈的元素,第一个被“弹出”栈,就像“弹夹”一样。
Java 1.0 中附带了一个 Stack 类,Java 6 添加了 ArrayDeque ,其中包含直接实现堆栈功能的方法。代码示例:
public class Test {
public static void main(String[] args) {
Deque<String> stack = new ArrayDeque<>();
for (String s : "This is the test of Stack".split(" ")) {
stack.push(s);
}
while (!stack.isEmpty()) {
System.out.print(stack.pop() + " ");
}
}
}
输出结果:
尽管已经有了 java.util.Stack ,但是 ArrayDeque 可以产生更好的 Stack 。
8. 集合 Set
Set 不保存重复的元素。Set 最常见的用途是测试归属性,可以很轻松地询问某个对象是否在一个 Set 中。因此,查找通常是 Set 最重要的操作,一般会选择 HashSet 实现,该实现针对快速查找进行了优化。
Set 具有与 Collection 相同的接口,没有任何额外的功能,不像前面两种不同类型的 List 那样。实际上, Set 就是一个 Collection ,只是行为不同。
早期 Java 版本中的 HashSet 产生的输出没有可辨别的顺序,出于对速度的追求, HashSet 使用了散列。由 HashSet 维护的顺序与 TreeSet 或 LinkedHashSet 不同,因为它们的实现具有不同的元素存储方式。 TreeSet 将元素存储在红黑树中,而 HashSet 使用散列函数。 LinkedHashSet 因为查询速度的原因也使用了散列,并使用了链表来维护元素的插入顺序。
9. 映射 Map
将对象映射到其他对象的能力是解决编程问题的有效方法。例如,考虑一个程序,它被用来检查 Java 的 Random 类的随机性。理想情况下, Random 会产生完美的数字分布,但为了测试这一点,则需要生成大量的随机数,并计算落在各种范围内的数字个数。 Map 可以很容易地解决这个问题。在本例中,键是 Random 生成的数字,而值是该数字出现的次数:
public class Test {
public static void main(String[] args) {
Random rand = new Random(47);
Map<Integer, Integer> m = new HashMap<>();
for(int i = 0; i < 10000; i++) {
int r = rand.nextInt(20);
Integer freq = m.get(r);
m.put(r, freq == null ? 1 : freq + 1);
}
System.out.println(m);
}
}
输出结果:
Map 与数组和其他的 Collection 一样,可以轻松地扩展到多个维度,只需要创建一个值为 Map 的 Map(这些 Map 的值可以是其他集合,甚至是其他 Map)。
10. 队列 Queue
队列是一个典型的“先进先出”集合。 即从集合的一端放入事物,再从另一端去获取它们,事物放入集合的顺序和被取出的顺序是相同的。队列在并发编程中尤为重要,因为它们可以安全地将对象从一个任务传输到另一个任务。
LinkedList 实现了 Queue 接口,并且提供了一些方法以支持队列行为,因此 LinkedList 可以用作 Queue 的一种实现。
与 Queue 相关的方法提供了完整而独立的功能,也就是说,对于 Queue 所继承的 Collection ,在不需要使用它的任何方法的情况下,就可以拥有一个可用的 Queue 。
优先级队列 PriorityQueue
先进先出描述了最典型的队列规则。队列规则是指在给定队列中的一组元素的情况下,确定下一个弹出队列的元素的规则。先进先出声明的是下一个弹出的元素应该是等待时间最长的元素。
而在Java 5 中添加了 PriorityQueue 优先级队列,可以声明下一个弹出的元素是最需要的元素(具有最高的优先级)。
当在 PriorityQueue 上调用 offer() 方法来插入一个对象时,该对象会在队列中被排序。默认的排序使用队列中对象的自然顺序,但是可以通过提供自己的 Comparator 来修改这个顺序。PriorityQueue 确保在调用 peek() , poll() 或 remove() 方法时,获得的元素将是队列中优先级最高的元素。
参考资料:On Java 8