原文:http://zetcode.com/lang/java/streams/

在 Java 教程的这一部分中,我们将使用流。 流极大地改善了 Java 中数据的处理。

Java 流定义

流是来自源的一系列元素,支持顺序和并行聚合操作。 常见的聚合操作是:过滤,映射,缩小,查找,匹配和排序。 源可以是将数据提供给流的集合,IO 操作或数组。

Java 集合是一种内存中的数据结构,所有元素都包含在内存中,而流是一种数据结构,其中的所有元素都是按需计算的。 与显式迭代的集合(外部迭代)相反,流操作为我们在后台进行迭代。 从 Java8 开始,Java 集合具有stream()方法,该方法从集合中返回流。

Stream接口在java.util.stream包中定义。

对流进行的操作会在不修改其源的情况下产生结果。

流的特征

  • 流不存储数据; 相反,它们从诸如集合,数组或 IO 通道之类的源中提供数据。
  • 流不修改数据源。 例如,在执行过滤操作时,它们会将数据转换为新的流。
  • 许多流操作是延迟求值的。 这允许自动代码优化和短路求值。
  • 流可以是无限的。 诸如limit()之类的方法使我们可以从无限流中获得一些结果。
  • 在流的生存期内,流的元素只能访问一次。 像Iterator一样,必须生成新的流以重新访问源中的相同元素。
  • 流具有用于流元素内部迭代的方法,例如forEach()forEachOrdered()
  • 流支持类似 SQL 的操作和常用函数式操作,例如过滤,映射,缩小,查找,匹配和排序。

Java 流管道

流管道由源,中间操作和终端操作组成。 中间操作返回新的修改后的流; 因此,可以链接多个中间操作。 另一方面,终端操作返回void或一个值。 终端操作后,将无法再使用该流。 使终端操作短路意味着流可以在处理所有值之前终止。 如果流是无限的,这很有用。

中间操作是懒惰的。 在执行终端操作之前,它们不会被调用。 当我们处理较大的数据流时,这可以提高性能。

Java 创建流

流是从各种源创建的,例如集合,数组,字符串,IO 资源或生成器。

CreatingStreams.java

  1. package com.zetcode;
  2. import java.util.Arrays;
  3. import java.util.List;
  4. import java.util.stream.IntStream;
  5. import java.util.stream.Stream;
  6. public class CreatingStreams {
  7. public static void main(String[] args) {
  8. List<String> words = Arrays.asList("pen", "coin", "desk", "chair");
  9. String word = words.stream().findFirst().get();
  10. System.out.println(word);
  11. Stream<String> letters = Arrays.stream(new String[]{ "a", "b", "c"});
  12. System.out.printf("There are %d letters%n", letters.count());
  13. String day = "Sunday";
  14. IntStream istr = day.codePoints();
  15. String s = istr.filter(e -> e != 'n').collect(StringBuilder::new,
  16. StringBuilder::appendCodePoint, StringBuilder::append).toString();
  17. System.out.println(s);
  18. }
  19. }

在此示例中,我们使用从列表,数组和字符串创建的流。

  1. List<String> words = Arrays.asList("pen", "coin", "desk", "chair");

将创建一个字符串列表。

  1. String word = words.stream().findFirst().get();

使用stream方法,我们从列表集合创建一个流。 在流上,我们调用findFirst()方法,该方法返回流的第一个元素。 (它返回一个Optional,我们使用get()方法从中获取值。)

  1. Stream<String> letters = Arrays.stream(new String[]{ "a", "b", "c"});
  2. System.out.printf("There are %d letters%n", letters.count());

我们从数组创建流。 流的count()方法返回流中的元素数。

  1. String day = "Sunday";
  2. IntStream istr = day.codePoints();
  3. String s = istr.filter(e -> e != 'n').collect(StringBuilder::new,
  4. StringBuilder::appendCodePoint, StringBuilder::append).toString();
  5. System.out.println(s);

在这里,我们从字符串创建流。 我们过滤字符并从过滤的字符构建新的字符串。

  1. $ java com.zetcode.CreatingStreams
  2. pen
  3. There are 3 letters
  4. Suday

