Bootstrap

Java 8 Stream API:从基础到高级,掌握流处理的艺术

一、Stream(流)基本介绍

Java 8 API 添加了一个新的抽象称为Stream(流),可以让你以一种声明的方式处理数据,这种风格将要处理的元素集合看做一种流,元素流在管道中传输,并在管道中间的节点上经过中间操作(intermediate operation)的处理(如:筛选,排序,聚合等),最后由最终操作(terminal operation)得到前面处理的结果。

Stream(流)使用一种类似于SQL语句从数据库查询数据的直观方式来提供一种对Java集合运算和表达的高阶抽象。

  • 元素是特定类型的对象,形成一个队列;
  • Java中的Stream并不会存储元素,而是按需计算;
  • 数据源可以是集合,数组,I/O channel,产生器generator等;
  • 很多中间操作的方法返回类型就是Stream,因此可以直接连接起来,如下图:

在这里插入图片描述

  • 流的操作不会改变原集合,会生产新的集合,List<String> newList = list.stream().xxx

Stream API 可以极大提高Java程序员的生产力,让程序员写出高效率、干净、整洁的代码。

二、Stream(流)的常用方法

// 准备测试类和数据
public class User{
    String name;
    Integer age;
}
List<User> userList = new ArrayList<>();
userList.add(new User("孙悟空", 500));
userList.add(new User("沙悟净", 600));
userList.add(new User("猪八戒", 400));
List<String> letterList = Arrays.asList("a", "", "b", "c", "d", "", "e");
List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5);

1、filter(element -> boolean表达式)

  • 过滤元素,将符合boolean表达式的元素保存下来。
// 过滤字母集合,过滤掉空字符串,结果为:["a","b","c","d","e"]
List<String> newLetterList = letterList.stream().filter(str -> !str.isEmpty()).collect(Collectors.toList());

2、distinct

  • 去重,这个方法是元素自身的equals方法来判断其元素是否相等。
userList = userList.stream().distinct().collect(Collectors.toList);

如果这里不重写User类的equals方法,相同的数据不会被处理。

3、sorted() / sorted((T, T) -> int)

  • 对流中的元素进行排序,若流中元素的类有自己的排序规则(即实现了Comparable接口)可直接sorted(),否则需要用sorted((T,T) -> int)说明排序规则。
// 根据年龄大小来排序
userList = userList.stream().sorted((u1,u2) -> u1.getAge() - u2.getAge()).collect(Collectors.toList());

// 也可直接替换为方法引用
userList = userList.stream().sorted(Comparator.comparingInt(User::getAge)).collect(Collectors.toList());

4、min、max

  • min() 和 max() 方法,用于查找流中的最小值和最大值。这些方法返回一个 Optional 对象,包含流中的最小或最大元素。

min(Comparator<? super T> comparator) 方法接受一个比较器作为参数,用于定义元素之间的顺序。它会遍历整个流,并返回其中的最小元素。如果流为空,则返回一个空的 Optional 对象。

max(Comparator<? super T> comparator) 方法与 min() 方法类似,唯一的区别是它返回流中的最大元素。同样地,如果流为空,则返回一个空的 Optional 对象。

以下是一个示例,展示如何使用 min() 方法找到一个字符串流中的最短字符串:

List<String> strings = Arrays.asList("apple", "banana", "cherry", "date");
Optional<String> shortestString = strings.stream().min((s1, s2) -> s1.length() - s2.length());
if (shortestString.isPresent()) {
    System.out.println("Shortest string: " + shortestString.get());
} else {
    System.out.println("No strings in the list.");
}

在这个例子中,我们首先创建了一个包含四个字符串的列表。然后,使用 stream() 方法将其转换为一个流。接着,调用 min() 方法并传入一个比较器,用于比较两个字符串的长度。最后,使用 isPresent() 方法检查是否找到了最短字符串,并打印结果。

5、summaryStatistics

  • summaryStatistics()它可以对数值型数据流进行统计汇总。这个方法返回一个 IntSummaryStatistics、LongSummaryStatistics 或 DoubleSummaryStatistics 对象,具体取决于流中元素的类型。

