Java8 新特性 —— Stream 流式编程

流概述

大多数情况下,我们将对象存储在集合是为了处理他们,使用流可以帮助我们处理对象,无需迭代集合中的元素,即可直接提取和操作元素,并进行查找、过滤、分组、排序等一系列操作

总而言之,流是一种高效且易于使用的处理数据的方式,当结合 Lambda 表达式和方法引用时效果更好

观察下面的例子:

public class Randoms {
    
    public static void main(String[] args) {
        new Random(47)	// 创建 Random 对象,并给一个种子
            .ints(5, 20)	// 产生一个限定了边界的随机整数流
            .distinct()	// 使流中的整数不重复
            .limit(7)	// 取前7个元素
            .sorted()	// 排序
            .forEach(System.out::println);	// 根据传递给它的函数对流中每个对象执行操作
    }
}

通过上面的示例,我们可以发现流有如下特点:

  1. 流是一种声明式编程风格,它声明想要做什么,而非指明如何做
  2. 流本身不存储元素,并且不会改变源对象,相反,它会返回一个持有结果的新流
  3. 流是懒加载的,它会等到需要时才执行

流创建

创建流的方式有很多,下面逐个介绍:

1. Stream.of()

通过 Stream.of() 可以很容易地将一组元素转化为流

Stream.of(new Bubble(1), new Bubble(2), new Bubble(3)).forEach(System.out::println);
Stream.of("a", "b", "c", "d", "e", "f").forEach(System.out::print);
Stream.of(3.14159, 2.718, 1.618).forEach(System.out::println);

2. stream()

每个集合也可以通过调用 stream() 方法来产生一个流

List<Bubble> list = Arrays.asList(new Bubble(1), new Bubble(2), new Bubble(3));
list.stream().forEach(System.out::print);
Set<String> set = new HashSet<>(Arrays.asList("a", "b", "c", "d", "e", "f"));
set.stream().forEach(System.out::print);

3. Stream.generate()

使用 Stream.generate() 搭配 Supplier<T> 生成 T 类型的流

Stream.generate(Math::random).limit(10).forEach(System.out::print);

4. Stream.iterate()

Stream.iterate() 产生的流的第一个元素是种子,然后把种子传递给方法,方法的运行结果被添加到流,并作为下次调用 iterate() 的第一个参数

Stream.iterate(0, n -> n + 1).limit(10).forEach(System.out::print)

使用 Stream.generate()Stream.iterate() 生成的无限流一定要用 limit() 截断

5. Stream.builder()

使用建造者模式创建一个 builder 对象,然后将创建流所需的多个信息传递给它,最后 builder 对象执行创建流的操作

Stream.Builder<String> builder = Stream.builder();
builder.add("a");
builder.add("b");
...
builder.build();	// 创建流
// builder.add("c")	// 调用 build() 方法后继续添加元素会产生异常

6. Arrays.stream()

Arrays 类中有一个名为 stream() 的静态方法用于把数组转换成流

Arrays.stream(new double[] {3.14159, 2.718, 1.618}).forEach(System.out::print);
Arrays.stream(new int[] {1, 3, 5}).forEach(System.out::print);
Arrays.stream(new long[] {11, 22, 44, 66}).forEach(System.out::print);
// 选择一个子域
Arrays.stream(new int[] {1, 3, 5, 7, 15, 28, 37}, 3, 6).forEach(System.out::print);

最后一次 stream() 的调用有两个额外的参数,第一个参数告诉 stream() 从数组的哪个位置开始选择元素,第二个参数告知在哪里停止

7. IntStream.range()

IntStream 类提供 range() 方法用于生成整型序列的流,编写循环时,这个方法会更加便利

IntStream.range(10, 20).sum();	// 求得 10 - 20 的序列和
IntStream.range(10, 20).forEach(System.out::print);	// 循环输出 10 - 20

8. 随机数流

Random 类被一组生成流的方式增强了,可以生成一组随机数流

Random rand = new Random(47);
// 产生一个随机流
rand.ints().boxed();
// 控制上限和下限
rand.ints(10, 20).boxed();
// 控制流的大小
rand.ints(2).boxed();
// 控制流的大小和界限
rand.ints(3, 3, 9).boxed();