这是输出。

Stream有三种数值流:IntStreamDoubleStreamLongStream

CreatingStreams2.java

  1. package com.zetcode;
  2. import java.util.stream.DoubleStream;
  3. import java.util.stream.IntStream;
  4. import java.util.stream.LongStream;
  5. public class CreatingStreams2 {
  6. public static void main(String[] args) {
  7. IntStream integers = IntStream.rangeClosed(1, 16);
  8. System.out.println(integers.average().getAsDouble());
  9. DoubleStream doubles = DoubleStream.of(2.3, 33.1, 45.3);
  10. doubles.forEachOrdered(e -> System.out.println(e));
  11. LongStream longs = LongStream.range(6, 25);
  12. System.out.println(longs.count());
  13. }
  14. }

该示例适用于上述三个类。

  1. IntStream integers = IntStream.rangeClosed(1, 16);
  2. System.out.println(integers.average().getAsDouble());

使用IntStream.rangeClosed()方法创建整数流。 我们将其平均值打印到控制台。

  1. DoubleStream doubles = DoubleStream.of(2.3, 33.1, 45.3);
  2. doubles.forEachOrdered(e -> System.out.println(e));

使用DoubleStream.of()方法创建双精度值流。 我们使用forEachOrdered()方法将元素的有序列表打印到控制台。

  1. LongStream longs = LongStream.range(6, 25);
  2. System.out.println(longs.count());

LongStream.range()方法创建一个长整数的字符串。 我们使用count()方法打印元素的数量。

  1. $ java com.zetcode.CreatingStreams2
  2. 8.5
  3. 2.3
  4. 33.1
  5. 45.3
  6. 19

这是示例的输出。

Stream.of()方法返回其元素为指定值的顺序有序流。

CreatingStreams3.java

  1. package com.zetcode;
  2. import java.util.Comparator;
  3. import java.util.stream.Stream;
  4. public class CreatingStreams3 {
  5. public static void main(String[] args) {
  6. Stream<String> colours = Stream.of("red", "green", "blue");
  7. String col = colours.skip(2).findFirst().get();
  8. System.out.println(col);
  9. Stream<Integer> nums = Stream.of(3, 4, 5, 6, 7);
  10. int maxVal = nums.max(Comparator.naturalOrder()).get();
  11. System.out.println(maxVal);
  12. }
  13. }

在示例中,我们使用Stream.of()方法创建两个流。

  1. Stream<String> colours = Stream.of("red", "green", "blue");

将创建三个字符串流。

  1. String col = colours.skip(2).findFirst().get();

使用skip()方法,我们跳过了两个元素,而使用findFirst()方法只找到了一个元素。

  1. Stream<Integer> nums = Stream.of(3, 4, 5, 6, 7);
  2. int maxVal = nums.max(Comparator.naturalOrder()).get();

我们创建一个整数流并找到其最大数目。输出如下:

  1. $ java com.zetcode.CreatingStreams3
  2. blue
  3. 7

创建流的其他方法是:Stream.iterate()Stream.generate()

CreatingStreams4.java

  1. package com.zetcode;
  2. import java.util.Random;
  3. import java.util.stream.Stream;
  4. public class CreatingStreams4 {
  5. public static void main(String[] args) {
  6. Stream<Integer> s1 = Stream.iterate(5, n -> n * 2).limit(10);
  7. s1.forEach(System.out::println);
  8. Stream.generate(new Random()::nextDouble)
  9. .map(e -> (e * 10))
  10. .limit(5)
  11. .forEach(System.out::println);
  12. }
  13. }

在示例中,我们使用Stream.iterate()Stream.generate()创建两个流。

  1. Stream<Integer> s1 = Stream.iterate(5, n -> n * 2).limit(10);
  2. s1.forEach(System.out::println);

Stream.iterate()返回通过将函数迭代应用到初始元素而产生的无限顺序有序流。 初始元素称为种子。 通过将函数应用于第一个元素来生成第二个元素。 通过将函数应用于第二个元素等来生成第三个元素。

  1. Stream.generate(new Random()::nextDouble)
  2. .map(e -> (e * 10))
  3. .limit(5)
  4. .forEach(System.out::println);