以下是一些常见的统计信息:

  • getCount(): 返回流中元素的数量。
  • getSum(): 返回流中所有元素的总和。
  • getAverage(): 返回流中所有元素的平均值。
  • getMin(): 返回流中最小的元素。
  • getMax(): 返回流中最大的元素。

这些方法可以帮助你快速获取流中数值型数据的基本统计信息。

例如,如果你有一个 Stream 对象,想要计算其中所有元素的平均值,你可以这样做:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
double average = numbers.stream()
                       .mapToDouble(i -> i)
                       .summaryStatistics()
                       .getAverage();
System.out.println("Average: " + average);

在这个例子中,我们首先将一个 List 转换为一个 Stream。然后,我们使用 mapToDouble() 方法将每个元素转换为一个 double 类型的值。接着,调用 summaryStatistics() 方法来计算流中所有元素的统计信息。最后,使用 getAverage() 方法获取平均值并打印出来。

同样地,如果你有一个 Stream 对象,想要计算其中所有元素的总和和最小值,你可以这样做:

List<Double> numbers = Arrays.asList(1.0, 2.0, 3.0, 4.0, 5.0);
DoubleSummaryStatistics stats = numbers.stream()
                                      .summaryStatistics();
double sum = stats.getSum();
double min = stats.getMin();
System.out.println("Sum: " + sum);
System.out.println("Min: " + min);

在这个例子中,我们首先将一个 List 转换为一个 Stream。然后,直接调用 summaryStatistics() 方法来计算流中所有元素的统计信息。最后,使用 getSum() 和 getMin() 方法获取总和和最小值并打印出来。

6、limit(long n)

  • 保留处理结果中的前n个元素。
userList = userList.stream().limit(1).collect(Collectors.toList());

7、skip(long n)

  • 去除(跳过)处理结果中的前n个元素。
// 从处理结果中保留2个元素,再从保留的2个元素中去除第1个元素
userList = userList.stream().limit(2).skip(1).collect(Collectors.toList());

// 从处理结果中去除2个元素后,再保留1个元素
userList = userList.stream().skip(2).limit(1).collect(Collectors.toList());

8、map(T -> R)

  • 将流中的每一个元素映射为R。

map方法接收一个lambda表达式,这个表达式是一个函数,输入类型是集合元素的类型,输出类型是任意类型,即你可以选择将元素映射为任意类型,并对映射后的值做下一步处理。

// 将集合中的每个元素+2,输出结果:[3,4,5,6,7]
numberList = numberList.stream().map(i -> i+2).collect(Collectors.toList());
// 将用户的name、age分别保存到新集合中
List<String> nameList = list.stream().map(Person::getName).collect(Collectors.toList());
List<Integer> ageList = list.stream().map(Person::getAge).collect(Collectors.toList());
// 将用户的属性取出做进一步处理
userList = userList.stream().map(u -> {
	u.setAge(u.getAge() + 1);
	u.setName("孙悟空".equals(p.getName()) ? "悟空" : "西游人物");
	return p;
}).collect(Collectors.toList());

9、flatMap(T -> Stream)

  • flatMap的用法和map类似,它们都接受一个函数作为参数,用于对流中的每个元素进行转换。

map和flatMap的区别:

  • map()操作将每个元素转换成一个新元素,并将所有这些新生成的元素收集到一个新的流中。
  • flatMap()操作将每个元素转换成一个新的流,并将所有这些新生成的流合并成一个单一的流。

flatMap()和map()之间还有一个重要的区别,那就是flatMap()支持处理包含嵌套数据结构的流。

在Java中,如果你有一个泛型类型中的数据本身也是一个泛型类型,例如List<List>,那么使用map()操作时,你可能会遇到一些困难。因为map()操作只会对最外层的元素进行转换,而不会深入到嵌套的数据结构中。

但是,flatMap()操作可以很好地处理这种情况。它可以将每个元素转换成一个流,并将所有这些流合并成一个单一的流。这样,你就可以在处理嵌套数据结构时使用flatMap()操作。

