1. 程式人生 > >Java8初體驗(一)lambda表示式語法

Java8初體驗(一)lambda表示式語法

感謝同事【天錦】的投稿。投稿請聯絡 [email protected]

本文主要記錄自己學習Java8的歷程,方便大家一起探討和自己的備忘。因為本人也是剛剛開始學習Java8,所以文中肯定有錯誤和理解偏差的地方,希望大家幫忙指出,我會持續修改和優化。本文是該系列的第一篇,主要介紹Java8對屌絲碼農最有吸引力的一個特性—lambda表示式。

java8的安裝

工欲善其器必先利其器,首先安裝JDK8。過程省略,大家應該都可以自己搞定。但是有一點這裡強調一下(Windows系統):目前我們工作的版本一般是java 6或者java 7,所以很多人安裝java8基本都是學習為主。這樣就在自己的機器上會存在多版本的JDK。而且大家一般是希望在命令列中執行java命令是基於老版本的jdk。但是在安裝完jdk8並且沒有設定path的情況下,你如果在命令列中輸入:java -version,螢幕上會顯示是jdk 8。這是因為jdk8安裝的時候,會預設在C:/Windows/System32中增加java.exe,這個呼叫的優先順序比path設定要高。所以即使path裡指定是老版本的jdk,但是執行java命令顯示的依然是新版本的jdk。這裡我們要做的就是刪除C:/Windows/System32中的java.exe檔案(不要手抖!)。

Lambda初體驗

下面進入本文的正題–lambda表示式。首先我們看一下什麼是lambda表示式。以下是維基百科上對於”Lambda expression”的解釋:

 a function (or a subroutine) defined, and possibly called, without being bound to an identifier。

簡單點說就是:一個不用被繫結到一個識別符號上,並且可能被呼叫的函式。這個解釋還不夠通俗,lambda表示式可以這樣定義(不精確,自己的理解):一段帶有輸入引數的可執行語句塊。這樣就比較好理解了吧?一例勝千言。有讀者反饋:不理解Stream的含義,所以這裡先提供一個沒用stream的lambda表示式的例子。

//這裡省略list的構造
List<String> names = ...;
Collections.sort(names, (o1, o2) -> o1.compareTo(o2));
//這裡省略list的構造
List<String> names = ...;
Collections.sort(names, new Comparator<String>() {
  @Override
  public int compare(String o1, String o2) {
    return o1.compareTo(o2);
  }
});

上面兩段程式碼分別是:使用lambda表示式來排序和使用匿名內部類來排序。這個例子可以很明顯的看出lambda表示式簡化程式碼的效果。接下來展示lambda表示式和其好基友Stream的配合。

List<String> names = new ArrayList<>();
names.add("TaoBao");
names.add("ZhiFuBao");
List<String> lowercaseNames = names.stream().map((String name) -> {return name.toLowerCase();}).collect(Collectors.toList());

這段程式碼就是對一個字串的列表,把其中包含的每個字串都轉換成全小寫的字串(熟悉Groovy和Scala的同學肯定會感覺很親切)。注意程式碼第四行的map方法呼叫,這裡map方法就是接受了一個lambda表示式(其實是一個java.util.function.Function的例項,後面會介紹)。

為什麼需要Lambda表示式呢?在嘗試回答這個問題之前,我們先看看在Java8之前,如果我們想做上面程式碼的操作應該怎麼辦。

先看看普通青年的程式碼:

List<String> names = new ArrayList<>();
names.add("TaoBao");
names.add("ZhiFuBao");
List<String> lowercaseNames = new ArrayList<>();
for (String name : names) {
  lowercaseNames.add(name.toLowerCase());
}

接下來看看文藝青年的程式碼(藉助Guava):

List<String> names = new ArrayList<>();
names.add("TaoBao");
names.add("ZhiFuBao");
List<String> lowercaseNames = FluentIterable.from(names).transform(new Function<String, String>() {
  @Override
  public String apply(String name) {
    return name.toLowerCase();
  }
}).toList();

在此,我們不再討論普通青年和文藝青年的程式碼風格孰優孰劣(有興趣的可以去google搜尋“指令式程式設計vs宣告式程式設計”)。本人更加喜歡宣告式的程式設計風格,所以偏好文藝青年的寫法。但是在文藝青年程式碼初看起來看起來干擾資訊有點多,Function匿名類的構造語法稍稍有點冗長。所以Java8的lambda表示式給我們提供了建立SAM(Single Abstract Method)介面更加簡單的語法糖。


Lambda語法詳解

我們在此抽象一下lambda表示式的一般語法:

(Type1 param1, Type2 param2, ..., TypeN paramN) -> {
  statment1;
  statment2;
  //.............
  return statmentM;
}

從lambda表示式的一般語法可以看出來,還是挺符合上面給出的非精確版本的定義–“一段帶有輸入引數的可執行語句塊”。

上面的lambda表示式語法可以認為是最全的版本,寫起來還是稍稍有些繁瑣。彆著急,下面陸續介紹一下lambda表示式的各種簡化版:

1. 引數型別省略–絕大多數情況,編譯器都可以從上下文環境中推斷出lambda表示式的引數型別。這樣lambda表示式就變成了:

(param1,param2, ..., paramN) -> {
  statment1;
  statment2;
  //.............
  return statmentM;
}

所以我們最開始的例子就變成了(省略了List的建立):

List<String> lowercaseNames = names.stream().map((name) -> {return name.toLowerCase();}).collect(Collectors.toList());

2. 當lambda表示式的引數個數只有一個,可以省略小括號。lambda表示式簡寫為:

param1 -> {
  statment1;
  statment2;
  //.............
  return statmentM;
}

所以最開始的例子再次簡化為:

List<String> lowercaseNames = names.stream().map(name -> {return name.toLowerCase();}).collect(Collectors.toList());

3. 當lambda表示式只包含一條語句時,可以省略大括號、return和語句結尾的分號。lambda表示式簡化為:

param1 -> statment

所以最開始的例子再次簡化為:

List<String> lowercaseNames = names.stream().map(name -> name.toLowerCase()).collect(Collectors.toList());

4. 使用Method Reference(具體語法後面介紹)

//注意,這段程式碼在Idea 13.0.2中顯示有錯誤,但是可以正常執行
List<String> lowercaseNames = names.stream().map(String::toLowerCase).collect(Collectors.toList());

Lambda表示式眼中的外部世界

我們前面所有的介紹,感覺上lambda表示式像一個閉關鎖國的傢伙,可以訪問給它傳遞的引數,也能自己內部定義變數。但是卻從來沒看到其訪問它外部的變數。是不是lambda表示式不能訪問其外部變數?我們可以這樣想:lambda表示式其實是快速建立SAM介面的語法糖,原先的SAM介面都可以訪問介面外部變數,lambda表示式肯定也是可以(不但可以,在java8中還做了一個小小的升級,後面會介紹)。

String[] array = {"a", "b", "c"};
for(Integer i : Lists.newArrayList(1,2,3)){
  Stream.of(array).map(item -> Strings.padEnd(item, i, '@')).forEach(System.out::println);
}

上面的這個例子中,map中的lambda表示式訪問外部變數Integer i。並且可以訪問外部變數是lambda表示式的一個重要特性,這樣我們可以看出來lambda表示式的三個重要組成部分:

  • 輸入引數
  • 可執行語句
  • 存放外部變數的空間

不過lambda表示式訪問外部變數有一個非常重要的限制:變數不可變(只是引用不可變,而不是真正的不可變)。

String[] array = {"a", "b", "c"};
for(int i = 1; i<4; i++){
  Stream.of(array).map(item -> Strings.padEnd(item, i, '@')).forEach(System.out::println);
}

上面的程式碼,會報編譯錯誤。因為變數i被lambda表示式引用,所以編譯器會隱式的把其當成final來處理(ps:大家可以想象問什麼上一個例子不報錯,而這個報錯。)細心的讀者肯定會發現不對啊,以前java的匿名內部類在訪問外部變數的時候,外部變數必須用final修飾。Bingo,在java8對這個限制做了優化(前面說的小小優化),可以不用顯示使用final修飾,但是編譯器隱式當成final來處理。

lambda眼中的this

在lambda中,this不是指向lambda表示式產生的那個SAM物件,而是宣告它的外部物件。

方法引用(Method reference)和構造器引用(construct reference)

方法引用

前面介紹lambda表示式簡化的時候,已經看過方法引用的身影了。方法引用可以在某些條件成立的情況下,更加簡化lambda表示式的宣告。方法引用語法格式有以下三種:

  • objectName::instanceMethod
  • ClassName::staticMethod
  • ClassName::instanceMethod

前兩種方式類似,等同於把lambda表示式的引數直接當成instanceMethod|staticMethod的引數來呼叫。比如System.out::println等同於x->System.out.println(x);Math::max等同於(x, y)->Math.max(x,y)。

最後一種方式,等同於把lambda表示式的第一個引數當成instanceMethod的目標物件,其他剩餘引數當成該方法的引數。比如String::toLowerCase等同於x->x.toLowerCase()。

構造器引用

構造器引用語法如下:ClassName::new,把lambda表示式的引數當成ClassName構造器的引數 。例如BigDecimal::new等同於x->new BigDecimal(x)。

吐槽一下方法引用

表面上看起來方法引用和構造器引用進一步簡化了lambda表示式的書寫,但是個人覺得這方面沒有Scala的下劃線語法更加通用。比較才能看出,翠花,上程式碼!

List<String> names = new ArrayList<>();
names.add("TaoBao");
names.add("ZhiFuBao");
names.stream().map(name -> name.charAt(0)).collect(Collectors.toList());

上面的這段程式碼就是給定一個String型別的List,獲取每個String的首字母,並將其組合成新的List。這段程式碼就沒辦法使用方法引用來簡化。接下來,我們簡單對比一下Scala的下劃線語法(不必太糾結Scala的語法,這裡只是做個對比):

//省略List的初始化
List[String] names = ....
names.map(_.charAt(0))

在Scala中基本不用寫lambda表示式的引數宣告。

引用文件

  1. 《Java SE 8 for the Really Impatient》
  2. Java 8 Tutorial
  3. Java 8 API doc