java8特性 stream笔记(工作流程与使用方式部分)

上一篇:java8特性 stream笔记(基础部分)
https://blog.csdn.net/Ding_Creator/article/details/117523051

流的工作过程

图的出处在参考资料中
流的工作过程
创建流->中间操作->终止操作
其中中间操作可以有0个至多个,终止操作只能有一个

/**
 * 如果有多个终止操作
 */
class DuplicateTerminal {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(2, 2);
        Stream<Integer> stream = list.stream();
        stream.collect(Collectors.toList());
        stream.forEach(System.out::println);
    }
}

输出结果:

Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
	at java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
	at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
	at DuplicateTerminal.main(Show.java:57)

Process finished with exit code 1

这里也有一个小问题,创建流的操作非常容易识别,最常用的list.stream()就是创建流的操作,那么怎么区分中间操作和终止操作呢?
其实也很简单,当使用的方法返回一个Stream的对象,那么就是中间操作,反之则是终止操作

在开始详细讲解流的工作过程之前,先要明确四个特点:
1.Stream自己不会存储元素。
2.Stream的操作不会改变源对象。相反,他们会返回一个新Stream
3.流是一次性的,每个流只能使用一次。

/**
 * 实际上,已被操作的流不能被再次操作,流是一次性的
 */
class DuplicateStream {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4);
        Stream<Integer> stream = list.stream().filter(t->t>1);
        stream.filter(t -> t > 3);
        stream.filter(t -> t > 2);
    }
}

输出结果:

Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
	at java.util.stream.AbstractPipeline.<init>(AbstractPipeline.java:203)
	at java.util.stream.ReferencePipeline.<init>(ReferencePipeline.java:94)
	at java.util.stream.ReferencePipeline$StatelessOp.<init>(ReferencePipeline.java:618)
	at java.util.stream.ReferencePipeline$2.<init>(ReferencePipeline.java:163)
	at java.util.stream.ReferencePipeline.filter(ReferencePipeline.java:162)
	at DuplicateStream.main(Show.java:69)

Process finished with exit code 1

4.Stream 操作是延迟执行的。它会等到需要结果的时候才执行。也就是执行终止操作的时候。也就是说,如果没有终止操作,那么流不会执行任何操作

/**
 * 无终止操作
 */
class NoTerminal {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(2, 3, 4);
        list.stream().filter(t -> {
            System.out.println(t);
            return t > 2;
        }).map(t -> {
            t = t + 2;
            System.out.println(t);
            return t;
        });
    }
}

输出结果:


Process finished with exit code 0

现在开始正式讲解流的工作流程

1.创建流

1.1 使用Collection中的stream()或者parallelStream()方法

List<Integer> list = new ArrayList<>();
// 创建串行流
list.stream();
// 创建并行流
list.parallelStream();

1.2 使用Arrays中的stream()方法

Integer[] array = {1, 2, 3, 4, 5};
// 第一种使用方式
Arrays.stream(array);
// 第二种使用方式,两个参数为两个下标,左闭右开区间
Stream<Integer> stream = Arrays.stream(array, 2, 4);
//打印一下看看结果
stream.forEach(System.out::println);

输出结果:

3
4

Process finished with exit code 0

1.3 Stream中的静态方法

Stream<Integer> stream = Stream.of(1,2,3,4,5,6);
Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 2).limit(6);
Stream<Double> stream3 = Stream.generate(Math::random).limit(2);

这里有一个很有意思的点,如果使用后面两个方法不加上limit(),那么生成的将是一个无限流,那么垃圾回收是否会回收无限流产生的数据呢?

/**
 * 无限流(垃圾回收是否会回收这些数据)
 */
class UnlimitedStream1 {
    public static void main(String[] args) {
        System.out.println(Runtime.getRuntime().maxMemory());

        Stream.generate(() -> {
            double d = Double.MAX_VALUE;
            System.out.println("number=" + d);
            return d;
        })
//                .forEach(System.out::println);
                .collect(Collectors.toList());
    }
}

设置启动参数以便更快看到效果

-Xms1m -Xmx1m

你会发现当使用forEach()时,并不会发生内存溢出的错误,但是当你使用的是collect(Collectors.toList())时,会发生内存溢出,其实这是因为当你调用collect()方法时,会将这些生成的数据收集起来,当数据量到一定程度,就溢出了,而使用forEach()方法时,每个数据走了一遍forEach()后就不再被使用,然后就被回收了

1.4 使用 BufferedReader.lines() 方法,将每行内容转成流

BufferedReader reader = new BufferedReader(new FileReader("文件路径"));
Stream<String> lineStream = reader.lines();

1.5 使用 Pattern.splitAsStream() 方法,将字符串分隔成流

Pattern pattern = Pattern.compile(",");
Stream<String> stringStream = pattern.splitAsStream("a,b,c,d");