示例:首先使用stream()方法将userPlus转换为一个流,由于userPlus是一个嵌套的集合,所以我们需要使用flatMap()操作来将其展平成一个单一的流,接下来,我们使用map()操作来从每个User对象中提取出其name属性,并将结果转换成一个新的流,再使用distinct()操作来去除重复的名字,最后,我们使用collect()操作将流收集到一个新的List中,这个列表包含了所有唯一的用户名。

List<User> user1 = new ArrayList<>();
user1.add(new User("A",23));
user1.add(new User("B",23));

List<User> user2 = new ArrayList<>();
user2.add(new User("C",23));
user2.add(new User("D",23));

List<List<User>> userPlus = new ArrayList<>();
userPlus.add(user1);
userPlus.add(user2);

// 最后输出的结果是:["A","B","C","D"]
List<String> nameList = userPlus.stream().flatMap(t -> t.stream()).map(t -> t.getName()).distinct().collect(Collectors.toList());

10、reduce((T, T) -> T) / reduce(T, (T, T) -> T)

  • reduce操作可以用来将流中的元素组合成一个单一的结果,可以用来执行各种聚合操作。

这个操作有两种重载形式:

  • reduce((T, T) -> T):这种形式的reduce接受一个二元操作符(即一个函数),该函数将两个元素合并成一个新元素。这个过程会一直重复,直到流中的所有元素都被合并成一个单一的结果。
  • reduce(T, (T, T) -> T): 这种形式的reduce()除了接受一个二元操作符外,还接受一个初始值。如果流为空,初始值将直接作为结果返回;否则,初始值将与流中的第一个元素合并,产生一个新的中间结果,然后再与流中的下一个元素合并,以此类推,直到流中的所有元素都被处理完毕。
// 计算年龄总和
int sum = personList.stream().map(Person::getAge).reduce(0, (a, b) -> a + b);
// 计算年龄总和
int sum = personList.stream().map(Person::getAge).reduce(0, Integer::sum);

// 价格使用BigDecimal防止精度丢失,将所有商品的价格累加
BigDecimal totalPrice = goodList.stream().map(GoodsCode::getPrice).reduce(BigDecimal.ZERO, BigDecimal::add);

11、anyMatch(T -> boolean表达式)

  • 流中是否有元素满足这个Boolean表达式
// 集合中是否存在一个元素的age等于500
boolean b = userList.stream().anyMatch(u-> u.getAge() == 500);

12、allMatch(T -> boolean) 和 noneMatch(T -> boolean)

  • allMatch(T -> boolean),即流中所有元素是否都满足boolean表达式。
  • noneMatch(T -> boolean),即是否流中没有一个元素满足boolean表达式。

可以配合filter一起使用。

示例:下面示例中准备了一个tagList集合中有标签A、B、C,3个元素,还有一个tagMap,其中key是序号,value是标签集合,现想获取tagMap中对应标签集合(value)和tagList集合完全不匹配的元素对应的序号(key)。

List<String> tagList = new ArrayList<>();
tagList.add("A");
tagList.add("B");
tagList.add("C");

Map<Integer, List<String>> tagMap = new HashMap<>();
List<String> list1 = new ArrayList<>();
list1.add("A");
list1.add("D");
List<String> list2 = new ArrayList<>();
list2.add("B");
List<String> list3 = new ArrayList<>();
list3.add("D");
tagMap.put(1, list1);
tagMap.put(2, list2);
tagMap.put(3, list3);
// 返回结果:[3]
List<Integer> collect = map.entrySet().stream().filter(entry -> list.stream().noneMatch(s -> entry.getValue().contains(s))).map(Map.Entry::getKey).collect(Collectors.toList());

13、count()

  • 返回流中元素的个数,返回long型。
int countOfAdult=persons.stream()
                   .filter(p -> p.getAge() > 18).map(person -> new Adult(person))
                       .count();

14、forEach()

  • 普通for循环或者增强for循环,break跳出整个循环,continue结束本次循环。Stream的forEach处理集合时需要使用关键字return跳出本次循环,并执行下次遍历(不能跳出整个流的forEach循环)。

