【编者按】虽然 Java 深得大量开发者喜好,可是对比其余现代编程语言,其语法确实略显冗长。可是经过 Java8,直接利用 lambda 表达式就能编写出既可读又简洁的代码。做者 Hussachai Puripunpinyo 的软件工程师,做者经过对比 Java 8和 Scala,对性能和表达方面的差别进行了分析,而且深刻讨论关于 Stream API 的区别,本文由 OneAPM 工程师编译整理。html
数年等待,Java 8 终于添加了高阶函数这个特性。本人很喜欢 Java,但不得不认可,相比其余现代编程语言,Java 语法很是冗长。然而经过 Java8,直接利用 lambda 表达式就能编写出既可读又简洁的代码(有时甚至比传统方法更具可读性)。java
Java 8于2014年3月3日发布,但笔者最近才有机会接触。由于笔者也很熟悉 Scala,因此就产生了对比 Java 8和Scala 在表达性和性能方面的差别,比较将围绕 Stream API 展开,同时也会介绍如何使用 Stream API 来操做集合。git
因为文章太长,因此分如下三个部分详细叙述。github
Part 1.Lambda 表达式express
Part 2. Stream API vs Scala collection API编程
Part 3. Trust no one, bench everything(引用自sbt-jmh)api
首先,咱们来了解下 Java 8的 lambda 表达式,虽然不知道即便表达式部分是可替代的,他们却称之为 lambda 表达式。这里彻底能够用声明来代替表达式,而后说 Java 8还支持 lambda 声明。编程语言将函数做为一等公民,函数能够被做为参数或者返回值传递,由于它被视为对象。Java是一种静态的强类型语言。因此,函数必须有类型,所以它也是一个接口。服务器
另外一方面,lambda 函数就是实现了函数接口的一个类。无需建立这个函数的类,编译器会直接实现。不幸的是,Java 没有 Scala 那样高级的类型接口。若是你想声明一个 lambda 表达式,就必须指定目标类型。实际上,因为 Java 必须保持向后兼容性,这也是可理解的,并且就目前来讲 Java 完成得很好。例如,Thread.stop() 在 JDK 1.0版时发布,已过期了十多年,但即使到今天仍然还在使用。因此,不要由于语言 XYZ 的语法(或方法)更好,就期望 Java 从根本上改变语法结构。oracle
因此,Java 8的语言设计师们奇思妙想,作成函数接口!函数接口是只有一个抽象方法的接口。要知道,大多数回调接口已经知足这一要求。所以,咱们能够不作任何修改重用这些接口。@FunctionalInterface 是表示已注释接口是函数接口的注释。此注释是可选的,除非有检查要求,不然不用再进行处理。app
请记住,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、Function一、……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 方法。
原文连接: https://dzone.com/articles/java-8-λe-vs-scalapart-i
OneAPM for Java 可以深刻到全部 Java 应用内部完成应用性能管理和监控,包括代码级别性能问题的可见性、性能瓶颈的快速识别与追溯、真实用户体验监控、服务器监控和端到端的应用性能管理。想阅读更多技术文章,请访问 OneAPM 官方博客。