操作符

流操作符

Java Stream 中最核心的就是操作符,往往某个 Stream 由多个中间操作符(Intermediate Operations)以及最终操作符(Terminal Operation)构成。中间操作符会依次获取并且处理元素,所有的最终操作符都是懒操作的,仅在流启动之后才会工作。

  • Intermediate: map (mapToInt, flatMap 等)、filter、distinct、sorted、peek、limit、skip、parallel、sequential、unordered

  • Terminal: forEach、forEachOrdered、toArray、reduce、collect、min、max、count、anyMatch、allMatch、noneMatch、findFirst、findAny、iterator

  • Short-circuiting: anyMatch、allMatch、noneMatch、findFirst、findAny、limit

Intermediate

Filter

Filter 接受一个 predicate 接口类型的变量,并将所有流对象中的元素进行过滤。该操作是一个中间操作,因此它允许我们在返回结果的基础上再进行其他的流操作(forEach)。ForEach 接受一个 function 接口类型的变量,用来执行对每一个元素的操作。ForEach 是一个中止操作。它不返回流,所以我们不能再调用其他的流操作。

stringCollection
    .stream()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);
// "aaa2", "aaa1"

Sorted

Sorted 是一个中间操作,能够返回一个排过序的流对象的视图。流对象中的元素会默认按照自然顺序进行排序,除非你自己指定一个 Comparator 接口来改变排序规则。

stringCollection
    .stream()
    .sorted()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);
// "aaa1", "aaa2"

一定要记住,sorted 只是创建一个流对象排序的视图,而不会改变原来集合中元素的顺序。原来 string 集合中的元素顺序是没有改变的。

System.out.println(stringCollection);
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1

Map

map 是一个对于流对象的中间操作,通过给定的方法,它能够把流对象中的每一个元素对应到另外一个对象上。下面的例子就演示了如何把每个 string 都转换成大写的 string. 不但如此,你还可以把每一种对象映射成为其他类型。对于带泛型结果的流对象,具体的类型还要由传递给 map 的泛型方法来决定。

stringCollection
    .stream()
    .map(String::toUpperCase)
    .sorted((a, b) -> b.compareTo(a))
    .forEach(System.out::println);

map()方法将底层序列包装在一个 Stream 实例中,而 flatMap()方法可以避免嵌套的 Stream<Stream>结构。在下面的例子中,map()产生了一个由对输入 Stream 的元素应用 toUpperCase()方法的结果组成的 Stream。

List<String> myList = Stream.of("a", "b")
  .map(String::toUpperCase)
  .collect(Collectors.toList());
assertEquals(asList("A", "B"), myList);

在 Java 8 中,我们可以使用 flatMap 将上述 2 个级别的 Stream 转换成一个 Stream 级别,或者将一个 2d 数组转换成一个 1d 数组。

List<List<String>> list = Arrays.asList(
  Arrays.asList("a"),
  Arrays.asList("b"));
System.out.println(list);

System.out.println(list
  .stream()
  .flatMap(Collection::stream)
  .collect(Collectors.toList()));

// 案例:字符串转化
String[][] array = new String[][]{{"a", "b"}, {"c", "d"}, {"e", "f"}};

List<String> collect = Stream.of(array)     // Stream<String[]>
        .flatMap(Stream::of)                // Stream<String>
        .filter(x -> !"a".equals(x))        // filter out the a
        .collect(Collectors.toList());      // return a List

collect.forEach(System.out::println);

// 案例:文件按行处理,统计单词数
/**
hello world Java
hello world Python
hello world Node JS
hello world Rust
hello world Flutter
**/
Path path = Paths.get("C:\\test\\test.txt");
// read file into a stream of lines
Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8);
// stream of array...hard to process.
// Stream<String[]> words = lines.map(line -> line.split(" +"));

// stream of stream of string....hmm...better flat to one level.
// Stream<Stream<String>> words = lines.map(line -> Stream.of(line.split(" +")));

// result a stream of words, good!
Stream<String> words = lines.flatMap(line -> Stream.of(line.split(" +")));

// count the number of words.
long noOfWords = words.count();

System.out.println(noOfWords);  // 16

Match

匹配操作有多种不同的类型,都是用来判断某一种规则是否与流对象相互吻合的。所有的匹配操作都是终结操作,只返回一个 boolean 类型的结果。

