作者:文镭(依来)
前言这篇文章不是工具举荐,也不是利用案例分享。其主题思想,是介绍一种全新的设计模式。它既领有形象的数学美感,仅仅从一个简略接口登程,就能推演出宏大的个性汇合,引出许多全新概念。同时也有扎实的工程实用价值,由其实现的工具,性能均可显著超过同类的头部开源产品。
这一设计模式并非因Java而生,而是诞生于一个非常简陋的脚本语言。它对语言个性的要求十分之低,因此其价值对泛滥古代编程语言都是普适的。
对于Stream首先大略回顾下Java里传统的流式API。自Java8引入lambda表达式和Stream以来,Java的开发便捷性有了质的飞跃,Stream在简单业务逻辑的解决上让人效率倍增,是每一位Java开发者都应该把握的根底技能。但排除掉parallelStream也即并发流之外,它其实并不是一个好的设计。
第一、封装过重,实现过于简单,源码极其难读。我能了解这或者是为了兼容并发流所做的斗争,但毕竟耦合太深,显得艰深晦涩。每一位初学者被源码吓到之后,想必都会产生流是一种非常高级且实现简单的个性的印象。实际上并不是这样,流其实能够用非常简单的形式构建。
第二、API过于简短。简短体现在stream.collect这一部分。作为比照,Kotlin提供的toList/toSet/associate(toMap)等等丰盛操作是能够间接作用在流上的。Java直到16才抠抠索索加进来一个Stream能够间接调用的toList,他们甚至不肯把toSet/toMap一起加上。
第三、API性能简陋。对于链式操作,在最后的Java8里只有map/filter/skip/limit/peek/distinct/sorted这七个,Java9又加上了takeWhile/dropWhile。然而在Kotlin中,除了这几个之外人还有许多额定的实用功能。
例如:
mapIndexed,mapNotNull,filterIndexed,filterNotNull,onEachIndexed,distinctBy, sortedBy,sortedWith,zip,zipWithNext等等,翻倍了不止。这些货色实现起来并不简单,就是个棘手的事,但对于用户而言有和没有的体验差别堪称微小。
在这篇文章里,我将提出一种全新的机制用于构建流。这个机制极其简略,任何能看懂lambda表达式(闭包)的同学都能亲手实现,任何反对闭包的编程语言都能利用该机制实现本人的流。也正是因为这个机制足够简略,所以开发者能够以相当低的老本撸出大量的实用API,应用体验甩开Stream两条街,不是问题。
对于生成器生成器(Generator)[1]是许多古代编程语言里一个广受好评的重要个性,在Python/Kotlin/C#/Javascript等等语言中均有间接反对。它的外围API就是一个yield关键字(或者办法)。
有了生成器之后,无论是iterable/iterator,还是一段乌七八糟的闭包,都能够间接映射为一个流。举个例子,假如你想实现一个下划线字符串转驼峰的办法,在Python里你能够利用生成器这么玩
def underscore_to_camelcase(s): def camelcase(): yield str.lower while True: yield str.capitalize return ''.join(f(sub) for sub, f in zip(s.split('_'), camelcase()))这短短几行代码能够说处处体现出了Python生成器的奇妙。首先,camelcase办法里呈现了yield关键字,解释器就会将其看作是一个生成器,这个生成器会首先提供一个lower函数,而后提供有数的capitalize函数。因为生成器的执行始终是lazy的,所以用while true的形式生成有限流是非常常见的伎俩,不会有性能或者内存上的节约。其次,Python里的流是能够和list一起进行zip的,无限的list和有限的流zip到一起,list完结了流天然也会完结。
这段代码中,开端那行join()括号里的货色,Python称之为生成器推导(Generator Comprehension)[2],其本质上仍然是一个流,一个zip流被map之后的string流,最终通过join办法聚合为一个string。
以上代码里的操作, 在任何反对生成器的语言里都能够轻易实现,然而在Java里你恐怕连想都不敢想。Java有史以来,无论是历久弥新的Java8,还是最新的引入了Project Loom[3]的OpenJDK19,连协程都有了,仍然没有间接反对生成器。
实质上,生成器的实现要依赖于continuation[4]的挂起和复原,所谓continuation能够直观了解为程序执行到指定地位后的断点,协程就是指在这个函数的断点挂起后跳到另一个函数的某个断点继续执行,而不会阻塞线程,生成器亦如是。
Python通过栈帧的保留与复原实现函数重入以及生成器[5],Kotlin在编译阶段利用CPS(Continuation Passing Style)[6]技术对字节码进行了变换,从而在JVM上模仿了协程[7]。其余的语言要么大体如此,要么有更间接的反对。
那么,有没有一种方法,能够在没有协程的Java里,实现或者至多模拟出一个yield关键字,从而动静且高性能地创立流呢。答案是,有。
注释Java里的流叫Stream,Kotlin里的流叫Sequence。我切实想不出更好的名字了,想叫Flow又被用了,简略起见权且叫Seq。
概念定义首先给出Seq的接口定义
public interface Seq<T> { void consume(Consumer<T> consumer);}它实质上就是一个consumer of consumer,其实在含意我后边会讲。这个接口看似形象,实则十分常见,java.lang.Iterable人造自带了这个接口,那就是大家耳熟能详的forEach。利用办法推导,咱们能够写出第一个Seq的实例
List<Integer> list = Arrays.asList(1, 2, 3);Seq<Integer> seq = list::forEach;能够看到,在这个例子里consume和forEach是齐全等价的,事实上这个接口我最早就是用forEach命名的,几轮迭代之后才改成含意更精确的consume。
利用单办法接口在Java里会自动识别为FunctionalInteraface这一平凡个性,咱们也能够用一个简略的lambda表达式来结构流,比方只有一个元素的流。
static <T> Seq<T> unit(T t) { return c -> c.accept(t);}这个办法在数学上很重要(实操上其实用的不多),它定义了Seq这个泛型类型的单位元操作,即T -> Seq<T>的映射。
...