Java8新特性Stream入门教程

1 篇文章 0 订阅
1 篇文章 0 订阅

目录

Stream是什么

怎么创建Stream

对Stream进行操作

filter

map

distinct

limit

skip

sorted

forEach

peek

reduce

Collect

match

进阶

顺序流,并行流

惰性的中间操作

操作的执行顺序

总结


 

Stream是什么

要回答这个问题,我们先来看一下相关的JavaDoc是怎么描述的:

A sequence of elements supporting sequential and parallel aggregate operations

“支持顺序和并行的聚合操作的元素序列”,乍看之下,感觉是和集合类似的数据结构,存放一组数据,支持某些特定操作,其实不然。先来看一个简单的例子:

// 创建一个流
List<String> list = Arrays.asList("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
list.stream().filter(item -> item.length() < 3) // 过滤掉长度大于等于3的
        .distinct() // 去除重复元素
        .map(String::toLowerCase) // 对每个元素进行转换
        .sorted() // 排序
        .forEach(item -> System.out.print(item + " ")); // 遍历每个元素

// 输出结果:a1 a2 b1 b2 

从这个例子可以看出,Stream是对集合对象功能的增强,它是有关算法和计算的,专注于对集合对象进行各种便利、高效的操作。借助Lambda表达式,极大的提高了编程效率和代码可读性。

Stream 就如同一个迭代器(Iterator),单向,不可往复。使用Stream对集合中的元素进行操作,感觉就像是灌装啤酒的流水线,一个个空酒瓶子(相当于集合中的元素)被运送到各个操作单元,灌酒/压瓶盖/贴标签/剔除有问题的瓶子(相当于对元素的操作),最后打包装箱堆放到仓库。

 

怎么创建Stream

常用的创建流的方式有三种。

1.使用Stream的静态方法创建流

        // 方式1:
        Stream<String> stream1 = Stream.of("A1", "B1", "A2", "B2", "A10", "B10");
        stream1.forEach(item -> System.out.print(item + " "));
        // 输出结果:A1 B1 A2 B2 A10 B10

        // 方式2:
        Stream<Integer> stream2 = Stream.iterate(1, (x) -> x + 1).limit(5);
        stream2.forEach(item -> System.out.print(item + " "));
        // 输出结果:1 2 3 4 5

        // 方式3:
        Stream<Double> stream3 = Stream.generate(Math::random).limit(3);
        stream3.forEach(item -> System.out.print(item + " "));
        // 输出结果:0.8974676207611573 0.7658795436834018 0.48552366426962845

使用iterate和generate比较适合用来方便的构建海量测试数据。使用时要注意配合limit使用,否则会创建出一个无限大的流。

2.使用数组转换

String[] arr = new String[]{"A1", "B1", "A2", "B2", "A10", "B10"};
Stream<String> stream1 = Arrays.stream(arr);
stream1.forEach(item -> System.out.print(item + " "));
// 输出结果:A1 B1 A2 B2 A10 B10

 其实在Stream.of()方法的内部也直接调用了Arrays.stream()。

 3.使用Collection接口的stream()方法创建

List<String> list = new ArrayList<>();
list.add("A1");
list.add("B1");
Stream<String> stream = list.stream();
stream.forEach(item -> System.out.print(item + " "));
// 输出结果:A1 B1

 对于常见的数值集合操作,JDK中额外提供了IntStream,LongStream,DoubleStream三种包装类型的Stream,相当于Stream<Integer>,、Stream<Long>、Stream<Double>,但是减少了额外的boxing 和unboxing操作,提升了效率。

其它的创建方式还有一些,比如:

  • java.io.BufferedReader.lines()
  • java.util.stream.IntStream.range()
  • java.nio.file.Files.walk()
  • Random.ints()
  • BitSet.stream()
  • Pattern.splitAsStream(java.lang.CharSequence)
  • JarFile.stream()

 

对Stream进行操作

当我们使用一个流时,通常包含三个步骤:获取数据源(source) > 数据转换 > 获取最终结果,套用前文中的示例,拆解如下:

Stream的常见操作如下图

  • 中间操作 :中间操作的返回结果都是一个新的Stream,意味着一个流后面可以跟随多个中间操作,像stream.a().b().c()……这样开火车。中间操作都是惰性化的(lazy),就是说,仅仅调用到这些方法,并没有真正开始流的遍历。
  • 终结操作 :一个流只能有一个终结操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历。
  • 无状态 :元素的操作不依赖于其它元素
  • 有状态 :元素的操作依赖于其它操作
  • 非短路操作 :流中的每个元素都会被处理到
  • 短路操作 :只需处理一部分元素就会终止执行。

下面就来为大家演示这些常见操作。演示中如果使用到了自定义的Person类,定义如下:

@Data
class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

filter

 filter可以用来对流中的元素进行筛选。它对每个元素进行测试运算,保留返回值为true的元素,形成一个新的Stream。

// filter
Stream<String> stream = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
stream.filter(item -> item.length() < 3).forEach(item -> System.out.print(item + " "));
// 输出结果:A1 A1 B1 A2 B2

map

对每个元素进行转换,形成一个新的流。

// map
Stream<String> stream = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
stream.map(String::toLowerCase).forEach(item -> System.out.print(item + " "));
// 相当于如下代码:
//stream.map(item -> item.toLowerCase()).forEach(item -> System.out.print(item + " "));
        
// 输出结果:a1 a1 b1 a2 b2 a10 b10 b10

distinct

去除重复元素。依据元素的equals()方法。

// distinct
Stream<String> stream = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
stream.distinct().forEach(item -> System.out.print(item + " "));

// 输出结果:A1 B1 A2 B2 A10 B10

limit

截取流的前n个元素,n大于流中元素个数时返回所有元素构成的新Stream。

// limit
Stream<String> stream = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
stream.limit(3).forEach(item -> System.out.print(item + " "));

// 输出结果:A1 A1 B1

skip

跳过前n个元素,用剩余的元素构成一个新的Stream。n可以大于原始流中的元素个数,此时会得到一个空流。skip可以和limit配合起来实现分页。

// skip
Stream<String> stream = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
stream.skip(2).forEach(item -> System.out.print(item + " "));
// 输出结果:B1 A2 B2 A10 B10 B10

Stream<String> stream2 = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
stream2.skip(8).forEach(item -> System.out.print(item + " "));
// 空流,没有任何输出结果

sorted

对流中的元素进行排序。sorted()方法要求流中的元素实现了Comparable接口,否则会抛出ClassCastException。sorted(Comparator<? super T> comparator)可以指定排序规则。

 // sorted
Stream<String> stream = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
stream.sorted().forEach(item -> System.out.print(item + " "));
// 输出结果:A1 A1 A10 A2 B1 B10 B10 B2

对自定义对象排序示例:

List<Person> list = new ArrayList<>();
for (int i = 3; i > 0; i--) {
    list.add(new Person("Name" + i, i * 10));
}
System.out.println(list);
// 输出结果:[Person(name=Name3, age=30), Person(name=Name2, age=20), Person(name=Name1, age=10)]

list.stream().sorted(Comparator.comparingInt(Person::getAge)).forEach(item -> System.out.println(item));
// 输出结果:
// Person(name=Name1, age=10)
// Person(name=Name2, age=20)
// Person(name=Name3, age=30)

forEach

接收一个Lambda表达式,对每一个元素执行该表达式,前面的例子中都使用了它来执行打印。注意forEach是一个终结操作,一旦执行,流就消耗完了,再次操作会抛出异常。forEach的内部实现仍然是传统的for循环,但是节省了很多编码,清爽极了。

// forEach
Stream<String> stream = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
stream.forEach(item -> System.out.print(item + " "));
// 输出结果:A1 A1 B1 A2 B2 A10 B10 B10

// 再一次调用forEach时会抛出异常 ,因为forEach是终结操作,一旦执行,流就消耗完了。
// java.lang.IllegalStateException: stream has already been operated upon or closed
stream.forEach(item -> System.out.print(item + " "));

peek

和forEach不同,peek是一个中间操作,操作后返回一个新流,可以继续进行操作。

List<Person> list = new ArrayList<>();
for (int i = 3; i > 0; i--) {
    list.add(new Person("Name" + i, i * 10));

}
System.out.println(list);
// 输出结果:[Person(name=Name3, age=30), Person(name=Name2, age=20), Person(name=Name1, age=10)]

Stream<Person> stream = list.stream();
stream.peek(item -> item.setName(item.getName().toLowerCase()))
        .forEach(item -> item.setAge(item.getAge() + 1));
System.out.println(list);
// 输出结果:[Person(name=name3, age=31), Person(name=name2, age=21), Person(name=name1, age=11)]

reduce

主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce

// reduce
Stream<String> stream = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
String str = stream.reduce("", String::concat);
System.out.println(str);
// 输出结果:A1A1B1A2B2A10B10B10

IntStream stream2 = IntStream.of(1, 2, 3, 4, 5);
Integer sum = stream2.reduce(0, (a, b) -> a + b);
System.out.println(sum);
// 输出结果:15

Collect

Collect(收集)是一种是十分有用的最终操作,它可以把stream中的元素转换成另外一种形式。Collect使用Collector作为参数,Java 8内置了各种复杂的收集操作,因此对于大部分常用的操作来说,可以直接使用。

转换为List/Set/Map是最常见的操作了:

// 转换成List
Stream<String> stream = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
List<String> list = stream.filter(item -> item.length() > 2).collect(Collectors.toList());
System.out.println(list);
// 输出:[A10, B10, B10]

// 转换成Set
Stream<String> stream2 = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
Set<String> set = stream2.collect(Collectors.toSet());
System.out.println(set);
// 输出:[A1, B2, A10, A2, B10, B1]

// 转换成Map
Stream<String> stream3 = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
Map<String, Integer> map = stream3.distinct().collect(Collectors.toMap(item -> item, String::length));
System.out.println(map);
// {A10=3, B2=2, A1=2, B10=3, A2=2, B1=2}

 Collectors.groupingBy可以用来进行分组,也是一个比较常用的功能:

// 按照字符串长度进行分组
Stream<String> stream = Stream.of("A", "A1", "B", "A2", "B1", "A11", "B11", "B11");
Map<Integer,List<String>> map = stream.collect(Collectors.groupingBy(String::length));
System.out.println(map);
// 输出:{1=[A, B], 2=[A1, A2, B1], 3=[A11, B11, B11]}

可以使用Collectors对数据进行统计,计算最大值、最小值、平均值、求和等等,有单项的操作方法,也可用summarizing一次性返回多项统计信息

// summingInt求和、averagingInt求平均值 等可以进行单项统计
Stream<String> stream = Stream.of("A", "A1", "A12", "A123", "A1234");
System.out.println(stream.collect(Collectors.summingInt(String::length)));
// 输出:15

// summarizingInt 等可以输出更多的统计信息
Stream<String> stream2 = Stream.of("A", "A1", "A12", "A123", "A1234");
IntSummaryStatistics s = stream2.collect(Collectors.summarizingInt(String::length));
System.out.println(s);
// 输出:IntSummaryStatistics{count=5, sum=15, min=1, average=3.000000, max=5}

前面的例子里,我们曾使用过reduce来将字符串流中的元素连成一个字符串,Collectors也能实现类似功能,

// 直接将元素拼接
Stream<String> stream = Stream.of("A", "A1", "A12", "A123", "A1234");
String s = stream.collect(Collectors.joining());
System.out.println(s);
// 输出:AA1A12A123A1234

// 使用指定的连接字符串、前缀、后缀(可选的)进行拼接,
Stream<String> stream2 = Stream.of("A", "A1", "A12", "A123", "A1234");
String s2 = stream2.collect(Collectors.joining("_","【","】"));
System.out.println(s2);
// 输出:【A_A1_A12_A123_A1234】

match

Stream 有三个 match 方法:

  • anyMatch:Stream 中只要有一个元素符合传入的 predicate,返回 true,否则返回false
  • allMatch:Stream 中全部元素符合传入的 predicate,返回 true,否则返回false
  • noneMatch:Stream 中没有一个元素符合传入的 predicate,返回 true,否则返回false

match属于短路操作,在执行过程中,一旦能够确定最终结果就立即返回。例如anyMatch 只要找到一个符合条件的元素,就立即返回 true,不会再去检测后续的元素。详见下面的示例

// anyMatch
Stream<String> stream = Stream.of("A", "A1", "A12");
boolean b = stream.anyMatch(item -> {
    System.out.println("当前元素:" + item);
    return item.length() >= 2;
});
System.out.println("最终结果:" + b);
//当前元素:A
//当前元素:A1
//最终结果:true

// allMatch
Stream<String> stream2 = Stream.of("A", "A1", "A12");
boolean b2 = stream2.allMatch(item -> {
    System.out.println("当前元素:" + item);
    return item.length() >= 2;
});
System.out.println("最终结果:" + b2);
//当前元素:A
//最终结果:false

// noneMatch
Stream<String> stream3 = Stream.of("A", "A1", "A12");
boolean b3 = stream3.noneMatch(item -> {
    System.out.println("当前元素:" + item);
    return item.length() >= 2;
});
System.out.println("最终结果:" + b3);
//当前元素:A
//当前元素:A1
//最终结果:false

进阶

顺序流,并行流

Stream可以分为顺序流(sequential)和并行流(parallel),前文的例子中使用到的都是顺序流,单线程对流进行处理。并行流使用多线程来处理数据,在大数据量下,可以极大的提升处理的速度。其背后是使用了通用的并发框架 ForkJoinPool ,这是通过利用静态方法 ForkJoinPool.commonPool() 来实现的。对于 ForkJoinPool,其实际使用的线程数取决于机器背后的实际 CPU 核数。

// 我的机器8核CPU
ForkJoinPool commonPool = ForkJoinPool.commonPool();
System.out.println(commonPool.getParallelism());
//输出: 7

可以通过以下JVM参数进行修改:

-Djava.util.concurrent.ForkJoinPool.common.parallelism=5

可以利用Collection的parallelStream()方法直接创建一个并行流,也可以使用parallel()方法将一个顺序流转为并行流。下面的例子清楚地展示了多个线程参与了并行流的处理。

List<String> list = Arrays.asList("A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9");
list.parallelStream()
        .forEach(item -> System.out.println(Thread.currentThread().getName() + " forEach:" + item));
// 输出:
//main forEach:A6
//main forEach:A5
//ForkJoinPool.commonPool-worker-1 forEach:A3
//ForkJoinPool.commonPool-worker-3 forEach:A4
//ForkJoinPool.commonPool-worker-2 forEach:A8
//ForkJoinPool.commonPool-worker-1 forEach:A1
//ForkJoinPool.commonPool-worker-4 forEach:A7
//ForkJoinPool.commonPool-worker-3 forEach:A9
//main forEach:A2

Stream的API中,有一部分要在并行流下才有用武之地,比如forEachOrdered,作用同forEach,可以遍历元素。主要是作用于并行流时,forEach不能保证遍历元素的顺序,而forEachOrdered可以。

Stream<String> stream1 = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
Stream<String> stream2 = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
Stream<String> stream3 = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");

// 顺序流,foreach会按照流中元素的顺序进行遍历
stream1.forEach(item -> System.out.print(item + " "));
// 输出结果:A1 A1 B1 A2 B2 A10 B10 B10
        
// 并行流,可以使用多线程对流进行处理,使用forEach遍历时顺序没有保证
stream2.parallel().forEach(item -> System.out.print(item + " "));
// 输出结果:A10 B2 B10 B10 A2 A1 B1 A1
        
// 并行流,使用forEachOrdered遍历时顺序有保证
stream3.parallel().forEachOrdered(item -> System.out.print(item + " "));
// 输出结果:A1 A1 B1 A2 B2 A10 B10 B10

惰性的中间操作

前文有提到过中间操作都是惰性化的(lazy),仅仅调用到这些方法,并没有真正开始流的遍历,只有执行了终结操作时,才会开始流的遍历。

Stream<String> stream = Stream.of("A1", "A2", "A3");
stream.peek(System.out::println);
// 这里不会有任何输出,没有调用终结操作,不会执行peek

Stream<String> stream2 = Stream.of("B1", "B2", "B3");
stream2.peek(System.out::println).count();
//输出:
//B1
//B2
//B3

操作的执行顺序

对于形如:stream.filter().map().forEach() 这样的操作,根据代码直观地理解,会对流中的元素进行2次遍历,分别执行filter、map,最后再用forEach进行一次遍历。是这样的吗?我们来看一个例子。

Stream<String> stream = Stream.of("A1", "A2", "B1", "B2");
stream.map(item -> {
    System.out.println("map:" + item);
    return item.toLowerCase();
}).filter(item -> {
    System.out.println("filter:" + item);
    return item.startsWith("a");
}).forEach(item -> {
    System.out.println("forEach:" + item);
});
//猜想中的输出是先打印4行map,再接着打印4行filter,最后再打印两行foreach

//实际的输出:
//map:A1
//filter:a1
//forEach:a1
//map:A2
//filter:a2
//forEach:a2
//map:B1
//filter:b1
//map:B2
//filter:b2

在这个例子中我们可以看到,处理的顺序是每个元素沿着操作链垂直移动,依次执行所有的操作,然后才是下一个元素。这样在某些场景下可以减少实际的操作执行次数。上面这个例子中总共执行了10次操作,我们来稍微调整一下操作链的顺序,把filter放到map前面:

Stream<String> stream = Stream.of("A1", "A2", "B1", "B2");
stream.filter(item -> {
    System.out.println("filter:" + item);
    return item.startsWith("A");
}).map(item -> {
    System.out.println("map:" + item);
    return item.toLowerCase();
}).forEach(item -> {
    System.out.println("forEach:" + item);
});

//输出:
//filter:A1
//map:A1
//forEach:a1
//filter:A2
//map:A2
//forEach:a2
//filter:B1
//filter:B2

操作的总次数降为8次。

limit操作为获取前n个元素,skip为跳过n个元素,看起来比较相似,但它们对操作次数的影响有些不同。

Stream<String> stream = Stream.of("A1", "A2", "A3", "A4");
stream.limit(2)
        .map(item -> {
            System.out.println("map:" + item);
            return item.toLowerCase();
        }).forEach(item -> System.out.println("forEach:" + item));
//输出:
//map:A1
//forEach:a1
//map:A2
//forEach:a2

Stream<String> stream2 = Stream.of("A1", "A2", "A3", "A4");
stream2.map(item -> {
    System.out.println("map:" + item);
    return item.toLowerCase();
}).limit(2)
 .forEach(item -> System.out.println("forEach:" + item));
//输出:
//map:A1
//forEach:a1
//map:A2
//forEach:a2

limit无论是在map()前还是后,输出都是一样的,操作执行的次数由limit的参数n决定。再来看一下skip:

Stream<String> stream = Stream.of("A1", "A2", "A3", "A4");
stream.skip(2)
        .map(item -> {
            System.out.println("map:" + item);
            return item.toLowerCase();
        }).forEach(item -> System.out.println("forEach:" + item));
//输出:
//map:A3
//forEach:a3
//map:A4
//forEach:a4

Stream<String> stream2 = Stream.of("A1", "A2", "A3", "A4");
stream2.map(item -> {
    System.out.println("map:" + item);
    return item.toLowerCase();
}).skip(2)
 .forEach(item -> System.out.println("forEach:" + item));
//输出:
//map:A1
//map:A2
//map:A3
//forEach:a3
//map:A4
//forEach:a4

可以看到skip的位置对最终的操作次数有影响,但无论是limit还是skip,都符合"元素沿着操作链垂直移动"这个逻辑。对于limit、skip有一个特殊的情况,当它们遇到排序操作sorted时:

Stream<String> stream = Stream.of("A1", "A2", "A3", "A4");
stream.sorted((a, b) -> {
    System.out.println("当前正在比较:" + a + " 和 " + b);
    return a.compareTo(b);
}).limit(2)
        .forEach(item -> System.out.println("forEach:" + item));
//输出:
//当前正在比较:A2 和 A1
//当前正在比较:A3 和 A2
//当前正在比较:A4 和 A3
//forEach:A1
//forEach:A2

可以看出,虽然有limit(2)的限定,但是sorted操作还是对所有元素执行了排序。

总结

  • Stream是对集合对象功能的增强,专注于对集合对象进行各种便利、高效的操作;
  • Stream的操作有中间操作和终结操作,中间操作是惰性的,只有终结操作才会触发中间操作;
  • Stream的消费是一次性的,一个stream只能执行一次终结操作;
  • 可以使用并行流提升操作的效率
  • 合理的设计操作链的顺序可以提升效率
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值