简介
流本身并不持有任何数据--它们只是从一个源头流出来。然而,普通的代码例程希望在处理数据后有某种结构来保存结果。这就是为什么在(可选择的)中间操作之后,流API提供了一些方法来将它可能处理过的元素转换成集合--比如列表,你可以在你的代码中进一步使用。
这些方式包括应用。
- 预定义的或自定义的收集器。
<R,A> R collect(Collector<? super T,A,R> collector);
这是你可以利用的最常见、最干净和最简单的方法,我们将首先介绍这个。
- 供应商、累积器和组合器(将一个
Collector分离成其组成部分)。
<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);
或者,你可以通过把一个流变成一个数组来终止它。然后,把这个数组变成一个列表。这是因为API已经有两种产生数组的方法。它们包括。
Object[] toArray();
返回一个包含流元素的数组。
<A> A[] toArray(IntFunction<A[]> generator);
其中,
generator是一个函数,它产生一个所需类型和提供长度的新数组。
这些产生数组的方法势必会使代码变得异常冗长。而且,这可能会使你的代码不那么可读。然而,最终,它们仍然会帮助你将一个流转换为一个列表。
如果你想阅读更多关于数组到列表的转换,请阅读《如何将Java数组转换为ArrayList》。
否则,本指南将详细研究所有这些方法如何工作。它还会抛出一些肮脏的黑客,这些黑客也会帮助你进行转换。不过,要小心使用它们--这样的修修补补一定会损害你的代码性能。
如何使用收集器将流转换为列表
官方文档将收集器定义为一个实现,它是。
- 可变的。
- 一个减少操作。
和。
[3]将输入元素累积到一个可变的结果容器中,[4]在所有输入元素被处理后,可选择将累积的结果转化为最终表示。
请注意,这4个条件看起来很拗口。但是,正如我们接下来要看到的,它们并不难满足。
预定义的收集器
Java 8的流API与收集器API协同工作。Collectors 类提供了现成的收集器,这些收集器在它们的实现中应用了供应者-积累者-组合者。
因此,使用来自Collectors 实用类的设施将极大地清理你的代码。
我们可以从
Collectors类中使用的方法是Collectors.toList()。
使用预先建立的Collectors ,将一个流转换为一个列表,我们只需将collect() 。
List list = Stream.of("David", "Scott", "Hiram").collect(Collectors.toList());
System.out.println(String.format("Class: %s\nList: %s", list.getClass(), list));
这个例子相当简单,只是处理了字符串。
Class: class java.util.ArrayList
List: [David, Scott, Hiram]
不过,如果你不是在处理字符串或更简单的类型,你很可能要在收集对象之前将其map() ,这是更常见的情况。让我们定义一个简单的Donor 对象,和一个记录它们的BloodBank ,并将一个StreamofDonors转换成一个List。
用map()和collect()将流转换为列表
让我们首先声明一个Donor 类,为献血者建模。
public class Donor implements Comparable<Donor>{
private final String name;
//O-, O+, A-, A+, B-, B+, AB-, AB+
private final String bloodGroup;
//The amount of blood donated in mls
//(An adult can donate about 450 ml of blood)
private final int amountDonated;
public Donor(String name, String bloodGroup, int amountDonated) {
//Validation of the name and the bloodtype should occur here
this.name = name;
this.bloodGroup = bloodGroup;
this.amountDonated = amountDonated;
}
@Override
public int compareTo(Donor otherDonor) {
return Comparator.comparing(Donor::getName)
.thenComparing(Donor::getBloodGroup)
.thenComparingInt(Donor::getAmountDonated)
.compare(this, otherDonor);
}
}
建议在这里实现Comparable 接口,因为它便于对集合中的Donor 对象进行排序和分类。你可以随时提供自定义的Comparator,尽管如此,一个Comparable 实体在工作中更容易、更干净。
然后,我们定义了一个BloodBank 接口,它规定血库可以从一个Donor ,以及返回所有可用的类型。
public interface BloodBank {
void receiveDonationFrom(Donor donor);
List<String> getAvailableTypes();
}
下一步是创建一个BloodBank 的具体实现。由于所有的具体实现都会接受捐赠者,而只有获取可用类型的方法是与实现相关的--让我们创建一个抽象类作为中间人。
public abstract class AbstractBloodBank implements BloodBank {
// Protected so as to expose the donors' records to all other blood banks that will extend this AbstractBloodBank
protected final List<Donor> donors;
public AbstractBloodBank() {
this.donors = new ArrayList<>();
}
@Override
public void receiveDonationFrom(Donor donor) {
donors.add(donor);
}
// Classes that extend AbstractBloodBank should offer their unique implementations
// of extracting the blood group types from the donors' records
@Override
public abstract List<String> getAvailableTypes();
}
Donor 最后,我们可以去创建一个具体的实现,map() 列表到他们的血型,在一个Stream ,并collect() ,回到一个列表,返回可用的血型。
public class CollectorsBloodBank extends AbstractBloodBank {
@Override
public List<String> getAvailableTypes() {
return donors.stream().map(Donor::getBloodGroup).collect(Collectors.toList());
}
}
你可以将捐赠者map() 到对象中的任何一个字段,并返回这些字段的列表,如amountDonated 或name 也是如此。有了可比的字段,也就有可能通过sorted() 对它们进行排序。
如果你想阅读更多关于
sorted()方法的内容,请阅读我们的《如何用Stream.sorted()对一个列表进行排序》。
你可以返回所有的Donor 实例,而不是简单地对它们的Stream ,调用collect() 。
@Override
public List<Donor> getAvailableDonors() {
return donors.stream().collect(Collectors.toList());
}
不过,你并不局限于将一个流收集成一个列表--这就是collectingAndThen() 方法发挥作用的地方。
用Collectors.collectionAndThen()将流转换为列表
早些时候,我们查阅了官方文档,它指出收集器有以下能力。
在所有输入元素被处理后,可选择将累积的结果转化为最终的表示。
例如,CollectorsBloodBank 中的累积结果是由Collectors.toList() 表示的。我们可以使用方法Collectors.collectingAndThen() ,进一步转换这个结果。
良好的实践要求我们返回不可变的集合对象。因此,如果我们要坚持这种做法,可以在流向列表的转换过程中加入一个结束步骤。
public class UnmodifiableBloodBank extends AbstractBloodBank {
@Override
public List<String> getAvailableTypes() {
return donors.stream()
.map(Donor::getBloodGroup)
.collect(
Collectors.collectingAndThen(
//Result list
Collectors.toList(),
//Transforming the mutable list into an unmodifiable one
Collections::unmodifiableList
)
);
}
}
另外,你也可以把任何Function<R, RR> ,作为一个结束器放在这里。
如果你想了解更多,你也可以阅读我们关于
Collectors.collectingAndThen()方法的详细指南(即将推出!)。
用供应商、累积器和组合器将流转换为列表
你可以使用单独的Supplier、Accumulators和Combiners,而不是使用预定义的收集器。这些都是以Suplier<R> 、BiConsumer<R, ? super T> 和BiConsumer<R,R> 的形式实现的,它们都可以依偎在一个collect() ,而不是一个预先定义的Collector 。
让我们来看看如何利用这种灵活性来返回所有可用的类型。
public class LambdaBloodBank extends AbstractBloodBank {
@Override
public List<String> getAvailableTypes() {
return donors.stream() //(1)
.map(donor -> donor.getBloodGroup()) //(2)
.collect(
() -> new ArrayList<String>(), //(3)
(bloodGroups, bloodGroup) -> bloodGroups.add(bloodGroup), //(4)
(resultList, bloodGroups) -> resultList.addAll(bloodGroups) //(5)
);
}
}
上面的实现在几个步骤中应用了必要的供应商-积累者-组合者模式。
首先,它把donors 列表字段变成了Donor 的元素流。
记住,
LambdaBloodBank可以访问donors字段,因为它扩展了AbstractBloodBank。而且,donors字段在AbstractBloodBank类中有受保护的访问权。
然后,在Donors 的流上执行一个中间映射操作。该操作创建了一个新的流,包含代表捐赠者血型的String 值。然后,一个可变的结果容器--即采集器的供应商被创建。这个供应商容器今后将被称为bloodGroups 。
我们将流中的每个血型(在这一步中命名为bloodgroup )添加到可变容器中:bloodGroups 。换句话说,积累就发生在这个步骤中。
可变的、供应商的容器�bloodGroups�在这一步被添加到被称为resultList 的结果容器中。因此,这是一个组合步骤。
我们可以通过使用方法引用而不是lambdas来进一步改进LambdaBloodBank'sgetAvailableTypes() 方法。
public class MembersBloodBank extends AbstractBloodBank {
@Override
public List<String> getAvailableTypes() {
return donors.stream()
.map(Donor::getBloodGroup)
.collect(
ArrayList::new,
ArrayList::add,
ArrayList::addAll
);
}
}
为Java 8流创建自定义采集器
当你传递时。
Collectors.collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);
你是在提供参数,Collectors 工具类将使用这些参数来为你创建一个自定义收集器,是隐含的。否则,创建一个自定义收集器的起点是Collector 接口的实现。
在我们的例子中,一个积累血型的收集器看起来就像这个CustomCollector 类。
public class CustomCollector implements Collector<String, List<String>, List<String>> {
// Defines the mutable container that will hold the results
@Override
public Supplier<List<String>> supplier() {
return ArrayList::new;
}
// Defines how the mutable container
// should accumulate the elements passed to it from the stream
@Override
public BiConsumer<List<String>, String> accumulator() {
return List::add;
}
// The combiner method will only be called when you are running the stream in parallel
// If you stick to sequential stream processing
// Only the supplier and accumulator will be called and, optionally the finisher method
@Override
public BinaryOperator<List<String>> combiner() {
return (bloodGroups, otherBloodGroups) -> {
bloodGroups.addAll(otherBloodGroups);
return bloodGroups;
};
}
//Defines any other transformations that should be carried out on the mutable container before
//it is finally returned at when the stream terminates
@Override
public Function<List<String>, List<String>> finisher() {
return Collections::unmodifiableList;
}
@Override
public Set<Characteristics> characteristics() {
return Collections.emptySet();
}
}
然后,CustomCollector 类可以帮助你将一个流转换为一个列表,就像在这个CustomCollectorBloodBank 类中。
public class CustomCollectorBloodBank extends AbstractBloodBank {
@Override
public List<String> getAvailableTypes() {
return donors.stream()
.map(Donor::getBloodGroup)
// Plug in the custom collector
.collect(new CustomCollector());
}
}
**注意:**如果你要全力以赴--你可以有多个方法,如toList() ,toMap() ,等等,使用这个相同的类返回不同的集合。
如何使用数组将流转换为列表
Stream API提供了一种将流管道中的元素收集成数组的方法。而且因为Arrays 实用类有将数组转化为列表的方法,所以这是一个你可以选择的途径。尽管这种方法在代码上比较冗长,我们建议利用预先建立的收集器,或者在标准收集器不适合你的使用情况下,定义你自己的收集器。
对象的数组
使用Stream.toArray() 方法,将一个流转换为一个对象的数组。(也就是基础Object 类的元素)。这可能会变得过于冗长,这取决于你的使用情况,而且有可能在很大程度上降低你的代码的可读性。
以这个ArrayOfObjectsBloodBank 类为例。
public class ArrayOfObjectsBloodBank extends AbstractBloodBank {
@Override
public List<String> getAvailableTypes() {
// Transform the stream into an array of objects
Object[] bloodGroupObjects = donors.stream()
.map(Donor::getBloodGroup)
.toArray();
// Initialize another array with the same length as that of the array of objects from the stream
String[] bloodGroups = new String[bloodGroupObjects.length];
// Iterate over the array of objects to read each object sequentially
for (int i = 0; i < bloodGroupObjects.length; i++) {
Object bloodGroupObject = bloodGroupObjects[i];
//Cast each object into an equivalent string representation
bloodGroups[i] = String.class.cast(bloodGroupObject);
}
// Transform the array of blood group string representations into a list
return Arrays.asList(bloodGroups);
}
}
这种方法是变化无常的,需要经典的for 循环和迭代,手动铸造,而且比以前的方法可读性差很多--但它是有效的。
需要IntFunction生成器的数组
Stream API提供的另一种将元素流变成数组的方法是Stream.toArray(IntFunction<A[]> generator) 。前面的派生对象数组的方法需要使用相当多的代码行,而生成器的方法则相当简洁。
public class ArrayBloodBank extends AbstractBloodBank {
@Override
public List<String> getAvailableTypes() {
// Transform the stream into an array holding elements of the same class type
// like those in the stream pipeline
String[] bloodGroupArr = donors.stream()
.map(Donor::getBloodGroup)
.toArray(String[]::new);
//Transform the array into a list
return Arrays.asList(bloodGroupArr);
}
}
这比之前的方法要好得多,而且实际上也不是那么糟糕--虽然,这里仍然有一个数组和列表之间的简单冗余转换。
将流转换为列表的其他(不鼓励的)策略
Stream API不鼓励在流管道中引入副作用。因为流可能被暴露在并行线程中,试图修改一个外部声明的源容器是很危险的。
因此,下面两个例子中,当你想把流转换为列表时,使用Stream.forEach() 和Stream.reduce() 是不好的黑客。
在Stream.forEach()上搭便车
public class ForEachBloodBank extends AbstractBloodBank {
@Override
public List<String> getAvailableTypes() {
List<String> bloodGroups = new ArrayList<>();
donors.stream()
.map(Donor::getBloodGroup)
//Side effects are introduced here - this is bad for parallelism
.forEach(bloodGroups::add);
return bloodGroups;
}
}
在没有并行的情况下,这样做很好,代码会产生你想要的结果*,但*它不适合未来,最好避免。
使用Stream.reduce()将一个流转换为列表
public class StreamReduceBloodBank extends AbstractBloodBank {
@Override
public List<String> getAvailableTypes() {
return donors.stream()
.map(Donor::getBloodGroup)
.reduce(
// Identity
new ArrayList<>(),
// Accumulator function
(bloodGroups, bloodGroup) -> {
bloodGroups.add(bloodGroup);
return bloodGroups;
},
// Combiner function
(bloodGroups, otherBloodGroups) -> {
bloodGroups.addAll(otherBloodGroups);
return bloodGroups;
}
);
}
}
结论
Stream API引入了多种方法,使Java在本质上更加实用。因为流有助于操作的并行运行,所以可选的中间和终端操作必须坚持以下原则。
- 不干涉
- 最大限度地减少副作用
- 保持操作行为的无状态
在本文所探讨的策略中,收集器的使用是有望帮助你实现所有三个原则的一个。因此,当你继续使用流时,你必须提高你处理预定义和自定义收集器的技能。
本指南的源代码可以在GitHub上找到。