ITEM 47: PREFER COLLECTION TO STREAM AS A RETURN TYPE
许多方法返回元素集合。在Java 8之前,这些方法的明显返回类型是 collection 接口、Set 接口和 List 接口; Iterable; 以及数组类型。通常,很容易决定返回哪些类型。规范是一个集合接口。如果该方法仅用于启用 for-each 循环,或者无法使返回的序列实现某些集合方法(通常是contains(Object)),则使用 Iterable 接口。如果返回的元素是原始值或有严格的性能要求,则使用数组。在 Java 8 中,流被添加到平台中,这大大增加了为序列返回方法选择适当返回类型的任务的复杂性。
您可能听说过,现在流是返回元素序列的最佳选择,但是正如 item 45 中所讨论的,流并没有使迭代变得过时:编写好的代码需要明智地结合流和迭代。如果一个 API 只返回一个流,而一些用户希望用 for-each 循环遍历返回的序列,那么这些用户会感到不安,这是可以理解的。尤其令人沮丧的是,Stream 接口在 Iterable 接口中包含唯一的抽象方法,而 Stream 对此方法的规范与Iterable 兼容。流未能扩展Iterable,这使得程序员不能使用 for-each 循环遍历流。
遗憾的是,这个问题没有好的解决办法。乍一看,将方法引用传递给 Stream 的迭代器方法似乎是可行的。生成的代码可能有点嘈杂和不透明,但也不是没有道理:
// Won't compile, due to limitations on Java's type inference
for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {
// Process the process
}
不幸的是,如果你试图编译这段代码,你会得到一个错误信息:
"Test.java:6: error: method reference not expected here
for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {"
为了使代码编译,您必须将方法引用转换为适当的参数化Iterable:
// Hideous workaround to iterate over a stream
for (ProcessHandle ph : (Iterable<ProcessHandle>) ProcessHandle.allProcesses()::iterator)
这个客户端代码可以工作,但是它太过嘈杂和不透明,不适合在实践中使用。更好的解决方法是使用适配器方法。JDK 没有提供这样的方法,但是很容易编写这样的方法,使用与上面代码片段中使用的内联技术相同的技术。注意,在适配器方法中不需要强制转换,因为 Java 的类型推断在这种情况下可以正常工作:
// Adapter from Stream<E> to Iterable<E>
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
return stream::iterator;
}
使用这个适配器,您可以使用for-each语句迭代任何流:
for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) {
// Process the process
}
注意,item 34 中的字谜程序的流版本使用了 Files.lines 方法读取字典,而迭代版本使用扫描器。 Files.lines 方法优于扫描器,扫描器可以在读取文件时自动处理遇到的任何异常。理想情况下,我们应该使用文件。迭代版本中的行也是如此。如果 API 只提供对序列的流访问,并且希望使用 for-each 语句在序列上进行迭代,那么程序员就会做出这种妥协。
相反,如果一个程序员想要使用流管道来处理一个序列,那么 API 只提供了一个可迭代的,这是可以理解的。同样,JDK 没有提供适配器,但是很容易编写一个适配器:
// Adapter from Iterable<E> to Stream<E>
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
return StreamSupport.stream(iterable.spliterator(), false);
}
如果您正在编写一个返回对象序列的方法,并且您知道它只会在流管道中使用,那么您当然可以随意地返回流。类似地,返回仅用于迭代的序列的方法应该返回一个Iterable。但是如果你写一个公共 API,它返回一个序列,你应该提供用户想写流管道以及那些想写 for-each语句,除非你有充分的理由相信大多数用户想要使用相同的机制。
集合接口是 Iterable 的子类型,并且有一个流方法,因此它提供了迭代和流访问。因此,集合或适当的子类型通常是公共序列返回方法的最佳返回类型。数组还提供了简单的迭代和对数组的流访问: Arrays.asList和 Stream.of 方法。如果返回的序列足够小,可以轻松地放入内存,那么最好返回一个标准集合实现,比如 ArrayList 或 HashSet。但是不要将一个大的序列存储在内存中,只是将它作为一个集合返回。
如果返回的序列很大,但是可以用简洁的方式表示,那么可以考虑实现一个特殊用途的集合。例如,假设您想要返回一个给定集合的幂集,该幂集由它的所有子集组成。的幂集{a, b, c} {{}, {}, {b}, {c}, {a、b}, {a, c}, {b, c}, {a, b, c}}。如果一个集合有n个元素,它的幂集就是2的n次方。因此,您甚至不应该考虑将 power 集存储在标准集合实现中。但是,在 AbstractList 的帮助下,很容易实现作业的自定义集合。
诀窍是使用索引幂集的每个元素作为一个位向量,在该指数的 n 位表示第 n 个元素的存在与否从源。在本质上,之间有一个自然的映射二进制数字从 0 到 2的n-1次方和一组n元的幂集。这是代码:
// Returns the power set of an input set as custom collection
public class PowerSet {
public static final <E> Collection<Set<E>> of(Set<E> s) {
List<E> src = new ArrayList<>(s);
if (src.size() > 30)
throw new IllegalArgumentException("Set too big " + s);
return new AbstractList<Set<E>>() {
@Override public int size() {
return 1 << src.size(); // 2 to the power srcSize
}
@Override public boolean contains(Object o) {
return o instanceof Set && src.containsAll((Set)o);
}
@Override public Set<E> get(int index) {
Set<E> result = new HashSet<>();
for (int i = 0; index != 0; i++, index >>= 1)
if ((index & 1) == 1) result.add(src.get(i));
return result;
}
};
}
}
注意, 如果输入集有超过30个元素,PowerSet.of 抛出异常。这突出了使用集合作为返回类型而不是流或 Iterable 的缺点: 集合有一个 int 返回大小方法,它将返回序列的长度限制为整数。Integer.MAX_VALUE,或者2(31 - 1)。集合规范确实允许 size 方法返回2(31 - 1)(如果集合更大,甚至是无穷大),但这不是一个完全令人满意的解决方案。
为了在 AbstractCollection 之上编写集合实现,除了 Iterable 所需的方法之外,您只需要实现两个方法: contains 和 size。通常很容易编写这些方法的有效实现。如果它不可行,可能是因为序列的内容在迭代发生之前没有预先确定,那么返回一个流或 iterable,无论哪个更自然。如果选择,可以使用两个单独的方法返回。
有时,您将仅根据实现的简单程度来选择返回类型。例如,假设您希望编写一个方法来返回输入列表的所有(连续的)子列表。只需要三行代码就可以生成这些子列表并将它们放入一个标准集合中,但是保存这个集合所需的内存是源列表大小的两倍。虽然这没有幂集那么糟糕,幂集是指数的,但显然是不可接受的。实现自定义集合(就像我们在 power 集中所做的那样)将是冗长乏味的,因为 JDK 缺少一个框架迭代器实现来帮助我们。
然而,实现一个输入列表的所有子列表的流是很简单的,尽管它需要一点洞察力。让我们将包含列表的第一个元素的子列表称为列表的前缀。例如,(a, b, c) 的前缀是(a), (a, b),和 (a, b, c)。同样,我们叫一个子列表,其中包含后缀,最后一个元素的后缀(a, b, c) (a, b, c)、(b, c)和(c),洞察力是列表的子列表只是后缀的前缀(或相同的前缀后缀)和空列表。这一观察直接导致了一个清晰、合理、简洁的实现:
// Returns a stream of all the sublists of its input list
public class SubLists {
public static <E> Stream<List<E>> of(List<E> list) {
return Stream.concat(Stream.of(Collections.emptyList()), prefixes(list).flatMap(SubLists::suffixes));
}
private static <E> Stream<List<E>> prefixes(List<E> list) {
return IntStream.rangeClosed(1, list.size()).mapToObj(end -> list.subList(0, end));
}
private static <E> Stream<List<E>> suffixes(List<E> list) {
return IntStream.range(0, list.size()).mapToObj(start -> list.subList(start, list.size()));
}
}
注意 Stream.concat 方法用于将空列表添加到返回的流中。还要注意,flatMap方法(item 45)用于生成由所有前缀的所有后缀组成的单一流。最后,请注意,我们通过映射 IntStream 返回的连续 int 值流来生成前缀和后缀。范围和 IntStream.rangeClosed。粗略地说,这个习惯用法相当于整数索引上的标准 for 循环。因此,我们的子列表实现在本质上类似于明显的嵌套 for 循环:
for (int start = 0; start < src.size(); start++)
for (int end = start + 1; end <= src.size(); end++)
System.out.println(src.subList(start, end));
可以将这个for循环直接转换为流。结果比我们之前的实现更简洁,但是可读性可能稍差一些。它在精神上类似于 item 45 中的笛卡尔积的 streams 代码:
// Returns a stream of all the sublists of its input list
public static <E> Stream<List<E>> of(List<E> list) {
return IntStream.range(0, list.size()).mapToObj(start -> IntStream.rangeClosed(start + 1, list.size()).mapToObj(end -> list.subList(start, end))).flatMap(x -> x);
}
  与前面的for循环一样,此代码不会发出空列表。为了修复这个缺陷,您可以使用concat,就像我们在前一个版本中所做的那样,或者在 rangeClosed 调用中使用 (int) Math.signum(start) 替换1。
  这两种子列表的流实现都很好,但是都需要一些用户使用流到迭代的适配器,或者在迭代更自然的地方使用流。流到迭代的适配器不仅使客户机代码变得混乱,而且还将我的机器上的循环速度降低了2.3倍。专门构建的集合实现(这里没有显示)要冗长得多,但是运行速度大约是基于流的实现的1.4倍。
  总之,在编写返回元素序列的方法时,请记住,有些用户可能希望将其作为流进行处理,而有些用户可能希望对其进行迭代。尽量容纳两组人。如果返回集合是可行的,就这样做。如果集合中已经有了元素,或者序列中的元素数量足够少,可以创建一个新的元素,那么返回一个标准集合,比如 ArrayList。否则,考虑实现自定义集合,就像我们对 power 集所做的那样。如果在将来的 Java 版本中,流接口声明被修改为扩展 Iterable,那么您应该可以自由地返回流,因为它们允许流处理和迭代。
网友评论