2.中间操作
中间操作包括:
无状态(Stateless)操作:每个数据的处理是独立的,不会影响或依赖之前的数据。如
filter()、flatMap()、flatMapToDouble()、flatMapToInt()、flatMapToLong()、map()、mapToDouble()、mapToInt()、mapToLong()、peek()、unordered() 等
有状态(Stateful)操作:处理时会记录状态,比如处理了几个。后面元素的处理会依赖前面记录的状态,或者拿到所有元素才能继续下去。如distinct()、sorted()、sorted(comparator)、limit()、skip() 等

接下来所有例子中用到的Student类都是这一个

class Student implements Comparable<Student> {
    private Long number;
    private Integer age;

    public Student(Long number) {
        this.number = number;
    }

    public Student(Long number, Integer age) {
        this.number = number;
        this.age = age;
    }

    public Long getNumber() {
        return number;
    }

    public void setNumber(Long number) {
        this.number = number;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "number=" + number +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(Student o) {
        if (o.getNumber() > this.getNumber()) {
            return -1;
        }
        return 1;
    }
}

下面来看看有状态和无状态的区别

class StatefulAndStateless {
    public static void main(String[] args) {
        Student s1 = new Student(101L, 10);
        Student s2 = new Student(102L, 12);
        Student s3 = new Student(100L, 8);
        Student s4 = new Student(104L, 9);
        List<Student> studentList = Arrays.asList(s1, s2, s3, s4);

        studentList.stream().map(t -> {
            System.out.println("开始排序");
            return t;
        }).forEach(t -> System.out.println("收集"));
        System.out.println("=======================");
        studentList.stream().sorted((o1, o2) -> {
                    System.out.println("开始排序");
                    return o1.compareTo(o2);
                }
        ).forEach(t -> System.out.println("收集"));
    }
}

运行结果:

开始排序
收集
开始排序
收集
开始排序
收集
开始排序
收集
=======================
开始排序
开始排序
开始排序
开始排序
开始排序
开始排序
收集
收集
收集
收集

很神奇吧,这两个操作打印出来的结果完全不一样。交叉打印的,说明后续操作不需要等前一操作全部完成即可进行,是无状态操作。而另一种这说明需要等前一步骤完成,才能进行下一步操作。这就是有状态和无状态操作的区别。

接下来介绍几个中间操作的用法
2.1 筛选与切片
筛选:用一个返回boolean的函数筛选,筛选出各不相同的元素
filter:通过设置的条件过滤流中的某些元素
distinct:通过流中元素的 hashCode() 和 equals() 去除重复元素
切片:忽略流中的头几个元素或截取流中的前几个元素
limit(n):获取n个元素
skip(n):跳过n元素,配合limit(n)可实现分页

List<Integer> list = Arrays.asList(2, 2, 3, 4, 1);
list.stream().filter(t -> t > 2).forEach(t -> System.out.print(t + ","));
System.out.println("\n============================");
list.stream().limit(2).forEach(t -> System.out.print(t + ","));
System.out.println("\n============================");
list.stream().skip(2).forEach(t -> System.out.print(t + ","));
System.out.println("\n============================");
list.stream().distinct().forEach(t -> System.out.print(t + ","));

输出结果:

3,4,
============================
2,2,
============================
3,4,1,
============================
2,3,4,1,

2.2 映射
  map:接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。

List<Integer> list1 = Arrays.asList(2, 2, 3, 4);
list1.stream().map(t -> t * 2).forEach(System.out::println);

输出结果

4
4
6
8

flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。(注意观察,跟map并不相同)

List<String> list2 = Arrays.asList("a,b,c", "1,2,3");
List<String> flatMapList = list2.stream().flatMap(t -> {
            Stream<String> stream = Arrays.stream(t.split(","));
            System.out.println("stream=" + stream);
            return stream;
}).collect(Collectors.toList());
System.out.println("======================");
System.out.println("flatMapList=" + flatMapList + ",size=" + flatMapList.size());

输出结果

stream=java.util.stream.ReferencePipeline$Head@27bc2616
stream=java.util.stream.ReferencePipeline$Head@3941a79c
======================
flatMapList=[a, b, c, 1, 2, 3],size=6

如果是map,中间过程打印出来两个流,那么collect后输出的结果也会是两个流,但是flatMap则是把两个流合成了一整个流,所以打印出来的是六个元素(如果不信,可以试试map,看看打出来的结果)

2.3 排序
sorted():自然排序,流中元素需实现Comparable接口
sorted(Comparator com):定制排序,自定义Comparator排序器

Student s1 = new Student(101L, 10);
Student s2 = new Student(102L, 12);
Student s3 = new Student(100L, 8);
Student s4 = new Student(104L, 9);
List<Student> studentList = Arrays.asList(s1, s2, s3, s4);
//默认排序(使用的排序方法为student实现compare接口时重写的方法)
studentList.stream().sorted().forEach(System.out::println);
System.out.println("==================");
//自定义排序:先按年龄升序,年龄相同则按学号升序
studentList.stream().sorted((o1, o2) -> {
       if (o1.getAge().equals(o2.getAge())) {
            return o1.getAge() - o2.getAge();
       } else {
            return o1.getAge().compareTo(o2.getAge());
       }
}).forEach(System.out::println);

输出结果

Student{number=100, age=8}
Student{number=101, age=10}
Student{number=102, age=12}
Student{number=104, age=9}
==================
Student{number=100, age=8}
Student{number=104, age=9}
Student{number=101, age=10}
Student{number=102, age=12}

2.4 消费
  peek:如同于map,能得到流中的每一个元素。但map接收的是一个Function表达式,有返回值;而peek接收的是Consumer表达式,没有返回值。

List<Integer> list1 = Arrays.asList(2, 2, 3, 4);
list1.stream().peek(t -> {
        t = t * 2;
        System.out.println("peek: t=" + t);
}).forEach(System.out::println);
System.out.println("=======================");
list1.stream().map(t -> {
        t = t * 2;
        System.out.println("map: t=" + t);
        return t;
}).forEach(System.out::println);

输出结果

peek: t=4
2
peek: t=4
2
peek: t=6
3
peek: t=8
4
=======================
map: t=4
4
map: t=4
4
map: t=6
6
map: t=8
8

可以看到,map中对元素的变化有影响到后续操作,而peek则没有

3.终止操作
终止操作包括:
非短路操作:处理完所有数据才能得到结果。如
collect()、count()、forEach()、forEachOrdered()、max()、min()、reduce()、toArray()等。
短路(short-circuiting)操作:拿到符合预期的结果就会停下来,不一定会处理完所有数据。如anyMatch()、allMatch()、noneMatch()、findFirst()、findAny() 等。
我们用一个例子来对比两者的区别

/**
 * 短路与非短路
 */
class ShortCircuit {
    public static void main(String[] args) {
        List<Integer> list1 = Arrays.asList(4, 6, 1, 3, 6, 6, 2, 4, 1, 8, 9, 7);
        list1.stream().peek(t -> System.out.println("peek:" + t)).anyMatch(t -> {
            System.out.print(t + ",");
            return t > 3;
        });

        System.out.println("\n============================");
        int max = list1.stream().peek(t -> System.out.print(t + ",")).max(Integer::compareTo).get();
        System.out.println("max=" + max);
    }
}

输出结果

peek:4
4,
============================
4,6,1,3,6,6,2,4,1,8,9,7,max=9

可以发现,当短路操作遇到第一个不符合条件的元素会立即停止,这个停止不仅包括终止操作,连中间操作的后续也被停止了。而非短路操作则会走完整个流程。

接下来介绍几个终止操作的用法
3.1 匹配、聚合操作
allMatch:接收一个 Predicate 函数,当流中每个元素都符合该断言时才返回true,否则返回false
noneMatch:接收一个 Predicate 函数,当流中每个元素都不符合该断言时才返回true,否则返回false
anyMatch:接收一个 Predicate 函数,只要流中有一个元素满足该断言则返回true,否则返回false
findFirst:返回流中第一个元素
findAny:返回流中的任意元素
count:返回流中元素的总个数
max:返回流中元素最大值
min:返回流中元素最小值

/**
 * 匹配、聚合操作
 */
class TerminalStream1 {
    public static void main(String[] args) {
        List<Integer> list1 = Arrays.asList(4, 6, 1, 3, 6, 6, 2, 4, 1, 8, 9, 7);

        boolean allMatchResult = list1.stream().allMatch(t -> {
            System.out.print(t + ",");
            return t > 3;
        });
        System.out.println("allMatchResult=" + allMatchResult);

        boolean anyMatchResult = list1.stream().anyMatch(t -> {
            System.out.print(t + ",");
            return t > 3;
        });
        System.out.println("anyMatchResult=" + anyMatchResult);
    }
}

输出结果

4,6,1,allMatchResult=false
4,anyMatchResult=true

3.2 规约操作
  Optional reduce(BinaryOperator accumulator):第一次执行时,accumulator函数的第一个参数为流中的第一个元素,第二个参数为流中元素的第二个元素;第二次执行时,第一个参数为第一次函数执行的结果,第二个参数为流中的第三个元素;依次类推。
  T reduce(T identity, BinaryOperator accumulator):流程跟上面一样,只是第一次执行时,accumulator函数的第一个参数为identity,而第二个参数为流中的第一个元素。
  U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator combiner):在串行流(stream)中,该方法跟第二个方法一样,即第三个参数combiner不会起作用。在并行流(parallelStream)中,我们知道流被fork join出多个线程进行执行,此时每个线程的执行流程就跟第二个方法reduce(identity,accumulator)一样,而第三个参数combiner函数,则是将每个线程的执行结果当成一个新的流,然后使用第一个方法reduce(accumulator)流程进行规约。

