Java 8 vs. Scala(一): Lambda表達式

【編者按】雖然 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 官方博客

相關文章
相關標籤/搜索