7. stream 流编程

1. 概念

stream 是一个高级的迭代器,它不是一个数据结构,它不是一个集合、不会存放数据,它关注的是如何把数据高效的处理。它其实是把数据放在一个流水线中处理的 pipeline ,和现实生活中的流水线一样,在开始端输入数据、结束端获得结果。

对比一下外部迭代和内部迭代:

public class StreamDemo1 {
    public static void main(String[] args) {
        int[] nums = {1, 2, 3};
//        外部迭代器
        int sum1 = 0;
        for (int i : nums) {
            sum1 += i;
        }
        System.out.println("sum1 = " + sum1);
//        内部迭代器
        int sum2 = IntStream.of(nums).sum();
        System.out.println("sum2 = " + sum2);
    }
}

对比两种迭代器,你会发现内部迭代器不需要不关注如何得到这个结果,循环是如何实现。而且在数据量大情况,外部迭代还需要你自己去优化代码,而内部迭代就直接使用并行流即可。

完整代码:  https://github.com/hmilyos/lambda-demo.git

中间操作、终止操作、惰性求值

在上面的 main 方法里面加上

int sum3 = IntStream.of(nums).map(i -> i * 2).sum();
System.out.println("sum3 = " + sum3);

还可以这样实现:

加一个静态方法:

public static int doubleNum(int i){
    System.out.println(i + " * 2 ");
    return i * 2;
}

然后调用时

int sum4 = IntStream.of(nums).map(StreamDemo1:: doubleNum).sum();
System.out.println("sum4 = " + sum4);

如上,map 就是中间操作(返回 stream 的操作),sum 就是终止操作。

一个很简单的判断方法就是,返回的是 stream 流就是中间操作,否则就是终止操作。

惰性求值就是终止操作没有调用的情况下,中间操作不会执行,例如下面的

IntStream.of(nums).map(StreamDemo1:: doubleNum);

这行代码没有调用终止操作,那么 doubleNum 并不会执行。

2. 流的创建

9167995-fc9cbeec1659b00a
image

代码示例如下:

/**
 * Stream 流的创建
 */
public class StreamDemo2 {
    public static void main(String[] args) {
//        从集合创建
        ArrayList<String> list = new ArrayList<>();
        list.stream();
        list.parallelStream(); // 并行流

//        从数组创建
        Arrays.stream(new int[]{1, 2, 3});

        //        创建数字流
        IntStream.of(1, 2, 3);
        IntStream.rangeClosed(1, 5); // 创建从 1  到 5 的数字流
        
//        使用 random 创建一个无限流,注意无限流要给个短路值,否则会一直创建下去
        new Random().ints().limit(5);
        
//        自己产生流
        Random random = new Random();
        Stream.generate(() -> random.nextInt()).limit(5);
    }
}

3. 中间操作

中间操作包括 无状态操作 和 有状态操作

无状态:我当前的操作与其他元素前后都没有依赖关系

有状态:我的结果要依赖其他元素,好比 排序操作就要依赖所有元素的计算完毕。

无状态:
map:  把 A对象 转换成 B对象
mapToXXX: 将 A对象转成 int 或者 long,更多的表示从 A对象获取一些属性信息,例如传入一个字符串,返回字符串的长度,并不是将 A 对象转成一个数字。
flatMap:可以这样理解  原有一个 A 对象,然后 A 对象下面有一个属性,假设为 B属性,B 属性是一个集合组成的
filter : 过滤器
peek: 入参是一个消费者,跟终止操作里面的 foreach 很像,但是 peek 是中间操作
unordered: 很少用到,在并行流里面会用到
有状态:
distinct: 去重
sorted: 排序
limit: 限流,指定个数
skip:跳过一些数据

来演示一下重要的:

/**
 *  Stream 的中间操作
 */
public class StreamDemo3 {
    public static void main(String[] args) {
        String str = "my name is hmily";
//        打印长度超过 2 的 单词的长度
        Stream.of(str.split(" "))
                .filter(s -> s.length() > 2)
                .map(s -> s.length())
                .forEach(System.out::println);

        System.out.println("--------------flatMap------------");
//        flatMap A->B属性(是个集合), 最终得到所有的A元素里面的所有B属性集合
//                A : "my name is hmily"
//                B: A 里面的每个字母的 ascii
//        intStream/longStream 并不是Stream的子类, 所以要进行装箱 boxed
        Stream.of(str.split(" "))
                .flatMap(s -> s.chars().boxed())
                .forEach(i -> System.out.println((char) i.intValue()));

        System.out.println("--------------peek------------");
//        peek 用于debug. 是个中间操作,和 forEach 是终止操作
        Stream.of(str.split(" "))
                .peek(System.out::println)
                .forEach(System.out::println);

        System.out.println("--------------limit------------");
//        limit: 主要用于做无限流的短路条件
        new Random().ints()
                .filter(i -> i > 100 && i < 1000)
                .limit(5)
                .forEach(System.out::println);
    }
}
9167995-d9c8fd90615aee4c
image

