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

news2024/10/20 17:32:15

一、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资源不足,存在频繁的线程切换反而会降低性能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2219449.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Kamailio-Sngrep 短小精悍的利器

一个sip的抓包小工具&#xff0c;在GitHub上竟然能够积累1K的star&#xff0c;看来还是有点东西&#xff0c;当然官方的友链也是发挥了重要作用 首先送上项目地址&#xff0c;有能力的宝子可以自行查看 经典的网络抓包工具有很多&#xff0c;比如&#xff1a; Wireshark&…

vue实现文件预览和文件上传、下载、预览——多图、模型、dwg图纸、文档(word、excel、ppt、pdf)

整体思路&#xff08;模型特殊不考虑&#xff0c;别人封装不具备参考性&#xff09; 图片上传采用单独的组件&#xff0c;其他三种类型采用一个上传组件&#xff08;仅仅文件格式不同&#xff09;文件上传采用前端直接上传阿里云的方式图片预览使用elementUI自带的image预览dw…

双目标定的原理

标定目的&#xff1a;建立相机成像几何模型并矫正透镜畸变。 建立相机成像几何模型&#xff1a;计算机视觉的首要任务就是要通过拍摄到的图像信息获取到物体在真实三维世界里相对应的信息&#xff0c;于是&#xff0c;建立物体从三维世界映射到相机成像平面这一过程中的几何模…

ssm剧本杀预约系统+vue

系统包含&#xff1a;源码论文 所用技术&#xff1a;SpringBootVueSSMMybatisMysql 免费提供给大家参考或者学习&#xff0c;获取源码请私聊我 需要定制请私聊 目 录 第1章 绪论 1 1.1 课题背景 1 1.2 课题意义 1 1.3 研究内容 2 第2章 开发环境与技术 3 2.1 Java语言…

【p2p、分布式,区块链笔记 Blockchain】truffle002 unleashed_rentable_nft 项目

上一篇&#xff1a;【p2p、分布式&#xff0c;区块链笔记 Blockchain】truffle001 以太坊开发框架truffle初步实践 项目结构 项目实现了一个简单的可租赁的 NFT 系统&#xff0c;用户可以铸造和销毁 NFT。这是作者写的项目介绍&#xff08;后边看issue才发现的&#xff09;&a…

xtrabackup工具介绍、安装及模拟数据库故障使用xtrabackup工具恢复数据等操作详细说明

一、xtrabackup工具介绍 Percona XtraBackup Percona XtraBackup是一个适用于MySQL的开源热备份工具&#xff0c;它在备份期间不锁表。它可以备份InnoDB、XtraDB以及MyISAM存储引擎的表。 2.4版本支持MySQL5.1、5.5、5.6以及5.7。 它有两个实用命令&#xff0c;分别是xtraback…

LeetCode_2469. 温度转换_java

1、问题 2469. 温度转换. - 备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/convert-the-temperature/description/ 给你一个四舍五入到两位小数的非负浮点数 celsiu…

day3:管道,解压缩,vim

一&#xff0c;管道&#xff08;|&#xff09; 引入 当我们要将本次命令结果作为下次命令参数时就可以用到&#xff0c;极大的简化了操作。 比如&#xff1a;head -5 文件| tail -1&#xff1a;表示显示第五行这就是管道的魅力 概述 管道符&#xff1a;| 作用&#xff1a…

计算机网络——可靠数据传输原理

变成什么状态取决于当时发生的事件 在停止等待协议中只用1位来表示序号0和1——新的和旧的 在这里插入图片描述

IT运维的365天--017 如何在两台Linux服务器之间快速传输文件夹(同时设置免密)

前情提要(两台Linux服务器之间传输批量文件夹): 两台都是外网服务器,都是Linux系统(CentOS),都安装了宝塔,用于搭建巨量的静态网站,由于A服务器准备不要了,所以要在A服务器转移几百个静态网站到B服务器。 Linux下scp单命令传输文件夹测试: 准备工作,先测试转移一…