Random 类除了能生成基本类型 int,long,double 的流,使用 boxed() 操作会自动把基本类型包装为对应的装箱类型

9. 正则表达式

Java8 在 java.util.regex.Pattern 中新增了一个方法 splitAsStream(),这个方法可以根据传入的公式将字符序列转化为流

Pattern.compile("[.,?]+").splitAsStream("a,b,c,d,e").forEach(System.out::print);

中间操作

中间操作具体包括去重、过滤、映射等操作,作用于从流中获取的每一个对象,并返回一个新的流对象

1. peek

提供一个消费函数,对流中所有元素进行操作,并返回一个新流

Stream.of("a b c d e".split(" ")).map(w -> w + " ").peek(System.out::print);

2. sorted

实现对流元素的排序,如果不使用默认的自然排序,则需要传入一个比较器,也可以把 Lambda 函数作为参数传递

Stream.of("a b c d e".split(" ")).sorted(Comparator.reverseOrder())
    .map(w -> w + " ").peek(System.out::print);

3. distinct

消除流中的重复元素,如果对对象去重,需要重写 hashcode() 和 equals() 方法

new Random(47).ints(5, 20).distinct().limit(7).forEach(System.out::println);

4. filter

将元素传递给过滤函数,若结果为 true,则保留元素

// 检测质数
Stream.iterate(2, n -> n + 1).filter(i -> i % 2 ==0)
    .limit(10).forEach(System.out::print)

5. map

将函数操作应用到输入流的元素,并将返回值传递到输出流

Arrays.stream(new String[] {"12", "23", "34"}).map(s -> "[" + s + "]")
    .forEach(System.out::print)

另外还有 mapToInt(ToIntFunction)mapToLong(ToLongFunction)mapToDouble(ToDoubleFunction),操作和 map(Function) 相似,只是结果流为各自对应的基本类型

如果在将函数应用到元素的过程中抛出了异常,此时会把原始元素放到输出流

6. flatMap

将产生流的函数应用在每个元素上,最终把所有生成的流合并为一个流

Stream.of(1, 2, 3).flatMap(i -> Stream.of("hello" + i)).forEach(System.out::println);

另外还有 flatMapToInt(Function)flatMapToLong(Function)flatMapToDouble(Function),操作和 flatMap() 相似,只是结果元素为各自对应的基本类型


终端操作

终端操作将获取流的最终结果,至此我们无法再继续往后传递流。可以说,终端操作总是我们在使用流时所做的最后一件事

1. toArray

返回一个包含此流元素的 Object 数组

Object[] objArray = Stream.of("A", "B", "C", "D").toArray();

上述例子数组元素的类型实际是 String,toArray 可以接收一个生成器函数,从而生成指定类型的数组

String[] strArray = Stream.of("A", "B", "C", "D").toArray(String[]::new);

2. foreach

对流中元素进行遍历

Stream.of("A", "B", "C", "D").foreach(System.out::println);

3. collect

collect 方法需要传入一个 Collector 类型的参数,将 stream 转换成集合,Java 提供了 Collectors 工具类帮助我们快捷构建 Collector

List<String> list = Arrays.asList("a", "b", "c", "d");

// 将 stream 转换为 list
List<String> listResult = list.stream().collect(Collectors.toList());

// 将 stream 转换为 set
Set<String> setResult = list.stream().collect(Collectors.toSet());

// 自定义转换类型,这里指定为 LinkedList
LinkedList<String> custListResult = list.stream().collect(Collectors.toCollection(LinkedList::new));

// 将 stream 转换为 map
// User 的 id 属性为 key,User 本身为 value
Map<Long, User> map1 = users.stream().collect(Collectors.toMap(User::getId, o -> o));
// User 的 id 属性为 key,User 的 name 为 value
Map<Long, String> map2 = users.stream().collect(Collectors.toMap(User::getId, o -> o.getName()));
// map 中的 key 不能重复,如果出现重复,可以选择忽略重复值或者直接覆盖
// 忽略后面的重复值,o1 是旧值,o2 是新值
Map<Long, User> map3 = users.stream().collect(Collectors.toMap(User::getId, o -> o, (o1, o2) -> o1));
// 直接覆盖旧值,o1 是旧值,o2 是新值
Map<Long, User> map4 = users.stream().collect(Collectors.toMap(User::getId, o -> o, (o1, o2) -> o2));