4. 终止操作

终止操作分为 短路操作 和 非短路操作

短路操作:不需要等待一些结果计算完就可以终止这个流的操作,例如 获得第一个数据 findFirst 、获得任意一个数据 findAny, 我这个流就可结束啦

注意:看到 Ordered 就应该知道这是跟并行流相关的

演示:

/**
 *  终止操作
 */
public class StreamDemo4 {
    public static void main(String[] args) {
        String str = "my name is hmily";
//        使用并行流
        str.chars().parallel().forEach(i -> System.out.print((char) i));
        System.out.println();
        System.out.println("--forEachOrdered--");
//        使用 forEachOrdered 保证顺序
        str.chars().parallel().forEachOrdered(i -> System.out.print((char) i));
    }
}
9167995-c2016530b312b9ca
image

看打印的信息:在并行流中,不使用 Ordered 会是乱序的

在上面的代码中加上

System.out.println();
//        收集到 list
        List<String> list = Stream.of(str.split(" ")).collect(Collectors.toList());
        System.out.println(list);

运行打印出来了

9167995-dae106ea5c3fd069
image

接着

//        使用 reduce 拼接字符串
        System.out.println();
        Optional<String> letters = Stream.of(str.split(" ")).reduce((s1, s2) -> s1 + "|" + s2);
        System.out.println(letters.orElse("")); // 如果 letters 为 null, 则打印空字符串出来

打印:


9167995-97cc645cb4fbd8bb
image

接着:

//        带初始化值的 reduce
        System.out.println();
        String reduce = Stream.of(str.split(" ")).reduce("", (s1, s2) -> s1 + "|" + s2);
        System.out.println(reduce);
9167995-dd2b8695e8f49f3a
image

接着
// 计算所有单词的总长度
System.out.println();
Integer length = Stream.of(str.split(" "))
.map(s -> s.length())
.reduce(0, (s1, s2) -> s1 + s2);
System.out.println(length);
System.out.println();
打印

9167995-b787b0128336d386
image

接着

// max 的使用
Optional<String> max = Stream.of(str.split(" "))
        .max((s1, s2) -> s1.length() - s2.length());
System.out.println(max.get());

打印


9167995-d43c82578277d613
image

短路操作

// 使用 findFirst 短路操作
OptionalInt findFirst = new Random().ints().findFirst();
System.out.println(findFirst.getAsInt());

打印


9167995-7201b720ba581c40
image

5. 并行流

来,先看一个示例:

/**
 * 并行流
 */
public class StreamDemo5 {
    private static final Logger log = LoggerFactory.getLogger(StreamDemo5.class);
    public static void debug(int i) {
        log.info("debug: {}", i);
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
IntStream.range(1, 100).peek(StreamDemo5::debug).count();
    }
}

运行后看日志,这是串行流的


9167995-3d180f25fed0e984
image

修改一下 main,调用一下 parallel 并行流方法

    public static void main(String[] args) {
//     IntStream.range(1, 100).peek(StreamDemo5::debug).count();
//        调用 parallel 产生一个并行流
        IntStream.range(1, 100).parallel().peek(StreamDemo5::debug).count();
    }

看日志,可以说是乱序的


9167995-df7ea2901941ee3c
image

再仔细看日志,你会发现它使用的是 JDK 自带的 ForkJoinPool.commonPool 线程池,默认的线程数是当前机器的 CPU 个数(我的机器是 4 核),可以使用 java.util.concurrent.ForkJoinPool.common.parallelism 这个参数进行修改, 如下:

// 并行流使用的线程池: ForkJoinPool.commonPool
// 默认的线程数是 当前机器的cpu个数
// 使用这个属性可以修改默认的线程数
 System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "20");
 IntStream.range(1, 100).parallel().peek(StreamDemo5::debug).count();

现在来看一个需求 先并行,再串行的效果,看看能否实现
先加一个 debug2 方法来做对比