它接受一个消费函数作为参数,该函数将被应用于流中的每个元素。

// 示例 1:打印流中的所有元素
letterList.stream().map(String::toUpperCase).forEach(System.out::println);

// 示例 2:将字符串流中的所有元素转换为大写并打印
letterList.stream().map(String::toUpperCase).forEach(System.out::println);

forEach()操作是终端操作之一,它不能被用于中间操作。也就是说,调用forEach()后,你不能再对流进行其他操作。另外,forEach()操作通常用于打印或其他副作用,而不是构建新的流或集合。

15、peek()

  • peek() 是一个中间操作方法,它允许你在不影响流的主要处理逻辑的情况下,查看或使用流中的每个元素。这个方法可以用来进行一些调试或日志记录等操作。

peek() 方法的签名如下:Stream<T> peek(Consumer<? super T> action)

其中,action 是一个 Consumer 函数,用于对流中的每个元素进行操作。这个函数不会改变流中的元素,也不会返回任何值。

以下是一个简单的示例,演示如何使用 peek() 方法来打印流中的每个元素:

List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape");

fruits.stream()
    .filter(f -> f.length() > 5)
    .peek(System.out::println)
    .collect(Collectors.toList());

在这个例子中,我们首先创建了一个包含四个水果的列表。然后,使用 stream() 方法将其转换为一个流。接着,调用 filter() 方法来过滤出长度大于 5 的水果。然后,使用 peek() 方法在处理流中的元素时打印每个元素。最后,使用 collect() 方法将剩余的元素收集到一个新的列表中。

注意事项:

  • peek()不会改变流的结果:无论你在 peek() 方法中做了什么操作,流的最终结果都不会受到影响。
  • peek()可能会被无限次调用:如果你在流的中间操作中使用了 peek(),那么在每次中间操作时,peek() 都会被调用。
  • peek()是非短路操作:与 forEach() 不同,peek() 不是终端操作,它不会使流处理短路。也就是说,所有的中间操作都会被执行完毕,包括 peek()。
  • peek()可能会影响流的性能:如果在 peek() 方法中执行了非常耗时的操作,那么可能会影响流的整体性能。

peek() 主要适用于以下场景:

  • 调试:可以用来打印流中的元素,帮助你理解流的处理过程。
  • 日志记录:可以用来记录流中的元素,例如在处理大数据集时,记录每个处理的元素。
  • 副作用操作:可以用来执行一些副作用操作,例如更新数据库或发送通知等。

需要注意的是,虽然 peek() 提供了一个方便的方式来查看流中的元素,但它不应该被用于实际的业务逻辑中。因为它的主要目的是为了调试和日志记录,而不是处理流的主要逻辑。

16、Stream.iterate

java.util.stream.Stream下共有两个iterate,都是 Java 8 中引入的 Stream API 方法,用于生成无限流。它们的主要区别在于第二个方法允许你指定一个条件来决定何时停止生成元素。

方法一:

iterate(T seed, final UnaryOperator<T> f)

这个方法接受两个参数,会不断地应用函数 f 到前一个元素上,生成一个无限流。

  • seed:流的初始元素。
  • f:一个函数,用于将前一个元素转换为下一个元素。

例如,我们可以使用这个方法来生成自然数流:

// 这将输出从 1 到 10 的自然数。
Stream.iterate(1, x -> x + 1).limit(10).forEach(System.out::println);

方法二:

iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> f)

这个方法接受三个参数,会不断地应用函数 f 到前一个元素上,并使用 hasNext 函数来检查是否应该继续生成下一个元素。如果 hasNext 返回 false,则流将结束。

  • seed:流的初始元素。
  • hasNext:一个谓词函数,用于判断是否应该继续生成下一个元素。
  • f:一个函数,用于将前一个元素转换为下一个元素。

例如,我们可以使用这个方法来生成小于等于 10 的自然数流:

// 这将输出从 1 到 10 的自然数。
Stream.iterate(1, x -> x <= 10, x -> x + 1).forEach(System.out::println);

总的来说,第二个方法提供了更灵活的方式来控制流的生成过程,可以根据特定的条件来决定何时停止生成元素。

