本系列文章翻译自
@shekhargulati
的java8-the-missing-tutorialjava
你已经学习了Stream API可以让你以声明式的方式帮助你处理集合。咱们看到collect是一个将管道流的结果集到一个list中的结束操做。collect是一个将数据流缩减为一个值的归约操做。这个值能够是集合、映射,或者一个值对象。你可使用collect达到如下目的:git
将数据流缩减为一个单一值:一个流执行后的结果可以被缩减为一个单一的值。单一的值能够是一个Collection,或者像int、double等的数值,再或者是一个用户自定义的值对象。github
将一个数据流中的元素进行分组:根据任务类型将流中全部的任务进行分组。这将产生一个Map<TaskType, List
分割一个流中的元素:你能够将一个流分割为两组——好比将任务分割为要作和已经作完的任务。app
为了感觉到Collector的威力,让咱们来看一下咱们要根据任务类型来对任务进行分类的例子。在Java8中,咱们能够经过编写以下的代码达到将任务根据类型分组的目的。ide
private static Map<TaskType, List<Task>> groupTasksByType(List<Task> tasks) { return tasks.stream().collect(Collectors.groupingBy(task -> task.getType())); }
上面的代码使用了定义在辅助类Collectors中的groupingBy收集器。它建立了一个映射,其中TaskType是它的键,而包含了全部拥有相同TaskType的任务的列表是它的值。为了在Java7中达到相同的效果,你须要编写以下的代码。学习
public static void main(String[] args) { List<Task> tasks = getTasks(); Map<TaskType, List<Task>> allTasksByType = new HashMap<>(); for (Task task : tasks) { List<Task> existingTasksByType = allTasksByType.get(task.getType()); if (existingTasksByType == null) { List<Task> tasksByType = new ArrayList<>(); tasksByType.add(task); allTasksByType.put(task.getType(), tasksByType); } else { existingTasksByType.add(task); } } for (Map.Entry<TaskType, List<Task>> entry : allTasksByType.entrySet()) { System.out.println(String.format("%s =>> %s", entry.getKey(), entry.getValue())); } }
Collectors辅助类提供了大量的静态辅助方法来建立收集器为常见的使用场景服务,像将元素收集到一个集合中、分组和分割元素,或者根据不一样的标准来概述元素。咱们将在这篇博文中涵盖大部分常见的Collector。测试
正如上面讨论的,收集器能够被用来收集流的输出到一个集合,或者产生一个单一的值。this
让咱们编写咱们的第一个测试用例——给定一个任务列表,咱们想将他们的标题收集进一个列表。google
import static java.util.stream.Collectors.toList; public class Example2_ReduceValue { public List<String> allTitles(List<Task> tasks) { return tasks.stream().map(Task::getTitle).collect(toList()); } }
toList收集器使用了列表的add方法来向结果列表中添加元素。toList收集器使用了ArrayList做为列表的实现。
若是咱们想要确保返回的标题都是惟一的,而且咱们不在意元素的顺序,那么咱们可使用toSet收集器。
import static java.util.stream.Collectors.toSet; public Set<String> uniqueTitles(List<Task> tasks) { return tasks.stream().map(Task::getTitle).collect(toSet()); }
toSet方法使用了HashSet做为集合的实现来存储结果集。
你可使用toMap收集器将一个流转换为一个映射。toMap收集器须要两个映射方法来得到映射的键和值。在下面展现的代码中,Task::getTitle是接收一个任务并产生一个只包含该任务标题的键的Function。task -> task是一个用来返回任务自己的lambda表达式。
private static Map<String, Task> taskMap(List<Task> tasks) { return tasks.stream().collect(toMap(Task::getTitle, task -> task)); }
咱们能够经过使用Function接口中的默认方法identity来改进上面展现的代码,以下所示,这样可让代码更加简洁,并更好地传达开发者的意图。
import static java.util.function.Function.identity; private static Map<String, Task> taskMap(List<Task> tasks) { return tasks.stream().collect(toMap(Task::getTitle, identity())); }
从一个流中建立映射的代码会在存在重复的键时抛出异常。你将会获得一个相似下面的错误。
Exception in thread "main" java.lang.IllegalStateException: Duplicate key Task{title='Read Version Control with Git book', type=READING} at java.util.stream.Collectors.lambda$throwingMerger$105(Collectors.java:133)
你能够经过使用toMap方法的另外一个变体来处理重复问题,它容许咱们指定一个合并方法。这个合并方法容许用户他们指定想如何处理多个值关联到同一个键的冲突。在下面展现的代码中,咱们只是使用了新的值,固然你也能够编写一个智能的算法来处理冲突。
private static Map<String, Task> taskMap_duplicates(List<Task> tasks) { return tasks.stream().collect(toMap(Task::getTitle, identity(), (t1, t2) -> t2)); }
你能够经过使用toMap方法的第三个变体来指定其余的映射实现。这须要你指定将用来存储结果的Map和Supplier。
public Map<String, Task> collectToMap(List<Task> tasks) { return tasks.stream().collect(toMap(Task::getTitle, identity(), (t1, t2) -> t2, LinkedHashMap::new)); }
相似于toMap收集器,也有toConcurrentMap收集器,它产生一个ConcurrentMap而不是HashMap。
像toList和toSet这类特定的收集器不容许你指定内部的列表或者集合实现。当你想要将结果收集到其它类型的集合中时,你能够像下面这样使用toCollection收集器。
private static LinkedHashSet<Task> collectToLinkedHaskSet(List<Task> tasks) { return tasks.stream().collect(toCollection(LinkedHashSet::new)); }
public Task taskWithLongestTitle(List<Task> tasks) { return tasks.stream().collect(collectingAndThen(maxBy((t1, t2) -> t1.getTitle().length() - t2.getTitle().length()), Optional::get)); }
统计标签的总数
public int totalTagCount(List<Task> tasks) { return tasks.stream().collect(summingInt(task -> task.getTags().size())); }
public String titleSummary(List<Task> tasks) { return tasks.stream().map(Task::getTitle).collect(joining(";")); }
收集器最多见的使用场景之一是对元素进行分类。让我来看一下不一样的例子来理解咱们如何进行分类。
咱们看一下下面展现的例子,咱们想要根据TaskType来对全部的任务进行分类。咱们能够经过使用Collectors辅助类中的groupingBy方法来轻易地进行该项任务。你能够经过使用方法引用和静态导入来使它更加高效。
import static java.util.stream.Collectors.groupingBy; private static Map<TaskType, List<Task>> groupTasksByType(List<Task> tasks) { return tasks.stream().collect(groupingBy(Task::getType)); }
它将会产生以下的输出。
{CODING=[Task{title='Write a mobile application to store my tasks', type=CODING, createdOn=2015-07-03}], WRITING=[Task{title='Write a blog on Java 8 Streams', type=WRITING, createdOn=2015-07-04}], READING=[Task{title='Read Version Control with Git book', type=READING, createdOn=2015-07-01}, Task{title='Read Java 8 Lambdas book', type=READING, createdOn=2015-07-02}, Task{title='Read Domain Driven Design book', type=READING, createdOn=2015-07-05}]}
private static Map<String, List<Task>> groupingByTag(List<Task> tasks) { return tasks.stream(). flatMap(task -> task.getTags().stream().map(tag -> new TaskTag(tag, task))). collect(groupingBy(TaskTag::getTag, mapping(TaskTag::getTask,toList()))); } private static class TaskTag { final String tag; final Task task; public TaskTag(String tag, Task task) { this.tag = tag; this.task = task; } public String getTag() { return tag; } public Task getTask() { return task; } }
将分类器和收集器结合起来。
private static Map<String, Long> tagsAndCount(List<Task> tasks) { return tasks.stream(). flatMap(task -> task.getTags().stream().map(tag -> new TaskTag(tag, task))). collect(groupingBy(TaskTag::getTag, counting())); }
private static Map<TaskType, Map<LocalDate, List<Task>>> groupTasksByTypeAndCreationDate(List<Task> tasks) { return tasks.stream().collect(groupingBy(Task::getType, groupingBy(Task::getCreatedOn))); }
不少时候你想根据一个断言来将一个数据集分割成两个数据集。举例来讲,咱们能够经过定义一个将任务分割为两组的分割方法来将任务分割成两组,一组是在今天以前已经到期的,另外一组是其余的任务。
private static Map<Boolean, List<Task>> partitionOldAndFutureTasks(List<Task> tasks) { return tasks.stream().collect(partitioningBy(task -> task.getDueOn().isAfter(LocalDate.now()))); }
另外一组很是有用的收集器是用来产生统计信息的收集器。这可以在像int、double和long这样的原始数据类型上起到做用;而且能被用来生成像下面这样的统计信息。
IntSummaryStatistics summaryStatistics = tasks.stream().map(Task::getTitle).collect(summarizingInt(String::length)); System.out.println(summaryStatistics.getAverage()); //32.4 System.out.println(summaryStatistics.getCount()); //5 System.out.println(summaryStatistics.getMax()); //44 System.out.println(summaryStatistics.getMin()); //24 System.out.println(summaryStatistics.getSum()); //162
也有其它的变种形式,像针对其它原生类型的LongSummaryStatistics和DoubleSummaryStatistics。
你也能够经过使用combine操做来将一个IntSummaryStatistics与另外一个组合起来。
firstSummaryStatistics.combine(secondSummaryStatistics); System.out.println(firstSummaryStatistics)
private static String allTitles(List<Task> tasks) { return tasks.stream().map(Task::getTitle).collect(joining(", ")); }
import com.google.common.collect.HashMultiset; import com.google.common.collect.Multiset; import java.util.Collections; import java.util.EnumSet; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collector; public class MultisetCollector<T> implements Collector<T, Multiset<T>, Multiset<T>> { @Override public Supplier<Multiset<T>> supplier() { return HashMultiset::create; } @Override public BiConsumer<Multiset<T>, T> accumulator() { return (set, e) -> set.add(e, 1); } @Override public BinaryOperator<Multiset<T>> combiner() { return (set1, set2) -> { set1.addAll(set2); return set1; }; } @Override public Function<Multiset<T>, Multiset<T>> finisher() { return Function.identity(); } @Override public Set<Characteristics> characteristics() { return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH)); } } import com.google.common.collect.Multiset; import java.util.Arrays; import java.util.List; public class MultisetCollectorExample { public static void main(String[] args) { List<String> names = Arrays.asList("shekhar", "rahul", "shekhar"); Multiset<String> set = names.stream().collect(new MultisetCollector<>()); set.forEach(str -> System.out.println(str + ":" + set.count(str))); } }
咱们将经过使用流和收集器在Java8中编写有名的字数统计样例来结束这一节。
public static void wordCount(Path path) throws IOException { Map<String, Long> wordCount = Files.lines(path) .parallel() .flatMap(line -> Arrays.stream(line.trim().split("\\s"))) .map(word -> word.replaceAll("[^a-zA-Z]", "").toLowerCase().trim()) .filter(word -> word.length() > 0) .map(word -> new SimpleEntry<>(word, 1)) .collect(groupingBy(SimpleEntry::getKey, counting())); wordCount.forEach((k, v) -> System.out.println(String.format("%s ==>> %d", k, v))); }