使用Stream.generate()方法创建五个随机双打的流。 每个元素乘以十。 最后,我们遍历流并将每个元素打印到控制台。输出如下:

  1. $ java com.zetcode.CreatingStreams4
  2. 5
  3. 10
  4. 20
  5. 40
  6. 80
  7. 160
  8. 320
  9. 640
  10. 1280
  11. 2560
  12. 8.704675577530493
  13. 5.732011478196306
  14. 3.8978402578067515
  15. 3.6986033299500933
  16. 6.0976417139147205

可以从文件创建流。

CreatingStreams5.java

  1. package com.zetcode;
  2. import java.io.IOException;
  3. import java.nio.file.Files;
  4. import java.nio.file.Path;
  5. import java.nio.file.Paths;
  6. import java.util.stream.Stream;
  7. public class CreatingStreams5 {
  8. public static void main(String[] args) throws IOException {
  9. Path path = Paths.get("/home/janbodnar/myfile.txt");
  10. Stream<String> stream = Files.lines(path);
  11. stream.forEach(System.out::println);
  12. }
  13. }

该示例读取文件并使用流打印其内容。

  1. Path path = Paths.get("/home/janbodnar/myfile.txt");

使用Paths.get()方法创建Path对象。 Path对象用于在文件系统中定位文件。

  1. Stream<String> stream = Files.lines(path);

从路径开始,我们使用Files.lines()方法创建一个流; 流的每个元素都是文件中的一行。

  1. stream.forEach(System.out::println);

我们浏览流中的元素并将它们打印到控制台。

内部和外部迭代

根据谁控制迭代过程,我们区分外部和内部迭代。 外部迭代,也称为活动或显式迭代,由程序员处理。 在 Java8 之前,它是 Java 中唯一的迭代类型。 对于外部迭代,我们使用forwhile循环。 内部迭代(也称为被动迭代或隐式迭代)由迭代器本身控制。 Java 流中提供了内部迭代。

ExternalIteration.java

  1. package com.zetcode;
  2. import java.util.Arrays;
  3. import java.util.Iterator;
  4. import java.util.List;
  5. public class ExternalIteration {
  6. public static void main(String[] args) {
  7. List<String> words = Arrays.asList("pen", "coin", "desk",
  8. "eye", "bottle");
  9. Iterator it = words.iterator();
  10. while (it.hasNext()) {
  11. System.out.println(it.next());
  12. }
  13. }
  14. }

在代码示例中,我们从字符串列表中检索和迭代器对象。 在while循环中使用迭代器的hasNext()next()方法,我们迭代列表的元素。

在下面的示例中,我们使用外部迭代来迭代相同的列表。

InternalIteration.java

  1. package com.zetcode;
  2. import java.util.Arrays;
  3. import java.util.List;
  4. public class InternalIteration {
  5. public static void main(String[] args) {
  6. List<String> words = Arrays.asList("pen", "coin", "desk",
  7. "eye", "bottle");
  8. words.stream().forEach(System.out::println);
  9. }
  10. }

在示例中,我们从列表创建流。 我们使用流的forEach()在内部对流元素进行迭代。

Java 流过滤器

过滤数据流是流最重要的功能之一。 filter()方法是一个中间操作,它返回由与给定谓词匹配的流元素组成的流。 谓词是一种返回布尔值的方法。

FilterStream.java

  1. package com.zetcode;
  2. import java.util.Arrays;
  3. import java.util.stream.IntStream;
  4. public class FilterStream {
  5. public static void main(String[] args) {
  6. IntStream nums = IntStream.rangeClosed(0, 25);
  7. int[] vals = nums.filter(e -> e > 15).toArray();
  8. System.out.println(Arrays.toString(vals));
  9. }
  10. }

该代码示例创建一个整数流。 流被过滤为仅包含大于 15 的值。

  1. IntStream nums = IntStream.rangeClosed(0, 25);

使用IntStream,创建了 26 个整数的流。 rangeClose()方法从两个值的边界创建整数流; 这两个值(开始和结束)都包含在范围内。

  1. int[] vals = nums.filter(e -> e > 15).toArray();

