Java 8 Stream和其他特性

咳咳…你好啊 (尴尬的咳嗽声)

时隔两年半 我又回来了.

昨夜直接在新的环境上重新构建了博客, 好在有之前的_config, 重新部署起来还是很快速的..

闲话少聊, 让我们直接开始吧.

2023年还在学Java 8也太逊了

Stream

此Stream非彼Stream

学过Java I/O的朋友肯定知道InputStreamOutputStream, 这里所谓的输入输出流表示的是字节流, 直接继承自java.lang.Object. 而我们今天讨论的Stream, 是一个完全不同的概念. (Stream直接就是java.util.Stream)

Stream为Java带来了函数式编程, Streams are Monads.

Monads简单的说就是一种结构, which 将计算通过一系列的步骤串起来. 更好理解的说法是 -> computation builder

在正式进入到Stream的讨论之前, 我们或许还需要了解一些其他的Java 8特性, 不然我们会看到很多Stream的报错信息都一头雾水.

Extension Methods

也叫做Default Methods. 其实就是通过default关键字, 我们可以直接在接口中实现一个非抽象方法. 例如:

1
2
3
4
5
interface Formula {
default double sqrt(int a) {
return Math.sqrt(a);
}
}

这样我们就可以直接使用到sqrt方法而不需要再override.

Functional Interfaces & Method and Constructor References

通过使用functional interface, 我们就可以将lambda引入到Java的类型系统中.

一个functional interface必须仅包含一个抽象方法. 一个最典型的例子就是我们经常使用到的Comparable接口.

他的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package java.lang;
import java.util.*;

public interface Comparable<T> {
/**
* Compares this object with the specified object for order. Returns a
* negative integer, zero, or a positive integer as this object is less
* than, equal to, or greater than the specified object.
*
* <p>The implementor must ensure {@link Integer#signum
* signum}{@code (x.compareTo(y)) == -signum(y.compareTo(x))} for
* all {@code x} and {@code y}. (This implies that {@code
* x.compareTo(y)} must throw an exception if and only if {@code
* y.compareTo(x)} throws an exception.)
*
* <p>The implementor must also ensure that the relation is transitive:
* {@code (x.compareTo(y) > 0 && y.compareTo(z) > 0)} implies
* {@code x.compareTo(z) > 0}.
*
* <p>Finally, the implementor must ensure that {@code
* x.compareTo(y)==0} implies that {@code signum(x.compareTo(z))
* == signum(y.compareTo(z))}, for all {@code z}.
*
* @apiNote
* It is strongly recommended, but <i>not</i> strictly required that
* {@code (x.compareTo(y)==0) == (x.equals(y))}. Generally speaking, any
* class that implements the {@code Comparable} interface and violates
* this condition should clearly indicate this fact. The recommended
* language is "Note: this class has a natural ordering that is
* inconsistent with equals."
*
* @param o the object to be compared.
* @return a negative integer, zero, or a positive integer as this object
* is less than, equal to, or greater than the specified object.
*
* @throws NullPointerException if the specified object is null
* @throws ClassCastException if the specified object's type prevents it
* from being compared to this object.
*/
public int compareTo(T o);
}

因此我们可以这样来写:

1
PriorityQueue<Integer> queue = new PriorityQueue<>((a, b) -> a - b);

类似的, 还有像Integer.valueOf() 也可以这么使用.

关于lambda的变量访问, 需要提醒的一点是, 在lambda中的outer scope变量, 需要是final的. Java允许不显式声明final关键字, 但是该变量不允许再被修改 (编译报错)

同样的, 在lambda中的写outer scope变量的操作也是不可以的.

当然了, 如果变量是在lambda内部, 我们是可以随意读写的. 这个规则和我们之前使用匿名对象/函数是一样的.

还记得我们在上一节提到的default methods吗? 这种方法不可以使用lambda表达式, 下面的code会编译报错:

1
Formula formula = (a) -> sqrt(a * 100); // Panic!

lambda已经可以简化很多代码, 还可以继续精简吗?

答案是可以. 通过使用Method and Constructor References, 我们可以使用::关键字来引用方法, 例如:

1
PriorityQueue<Integer> queue = new PriorityQueue<>(Integer::compareTo);

既然方法可以, 接下来我们再来看看构造器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
String firstName;
string lastName;

Person() {}

Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}