【Kafka】Kafka Producer的缓冲池机制原理

如何初始化的bufferPool的 在初始化的时候 初始化BufferPool对象 // 设置缓冲区 this.accumulator new RecordAccumulator(xxxxx,其他参数,new BufferPool(this.totalMemorySize, config.getInt(ProducerConfig.BATCH_SIZE_CONFIG), metrics, time, PRODUCER_METRIC_GROUP_N…

免费送源码:Java+SpringBoot+MySQL SpringBoot珠宝店客户关系管理系统 计算机毕业设计原创定制

摘 要 随着计算机技术的发展&#xff0c;特别是计算机网络技术与数据库技术的发展&#xff0c;使用人们的生活与工作方式发生了很大的改观。本课题研究的珠宝店客户关系管理系统&#xff0c;主要功能模块包括系统用户&#xff0c;部门类别&#xff0c;职务类别&#xff0c;外出…

【ssh】Mac 使用 ssh 连接阿里云报错:Connection reset by 8.155.1.xxx port 22

Mac 使用 ssh 连接阿里云报错&#xff1a;Connection reset by 8.155.1.xxx port 22 问题描述解决办法 问题描述 Connection reset by 8.155.1.xxx port 22解决办法 关掉代理 VPN

SpringCloudAlibaba升级手册

目录 1. 版本对照 版本现状 SpringCloud与AlibabaCloud对应版本 Springboot与Elasticsearch版本对应 2. openfeign问题 问题 解决方案 3. Feign请求问题 问题 解决方法 4. Sentinel循环依赖 问题 解决方案 5. bootstrap配置文件不生效 问题 解决方案 6. Nacos的…

visio导出pdf公式变形问题杂谈

其实不会变形。 我自己的情况是直接用edge PDF阅读器打开pdf看到的是公式有变形&#xff08;常见是字体、形状变了&#xff09;&#xff0c;但换一个pdf阅读器如adobe的就是正常的了 不过大家一般是用edge pdf阅读器直接打开查看&#xff0c;所以通过visio打印的方式导出pdf可…

DNS 与 ICMP

DNS(Domain Name System)快速了解 DNS 是一整套从域名映射到 IP 的系统 DNS 背景 TCP/IP 中使用 IP 地址和端口号来确定网络上的一台主机的一个程序. 但是 IP 地址不方便记忆 于是人们发明了一种叫主机名的东西, 是一个字符串, 并且使用 hosts 文件来描述主机 名和 IP 地…

【Hive】8-Hive性能优化及Hive3新特性

Hive性能优化及Hive3新特性 Hive表设计优化 Hive查询基本原理 Hive的设计思想是通过元数据解析描述将HDFS上的文件映射成表 基本的查询原理是当用户通过HQL语句对Hive中的表进行复杂数据处理和计算时&#xff0c;默认将其转换为分布式计算 MapReduce程序对HDFS中的数据进行…

基于排名的股票预测的关系时态图卷积网络(RT-GCN)

“ 为了充分利用股票之间的关系&#xff0c;获得最高收益&#xff0c;提出了一种关系时态图卷积网络(RT-GCN)。” 标题&#xff1a;Relational Temporal Graph Convolutional Networks for Ranking-Based Stock Prediction 链接&#xff1a;https://ieeexplore.ieee.org/do…

Android15之解决gdb:Remote register badly formatted问题(二百三十六)

简介&#xff1a; CSDN博客专家、《Android系统多媒体进阶实战》一书作者 新书发布&#xff1a;《Android系统多媒体进阶实战》&#x1f680; 优质专栏&#xff1a; Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a; 多媒体系统工程师系列【…

2024全国大数据与计算智能挑战赛火热报名中!

一年一度的 全国大数据与计算智能挑战赛震撼来袭&#xff01; 报名速通&#xff1a; https://www.datafountain.cn/special/BDSSF2024 大数据与决策&#xff08;国家级&#xff09;实验室连续三年组织发起全国大数据与计算智能挑战赛&#xff0c;旨在深入挖掘大数据应用实践中亟…