public static void debug2(int i) {
    log.info("debug2: {}", i);
    try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

再改一下 main 方法,

    public static void main(String[] args) {
//        IntStream.range(1, 100).peek(StreamDemo5::debug).count();
//        调用 parallel 产生一个并行流
//        IntStream.range(1, 100).parallel().peek(StreamDemo5::debug).count();


//        结论:多次调用 parallel、sequential,以最后一次调用为准
        IntStream.range(1, 100)
//                先调用 parallel 产生并行流
                .parallel().peek(StreamDemo5::debug)
//                 再调用 sequential 产生串行流
                .sequential().peek(StreamDemo5::debug2)
                .count();
    }

运行,看日志,你会发现这是串行的


9167995-1b5922157a8c1059
image

结论:多次调用 parallel、sequential,以最后一次调用为准

这里的并行流都是使用同一个默认的线程池,就会存在以下的问题:

线程池是有排队、调度等相关的操作的,那么我们所有的并行流都是用同一个线程池的话,那就会有堵塞。我现在往并行流里面加了一些任务,但是由于还有其他的并行任务在处理,那么我们的任务可能就处理得非常的晚了,这个是不可预知的。所以在一些重要的场合,我们可能就要使用自己的线程池,这样就不会受制于默认的线程池,避免刚才的情况出现。

代码演示:

// 使用自己的线程池, 不使用默认线程池, 防止任务被阻塞
        // 线程名字 : ForkJoinPool-1
        ForkJoinPool pool = new ForkJoinPool(6);
        pool.submit(
                () -> IntStream.range(1, 100)
                        .parallel().peek(StreamDemo5::debug)
                        .count());
        pool.shutdown();
//        下面的代码是为了使上面的线程池能在 main方法运行起来
        synchronized (pool){
            try {
                pool.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

运行看日志,使用的是我们自己的线程池


9167995-456894843bb2d5d5
image

6. 收集器

收集器就是将流处理后的数据收集起来,收集到集合类里面或者对数据进行再处理,再处理成一条,例如求和、拼接

/**
 * 收集器
 */

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.List;
import java.util.TreeSet;
import java.util.stream.Collectors;

/**
 * 学生 对象
 */
class Student {
    /**
     * 姓名
     */
    private String name;

    /**
     * 年龄
     */
    private int age;

    /**
     * 性别
     */
    private Gender gender;

    /**
     * 班级
     */
    private Grade grade;

    public Student(String name, int age, Gender gender, Grade grade) {
        super();
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.grade = grade;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

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

    public Grade getGrade() {
        return grade;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }

    public Gender getGender() {
        return gender;
    }

    public void setGender(Gender gender) {
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "[name=" + name + ", age=" + age + ", gender=" + gender
                + ", grade=" + grade + "]";
    }

}

/**
 * 性别
 */
enum Gender {
    MALE, FEMALE
}

/**
 * 班级
 */
enum Grade {
    ONE, TWO, THREE, FOUR;
}

public class CollectDemo {
    private static final Logger log = LoggerFactory.getLogger(CollectDemo.class);

    public static void main(String[] args) {
        // 测试数据
        List<Student> students = Arrays.asList(
                new Student("小明", 10, Gender.MALE, Grade.ONE),
                new Student("大明", 9, Gender.MALE, Grade.THREE),
                new Student("小白", 8, Gender.FEMALE, Grade.TWO),
                new Student("小黑", 13, Gender.FEMALE, Grade.FOUR),
                new Student("小红", 7, Gender.FEMALE, Grade.THREE),
                new Student("小黄", 13, Gender.MALE, Grade.ONE),
                new Student("小青", 13, Gender.FEMALE, Grade.THREE),
                new Student("小紫", 9, Gender.FEMALE, Grade.TWO),
                new Student("小王", 6, Gender.MALE, Grade.ONE),
                new Student("小李", 6, Gender.MALE, Grade.ONE),
                new Student("小马", 14, Gender.FEMALE, Grade.FOUR),
                new Student("小刘", 13, Gender.MALE, Grade.FOUR));

        // 得到所有学生的年龄列表
//        s -> s.getAge() --> Student::getAge , 不会多生成一个类似 lambda$0 这样的函数
        TreeSet<Integer> ages = students.stream().map(Student::getAge).collect(Collectors.toCollection(TreeSet::new));
        log.info("all ages: {}", ages);
    }
}

运行,看输出


9167995-53a6aa964c2fc014
image

再接着来看:

// 统计汇总信息
IntSummaryStatistics agesSummaryStatistics = students.stream()
        .collect(Collectors.summarizingInt(Student::getAge));
log.info("年龄汇总信息: {}", agesSummaryStatistics);

// 分块
Map<Boolean, List<Student>> genders = students.stream().collect(
        Collectors.partitioningBy(s -> s.getGender() == Gender.MALE));
MapUtils.verbosePrint(System.out, "男女学生列表", genders);
System.out.println();

// 分组
Map<Grade, List<Student>> grades = students.stream()
        .collect(Collectors.groupingBy(Student::getGrade));
MapUtils.verbosePrint(System.out, "学生班级列表", grades);
System.out.println();

// 得到所有班级学生的个数
Map<Grade, Long> gradesCount = students.stream().collect(Collectors
        .groupingBy(Student::getGrade, Collectors.counting()));
MapUtils.verbosePrint(System.out, "班级学生个数列表", gradesCount);

运行,看日志


9167995-141766f657c33bc8
image

输出的符合我们预期的结果

7. Stream 运行机制

1. 所有操作是链式调用, 一个元素只迭代一次
验证:

public class RunStream {
    private static final Logger log = LoggerFactory.getLogger(RunStream.class);
    public static void main(String[] args) {
        Random random = new Random();
//        随机产生数据
        Stream<Integer> stream = Stream.generate(() -> random.nextInt())
//        产生 10 个 ( 无限流需要短路操作. )
                .limit(10)
//        第1个无状态操作
                .peek(s -> print("peek " + s))
//        第2个无状态操作
                .filter(s -> {
                    print("filter " + s);
                    return s > 1000000;
                });
//        终止操作
        stream.count();
    }

    /**
     * 打印日志并sleep 5 毫秒
     * @param s
     */
    public static void print(String s) {
        // System.out.println(s);
        // 带线程名(测试并行情况)
        log.info( "s: {}", s);
        try {
            TimeUnit.MILLISECONDS.sleep(5);
        } catch (InterruptedException e) {

        }
    }
}

运行 main 方法看日志


9167995-b68162fe0b13918f
image

所有操作是链式调用, 一个元素只迭代一次 ,而不是先全部 peek 完,再 filter

2. 每一个中间操作返回一个新的流. 流里面有一个属性 sourceStage , 指向同一个 地方,就是Head

3. Head -> nextStage -> nextStage -> ... -> null

4. 有状态操作会把无状态操作阶段,单独处理

验证:

将 main 方法改成如下:

 public static void main(String[] args) {
        Random random = new Random();
//        随机产生数据
        Stream<Integer> stream = Stream.generate(() -> random.nextInt())
//        产生 10 个 ( 无限流需要短路操作. )
                .limit(10)
//        第1个无状态操作
                .peek(s -> print("peek " + s))
//        第2个无状态操作
                .filter(s -> {
                    print("filter " + s);
                    return s > 1000000;
                })
                .sorted((i1, i2) -> {
                    print("排序: " + i1 + ", " + i2);
                    return i1.compareTo(i2);
                })
                .peek(s -> print("peek2 " + s));
//        终止操作
        stream.count();
    }

在最后 count 这里打个断点,然后 debug 运行起来

9167995-2feccb266cdb15eb
image

再看打印的日志


9167995-61bc79c1f1d43666
image

有状态操作(排序)会把无状态操作阶段,单独处理。

可以这样理解:有状态的一般都是两个或者更多的入参,无状态就是不依赖其他数据,所以都是一个入参。

5. 并行环境下, 有状态的中间操作不一定能并行操作.

6. parallel/ sequetial 这2个操作也是中间操作(也是返回 stream ),但是他们不创建流, 他们只修改 Head的并行标志。

验证:在之前的代码,第二个 peek 后面加个 parallel 变成并行流

 public static void main(String[] args) {
        Random random = new Random();
//        随机产生数据
        Stream<Integer> stream = Stream.generate(() -> random.nextInt())
//        产生 10 个 ( 无限流需要短路操作. )
                .limit(10)
//        第1个无状态操作
                .peek(s -> print("peek " + s))
//        第2个无状态操作
                .filter(s -> {
                    print("filter " + s);
                    return s > 1000000;
                })
                .sorted((i1, i2) -> {
                    print("排序: " + i1 + ", " + i2);
                    return i1.compareTo(i2);
                })
                .peek(s -> print("peek2 " + s))
                .parallel();
//        终止操作
        stream.count();
    }

debug 运行起来,

9167995-58b4e7eb35d8b22b
image

这就是标志
然后看日志


9167995-aa95cd6731ff0d55
image

如图所示,在有状态的操作阶段,都是用 main 线程去执行,并没有并行。

归纳:

1. 所有操作是链式调用, 一个元素只迭代一次

2. 每一个中间操作返回一个新的流. 流里面有一个属性 sourceStage , 指向同一个 地方,就是Head

3. Head -> nextStage -> nextStage -> ... -> null

4. 有状态操作会把无状态操作阶段,单独处理

5. 并行环境下, 有状态的中间操作不一定能并行操作.

6. parallel/ sequetial 这2个操作也是中间操作(也是返回 stream ),但是他们不创建流, 他们只修改 Head的并行标志。

猜你喜欢

转载自blog.csdn.net/weixin_34247032/article/details/87273106