您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
Java 8 vs. Scala(一): Lambda表达式
 
 作者: Hussachai Puripunpinyo   来源: Dzone  发布于 2015-12-3
  1921  次浏览      16
 

摘要:虽然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 方法。

 

   
1921 次浏览       16
相关文章

Java微服务新生代之Nacos
深入理解Java中的容器
Java容器详解
Java代码质量检查工具及使用案例
相关文档

Java性能优化
Spring框架
SSM框架简单简绍
从零开始学java编程经典
相关课程

高性能Java编程与系统性能优化
JavaEE架构、 设计模式及性能调优
Java编程基础到应用开发
JAVA虚拟机原理剖析
最新课程计划
信息架构建模(基于UML+EA)3-21[北京]
软件架构设计师 3-21[北京]
图数据库与知识图谱 3-25[北京]
业务架构设计 4-11[北京]
SysML和EA系统设计与建模 4-22[北京]
DoDAF规范、模型与实例 5-23[北京]

Java 中的中文编码问题
Java基础知识的三十个经典问答
玩转 Java Web 应用开发
使用Spring更好地处理Struts
用Eclipse开发iPhone Web应用
插件系统框架分析
更多...   


Struts+Spring+Hibernate
基于J2EE的Web 2.0应用开发
J2EE设计模式和性能调优
Java EE 5企业级架构设计
Java单元测试方法与技术
Java编程方法与技术


Struts+Spring+Hibernate/EJB+性能优化
华夏基金 ActiveMQ 原理与管理
某民航公司 Java基础编程到应用开发
某风电公司 Java 应用开发平台与迁移
日照港 J2EE应用开发技术框架与实践
某跨国公司 工作流管理JBPM
东方航空公司 高级J2EE及其前沿技术
更多...