在执行 Stream.iterate 时并没有生成具体数据,只是产生了一个流,只有在使用时才会有数据。

三、收集方法collect()详解

收集流中元素的方法,传参是一个收集器接口Collectors,下面是Collectors中的方法:

在这里插入图片描述
下面我们逐个介绍这些方法的使用。

1、将流中的元素放到集合中

  • .collect(Collectors.toCollection(Supplier<C> collectionFactory));
//源码    
public static <T, C extends Collection<T>> Collector<T, ?, C> toCollection(Supplier<C> collectionFactory) {
        return new CollectorImpl<>(collectionFactory, Collection<T>::add,
                                   (r1, r2) -> { r1.addAll(r2); return r1; },
                                   CH_ID);
}
//使用示例
LinkedList<Integer> collect = userList.stream().map(User::getAge).collect(Collectors.toCollection(LinkedList::new));
  • toList()、toSet()
List<Integer> list = userList.stream().map(User::getAge).collect(Collectors.toList());
 
Set<String> nameSet = userList.stream().map(User::getName).collect(Collectors.toSet());

2、分组

  • groupingBy:将处理后的元素进行分组,得到一个Map集合,它有三个重载方法:
public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function classifier) {
	return groupingBy(classifier, toList());
}

public static <T, K, A, D> Collector<T, ?, Map<K, D>> groupingBy(Function classifier, Collector downstream) {
	return groupingBy(classifier, HashMap::new, downstream);
}

public static <T, K, D, A, M extends Map<K, D>> Collector<T, ?, M> groupingBy(Function classifier, Supplier mapFactory, Collector downstream) {
   // ......
}

第一个方法只需一个分组参数classifier,内部自动将结果保存到一个map中,每个map的键为 ‘?’ 类型(即classifier的结果类型),值为一个list,这个list中保存在属于这个组的元素。

但是它实际是调用了第二个方法-- Collector 默认为list。

而第二个方法实际是调用第三个方法,默认Map的生成方式为HashMap。

第三个方法才是真实完整的分组逻辑处理。

下面是一些实际的案例:

//数据准备
@Data
@AllArgsConstructor
public class Books {

    private Integer id;

    private Integer num;

    private String name;

    private Double price;

    private String category;
}

Books book1 = new Books(1,100,"Java入门",60.0,"互联网类") ;
Books book2 = new Books(2,200,"Linux私房菜",100.0,"互联网类") ;
Books book3 = new Books(3,200,"Docker进阶",70.0,"互联网类") ;
Books book4 = new Books(4,600,"平凡的世界",200.0,"小说类") ;
Books book5 = new Books(5,1000,"白鹿原",190.0,"小说类") ;
List<Books> booksList = Lists.newArrayList(book1,book2,book3,book4,book5);
  • case1:按照某个属性分组,即以该属性为Map集合的key,把这个属性相同的对象放在一个List集合中做为value。
//按照category分类
Map<String,List<Books>> map = booksList.stream().collect(Collectors.groupingBy(Books::getCategory));
// 运行结果
{
互联网类=[Books(id=1, num=100, name=Java入门, price=60.0, category=互联网类), Books(id=2, num=200, name=Linux私房菜, price=100.0, category=互联网类), Books(id=3, num=200, name=Docker进阶, price=70.0, category=互联网类)],
小说类=[Books(id=4, num=600, name=平凡的世界, price=200.0, category=小说类), Books(id=5, num=1000, name=白鹿原, price=190.0, category=小说类)]
}
  • case2: 按照某几个属性拼接分组
Map<String,List<Books>> map = booksList.stream().collect(Collectors.groupingBy(t -> t.getCategory() +"_" + t.getName()));
// 运行结果
{
互联网类_Linux私房菜=[Books(id=2, num=200, name=Linux私房菜, price=100.0, category=互联网类)],
小说类_平凡的世界=[Books(id=4, num=600, name=平凡的世界, price=200.0, category=小说类)],
互联网类_Docker进阶=[Books(id=3, num=200, name=Docker进阶, price=70.0, category=互联网类)],
互联网类_Java入门=[Books(id=1, num=100, name=Java入门, price=60.0, category=互联网类)],
小说类_白鹿原=[Books(id=5, num=1000, name=白鹿原, price=190.0, category=小说类)]
}
  • case3: 按照不同的条件分组