我们将 lambda 表达式(e -> e > 15)传递给filter()函数; 对于大于 15 的值,该表达式返回truetoArray()是将流转换为整数数组的终端操作。

  1. System.out.println(Arrays.toString(vals));

将数组打印到控制台。

  1. $ java com.zetcode.FilterStream
  2. [16, 17, 18, 19, 20, 21, 22, 23, 24, 25]

该示例产生此输出。

下一个示例生成事件编号列表。

FilterStream2.java

  1. package com.zetcode;
  2. import java.util.stream.IntStream;
  3. public class FilterStream2 {
  4. public static void main(String[] args) {
  5. IntStream nums = IntStream.rangeClosed(0, 30);
  6. nums.filter(FilterStream2::isEven).forEach(System.out::println);
  7. }
  8. private static boolean isEven(int e) {
  9. return e % 2 == 0;
  10. }
  11. }

为了从流中获得偶数,我们将isEven()方法引用传递给filter()方法。

  1. nums.filter(FilterStream2::isEven).forEach(System.out::println);

双冒号 :: 运算符用于传递方法引用。 forEach()方法是对流的元素进行迭代的终端操作。 它引用了System.out.println()方法的方法。

跳过和限制元素

skip(n)方法跳过流的前n个元素,limit(m)方法将流中的元素数限制为m

SkipLimit.java

  1. package com.zetcode;
  2. import java.util.stream.IntStream;
  3. public class SkipLimit {
  4. public static void main(String[] args) {
  5. IntStream s = IntStream.range(0, 15);
  6. s.skip(3).limit(5).forEach(System.out::println);
  7. }
  8. }

该示例创建了一个十五个整数的流。 我们使用skip()方法跳过前三个元素,并将元素个数限制为 5。输出如下:

  1. $ java com.zetcode.SkipLimit
  2. 3
  3. 4
  4. 5
  5. 6
  6. 7

Java 流排序元素

sorted()方法根据提供的Comparator对该流的元素进行排序。

Sorting.java

  1. package com.zetcode;
  2. import java.util.Comparator;
  3. import java.util.stream.IntStream;
  4. public class Sorting {
  5. public static void main(String[] args) {
  6. IntStream nums = IntStream.of(4, 3, 2, 1, 8, 6, 7, 5);
  7. nums.boxed().sorted(Comparator.reverseOrder())
  8. .forEach(System.out::println);
  9. }
  10. }

该示例按降序对整数元素进行排序。 boxed()方法将IntStream转换为Stream<Integer>。输出如下:

  1. $ java com.zetcode.Sorting
  2. 8
  3. 7
  4. 6
  5. 5
  6. 4
  7. 3
  8. 2
  9. 1

下一个示例显示如何比较对象流。

Sorting2.java

  1. package com.zetcode;
  2. import java.util.Arrays;
  3. import java.util.Comparator;
  4. import java.util.List;
  5. class Car {
  6. private String name;
  7. private int price;
  8. public Car(String name, int price ) {
  9. this.name = name;
  10. this.price = price;
  11. }
  12. public String getName() {
  13. return name;
  14. }
  15. public void setName(String name) {
  16. this.name = name;
  17. }
  18. public int getPrice() {
  19. return price;
  20. }
  21. public void setPrice(int price) {
  22. this.price = price;
  23. }
  24. @Override
  25. public String toString() {
  26. return "Car{" + "name=" + name + ", price=" + price + '}';
  27. }
  28. }
  29. public class Sorting2 {
  30. public static void main(String[] args) {
  31. List<Car> cars = Arrays.asList(new Car("Citroen", 23000),
  32. new Car("Porsche", 65000), new Car("Skoda", 18000),
  33. new Car("Volkswagen", 33000), new Car("Volvo", 47000));
  34. cars.stream().sorted(Comparator.comparing(Car::getPrice))
  35. .forEach(System.out::println);
  36. }
  37. }

