Java 8 vs. Scala:Part I

比较 Java 8 和 Scala 在使用 Stream API 时的表达方式和性能的差异。

经过漫长的等待,终于等到了有着高阶函数的 Java 8。我迷恋 Java,但是我必须承认和现在一些其它的语言相比 Java 的语法确实是十分的冗余。现在使用 lambda 表达式,我就可以编写实用且可读性的代码(有时,比传统的方式可读性更强)。

虽然 Java 8 在 2014 年 3 月就发布了,但是我最近才有机会使用它。自从我知道 Scala 后,就一直想比较一下 Java 8 和 Scala 的表达方式和性能。主要是在使用 Stream API 的时候,并且我将为你展示怎么使用 Stream API 去操作集合。

因为这篇文章很长所以我把它文件分成了 3 个部分。

  1. lambda 表达式
  2. Stream API vs Scala collection API
  3. 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]

时间: 2024-11-03 08:24:15

Java 8 vs. Scala:Part I的相关文章

给Java开发者的Scala教程

author:Michel Schinz,Philipp Haller 1. 简介 本文将该要的介绍Scala语言和其编译.这里假设读者已经有一定的java开发经验,需要概要的了解他们可以用Scala 做些什么. 2. 第一个例子 我们用全世界最著名的代码来作为开始.虽然没什么用,但是可以很好地直观的了解Scala: object HelloWorld { def main(args: Array[String]): Unit = { println("Hello, world!") 

向Java开发者介绍Scala

Scala结合了面向对象编程与函数编程思想,使用一种能够完全兼容Java.可以运行在Java虚拟机上的.简洁的语法.对于函数编程风格的支持,尤其是对于Lambda表达式的支持,能够有助于减少必须要编写的逻辑无关固定代码,也许让它可以更简单的关注要面对的任务本身,而相对的Java中对Lamdba表达式的支持要到预定于2012年发布的JavaSE8才会实现.本文就是对于Scala介绍. 相关厂商内容 Flash Builder 4.5高级版试用版免费高速下载 QClub(北京站)--<云计算与虚拟化

JAVA代码编译出错:找不到符号,求解答

问题描述 JAVA代码编译出错:找不到符号,求解答 源代码: package com.tarena.shoot; import java.util.Random; //Airplane----敌机既是飞行物, public class Airplane extends FlyingObject implements Enemy{ private int speed = 2;//敌机走步的步数 public Airplane(){ image = ShootGame.airplane; width

JAVA程序员必读:基础篇(8)

程序|程序员 JAVA程序员必读:基础篇时间:2001/09/13 13:31 作者:ZSC 太平洋网络学院 2.6什么是接口 接口是一个收集方法和常数表单的契约.当类执行一个接口,它就许诺声明在那个接口中执行所有的方法. 接口是一个设备或者一个系统,它是用于交互的无关的实体.根据这个定义,远程控制是一个在你和电视的接口:而英语是两个人之间的接口:强制在军事中的行为协议是不同等价人之间的接口.在JAVA语言中,接口是一个设备,它是用来与其它对象交互的设备.一个接口可能对一个协议是类似的.实际上,

JAVA程序员必读:基础篇(9)

程序|程序员 JAVA程序员必读:基础篇时间:2001/09/13 13:31作者:ZSC 太平洋网络学院 2.8 面向对象概念的问题和练习 本节教程测试一下你对对象.类.消息等等的理解,我们是通过做一些练习以及回答一些问题来进行的. 2.8.1 问题 你可以使用API文档来回答这些问题: ClickMe applet使用Color.red来设置画图颜色为红色.其它有什么颜色可以象这样来使用? 怎样设置颜色为紫色(purple)? 2.8.2 练习 现在,利用你从API文档中学到的知识来修改Cl

JAVA程序员必读:基础篇(7)

程序|程序员 JAVA程序员必读:基础篇时间:2001/09/13 13:31作者:ZSC 太平洋网络学院 2.5什么是继承 一个类可以从它的父类继承状态和行为.继承为组织和构造软件程序提供了一个强大的和自然的机理. 总得说来,对象是以类得形式来定义得.你可能现在已经可以从它类知道许多对象了.即使你如知道,如果我告诉你它是一辆自行车,你就会知道它有两个轮子和脚踏板等等.面向对象系统就更深入一些了,它允许类在其它类中定义.比如,山地自行车.赛车以及串座双人自行车都是各种各样的自行车.在面向对象技术

JAVA程序员必读:基础篇(6)

程序|程序员 JAVA程序员必读:基础篇时间:2001/09/13 13:31作者:ZSC 太平洋网络学院 2.4.2初始化实例和类成员 下面讲讲初始化实例和类成员: 你可以在类中定义它们的时候,使用static初始化程序和实例初始化程序来为类和实例成员提供初始化数值: class BedAndBreakfast { static final int MAX_CAPACITY = 10; boolean full = false; } 这个对于原始数据类型是没有问题的.有时候,它可以用在创建数组

JAVA程序员必读:基础篇(5)

程序|程序员 JAVA程序员必读:基础篇时间:2001/09/13 13:31作者:ZSC 太平洋网络学院 2.4实例和类成员 2.4.1理解实例和类成员 下面详细讨论一下实例和类成员,具体涉及变量和方法以及类变量和方法: 你这样声明一个成员变量,比如在类Myclass中有一个float型的aFloat: class MyClass { float aFloat; } 这样你就声明一个实例变量.每次你创建一个类的实例的时候,系统就为实例创建了类的每一个实例变量的副本.你可以从对象中访问对象的实例

JAVA程序员必读:基础篇(4)

程序|程序员 JAVA程序员必读:基础篇时间:2001/09/13 13:31作者:ZSC 太平洋网络学院 2.3什么是类 类实际上是对某种类型的对象定义变量和方法的原型. 在现实世界中,你经常看到相同类型的许多对象.比如 ,你的自行车只是现实世界中许多自行车的其中一辆.使用面向对象技术,我们可以说你的自行车是自行车对象类的一个实例.通常,自行车有一些状态(当前档位.两个轮子等等)以及行为(改变档位.刹车等等).但是,每辆自行车的状态都是独立的并且跟其它自行车不同. 当厂家制造自行车的时候,厂商