// 连接 stream 中的元素
// abcd
String joinResult1 = list.stream().collect(Collectors.joining());
// a-b-c-d
String joinResult2 = list.stream().collect(Collectors.joining("-"));
// [a-b-c-d]
String joinResult3 = list.stream().collect(Collectors.joining("-", "[", "]"));

// 根据指定属性分组,并返回一个 Map
// 按照年龄对用户分组
Map<Integer, List<User>> gmap1 = users.stream().collect(Collectors.groupingBy(User::getAge));
// 按照年龄对用户分组,统计每个年龄分组的用户数量
Map<Integer, Long> gmap2 = users.stream().collect(Collectors.groupingBy(User::getAge, Collectors.counting()));

4. reduce

组合意味着将流中所有元素以某种方式组合为一个元素

  • reduce(BinaryOperator)

    使用 BinaryOperator 来组合所有流中的元素。因为流可能为空,其返回值为 Optional

  • reduce(identity, BinaryOperator)

    功能同上,但是使用 identity 作为其组合的初始值。因此如果流为空,identity 就是结果

看一段代码示例:

Stream.generate(Math::random).limit(10)
	.reduce((fr0, fr1) -> fr0.size < 50 ? fr0 : fr1).ifPresent(System.out::println);

返回的结果是 Optional 类型,Lambda 表达式中的第一个参数 fr0 是 reduce 中上一次调用的结果,而第二个参数 fr1 是从流传递过来的值

5. match

allMatch(Predicate)

如果流的每个元素提供给 Predicate 函数都返回 true ,结果返回为 true,否则返回 false

anyMatch(Predicate)

如果流的任意一个元素提供给 Predicate 返回 true ,结果返回为 true,否则返回 false

noneMatch(Predicate)

如果流的每个元素提供给 Predicate 都返回 false 时,结果返回为 true,否则返回 false

6. find

findFirst()

返回第一个流元素的 Optional,如果流为空返回 Optional.empty

findAny(

返回含有任意流元素的 Optional,如果流为空返回 Optional.empty

7. info

count()

流中的元素个数

max(Comparator)

根据所传入的 Comparator 所决定的最大元素

min(Comparator)

根据所传入的 Comparator 所决定的最小元素

average()

求取流元素平均值

max()min()

数值流操作无需 Comparator

sum()

对所有流元素进行求和


parallelStream

前面提到的 stream 流可以理解为串行的流式计算,parallelStream 则是一种多线程并行流式计算,注意,parallelStream 是非线程安全的

List<String> str = new ArrayList<>();
str.add("1");
str.add("2");
str.add("3");
str.add("4");
str.add("5");
str.add("6");

// 单个线程执行任务
str.stream().filter(e -> {
    System.out.println(Thread.currentThread().getName() + "\t过滤" + e);
    return Integer.parseInt(e) % 2 == 0 ? true : false;
}).collect(Collectors.toList());
// 

输出如下:

main 过滤1
main 过滤2
main 过滤3
main 过滤4
main 过滤5
main 过滤6

使用 parallelStream

List<String> str = new ArrayList<>();
str.add("1");
str.add("2");
str.add("3");
str.add("4");
str.add("5");
str.add("6");

str.parallelStream().filter(e -> {
    System.out.println(Thread.currentThread().getName() + "\t过滤" + e);
  return Integer.parseInt(e) % 2 == 0 ? true : false;
}).collect(Collectors.toList());

输出如下:

ForkJoinPool.commonPool-worker-1 过滤2
ForkJoinPool.commonPool-worker-1 过滤3
ForkJoinPool.commonPool-worker-1 过滤5
main 过滤4
ForkJoinPool.commonPool-worker-1 过滤1
ForkJoinPool.commonPool-worker-1 过滤6

可以看到任务是并行执行的,底层使用了Fork/Join 分支计算最终合并技术,简单来说是把一个大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果,具体可参考:https://www.cnblogs.com/Yee-Q/p/14584678.html

posted @ 2020-11-14 18:49  低吟不作语  阅读(7465)  评论(0编辑  收藏  举报