//不同条件下,使用不同的key
Map<String,List<Books>> map = booksList.stream()
	.collect(Collectors.groupingBy(t -> {
	    if(t.getNum() > 500){
	        return "数量充足";
	    }else{
	        return "数量较少";
	    }
	}));
// 运行结果
{
数量充足=[Books(id=4, num=600, name=平凡的世界, price=200.0, category=小说类), Books(id=5, num=1000, name=白鹿原, price=190.0, category=小说类)],
数量较少=[Books(id=1, num=100, name=Java入门, price=60.0, category=互联网类), Books(id=2, num=200, name=Linux私房菜, price=100.0, category=互联网类), Books(id=3, num=200, name=Docker进阶, price=70.0, category=互联网类)]
}
  • case4:实现多级分组,即由双参数版本的Collectors.groupingBy,对由第一个参数分类后的结果再进行分类,此时结果类型。
//接case3,想先按照类别分组,再给每个组按照数量再分一次
Map<String,Map<String,List<Books>>> map = booksList.stream()
	.collect(Collectors.groupingBy(t -> t.getCategory(), Collectors.groupingBy( t -> {
	    if(t.getNum() > 100){
	        return "数量充足";
	    }else{
	        return "数量较少";
	    }
	})));
// 运行结果
{
互联网类={数量充足=[Books(id=2, num=200, name=Linux私房菜, price=100.0, category=互联网类), Books(id=3, num=200, name=Docker进阶, price=70.0, category=互联网类)], 数量较少=[Books(id=1, num=100, name=Java入门, price=60.0, category=互联网类)]}, 
小说类={数量充足=[Books(id=4, num=600, name=平凡的世界, price=200.0, category=小说类), Books(id=5, num=1000, name=白鹿原, price=190.0, category=小说类)]}
}
  • case5:分组后,统计每个分组中元素的个数,Map集合的value类型为long型
Map<String,Long> map = booksList.stream().collect(Collectors.groupingBy(Books::getCategory,Collectors.counting()));
// 运行结果
{互联网类=3, 小说类=2}
  • case6:分组后,统计每个分组中元素的某属性的总和
Map<String,Integer> map = booksList.stream().collect(Collectors.groupingBy(Books::getCategory,Collectors.summingInt(Books::getNum)));
// 运行结果
{互联网类=500, 小说类=1600}
  • case7: 加比较器取某属性最值
Map<String,Books> map3 = booksList.stream().collect(Collectors.groupingBy(Books::getCategory, Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparingInt(Books::getNum)), Optional::get)));
// 运行结果
{互联网类=Books(id=2, num=200, name=Linux私房菜, price=100.0, category=互联网类), 小说类=Books(id=5, num=1000, name=白鹿原, price=190.0, category=小说类)}
  • case8:联合其他收集器
Map<String, Set<String>> map2 = booksList.stream().collect(Collectors.groupingBy(Books::getCategory,Collectors.mapping(t->t.getName(),Collectors.toSet())));
// 运行结果
{互联网类=[Linux私房菜, Docker进阶, Java入门], 小说类=[平凡的世界, 白鹿原]}  
  • groupingByConcurrent

返回一个并发Collector收集器对T类型的输入元素执行"group by"操作, 也有三个重载的方法, 其使用与groupingBy 基本相同。

  • partitioningBy

该方法将流中的元素按照给定的校验规则的结果分为两个部分,放到一个map中返回,map的键是Boolean类型,值为元素的集合。

//源码(两个重载方法)
    public static <T> Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) {
        return partitioningBy(predicate, toList());
    }
 
    public static <T, D, A> Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,
                                                    Collector<? super T, A, D> downstream) {
        ......
    }

//从上面的重载方法中可以看出,partitioningBy 与 groupingBy 类似, 只不过partitioningBy 生成的map的key的类型限制只能是Boolean类型。

