• 初识Java 12-1 流


    目录

    Java 8对流的支持

    流的创建

    随机数流

    int类型的区间范围

    generate()

    iterate()

    流生成器

    Arrays

    正则表达式


    本笔记参考自: 《On Java 中文版》


    ||| 流的概念:流是一个与任何特定的存储机制都没有关系的元素序列

            流与对象的成批处理有关。使用流的时候,我们会从一个管道Pipe,这一概念主要用于不同线程之间的通信)中抽取元素,并对它们进行操作。

        这些管道通常会被串联起来,形成这个流上的一个操作管线。

            流的一个核心优点是,它们能使我们的程序更小,也更好理解。而当配合流进行使用时,lambda表达式和方法引用能更好的发挥它们的作用。

    【例子:以有序方式显示int数】

            在该例子中,需要按照有序方式显示随机选择的5~20范围内、不重复的int数。

    1. import java.util.Random;
    2. public class Randoms {
    3. public static void main(String[] args) {
    4. new Random(47)
    5. .ints(5, 20) // 生成一个ints类型的流,范围是5~20
    6. .distinct() // 中间流操作distinct():去除重复的值
    7. .limit(7) // 选择前7个值
    8. .sorted() // 进行排序
    9. .forEach(System.out::println); // 为每一个流元素执行括号中的操作
    10. }
    11. }

            程序执行的结果如下:

            ints()方法会生成一个流,该方法有多个重载版本,其中两个元素的版本可以设置所生成值的上下界。

            注意:Randoms.java没有声明任何变量。流可以对有状态的系统进行建模,而不需要使用赋值或可变数据(在之后的例子中这种做法会经常见到)。

        上述这个例子展示了一种编程风格:声明式编程。我们说明想要完成什么(what),而不是指名怎么做(how)。

            上述例子的命令式编程形式如下,这种形式更难理解:

            这种形式需要定义3个变量,并且由于nextInt()无法确定下界,因此需要我们手动进行设定。这就体现了Randoms.java这种写法的优势:方便,并且表达清晰

            还可以这样区分上述这两个示例:

    • 外部迭代:像ImperativeRandoms.java这样显式地编写的迭代机制。
    • 内部迭代:像Randoms.java这样,我们看不见任何迭代机制。这种迭代产生的代码具有更好的可读性,并且更适用于多处理器。

            除此之外,流还有一个重要特性:惰性求值。这意味着它们只有在绝对必要时才会被求值(通常是在表达式的值被使用时)。这种延迟求值的特性,使得我们可以表示非常大的序列,而不用考虑内存问题。

    Java 8对流的支持

            早期的Java并没有引入流的概念。这使得在后来,当Java的设计者想要引入流的概念时,面对的是一整套现有的库。若想要将流这个概念融入Java,就需要向接口中放入新的方法,这无疑会破坏原有的结构。

            Java 8引入的解决方案是接口中的默认(default)方法。使用它,Java的设计者们将流方法硬塞进了现有的类中(并且放入的操作还不少)。现在,可以将Java中的流操作分为三种类型:

    1. 创建流。
    2. 修改流元素(即中间操作)。
    3. 消费流元素(即终结操作),这种操作往往意味着收集一个流的元素(通常会将收集的元素放入一个集合中)。

    流的创建

    【例子:Stream.of()的使用例】

    1. import java.util.stream.Stream;
    2. public class StreamOf {
    3. public static void main(String[] args) {
    4. Stream.of( // 在这里,构造器Bubble()只是用于生成一个普通的类
    5. new Bubble(1), new Bubble(2), new Bubble(3))
    6. .forEach(System.out::println);
    7. Stream.of("八百", "标兵", "奔", "北坡")
    8. .forEach(System.out::print);
    9. System.out.println();
    10. Stream.of(3.14159, 2.718, 1.618)
    11. .forEach(System.out::println);
    12. }
    13. }

            程序执行的结果如下:

            通过使用Stream.of(),可以轻松地将一组条目变为一个流。

            另外,Collection也包含了一个stream()操作,通过它,可以生成一个基于Collection的流:

    【例子:通过Collection生成一个流】

    1. import java.util.*;
    2. public class CollectionToStream {
    3. public static void main(String[] args) {
    4. List bubbles = Arrays.asList(
    5. new Bubble(1), new Bubble(2), new Bubble(3));
    6. System.out.println(
    7. bubbles.stream() // 通过Collection生成一个流
    8. .mapToInt(b -> b.i) // 将对象流转变成一个包含Integer的IntStream
    9. .sum());
    10. Set w = new HashSet<>(Arrays.asList(
    11. "G F E D C B A".split(" ")
    12. ));
    13. // map()操作会接受流中的每个元素,在其上应用括号中的操作来创建一个新的元素(这个新元素会继续顺着流传递下去)
    14. w.stream()
    15. .map(x -> x + " ")
    16. .forEach(System.out::print);
    17. System.out.println();
    18. Map m = new HashMap<>();
    19. m.put("pi", 3.14159);
    20. m.put("e", 2.718);
    21. m.put("phi", 1.618);
    22. m.entrySet().stream()
    23. .map(e -> e.getKey() + ": " + e.getValue())
    24. .forEach(System.out::println);
    25. }
    26. }

            程序执行的结果是:

            map()方法接受流中的每个元素,在其上应用一个操作来创建一个新的元素,然后将这个新元素沿着流继续传递下去。map()有许多不同的版本,比如mapToInt()等。

        所有集合类都有stream()方法。

            为了从Map集合中生成一个流,需要首先调用entrySet()来生成一个对象流(其中的每个对象都包含一个键和与其相关联的值)。

    随机数

            就像之前示例所示,Random类在Java 8时得到了增强,它获得了一组可以生成流的方法:

    【例子:Random类的stream()方法使用(以int类型为例)】

    1. import java.util.Random;
    2. import java.util.stream.Stream;
    3. public class RandomGenerators {
    4. public static void show(Stream stream) {
    5. stream
    6. .limit(4)
    7. .forEach(System.out::println);
    8. System.out.println("++++++++");
    9. }
    10. public static void main(String[] args) {
    11. Random rand = new Random(47);
    12. System.out.format("%n无参数时:%n");
    13. show(rand.ints().boxed()); // boxed()方法可以将基本类型转换为对应的包装器类型
    14. // 一个参数:控制流的大小
    15. System.out.format("%n一个参数时:%n");
    16. show(rand.ints(2).boxed());
    17. // 两个参数:控制上下边界
    18. System.out.format("%n两个参数时:%n");
    19. show(rand.ints(10, 20).boxed());
    20. // 三个参数:控制流的大小和边界
    21. System.out.format("%n三个参数时:%n");
    22. show(rand.ints(3, 3, 9).boxed());
    23. }
    24. }

            程序执行的结果是:

            其他类型也有可以通过类型的方式创建流。

            在上述程序中出现了这样的语句:

    public static  void show(Stream stream) {

    其中,类型参数T可以是任何东西,因此可以使用IntegerLong或者Double等)。另外,虽然Random类只会生成intdouble之类的基本类型的值。但boxed()流会自动将基本类型转换为其对应的包装器类型。

            使用Random,也可以创建一个可用于提供任何一组对象的Supplier

    【例子:使用Random创建Supplier

            要求:读取以下文本文件(Cheese.dat),生成String对象。

    1. Not much of a cheese shop really, is it?
    2. Finest in the district, sir.
    3. And what leads you to that conclusion?
    4. Well, it's so clean.
    5. It's certainly uncontaminated by cheese.

            编写程序,通过File类读取文本:

    1. import java.io.IOException;
    2. import java.nio.file.Files;
    3. import java.nio.file.Paths;
    4. import java.util.ArrayList;
    5. import java.util.List;
    6. import java.util.Random;
    7. import java.util.function.Supplier;
    8. import java.util.stream.Collectors;
    9. import java.util.stream.Stream;
    10. public class RandomWords implements Supplier {
    11. List words = new ArrayList<>();
    12. Random rand = new Random(47);
    13. RandomWords(String fname) throws IOException { // throws:声明一个方法可能抛出的异常
    14. List lines = Files.readAllLines(Paths.get(fname));
    15. // 使用的是外部迭代(可优化)
    16. for (String line : lines.subList(1, lines.size())) { // subList()方法用于去除第一行
    17. for (String word : line.split("[ .?,]+"))
    18. words.add(word.toLowerCase());
    19. }
    20. }
    21. @Override
    22. public String get() {
    23. return words.get(rand.nextInt(words.size()));
    24. }
    25. @Override
    26. public String toString() {
    27. return words.stream().collect(Collectors.joining(" "));
    28. }
    29. public static void main(String[] args) throws Exception {
    30. System.out.println(
    31. Stream.generate(
    32. new RandomWords("Cheese.dat"))
    33. .limit(10)
    34. .collect(Collectors.joining(" "))
    35. );
    36. }
    37. }

            程序执行的结果是:

            上述语句出现中,split()方法的表达式变得更加复杂了。

    line.split("[ .?,]+")

    这条语句会在遇见 ①空格 或 ②方括号内存在的标点符号 时进行分隔。另外,方括号右边的+表示其前面出现的事物是可以重复出现的。

            在toString()main()中都出现了collect()操作,这一操作会根据参数将所有的流元素组合起来。当向collect()中传入一个Collectors.joining()时,得到的结果是一个String,它会根据joining()中的参数进行分隔。

        除上面演示的之外,还存在着许多的Collectors

            注意Stream.generate(),它可以接受任何的Supplier,并生成一个由T类型的对象组成的流(这个流的长度可以看做无限长)


    int类型的区间范围

            IntStream类提供了一个range()方法,可以生成一个由int值组成的流:

    【例子:IntStream中的range()方法】

    1. import static java.util.stream.IntStream.*;
    2. public class Ranges {
    3. public static void main(String[] args) {
    4. // 传统方式:生成一个int值组成的序列
    5. int result = 0;
    6. for (int i = 10; i < 20; i++)
    7. result += i;
    8. System.out.println(result);
    9. // for-in搭配一个区间范围
    10. result = 0;
    11. for (int i : range(10, 20).toArray())
    12. result += i;
    13. System.out.println(result);
    14. // 使用流
    15. System.out.println(range(10, 20).sum());
    16. }
    17. }

            程序执行的结果如下:

            可以看出,使用流的第三种方法更加简便。

            另外,可以使用repeat()工具函数来取代简单的for循环:

    【例子:repeat()方法及其使用例】

    1. package onjava;
    2. import static java.util.stream.IntStream.*;
    3. public class Repeat {
    4. public static void repeat(int n, Runnable action) {
    5. range(0, n).forEach(i -> action.run());
    6. }
    7. }

            使循环变得更加简洁:

    1. import static onjava.Repeat.*;
    2. public class Looping {
    3. static void hi() {
    4. System.out.println("嗨!");
    5. }
    6. public static void main(String[] args) {
    7. repeat(3, ()->System.out.println("循环中..."));
    8. repeat(2, Looping::hi);
    9. }
    10. }

            程序执行的结果是:


    generate()

            再来看一个generate()方法的使用例:

    【例子:generate()方法的使用例】

    1. import java.util.Random;
    2. import java.util.function.Supplier;
    3. import java.util.stream.Collectors;
    4. import java.util.stream.Stream;
    5. public class Generator implements Supplier {
    6. Random rand = new Random(47);
    7. char[] letters = "ABCDEFGHIJKLMN".toCharArray();
    8. @Override
    9. public String get() {
    10. return "" + letters[rand.nextInt(letters.length)];
    11. }
    12. public static void main(String[] args) {
    13. String word = Stream.generate(new Generator())
    14. .limit(30) // 选择前30个值
    15. .collect(Collectors.joining()); // collect()在后台最终会调用Supplier<>的get()(可查看堆栈)
    16. System.out.println(word);
    17. }
    18. }

            程序执行的结果是:

            在官方文档中,有关于generate()的说明:

    这个方法会返回一个无限的、连续的并且没有顺序的流,其中的每个元素有Supplier生成。

            若想要创建一个由完全相同的对象组成的流,只需要将一个生成这些对象的lambda表达式传递给generate()即可:

    1. import java.util.stream.Stream;
    2. public class Duplicator {
    3. public static void main(String[] args) {
    4. Stream.generate(() -> "duplicate")
    5. .limit(3)
    6. .forEach(System.out::println);
    7. }
    8. }

            程序执行的结果是:

            Java会根据我们传入的lambda表达式(方法引用也是),在底层创建一个实现了目标接口的类的示例。因此generate()可以接受一个lambda表达式。

        复习:Supplier是一个函数式接口。

            接下来展示的是之前提到过的Bubble类,它包含了自己的静态生成器方法

    1. public class Bubble {
    2. public final int i;
    3. public Bubble(int n) {
    4. i = n;
    5. }
    6. @Override
    7. public String toString() {
    8. return "Bubble(" + i + ")";
    9. }
    10. private static int count = 0;
    11. public static Bubble bubbler() {
    12. return new Bubble(count++);
    13. }
    14. }

            在这里,bubbler()方法能够与Supplier接口相兼容,理由如下(参考讯飞星火):

    • bubbler()方法是静态的(可用于静态方法引用),并且返回类型是Bubble
    • 无参,与Supplier的无参构造器相匹配。
    • bubbler()的设计目的与Supplier的相符合,即提供一个无参数的工厂方法来生成特定类型的对象。

    因此可以将该方法引用传递给Stream.generate()

    1. import java.util.stream.Stream;
    2. public class Bubbles {
    3. public static void main(String[] args) {
    4. Stream.generate(Bubble::bubbler)
    5. .limit(5)
    6. .forEach(System.out::println);
    7. }
    8. }

            程序执行的结果是:

            这是创建一个单独的工厂方法的一个替代方案。


    iterate()

            Stream.iterate()的官方描述是这样的:

    这个方法会从第一个种子(seed,即第一个参数)开始,将其传递给第二个参数所引用的方法。方法的结果会被添加到这个流上,并被保存下来作为下一次iterate()调用的第一个参数,以此类推。

        iterate()方法还有一个3参数的版本,多了一个用于筛选的谓词(Predicate)。

    【例子:斐波那契数列】

    1. import java.util.stream.Stream;
    2. public class Fibonacci {
    3. int x = 1;
    4. Stream numbers() {
    5. return Stream.iterate(0, // 0被传递给i,作为i的初始值
    6. i -> { // i中储存的是return语句的返回值
    7. int result = x + i;
    8. x = i; // 需要使用一个x来保存另一个数值
    9. return result;
    10. });
    11. }
    12. public static void main(String[] args) {
    13. new Fibonacci().numbers()
    14. .skip(20) // 跳过前20个数据
    15. .limit(10) // 然后再从中取出前10个
    16. .forEach(System.out::println);
    17. }
    18. }

            程序执行的结果是:

            斐波那契数列将数列中的最后两个元素相加,生成下一个元素。由于iterate()只会记住结果(result),因此需要使用x来记住另一个元素。


    流生成器

            在生成器(Builder)设计模式中,我们会创建一个生成器对象,为该对象提供多段构造信息,最终执行“生成”动作。Stream库提供了这样一个Builder

    【例子:Builder的使用例】

    1. import java.nio.file.Files;
    2. import java.nio.file.Paths;
    3. import java.util.stream.Stream;
    4. public class FileToWordsBuilder {
    5. Stream.Builder builder = Stream.builder();
    6. public FileToWordsBuilder(String filePath)
    7. throws Exception {
    8. Files.lines(Paths.get(filePath))
    9. .skip(1) // 跳过开头行
    10. .forEach(line -> {
    11. for (String w : line.split("[ .?,]+"))
    12. builder.add(w); // 从文件中读取的数据需要被分解成流中的一个个元素
    13. });
    14. }
    15. Stream stream() {
    16. return builder.build();
    17. }
    18. public static void main(String[] args) throws Exception {
    19. new FileToWordsBuilder("Cheese.dat").stream() // 若Cheese.dat不在当前文件夹中,可使用绝对路径等进行指引
    20. .limit(7)
    21. .map(w -> w + " ")
    22. .forEach(System.out::print);
    23. }
    24. }

            程序执行的结果是:

            注意,在构造器添加文件中的单词时,它没有调用build()。这意味着,只要不调用stream()方法,就可以继续往builder对象中添加单词。

        可以加入一个标志来查看build()是否已经被调用,加入一个方法在可能的情况下继续添加单词。


    Arrays

            Arrays类中同样包含了名为stream()的静态方法,可以将数组转换成流。

    【例子:Arraysstream()

    1. import java.util.Arrays;
    2. import onjava.Operation;
    3. public class MetalWork2 {
    4. public static void main(String[] args) {
    5. Arrays.stream(new Operation[]{
    6. () -> Operation.show("Heat"),
    7. () -> Operation.show("Hammer"),
    8. () -> Operation.show("Twist"),
    9. () -> Operation.show("Anneal"),
    10. }).forEach(Operation::execute); // execute负责执行流中每个元素对应的操作
    11. }
    12. }

            程序执行的结果是:

            上述例子中出现的Operation接口定义如下:

    这是一个函数式接口,定义了show()方法和runOps()方法。

            new Operation[]表达式动态地创建了一个由Operation对象组成的类型化数组。forEach()操作会为数组中的每一个元素执行execute()。也就是说,在没有进入这条ForEach()操作之前,都可以继续往Operation[]中添加内容。

            使用stream()方法也可以生成IntStreamLongStreamDoubleStream

    【例子:stream()方法生成其他Stream

    1. import java.lang.reflect.Array;
    2. import java.util.Arrays;
    3. public class ArrayStreams {
    4. public static void main(String[] args) {
    5. Arrays.stream(
    6. new double[]{3.14159, 2.718, 1.618})
    7. .forEach(n -> System.out.format("%f ", n));
    8. System.out.println();
    9. Arrays.stream(new int[]{1, 3, 5})
    10. .forEach(n -> System.out.format("%d ", n));
    11. System.out.println();
    12. Arrays.stream(new long[]{11, 22, 33, 44})
    13. .forEach(n -> System.out.format("%d ", n));
    14. System.out.println();
    15. // 选择一个子区间:
    16. Arrays.stream(
    17. new int[]{1, 3, 5, 7, 15, 28, 37}, 3, 6)
    18. .forEach(n->System.out.format("%d ", n));
    19. System.out.println();
    20. }
    21. }

            程序执行的结果是:

            最后出现的语句

    (new int[]{1, 3, 5, 7, 15, 28, 37}, 3, 6)

    多使用了两个额外的参数:第一个参数告诉stream()从数组哪个位置开始选择元素,第二个参数告诉stream()在哪里停下。


    正则表达式

            Java 8向java.util.regex.Patern类中加入了一个新方法:splitAsStream()。这个新方法接受一个字符序列,并根据我们传入的公式将其分割为一个流。

        splitAsStream()有一个约束,其输入应该是一个CharSequence,因此我们不能将一个流传入splitAsStream()中。

    【例子:利用正则表达式切割String

    1. import java.nio.file.Files;
    2. import java.nio.file.Paths;
    3. import java.util.regex.Pattern;
    4. import java.util.stream.Collectors;
    5. import java.util.stream.Stream;
    6. public class FileToWordsRegexp {
    7. private String all;
    8. public FileToWordsRegexp(String filepath)
    9. throws Exception {
    10. all = Files.lines(Paths.get(filepath))
    11. .skip(1) // 跳过第一行
    12. .collect(Collectors.joining(" "));
    13. }
    14. public Stream stream() {
    15. return Pattern.compile("[ ,.?]+").splitAsStream(all);
    16. }
    17. public static void main(String[] args)
    18. throws Exception {
    19. FileToWordsRegexp fw = new FileToWordsRegexp("Cheese.dat");
    20. fw.stream()
    21. .limit(7)
    22. .map(w -> w + " ")
    23. .forEach(System.out::print);
    24. System.out.println();
    25. fw.stream()
    26. .skip(7)
    27. .limit(2)
    28. .map(w -> w + " ")
    29. .forEach(System.out::print);
    30. System.out.println();
    31. }
    32. }

            程序执行的结果是:

            构造器读取了文件中的信息,将其转入一个String中。在这里,我们可以多次回头调用stream(),并且每次都可以从保存的String中创建一个新的流。

            这个例子存在一个缺点:被读取的整个文件都要存储在内存中。这会导致流的两个优势(占据极少的内部存储,以及惰性求值)无法发挥其的作用。

  • 相关阅读:
    百趣代谢组学资讯:项目文章Nature,揭示低温暴露抑制实体瘤生长机制,‘饿死’癌细胞
    满级大牛 HuaWei 首次出这份 598 页【网络协议全彩手册】,建议大家收藏
    基于PHP+MySQL高校教务选课系统的设计与实现
    (229)Verilog HDL:与运算
    fpga图像处理------常用算法(二)
    处理非线性分类的 SVM一种新方法(Matlab代码实现)
    drone ci 是什么
    济南槐荫吴家堡 国稻种芯·中国水稻节:山东稻出黄河大米
    java毕设项目网上图书分享系统(附源码)
    创新指南|如何以STEPPS模型6招打造病毒式传播产品
  • 原文地址:https://blog.csdn.net/w_pab/article/details/133393055