interface PersonFactory<P extends Person> {
P create(String firstName, String lastName);
}

PersonalFactory<Person> pf = Person::new;
Personal person = personFactory.create("Jane", "Doe");

Built-in Functional Interfaces

接下来让我们看看几个内置的functional interfaces.

Predicates

学过逻辑的话, Predicates就很好理解了. 这就是一个布尔类型的函数接口, 仅包含一个test抽象方法. 请看:

1
2
3
4
5
6
7
8
Predicate<String> predicate = (s) -> s.length() > 0;

predicate.test("foo"); // true
predicate.negate().test("foo"); // false

Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();

Function

见名知意, Function代表的就是一个接受参数 -> 输出结果的函数, 包含一个apply(Object)抽象方法, 我们可以使用andThen方法来连接Functions:

1
2
3
4
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);

backToString.apply("123"); // "123"

Supplier

Supplier就是根据给定的类型来生产结果(生产者), Supplier是没有参数的

1
2
Supplier<Person> personSupplier = Person::new;
personSupplier.get(); // new Person

Consumer

与Supplier相反, Consumer接受一个参数然后执行操作(消费者)

1
2
Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));

Comparators

再然后就是我们熟知的Comparator了, 与Comparable接口提供的自然排序不同, Comparator可以自定义排序结果, 并且Java还提供了很多默认的排序方法, 例如倒序:

1
PriorityQueue<Integer> pq = new PriorityQueue<>(Comparator.reverseOrder()); 

Comparator.reverseOrder 会返回一个<T extends Comparable<? super T>>Comparator<T>.

Optional (不是接口)

与所有支持函数式编程语言一样, Optional类是是一个**container**对象, 其中包含(contain)的对象可能是null也可能非空. 通过使用isPresent我们就可以得知. 通过使用Optional类, 我们就可以避免掉NullPointerException了.

Optional类包含了很多有用的方法, 请看:

1
2
3
4
5
6
7
Optional<String> optional = Optional.of("bam");

optional.isPresent(); // true
optional.get(); // bam
optional.orElse("fallback"); // bam

optional.ifPresent((s) -> System.out.println(s.charAt(0))); // b

除了这些简单的判断之外, Optional可以跟上面的Predicate, SupplierConsumer一起使用从而达到一些酷炫的效果, 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Supplier<String> stringSupplier = () -> new String("abc");
Consumer<String> greeter = (s) -> System.out.println(s);

Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();

Optional<String> optional = Optional.ofNullable(null);
// 使用之前的Predicate来进行了过滤,
// 如果Optional有值, 则会调用Consumer的accept方法来使用掉该值, 如果没有则跳过不执行
optional.filter(isNotEmpty).ifPresent(greeter);

// 这里的Optional只有个null, 因此会调用supplier的get方法来获取一个值
// 如果在调用时Optional有值, 则跳过获取操作
greeter.accept(optional.orElseGet(stringSupplier));

// 最终只会打印一个abc

Streams

前面铺垫了这么多, 终于我们可以进入到Stream了. 这个时候如果你正在看Javadoc, 会发现Stream大量使用到了我们刚刚说到的Functional Interfaces和Optional类.

我们知道Java的集合类分三大块:

  • Lists
  • Sets
  • Map

这里我们先忽略Map, 来看Lists和Sets. (以及数组)

我们常用到的除了Stream本身, 还有IntStream, DoubleStream, LongStream, 这些都继承自一个BaseStream. 这个BaseStream提供了诸多基本的流操作, 例如返回迭代器, 关闭流, 设定关闭时的触发, 以及返回一个相同的流等等.

这里你会发现, 不同的操作在一个流当中的时间节点是不同的. 所以我们又可以把流中的操作定义为:

  • 中间操作 (intermediate operation)
  • 终结操作 (terminal operation)

Stream的创建

我们可以通过Collection和Array的stream方法来创建一个Stream, 也可以直接通过Stream.of()

1
2
3
4
5
6
7
8
Arrays.asList("1", "2", "3")
.stream()
.findFirst() // Optional
.isPresent(System.out::println); // 1

Stream.of("1", "2", "3")
.findFirst()
.ifPresent(System.out::println); // 1

这里对于字符串对象, 我们所创建的流就是Stream类型, 而如果对于像int, long这些基本数据类型, 我们还可以得到IntStream, LongStream对象, 从而获得更多特定的操作:

1
2
3
4
Arrays.stream(new int[] {1,2,3})
.map(n -> n * 2 + 1)
.average() // 这个地方我们拿到的是OptionalDouble类
.getAsDouble(); // 5.0

我们也可以直接使用IntStream:

1
2
3
4
5
6
IntStream.range(1, 4) // 左闭右开
.forEach(System.out::println);

// 1
// 2
// 3

在使用到这些基本数据流的时候, 参与的Predicate, Function, Optional其实也都是对应的基本数据类型. 是的, 我们有IntPredicate, IntFunctionOptionalInt.

为了更方便的处理数据, 我们是可以将一个Object流转换成对应的基本数据类型流的, 通过使用

  • mapToDouble(ToDoubleFunction<? super T> mapper)
  • mapToInt(ToIntFunction<? super T> mapper)
  • mapToLong(ToLongFunction<? super T> mapper)

来看我们之前的例子, 稍微修改一下:

1
2
3
4
5
Stream.of("a1", "a2", "a3")
.map(s -> s.substring(1))
.mapToInt(Integer::parseInt)
.max()
.ifPresent(System.out::println); // 3

Stream的操作

流的处理都是惰性的!

意思是说, 只要流没有被终结, 中间操作是不会被执行的. 例如这个例子:

1
2
3
4
5
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
});

这里是不会有任何输出的. 而如果我们加上一个终结操作, 例如forEach:

1
2
3
4
5
6
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
})
.forEach(s -> System.out.println("forEach: " + s));

这里你如果实际执行了上述代码, 你可能会发现输出的结果竟然不是有序(先filter再foreach)的. 这时我们就明白了, 事实上我们是先将第一个值传递到filter, 再到forEach之后才去执行到第二个值.

通过下面的这个例子可以更好的理解这样做的用意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Stream.of("d2", "a2", "b1", "b3", "c")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.anyMatch(s -> {
System.out.println("anyMatch: " + s);
return s.startsWith("A");
});

// map: d2
// anyMatch: D2
// map: a2
// anyMatch: A2

在我们执行到第二个值的时候就已经结束了, map也只被执行了2次操作, 而不是5次.

类似的, 通过合理的安排流的中间操作执行顺序, 我们也可以达到优化性能的目的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));

// filter: d2
// filter: a2
// map: a2
// forEach: A2
// filter: b1
// filter: b3
// filter: c

这个地方, 我们先进行了filter操作, 再执行map. 如果我们反转这两者的顺序, map就会被执行5次, 而不是像现在这样只执行一次.

说到性能优化, 我们再提一嘴流的重用. Java是不允许在close或者执行过终结操作之后继续访问一个流的, 为了解决这个问题, 我们可以使用之前的Supplier:

1
2
3
4
5
6
Supplier<Stream<String>> streamSupplier =
() -> Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> s.startsWith("a"));

streamSupplier.get().anyMatch(s -> true); // ok
streamSupplier.get().noneMatch(s -> true); // ok

Okay, 有点扯远了. 让我们回到流的操作上, 除了上面提到的filtermap之外, collect也是非常常用的一个操作. 它可以将流转换成Collections, 我们处理好的数据总要变回去嘛, 这个东西这么用:

1
2
3
4
5
// 假设有个persons List装了一堆人名
List<Person> filtered = persons
.stream()
.filter(p -> p.name.startsWith("P"))
.collect(Collectors.toList());

如你所见, 非常简单, 我们也可以使用Collectors.toSet()就可以获得一个Set.

collect需要接受一个Collector类型的参数, 这个collector本身是蛮复杂的, 你需要一个supplier + accumulator + combiner + finisher, 好在Java给我们提供了很多的Collector, 可以直接拿来就用.

除了转换成Collection数据类型之外, 我们还可以”收集”很多东西, 例如:

1
2
3
Double averageAge = persons
.stream()
.collect(Collectors.averagingInt(p -> p.age));

Moreover:

1
2
3
4
5
6
7
IntSummaryStatistics ageSummary =
persons
.stream()
.collect(Collectors.summarizingInt(p -> p.age));

System.out.println(ageSummary);
// IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23}

如果你想自己构建一个Collector的话, 只需要调用其of方法, 然后传入那四个函数接口即可.

Reference

JavaDoc

Java 8 Stream Tutorial

Java 8 Tutorial

Collection Pipeline