那么问题来了,如果流中没有元素或者只有一个元素会发生什么呢?

List<Integer> emptyList = new ArrayList<>();
emptyList.stream().peek(System.out::println).reduce(Integer::sum);
emptyList.add(1);
emptyList.stream().peek(System.out::println).reduce(Integer::sum);

输出结果

1

好像无事发生,但当我们尝试去get

List<Integer> emptyList = new ArrayList<>();
emptyList.stream().peek(System.out::println).reduce(Integer::sum).get();

输出结果

Exception in thread "main" java.util.NoSuchElementException: No value present
	at java.util.Optional.get(Optional.java:135)
	at ReduceStream.main(Show.java:359)

这也就说明,我们对于返回的Optional对象,在get之前必须通过isPresent()方法去判断是否有值

那么接下来具体讲讲reduce()方法
第一个和第二个reduce比较好理解,那么第三个reduce是什么效果呢?我们可以看下例子

/**
 * 规约操作
 */
class ReduceStream {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4);
        int reduce1 = list.stream().reduce(Integer::sum).get();
        int reduce2 = list.stream().reduce(5, Integer::sum);
        int reduce3 = list.stream().reduce(5, Integer::sum, Integer::sum);
        int reduceP3 = list.stream().parallel().reduce(5, Integer::sum, Integer::sum);
        System.out.println("reduce1:" + reduce1 + ", reduce2:" + reduce2 + ", reduce3:" + reduce3 + ", reduceP3:" + reduceP3);
    }
}

