比较 Java 8 和 Scala 在使用 Stream API 时的表达方式和性能的差异。
经过漫长的等待,终于等到了有着高阶函数的 Java 8。我迷恋 Java,但是我必须承认和现在一些其它的语言相比 Java 的语法确实是十分的冗余。现在使用 lambda 表达式,我就可以编写实用且可读性的代码(有时,比传统的方式可读性更强)。
虽然 Java 8 在 2014 年 3 月就发布了,但是我最近才有机会使用它。自从我知道 Scala 后,就一直想比较一下 Java 8 和 Scala 的表达方式和性能。主要是在使用 Stream API 的时候,并且我将为你展示怎么使用 Stream API 去操作集合。
因为这篇文章很长所以我把它文件分成了 3 个部分。
- lambda 表达式
- Stream API vs Scala collection API
- Trust no one, bench everything. (来自 sbt-jmh)
首先我们来看下 Java 8 中的 lambda 表达式。我不知道即使它的一部分是可替换的他们仍然把这个叫做 lambda 表达式。我能用一些声明替换它,我也可以说 Java 8 也支持拉姆达声明。我们讨论的就是编程语言把函数当成一等公民。即使不给函数传参它也能编译成功因为函数被当作对象看待。Java 是一个静态类型和强类型的语言。所以,函数必须是有个类型,因此它也是一个接口。也就是说,lambda 函数就是继承函数接口的一个类。你不用去创建这个函数的类,编译器会帮你实现。不幸的是,Java 没有像 Scala 那样完美的接口。如果你想声明一个 lambda 表达式,你必须给出输出结果的数据类型。因为 Java 是在后台编译它并且做的很好所以还是可以接受的。例如,Thread.stop() 在JDK 1.0 的时候就被移除了,并被标志为不可用的长达十年,但是现在它又回来了。所以你不能仅仅因为 语言 xyz 的语法更好就期望 Java 去完全去改变它的语法。
所以,Java 8 的设计就非常酷了。它是一个函数接口!函数接口就是只有一个抽象方法的接口。大多数的回调函数已经满足这样要求,所以我们不做任何修改就可以直接使用这些接口。@FunctionalInterface 是用来表明带注释接口是一个函数接口的注释。这个注释是可选的,而且除非你有一个检查的需求否则不需要对接口做任何特殊的处理。
请谨记 lambda 表达式必须要有返回的类型并且这个类型只能有一个抽象方法。
//Java 8之前
Runnable r = new Runnable(){
public void run(){
System.out.println(“This should be run in another thread”);
}
};
//Java 8
Runnable r = () -> System.out.println(“This should be run in another thread”);
如果一个函数有一个或者多个参数并且有返回值会怎样?
现在你可以使用 Java 8 提供的一系列的函数接口去解决这个问题。它们在 java.util.function 包里。
//Java 8
Function<String, Integer> parseInt = (String s) -> Integer.parseInt(s);
如果一个函数有两个参数呢?
不用担心 Java 8 中有 BiFunction。
//Java 8
BiFunction<Integer, Integer, Integer> multiplier =
(i1, i2) -> i1 * i2; //you can’t omit parenthesis here!
你能想象一个有三个参数的函数吗?
TriFunction?不过因为语言的设计者不是拉丁语的专家,否则我们就有TriFunction,QuadFunction,PentFunction 等等这些了。然而我们可以定义我们自己的TriFunction。
//Java 8
@FunctionalInterface
interface TriFunction<A, B, C, R> {
public R apply(A a, B b, C c);
}
然后只要引用这个接口就可以把它当成一个 lambda 表达式类型进行使用了
//Java 8
TriFunction<Integer, Integer, Integer, Integer> sumOfThree
= (i1, i2, i3) -> i1 + i2 + i3;
我想你已经知道为什么设计者就只设计到 BiFunction。
如果你仍然不明白为什么,让我们看下 PentFunction。假设我们已经在某个地方定义了 PentFunction 接口。
//Java 8
PentFunction<Integer, Integer, Integer, Integer, Integer, Integer>
sumOfFive = (i1, i2, i3, i4, i5) -> i1 + i2 + i3 + i4 + i5;
你能想象 EnnFunction 有多长吗?(在拉丁语中 Enn 就是9)你必须声明 10 个类型(前 9 个是参数,最后一个是返回类型)看起来整行就只有类型了。由于声明的类型太多,你可能会想,我们是不是已经声明了一个什么类型了。
答案是“是的”(这就是为什么我认为 Scala 的类型接口比 Java 好多了)。
Scala 也有一个 lambda 表达式类型,但是看起来它的设计者并不怎么喜欢拉丁语。他决定用函数的序列数来代替拉丁词语。在 Scala 中,一个 lambda 表达式最多可以有 22 个参数,而且对每个函数 Scala 都有一个对应的类型。函数的类型在 Scala 中是一个 Trait。Trait 像 Java 中抽象类但是可以当做一个混合类型使用。
如果你想要使用超过 22 个参数,那个我必须说你的设计肯定是有错误的。因为你必须要考虑你用过的所有类型。
在这里我就不过多的描述 lambda 表达式的细节了,你可以很容易从其他地方得到关于它的很多信息。你可以在 这里 和这里找到。
让我们来看一看Scala的一些其它内容。Scala 也是像 Java 一样是一个静态类型和强类型的语言,但是它的本质是一个函数语言。所以它是面向对象和函数编程混合产物。
我不能给你展示一个 Runnerable 的 Scala 例子(如同我上面给 java 做的),因为它是来自 java 不同方法途径。Scala 有它自己的解决问题的方式。所以,我会展示用 Scala 的方式替代。
//Scala
Future(println{“This should be run in another thread”})
和下面的 Java 8 代码等效
//Java 8
//assume that you have instantiated ExecutorService beforehand.
Runnable r = () -> System.out.println(“This should be run in another thread”);
executorService.submit(r);
如果你声明了一个 lambda 表达式,你可以不用去声明一个明确类型像 java 那样。
如果你想描述一个 lambda 表达式,可以直接描述而不需要像在 Java 里那样描述 explicit。
//Java 8
Function<String, Integer> parseInt = s -> Integer.parseInt(s);
//Scala
val parseInt = (s: String) => s.toInt
//or
val parseInt:String => Int = s => s.toInt
//or
val parseInt:Function1[String, Int] = s => s.toInt
喔! 在 Scala 中有如此多的变量声明方式。让编译器做它的工作。
PentFunction 怎么样?
//Java 8
PentFunction<Integer, Integer, Integer, Integer, Integer, Integer> sumOfFive
= (i1, i2, i3, i4, i5) -> i1 + i2 + i3 + i4 + i5;
//Scala
val sumOfFive = (i1: Int, i2: Int, i3: Int, i4: Int, i5: Int) =>
i1 + i2 + i3 + i4 + i5;
Scala 更短些 因为你不需要要声明接口类型,并且 Integer 类型在 Scala 中是 Int(Integer 的丰富版本有个更短名字)。更短不意味着总是更好。Scala 的方式更好并不是因为它更短,而是因为更容易读。类型的上下文就在这个参数列表中。你一眼能算出参数的类型。
如果你不同意,再看一下这个。
//Java 8
PentFunction<String, Integer, Double, Boolean, String, String>
sumOfFive = (i1, i2, i3, i4, i5) -> i1 + i2 + i3 + i4 + i5;
//Scala
val sumOfFive = (i1: String, i2: Int, i3: Double, i4: Boolean, i5: String)
=> i1 + i2 + i3 + i4 + i5;
在 Scala 中, 你几乎可以立即说出 i3 的类型是 Double,但是在 java8 中,你不得不去数它来找到 i3 是什么类型。除非你有一个超人的眼睛。
你可能也会说,Java 也能这样做。是的, 你是对的,但是就像这样了:
//Java 8
PentFunction<Integer, String, Integer, Double, Boolean, String> sumOfFive
= (Integer i1, String i2, Integer i3, Double i4, Boolean i5)
-> i1 + i2 + i3 + i4 + i5;
非常恐怖了吧,不是吗?因为你不得不一遍又一遍的重复类型。
此外, Java 8 没有 PentFunction。 需要自己定义。
//Java 8
@FunctionalInterface
interface PentFunction<A, B, C, D, E, R> {
public R apply(A a, B b, C c, D d, E e);
}
我不会说 Scala 更好,但是有些地方 Scala 确实更好。Scala 也有很多比 Java 糟糕的地方。但是我不会告诉你哪一个更好,因为那只是我的观点。我用 Scala 和 Java 8 比较是因为 Scala 是一个函数语言而 Java 8 有一些函数的组件。所以我必须找一个函数语言去和比较 Java 8 的这些函数组件。
因为 Scala 是运行在 JVM 上的所以我选择 Scala 来和 Java 8 比较。你可能会看到当使用函数的时候 Scala 有着简单且好的语法和方法。因为 Java 的设计者在设计 Java 的时候考虑更多的是在不打破一些旧的东西的基础上设计一些新的东西。所以尽管 Java 在使用拉姆达表达式时有一些限制,但是它也引入一些比较酷的东西。例如,利用方法引用的特性通过重现现有的方法使得编写拉姆达表达式更简洁。等一下,使得拉姆达表达式更简洁?
//Java 8
Function<String, Integer> parseInt = s -> Integer.parseInt(s);
利用方法引用可以重写
//Java 8
Function<String, Integer> parseInt = Integer::parseInt;
你也可以引用一个带实例的方法。在第二部分当我们讨论 Stream API 的时候我将指出为什么引用一个带实例的方法是十分有用的。
方法引用构造规则
1.(args) -> ClassName.staticMethod(args);
可以像这样重写 ClassName::staticMethod;
2.(instance, args) -> instance.instanceMethod(args);
可以像这样重写 ClassName::instanceMethod;
BiFunction<String, String, Integer> indexOf = String::indexOf;
3.(args) -> expression.instanceMethod(args);
可以像这样重写 expression::instanceMethod;
Function<String, Integer> indexOf = new String()::indexOf;
是否你注意到规则2有点奇怪?这有点让人困惑?虽然 indexOf 函数 只需要一个参数,目标类型是一个双重函数表明了他需要两个参数。实际上,这个奇特指出常常用在 Stream API 并且当你不看类型名的时候才有意义。
pets.stream().map(Pet::getName).collect(toList());
// map()函数签名能够推导出下面的形式
// <String> Stream<String> map(Function<? super Pet, ? extends String> mapper)
从规则3,你可能会好奇,能否用 lambda 表达式替换 new String()?
你可以建造一个对象像这样使用方法引用
Supplier<String> str = String::new;
那么,我们能否这样做呢?
Function<Supplier<String>, Integer> indexOf = (String::new)::indexOf;
不幸的是,不能!它不会被编译并且编译器提示“表达式的目标类型必须是一个函数式接口”。这个很有趣,因为目标类型实际上就是函数式接口。你能告诉我为什么吗?说真的,你能吗,反正我不确定原因。
文章转载自 开源中国社区[https://www.oschina.net]