boolean anyStartsWithA =
    stringCollection
        .stream()
        .anyMatch((s) -> s.startsWith("a"));

System.out.println(anyStartsWithA);      // true

boolean allStartsWithA =
    stringCollection
        .stream()
        .allMatch((s) -> s.startsWith("a"));

System.out.println(allStartsWithA);      // false

boolean noneStartsWithZ =
    stringCollection
        .stream()
        .noneMatch((s) -> s.startsWith("z"));

System.out.println(noneStartsWithZ);      // true

peek

peek 操作符往往被用于调试,或者在变换之中进行某些元素操作:

// 用于打印中间状态
Stream.of("one", "two", "three", "four")
  .filter(e -> e.length() > 3)
  .peek(e -> System.out.println("Filtered value: " + e))
  .map(String::toUpperCase)
  .peek(e -> System.out.println("Mapped value: " + e))
  .collect(Collectors.toList());

// 用于修改初始状态
Stream<User> userStream = Stream.of(new User("Alice"), new User("Bob"), new User("Chuck"));
userStream.peek(u -> u.setName(u.getName().toLowerCase()))
.forEach(System.out::println);

Terminal

Collect

java.util.stream.Collectors 类的主要作用就是辅助进行各类有用的 reduction 操作,例如转变输出为 Collection,把 Stream 元素进行归组。

    // Accumulate names into a  List
    List<String> list = people.stream().map(Person::getName).collect(Collectors.toList());

    // Accumulate names into a TreeSet
    Set<String> set = people.stream().map(Person::getName).collect(Collectors.toCollection(TreeSet::new));

    // Convert elements to strings and concatenate them, separated by commas
    String joined = things.stream()
                        .map(Object::toString)
                        .collect(Collectors.joining(", "));

    // Compute sum of salaries of employee
    int total = employees.stream()
                        .collect(Collectors.summingInt(Employee::getSalary)));

    // Group employees by department
    Map<Department, List<Employee>> byDept
        = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment));

    // Compute sum of salaries by department
    Map<Department, Integer> totalByDept
        = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment,
                                                Collectors.summingInt(Employee::getSalary)));

    // Partition students into passing and failing
    Map<Boolean, List<Student>> passingFailing =
        students.stream()
                .collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));

groupingBy/partitioningBy

  • 按照年龄归组
Map<Integer, List<Person>> personGroups = Stream.generate(new PersonSupplier()).
 limit(100).
 collect(Collectors.groupingBy(Person::getAge));

Iterator it = personGroups.entrySet().iterator();

while (it.hasNext()) {
 Map.Entry<Integer, List<Person>> persons = (Map.Entry) it.next();
 System.out.println("Age " + persons.getKey() + " = " + persons.getValue().size());
}

上面的 code,首先生成 100 人的信息,然后按照年龄归组,相同年龄的人放到同一个 list 中,可以看到如下的输出:

Age 0 = 2
Age 1 = 2
Age 5 = 2
Age 8 = 1
Age 9 = 1
Age 11 = 2
……
  • 按照未成年人和成年人归组
Map<Boolean, List<Person>> children = Stream.generate(new PersonSupplier()).
 limit(100).
 collect(Collectors.partitioningBy(p -> p.getAge() < 18));
System.out.println("Children number: " + children.get(true).size());
System.out.println("Adult number: " + children.get(false).size());

输出结果:

Children number: 23
Adult number: 77

在使用条件“年龄小于 18”进行分组后可以看到,不到 18 岁的未成年人是一组,成年人是另外一组。partitioningBy 其实是一种特殊的 groupingBy,它依照条件测试的是否两种结果来构造返回的数据结构,get(true) 和 get(false) 能即为全部的元素对象。

Count

Count 是一个终结操作,它的作用是返回一个数值,用来标识当前流对象中包含的元素数量。

long startsWithB =
    stringCollection
        .stream()
        .filter((s) -> s.startsWith("b"))
        .count();

System.out.println(startsWithB);    // 3

Reduce

该操作是一个终结操作,它能够通过某一个方法,对元素进行削减操作。该操作的结果会放在一个 Optional 变量里返回。

Optional<String> reduced =
    stringCollection
        .stream()
        .sorted()
        .reduce((s1, s2) -> s1 + "#" + s2);

reduced.ifPresent(System.out::println);
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
上一页
下一页