输出结果

reduce1:10, reduce2:15, reduce3:15, reduceP3:30

可见在并行流下第三个reduce计算出的结果和串行流是不一致的,这个要特别注意。这是因为这四个元素被分配到了四个任务中,每个任务计算的时候都有一个初始值5,这样合并的时候就会比串行流多出来3个5,就变成了30。大家可以试下换几个值验证一下,也可以更改第二个参数和第三个参数中的重写方法去验证

3.3 收集操作
顾名思义,收集操作就是将所有元素收集起来。
我们先捋一遍收集操作需要哪些步骤。在串行流中,第一步,肯定是要有一个容器容纳这些元素。第二步,需要把处理好的元素放入这个容器。第三步,如果类型发生了变化,需要转型。
而在并行流中,还有一个将各个任务的结果集进行合并的过程。因此,在第二步与第三步之间,还要加入一个合并结果集的方法。

接下来我们看看官方给的操作。官方给出的Collectors里面有toList(),toMap()等多个方法,这里以比较常用的toList方法为例。toList实质上返回了一个CollectorImpl对象,构建这个对象需要以下五个抽象方法:

Supplier<A> supplier()(步骤一使用)
创建一个结果容器A,在toList方法中被重写为new ArrayList()

BiConsumer<A, T> accumulator()(步骤二使用)
主要用于将处理好的元素放入结果集,在toList方法中被重写为list.add(e)

BinaryOperator<A> combiner()(并行流中使用)
将并行流中各个子进程的运行结果(accumulator函数操作后的容器A)进行合并,在toList()中被重写为left.addAll(right),其中left和right为两个任务的结果集

Function<A, R> finisher()(步骤三使用)
如果第五个参数中,有IDENTITY_FINISH属性,那么这个方法不会被使用。(toList是有这个属性的)
如果没有,那么会调用此方法将元素逐个转化为集合所希望的类型

Set<Characteristics> characteristics()(标识)
返回一个不可变的Set集合,用来表明该Collector的特征。可能会包含有以下三个特征:
CONCURRENT:表示此收集器支持并发。
UNORDERED:表示该收集操作不会保留流中元素原有的顺序。
IDENTITY_FINISH:表示finisher参数。

下一篇 java8特性 stream笔记(源码解析部分)
https://blog.csdn.net/Ding_Creator/article/details/117665882

参考资料

  1. https://blog.csdn.net/TheLudlows/article/details/84778817
  2. https://blog.csdn.net/y4x5M0nivSrJaY3X92c/article/details/83155483?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-17.control&dist_request_id=&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-17.control
  3. https://www.cnblogs.com/CarpenterLee/archive/2017/03/28/6637118.html
  4. https://blog.csdn.net/weixin_41044036/article/details/113516439
  5. https://blog.csdn.net/qq_31865983/article/details/106443244
  6. https://www.cnblogs.com/CarpenterLee/p/6637118.html
  7. https://blog.csdn.net/xiliunian/article/details/88364200?spm=1001.2014.3001.5502
  8. https://blog.csdn.net/weixin_41131531/article/details/100007974
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值