在《Java8實戰》中第三章主要講的是Lambda表達式
,在上一章節的筆記中咱們利用了行爲參數化來因對不斷變化的需求,最後咱們也使用到了Lambda,經過表達式爲咱們簡化了不少代碼從而極大地提升了咱們的效率。那咱們就來更深刻的瞭解一下如何使用Lambda表達式,讓咱們的代碼更加具備簡潔性和易讀性。java
什麼是Lambda表達式?簡單的來講,Lambda表達式是一個匿名函數,Lambda表達式基於數學中的λ演算得名,直接對應其中的Lambda抽象(lambda abstraction),是一個匿名函數,既沒有函數名的函數。Lambda表達式能夠表示閉包(注意和數學傳統意義的不一樣)。你也能夠理解爲,簡潔的表示可傳遞的匿名函數的一種方式:它沒有名稱,但它有參數列表、函數主體、返回類型,可能還有一個能夠拋出異常的列表。git
有時候,咱們爲了簡化代碼而去使用匿名類,雖然匿名類能簡化一部分代碼,可是看起來很囉嗦。爲了更好的的提升開發的效率以及代碼的簡潔性和可讀性,Java8推出了一個核心的新特性之一:Lambda表達式。程序員
Java8以前,使用匿名類給蘋果排序的代碼:github
apples.sort(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
});
複製代碼
是的,這段代碼看上去並非那麼的清晰明瞭,使用Lambda表達式改進後:express
Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
複製代碼
或者是:bash
Comparator<Apple> byWeight = Comparator.comparing(Apple::getWeight);
複製代碼
不得不認可,代碼看起來跟清晰了。要是你以爲Lambda表達式看起來一頭霧水的話也不要緊,咱們慢慢的來了解它。微信
如今,咱們來看看幾個Java8中有效的Lambda表達式加深對Lambda表達式的理解:閉包
// 這個表達式具備一個String類型的參數並返回一個int,Lambda並無return語句,由於已經隱含了return。
(String s) -> s.length()
// 這個表達式有一個Apple類型的參數並返回一個boolean(蘋果重來是否大於150克)
(Apple a) -> a.getWeight() > 150
// 這個表達式具備兩個int類型二的參數而且沒有返回值。Lambda表達式能夠包含多行代碼,不僅是這兩行。
(int x, int y) -> {
System.out.println("Result:");
System.out.println(x + y);
}
// 這個表達式沒有參數類型,返回一個int。
() -> 250
// 顯式的指定爲Apple類型,並對重量進行比較返回int
(Apple a2, Apple a2) -> a1.getWeight.compareTo(a2.getWeight())
複製代碼
Java語言設計者選選擇了這樣的語法,是由於C#和Scala等語言中的相似功能廣受歡迎。Lambda的基本語法是:app
(parameters) -> expression
複製代碼
或者(請注意花括號):ide
(parameters) -> {statements;}
複製代碼
是的,Lambda表達式的語法看起來就是那麼簡單。那咱們繼續看幾個例子,看看如下哪幾個是有效的:
(1) () -> {}
(2) () -> "Jack"
(3) () -> {return "Jack"}
(4) (Interge i) -> return "Alan" + i;
(5) (String s) -> {"IronMan";}
複製代碼
正確答案是:(1)、(2)、(3)
緣由:
(1) 是一個無參而且無返回的,相似與private void run() {}.
(2) 是一個無參而且返回的是一個字符串。
(3) 是一個無參,而且返回的是一個字符串,不過裏面還能夠繼續寫一些其餘的代碼(利用顯式返回語句)。
(4) 它沒有使用使用顯式返回語句,因此它不能算是一個表達式。想要有效就必須加一對花括號, (Interge i) -> {return "Alan" + i}
(5) "IronMan"很顯然是一個表達式,不是一個語句,去掉這一對花括號或者使用顯式返回語句便可有效。
咱們剛剛已經看了不少關於Lambda表達式的語法例子,可能你還不太清楚這個Lambda表達式到底如何使用。
還記得在上一章的讀書筆記中,實現的filter方法中,咱們使用的就是Lambda:
List<Apple> heavyApples = filter(apples, (Apple apple) -> apple.getWeight() > 150);
複製代碼
咱們能夠在函數式接口上使用Lambda表達式,函數式接口聽起來很抽象,可是不用太擔憂接下來就會解釋函數式接口是什麼。
還記得第二章中的讀書筆記,爲了參數化filter方法的行爲使用的Predicate接口嗎?它就是一個函數式接口。什麼是函數式接口?一言蔽之,函數式接口就是隻定義了一個抽象方法的接口。例如JavaAPI中的:Comparator、Runnable、Callable:
public interface Comparable<T> {
public int compareTo(T o);
}
public interface Runnable {
public abstract void run();
}
public interface Callable<V> {
V call() throws Exception;
}
複製代碼
固然,不僅是它們,還有不少一些其餘的函數式接口。
函數式接口到底能夠用來幹什麼?Lambda表達式容許你直接之內聯的形式爲函數式接口的抽象方法提供實例,並把整個表達式做爲函數式接口的實例(具體來講,是函數式接口一個具體實現的實例)。你也可使用匿名類實現,只不過看來並非那麼的一目瞭然。使用匿名類你須要提供一個實例,而後在直接內聯將它實例化。
經過下面的代碼,你能夠來比較一下使用函數式接口和使用匿名類的區別:
// 使用Lambda表達式
Runnable r1 = () -> System.out.println("HelloWorld 1");
// 使用匿名類
Runnable r2 = new Runnable() {
@Override
public void run() {
System.out.println("HelloWorld 2");
}
};
// 運行結果
System.out.println("Runnable運行結果:");
// HelloWorld 1
process(r1);
// HelloWorld 2
process(r2);
// HelloWorld 3
process(() -> System.out.println("HelloWorld 3"));
private static void process(Runnable r) {
r.run();
}
複製代碼
酷,從上面的代碼能夠看出使用Lambda表達式你能夠減小不少代碼同時也提升了代碼的可讀性而使用匿名類卻要四五行左右的代碼。
函數接口的抽象方法的前面基本上就是Lambda表達式的簽名。咱們將這種抽象方法叫作函數描述符。例如,Runnable接口能夠看做一個什麼也不接受什麼也不返回的函數簽名,由於它只有一個叫作run的抽象方法,這個方法沒有參數而且是無返回的。
函數式接口頗有用,由於抽象方法的簽名能夠描述Lambda表達式的簽名。函數式接口的抽象方法的簽名稱爲函數描述符。
在第一章的讀書筆記中,有提到過Predicate這個接口,如今咱們來詳細的瞭解一下它。
java.util.function.Predicate接口定義了一個名字叫test的抽象方法,它接受泛型T對象,並返回一個boolean值。以前咱們是建立了一個Predicate這樣的一個接口,如今咱們所說到的這個接口和以前建立的同樣,如今咱們不須要再去建立一個這樣的接口就直接可使用了。在你須要表示一個涉及類型T的布爾表達式時,就可使用這個接口。好比,你能夠定義一個接受String對象的Lambda表達式:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
private static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for (T t : list) {
if (predicate.test(t)) {
result.add(t);
}
}
return result;
}
List<String> strings = Arrays.asList("Hello", "", "Java8", "", "In", "Action");
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> stringList = filter(strings, nonEmptyStringPredicate);
// [Hello, Java8, In, Action]
System.out.println(stringList);
複製代碼
若是,你去查看Predicate這個接口的源碼你會發現有一些and或者or等等一些其餘的方法,而且這個方法還有方法體,不過你目前無需關注這樣的方法,之後的文章將會介紹到爲何在接口中能定義有方法體的方法。
java.util.function.Consumer定義了一個叫作accept的抽象方法,它接受泛型T的對象,而且是一個無返回的方法。你若是須要訪問類型T的對象,並對其執行某些操做,就可使用這個接口。好比,你能夠用它來建立一個foreach方法,並配合Lambda來打印列表中的全部元素.
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
private static <T> void forEach(List<T> list, Consumer<T> consumer) {
for (T i : list) {
consumer.accept(i);
}
}
// 使用Consumer
forEach(Arrays.asList("Object", "Not", "Found"), (String str) -> System.out.println(str));
forEach(Arrays.asList(1, 2, 3, 4, 5, 6), System.out::println);
複製代碼
java.util.function.Function<T, R>接口定義了一個叫作apply的方法,它接受一個泛型T的對象,並返回一個泛型R的對象。若是你須要定義一個Lambda,將輸入對象的信息映射到輸出,就可使用這個接口(好比提取蘋果的重量,把字符串映射爲它的長度)。在下面的代碼中,咱們來看看如何利用它來建立一個map方法,將以一個String列表映射到包含每一個String長度的Integer列表。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
private static <T, R> List<R> map(List<T> list, Function<T, R> function) {
List<R> result = new ArrayList<>();
for (T s : list) {
result.add(function.apply(s));
}
return result;
}
List<Integer> map = map(Arrays.asList("lambdas", "in", "action"), (String s) -> s.length());
// [7, 2, 6]
System.out.println(map);
複製代碼
咱們剛剛瞭解了三個泛型函數式接口:Predicate、Consumer和Function<T, R>。還有些函數式接口專爲某些類而設計。
回顧一下:Java類型要麼用引用類型(好比:Byte、Integer、Object、List),要麼是原始類型(好比:int、double、byte、char)。可是泛型(好比Consumer中的T)只能綁定到引用類型。這是由泛型接口內部實現方式形成的。所以,在Java裏面有一個將原始類型轉爲對應的引用類型的機制。這個機制叫做裝箱(boxing)。相反的操做,也就是將引用類型轉爲對應的原始類型,叫做拆箱(unboxing)。Java還有一個自動裝箱機制來幫助程序員執行這一任務:裝箱和拆箱操做都是自動完成的。好比,這就是爲何下面的代碼是有效的(一個int被裝箱成爲Integer):
List<Integer> list = new ArrayList<>;
for (int i = 0; i < 100; i++) {
list.add(i);
}
複製代碼
可是像這種自動裝箱和拆箱的操做,性能方面是要付出一些代價的。裝箱的本質就是將原來的原始類型包起來,並保存在堆裏。所以,裝箱後的值須要更多的內存,並須要額外的內存搜索來獲取被包裹的原始值。
Java8爲咱們前面所說的函數式接口帶來了一個專門的版本,以便在輸入和輸出都是原始類型時,避免自動裝箱的操做。好比,在下面的代碼中,使用IntPredicate就避免了對值1000進行裝箱操做,但要使用Predicate就會把參數1000裝箱到一個Integer對象中:
@FunctionalInterface
public interface IntPredicate {
boolean test(int value);
}
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
// 無裝箱
evenNumbers.test(1000);
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1;
// 裝箱
oddNumbers.test(1000);
複製代碼
通常來講,針對專門的輸入參數類型的函數式接口的名稱都要加上對應的原始類型前綴,好比DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function接口還有針對輸出參數類型變種:ToIntFunction、IntToDoubleFunction等。
Java8中還有不少經常使用的函數式接口,若是你有興趣能夠去查找一些相關的資料,瞭解了這些經常使用的函數接口以後,會對你之後瞭解Stream的知識有很大的幫助。
《Java8實戰》這本書第三章的內容不少,因此我打算分兩篇文章來寫。這些讀書筆記系列的文章內容不少地方都是借鑑書中的內容。若是您有時間、興趣和經濟的話能夠去買這本書籍。這本書我看了兩遍,是一本很不錯的技術書籍。若是,您沒有太多的時間那麼您就能夠關注個人微信公衆號或者當前的技術社區的帳號,利用空閒的時間看看個人文章,很是感謝您對個人關注!