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

OneAPM官方技術部落格發表於2015-11-30

【編者按】雖然 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,而且它只有方法命名為 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 官方部落格

相關文章