咳咳…你好啊 (尴尬的咳嗽声)
时隔两年半 我又回来了.
昨夜直接在新的环境上重新构建了博客, 好在有之前的
_config
, 重新部署起来还是很快速的..闲话少聊, 让我们直接开始吧.
2023年还在学Java 8也太逊了
Stream
此Stream非彼Stream
学过Java I/O的朋友肯定知道InputStream
和OutputStream
, 这里所谓的输入输出流表示的是字节流, 直接继承自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 | interface Formula { |
这样我们就可以直接使用到sqrt方法而不需要再override.
Functional Interfaces & Method and Constructor References
通过使用functional interface
, 我们就可以将lambda引入到Java的类型系统中.
一个functional interface
必须仅包含一个抽象方法. 一个最典型的例子就是我们经常使用到的Comparable
接口.
他的代码如下:
1 | package java.lang; |
因此我们可以这样来写:
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 | class Person { |
Built-in Functional Interfaces
接下来让我们看看几个内置的functional interfaces
.
Predicates
学过逻辑的话, Predicates就很好理解了. 这就是一个布尔类型的函数接口, 仅包含一个test
抽象方法. 请看:
1 | Predicate<String> predicate = (s) -> s.length() > 0; |
Function
见名知意, Function代表的就是一个接受参数 -> 输出结果的函数, 包含一个apply(Object)
抽象方法, 我们可以使用andThen
方法来连接Functions:
1 | Function<String, Integer> toInteger = Integer::valueOf; |
Supplier
Supplier就是根据给定的类型来生产结果(生产者), Supplier是没有参数的
1 | Supplier<Person> personSupplier = Person::new; |
Consumer
与Supplier相反, Consumer接受一个参数然后执行操作(消费者)
1 | Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName); |
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 | Optional<String> optional = Optional.of("bam"); |
除了这些简单的判断之外, Optional可以跟上面的Predicate
, Supplier
和Consumer
一起使用从而达到一些酷炫的效果, 例如:
1 | Supplier<String> stringSupplier = () -> new String("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 | Arrays.asList("1", "2", "3") |
这里对于字符串对象, 我们所创建的流就是Stream
类型, 而如果对于像int
, long
这些基本数据类型, 我们还可以得到IntStream
, LongStream
对象, 从而获得更多特定的操作:
1 | Arrays.stream(new int[] {1,2,3}) |
我们也可以直接使用IntStream:
1 | IntStream.range(1, 4) // 左闭右开 |
在使用到这些基本数据流的时候, 参与的Predicate
, Function
, Optional
其实也都是对应的基本数据类型. 是的, 我们有IntPredicate
, IntFunction
和OptionalInt
.
为了更方便的处理数据, 我们是可以将一个Object流转换成对应的基本数据类型流的, 通过使用
mapToDouble(ToDoubleFunction<? super T> mapper)
mapToInt(ToIntFunction<? super T> mapper)
mapToLong(ToLongFunction<? super T> mapper)
来看我们之前的例子, 稍微修改一下:
1 | Stream.of("a1", "a2", "a3") |
Stream的操作
流的处理都是惰性的!
意思是说, 只要流没有被终结, 中间操作是不会被执行的. 例如这个例子:
1 | Stream.of("d2", "a2", "b1", "b3", "c") |
这里是不会有任何输出的. 而如果我们加上一个终结操作, 例如forEach
:
1 | Stream.of("d2", "a2", "b1", "b3", "c") |
这里你如果实际执行了上述代码, 你可能会发现输出的结果竟然不是有序(先filter再foreach)的. 这时我们就明白了, 事实上我们是先将第一个值传递到filter, 再到forEach之后才去执行到第二个值.
通过下面的这个例子可以更好的理解这样做的用意:
1 | Stream.of("d2", "a2", "b1", "b3", "c") |
在我们执行到第二个值的时候就已经结束了, map也只被执行了2次操作, 而不是5次.
类似的, 通过合理的安排流的中间操作执行顺序, 我们也可以达到优化性能的目的.
1 | Stream.of("d2", "a2", "b1", "b3", "c") |
这个地方, 我们先进行了filter操作, 再执行map. 如果我们反转这两者的顺序, map就会被执行5次, 而不是像现在这样只执行一次.
说到性能优化, 我们再提一嘴流的重用. Java是不允许在close或者执行过终结操作之后继续访问一个流的, 为了解决这个问题, 我们可以使用之前的Supplier
:
1 | Supplier<Stream<String>> streamSupplier = |
Okay, 有点扯远了. 让我们回到流的操作上, 除了上面提到的filter
和map
之外, collect
也是非常常用的一个操作. 它可以将流转换成Collections, 我们处理好的数据总要变回去嘛, 这个东西这么用:
1 | // 假设有个persons List装了一堆人名 |
如你所见, 非常简单, 我们也可以使用Collectors.toSet()
就可以获得一个Set
.
collect需要接受一个Collector
类型的参数, 这个collector本身是蛮复杂的, 你需要一个supplier + accumulator + combiner + finisher
, 好在Java给我们提供了很多的Collector, 可以直接拿来就用.
除了转换成Collection数据类型之外, 我们还可以”收集”很多东西, 例如:
1 | Double averageAge = persons |
Moreover:
1 | IntSummaryStatistics ageSummary = |
如果你想自己构建一个Collector
的话, 只需要调用其of
方法, 然后传入那四个函数接口即可.
Reference
JavaDoc