本节描述Java集合框架。在这里,您将了解什么是集合,以及它们如何使您的工作更轻松,程序更好。您将了解组成Java Collections Framework的核心元素——接口、实现、聚合操作和算法。
介绍告诉您集合是什么,以及它们如何使您的工作更轻松,程序更好。您将了解组成集合框架的核心元素:接口,实现和算法。
接口描述了核心集合接口,它们是Java集合框架的核心和灵魂。您将了解有效使用这些接口的一般准则,包括何时使用哪个接口。您还将学习每个接口的习惯用法,这些习惯用法将帮助您最大限度地利用这些接口。
聚合操作代表您遍历集合,这使您能够编写更简洁、更有效的代码来处理存储在集合中的元素。
实现描述了JDK的通用集合实现,并告诉您何时使用哪个实现。您还将了解包装器实现(wrapper implementations
),它向通用实现添加功能。
算法描述了JDK提供的用于操作集合的多态算法(polymorphic algorithms
)。幸运的话,您将再也不用编写自己的排序例程了!
自定义实现告诉您为什么可能希望编写自己的集合实现(而不是使用JDK提供的通用实现之一),以及如何实现它。使用JDK的抽象集合实现(abstract collection implementations
)很容易!
互操作性告诉您集合框架如何与早于向Java添加集合的旧api进行互操作。此外,它还告诉您如何设计新的api,以便它们能够与其他新api无缝地互操作。
集合(有时称为容器
)就是将多个元素组合成单个单元的对象。集合用于存储、检索、操作和传送聚合数据。通常,它们表示形成一个自然组的数据项,例如扑克手牌(一组纸牌)、邮件文件夹(一组信件)或电话目录(姓名到电话号码的映射)。如果您使用过Java编程语言——或者任何其他编程语言——那么您已经熟悉集合了。
集合框架(collections framework
)是用于表示和操作集合的统一体系结构。所有集合框架都包含以下内容:
Interfaces
): 这些是表示集合的抽象数据类型。接口允许对集合进行独立于其表示细节的操作。在面向对象语言中,接口通常形成继承。除了Java集合框架,最著名的集合框架的例子是c++标准模板库(STL)和Smalltalk的集合层次结构。从历史上看,集合框架非常复杂,这使它们以具有陡峭的学习曲线而闻名。我们相信Java集合框架打破了这一传统,你将在本章中了解到这一点。
Java集合框架提供了以下好处:
核心集合接口(core collection interfaces
)封装了不同类型的集合,如下图所示。这些接口允许对集合进行独立于其表示细节的操作。核心集合接口是Java集合框架的基础。如下图所示,核心集合接口形成了一个层次结构。
Set
是一种特殊类型的Collection
, SortedSet
是一种特殊类型的Set
,等等。还要注意,层次结构由两个不同的树组成——Map
不是真正的Collection
。
注意,所有核心集合接口都是泛型的。例如,这是Collection
接口的声明:
public interface Collection<E> extends Iterable<E> {
}
语法告诉您该接口是泛型的。在声明Collection
实例时,可以而且应该指定集合中包含的对象的类型。指定类型允许编译器(在编译时)验证放入集合中的对象的类型是否正确,从而减少运行时的错误。有关泛型类型的信息,请参阅泛型(已更新)课程。
当您了解如何使用这些接口时,您将了解关于Java集合框架的大部分知识。本章讨论有效使用接口的一般准则,包括何时使用哪个接口。您还将学习每个接口的编程习惯,以帮助您充分利用它。
为了保持核心集合接口的可管理数量,Java平台没有为每个集合类型的每个变体提供单独的接口。(这些变体可能包括不可变的、固定大小的和仅追加的。)相反,每个接口中的修改操作都被指定为可选的——给定的实现可以选择不支持所有操作。如果调用了不受支持的操作,则集合将抛出UnsupportedOperationException。实现负责记录它们支持哪些可选操作。所有Java平台的通用实现都支持所有可选操作。
Iterable
For-each循环
// 实现这个接口允许一个对象成为“for-each loop”语句的目标。
// 参见For-each循环
// 迭代器返回的元素类型
public interface Iterable<T> {
// 返回一个遍历T类型元素的迭代器。
Iterator<T> iterator();
// 对 Iterable 的每个元素执行给定的操作,直到处理完所有元
// 素或该操作抛出异常。除非实现类另有指定,否则操作将按照
// 迭代顺序执行(如果指定了迭代顺序)。动作引发的异常被传递
// 给调用方。
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
// 在这个Iterable描述的元素上创建一个Spliterator。
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}
Spliterator
// 用于遍历和划分源元素的对象。Spliterator覆盖的元素源可以是
// 数组、Collection、IO通道或生成器函数。
// Spliterator可以单独遍历元素(tryAdvance()),也可以批量遍
// 历元素(forEachRemaining())。
// Spliterator也可以将它的一些元素(使用tryssplit())划分为另一
// 个Spliterator,用于可能的并行操作。使用Spliterator的操作
// 如果不能进行拆分,或者以极不平衡或效率低下的方式进行拆分,
// 则不太可能从并行性中获益。遍历和划分元素;每个
// Spliterator仅对单个批量计算有用。
public interface Spliterator<T> {
}
Iterator
// 集合上的迭代器。在Java集合框架中,迭代器取代了枚举(Enumeration)。迭代器
// 与枚举在两个方面不同:
// 迭代器允许调用者在迭代期间使用定义良好的语义从底层集合中删除元素。
// 方法命名得到了改进。
public interface Iterator<E> {
// 如果迭代包含更多元素,则返回true。(换句话说,如果next
// 将返回一个元素而不是抛出异常,则返回true。)
boolean hasNext();
// 返回迭代中的下一个元素。
E next();
// 从底层集合中移除此迭代器返回的最后一个元素(可选操作)。
// 对next的每次调用只能调用此方法一次。如果在迭代过程中以
// 除调用此方法之外的任何方式修改了底层集合,则迭代器的行
// 为是未指定的。
default void remove() {
throw new UnsupportedOperationException("remove");
}
// 对每个剩余元素执行给定的操作,直到处理完所有元素或该操
// 作抛出异常。如果指定了迭代顺序,则按照迭代顺序执行操
// 作。动作引发的异常被传递给调用方。
default void forEachRemaining(Consumer<? super E> action) {}
}
public class Collections {
// 返回一个只包含指定对象的不可变 set。返回的 set是可序列化的。
public static <T> Set<T> singleton(T o) {}
}
核心集合接口介绍如下:
Collection
—— 集合层次结构的根。集合表示一组被称为其元素(elements
)的对象。Collection
接口是所有集合实现的最小公分母,用于传递集合,并在需要最大通用性时对其进行操作。有些类型的集合允许重复元素,有些则不允许。有些是有序的,有些是无序的。Java平台不提供此接口的任何直接实现,但提供了更具体的子接口的实现,例如Set
和List
。也请参见集合接口部分。Set
—— 不能包含重复元素的集合。该接口对数学集合抽象进行建模,并用于表示集合,例如组成扑克手牌的纸牌、组成学生课程表的课程或在机器上运行的进程。参见Set 接口部分。List 列表
—— 一个有序的集合(有时称为序列, sequence)。列表(Lists
)可以包含重复的元素。List
的用户通常可以精确控制每个元素在列表中的插入位置,并且可以通过整数索引(位置)访问元素。如果您使用过Vector
,那么您应该熟悉List的一般风格。另请参见List 接口部分。Queue 队列
—— 在处理之前用于保存多个元素的集合。除了基本的Collection
操作之外,Queue
还提供了额外的插入、提取和检查操作。remove
或poll
调用删除的元素。在FIFO队列中,所有新元素都插入到队列的尾部。其他类型的队列可能使用不同的放置规则。每个Queue
实现都必须指定其排序属性。另请参见队列接口一节。Deque
—— 在处理之前用于保存多个元素的集合。除了基本的Collection
操作外,Deque
还提供了附加的插入、提取和检查操作。Deque
可以作为FIFO(先进先出)和LIFO(后进先出)使用。在Deque
中,可以在两端插入、检索和删除所有新元素。另请参见Deque Interface一节。Map
—— 将键映射到值的对象。Map
不能包含重复的键;每个键最多只能映射到一个值。如果您使用过Hashtable
,那么您已经熟悉Map
的基础知识。也请参见Map接口部分。最后两个核心集合接口仅仅是Set
和Map
的排序版本:
SortedSet
—— 按升序维护其元素的Set。提供了几个额外的操作来利用排序。有序集(Sorted sets
)用于自然有序集,如单词列表和成员名册。也请参见SortedSet Interface一节。SortedMap
—— 按键的升序顺序维护其映射的Map。这是SortedSet
的Map
类比。排序映射用于键/值对的自然排序集合,例如字典和电话目录。也请参阅SortedMap接口一节。要了解排序接口如何维护其元素的顺序,请参阅对象排序一节。
public interface Collection<E> extends Iterable<E> {
// Query Operations
// 返回此集合中元素的数目。如果此集合包含多于
// Integer.MAX_VALUE元素,返回Integer.MAX_VALUE。
int size();
// 如果此集合不包含任何元素,则返回true。
boolean isEmpty();
// 如果此集合包含指定的元素,则返回true。更正式地说,当且
// 仅当此集合包含至少一个元素e满足 (o==null ? e==null : o.equals(e))。
boolean contains(Object o);
// 返回此集合中元素的迭代器。对于元素返回的顺序没有保证
// (除非此集合是提供保证的某个类的实例)。
Iterator<E> iterator();
// 返回包含此集合中所有元素的数组。如果此集合保证其迭代器
// 返回元素的顺序,则此方法必须以相同的顺序返回元素。
// 返回的数组将是“安全的”,因为此集合不维护对它的引用。
// (换句话说,这个方法必须分配一个新的数组,即使这个集合
// 是由数组支持的)。因此,调用者可以自由地修改返回的数组。
// 此方法充当基于数组和基于集合的api之间的桥梁。
Object[] toArray();
// 返回一个包含此集合中所有元素的数组;返回数组的运行时类
// 型为指定数组的运行时类型。如果集合适合指定的数组,则在
// 其中返回它。否则,将使用指定数组的运行时类型和此集合的
// 大小分配新数组。
// 如果这个集合适合指定的数组,并且有多余的空间(即,数组
// 的元素比这个集合的元素多),那么紧接在集合末尾的数组中
// 的元素将被设置为空。(只有当调用方知道该集合不包含任何
// null元素时,这才有助于确定该集合的长度。)
// 如果此集合保证其迭代器返回元素的顺序,则此方法必须以相
// 同的顺序返回元素。
// 与toArray()方法一样,该方法充当基于数组和基于集合的
// api之间的桥梁。此外,此方法允许对输出数组的运行时类型
// 进行精确控制,并且在某些情况下可以用于节省分配成本。
// 假设x是已知只包含字符串的集合。以下代码可用于将集合转
// 储到新分配的String数组中:
// String[] y = x.toArray(new String[0]);
// 注意,toArray(new Object[0])在功能上与toArray()相同。
// a -存储此集合元素的数组(如果该数组足够大);否则,将为此目的分配一个相同运行时类型的新数组。
<T> T[] toArray(T[] a);
// Modification Operations
// 确保此集合包含指定的元素(可选操作)。如果此集合因调用而
// 更改,则返回true。(如果此集合不允许重复且已包含指定元
// 素,则返回false。)
// 支持此操作的集合可能会对可以添加到该集合的元素设置限
// 制。特别是,一些集合将拒绝添加null元素,而其他集合将对
// 可能添加的元素类型施加限制。集合类应该在它们的文档中清
// 楚地指定可以添加哪些元素的任何限制。
// 如果集合拒绝添加特定元素的原因不是因为它已经包含该元
// 素,它必须抛出异常(而不是返回false)。这保留了在此调用
// 返回后集合始终包含指定元素的不变性。
boolean add(E e);
// 从此集合中删除指定元素的单个实例(如果存在)(可选操作)。
// 更正式地说,删除元素e,使(o==null ? e==null : o.equals(e)),
// 如果这个集合包含一个或多个这样的元素。如果此集合包含指
// 定的元素(或者等价地,如果此集合因调用而更改),则返回true。
boolean remove(Object o);
// Bulk Operations
// 如果此集合包含指定集合中的所有元素,则返回true。
boolean containsAll(Collection<?> c);
// 将指定集合中的所有元素添加到此集合(可选操作)。如果在操
// 作进行期间修改了指定的集合,则此操作的行为是未定义的。
// (这意味着,如果指定的集合是此集合,并且此集合是非空
// 的,则此调用的行为是未定义的。)
boolean addAll(Collection<? extends E> c);
// 移除指定集合中也包含的此集合的所有元素(可选操作)。在此
// 调用返回后,此集合将不包含与指定集合相同的元素。
// 如果此集合因调用而更改,则返回True
boolean removeAll(Collection<?> c);
// 删除此集合中满足给定谓词的所有元素。在迭代期间或由谓词
// 抛出的错误或运行时异常将传递给调用方。
// filter 对要删除的元素返回true的谓词
// 如果删除任何元素,则返回True
default boolean removeIf(Predicate<? super E> filter) {}
// 仅保留此集合中包含在指定集合中的元素(可选操作)。换句话
// 说,从此集合中删除未包含在指定集合中的所有元素。
// 如果此集合因调用而更改,则返回True
boolean retainAll(Collection<?> c);
// 从该集合中删除所有元素(可选操作)。此方法返回后,集合将为空。
void clear();
// Comparison and hashing
// 比较指定对象与此集合是否相等。
// 而Collection接口没有为Object的通用契约添加任何规定。
// 因此,“直接”实现Collection接口的程序员(换句话说,创建
// 一个是Collection但不是Set或List的类)在选择覆盖
// Object.equals时必须非常小心。没有必要这样做,最简单的
// 做法是依赖Object的实现,但实现者可能希望实现“值比较”
// 来代替默认的“引用比较”。(List和Set接口要求进行这样的值比较。)
// Object.equals 方法的一般约定是等于必须对称(换句话
// 说,a.equals(b)如果并且只有在b.equals(a))。
// List.equals 和 Set.equals 的约定是列表只等于其他列表,集合只等于其他集合。
// 因此,既不实现List也不实现Set接口的集合类的自定义
// equals方法在将该集合与任何列表或集合进行比较时必须返
// 回false。(按照同样的逻辑,不可能编写一个同时正确实现Set和List接口的类。)
// 如果指定的对象等于此集合,则返回True
boolean equals(Object o);
// 返回此集合的哈希码值。而Collection接口没有为Object.hashCode 方法的
// 通用契约添加任何规定。程序员应该注意任
// 何覆盖 Object.equals方法也必须覆盖 Object.hashCode
// 方法,以满足 Object.hashCode方法的一般契约。特别地,
// c1.equals(c2)意味着 c1.hashCode()==c2.hashCode()。
int hashCode();
// 返回以此集合为源的顺序流(sequential Stream)。
// 当spliterator()方法不能返回不可变、并发或延迟绑定的spliterator
// 时,应该重写此方法。(详细信息请参见spliterator()。)
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
// 返回以此集合作为源的可能并行流。允许此方法返回顺序流。
// 当spliterator()方法不能返回不可变、并发或延迟绑定的spliterator
// 时,应该重写此方法。(详细信息请参见spliterator()。)
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
}
Collection 表示一组被称为其元素的对象。Collection接口用于传递需要最大通用性的对象集合。例如,按照约定,所有通用集合实现都有一个接受Collection
参数的构造函数。这个构造函数称为转换构造函数(conversion constructor),它初始化新集合以包含指定集合中的所有元素,而不管给定集合的子接口或实现类型如何。换句话说,它允许您转换集合的类型。
例如,假设您有一个Collection
,它可以是List
、Set
或其他类型的Collection
。这个习惯用法创建了一个新的ArrayList
(List
接口的实现),最初包含c
中的所有元素。
List<String> list = new ArrayList<String>(c);
或者——如果你使用的是JDK 7或更高版本——你可以使用菱形操作符:
List<String> list = new ArrayList<>(c);
Collection
接口包含执行基本操作的方法,例如int size()
, boolean isEmpty()
, boolean contains(Object element)
, boolean add(E element)
, boolean remove(Object element)
, and Iterator
.
它还包含对整个集合进行操作的方法,例如boolean containsAll(Collection> c)
, boolean addAll(Collection extends E> c)
, boolean removeAll(Collection> c)
, boolean retainAll(Collection> c)
, 和void clear()
.
用于数组操作的附加方法such as Object[] toArray()
和
在JDK 8及更高版本中,Collection
接口还公开了Stream
和Stream
,方法,用于从底层集合获取顺序或并行流。(有关使用流的更多信息,请参阅“聚合操作”一课。)
假定Collection
表示一组对象,那么Collection
接口所做的工作与您所期望的差不多。它有告诉您集合中有多少元素的方法(size
, isEmpty
),检查给定对象是否在集合中(contains
)的方法,从集合中添加和删除元素的方法(add
, remove
),以及提供在集合上的迭代器的方法(iterator
)。
add
方法的定义通常足够充分,因此它对允许重复的集合和不允许重复的集合都有意义。它保证在调用完成后,Collection
将包含指定的元素,如果调用导致Collection
发生更改,则返回true
。类似地,remove
方法被设计为从Collection中
删除指定元素的单个实例(假设它包含要开始的元素),并且如果Collection
因此被修改,则返回true
。
有三种遍历集合的方法:
(1)使用聚合操作;
(2)使用 for-each 结构;
(3)使用Iterators
。
在JDK 8及以后的版本中,迭代集合的首选方法是获取流并对其执行聚合操作。聚合操作通常与lambda表达式结合使用,以使用更少的代码行,使编程更具表现力。下面的代码依次遍历一组形状并打印出红色的对象:
myShapesCollection.stream()
.filter(e -> e.getColor() == Color.RED)
.forEach(e -> System.out.println(e.getName()));
同样地,你可以很容易地请求并行流,如果集合足够大并且你的计算机有足够的核心,这可能是有意义的:
myShapesCollection.parallelStream()
.filter(e -> e.getColor() == Color.RED)
.forEach(e -> System.out.println(e.getName()));
有许多不同的方法可以使用这个API收集数据。例如,您可能希望将Collection
的元素转换为String
对象,然后将它们连接起来,并用逗号分隔:
String joined = elements.stream()
.map(Object::toString)
.collect(Collectors.joining(", "));
或者把所有员工的工资加起来:
int total = employees.stream()
.collect(Collectors.summingInt(Employee::getSalary)));
这些只是你可以用流和聚合操作做什么的几个例子。有关更多信息和示例,请参阅题为“聚合操作”的课程。
Collections
框架一直提供许多所谓的“批量操作”作为其API的一部分。这些方法包括对整个集合进行操作的方法,如containsAll
、addAll
、removeAll
等。不要将这些方法与JDK 8中引入的聚合操作混淆。新的聚合操作和现有的批量操作(containsAll
、addAll
等)之间的关键区别在于,旧版本都是可变的,这意味着它们都修改底层集合。相反,新的聚合操作不会修改底层集合。在使用新的聚合操作和lambda表达式时,必须注意避免改变,以免在以后从并行流运行代码时引入问题。
for-each
结构允许您使用for循环简明地遍历集合或数组——参见for语句。下面的代码使用for-each结构在单独的行上打印出集合的每个元素。
for (Object o : collection)
System.out.println(o);
Iterator是一个对象,它使您能够遍历集合,并根据需要有选择地从集合中删除元素。通过调用一个集合的Iterator
方法,可以获得一个集合的Iterator
。下面是Iterator
接口。
public interface Iterator<E> {
boolean hasNext();
E next();
void remove(); //optional
}
如果迭代有更多元素,hasNext
方法返回true
, next
方法返回迭代中的下一个元素。remove
方法从底层Collection
中删除next
返回的最后一个元素。每次调用next
时只能调用remove
方法一次,如果违反此规则则抛出异常。
注意,Iterator.remove
是在迭代期间修改集合的唯一安全方法;如果在迭代进行过程中以任何其他方式修改底层集合,则未指定行为。
当需要使用Iterator
代替for-each
结构:
for-each
构造隐藏了迭代器,因此不能调用remove
。因此,for-each
结构不能用于过滤。下面的方法向您展示了如何使用Iterator
筛选任意Collection
—即遍历集合以删除特定元素。
static void filter(Collection<?> c) {
for (Iterator<?> it = c.iterator(); it.hasNext(); )
if (!cond(it.next()))
it.remove();
}
这段简单的代码是多态的,这意味着无论实现如何,它都适用于任何集合。这个示例演示了使用Java Collections Framework编写多态算法是多么容易。
批量操作(Bulk operations
)对整个Collection
执行操作。您可以使用基本操作来实现这些简写操作,尽管在大多数情况下,这样的实现效率较低。以下是批量操作:
containsAll
—如果目标集合包含指定集合中的所有元素,则返回true
。addAll
—将指定Collection
中的所有元素添加到目标Collection
。removeAll
—从目标Collection
中删除指定Collection
中也包含的所有元素。retainAll
—从目标Collection
中删除指定Collection
中未包含的所有元素。也就是说,它只保留目标Collection
中也包含在指定Collection
中的那些元素。clear
-从Collection
中删除所有元素如果在执行操作的过程中修改了目标Collection
,那么addAll
、removeAll
和retainAll
方法都返回true
。
作为批量操作强大功能的一个简单示例,考虑以下从Collection
c
中删除指定元素e
的所有实例的习惯用法。
c.removeAll(Collections.singleton(e));
更具体地说,假设您希望从集合中删除所有null
元素。
c.removeAll(Collections.singleton(null));
这个习惯使用的 Collections.singleton
,它是一个静态工厂方法,返回一个只包含指定元素的不可变Set
。
toArray
方法是作为集合和期望输入数组的旧api之间的桥梁提供的。数组操作允许将Collection
的内容转换为数组。不带参数的简单表单创建一个新的Object
数组。更复杂的形式允许调用者提供数组或选择输出数组的运行时类型。
例如,假设c
是一个Collection
。下面的代码片段将c
的内容转储到一个新分配的Object
数组中,该数组的长度与c
中的元素数量相同。
Object[] a = c.toArray();
假设已知c
只包含字符串(可能因为c
的类型是Collection
)。下面的代码片段将c
的内容转储到一个新分配的String
数组中,该数组的长度与c
中的元素数量相同。
String[] a = c.toArray(new String[0]);