//示例
Stream<Student> stream = studentList.stream();
 
Map<Boolean, List<Student>> m4 = stream.collect(Collectors.partitioningBy(stu -> stu.getScore() > 60));
 
Map<Boolean, Set<Student>> m5 = stream.collect(Collectors.partitioningBy(stu -> stu.getScore() > 60, Collectors.toSet()));

3、将流中的元素放到Map中

  • .collect(Collectors.toMap(x,x,x))

toMap方法是根据给定的键生成器和值生成器生成的键和值保存到一个map中返回,键和值的生成都依赖于元素,可以指定出现重复键时的处理方案和保存结果的map。

//源码(三个重载方法)
public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function keyMapper,Function valueMapper) {
    return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}
 
public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function keyMapper,Function valueMapper,BinaryOperator mergeFunction) {
    return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}
 
public static <T, K, U, M extends Map<K, U>> Collector<T, ?, M> toMap(Function keyMapper,Function valueMapper,BinaryOperator mergeFunction,Supplier mapSupplier){
    ......
}

//三个重载的方法,最终都是调用第三个方法来实现, 第一个方法中默认指定了key重复的处理方式和map的生成方式; 而第二个方法默认指定了map的生成方式,用户可以自定义key重复的处理方式。

//示例
Map<Integer, Student> map1 = stream.collect(Collectors.toMap(Student::getId, v->v));
Map<Integer, String> map2 = stream.collect(Collectors.toMap(Student::getId, Student::getName, (a, b)->a));
Map<Integer, String> map3 = stream.collect(Collectors.toMap(Student::getId, Student::getName, (a, b)->a, HashMap::new));

4、元素拼接joining

  • joining() : 没有分隔符和前后缀,直接拼接
  • joining(CharSequence delimiter) : 指定元素间的分隔符
  • joining(CharSequence delimiter,CharSequence prefix, CharSequence suffix): 指定分隔符和整个字符串的前后缀。
String s = list.stream().map(Person::getName).collect(joining());
//结果:jackmiketom

String s = list.stream().map(Person::getName).collect(joining(","));
//结果:jack,mike,tom

String s = list.stream().map(Person::getName).collect(joining(",", "name:", "1"));
//结果:name:jack1,name:mike1,name:tom1

5、类型转换

mapping:这个映射是首先对流中的每个元素进行映射,即类型转换,然后再将新元素以给定的Collector进行归纳。 类似与Stream的map方法。

collectingAndThen:在归纳动作结束之后,对归纳的结果进行再处理。

Stream<Student> stream = studentList.stream();
 
List<Integer> idList = stream.collect(Collectors.mapping(Student::getId, Collectors.toList()));
 
Integer size = stream.collect(Collectors.collectingAndThen(Collectors.mapping(Student::getId, Collectors.toList()), o -> o.size()));

6、聚合

  • counting: 同 stream.count()
  • minBy: 同stream.min()
  • maxBy: 同stream.max()
  • summingInt:
  • summingLong:
  • summingDouble:
  • averagingInt:
  • averagingLong:
  • averagingDouble:
Long count = stream.collect(Collectors.counting());
stream.count();
stream.collect(Collectors.minBy((a,b)-> a.getId() - b.getId()));
stream.min(Comparator.comparingInt(Student::getId));
stream.collect(Collectors.summarizingInt(Student::getId));
stream.collect(Collectors.summarizingLong(Student::getTimeStamp));
stream.collect(Collectors.averagingDouble(Student::getScore));

7、reducing

reducing方法有三个重载方法,其实是和Stream里的三个reduce方法对应的,二者是可以替换使用的,作用完全一致,也是对流中的元素做统计归纳作用。

//源码(三个重载方法)
public static <T> Collector<T, ?, Optional<T>> reducing(BinaryOperator<T> op) {
    ......
}
 
public static <T> Collector<T, ?, T> reducing(T identity, BinaryOperator<T> op) {
    ......
}
 
public static <T, U> Collector<T, ?, U> reducing(U identity,Function mapper, BinaryOperator<U> op) {
    ......
}