该示例按价格对汽车进行排序。

  1. List<Car> cars = Arrays.asList(new Car("Citroen", 23000),
  2. new Car("Porsche", 65000), new Car("Skoda", 18000),
  3. new Car("Volkswagen", 33000), new Car("Volvo", 47000));

将创建汽车列表。

  1. cars.stream().sorted(Comparator.comparing(Car::getPrice))
  2. .forEach(System.out::println);

使用stream()方法从列表中生成流。 我们传递了CargetPrice()方法的引用,该方法在按汽车价格进行比较时使用。输出如下:

  1. $ java com.zetcode.Sorting2
  2. Car{name=Skoda, price=18000}
  3. Car{name=Citroen, price=23000}
  4. Car{name=Volkswagen, price=33000}
  5. Car{name=Volvo, price=47000}
  6. Car{name=Porsche, price=65000}

Java 流唯一值

distinct()方法返回由唯一元素组成的流。

UniqueElements.java

  1. package com.zetcode;
  2. import java.util.Arrays;
  3. import java.util.stream.IntStream;
  4. public class UniqueElements {
  5. public static void main(String[] args) {
  6. IntStream nums = IntStream.of(1, 1, 3, 4, 4, 6, 7, 7);
  7. int a[] = nums.distinct().toArray();
  8. System.out.println(Arrays.toString(a));
  9. }
  10. }

该示例从整数流中删除重复的值。

  1. IntStream nums = IntStream.of(1, 1, 3, 4, 4, 6, 7, 7);

流中有三个重复的值。

  1. int a[] = nums.distinct().toArray();

我们使用distinct()方法删除重复项。输出如下:

  1. $ java com.zetcode.UniqueElements
  2. [1, 3, 4, 6, 7]

Java 流映射

可以将元素更改为新的流; 原始来源未修改。 map()方法返回一个流,该流由将给定函数应用于流的元素的结果组成。 map()是一个中间操作。

Mapping.java

  1. package com.zetcode;
  2. import java.util.Arrays;
  3. import java.util.stream.IntStream;
  4. public class Mapping {
  5. public static void main(String[] args) {
  6. IntStream nums = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8);
  7. int[] squares = nums.map(e -> e * e).toArray();
  8. System.out.println(Arrays.toString(squares));
  9. }
  10. }

我们在流的每个元素上映射一个转换函数。

  1. int[] squares = nums.map(e -> e * e).toArray();

我们在流上应用一个 lambda 表达式(e -> e * e):每个元素都是平方的。 创建一个新的流,并使用toArray()方法将其转换为数组。输出如下:

  1. $ java com.zetcode.Mapping
  2. [1, 4, 9, 16, 25, 36, 49, 64]

在下一个示例中,我们转换字符串流。

Mapping2.java

  1. package com.zetcode;
  2. import java.util.stream.Stream;
  3. public class Mapping2 {
  4. public static void main(String[] args) {
  5. Stream<String> words = Stream.of("cardinal", "pen", "coin", "globe");
  6. words.map(Mapping2::capitalize).forEach(System.out::println);
  7. }
  8. private static String capitalize(String word) {
  9. word = word.substring(0, 1).toUpperCase() + word.substring(1).toLowerCase();
  10. return word;
  11. }
  12. }

我们有一串串的字符串。 我们将流中的每个字符串都大写。

  1. words.map(Mapping2::capitalize).forEach(System.out::println);

我们将对capitalize()方法的引用传递给map()方法。输出如下:

  1. $ java com.zetcode.Mapping2
  2. Cardinal
  3. Pen
  4. Coin
  5. Globe

Java 流归约

归约是将流聚合为类或原始类型的终端操作。

Reduction.java

  1. package com.zetcode;
  2. import java.util.stream.IntStream;
  3. public class Reduction {
  4. public static void main(String[] args) {
  5. IntStream nums = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8);
  6. int maxValue = nums.max().getAsInt();
  7. System.out.printf("The maximum value is: %d%n", maxValue);
  8. }
  9. }

从整数流中获取最大值是一种归约运算。

  1. int maxValue = nums.max().getAsInt();

使用max()方法,我们获得了流的最大元素。 该方法返回一个Optional,使用getAsInt()方法从中获得整数。输出如下:

  1. $ java com.zetcode.Reduction
  2. The maximum value is: 8

