Java8學習筆記(七)--Collectors

本系列文章翻譯自@shekhargulatijava8-the-missing-tutorialjava

你已經學習了Stream API可以讓你以聲明式的方式幫助你處理集合。咱們看到collect是一個將管道流的結果集到一個list中的結束操做。collect是一個將數據流縮減爲一個值的歸約操做。這個值能夠是集合、映射,或者一個值對象。你可使用collect達到如下目的:git

  • 將數據流縮減爲一個單一值:一個流執行後的結果可以被縮減爲一個單一的值。單一的值能夠是一個Collection,或者像int、double等的數值,再或者是一個用戶自定義的值對象。github

  • 將一個數據流中的元素進行分組:根據任務類型將流中全部的任務進行分組。這將產生一個Map<TaskType, List >的結果,其中每一個實體包含一個任務類型以及與它相關的任務。你也可使用除了列表之外的任何其餘的集合。若是你不須要與一任務類型相關的全部的任務,你能夠選擇產生一個Map<TaskType, Task>。這是一個可以根據任務類型對任務進行分類並獲取每類任務中第一個任務的例子。 算法

  • 分割一個流中的元素:你能夠將一個流分割爲兩組——好比將任務分割爲要作和已經作完的任務。app

Collector實際應用

爲了感覺到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(";"));
}

分類收集器

收集器最多見的使用場景之一是對元素進行分類。讓我來看一下不一樣的例子來理解咱們如何進行分類。

例子1:根據類型對任務分類

咱們看一下下面展現的例子,咱們想要根據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}]}

例子2:根據標籤分類

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;
        }
    }

例子3:根據標籤和數量對任務分類

將分類器和收集器結合起來。

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()));
}

例子4:根據任務類型和建立日期分類

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中的字數統計

咱們將經過使用流和收集器在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)));
}
相關文章
相關標籤/搜索