Java 中文官方教程 2022 版(二十七)
对象排序
原文:
docs.oracle.com/javase/tutorial/collections/interfaces/order.html
一个List
l
可以按以下方式排序。
Collections.sort(l);
如果List
包含String
元素,则将按字母顺序对其进行排序。如果包含Date
元素,则将按时间顺序对其进行排序。这是如何发生的呢?String
和Date
都实现了Comparable
接口。
Comparable实现为一个类提供了*自然排序*,允许该类的对象自动排序。下表总结了一些更重要的实现
Comparable`接口的 Java 平台类。
实现 Comparable 接口的类
类 | 自然排序 |
---|---|
Byte |
有符号数值 |
Character |
无符号数值 |
Long |
有符号数值 |
Integer |
有符号数值 |
Short |
有符号数值 |
Double |
有符号数值 |
Float |
有符号数值 |
BigInteger |
有符号数值 |
BigDecimal |
有符号数值 |
Boolean |
Boolean.FALSE < Boolean.TRUE |
File |
基于路径名的系统相关字典序 |
String |
字典序 |
Date |
时间顺序 |
CollationKey |
区域特定字典序 |
如果你尝试对一个不实现Comparable
接口的列表进行排序,Collections.sort(list)
将抛出一个ClassCastException
。同样,如果你尝试使用comparator
对无法相互比较的列表进行排序,Collections.sort(list, comparator)
将抛出ClassCastException
。可以相互比较的元素被称为可相互比较的。尽管不同类型的元素可能是可相互比较的,但这里列出的类中没有一个允许跨类比较。
如果你只想对可比较元素的列表进行排序或创建排序的集合,那么关于Comparable
接口,这就是你真正需要知道的全部内容。如果你想要实现自己的Comparable
类型,那么下一节将对你感兴趣。
编写自己的可比较类型
Comparable
接口包含以下方法。
public interface Comparable<T> {
public int compareTo(T o);
}
compareTo
方法比较接收对象与指定对象,并根据接收对象是小于、等于还是大于指定对象返回负整数、0 或正整数。如果指定对象无法与接收对象比较,则该方法会抛出ClassCastException
。
下面的类代表一个人的名字,实现了Comparable
。examples/Name.java
import java.util.*;
public class Name implements Comparable<Name> {
private final String firstName, lastName;
public Name(String firstName, String lastName) {
if (firstName == null || lastName == null)
throw new NullPointerException();
this.firstName = firstName;
this.lastName = lastName;
}
public String firstName() { return firstName; }
public String lastName() { return lastName; }
public boolean equals(Object o) {
if (!(o instanceof Name))
return false;
Name n = (Name) o;
return n.firstName.equals(firstName) && n.lastName.equals(lastName);
}
public int hashCode() {
return 31*firstName.hashCode() + lastName.hashCode();
}
public String toString() {
return firstName + " " + lastName;
}
public int compareTo(Name n) {
int lastCmp = lastName.compareTo(n.lastName);
return (lastCmp != 0 ? lastCmp : firstName.compareTo(n.firstName));
}
}
为了保持前面的示例简洁,该类有一定的限制:它不支持中间名,要求同时有名和姓,而且在任何方面都没有国际化。尽管如此,它阐明了以下重要点:
-
Name
对象是不可变的。其他条件相同的情况下,不可变类型是最好的选择,特别是对于将用作Set
中的元素或Map
中的键的对象。如果在集合中修改它们的元素或键,这些集合将会中断。 -
构造函数检查其参数是否为
null
。这确保所有的Name
对象都是格式良好的,以便其他方法永远不会抛出NullPointerException
。 -
hashCode
方法被重新定义。这对于重新定义equals
方法的任何类都是必不可少的。(相等的对象必须具有相等的哈希码。) -
equals
方法在指定对象为null
或不合适类型时返回false
。compareTo
方法在这些情况下会抛出运行时异常。这两种行为都是各自方法的一般契约所要求的。 -
toString
方法已被重新定义,以便以人类可读的形式打印Name
。这总是一个好主意,特别是对于将要放入集合中的对象。各种集合类型的toString
方法依赖于它们的元素、键和值的toString
方法。
由于本节是关于元素排序的,让我们再谈一下 Name
的 compareTo
方法。它实现了标准的名称排序算法,其中姓氏优先于名字。这正是你在自然排序中想要的。如果自然排序是不自然的,那将会非常令人困惑!
看一下 compareTo
的实现方式,因为它是相当典型的。首先,你比较对象的最重要部分(在这种情况下是姓)。通常情况下,你可以直接使用部分类型的自然排序。在这种情况下,部分是一个 String
,自然(词典)排序正是所需的。如果比较结果不是零,表示相等,你就完成了:只需返回结果。如果最重要的部分相等,你继续比较下一个最重要的部分。在这种情况下,只有两个部分——名和姓。如果有更多的部分,你会按照明显的方式继续,比较部分直到找到两个不相等的部分或者你正在比较最不重要的部分,此时你会返回比较的结果。
为了展示它是如何工作的,这里是一个构建名称列表并对其进行排序的程序。
import java.util.*;
public class NameSort {
public static void main(String[] args) {
Name nameArray[] = {
new Name("John", "Smith"),
new Name("Karl", "Ng"),
new Name("Jeff", "Smith"),
new Name("Tom", "Rich")
};
List<Name> names = Arrays.asList(nameArray);
Collections.sort(names);
System.out.println(names);
}
}
如果你运行这个程序,这是它打印的内容。
[Karl Ng, Tom Rich, Jeff Smith, John Smith]
compareTo
方法的行为有四个限制,我们现在不会详细讨论,因为它们相当技术性和乏味,并且最好留在 API 文档中。所有实现Comparable
的类都必须遵守这些限制,因此如果您正在编写实现它的类,请阅读Comparable
的文档。尝试对违反这些限制的对象列表进行排序会导致未定义的行为。从技术上讲,这些限制确保自然排序是实现它的类的对象上的全序;这是确保排序是明确定义的必要条件。
比较器
如果您想按照除自然排序之外的顺序对一些对象进行排序怎么办?或者如果您想对一些不实现Comparable
接口的对象进行排序怎么办?要做到这两点,您需要提供一个Comparator
一个封装排序的对象。与Comparable
接口一样,Comparator
接口由一个方法组成。
public interface Comparator<T> {
int compare(T o1, T o2);
}
compare
方法比较其两个参数,根据第一个参数是否小于、等于或大于第二个参数返回负整数、0 或正整数。如果任一参数的类型对于Comparator
不合适,则compare
方法会抛出ClassCastException
。
大部分关于Comparable
的内容也适用于Comparator
。编写compare
方法几乎与编写compareTo
方法相同,只是前者将两个对象作为参数传递。compare
方法必须遵守与Comparable
的compareTo
方法相同的四个技术限制,原因是Comparator
必须对其比较的对象引入一个全序。
假设您有一个名为Employee
的类,如下所示。
public class Employee implements Comparable<Employee> {
public Name name() { ... }
public int number() { ... }
public Date hireDate() { ... }
...
}
假设Employee
实例的自然排序是根据员工姓名(如前面的示例中定义的)的Name
排序。不幸的是,老板要求按资历顺序列出员工名单。这意味着我们需要做一些工作,但不多。以下程序将生成所需的列表。
import java.util.*;
public class EmpSort {
static final Comparator<Employee> SENIORITY_ORDER =
new Comparator<Employee>() {
public int compare(Employee e1, Employee e2) {
return e2.hireDate().compareTo(e1.hireDate());
}
};
// Employee database
static final Collection<Employee> employees = ... ;
public static void main(String[] args) {
List<Employee> e = new ArrayList<Employee>(employees);
Collections.sort(e, SENIORITY_ORDER);
System.out.println(e);
}
}
程序中的Comparator
相当简单。它依赖于应用于hireDate
访问器方法返回的值的Date
的自然排序。请注意,Comparator
将其第二个参数的入职日期传递给其第一个参数,而不是反过来。原因是最近入职的员工资历最低;按照入职日期排序会将列表按照逆资历顺序排列。有时人们用来实现这种效果的另一种技术是保持参数顺序,但对比较结果取反。
// Don't do this!!
return -r1.hireDate().compareTo(r2.hireDate());
你应该始终使用前一种技术而不是后一种,因为后一种不能保证有效。原因是compareTo
方法如果其参数小于调用它的对象,则可以返回任何负的int
。有一个负的int
在取反后仍然是负的,尽管这看起来很奇怪。
-Integer.MIN_VALUE == Integer.MIN_VALUE
前面程序中的Comparator
用于对List
进行排序很好,但它有一个缺陷:它不能用于对已排序的集合(如TreeSet
)进行排序,因为它生成的排序与equals
不兼容。这意味着这个Comparator
将把equals
方法不认为相等的对象等同起来。特别是,任何在同一天入职的两名员工将被视为相等。当你对List
进行排序时,这并不重要;但当你使用Comparator
对已排序的集合进行排序时,这是致命的。如果你使用这个Comparator
将多名在同一天入职的员工插入TreeSet
,只有第一个会被添加到集合中;第二个将被视为重复元素并被忽略。
要解决这个问题,只需微调Comparator
,使其生成一个与equals
兼容的排序。换句话说,调整它使得当使用compare
进行比较时,只有那些在使用equals
进行比较时也被视为相等的元素才被视为相等。做法是执行一个两部分比较(如对Name
),其中第一部分是我们感兴趣的部分——在这种情况下是入职日期——第二部分是一个唯一标识对象的属性。在这里,员工编号是显而易见的属性。这是产生的Comparator
。
static final Comparator<Employee> SENIORITY_ORDER =
new Comparator<Employee>() {
public int compare(Employee e1, Employee e2) {
int dateCmp = e2.hireDate().compareTo(e1.hireDate());
if (dateCmp != 0)
return dateCmp;
return (e1.number() < e2.number() ? -1 :
(e1.number() == e2.number() ? 0 : 1));
}
};
最后一点:你可能会想要用更简单的方式替换Comparator
中的最后一个return
语句:
return e1.number() - e2.number();
不要这样做,除非你绝对确定没有人会有负的员工编号!这个技巧通常不起作用,因为有符号整数类型不足以表示任意两个有符号整数的差值。如果i
是一个很大的正整数,而j
是一个很大的负整数,i - j
会溢出并返回一个负整数。所得到的comparator
违反了我们一直谈论的四个技术限制之一(传递性),并产生可怕的、微妙的错误。这不仅仅是理论上的担忧;人们会因此受到伤害。
SortedSet 接口
原文:
docs.oracle.com/javase/tutorial/collections/interfaces/sorted-set.html
SortedSet
是一个按升序维护其元素的集合,根据元素的自然顺序或在 SortedSet
创建时提供的 Comparator
进行排序。除了正常的 Set
操作外,SortedSet
接口还提供以下操作:
-
Range view
允许对排序集合进行任意范围操作 -
Endpoints
返回排序集合中的第一个或最后一个元素 -
Comparator access
返回用于对集合进行排序的Comparator
(如果有的话)
SortedSet
接口的代码如下。
public interface SortedSet<E> extends Set<E> {
// Range-view
SortedSet<E> subSet(E fromElement, E toElement);
SortedSet<E> headSet(E toElement);
SortedSet<E> tailSet(E fromElement);
// Endpoints
E first();
E last();
// Comparator access
Comparator<? super E> comparator();
}
集合操作
SortedSet
从 Set
继承的操作在排序集合和普通集合上的行为完全相同,但有两个例外:
-
iterator
操作返回的Iterator
按顺序遍历排序集合。 -
toArray
返回的数组按顺序包含了排序后的集合元素。
尽管接口不保证,但 Java 平台的 SortedSet
实现的 toString
方法返回一个包含排序集合所有元素的字符串,按顺序排列。
标准构造函数
按照惯例,所有通用的 Collection
实现都提供一个标准的转换构造函数,接受一个 Collection
;SortedSet
实现也不例外。在 TreeSet
中,这个构造函数创建一个根据元素的自然顺序排序的实例。这可能是一个错误。最好动态检查指定的集合是否是 SortedSet
实例,如果是,则根据相同的标准(比较器或自然顺序)对新的 TreeSet
进行排序。因为 TreeSet
采取了它的方法,它还提供一个接受 SortedSet
的构造函数,并返回一个包含相同元素并根据相同标准排序的新 TreeSet
。请注意,参数的编译时类型,而不是运行时类型,决定调用这两个构造函数中的哪一个(以及是否保留排序标准)。
SortedSet
实现通常还提供一个构造函数,接受一个 Comparator
并返回一个根据指定 Comparator
排序的空集合。如果传递 null
给这个构造函数,它将返回一个根据元素的自然顺序排序的集合。
范围视图操作
range-view
操作在某种程度上类似于List
接口提供的操作,但有一个重大区别。排序集的范围视图即使在直接修改支持的排序集的情况下仍然有效。这是因为排序集的范围视图的端点是元素空间中的绝对点,而不是备份集合中的特定元素,这对于列表是成立的。排序集的range-view
实际上只是窗口,显示在元素空间的指定部分中集合的任何部分。对排序集的range-view
的更改会写回到支持的排序集中,反之亦然。因此,长时间使用排序集上的range-view
是可以的,不像列表上的range-view
那样。
排序集提供三种range-view
操作。第一种是subSet
,类似于subList
,需要两个端点。端点不是索引,而是对象,并且必须与排序集中的元素可比,使用Set
的Comparator
或其元素的自然排序,取决于Set
用于对自身排序的方式。与subList
类似,范围是半开的,包括其低端点但不包括高端点。
因此,下面这行代码告诉你在名为dictionary
的字符串SortedSet
中包含多少个介于"doorbell"
和"pickle"
之间的单词,包括"doorbell"
但不包括"pickle"
。
int count = dictionary.subSet("doorbell", "pickle").size();
以类似的方式,以下一行代码可以删除所有以字母f
开头的元素。
dictionary.subSet("f", "g").clear();
类似的技巧可以用来打印一个表格,告诉你每个字母开头的单词有多少个。
for (char ch = 'a'; ch <= 'z'; ) {
String from = String.valueOf(ch++);
String to = String.valueOf(ch);
System.out.println(from + ": " + dictionary.subSet(from, to).size());
}
假设你想查看一个闭区间,其中包含两个端点,而不是一个开区间。如果元素类型允许计算元素空间中给定值的后继,只需请求从lowEndpoint
到successor(highEndpoint)
的subSet
。虽然这并不是完全明显的,但在String
的自然排序中,字符串s
的后继是s + "\0"
,也就是在s
后附加一个null
字符。
因此,以下一行代码告诉你在字典中包含多少个介于"doorbell"
和"pickle"
之间的单词,包括doorbell
和 pickle
。
count = dictionary.subSet("doorbell", "pickle\0").size();
类似的技巧可以用来查看一个开区间,其中不包含任何端点。从lowEndpoint
到highEndpoint
的开区间视图是从successor(lowEndpoint)
到highEndpoint
的半开区间。使用以下代码计算介于"doorbell"
和"pickle"
之间的单词数量,不包括两者。
count = dictionary.subSet("doorbell\0", "pickle").size();
SortedSet
接口包含两个更多的 range-view
操作 headSet
和 tailSet
,两者都接受一个 Object
参数。前者返回一个视图,显示了支持 SortedSet
的初始部分,直到但不包括指定的对象。后者返回一个视图,显示了支持 SortedSet
的最终部分,从指定的对象开始,一直到支持 SortedSet
的末尾。因此,以下代码允许您将字典视为两个不相交的 卷
(a-m
和 n-z
)。
SortedSet<String> volume1 = dictionary.headSet("n");
SortedSet<String> volume2 = dictionary.tailSet("n");
端点操作
SortedSet
接口包含返回排序集合中第一个和最后一个元素的操作,分别称为 first
和 last
。除了它们的明显用途外,last
还允许解决 SortedSet
接口中的一个缺陷。您希望对 SortedSet
进行的一件事是进入 Set
的内部并向前或向后迭代。从内部向前迭代很容易:只需获取一个 tailSet
并对其进行迭代。不幸的是,向后迭代没有简单的方法。
以下习语获取了元素空间中小于指定对象 o
的第一个元素。
Object predecessor = ss.headSet(o).last();
这是从排序集合内部的某一点向后移动一个元素的好方法。可以重复应用它来向后迭代,但这非常低效,每返回一个元素都需要查找。
比较器访问器
SortedSet
接口包含一个名为 comparator
的访问器方法,返回用于对集合进行排序的 Comparator
,如果集合根据其元素的 自然顺序 进行排序,则返回 null
。提供此方法是为了可以将排序集合复制到具有相同顺序的新排序集合中。它被描述为 SortedSet
构造函数使用的 先前。
排序地图接口
原文:
docs.oracle.com/javase/tutorial/collections/interfaces/sorted-map.html
一个SortedMap
是一个维护其条目按升序排列的Map
,根据键的自然顺序排序,或者根据在创建SortedMap
时提供的Comparator
排序。自然排序和Comparator
在对象排序部分讨论。SortedMap
接口提供了常规Map
操作以及以下操作:
-
Range view
在排序地图上执行任意范围操作 -
Endpoints
返回排序地图中的第一个或最后一个键 -
Comparator access
返回用于对地图进行排序的Comparator
(如果有的话)。
以下接口是SortedSet
的Map
模拟。
public interface SortedMap<K, V> extends Map<K, V>{
Comparator<? super K> comparator();
SortedMap<K, V> subMap(K fromKey, K toKey);
SortedMap<K, V> headMap(K toKey);
SortedMap<K, V> tailMap(K fromKey);
K firstKey();
K lastKey();
}
地图操作
SortedMap
从Map
继承的操作在排序地图和普通地图上的行为相同,有两个例外:
-
任何排序地图的
Collection
视图上的iterator
操作返回的Iterator
按顺序遍历集合。 -
Collection
视图的toArray
操作返回的数组按顺序包含键、值或条目。
虽然接口不能保证,但 Java 平台所有SortedMap
实现中Collection
视图的toString
方法返回一个字符串,其中包含视图中的所有元素,按顺序排列。
标准构造函数
按照惯例,所有通用Map
实现都提供一个标准转换构造函数,接受一个Map
;SortedMap
实现也不例外。在TreeMap
中,这个构造函数创建一个根据其键的自然顺序排序其条目的实例。这可能是一个错误。最好动态检查指定的Map
实例是否是SortedMap
,如果是,则根据相同的标准(比较器或自然顺序)对新地图进行排序。因为TreeMap
采取了它的方法,它还提供了一个接受SortedMap
的构造函数,并返回一个包含与给定SortedMap
相同映射的新TreeMap
,根据相同标准排序。请注意,参数的编译时类型,而不是运行时类型,决定了是否优先调用SortedMap
构造函数而不是普通的map
构造函数。
按照惯例,SortedMap
实现还提供一个接受Comparator
的构造函数,并返回根据指定Comparator
排序的空地图。如果将null
传递给此构造函数,则返回一个根据其键的自然顺序对其映射进行排序的Map
。
与 SortedSet 的比较
因为这个接口是SortedSet
的精确Map
模拟,所以在 SortedSet 接口部分中的所有习语和代码示例都适用于SortedMap
,只需进行微不足道的修改。
接口总结
原文:
docs.oracle.com/javase/tutorial/collections/interfaces/summary.html
核心集合接口是 Java 集合框架的基础。
Java 集合框架层次结构由两个不同的接口树组成:
-
第一个树以
Collection
接口开始,该接口提供了所有集合使用的基本功能,如add
和remove
方法。它的子接口——Set
、List
和Queue
——提供了更专门化的集合。 -
Set
接口不允许重复元素。这对于存储诸如一副牌或学生记录之类的集合非常有用。Set
接口有一个子接口,SortedSet
,用于对集合中的元素进行排序。 -
List
接口提供了一个有序的集合,用于需要精确控制每个元素插入位置的情况。您可以通过它们的确切位置从List
中检索元素。 -
Queue
接口允许额外的插入、提取和检查操作。Queue
中的元素通常按照 FIFO 顺序排序。 -
Deque
接口允许在两端进行插入、删除和检查操作。Deque
中的元素可以同时用于 LIFO 和 FIFO。 -
第二个树以
Map
接口开始,类似于Hashtable
将键和值进行映射。 -
Map
的子接口SortedMap
将其键值对按升序或按Comparator
指定的顺序维护。
这些接口允许集合在不考虑其表示细节的情况下进行操作。
问题和练习:接口
原文:
docs.oracle.com/javase/tutorial/collections/interfaces/QandE/questions.html
问题
-
在本课程的开始,您了解到核心集合接口被组织成两个不同的继承树。特别是一个接口被认为不是真正的
Collection
,因此位于自己的树的顶部。这个接口的名称是什么? -
集合框架中的每个接口都使用
<E>
语法声明,告诉您它是泛型的。当您声明一个Collection
实例时,指定它将包含的对象类型有什么优势? -
什么接口代表不允许重复元素的集合?
-
什么接口形成了集合层次结构的根?
-
什么接口代表可能包含重复元素的有序集合?
-
什么接口代表在处理之前保存元素的集合?
-
什么接口代表将键映射到值的类型?
-
什么接口代表双端队列?
-
列出遍历
List
元素的三种不同方法。 -
真或假:聚合操作是修改基础集合的变异操作。
练习
-
编写一个以随机顺序打印其参数的程序。不要复制参数数组。演示如何使用流和传统的增强 for 语句打印元素。
-
取
FindDups
示例,并修改它以使用SortedSet
而不是Set
。指定一个Comparator
,以便在排序和识别集合元素时忽略大小写。 -
编写一个方法,接受一个
List<String>
并对每个元素应用String.trim
。 -
考虑四个核心接口,
Set
、List
、Queue
和Map
。对于以下四个任务中的每一个,指定哪个核心接口最适合,并解释如何使用它来实现任务。-
Whimsical Toys Inc(WTI)需要记录所有员工的姓名。每个月,将从这些记录中随机选择一个员工以获得免费玩具。
-
WTI 已决定每个新产品都将以员工的名字命名,但只使用名字的第一个字母,并且每个名字只能使用一次。准备一个独特的名字列表。
-
WTI 决定只想使用最受欢迎的名字来命名其玩具。统计每个名字的员工数量。
-
WTI 为当地的长曲棍球队购买季票,将由员工共享。为这项受欢迎的运动创建一个等待名单。
-
检查你的答案。
课程:聚合操作
原文:
docs.oracle.com/javase/tutorial/collections/streams/index.html
注意:要更好地理解本节中的概念,请查看 Lambda 表达式和方法引用部分。
您使用集合做什么?您不仅仅将对象存储在集合中并将其留在那里。在大多数情况下,您使用集合来检索其中存储的项目。
再次考虑 Lambda 表达式部分中描述的场景。假设您正在创建一个社交网络应用。您希望创建一个功能,使管理员能够对满足某些条件的社交网络应用成员执行任何操作,例如发送消息。
与之前一样,假设这个社交网络应用的成员由以下Person
类表示:
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
// ...
public int getAge() {
// ...
}
public String getName() {
// ...
}
}
以下示例使用 for-each 循环打印集合roster
中包含的所有成员的名称:
for (Person p : roster) {
System.out.println(p.getName());
}
以下示例使用聚合操作forEach
打印出集合roster
中包含的所有成员:
roster
.stream()
.forEach(e -> System.out.println(e.getName());
尽管在此示例中,使用聚合操作的版本比使用 for-each 循环的版本更长,但您将看到,对于更复杂的任务,使用批量数据操作的版本将更简洁。
下面涵盖了以下主题:
-
管道和流
-
聚合操作和迭代器之间的区别
在示例BulkDataOperationsExamples
中找到本节描述的代码片段。
管道和流
管道是一系列聚合操作。以下示例使用由聚合操作filter
和forEach
组成的管道打印集合roster
中包含的男性成员:
roster
.stream()
.filter(e -> e.getGender() == Person.Sex.MALE)
.forEach(e -> System.out.println(e.getName()));
将此示例与使用 for-each 循环打印集合roster
中的男性成员的示例进行比较:
for (Person p : roster) {
if (p.getGender() == Person.Sex.MALE) {
System.out.println(p.getName());
}
}
一个管道包含以下组件:
-
源:这可以是集合、数组、生成器函数或 I/O 通道。在此示例中,源是集合
roster
。 -
零个或多个中间操作。中间操作,如
filter
,会生成一个新流。流是元素的序列。与集合不同,它不是存储元素的数据结构。相反,流通过管道从源头传递值。此示例通过调用
stream
方法从集合roster
创建流。filter
操作返回一个包含与其谓词(该操作的参数)匹配的元素的新流。在这个示例中,谓词是 lambda 表达式e -> e.getGender() == Person.Sex.MALE
。如果对象e
的gender
字段的值为Person.Sex.MALE
,则返回布尔值true
。因此,在这个示例中,filter
操作返回一个包含集合roster
中所有男性成员的流。 -
终端操作。终端操作,比如
forEach
,产生一个非流结果,比如一个原始值(比如一个双精度值)、一个集合,或者在forEach
的情况下,根本没有值。在这个示例中,forEach
操作的参数是 lambda 表达式e -> System.out.println(e.getName())
,它在对象e
上调用getName
方法。(Java 运行时和编译器推断出对象e
的类型是Person
。)
以下示例计算了集合roster
中所有男性成员的平均年龄,使用了由filter
、mapToInt
和average
聚合操作组成的流水线:
double average = roster
.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.mapToInt(Person::getAge)
.average()
.getAsDouble();
mapToInt
操作返回一个新的IntStream
类型的流(这是一个只包含整数值的流)。该操作将其参数中指定的函数应用于特定流中的每个元素。在这个示例中,函数是Person::getAge
,这是一个返回成员年龄的方法引用。(或者,你可以使用 lambda 表达式e -> e.getAge()
。)因此,在这个示例中,mapToInt
操作返回一个包含集合roster
中所有男性成员年龄的流。
average
操作计算IntStream
类型流中包含的元素的平均值。它返回一个OptionalDouble
类型的对象。如果流不包含任何元素,则average
操作返回一个空的OptionalDouble
实例,并调用getAsDouble
方法会抛出NoSuchElementException
。JDK 包含许多像average
这样返回通过组合流内容得到的一个值的终端操作。这些操作被称为归约操作;更多信息请参见归约部分。
聚合操作和迭代器之间的区别
聚合操作,如forEach
,看起来像迭代器。然而,它们有几个根本的区别:
-
它们使用内部迭代:聚合操作不包含像
next
这样的方法来指示它们处理集合的下一个元素。通过内部委托,您的应用程序确定要迭代的集合,但 JDK 确定如何迭代集合。通过外部迭代,您的应用程序确定要迭代的集合以及如何迭代它。然而,外部迭代只能按顺序迭代集合的元素。内部迭代没有这种限制。它可以更容易地利用并行计算,这涉及将问题分解为子问题,同时解决这些问题,然后将子问题的解决方案合并为结果。有关更多信息,请参见并行性部分。 -
它们从流中处理元素:聚合操作从流中处理元素,而不是直接从集合中处理。因此,它们也被称为流操作。
-
它们支持行为作为参数:您可以为大多数聚合操作指定 lambda 表达式作为参数。这使您可以自定义特定聚合操作的行为。
缩减
原文:
docs.oracle.com/javase/tutorial/collections/streams/reduction.html
章节聚合操作描述了以下操作流水线,它计算roster
集合中所有男性成员的平均年龄:
double average = roster
.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.mapToInt(Person::getAge)
.average()
.getAsDouble();
JDK 包含许多终端操作(如average
、sum
、min
、max
和count
),它们通过组合流的内容返回一个值。这些操作称为缩减操作。JDK 还包含返回集合而不是单个值的缩减操作。许多缩减操作执行特定任务,比如找到值的平均值或将元素分组到类别中。然而,JDK 为您提供了通用的缩减操作reduce
和collect
,本节将详细描述这些操作。
本节涵盖以下主题:
-
Stream.reduce 方法
-
Stream.collect 方法
你可以在示例ReductionExamples
中找到本节中描述的代码片段。
Stream.reduce
方法
Stream.reduce
方法是一个通用的缩减操作。考虑以下流水线,它计算roster
集合中男性成员年龄的总和。它使用Stream.sum
缩减操作:
Integer totalAge = roster
.stream()
.mapToInt(Person::getAge)
.sum();
将此与以下使用Stream.reduce
操作计算相同值的流水线进行比较:
Integer totalAgeReduce = roster
.stream()
.map(Person::getAge)
.reduce(
0,
(a, b) -> a + b);
在这个例子中,reduce
操作接受两个参数:
-
identity
:身份元素既是缩减的初始值,也是如果流中没有元素时的默认结果。在这个例子中,身份元素是0
;这是年龄总和的初始值,也是如果roster
集合中不存在成员时的默认值。 -
accumulator
: 累加器函数接受两个参数:减少的部分结果(在这个例子中,到目前为止所有处理过的整数的总和)和流的下一个元素(在这个例子中,一个整数)。它返回一个新的部分结果。在这个例子中,累加器函数是一个 lambda 表达式,它将两个Integer
值相加并返回一个Integer
值:(a, b) -> a + b
reduce
操作总是返回一个新值。然而,累加器函数在处理流的每个元素时也返回一个新值。假设你想将流的元素减少到一个更复杂的对象,比如一个集合。这可能会影响你的应用程序的性能。如果你的reduce
操作涉及将元素添加到一个集合中,那么每次累加器函数处理一个元素时,它都会创建一个包含该元素的新集合,这是低效的。更新现有集合会更有效。你可以使用Stream.collect
方法来实现这一点,下一节将介绍。
collect
方法
与reduce
方法不同,它在处理元素时总是创建一个新值,collect
方法修改或改变了现有值。
考虑如何在流中找到值的平均值。你需要两个数据:值的总数和这些值的总和。然而,像reduce
方法和所有其他减少方法一样,collect
方法只返回一个值。你可以创建一个包含成员变量来跟踪值的总数和这些值总和的新数据类型,比如下面的类Averager
:
class Averager implements IntConsumer
{
private int total = 0;
private int count = 0;
public double average() {
return count > 0 ? ((double) total)/count : 0;
}
public void accept(int i) { total += i; count++; }
public void combine(Averager other) {
total += other.total;
count += other.count;
}
}
以下管道使用Averager
类和collect
方法来计算所有男性成员的平均年龄:
Averager averageCollect = roster.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.map(Person::getAge)
.collect(Averager::new, Averager::accept, Averager::combine);
System.out.println("Average age of male members: " +
averageCollect.average());
这个例子中的collect
操作接受三个参数:
-
supplier
: 供应商是一个工厂函数;它构造新实例。对于collect
操作,它创建结果容器的实例。在这个例子中,它是Averager
类的一个新实例。 -
accumulator
: 累加器函数将流元素合并到结果容器中。在这个例子中,它通过将count
变量加一并将流元素的值(代表男性成员年龄的整数)加到total
成员变量中,修改了Averager
结果容器。 -
combiner
:合并器函数接受两个结果容器并合并它们的内容。在这个例子中,它通过增加另一个Averager
实例的count
成员变量的值到Averager
结果容器的count
变量,并将另一个Averager
实例的total
成员变量的值加到total
成员变量中来修改Averager
结果容器。
请注意以下内容:
-
供应商是一个 lambda 表达式(或方法引用),与
reduce
操作中的单位元素等值相对。 -
累加器和合并器函数不返回值。
-
您可以在并行流中使用
collect
操作;有关更多信息,请参阅并行性部分。(如果您在并行流中运行collect
方法,那么当合并器函数创建一个新对象时,例如在这个例子中创建一个Averager
对象时,JDK 会创建一个新线程。因此,您不必担心同步。)
尽管 JDK 为您提供了average
操作来计算流中元素的平均值,但如果您需要从流的元素计算多个值,可以使用collect
操作和自定义类。
collect
操作最适合集合。以下示例使用collect
操作将男性成员的姓名放入集合中:
List<String> namesOfMaleMembersCollect = roster
.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.map(p -> p.getName())
.collect(Collectors.toList());
此版本的collect
操作接受一个类型为Collector
的参数。这个类封装了在collect
操作中用作参数的函数,该操作需要三个参数(供应商、累加器和合并器函数)。
Collectors
类包含许多有用的归约操作,例如将元素累积到集合中并根据各种标准对元素进行总结。这些归约操作返回Collector
类的实例,因此您可以将它们作为collect
操作的参数使用。
此示例使用Collectors.toList
操作,将流元素累积到一个新的List
实例中。与Collectors
类中的大多数操作一样,toList
操作符返回一个Collector
实例,而不是一个集合。
以下示例按性别对集合roster
的成员进行分组:
Map<Person.Sex, List<Person>> byGender =
roster
.stream()
.collect(
Collectors.groupingBy(Person::getGender));
groupingBy
操作返回一个映射,其键是应用作为其参数指定的 lambda 表达式(称为分类函数)的结果值。在这个例子中,返回的映射包含两个键,Person.Sex.MALE
和 Person.Sex.FEMALE
。键对应的值是包含通过分类函数处理时对应于键值的流元素的 List
实例。例如,对应于键 Person.Sex.MALE
的值是一个包含所有男性成员的 List
实例。
以下示例按性别检索集合 roster
中每个成员的名称并按性别分组:
Map<Person.Sex, List<String>> namesByGender =
roster
.stream()
.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.mapping(
Person::getName,
Collectors.toList())));
在这个例子中,groupingBy
操作需要两个参数,一个分类函数和一个 Collector
实例。Collector
参数称为下游收集器。这是 Java 运行时应用于另一个收集器结果的收集器。因此,这个 groupingBy
操作使您能够对 groupingBy
操作符创建的 List
值应用 collect
方法。这个例子应用了收集器 mapping
,它将映射函数 Person::getName
应用于流的每个元素。因此,结果流只包含成员的名称。包含一个或多个下游收集器的管道,像这个例子一样,称为多级减少。
以下示例检索每个性别成员的总年龄:
Map<Person.Sex, Integer> totalAgeByGender =
roster
.stream()
.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.reducing(
0,
Person::getAge,
Integer::sum)));
reducing
操作需要三个参数:
-
identity
:类似于Stream.reduce
操作,身份元素既是减少的初始值,也是如果流中没有元素时的默认结果。在这个例子中,身份元素是0
;这是年龄总和的初始值,如果没有成员存在,则是默认值。 -
mapper
:reducing
操作将此映射函数应用于所有流元素。在这个例子中,映射器检索每个成员的年龄。 -
operation
:操作函数用于减少映射值。在这个例子中,操作函数添加Integer
值。
以下示例检索每个性别成员的平均年龄:
Map<Person.Sex, Double> averageAgeByGender = roster
.stream()
.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.averagingInt(Person::getAge)));
并行性
原文:
docs.oracle.com/javase/tutorial/collections/streams/parallelism.html
并行计算涉及将问题分解为子问题,同时解决这些问题(并行执行,每个子问题在单独的线程中运行),然后将这些子问题的解决方案合并。Java SE 提供了分支/合并框架,它使您能够更轻松地在应用程序中实现并行计算。然而,使用此框架时,您必须指定如何将问题细分(分区)。使用聚合操作,Java 运行时为您执行此分区和解决方案的合并。
在使用集合的应用程序中实现并行性的一个困难在于集合不是线程安全的,这意味着多个线程不能在不引入线程干扰或内存一致性错误的情况下操作集合。集合框架提供了同步包装器,它可以为任意集合添加自动同步,使其线程安全。然而,同步会引入线程争用。你应该避免线程争用,因为它会阻止线程并行运行。聚合操作和并行流使你能够在非线程安全的集合上实现并行性,前提是在操作集合时不修改它。
请注意,并行性并不自动比串行执行操作更快,尽管在有足够数据和处理器核心的情况下可能会更快。虽然聚合操作使您更容易实现并行性,但确定您的应用程序是否适合并行性仍然是您的责任。
本节涵盖以下主题:
-
并行执行流
-
并发归约
-
顺序
-
副作用
-
懒惰性
-
干扰
-
有状态的 Lambda 表达式
-
你可以在示例ParallelismExamples
中找到本节中描述的代码摘录。
并行执行流
你可以串行或并行执行流。当流并行执行时,Java 运行时将流分成多个子流。聚合操作并行迭代和处理这些子流,然后将结果合并。
当您创建一个流时,除非另有说明,它总是一个串行流。要创建一个并行流,请调用操作Collection.parallelStream
。或者,调用操作BaseStream.parallel
。例如,以下语句计算所有男性成员的平均年龄:
double average = roster
.parallelStream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.mapToInt(Person::getAge)
.average()
.getAsDouble();
并发减少
再次考虑以下示例(在减少部分中描述),该示例按性别对成员进行分组。此示例调用collect
操作,将集合roster
减少为Map
:
Map<Person.Sex, List<Person>> byGender =
roster
.stream()
.collect(
Collectors.groupingBy(Person::getGender));
以下是并行等价的:
ConcurrentMap<Person.Sex, List<Person>> byGender =
roster
.parallelStream()
.collect(
Collectors.groupingByConcurrent(Person::getGender));
这被称为并发减少。如果包含collect
操作的特定管道满足以下所有条件,则 Java 运行时执行并发减少:
-
流是并行的。
-
collect
操作的参数,收集器,具有特征Collector.Characteristics.CONCURRENT
。要确定收集器的特征,请调用Collector.characteristics
方法。 -
流要么是无序的,要么收集器具有特征
Collector.Characteristics.UNORDERED
。要确保流是无序的,请调用BaseStream.unordered
操作。
注意:此示例返回一个ConcurrentMap
的实例,而不是Map
,并调用groupingByConcurrent
操作,而不是groupingBy
。(有关ConcurrentMap
的更多信息,请参见并发集合部分。)与操作groupingByConcurrent
不同,操作groupingBy
在并行流中表现不佳。(这是因为它通过键合并两个映射,这在计算上是昂贵的。)同样,操作Collectors.toConcurrentMap
在并行流中的性能优于操作Collectors.toMap
。
排序
流水线处理流的元素的顺序取决于流是串行执行还是并行执行、流的来源以及中间操作。例如,考虑以下示例,该示例多次使用forEach
操作打印ArrayList
实例的元素:
Integer[] intArray = {1, 2, 3, 4, 5, 6, 7, 8 };
List<Integer> listOfIntegers =
new ArrayList<>(Arrays.asList(intArray));
System.out.println("listOfIntegers:");
listOfIntegers
.stream()
.forEach(e -> System.out.print(e + " "));
System.out.println("");
System.out.println("listOfIntegers sorted in reverse order:");
Comparator<Integer> normal = Integer::compare;
Comparator<Integer> reversed = normal.reversed();
Collections.sort(listOfIntegers, reversed);
listOfIntegers
.stream()
.forEach(e -> System.out.print(e + " "));
System.out.println("");
System.out.println("Parallel stream");
listOfIntegers
.parallelStream()
.forEach(e -> System.out.print(e + " "));
System.out.println("");
System.out.println("Another parallel stream:");
listOfIntegers
.parallelStream()
.forEach(e -> System.out.print(e + " "));
System.out.println("");
System.out.println("With forEachOrdered:");
listOfIntegers
.parallelStream()
.forEachOrdered(e -> System.out.print(e + " "));
System.out.println("");
该示例包含五个流水线。它打印类似以下的输出:
listOfIntegers:
1 2 3 4 5 6 7 8
listOfIntegers sorted in reverse order:
8 7 6 5 4 3 2 1
Parallel stream:
3 4 1 6 2 5 7 8
Another parallel stream:
6 3 1 5 7 8 4 2
With forEachOrdered:
8 7 6 5 4 3 2 1
该示例执行以下操作:
-
第一个流水线按照它们添加到列表中的顺序打印
listOfIntegers
列表的元素。 -
第二个流水线在使用
Collections.sort
方法对listOfIntegers
进行排序后打印元素。 -
第三和第四个流水线以一种看似随机的顺序打印列表的元素。请记住,流操作在处理流的元素时使用内部迭代。因此,当您并行执行流时,除非流操作另有规定,否则 Java 编译器和运行时会确定处理流元素的顺序,以最大化并行计算的好处。
-
第五个流水线使用
forEachOrdered
方法,该方法按照其来源指定的顺序处理流的元素,无论您是串行还是并行执行流。请注意,如果您在并行流中使用类似forEachOrdered
的操作,可能会丧失并行性的好处。
副作用
如果一个方法或表达式除了返回或产生一个值外,还修改了计算机的状态,那么它就具有副作用。例如,可变归约(使用collect
操作的操作;有关更多信息,请参见 Reduction 部分)以及调用System.out.println
方法进行调试。JDK 在流水线中处理某些副作用很好。特别是,collect
方法被设计为以并行安全的方式执行具有副作用的最常见流操作。forEach
和peek
等操作是为了副作用而设计的;一个返回 void 的 Lambda 表达式,比如调用System.out.println
的 Lambda 表达式,除了具有副作用外什么也做不了。即便如此,你应该谨慎使用forEach
和peek
操作;如果你在并行流中使用其中一个操作,那么 Java 运行时可能会从多个线程同时调用你指定为参数的 Lambda 表达式。此外,永远不要在filter
和map
等操作中传递具有副作用的 Lambda 表达式作为参数。接下来的章节讨论干扰和有状态的 Lambda 表达式,它们都可能是副作用的来源,并且可能返回不一致或不可预测的结果,特别是在并行流中。然而,首先讨论懒惰的概念,因为它对干扰有直接影响。
懒惰
所有中间操作都是懒惰的。如果一个表达式、方法或算法只有在需要时才会被评估,那么它就是懒惰的(如果算法立即被评估或处理,则是急切的)。中间操作是懒惰的,因为它们直到终端操作开始才开始处理流的内容。懒惰地处理流使得 Java 编译器和运行时能够优化它们处理流的方式。例如,在像filter
-mapToInt
-average
这样的流水线中,average
操作可以从mapToInt
操作创建的流中获取前几个整数,而这些整数是从filter
操作获取的。average
操作会重复这个过程,直到它从流中获取了所有需要的元素,然后计算平均值。
干扰
流操作中的 Lambda 表达式不应该干扰。当流的源在流水线处理流时被修改时就会发生干扰。例如,下面的代码尝试连接List
listOfStrings
中包含的字符串。然而,它会抛出ConcurrentModificationException
:
try {
List<String> listOfStrings =
new ArrayList<>(Arrays.asList("one", "two"));
// This will fail as the peek operation will attempt to add the
// string "three" to the source after the terminal operation has
// commenced.
String concatenatedString = listOfStrings
.stream()
// Don't do this! Interference occurs here.
.peek(s -> listOfStrings.add("three"))
.reduce((a, b) -> a + " " + b)
.get();
System.out.println("Concatenated string: " + concatenatedString);
} catch (Exception e) {
System.out.println("Exception caught: " + e.toString());
}
此示例使用reduce
操作将listOfStrings
中包含的字符串连接成一个Optional<String>
值,reduce
是一个终端操作。然而,此处的管道调用了中间操作peek
,它试图向listOfStrings
添加一个新元素。请记住,所有中间操作都是惰性的。这意味着在此示例中,管道在调用操作get
时开始执行,并在get
操作完成时结束执行。peek
操作的参数在管道执行过程中尝试修改流源,这会导致 Java 运行时抛出ConcurrentModificationException
。
有状态的 Lambda 表达式
避免在流操作中将有状态的 lambda 表达式用作参数。有状态的 lambda 表达式是指其结果取决于在管道执行过程中可能发生变化的任何状态。以下示例使用map
中间操作将List
listOfIntegers
中的元素添加到新的List
实例中。它分别使用串行流和并行流执行两次:
List<Integer> serialStorage = new ArrayList<>();
System.out.println("Serial stream:");
listOfIntegers
.stream()
// Don't do this! It uses a stateful lambda expression.
.map(e -> { serialStorage.add(e); return e; })
.forEachOrdered(e -> System.out.print(e + " "));
System.out.println("");
serialStorage
.stream()
.forEachOrdered(e -> System.out.print(e + " "));
System.out.println("");
System.out.println("Parallel stream:");
List<Integer> parallelStorage = Collections.synchronizedList(
new ArrayList<>());
listOfIntegers
.parallelStream()
// Don't do this! It uses a stateful lambda expression.
.map(e -> { parallelStorage.add(e); return e; })
.forEachOrdered(e -> System.out.print(e + " "));
System.out.println("");
parallelStorage
.stream()
.forEachOrdered(e -> System.out.print(e + " "));
System.out.println("");
Lambda 表达式e -> { parallelStorage.add(e); return e; }
是一个有状态的 lambda 表达式。其结果可能每次运行代码时都会有所不同。此示例打印如下内容:
Serial stream:
8 7 6 5 4 3 2 1
8 7 6 5 4 3 2 1
Parallel stream:
8 7 6 5 4 3 2 1
1 3 6 2 4 5 8 7
操作forEachOrdered
按照流指定的顺序处理元素,无论流是串行还是并行执行。然而,当流并行执行时,map
操作处理由 Java 运行时和编译器指定的流元素。因此,lambda 表达式e -> { parallelStorage.add(e); return e; }
向List
parallelStorage
添加元素的顺序可能每次运行代码时都会有所不同。为了获得确定性和可预测的结果,请确保流操作中的 lambda 表达式参数不是有状态的。
注意:此示例调用了方法synchronizedList
,以使List
parallelStorage
线程安全。请记住,集合不是线程安全的。这意味着多个线程不应同时访问特定集合。假设在创建parallelStorage
时未调用方法synchronizedList
:
List<Integer> parallelStorage = new ArrayList<>();
该示例行为不稳定,因为多个线程访问和修改parallelStorage
而没有像同步这样的机制来安排特定线程何时可以访问List
实例。因此,该示例可能打印类似以下内容的输出:
Parallel stream:
8 7 6 5 4 3 2 1
null 3 5 4 7 8 1 2
问题和练习:聚合操作
原文:
docs.oracle.com/javase/tutorial/collections/streams/QandE/questions.html
问题
-
一系列的聚合操作被称为 ___。
-
每个流水线包含零个或多个 ___ 操作。
-
每个流水线以一个 ___ 操作结束。
-
什么样的操作以另一个流作为输出?
-
描述
forEach
聚合操作与增强的for
语句或迭代器之间的一个区别。 -
真或假:流类似于集合,因为它是一个存储元素的数据结构。
-
在这段代码中识别中间操作和终端操作:
double average = roster .stream() .filter(p -> p.getGender() == Person.Sex.MALE) .mapToInt(Person::getAge) .average() .getAsDouble();
-
代码
p -> p.getGender() == Person.Sex.MALE
是什么的一个例子? -
代码
Person::getAge
是什么的一个例子? -
将流的内容组合并返回一个值的终端操作被称为什么?
-
Stream.reduce
方法和Stream.collect
方法之间的一个重要区别是什么? -
如果你想处理一个包含姓名的流,提取男性姓名,并将它们存储在一个新的
List
中,那么Stream.reduce
或Stream.collect
是最合适的操作? -
真或假:聚合操作使得可以在非线程安全的集合中实现并行性。
-
流通常是串行的,除非另有规定。如何请求以并行方式处理流?
练习
-
将以下增强的
for
语句编写为使用 lambda 表达式的流水线。提示:使用filter
中间操作和forEach
终端操作。for (Person p : roster) { if (p.getGender() == Person.Sex.MALE) { System.out.println(p.getName()); } }
-
将以下代码转换为一个使用 lambda 表达式和聚合操作而不是嵌套
for
循环的新实现。提示:创建一个依次调用filter
、sorted
和collect
操作的流水线。List<Album> favs = new ArrayList<>(); for (Album a : albums) { boolean hasFavorite = false; for (Track t : a.tracks) { if (t.rating >= 4) { hasFavorite = true; break; } } if (hasFavorite) favs.add(a); } Collections.sort(favs, new Comparator<Album>() { public int compare(Album a1, Album a2) { return a1.name.compareTo(a2.name); }});
检查你的答案。
教训:实现
原文:
docs.oracle.com/javase/tutorial/collections/implementations/index.html
实现是用于存储集合的数据对象,实现了接口部分中描述的接口。本课程描述了以下类型的实现:
-
通用实现是最常用的实现,设计用于日常使用。它们在标题为通用实现的表中总结。
-
特殊用途实现设计用于特殊情况,并显示非标准性能特征、使用限制或行为。
-
并发实现旨在支持高并发性,通常以牺牲单线程性能为代价。这些实现是
java.util.concurrent
包的一部分。 -
包装实现通常与其他类型的实现结合使用,通常是通用实现,以提供附加或受限功能。
-
便利实现是迷你实现,通常通过静态工厂方法提供,为特殊集合提供方便、高效的替代方案(例如,单例集合)。
-
抽象实现是骨架实现,有助于构建自定义实现,稍后在自定义集合实现部分中描述。这是一个高级主题,不是特别困难,但相对较少的人会需要这样做。
通用实现总结如下表所示。
通用实现
接口 | 哈希表实现 | 可调整大小数组实现 | 树实现 | 链表实现 | 哈希表 + 链表实现 |
---|---|---|---|---|---|
Set |
HashSet |
TreeSet |
LinkedHashSet |
||
List |
ArrayList |
LinkedList |
|||
Queue |
|||||
Deque |
ArrayDeque |
LinkedList |
|||
Map |
HashMap |
TreeMap |
LinkedHashMap |
正如您从表中所看到的,Java 集合框架提供了几个通用实现的Set
、List
和Map
接口。在每种情况下,一个实现——HashSet
、ArrayList
和HashMap
——显然是大多数应用程序中要使用的实现,其他条件相等。请注意,SortedSet
和SortedMap
接口在表中没有行。每个接口都有一个实现(TreeSet
和TreeMap
),并列在Set
和Map
行中。有两个通用的Queue
实现——LinkedList
,也是List
实现,和PriorityQueue
,在表中被省略。这两个实现提供非常不同的语义:LinkedList
提供 FIFO 语义,而PriorityQueue
根据其值对元素进行排序。
每个通用实现都提供其接口中包含的所有可选操作。所有允许null
元素、键和值。没有同步(线程安全)。所有都有快速失败迭代器,在迭代期间检测到非法并发修改,并快速干净地失败,而不是在未来的某个不确定时间冒险出现任意、非确定性的行为。所有都是Serializable
,并支持公共clone
方法。
这些实现不同步的事实代表了与过去的断裂:传统集合Vector
和Hashtable
是同步的。采取当前方法是因为在同步没有好处时集合经常被使用。这些用途包括单线程使用、只读使用以及作为执行自身同步的较大数据对象的一部分使用。一般来说,良好的 API 设计实践是不让用户为他们不使用的功能付费。此外,不必要的同步可能在某些情况下导致死锁。
如果你需要线程安全的集合,包装器实现部分描述的同步包装器允许任何集合转换为同步集合。因此,同步对于通用实现是可选的,而对于传统实现是强制的。此外,java.util.concurrent
包提供了BlockingQueue
接口的并发实现,它扩展了Queue
,以及ConcurrentMap
接口的并发实现,它扩展了Map
。这些实现比单纯的同步实现具有更高的并发性。
通常情况下,你应该考虑接口,而不是实现。这就是为什么本节中没有编程示例。在很大程度上,实现的选择只影响性能。如接口部分所述,首选的风格是在创建Collection
时选择一个实现,并立即将新集合分配给相应接口类型的变量(或将集合传递给期望接口类型参数的方法)。通过这种方式,程序不会依赖于给定实现中添加的任何方法,使程序员可以自由更改实现,只要性能或行为细节需要。
接下来的部分简要讨论了实现。使用诸如常数时间、对数、线性、n log(n)和二次等词语描述了实现的性能,以指代执行操作的时间复杂度的渐近上界。所有这些都是相当复杂的术语,如果你不知道它们的含义也没关系。如果你对更多信息感兴趣,请参考任何一本优秀的算法教材。要记住的一件事是,这种性能指标有其局限性。有时,名义上更慢的实现可能更快。如果有疑问,就要测量性能!
集合实现
原文:
docs.oracle.com/javase/tutorial/collections/implementations/set.html
Set
实现分为通用实现和特殊实现两类。
通用集合实现
有三种通用的Set
实现 HashSet
, TreeSet
, 和 LinkedHashSet
。在这三种中选择哪一种通常很简单。HashSet
比TreeSet
快得多(大部分操作的时间复杂度是常数时间对数时间),但不提供排序保证。如果你需要使用SortedSet
接口中的操作,或者需要按值排序的迭代,使用TreeSet
;否则,使用HashSet
。很可能大部分时间你会使用HashSet
。
LinkedHashSet
在某种意义上介于HashSet
和TreeSet
之间。作为一个带有链表的哈希表,它提供了插入顺序的迭代(最近插入的到最近插入的)并且运行速度几乎和HashSet
一样快。LinkedHashSet
实现避免了HashSet
提供的未指定、通常混乱的排序,而又不会增加TreeSet
的成本。
关于HashSet
值得注意的一点是,迭代在条目数和桶数(容量)的总和上是线性的。因此,选择一个初始容量太高会浪费空间和时间。另一方面,选择一个初始容量太低会浪费时间,因为每次强制增加容量时都需要复制数据结构。如果你不指定初始容量,默认值是 16。过去,选择一个质数作为初始容量有一些优势。但现在不再成立。在内部,容量总是向上舍入为 2 的幂。初始容量是通过使用int
构造函数指定的。以下代码行分配了一个初始容量为 64 的HashSet
。
Set<String> s = new HashSet<String>(64);
HashSet
类还有一个称为负载因子的调整参数。如果你非常关心你的HashSet
的空间消耗,阅读HashSet
文档以获取更多信息。否则,只需接受默认值;这几乎总是正确的做法。
如果你接受默认的负载因子但想指定一个初始容量,选择一个大约是你期望集合增长到两倍大小的数字。如果你的猜测完全错误,可能会浪费一点空间、时间或两者,但这不太可能成为一个大问题。
LinkedHashSet
具有与HashSet
相同的调整参数,但迭代时间不受容量影响。TreeSet
没有调整参数。
特殊用途的 Set 实现
有两个特殊用途的Set
实现 EnumSet
和 CopyOnWriteArraySet
。
EnumSet
是用于枚举类型的高性能Set
实现。枚举集合的所有成员必须是相同的枚举类型。在内部,它由一个位向量表示,通常是一个long
。枚举集合支持对枚举类型范围的迭代。例如,给定一周中的工作日的枚举声明,你可以迭代工作日。EnumSet
类提供了一个静态工厂,使其易于使用。
for (Day d : EnumSet.range(Day.MONDAY, Day.FRIDAY))
System.out.println(d);
枚举集合也为传统位标志提供了丰富的、类型安全的替代品。
EnumSet.of(Style.BOLD, Style.ITALIC)
CopyOnWriteArraySet
是一个由写时复制数组支持的Set
实现。所有的变动操作,比如add
、set
和remove
,都是通过创建数组的新副本来实现的;永远不需要加锁。即使在元素插入和删除的同时进行迭代也是安全的。与大多数Set
实现不同,add
、remove
和contains
方法所需的时间与集合大小成正比。这种实现仅适用于很少修改但频繁迭代的集合。它非常适合维护必须防止重复的事件处理程序列表。
列表实现
原文:
docs.oracle.com/javase/tutorial/collections/implementations/list.html
List
实现分为通用和特殊用途的实现。
通用列表实现
有两种通用的List
实现——ArrayList
和LinkedList
。大多数情况下,你可能会使用ArrayList
,它提供常数时间的位置访问,并且非常快速。它不必为List
中的每个元素分配一个节点对象,并且在需要同时移动多个元素时可以利用System.arraycopy
。将ArrayList
视为没有同步开销的Vector
。
如果你经常在List
的开头添加元素或者迭代List
以删除其内部的元素,你应该考虑使用LinkedList
。这些操作在LinkedList
中需要常数时间,在ArrayList
中需要线性时间。但你会在性能上付出很大的代价。在LinkedList
中,位置访问需要线性时间,在ArrayList
中需要常数时间。此外,LinkedList
的常数因子要糟糕得多。如果你认为你想使用LinkedList
,在做出选择之前用LinkedList
和ArrayList
测量你的应用程序的性能;ArrayList
通常更快。
ArrayList
有一个调整参数——初始容量,指的是ArrayList
在增长之前可以容纳的元素数量。LinkedList
没有调整参数,但有七个可选操作,其中之一是clone
。另外六个是addFirst
、getFirst
、removeFirst
、addLast
、getLast
和removeLast
。LinkedList
还实现了Queue
接口。
特殊用途的列表实现
CopyOnWriteArrayList
是一个由写入时复制数组支持的List
实现。这种实现与CopyOnWriteArraySet
类似。即使在迭代期间,也不需要同步,并且迭代器永远不会抛出ConcurrentModificationException
。这种实现非常适合维护事件处理程序列表,其中变化不频繁,遍历频繁且可能耗时。
如果你需要同步,Vector
比使用Collections.synchronizedList
同步的ArrayList
稍快一些。但Vector
有很多遗留操作,所以一定要小心,始终使用List
接口操作Vector
,否则你将无法在以后替换实现。
如果你的List
大小是固定的 也就是说,你永远不会使用remove
、add
或者除了containsAll
之外的任何批量操作 那么你有第三个选项,绝对值得考虑。查看方便实现部分的Arrays.asList
获取更多信息。
Map 实现
原文:
docs.oracle.com/javase/tutorial/collections/implementations/map.html
Map
实现分为通用、特殊和并发实现。
通用 Map 实现
三种通用Map
实现是HashMap
、TreeMap
和LinkedHashMap
。如果需要SortedMap
操作或基于键排序的Collection
-view 迭代,请使用TreeMap
;如果希望最大速度且不关心迭代顺序,请使用HashMap
;如果希望接近HashMap
性能且插入顺序迭代,请使用LinkedHashMap
。在这方面,Map
的情况类似于Set
。同样,Set Implementations 部分中的其他所有内容也适用于Map
实现。
LinkedHashMap
提供了两个LinkedHashSet
不可用的功能。当您创建一个LinkedHashMap
时,您可以根据键访问而不是插入对其进行排序。换句话说,仅查找与键关联的值会将该键移到地图的末尾。此外,LinkedHashMap
提供了removeEldestEntry
方法,可以被覆盖以在向地图添加新映射时自动实施删除过时映射的策略。这使得实现自定义缓存非常容易。
例如,这个覆盖将允许地图增长到多达 100 个条目,然后每次添加新条目时都会删除最老的条目,保持 100 个条目的稳定状态。
private static final int MAX_ENTRIES = 100;
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_ENTRIES;
}
特殊用途 Map 实现
有三种特殊用途的 Map 实现 EnumMap
、WeakHashMap
和IdentityHashMap
。EnumMap
,内部实现为array
,是用于枚举键的高性能Map
实现。此实现将Map
接口的丰富性和安全性与接近数组的速度结合在一起。如果要将枚举映射到值,应始终优先使用EnumMap
而不是数组。
WeakHashMap
是Map
接口的一个实现,只存储对其键的弱引用。只存储弱引用允许在其键不再在WeakHashMap
之外被引用时,键值对可以被垃圾回收。这个类提供了利用弱引用功能的最简单方法。它对于实现“注册表样”数据结构非常有用,其中一个条目的实用性在其键不再被任何线程引用时消失。
IdentityHashMap
是基于哈希表的基于身份的Map
实现。这个类对于保持拓扑结构的对象图转换非常有用,比如序列化或深拷贝。为了执行这样的转换,你需要维护一个基于身份的“节点表”,用于跟踪哪些对象已经被看到。基于身份的映射也用于在动态调试器和类似系统中维护对象到元信息的映射。最后,基于身份的映射对于阻止由于故意扭曲的equals
方法而导致的“欺骗攻击”非常有用,因为IdentityHashMap
永远不会在其键上调用equals
方法。这个实现的一个额外好处是它很快。
并发映射实现
java.util.concurrent
包含了ConcurrentMap
接口,它通过原子的putIfAbsent
、remove
和replace
方法扩展了Map
,以及该接口的ConcurrentHashMap
实现。
ConcurrentHashMap
是一个高度并发、高性能的哈希表实现。在执行检索操作时,此实现永远不会阻塞,并允许客户端选择更新的并发级别。它旨在作为Hashtable
的一个可替换项:除了实现ConcurrentMap
外,它还支持所有Hashtable
特有的传统方法。再次强调,如果你不需要传统操作,请小心使用ConcurrentMap
接口来操作它。
队列实现
原文:
docs.oracle.com/javase/tutorial/collections/implementations/queue.html
Queue
实现分为通用和并发实现。
通用队列实现
如前一节所述,LinkedList
实现了 Queue
接口,为 add
、poll
等提供先进先出(FIFO)队列操作。
PriorityQueue
类是基于 堆 数据结构的优先队列。此队列根据在构造时指定的顺序对元素进行排序,可以是元素的自然顺序或由显式 Comparator
强加的顺序。
队列检索操作 poll
、remove
、peek
和 element
访问队列头部的元素。队列的 头部 是相对于指定顺序的最小元素。如果多个元素具有最小值,则头部是这些元素之一;平局将被任意打破。
PriorityQueue
及其迭代器实现了 Collection
和 Iterator
接口的所有可选方法。在 iterator
方法中提供的迭代器不能保证以任何特定顺序遍历 PriorityQueue
的元素。对于有序遍历,请考虑使用 Arrays.sort(pq.toArray())
。
并发队列实现
java.util.concurrent
包含一组同步的 Queue
接口和类。BlockingQueue
扩展了 Queue
,具有在检索元素时等待队列变得非空以及在存储元素时等待队列中有空间可用的操作。该接口由以下类实现:
-
LinkedBlockingQueue
由链表节点支持的可选有界 FIFO 阻塞队列 -
ArrayBlockingQueue
由数组支持的有界 FIFO 阻塞队列 -
PriorityBlockingQueue
由堆支持的无界阻塞优先级队列 -
DelayQueue
由堆支持的基于时间的调度队列 -
SynchronousQueue
使用BlockingQueue
接口的简单会合机制
在 JDK 7 中,TransferQueue
是一个专门的BlockingQueue
,其中向队列添加元素的代码可以选择等待(阻塞),直到另一个线程中的代码检索元素。TransferQueue
只有一个实现:
LinkedTransferQueue
基于链表节点的无界TransferQueue
Deque 实现
原文:
docs.oracle.com/javase/tutorial/collections/implementations/deque.html
Deque
接口,发音为"deck", 代表双端队列。Deque
接口可以被实现为各种类型的Collections
。Deque
接口的实现被分为通用和并发实现。
通用 Deque 实现
通用实现包括LinkedList
和 ArrayDeque
类。Deque
接口支持在两端插入、删除和检索元素。ArrayDeque
类是Deque
接口的可调整大小数组实现,而LinkedList
类是列表实现。
Deque
接口中的基本插入、删除和检索操作有addFirst
、addLast
、removeFirst
、removeLast
、getFirst
和 getLast
。addFirst
方法在头部添加元素,而addLast
方法在Deque
实例的尾部添加元素。
LinkedList
实现比ArrayDeque
实现更灵活。LinkedList
实现了所有可选的列表操作。LinkedList
实现允许null
元素,但ArrayDeque
实现不允许。
就效率而言,ArrayDeque
在两端的添加和删除操作上比LinkedList
更高效。在迭代过程中,LinkedList
实现中最好的操作是删除当前元素。LinkedList
实现不是理想的迭代结构。
LinkedList
实现比ArrayDeque
实现消耗更多内存。对于ArrayDeque
实例的遍历,可以使用以下任意一种:
foreach
foreach
是一种快速且适用于各种列表的方法。
ArrayDeque<String> aDeque = new ArrayDeque<String>();
. . .
for (String str : aDeque) {
System.out.println(str);
}
迭代器
Iterator
可用于所有类型的数据的所有类型列表的正向遍历。
ArrayDeque<String> aDeque = new ArrayDeque<String>();
. . .
for (Iterator<String> iter = aDeque.iterator(); iter.hasNext(); ) {
System.out.println(iter.next());
}
ArrayDeque
类在本教程中用于实现Deque
接口。本教程中使用的示例完整代码在ArrayDequeSample
中可用。LinkedList
和 ArrayDeque
类都不支持多线程并发访问。
并发 Deque 实现
LinkedBlockingDeque
类是Deque
接口的并发实现。如果双端队列为空,则takeFirst
和 takeLast
等方法会等待直到元素变为可用,然后检索并移除相同的元素。
包装器实现
原文:
docs.oracle.com/javase/tutorial/collections/implementations/wrapper.html
包装器实现将所有真正的工作委托给指定的集合,但在该集合提供的功能之上添加额外的功能。对于设计模式爱好者,这是装饰者模式的一个例子。虽然这可能看起来有点奇特,但实际上非常简单。
这些实现是匿名的;而不是提供一个公共类,库提供了一个静态工厂方法。所有这些实现都在 Collections
类中,该类仅包含静态方法。
同步包装器
同步包装器为任意集合添加了自动同步(线程安全)。六个核心集合接口 Collection
, Set
, List
, Map
, SortedSet
, 和 SortedMap
都有一个静态工厂方法。
public static <T> Collection<T> synchronizedCollection(Collection<T> c);
public static <T> Set<T> synchronizedSet(Set<T> s);
public static <T> List<T> synchronizedList(List<T> list);
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s);
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);
这些方法中的每一个都返回由指定集合支持的同步(线程安全)Collection
。为了保证串行访问,所有对支持集合的访问必须通过返回的集合完成。确保这一点的简单方法是不保留对支持集合的引用。使用以下技巧创建同步集合。
List<Type> list = Collections.synchronizedList(new ArrayList<Type>());
以这种方式创建的集合与通常同步的集合(如 Vector
)一样线程安全。
面对并发访问时,在迭代时,用户必须手动对返回的集合进行同步。原因是迭代是通过对集合的多次调用完成的,这些调用必须组合成单个原子操作。以下是迭代包装同步集合的惯用法。
Collection<Type> c = Collections.synchronizedCollection(myCollection);
synchronized(c) {
for (Type e : c)
foo(e);
}
如果使用显式迭代器,则必须在 synchronized
块内调用 iterator
方法。不遵循此建议可能导致不确定的行为。在同步 Map
的 Collection
视图上进行迭代的惯用法类似。在迭代任何 Collection
视图时,用户必须同步在同步的 Map
上,而不是在 Collection
视图本身上进行同步,如下例所示。
Map<KeyType, ValType> m = Collections.synchronizedMap(new HashMap<KeyType, ValType>());
...
Set<KeyType> s = m.keySet();
...
// Synchronizing on m, not s!
synchronized(m) {
while (KeyType k : s)
foo(k);
}
使用包装器实现的一个小缺点是,您无法执行包装实现的任何非接口操作。因此,在前面的List
示例中,您无法在包装的ArrayList
上调用ArrayList
的ensureCapacity
操作。
不可修改的包装器
与添加功能到包装集合的同步包装器不同,不可修改的包装器会剥夺功能。特别是,它们剥夺了通过拦截所有可能修改集合的操作并抛出UnsupportedOperationException
来修改集合的能力。不可修改的包装器有两个主要用途,如下所示:
-
使集合在构建后变为不可变。在这种情况下,最好不要保留对支持集合的引用。这绝对保证了不可变性。
-
允许某些客户端对您的数据结构进行只读访问。您保留对支持集合的引用,但分发对包装器的引用。这样,客户端可以查看但不能修改,而您保持完全访问权限。
与同步包装器一样,每个六个核心Collection
接口都有一个静态工厂方法。
public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c);
public static <T> Set<T> unmodifiableSet(Set<? extends T> s);
public static <T> List<T> unmodifiableList(List<? extends T> list);
public static <K,V> Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m);
public static <T> SortedSet<T> unmodifiableSortedSet(SortedSet<? extends T> s);
public static <K,V> SortedMap<K, V> unmodifiableSortedMap(SortedMap<K, ? extends V> m);
检查接口包装器
Collections.checked
接口包装器用于与泛型集合一起使用。这些实现返回指定集合的动态类型安全视图,如果客户端尝试添加错误类型的元素,则会抛出ClassCastException
。语言中的泛型机制提供了编译时(静态)类型检查,但有可能击败此机制。动态类型安全视图完全消除了这种可能性。
便利实现
原文:
docs.oracle.com/javase/tutorial/collections/implementations/convenience.html
这一部分描述了几个迷你实现,当你不需要它们的全部功能时,它们可能比通用实现更方便、更高效。本节中的所有实现都是通过静态工厂方法而不是public
类提供的。
数组的列表视图
Arrays.asList
方法返回其数组参数的List
视图。对List
的更改会写入数组,反之亦然。集合的大小与数组相同且不可更改。如果在List
上调用add
或remove
方法,将导致UnsupportedOperationException
。
这种实现的正常用法是作为基于数组和基于集合的 API 之间的桥梁。它允许你将数组传递给期望Collection
或List
的方法。然而,这种实现还有另一个用途。如果你需要一个固定大小的List
,它比任何通用List
实现更高效。这就是惯用法。
List<String> list = Arrays.asList(new String[size]);
注意,不会保留对支持数组的引用。
不可变多副本列表
有时你会需要一个由多个相同元素副本组成的不可变List
。Collections.nCopies
方法返回这样一个列表。这种实现有两个主要用途。第一个是初始化一个新创建的List
;例如,假设你想要一个最初由 1,000 个null
元素组成的ArrayList
。下面的咒语就能实现。
List<Type> list = new ArrayList<Type>(Collections.nCopies(1000, (Type)null));
当然,每个元素的初始值不一定是null
。第二个主要用途是扩展现有的List
。例如,假设你想要向List<String>
的末尾添加 69 个字符串"fruit bat"
的副本。不清楚为什么你想要做这样的事情,但让我们假设你确实想要。下面是如何做到的。
lovablePets.addAll(Collections.nCopies(69, "fruit bat"));
通过使用同时接受索引和Collection
的addAll
形式,你可以将新元素添加到List
的中间而不是末尾。
不可变单例集合
有时你会需要一个不可变的单例Set
,它由一个指定的元素组成。Collections.singleton
方法返回这样一个Set
。这种实现的一个用途是从Collection
中删除所有指定元素的出现。
c.removeAll(Collections.singleton(e));
一个相关的惯用法是从Map
中删除映射到指定值的所有元素。例如,假设你有一个将人员映射到他们的工作领域的Map
job
并且假设你想要消除所有律师。下面的一行代码就能完成任务。
job.values().removeAll(Collections.singleton(LAWYER));
这种实现的另一个用途是为一个写成接受值集合的方法提供单个输入值。
空 Set、List 和 Map 常量
Collections
类提供了返回空Set
、List
和Map
的方法 emptySet
、emptyList
和emptyMap
。这些常量的主要用途是作为不提供任何值时作为值的集合的输入方法的输入,就像这个例子中一样。
tourist.declarePurchases(Collections.emptySet());
实现摘要
原文:
docs.oracle.com/javase/tutorial/collections/implementations/summary.html
实现是用于存储集合的数据对象,这些对象实现了接口课程中描述的接口。
Java 集合框架提供了几个核心接口的通用实现:
-
对于
Set
接口,HashSet
是最常用的实现。 -
对于
List
接口,ArrayList
是最常用的实现。 -
对于
Map
接口,HashMap
是最常用的实现。 -
对于
Queue
接口,LinkedList
是最常用的实现。 -
对于
Deque
接口,ArrayDeque
是最常用的实现。
每个通用实现提供其接口中包含的所有可选操作。
Java 集合框架还提供了几个特殊用途的实现,用于需要非标准性能、使用限制或其他异常行为的情况。
java.util.concurrent
包含几个集合实现,这些实现是线程安全的,但不受单个排他锁的控制。
Collections
类(与Collection
接口相对),提供了在集合上操作或返回集合的静态方法,这些方法被称为包装器实现。
最后,还有几个便利实现,当您不需要它们的全部功能时,这些便利实现可能比通用实现更有效。这些便利实现通过静态工厂方法提供。
问题和练习:实现方式
原文:
docs.oracle.com/javase/tutorial/collections/implementations/QandE/questions.html
问题
-
你计划编写一个程序,使用几个基本的集合接口:
Set
、List
、Queue
和Map
。你不确定哪种实现方式最适合,所以决定在了解程序在实际环境中如何运行之前,先使用通用的实现方式。这些实现方式是哪些? -
如果你需要一个提供按值排序迭代的
Set
实现,应该使用哪个类? -
你使用哪个类来访问包装器实现?
练习
- 编写一个程序,将由第一个命令行参数指定的文本文件读入一个
List
中。然后,程序应该打印文件中的随机行,打印的行数由第二个命令行参数指定。编写程序时,应一次性分配正确大小的集合,而不是在读取文件时逐渐扩展。提示:要确定文件中的行数,可以使用java.io.File.length
来获取文件的大小,然后除以平均行的假定大小。
检查你的答案。
教训:算法
原文:
docs.oracle.com/javase/tutorial/collections/algorithms/index.html
这里描述的多态算法是 Java 平台提供的可重用功能块。所有这些功能块都来自Collections
类,并且都采用静态方法的形式,其第一个参数是要执行操作的集合。Java 平台提供的绝大多数算法都是在List
实例上操作的,但其中有一些是在任意Collection
实例上操作的。本节简要描述了以下算法:
-
排序
-
洗牌
-
常规数据操作
-
搜索
-
组合
-
查找极值
排序
sort
算法重新排列一个List
,使其元素按照一种排序关系升序排列。提供了两种形式的操作。简单形式接受一个List
,并根据其元素的自然排序对其进行排序。如果您对自然排序的概念不熟悉,请阅读对象排序部分。
sort
操作使用了一个稍微优化的归并排序算法,速度快且稳定:
-
快速:它保证在
n log(n)
时间内运行,并在几乎排序好的列表上运行得更快。经验测试表明它与高度优化的快速排序一样快。快速排序通常被认为比归并排序更快,但不稳定且不能保证n log(n)
性能。 -
稳定:它不会重新排序相等的元素。这一点对于在不同属性上重复对同一列表进行排序很重要。如果邮件程序的用户按邮件日期对收件箱进行排序,然后按发件人对其进行排序,用户自然期望来自同一发件人的现在连续的邮件列表仍然按邮件日期排序。只有第二次排序是稳定的才能保证这一点。
以下简单程序
按字典顺序打印出其参数。
import java.util.*;
public class Sort {
public static void main(String[] args) {
List<String> list = Arrays.asList(args);
Collections.sort(list);
System.out.println(list);
}
}
让我们运行程序。
% java Sort i walk the line
产生了以下输出。
[i, line, the, walk]
该程序仅用于向您展示算法确实像看起来那样易于使用。
第二种sort
形式除了List
之外还需要一个Comparator
,并使用Comparator
对元素进行排序。假设你想要按照我们之前示例中的字谜分组的大小倒序打印出来,即最大的字谜组首先显示。接下来的示例展示了如何借助sort
方法的第二种形式实现这一目标。
请记住,变位词组以List
实例的形式存储在Map
中的值中。修改后的打印代码通过Map
的值视图迭代,将通过最小大小测试的每个List
放入一个List
的List
中。然后,该代码对此List
进行排序,使用一个期望List
实例的Comparator
,并实现逆大小排序。最后,该代码对排序后的List
进行迭代,打印其元素(变位词组)。以下代码替换了Anagrams
示例中main
方法末尾的打印代码。
// Make a List of all anagram groups above size threshold.
List<List<String>> winners = new ArrayList<List<String>>();
for (List<String> l : m.values())
if (l.size() >= minGroupSize)
winners.add(l);
// Sort anagram groups according to size
Collections.sort(winners, new Comparator<List<String>>() {
public int compare(List<String> o1, List<String> o2) {
return o2.size() - o1.size();
}});
// Print anagram groups.
for (List<String> l : winners)
System.out.println(l.size() + ": " + l);
在与 The Map Interface 部分相同的字典上运行程序
,具有相同的最小变位词组大小(八),产生以下输出。
12: [apers, apres, asper, pares, parse, pears, prase,
presa, rapes, reaps, spare, spear]
11: [alerts, alters, artels, estral, laster, ratels,
salter, slater, staler, stelar, talers]
10: [least, setal, slate, stale, steal, stela, taels,
tales, teals, tesla]
9: [estrin, inerts, insert, inters, niters, nitres,
sinter, triens, trines]
9: [capers, crapes, escarp, pacers, parsec, recaps,
scrape, secpar, spacer]
9: [palest, palets, pastel, petals, plates, pleats,
septal, staple, tepals]
9: [anestri, antsier, nastier, ratines, retains, retinas,
retsina, stainer, stearin]
8: [lapse, leaps, pales, peals, pleas, salep, sepal, spale]
8: [aspers, parses, passer, prases, repass, spares,
sparse, spears]
8: [enters, nester, renest, rentes, resent, tenser,
ternes,��treens]
8: [arles, earls, lares, laser, lears, rales, reals, seral]
8: [earings, erasing, gainers, reagins, regains, reginas,
searing, seringa]
8: [peris, piers, pries, prise, ripes, speir, spier, spire]
8: [ates, east, eats, etas, sate, seat, seta, teas]
8: [carets, cartes, caster, caters, crates, reacts,
recast,��traces]
洗牌
shuffle
算法与sort
的作用相反,破坏了List
中可能存在的任何顺序痕迹。换句话说,该算法根据来自随机源的输入重新排序List
,以使所有可能的排列以相等的概率发生,假设有一个公平的随机源。该算法在实现游戏机会时非常有用。例如,它可以用来洗牌代表一副牌的Card
对象的List
。此外,它还用于生成测试用例。
此操作有两种形式:一种接受一个List
并使用默认的随机源,另一种要求调用者提供一个Random对象作为随机源。该算法的代码被用作List
部分的示例。
常规数据操作
Collections
类提供了五种用于在List
对象上进行常规数据操作的算法,所有这些算法都非常简单:
-
reverse
颠倒List
中元素的顺序。 -
fill
用指定值覆盖List
中的每个元素。此操作对重新初始化List
非常有用。 -
copy
接受两个参数,目标List
和源List
,并将源的元素复制到目标中,覆盖其内容。目标List
的长度必须至少与源相同。如果目标更长,则目标中剩余的元素不受影响。 -
swap
交换List
中指定位置的元素。 -
addAll
将所有指定的元素添加到Collection
中。要添加的元素可以逐个指定,也可以作为数组指定。
搜索
binarySearch
算法在排序的List
中搜索指定的元素。该算法有两种形式。第一种接受一个List
和一个要搜索的元素("搜索键")。这种形式假定List
根据其元素的自然顺序按升序排序。第二种形式除了List
和搜索键外还接受一个Comparator
,并假定List
根据指定的Comparator
按升序排序。在调用binarySearch
之前,可以使用sort
算法对List
进行排序。
两种形式的返回值相同。如果List
包含搜索键,则返回其索引。如果没有,则返回值为(-(插入点) - 1)
,其中插入点是将值插入List
的位置,或者大于该值的第一个元素的索引,或者list.size()
如果List
中的所有元素都小于指定值。这个确实丑陋的公式保证了返回值仅当找到搜索键时为>= 0
。这基本上是一个将布尔值(found)
和整数值(index)
组合成单个int
返回值的技巧。
下面的习语可用于binarySearch
操作的两种形式,查找指定的搜索键,并在适当位置插入它(如果尚未存在)。
int pos = Collections.binarySearch(list, key);
if (pos < 0)
l.add(-pos-1, key);
组成
频率和不相交算法测试一个或多个Collections
的组成方面:
-
frequency
计算指定元素在指定集合中出现的次数 -
disjoint
确定两个Collections
是否不相交;即它们是否没有共同元素。
查找极值
min
和max
算法分别返回指定Collection
中包含的最小和最大元素。这两个操作有两种形式。简单形式只接受一个Collection
,根据元素的自然顺序返回最小(或最大)元素。第二种形式除了Collection
外还接受一个Comparator
,根据指定的Comparator
返回最小(或最大)元素。
教训:自定义集合实现
原文:
docs.oracle.com/javase/tutorial/collections/custom-implementations/index.html
许多程序员永远不需要实现自己的Collection
类。您可以通过使用本章前面描述的实现来走得很远。但是,总有一天您可能想要编写自己的实现。借助 Java 平台提供的抽象实现,这样做相当容易。在讨论如何编写实现之前,让我们讨论一下为什么您可能想要编写一个。
编写实现的原因
以下列表说明了您可能想要实现的自定义Collection
类型。这并不是详尽无遗的:
-
持久性:所有内置的
Collection
实现都驻留在主内存中,并在程序退出时消失。如果您希望在下次程序启动时仍然存在的集合,可以通过在外部数据库上构建一个薄层来实现它。这样的集合可能可以被多个程序同时访问。 -
特定应用:这是一个非常广泛的类别。一个例子是包含实时遥测数据的不可修改的
Map
。键可以表示位置,值可以根据get
操作从这些位置的传感器中读取。 -
高性能,特定用途:许多数据结构利用受限使用来提供比通用实现更好的性能。例如,考虑一个包含长时间相同元素值的
List
。这样的列表在文本处理中经常出现,可以进行运行长度编码 运行可以表示为包含重复元素和连续重复次数的单个对象。这个例子很有趣,因为它在性能方面进行了权衡:它需要更少的空间但比ArrayList
需要更多的时间。 -
高性能,通用用途:Java 集合框架的设计者试图为每个接口提供最佳的通用实现,但是可以使用许多许多数据结构,并且每天都会有新的数据结构被发明。也许您可以想出更快的解决方案!
-
增强功能:假设您需要一个高效的包实现(也称为multiset):一个
Collection
,它在允许重复元素的同时提供常数时间的包含性检查。在HashMap
之上实现这样一个集合是相当简单的。 -
便利性:您可能希望提供超出 Java 平台提供的便利的其他实现。例如,您可能经常需要表示一系列
Integer
的连续范围的List
实例。 -
适配器:假设您正在使用具有自己的特定集合 API 的旧 API。您可以编写一个适配器实现,使这些集合能够在 Java 集合框架中运行。适配器实现是一个薄膜,包装一种类型的对象,并通过将对后者类型的操作转换为对前者类型的操作来使其行为类似于另一种类型的对象。
如何编写自定义实现
编写自定义实现出人意料地容易。Java 集合框架提供了专门设计用于促进自定义实现的抽象实现。我们将从以下Arrays.asList
实现的示例开始。
public static <T> List<T> asList(T[] a) {
return new MyArrayList<T>(a);
}
private static class MyArrayList<T> extends AbstractList<T> {
private final T[] a;
MyArrayList(T[] array) {
a = array;
}
public T get(int index) {
return a[index];
}
public T set(int index, T element) {
T oldValue = a[index];
a[index] = element;
return oldValue;
}
public int size() {
return a.length;
}
}
信不信由你,这与包含在java.util.Arrays
中的实现非常接近。就是这么简单!您提供一个构造函数和get
,set
和size
方法,AbstractList
会处理其余的所有事情。您将获得ListIterator
,批量操作,搜索操作,哈希码计算,比较和字符串表示。
假设您想让实现变得更快一点。抽象实现的 API 文档准确描述了每个方法的实现方式,因此您将知道要重写哪些方法以获得所需的性能。前面的实现性能很好,但可以稍微改进一下。特别是,toArray
方法会遍历List
,一次复制一个元素。鉴于内部表示,仅克隆数组会更快更合理。
public Object[] toArray() {
return (Object[]) a.clone();
}
添加此覆盖和其他几个类似的覆盖后,此实现与java.util.Arrays
中找到的实现完全相同。为了充分披露,使用其他抽象实现会有点困难,因为您将不得不编写自己的迭代器,但仍然不是那么困难。
以下列表总结了抽象实现:
-
AbstractCollection
既不是Set
也不是List
的Collection
。至少,您必须提供iterator
和size
方法。 -
AbstractSet
一个Set
;使用方式与AbstractCollection
相同。 -
AbstractList
由随机访问数据存储支持的List
。至少,您必须提供位置访问
方法(get
,可选的set
,remove
和add
)以及size
方法。抽象类负责listIterator
(和iterator
)。 -
AbstractSequentialList
由顺序访问数据存储支持的List
,例如链表。至少,你必须提供listIterator
和size
方法。抽象类负责处理位置访问方法。(这与AbstractList
相反。) -
AbstractQueue
至少,你必须提供offer
、peek
、poll
和size
方法以及支持remove
的iterator
。 -
AbstractMap
一个Map
。至少你必须提供entrySet
视图。通常使用AbstractSet
类来实现。如果Map
是可修改的,你还必须提供put
方法。
编写自定义实现的过程如下:
-
从上述列表中选择适当的抽象实现类。
-
为类的所有抽象方法提供实现。如果你的自定义集合是可修改的,你还必须重写一个或多个具体方法。抽象实现类的 API 文档将告诉你哪些方法需要重写。
-
测试并且,如果需要,调试实现。现在你有一个可工作的自定义集合实现。
-
如果你关心性能,阅读抽象实现类的 API 文档,了解你要继承的所有方法的实现。如果有任何方法看起来太慢,就重写它们。如果你重写任何方法,请确保在重写之前和之后测量方法的性能。调整性能的努力程度应该取决于实现的使用程度以及其使用对性能的关键性。 (通常最好省略此步骤。)
课程:互操作性
原文:
docs.oracle.com/javase/tutorial/collections/interoperability/index.html
在这一部分,您将学习互操作性的以下两个方面:
-
兼容性:本小节描述了如何使集合能够与早于将
Collection
添加到 Java 平台的旧 API 一起工作。 -
API 设计:本小节描述了如何设计新的 API,以便它们能够与其他 API 无缝互操作。
兼容性
原文:
docs.oracle.com/javase/tutorial/collections/interoperability/compatibility.html
Java 集合框架旨在确保核心集合接口与早期 Java 平台版本中用于表示集合的类型之间的完全互操作性:Vector
,Hashtable
,数组,以及Enumeration
。在本节中,您将学习如何将旧集合转换为 Java 集合框架集合,反之亦然。
向上兼容性
假设你正在使用一个返回传统集合的 API 以及另一个 API,需要对象实现集合接口。为了使这两个 API 顺利互操作,你必须将传统集合转换为现代集合。幸运的是,Java 集合框架使这变得容易。
假设旧的 API 返回一个对象数组,而新的 API 需要一个Collection
。集合框架有一个方便的实现,允许将对象数组视为List
。你可以使用Arrays.asList
将数组传递给任何需要Collection
或List
的方法。
Foo[] result = oldMethod(arg);
newMethod(Arrays.asList(result));
如果旧的 API 返回一个Vector
或Hashtable
,你根本不需要做任何工作,因为Vector
已经被改装为实现List
接口,而Hashtable
已经被改装为实现Map
。因此,Vector
可以直接传递给任何需要Collection
或List
的方法。
Vector result = oldMethod(arg);
newMethod(result);
类似地,Hashtable
可以直接传递给任何需要Map
的方法。
Hashtable result = oldMethod(arg);
newMethod(result);
较少情况下,一个 API 可能返回一个代表对象集合的Enumeration
。Collections.list
方法将Enumeration
转换为Collection
。
Enumeration e = oldMethod(arg);
newMethod(Collections.list(e));
向后兼容性
假设你正在使用一个返回现代集合的 API 以及另一个 API,需要你传递传统集合。为了使这两个 API 顺利互操作,你必须将现代集合转换为旧集合。同样,Java 集合框架使这变得容易。
假设新的 API 返回一个Collection
,而旧的 API 需要一个Object
数组。正如你可能知道的那样,Collection
接口包含一个专门设计用于这种情况的toArray
方法。
Collection c = newMethod();
oldMethod(c.toArray());
如果旧的 API 需要一个String
数组(或其他类型)而不是一个Object
数组怎么办?你只需使用toArray
的另一种形式 需要一个数组作为输入的形式。
Collection c = newMethod();
oldMethod((String[]) c.toArray(new String[0]));
如果旧的 API 需要一个Vector
,标准集合构造函数会派上用场。
Collection c = newMethod();
oldMethod(new Vector(c));
需要Hashtable
的旧 API 的情况类似处理。
Map m = newMethod();
oldMethod(new Hashtable(m));
最后,如果旧的 API 需要一个Enumeration
怎么办?这种情况并不常见,但偶尔会发生,Collections.enumeration
方法就是为了处理这种情况而提供的。这是一个静态工厂方法,接受一个Collection
并返回该Collection
中元素的Enumeration
。
Collection c = newMethod();
oldMethod(Collections.enumeration(c));
API 设计
原文:
docs.oracle.com/javase/tutorial/collections/interoperability/api-design.html
在这个简短但重要的部分,你将学到一些简单的准则,让你的 API 能够与遵循这些准则的所有其他 API 无缝交互。实质上,这些规则定义了在集合世界中成为一个好“公民”所需的条件。
参数
如果你的 API 包含一个需要在输入时传入集合的方法,那么声明相关参数类型为集合接口类型至关重要。永远不要使用实现类型,因为这违背了基于接口的集合框架的目的,即允许集合在不考虑实现细节的情况下进行操作。
此外,你应该始终使用最不具体的类型。例如,如果Collection
足够了,就不要要求一个List
或者一个Set
。并不是说你在输入时永远不应该要求一个List
或者Set
;如果一个方法依赖于这些接口的属性,那么这样做是正确的。例如,Java 平台提供的许多算法在输入时需要一个List
,因为它们依赖于列表是有序的这一事实。然而,作为一般规则,最好在输入时使用最通用的类型:Collection
和Map
。
注意: 永远不要定义自己的临时collection
类,并要求在输入时使用这些类的对象。这样做会使你失去 Java 集合框架提供的所有好处。
返回值
对于返回值,你可以比输入参数更加灵活。可以返回任何实现或扩展集合接口之一的类型的对象。这可以是接口之一,也可以是扩展或实现这些接口的特殊用途类型。
例如,可以想象一个图像处理包,称为ImageList
,它返回实现List
的新类的对象。除了List
操作外,ImageList
还可以支持任何看起来有用的特定应用操作。例如,它可能提供一个indexImage
操作,返回一个包含ImageList
中每个图形缩略图的图像。需要注意的是,即使 API 在输出时提供ImageList
实例,它也应该接受任意的Collection
(或者也许是List
)实例作为输入。
从某种意义上说,返回值应该具有与输入参数相反的行为:最好返回最具体的适用集合接口,而不是最一般的。例如,如果你确定总是返回一个SortedMap
,你应该给相关方法返回类型为SortedMap
,而不是Map
。SortedMap
实例比普通的Map
实例更耗时构建,也更强大。考虑到你的模块已经投入了时间来构建SortedMap
,让用户访问其增强功能是明智的。此外,用户将能够将返回的对象传递给需要SortedMap
的方法,以及接受任何Map
的方法。
传统 API
目前有很多 API 定义了自己的临时集合类型。虽然这很不幸,但考虑到 Java 平台的前两个主要版本中没有集合框架,这是现实。假设你拥有其中一个这样的 API;以下是你可以采取的措施。
如果可能的话,更新你的传统集合类型以实现标准集合接口之一。然后,你返回的所有集合将与其他基于集合的 API 无缝地进行交互。如果这是不可能的(例如,因为一个或多个现有类型签名与标准集合接口冲突),定义一个适配器类,包装你的传统集合对象之一,使其能够作为标准集合运行。(Adapter
类是自定义实现的一个示例。)
如果可能的话,通过新的调用来更新你的 API,遵循输入指南以接受标准集合接口的对象。这样的调用可以与接受传统集合类型的调用共存。如果这是不可能的,为你的传统类型提供一个构造函数或静态工厂,接受一个标准接口的对象,并返回包含相同元素(或映射)的传统集合。这两种方法中的任何一种都将允许用户将任意集合传递给你的 API。
教程:日期时间
日期时间包,java.time,在 Java SE 8 发布中引入,提供了一个全面的日期和时间模型,并在JSR 310: 日期和时间 API下开发。虽然java.time
基于国际标准化组织(ISO)日历系统,但也支持常用的全球日历。
这个教程涵盖了使用基于 ISO 的类来表示日期和时间以及操作日期和时间值的基础知识。
教训:日期时间概述
原文:
docs.oracle.com/javase/tutorial/datetime/overview/index.html
时间似乎是一个简单的主题;即使是一只廉价的手表也可以提供相当准确的日期和时间。然而,仔细观察后,你会意识到时间的微妙复杂性和许多影响你对时间理解的因素。例如,将一个月加到 1 月 31 日的结果对于闰年和其他年份是不同的。时区也增加了复杂性。例如,一个国家可能会在短时间内进入和退出夏令时,或者一年内多次进入和退出夏令时,或者在某一年完全跳过夏令时。
日期时间 API 使用ISO-8601中定义的日历系统作为默认日历。这个日历基于格里高利历系统,在全球范围内被用作代表日期和时间的事实标准。日期时间 API 中的核心类的名称如LocalDateTime
、ZonedDateTime
和OffsetDateTime
。所有这些类都使用 ISO 日历系统。如果你想使用另一个日历系统,比如伊斯兰历或泰国佛历,java.time.chrono
包允许你使用其中一个预定义的日历系统。或者你也可以创建自己的日历系统。
日期时间 API 使用Unicode 通用区域数据存储库 (CLDR)。这个存储库支持世界上的语言,并包含可用的最大的区域数据集合。这个存储库中的信息已被本地化到数百种语言。日期时间 API 还使用时区数据库 (TZDB)。这个数据库提供自 1970 年以来全球每个时区变更的信息,以及自该概念引入以来主要时区的历史。
日期时间设计原则
原文:
docs.oracle.com/javase/tutorial/datetime/overview/design.html
日期时间 API 是根据几个设计原则开发的。
清晰
API 中的方法被明确定义,其行为清晰可预期。例如,使用null
参数值调用日期时间方法通常会触发NullPointerException
。
流畅
日期时间 API 提供了流畅的接口,使代码易于阅读。因为大多数方法不允许带有null
值的参数,并且不返回null
值,方法调用可以链接在一起,结果代码可以快速理解。例如:
LocalDate today = LocalDate.now();
LocalDate payday = today.with(TemporalAdjusters.lastDayOfMonth()).minusDays(2);
不可变
日期时间 API 中的大多数类创建的对象是不可变的,这意味着在对象创建后,它是不能被修改的。要修改不可变对象的值,必须构建一个修改后的原始副本作为新对象。这也意味着日期时间 API 在定义上是线程安全的。这影响了 API,大多数用于创建日期或时间对象的方法都以of
、from
或with
为前缀,而不是构造函数,并且没有set
方法。例如:
LocalDate dateOfBirth = LocalDate.of(2012, Month.MAY, 14);
LocalDate firstBirthday = dateOfBirth.plusYears(1);
可扩展
日期时间 API 在尽可能的地方是可扩展的。例如,您可以定义自己的时间调整器和查询,或构建自己的日历系统。
日期时间包
原文:
docs.oracle.com/javase/tutorial/datetime/overview/packages.html
日期时间 API 由主要包java.time
和四个子包组成:
java.time
该 API 的核心用于表示日期和时间。它包括了日期、时间、日期和时间的组合、时区、瞬间、持续时间和时钟的类。这些类基于 ISO-8601 中定义的日历系统,是不可变的和线程安全的。
java.time.chrono
用于表示除默认 ISO-8601 之外的日历系统的 API。您也可以定义自己的日历系统。本教程不会详细介绍这个包。
java.time.format
用于格式化和解析日期和时间的类。
java.time.temporal
扩展 API,主要用于框架和库的编写者,允许日期和时间类之间的互操作,查询和调整。在这个包中定义了字段(TemporalField
和 ChronoField
)和单位(TemporalUnit
和 ChronoUnit
)。
java.time.zone
支持时区、时区偏移和时区规则的类。如果涉及时区,大多数开发人员只需要使用ZonedDateTime
、ZoneId
或ZoneOffset
。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
2023-04-12 NumPy 初学者指南中文第三版:11~14
2023-04-12 NumPy 初学者指南中文第三版:6~10
2023-04-12 NumPy 初学者指南中文第三版:1~5
2023-04-12 NumPy 秘籍中文第二版:11~12
2023-04-12 NumPy 秘籍中文第二版:6~10
2023-04-12 NumPy 秘籍中文第二版:1~5
2023-04-12 NumPy 秘籍中文第二版:十二、使用 NumPy 进行探索性和预测性数据分析