//示例
List<String> list2 = Arrays.asList("123","456","789","qaz","wsx","edc");
 
Optional<Integer> optional = list2.stream().map(String::length).collect(Collectors.reducing(Integer::sum));
 
Integer sum1 = list2.stream().map(String::length).collect(Collectors.reducing(0, Integer::sum));
 
Integer sum2 = list2.stream().limit(4).collect(Collectors.reducing(0, String::length, Integer::sum));


//拓展:实际运用中,可能会用到比较复杂的 groupingBy、mapping、toMap 嵌套、组合使用,进行多级分组处理数据。如: 
Stream<Student> stream = studentList.stream();
// 根据score分组,并提取ID作为集合元素
Map<Double, List<Integer>> map1 = stream.collect(Collectors.groupingBy(Student::getScore, Collectors.mapping(Student::getId, Collectors.toList())));
// 根据score分组, 并将ID和name组成map作为元素
Map<Double, Map<Integer, String>> map2 = stream.collect(Collectors.groupingBy(Student::getScore, Collectors.toMap(Student::getId, Student::getName)));
// 先根据score分组,再根据name进行二次分组
Map<Double, Map<String, List<Student>>> map3 = stream.collect(Collectors.groupingBy(Student::getScore, Collectors.groupingBy(Student::getName)));

//当然也可以根据我们想要的条件,设置分组的组合条件,只需要替换 Student::getScore ,换成我们想要的条件即可, 如: 
Map<String, List<Integer>> map3 = stream.collect(Collectors.groupingBy(stu -> {
    if (stu.getScore() > 60) {
        return "PASS";
    } else {
        return "FAIL";
    }
}, Collectors.mapping(Student::getId, Collectors.toList())));
//按照这种思路,我们可以随意处理stream中的元素成我们想要的结果数据。

四、并行流parallelStream

1、parallelStream

  • 每个Stream都有两种模式:顺序执行和并行执行,调用parallelStream()和stream()方法,返回的都是一个流;

  • 并行流的创建可以通过:xx.parallelStream() 或 xx.stream().parallel();

顾名思义,当使用顺序方式去遍历时,每个item读完后再读下一个item。而使用并行去遍历时,数组会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。

  • parallelStream原理:
List originalList = someData;
split1 = originalList(0, mid);//将数据分小部分
split2 = originalList(mid,end);
new Runnable(split1.process());//小部分执行操作
new Runnable(split2.process());
List revisedList = split1 + split2;//将结果合并

大家对hadoop有稍微了解就知道,里面的 MapReduce 本身就是用于并行处理大数据集的软件框架,其 处理大数据的核心思想就是大而化小,分配到不同机器去运行map,最终通过reduce将所有机器的结果结合起来得到一个最终结果,与MapReduce不同,Stream则是利用多核技术可将大数据通过多核并行处理,而MapReduce则可以分布式的。

  • parallelStream默认的并发线程数比CPU处理器的数量少1个(最优策略是每个CPU处理器分配一个线程,然而主线程也算一个线程)
// 获取当前机器CPU处理器的数量
System.out.println(Runtime.getRuntime().availableProcessors());// 输出 6
// parallelStream默认的并发线程数
System.out.println(ForkJoinPool.getCommonPoolParallelism());// 输出 5
// 设置全局并行流并发线程数
//这是全局配置,会影响所有的并行流
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "3");
  • parallelStream是线程不安全的
List<Integer> listStream = new ArrayList<>();
List<Integer> listParallelStream = new ArrayList<>();
IntStream.range(0, 1000).forEach(t -> listStream.add(t));
IntStream.range(0, 1000).parallel().forEach(listParallelStream::add);
System.out.println("listStream size:" + listStream.size());
System.out.println("listParallelStream size:" + listParallelStream.size());
// 输出结果
listStream size:1000
listParallelStream size: 969
  • 使用并行流遍历打印一个集合元素,并输出当前线程,可以看到线程抬头是ForkJoinPool,且遍历输出的元素是无序的。

在这里插入图片描述

  • 并发不一定就能提高性能,CPU资源不足,存在频繁的线程切换反而会降低性能。
;