可以使用reduce()方法创建自定义归约。

Reduction2.java

  1. package com.zetcode;
  2. import java.util.stream.IntStream;
  3. public class Reduction2 {
  4. public static void main(String[] args) {
  5. IntStream nums = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8);
  6. int product = nums.reduce((a, b) -> a * b).getAsInt();
  7. System.out.printf("The product is: %d%n", product);
  8. }
  9. }

该示例返回流中整数元素的乘积。输出如下:

  1. $ java com.zetcode.Reduction
  2. The product is: 40320

Java 流收集操作

收集是一种终端归约操作,可将流的元素还原为 Java 集合,字符串,值或特定的分组。

Collecting.java

  1. package com.zetcode;
  2. import java.util.Arrays;
  3. import java.util.List;
  4. import java.util.stream.Collectors;
  5. class Car {
  6. private String name;
  7. private int price;
  8. public Car(String name, int price) {
  9. this.name = name;
  10. this.price = price;
  11. }
  12. public String getName() {
  13. return name;
  14. }
  15. public void setName(String name) {
  16. this.name = name;
  17. }
  18. public int getPrice() {
  19. return price;
  20. }
  21. public void setPrice(int price) {
  22. this.price = price;
  23. }
  24. @Override
  25. public String toString() {
  26. return "Car{" + "name=" + name + ", price=" + price + '}';
  27. }
  28. }
  29. public class Collecting {
  30. public static void main(String[] args) {
  31. List<Car> cars = Arrays.asList(new Car("Citroen", 23000),
  32. new Car("Porsche", 65000), new Car("Skoda", 18000),
  33. new Car("Volkswagen", 33000), new Car("Volvo", 47000));
  34. List<String> names = cars.stream().map(Car::getName)
  35. .filter(name -> name.startsWith("Vo"))
  36. .collect(Collectors.toList());
  37. for (String name: names) {
  38. System.out.println(name);
  39. }
  40. }
  41. }

该示例从汽车对象列表创建流,按汽车名称过滤汽车,并返回匹配的汽车名称列表。

  1. List<String> names = cars.stream().map(Car::getName)
  2. .filter(name -> name.startsWith("Vo"))
  3. .collect(Collectors.toList());

在管道的最后,我们使用collect()方法进行转换。 输出如下:

  1. $ java com.zetcode.Collecting
  2. Volkswagen
  3. Volvo

在下一个示例中,我们使用collect()方法对数据进行分组。

Collecting2.java

  1. package com.zetcode;
  2. import java.util.Arrays;
  3. import java.util.List;
  4. import java.util.Map;
  5. import java.util.function.Function;
  6. import java.util.stream.Collectors;
  7. public class Collecting2 {
  8. public static void main(String[] args) {
  9. List<String> items = Arrays.asList("pen", "book", "pen", "coin",
  10. "book", "desk", "book", "pen", "book", "coin");
  11. Map<String, Long> result = items.stream().collect(
  12. Collectors.groupingBy(
  13. Function.identity(), Collectors.counting()
  14. ));
  15. for (Map.Entry<String, Long> entry : result.entrySet()) {
  16. String key = entry.getKey();
  17. Long value = entry.getValue();
  18. System.out.format("%s: %d%n", key, value);
  19. }
  20. }
  21. }

该代码示例按元素在流中的出现将其分组。

  1. Map<String, Long> result = items.stream().collect(
  2. Collectors.groupingBy(
  3. Function.identity(), Collectors.counting()
  4. ));

使用Collectors.groupingBy()方法,我们可以计算流中元素的出现次数。 该操作返回一个映射。

  1. for (Map.Entry<String, Long> entry : result.entrySet()) {
  2. String key = entry.getKey();
  3. Long value = entry.getValue();
  4. System.out.format("%s: %d%n", key, value);
  5. }

我们浏览映射并打印其键/值对。输出如下:

  1. $ java com.zetcode.Collecting2
  2. desk: 1
  3. book: 4
  4. pen: 3
  5. coin: 2

Java 教程的这一部分介绍了流。