摘要:虽然Java深得大量开发者喜爱,但是对比其他现代编程语言,其语法确实略显冗长。但是通过Java8,直接利用lambda表达式就能编写出既可读又简洁的代码。
【编者按】本文作者Hussachai Puripunpinyo的软件工程师,作者通过对比Java 8和Scala,对性能和表达方面的差异进行了分析,并且深入讨论关于Stream API的区别,由OneAPM工程师翻译。
以下为译文
数年等待,Java 8终于添加了高阶函数这个特性。本人很喜欢Java,但不得不承认,相比其他现代编程语言,Java语法非常冗长。然而通过Java8,直接利用lambda表达式就能编写出既可读又简洁的代码(有时甚至比传统方法更具可读性)。
Java 8于2014年3月3日发布,但笔者最近才有机会接触。因为笔者也很熟悉Scala,所以就产生了对比Java 8和Scala在表达性和性能方面的差异,比较将围绕Stream API展开,同时也会介绍如何使用Stream API来操作集合。
由于文章太长,所以分以下三个部分详细叙述。
Part 1.Lambda表达式
Part 2.Stream API vs Scala collection API
Part 3.Trust no one, bench everything(引用自sbt-jmh)
首先,我们来了解下Java 8的lambda表达式,虽然不知道即使表达式部分是可替代的,他们却称之为lambda表达式。这里完全可以用声明来代替表达式,然后说Java 8还支持lambda声明。编程语言将函数作为一等公民,函数可以被作为参数或者返回值传递,因为它被视为对象。Java是一种静态的强类型语言。所以,函数必须有类型,因此它也是一个接口。
另一方面,lambda 函数就是实现了函数接口的一个类。无需创建这个函数的类,编译器会直接实现。不幸的是,Java没有 Scala那样高级的类型接口。如果你想声明一个lambda表达式,就必须指定目标类型。实际上,由于Java必须保持向后兼容性,这也是可理解的,而且就目前来说Java完成得很好。例如,Thread.stop() 在JDK 1.0版时发布,已过时了十多年,但即便到今天仍然还在使用。所以,不要因为语言XYZ的语法(或方法)更好,就指望Java从根本上改变语法结构。
所以,Java 8的语言设计师们奇思妙想,做成函数接口!函数接口是只有一个抽象方法的接口。要知道,大多数回调接口已经满足这一要求。因此,我们可以不做任何修改重用这些接口。@FunctionalInterface是表示已注释接口是函数接口的注释。此注释是可选的,除非有检查要求,否则不用再进行处理。
请记住,lambda表达式必须定义类型,而该类型必须只有一个抽象方法。
//Before 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); |
该参数类型可以从函数中推断,就像Java7中的diamond operator,所以可以省略。我们可以重写该函数,如下所示:
//Java 8 Function<String, Integer> parseInt = 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?语言设计者止步于BiFunction。否则,可能真会有TriFunction、quadfunction、pentfunction等。解释一下,笔者是采用IUPAC规则来命名函数的。然后,可以按如下所示定义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中,你可以创建有22个参数的lambda表达式,意味着Scala有每个函数的类型(Function0、Function1、……Function22)。函数类型在Scala函数中是一个Trait,Trait就像 Java中的抽象类,但可以当做混合类型使用。如果还需要22个以上的参数,那大概是你函数的设计有问题。必须要考虑所传递的一组参数的类型。在此,笔者将不再赘述关于Lambda表达式的细节。
下面来看看Scala的其他内容。Scala也是类似Java的静态强类型语言,但它一开始就是函数语言。因此,它能很好地融合面向对象和函数编程。由于Scala和Java所采用的方法不同,这里不能给出Runnable的Scala实例。Scala有自己解决问题的方法,所以接下来会详细探讨。
//Scala Future(println{“This should be run in another thread”}) |
与以下Java8 的代码等效。
//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那样声明一个显式类型。
//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更短,因为不需要声明接口类型,而整数类型在Scala中是int。短不总意味着更好。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型,但在Java 8中,还需要算算它是什么类型。你可能争辩说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; |
你必须一遍又一遍的重复下去。
除此之外,Java8并没有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不如Java。所以很难说到底哪种更好,我之所以对两者进行比较,是因为Scala是一种函数语言,而Java 8支持一些函数特点,所以得找函数语言来比较。由于Scala可以运行在JVM上,用它来对比再好不过。可能你会在使用函数时,Scala有更简洁的语法和方法,这是因为它本来就是函数语言,而Java的设计者在不破坏之前的基础上拓展设计,显然会有更多限制。
尽管Java在语法上与lambda表达式相比有一定局限性,但Java8 也引进了一些很酷的功能。例如,利用方法引用的特性通过重用现有方法使得编写lambda表达式更简洁。更简洁吗???
//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;
Function<Integer, String> intToStr = String::valueOf; |
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函数只需要1个参数,但BiFunction的目标类型是需要2个参数。其实,这种用法通常在Stream API中使用,当看不到类型名时才有意义。
pets.stream().map(Pet::getName).collect(toList()); // The signature of map() function can be derived as // <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; |
不能。它不能编译,编译器会提示“The target type of this expression must be a functional interface”。错误信息很容易引起误解,而且似乎Java 8通过泛型参数并不支持类型接口。即使使用一个Functionalinterface的实例(如前面提到的“STR”),也会出现另一个错误“The type Supplier<String> does not define indexOf(Supplier<String>) that is applicable here”。String::new的函数接口是Supplier<String>,而且它只有方法命名为get()。indexOf是一个属于String对象的实例方法。因此,必须重写这段代码,如下所示。
//Java Function<String, Integer> indexOf = ((Supplier<String>)String::new).get()::indexOf; |
Java 8 是否支持currying (partial function)?
的确可行,但你不能使用方法引用。你可以认为是一个partial函数,但是它返回的是函数而不是结果。接着将要介绍使用currying的简单实例,但这个例子也可能行不通。在传递到函数之前,我们通常进行参数处理。但无论如何,先看看如何利用lambda表达式实现partial 函数。假设你需要利用currying实现两个整数相加的函数。
//Java IntFunction<IntUnaryOperator>add = a -> b -> a + b; add.apply(2).applyAsInt(3);//the result is 4! I'm kidding it's 5. |
该函数可以同时采用两个参数。
//Java Supplier<BiFunction<Integer,Integer, Integer>> add = () -> (a, b) -> a + b; add.get().apply(2, 3); |
现在,可以看看Scala方法。
//Scala val add = (a: Int) => (b:Int) => a + b add(1)(2) |
//Scala val add = () => (a: Int,b: Int) => a + b add2()(1,2) |
因为类型引用和语法糖,Scala的方法比Java更简短。在Scala中,你不需要在Function trait上调用apply 方法,编译器会即时地将()转换为apply 方法。
|