Java 8与往常一样是妥协和向后兼容的版本。 JSR-335专家组可能无法与某些读者就某些功能的范围或可行性达成一致的发行版。 请参阅Brian Goetz关于为什么…的一些具体解释。
- …Java 8默认方法中不允许“最终”
- …Java 8默认方法中不允许“同步”
但是今天,我们将专注于Streams API的“缺点”,或者正如Brian Goetz可能指出的那样:考虑到设计目标,事情超出了范围。
并行流?
并行计算很困难,曾经很痛苦。 当Java 7首次附带新的(现在已旧的) Fork / Join API时,人们并不完全喜欢它。相反,显然,调用Stream.parallel()
的简洁性是无与伦比的。
但是实际上很多人并不需要并行计算(不要与多线程混淆!)。 在95%的情况下,人们可能更喜欢功能更强大的Streams API,或者更通用的功能更强大的Collections API, Iterable
各种Iterable
子类型都有很多很棒的方法。
但是,更改Iterable
是危险的。 甚至不Iterable
通过潜在的Iterable.stream()
方法将Iterable
转换为Stream
似乎也冒着打开潘多拉盒子的风险! 。
顺序流!
因此,如果JDK不发货,我们将自己创建!
流本身就很棒。 它们可能是无限的,这是一个很酷的功能。 通常,尤其是在函数式编程中,集合的大小并不重要,因为我们可以使用函数逐个元素地进行转换。
如果我们允许Streams纯粹是顺序的,那么我们也可以使用以下任何一种很酷的方法(并行Streams也可以使用其中的一些方法):
-
cycle()
–使每个流无限的保证方式 -
duplicate()
–将一个流复制为两个等效流 -
foldLeft()
-顺序和非缔合的替代reduce()
-
foldRight()
-顺序和非缔合的替代reduce()
-
limitUntil()
–将流限制为满足第一个条件的第一个条件之前的记录 -
limitWhile()
–将流限制为第一个不满足谓词之前的记录 -
maxBy()
–将流减少到最大映射值 -
minBy()
–将流减少到最小映射值 -
partition()
–将一个流划分为两个流,一个满足一个谓词,另一个不满足相同的谓词 -
reverse()
–以相反的顺序产生一个新的流 -
skipUntil()
–跳过记录直到满足谓词 -
skipWhile()
–只要满足谓词,就跳过记录 -
slice()
–截取流的一部分,即合并skip()
和limit()
-
splitAt()
–在给定位置将一个流分成两个流 -
unzip()
–将成对的流分成两个流 -
zip()
–将两个流合并为一个成对的流 -
zipWithIndex()
–将流及其对应的索引流合并为单个对流
jOOλ的新Seq类型可以完成所有操作
以上所有都是jOOλ的一部分。 jOOλ(读作“ jewel”或“ dju-lambda”,也用URL编写jOOL等)是一种ASL 2.0许可的库,它是在我们使用Java 8实施jOOQ集成测试时从我们自己的开发需求中产生的 。适合编写有关集合,元组,记录和所有SQL原因的测试。
但是Streams API感觉有点不足,因此我们将JDK的Streams包装到我们自己的Seq
类型中(用于序列/顺序Stream的Seq):
// Wrap a stream in a sequence
Seq<Integer> seq1 = seq(Stream.of(1, 2, 3));// Or create a sequence directly from values
Seq<Integer> seq2 = Seq.of(1, 2, 3);
我们已经将Seq
扩展JDK Stream
接口的新接口,因此您可以与其他Java API完全互操作地使用Seq
保持现有方法不变:
public interface Seq<T> extends Stream<T> {/*** The underlying {@link Stream} implementation.*/Stream<T> stream();// [...]
}
现在,如果没有元组,函数式编程仅是乐趣的一半。 不幸的是,Java没有内置的元组,虽然使用泛型创建元组库很容易,但是当将Java与Scala或C#甚至VB.NET 进行比较时 ,元组仍然是第二类语法公民。
但是…
jOOλ也有元组
我们已经运行了一个代码生成器,以生成1-8级的元组(将来可能会添加更多,例如,以匹配Scala和jOOQ的 “魔术”级22)。
并且如果库中有这样的元组,则该库也需要相应的功能。 这些TupleN
和FunctionN
类型的本质总结如下:
public class Tuple3<T1, T2, T3>
implements Tuple, Comparable<Tuple3<T1, T2, T3>>, Serializable, Cloneable {public final T1 v1;public final T2 v2;public final T3 v3;// [...]
}
和
@FunctionalInterface
public interface Function3<T1, T2, T3, R> {default R apply(Tuple3<T1, T2, T3> args) {return apply(args.v1, args.v2, args.v3);}R apply(T1 v1, T2 v2, T3 v3);
}
元组类型还有许多其他功能,但让我们今天不做介绍。
另外,我最近在reddit上与Gavin King(Hibernate的创建者)进行了有趣的讨论 。 从ORM的角度来看,Java类似乎是SQL /关系元组的合适实现,而且确实如此。 从ORM角度来看。
但是类和元组在本质上是不同的,这对于大多数ORM来说是一个非常微妙的问题- 例如,如Vlad Mihalcea所解释的 。
此外,SQL的行值表达式(即元组)的概念与Java类可以建模的完全不同。 此主题将在后续的博客文章中介绍。
一些jOOλ示例
考虑到上述目标,让我们来看一下如何通过示例来实现上述API:
拉链
// (tuple(1, "a"), tuple(2, "b"), tuple(3, "c"))
Seq.of(1, 2, 3).zip(Seq.of("a", "b", "c"));// ("1:a", "2:b", "3:c")
Seq.of(1, 2, 3).zip(Seq.of("a", "b", "c"), (x, y) -> x + ":" + y
);// (tuple("a", 0), tuple("b", 1), tuple("c", 2))
Seq.of("a", "b", "c").zipWithIndex();// tuple((1, 2, 3), (a, b, c))
Seq.unzip(Seq.of(tuple(1, "a"),tuple(2, "b"),tuple(3, "c")
));
元组已经变得非常方便了。 当我们将两个流“压缩”到一个流中时,我们需要一个将两个值组合在一起的包装器值类型。 传统上,人们可能已经使用Object[]
来解决问题,但是数组并不指示属性类型或程度。
不幸的是,Java编译器无法Seq<T>
<T>
类型的有效边界。 这就是为什么我们只能有一个静态unzip()
方法(而不是实例方法)的原因,该方法的签名如下所示:
// This works
static <T1, T2> Tuple2<Seq<T1>, Seq<T2>> unzip(Stream<Tuple2<T1, T2>> stream) { ... }// This doesn't work:
interface Seq<T> extends Stream<T> {Tuple2<Seq<???>, Seq<???>> unzip();
}
跳过和限制
// (3, 4, 5)
Seq.of(1, 2, 3, 4, 5).skipWhile(i -> i < 3);// (3, 4, 5)
Seq.of(1, 2, 3, 4, 5).skipUntil(i -> i == 3);// (1, 2)
Seq.of(1, 2, 3, 4, 5).limitWhile(i -> i < 3);// (1, 2)
Seq.of(1, 2, 3, 4, 5).limitUntil(i -> i == 3);
其他功能库可能使用与跳过(例如,删除)和限制(例如,采用)不同的术语。 最终并不重要。 我们选择了现有Stream API中已经存在的术语: Stream.skip()
和Stream.limit()
折叠式
// "abc"
Seq.of("a", "b", "c").foldLeft("", (u, t) -> t + u);// "cba"
Seq.of("a", "b", "c").foldRight("", (t, u) -> t + u);
Stream.reduce()
操作专为并行化而设计。 这意味着传递给它的函数必须具有以下重要属性:
- 关联性
- 不干涉
- 无国籍
但是有时候,您确实想使用不具有上述属性的函数来“减少”流,因此,您可能并不在乎减少的并行性。 这就是“折叠”出现的地方。
在此处可以看到有关缩小和折叠(在Scala中)各种差异的很好的解释。
分裂
// tuple((1, 2, 3), (1, 2, 3))
Seq.of(1, 2, 3).duplicate();// tuple((1, 3, 5), (2, 4, 6))
Seq.of(1, 2, 3, 4, 5, 6).partition(i -> i % 2 != 0)// tuple((1, 2), (3, 4, 5))
Seq.of(1, 2, 3, 4, 5).splitAt(2);
上面的功能都有一个共同点:它们在单个流上运行以产生两个新的流,这些流可以独立使用。
显然,这意味着在内部必须消耗一些内存以保留部分消耗的流的缓冲区。 例如
- 复制需要跟踪一个流中已消耗的所有值,而另一流中没有
- 分区需要快速前进到满足(或不满足)谓词的下一个值,而不会丢失所有丢弃的值
- 拆分可能需要快速前进到拆分索引
为了获得一些真正的功能乐趣,让我们看一下可能的splitAt()
实现:
static <T> Tuple2<Seq<T>, Seq<T>>
splitAt(Stream<T> stream, long position) {return seq(stream).zipWithIndex().partition(t -> t.v2 < position).map((v1, v2) -> tuple(v1.map(t -> t.v1),v2.map(t -> t.v1)));
}
…或附有评论:
static <T> Tuple2<Seq<T>, Seq<T>>
splitAt(Stream<T> stream, long position) {// Add jOOλ functionality to the stream// -> local Type: Seq<T>return seq(stream)// Keep track of stream positions// with each element in the stream// -> local Type: Seq<Tuple2<T, Long>>.zipWithIndex()// Split the streams at position// -> local Type: Tuple2<Seq<Tuple2<T, Long>>,// Seq<Tuple2<T, Long>>>.partition(t -> t.v2 < position)// Remove the indexes from zipWithIndex again// -> local Type: Tuple2<Seq<T>, Seq<T>>.map((v1, v2) -> tuple(v1.map(t -> t.v1),v2.map(t -> t.v1)));
}
很好,不是吗? 另一方面, partition()
可能实现要复杂一些。 这里Spliterator
地用Iterator
代替了新的Spliterator
:
static <T> Tuple2<Seq<T>, Seq<T>> partition(Stream<T> stream, Predicate<? super T> predicate
) {final Iterator<T> it = stream.iterator();final LinkedList<T> buffer1 = new LinkedList<>();final LinkedList<T> buffer2 = new LinkedList<>();class Partition implements Iterator<T> {final boolean b;Partition(boolean b) {this.b = b;}void fetch() {while (buffer(b).isEmpty() && it.hasNext()) {T next = it.next();buffer(predicate.test(next)).offer(next);}}LinkedList<T> buffer(boolean test) {return test ? buffer1 : buffer2;}@Overridepublic boolean hasNext() {fetch();return !buffer(b).isEmpty();}@Overridepublic T next() {return buffer(b).poll();}}return tuple(seq(new Partition(true)), seq(new Partition(false)));
}
我将让您进行练习并验证上面的代码。
立即获取并为jOOλ做出贡献!
以上所有内容都是jOOλ的一部分,可从GitHub免费获得。 已经有部分的Java-8就绪,全面的库调用functionaljava ,它走的更远,比jOOλ。
但是,我们相信Java 8的Streams API所缺少的实际上只是对顺序流非常有用的几种方法。
在上一篇文章中,我们展示了如何使用简单的JDBC包装器将lambda引入基于String的SQL中 ( 当然,我们仍然认为应该使用jOOQ代替 )。
今天,我们已经展示了如何使用jOOλ轻松编写出色的功能和顺序流处理。
请继续关注,以在不久的将来获得更多收益(当然,非常欢迎拉动请求!)
翻译自: https://www.javacodegeeks.com/2014/09/when-the-java-8-streams-api-is-not-enough.html