一、java8的Lambda表示式
什麼是Lambda表示式
Lambda表示式是一段可以傳遞的程式碼。
λ表示式本質上是一個匿名方法。使用Lambda表示式可以使程式碼變的更加緊湊,例如在Java中實現一個執行緒,只輸出一個字串Hello World!,我們的程式碼如下所示:
public static void main(String[] args) throws Exception {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello World!" );
}
}).start();
TimeUnit.SECONDS.sleep(1000);
}
使用Lambda表示式之後程式碼變成如下形式:
public static void main(String[] args) throws Exception {
new Thread(() -> System.out.println("Hello World!")).start();
TimeUnit.SECONDS.sleep(1000);
}
引數型別也可以省略,Java編譯器會根據上下文推斷出來:
可見λ表示式有三部分組成:引數列表,箭頭(->),以及一個表示式或語句塊。
下面這個例子裡的λ表示式沒有引數,也沒有返回值(相當於一個方法接受0個引數,返回void,其實就是Runnable裡run方法的一個實現):
() -> { System.out.println("Hello Lambda!"); }
如果只有一個引數且可以被Java推斷出型別,那麼引數列表的括號也可以省略:
Lambda表示式的語法
(int x, int y) -> x + y
() -> 42
(String s) -> { System.out.println(s); }
第一個lambda表示式接收x和y這兩個整形引數並返回它們的和;第二個lambda表示式不接收引數,返回整數’42’;第三個lambda表示式接收一個字串並把它列印到控制檯,不返回值。
lambda表示式的語法由引數列表、箭頭符號->和函式體組成。函式體既可以是一個表示式,也可以是一個語句塊:
- 表示式:表示式會被執行然後返回執行結果。
- 語句塊:語句塊中的語句會被依次執行,就像方法中的語句一樣——
- return語句會把控制權交給匿名方法的呼叫者
- break和continue只能在迴圈中使用
- 如果函式體有返回值,那麼函式體內部的每一條路徑都必須返回值
函式式介面
我們把這些只擁有一個方法的介面稱為函式式介面。(之前它們被稱為SAM型別,即單抽象方法型別(Single Abstract Method))
我們並不需要額外的工作來宣告一個介面是函式式介面:編譯器會根據介面的結構自行判斷
(判斷過程並非簡單的對介面方法計數:一個介面可能冗餘的定義了一個Object已經提供的方法,比如toString(),或者定義了靜態方法或預設方法,這些都不屬於函式式介面方法的範疇)。
不過API作者們可以通過@FunctionalInterface註解來顯式指定一個介面是函式式介面(以避免無意聲明瞭一個符合函式式標準的介面),加上這個註解之後,編譯器就會驗證該介面是否滿足函式式介面的要求。
下面是Java SE 7中已經存在的函式式介面:
java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.beans.PropertyChangeListener
java.util.function
除此之外,Java SE 8中增加了一個新的包:java.util.function,它裡面包含了常用的函式式介面,例如
● Predicate——接收T物件並返回boolean
● Consumer——接收T物件,不返回值
● Function
Predicate介面
Predicate 介面只有一個引數,返回boolean型別。該介面包含多種預設方法來將Predicate組合成其他複雜的邏輯(比如:與,或,非):
Predicate<String> predicate = (s) -> s.length() > 0;
predicate.test("foo"); // true
predicate.negate().test("foo"); // false
Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();
Function 介面
Function 介面有一個引數並且返回一個結果,並附帶了一些可以和其他函式組合的預設方法(compose, andThen):
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123"); // "123"
Supplier 介面
Supplier 介面返回一個任意範型的值,和Function介面不同的是該介面沒有任何引數
Supplier<Person> personSupplier = Person::new;
personSupplier.get(); // new Person
Consumer 介面
Consumer 介面表示執行在單個引數上的操作。
Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));
Comparator 介面
Comparator 是老Java中的經典介面, Java 8在此之上添加了多種預設方法:
Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);
Person p1 = new Person("John", "Doe");
Person p2 = new Person("Alice", "Wonderland");
comparator.compare(p1, p2); // > 0
comparator.reversed().compare(p1, p2); // < 0
方法引用
lambda表示式允許我們定義一個匿名方法,並允許我們以函式式介面的方式使用它。我們也希望能夠在已有的方法上實現同樣的特性。
方法引用有很多種,它們的語法如下:
靜態方法引用:ClassName::methodName
例項上的例項方法引用:instanceReference::methodName
超類上的例項方法引用:super::methodName
型別上的例項方法引用:ClassName::methodName
構造方法引用:Class::new
陣列構造方法引用:TypeName[]::new
對於靜態方法引用,我們需要在類名和方法名之間加入::分隔符,例如Integer::sum。
對於具體物件上的例項方法引用,我們則需要在物件名和方法名之間加入分隔符:
Set<String> knownNames = ...
Predicate<String> isKnown = knownNames::contains;
這裡的隱式lambda表示式(也就是例項方法引用)會從knownNames中捕獲String物件,而它的方法體則會通過Set.contains使用該String物件。
有了例項方法引用,在不同函式式介面之間進行型別轉換就變的很方便:
Callable<Path> c = ...
Privileged<Path> a = c::call;
引用任意物件的例項方法則需要在例項方法名稱和其所屬型別名稱間加上分隔符:
Function<String, String> upperfier = String::toUpperCase;
這裡的隱式lambda表示式(即String::toUpperCase例項方法引用)有一個String引數,這個引數會被toUpperCase方法使用。
如果型別的例項方法是泛型的,那麼我們就需要在::分隔符前提供型別引數,或者(多數情況下)利用目標型別推匯出其型別。
需要注意的是,靜態方法引用和型別上的例項方法引用擁有一樣的語法。編譯器會根據實際情況做出決定。
一般我們不需要指定方法引用中的引數型別,因為編譯器往往可以推匯出結果,但如果需要我們也可以顯式在::分隔符之前提供引數型別資訊。
和靜態方法引用類似,構造方法也可以通過new關鍵字被直接引用:
SocketImplFactory factory = MySocketImpl::new;
如果型別擁有多個構造方法,那麼我們就會通過目標型別的方法引數來選擇最佳匹配,這裡的選擇過程和呼叫構造方法時的選擇過程是一樣的。
如果待例項化的型別是泛型的,那麼我們可以在型別名稱之後提供型別引數,否則編譯器則會依照”菱形”構造方法呼叫時的方式進行推導。
陣列的構造方法引用的語法則比較特殊,為了便於理解,你可以假想存在一個接收int引數的陣列構造方法。參考下面的程式碼:
IntFunction<int[]> arrayMaker = int[]::new;
int[] array = arrayMaker.apply(10) // 建立陣列 int[10]
變數的作用域
捕獲的概念在於解決在λ表示式中我們可以使用哪些外部變數(即除了它自己的引數和內部定義的本地變數)的問題。
答案是:與內部類非常相似,但有不同點。不同點在於內部類總是持有一個其外部類物件的引用。而λ表示式呢,除非在它內部用到了其外部類(包圍類)物件的方法或者成員,否則它就不持有這個物件的引用。
在Java8以前,如果要在內部類訪問外部物件的一個本地變數,那麼這個變數必須宣告為final才行。在Java8中,這種限制被去掉了,代之以一個新的概念,“effectively final”。它的意思是你可以宣告為final,也可以不宣告final但是按照final來用,也就是一次賦值永不改變。換句話說,保證它加上final字首後不會出編譯錯誤。
在Java8中,內部類和λ表示式都可以訪問effectively final的本地變數。λ表示式的例子如下:
int tmp1 = 1; //包圍類的成員變數
static int tmp2 = 2; //包圍類的靜態成員變數
public void testCapture() {
int tmp3 = 3; //沒有宣告為final,但是effectively final的本地變數
final int tmp4 = 4; //宣告為final的本地變數
int tmp5 = 5; //普通本地變數
Function<Integer, Integer> f1 = i -> i + tmp1;
Function<Integer, Integer> f2 = i -> i + tmp2;
Function<Integer, Integer> f3 = i -> i + tmp3;
Function<Integer, Integer> f4 = i -> i + tmp4;
Function<Integer, Integer> f5 = i -> {
tmp5 += i; // 編譯錯!對tmp5賦值導致它不是effectively final的
return tmp5;
};
tmp5 = 9; // 編譯錯!對tmp5賦值導致它不是effectively final的
}
Java要求本地變數final或者effectively final的原因是多執行緒併發問題。內部類、λ表示式都有可能在不同的執行緒中執行,允許多個執行緒同時修改一個本地變數不符合Java的設計理念。