導航
引例
有這樣一位農場主,他經營着一片蘋果園。某天這位農場主突發奇想,他想找出果園裏所有的綠蘋果。這種簡單的要求,我們可以很輕鬆的幫他實現:
public class Apple {
private String color; // 顏色
private int weight; // 重量
public Apple(String color, int weight) {
this.color = color;
this.weight = weight;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
@Override
public String toString() {
return "Apple [color=" + color + ", weight=" + weight + "]";
}
}
public class FindApple1 {
// 果園
public static List<Apple> orchard = Arrays.asList(new Apple("green", 150),
new Apple("green", 200), new Apple("yellow", 150),new Apple("red", 170));
public static void main(String[] args) {
List<Apple> basket = new ArrayList<>();
// 找到所有的綠蘋果
for (Apple apple : orchard) {
if("green".equals(apple.getColor())) {
basket.add(apple);
}
}
System.out.println(basket);
}
}
然而這位農場主是一個善變的人,突然他改變了主意——找出果園裏所有的紅蘋果而不是綠蘋果。爲了應對需求的變更,同時考慮到這位善變的農場主以後可能會想要找其他顏色的蘋果,我們對程序做出相應的修改:
public class FindApple2 {
public static void main(String[] args) {
List<Apple> basket = appleFilter(FindApple1.orchard, "red");
System.out.println(basket);
}
// 找到指定顏色的蘋果
private static List<Apple> appleFilter(List<Apple> orchard, String color) {
List<Apple> temp = new ArrayList<>();
for (Apple apple : orchard) {
if(color.equals(apple.getColor())) {
temp.add(apple);
}
}
return temp;
}
}
使用修改之後的程序,即使農場主再次改變主意——找出果園裏所有的黃蘋果,仍然可以應對需求的變更。然而農場主的確又改變主意了,只不過這一次他要找的並不是黃蘋果,而是重量大於150g的蘋果。這樣一來,我們修改過的程序又不適用於新的需求了。爲了一勞永逸,我們把程序調整成這樣:
public interface AppleCheck {
boolean test(Apple apple);
}
public class FindApple3 {
public static void main(String[] args) {
List<Apple> basket = appleFilter(FindApple1.orchard, new AppleCheck() {
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
});
System.out.println(basket);
}
// 根據指定條件找蘋果(條件可以隨意變更)
private static List<Apple> appleFilter(List<Apple> orchard, AppleCheck appleCheck) {
List<Apple> temp = new ArrayList<>();
for (Apple apple : orchard) {
if(appleCheck.test(apple)) {
temp.add(apple);
}
}
return temp;
}
}
使用最終版的程序,不管農場主再冒出什麼新的想法,我們只需要修改找蘋果的方式(AppleCheck的具體實現)就可以應對需求變更。
Lambda表達式
引例的最後,我們通過引入接口成功的解決了農場主不斷變化需求的問題。但是使用匿名內部類實現接口的做法讓我們的代碼看起來很笨重,不夠簡潔。那麼還有什麼更好的做法嗎?答案是肯定的,下面使用Lambda表達式來修改程序:
public class FindApple4 {
public static void main(String[] args) {
// 使用Lambda表達式代替匿名內部類
List<Apple> basket = appleFilter(FindApple1.orchard, apple -> apple.getWeight() > 150);
System.out.println(basket);
}
private static List<Apple> appleFilter(List<Apple> orchard, AppleCheck appleCheck) {
List<Apple> temp = new ArrayList<>();
for (Apple apple : orchard) {
if(appleCheck.test(apple)) {
temp.add(apple);
}
}
return temp;
}
}
使用Lambda表達式之後的代碼看上去是不是很優雅,很簡潔?我們只用一行代碼就完成了匿名內部類的工作。 那麼Lambda表達式應該如何使用呢?先不要着急,我們先了解一下Lambda表達式的格式。
格式
在FindApple4中出現的Lambda表達式是這樣的:
apple -> apple.getWeight() > 150 // 簡化版Lambda表達式
上面的Lambda表達式是簡化之後的樣子,完整版是這樣的:
(Apple apple) -> { return apple.getWeight() > 150;} // 完整版Lambda表達式
一個完整的Lambda表達式的格式如下所示:
(參數列表) -> { 方法實現 }
然而通常情況下我們不會書寫完整的Lambda表達式,而是會進行適當的簡化。
格式簡化
Lambda表達式可以根據傳入參數自動推導該參數的類型(類型推導),所以參數列表中的的參數類型可以省略。參數列表簡化規則如下:
- 參數類型可以省略
- 若參數列表只存在一個參數,參數列表的括號可以和參數類型一起省略
- 若參數列表爲空,則參數列表的括號不可以省略
下面通過幾個例子演示上述規則:
(a, b) -> { System.out.println(a+b); } // 正確
a, b -> { System.out.println(a+b); } // 錯誤:不可以存在兩個參數並省略括號
a -> { System.out.println(a); } // 正確
String a -> { System.out.println(a); } // 錯誤:不可以存在參數類型並省略括號() -> { System.out.println("no paramter"); } // 正確
-> { System.out.println("no paramter"); } // 錯誤:參數列表爲空,不可以省略括號
方法實現簡化規則如下:
- 方法實現有多條語句,方法體必須使用花括號
- 方法實現只有一條語句,方法體可以省略花括號(若省略花括號,語句末尾的分號也要一起省略)
- 方法實現只有一條語句且這條語句包含return,則在規則2的基礎上還需要省略return
規則演示:
(a, b) -> { System.out.println(a); System.out.println(b);} // 正確
(a, b) -> System.out.println(a); System.out.println(b); // 錯誤:方法實現有多條語句要用花括號包起來a -> System.out.println(a) // 正確
a -> System.out.println(a); // 錯誤:方法實現只有一條語句,省略花括號時分號必須一起省略a -> {return a;} // 正確
a -> a // 正確
a -> return a // 錯誤:方法實現只有一條包含return的語句,省略花括號時return必須一起省略
Tip:上面羅列的規則可能沒有考慮到所有的情況,總之大家多嘗試一下,不正確的格式是無法通過編譯的。
函數式接口
當然,Lambda表達式是有使用條件的,能夠使用Lambda表達式的前提是函數式接口——只聲明一個抽象方法的接口。再去看我們創建的AppleCheck接口,你就會發現它是一個函數式接口。
我們可以將Lambda表達式作爲函數式接口的一個具體實現,例如這樣:
AppleCheck appleCheck = apple -> "green".equals(apple.getColor());
@FunctionalInterface
我們可以使用Lambda表達式表示函數式接口的一個具體實現,但是在實際開發中,別人可能並不知道某個接口是一個函數式接口,並向其中添加了新的抽象的方法,那麼你之前的使用Lambda表達式作爲該接口的具體實現的代碼就會報錯。因此爲了表示某個接口是一個函數式接口,我們可以使用@FunctionalInterface註解該接口。
使用@FunctionalInterface註解的接口只能聲明一個抽象方法。若接口聲明兩個抽象方法則無法通過編譯。
使用@FunctionalInterface註解的接口一定是函數式接口,不使用@FunctionalInterface註解的接口也可以是函數式接口(只要能保證該接口中只存在一個抽象方法)。
下面將AppleCheck修改爲函數式接口:
@FunctionalInterface // 註解爲函數式接口
public interface AppleCheck {
boolean test(Apple apple);
// Apple getApple(); // 函數式接口中只允許存在一個抽象方法
boolean equals(Object obj); // Object類中public抽象方法
default void getAppleByDefault() { // default方法
System.out.println("getAppleByDefault");
}
static void getAppleByStatic() { // 靜態方法
System.out.println("getAppleByStatic");
}
}
看到上面的代碼你可能會覺得很奇怪,不是說函數式接口只能聲明一個抽象方法嗎?怎麼我們定義的函數式接口存在這麼多方法?確實函數式接口只允許聲明一個抽象方法,但是除抽象方法之外函數式接口中還可以聲明以下兩種方法:
- default方法和靜態方法(具體可以參考這篇文章:Java8新特性(五) default)
- Object類中public抽象方法
關於上述第二點,在FunctionalInterface的JavaDoc中有如下描述:
If an interface declares an abstract method overriding one of the public methods of {@code java.lang.Object}, that also does not count toward the interface's abstract method count.
即接口聲明的抽象方法重寫了Object類中public抽象方法,該抽象方法不計入抽象方法總數。
JDK8中可以定義爲函數式接口的接口都加上了@FunctionalInterface註解,如Comparator接口:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
...
}
四大核心函數式接口
如果僅僅爲了使用Lambda表達式而特意定義一個函數式接口,未免得不償失。其實JDK8已經爲我們預先定義了大量的函數式接口,下面是四大核心函數式接口:
Predicate
Predicate(斷言)接口聲明抽象方法test(),該方法接收一個泛型對象並返回一個布爾值。看到這個接口你可能會覺得似曾相識,沒錯,Predicate接口就是我們定義的AppleCheck接口的泛型版本。
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> or(Predicate<? super T> other) { ... }
default Predicate<T> and(Predicate<? super T> other) { ... }
default Predicate<T> negate() { ... }
...
}
除了抽象方法test()之外,Predicate還提供三個默認方法:and(),or()和negate()。這三個方法的返回值都是Predicate類型,通過這三個方法可以構建更爲複雜的Predicate。看下面一個例子:
public class TestPredicate {
public static void main(String[] args) {
// 條件1
Predicate<Apple> redApple = apple -> "red".equals(apple.getColor());
// 條件2
Predicate<Apple> heavyApple = apple -> apple.getWeight() > 150;
// 條件3
Predicate<Apple> greenApple = apple -> "green".equals(apple.getColor());
// 條件組合
Predicate<Apple> complexPredicate = redApple.and(heavyApple).or(greenApple);
for (Apple apple : FindApple1.orchard) {
if(complexPredicate.test(apple)) System.out.println(apple);;
}
}
}
這三個方法的作用相當於邏輯運算符&&、||和!,complexPredicate的判斷邏輯相當於這樣:
(redApple && heavyApple) || greenApple
除了Predicate之外JDK8還提供特殊版本的Predicate接口:IntPredicate、LongPredicate、DoublePredicate等。
Consumer
Consumer(消費者)接口聲明抽象方法accept(),該方法接收一個泛型對象,沒有返回值。
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) { ... }
}
Consumer提供一個默認方法:andThen(),該方法的返回值爲Consumer類型,可以組合多個Consumer,串聯調用。
public class TestConsumer {
public static void main(String[] args) {
// 操作1
Consumer<Apple> applePrinter1 =
apple -> System.out.print("apple color: " + apple.getColor());
// 操作2
Consumer<Apple> applePrinter2 =
apple -> System.out.println(", apple weight: " + apple.getWeight());
// 組合操作
Consumer<Apple> applePrinter = applePrinter1.andThen(applePrinter2);
for (Apple apple : FindApple1.orchard) {
applePrinter.accept(apple);
}
}
}
同Predicate一樣,除了Consumer之外JDK8還提供IntConsumer、LongConsumer、DoubleConsumer、BiConsumer等接口。
Supplier
Supplier(供應商)接口聲明抽象方法get(),該方法返回一個泛型對象。
@FunctionalInterface
public interface Supplier<T> {
T get();
}
public class TestSupplier {
public static void main(String[] args) {
// 提供200以下的隨機數
Supplier<Integer> weight = () -> new Random().nextInt(200);
// 提供重量在200一下的紅蘋果
Supplier<Apple> appleCreator = () -> new Apple("red", weight.get());
System.out.println(appleCreator.get());
}
}
除了Supplier之外JDK8還提供IntSupplier、LongSupplier、DoubleSupplier、BooleanSupplier等接口。
Function
Function(函數)接口聲明抽象方法apply(),該方法接收一個泛型對象並返回一個泛型對象。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { ... }
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { ... }
...
}
正如其名,調用Function接口類似數學中的函數:,給出入參t,經過f()運算之後,得到出參r。
Function接口提供兩個默認方法:compose()和andThen(),它們的返回值都是Function類型。通過這兩個方法可以將多個Function進行組合調用。
public class TestFunction {
public static void main(String[] args) {
// 函數f()
Function<Integer, Integer> f = x -> x + 1;
// 函數g()
Function<Integer, Integer> g = x -> x * 2;
// 先進行函數g()運算,再開始函數f()運算
Function<Integer, Integer> compose1 = f.compose(g);
// 運算順序和compose1相反
Function<Integer, Integer> compose2 = f.andThen(g);
System.out.println(compose1.apply(3)); // f(g(3)) : 7
System.out.println(compose2.apply(3)); // g(f(3)) : 8
}
}
當然,JDK8也提供了IntFunction、LongToDoubleFunction、BiFunction等接口。
改進
下面使用JDK8提供的函數式接口來修改FindApple4,這裏提供兩種思路:
public class FindApple5 {
public static void main(String[] args) {
List<Apple> basket = appleFilter(FindApple1.orchard, apple -> apple.getWeight() > 150);
System.out.println(basket);
}
private static List<Apple> appleFilter(List<Apple> orchard, Predicate<Apple> predicate) {
List<Apple> temp = new ArrayList<>();
for (Apple apple : orchard) {
if(predicate.test(apple)) {
temp.add(apple);
}
}
return temp;
}
}
public class FindApple6 {
public static void main(String[] args) {
List<Apple> basket = new ArrayList<>();
appleFilter(FindApple1.orchard, basket, (result, apple) -> {
if(apple.getWeight() > 150)
result.add(apple);
});
System.out.println(basket);
}
private static void appleFilter(List<Apple> orchard, List<Apple> basket,
BiConsumer<List<Apple>, Apple> biConsumer) {
for (Apple apple : orchard) {
biConsumer.accept(basket ,apple);
}
}
}
Lambda表達式與變量捕獲
其實說到這兒,好像也沒明說Lambda表達式是個啥。不過你可能已經發現了,Lambda表達式本質上就是特定匿名內部類的簡寫形式。所以既然如此,匿名內部類存在的問題——匿名內部類中訪問的局部變量需要修飾爲final類型,Lambda表達式也一併繼承了下來。
Java8之前,如果在匿名內部類中訪問局部變量,需要顯式的將此變量聲明爲final類型,Java8中則會隱式的將匿名內部類中訪問的局部聲明爲final類型:在Lambda表達式中訪問局部變量的操作,稱之爲變量捕獲。一旦局部變量被Lambda表達式捕獲,那麼該變量會被隱式聲明成final類型。見下面一個例子:
public class TestLocalVariable1 {
public static void main(String[] args) {
int num = 3;
IntConsumer consumer = (n) -> {
System.out.println(num + n); // 此時num已經被聲明爲 final int num = 3
};
consumer.accept(2);
}
}
被Lambda表達式捕獲的變量,我們不能修改它的值——不論是Lambda表達式內還是外。
對於前者,由於被捕獲的變量已經隱式聲明爲final類型,所以我們不能再去修改它的值,故而無法通過編譯;對於後者,由於在Lambda表達式外該變量的值發生了變化,所以這個變量無法被隱式聲明爲final類型,因此報錯。見下面一個例子:
public class TestLocalVariable2 {
public static void main(String[] args) {
int num = 3;
IntConsumer consumer = (n) -> {
// num++; // 無法修改final類型變量的值
System.out.println(num + n);
};
// num++; // 變量的值發生變化,無法被隱式聲明爲final類型
consumer.accept(2);
}
}
不過對於引用類型變量而言,我們可以Lambda表達式中修改該引用指向的對象。因爲引用類型變量中存放的是地址值,用final修飾引用類型變量表示的含義是該引用不可以指向其他的對象。
public class TestLocalVariable3 {
public static void main(String[] args) {
Apple apple = new Apple("red",150);
Consumer<String> consumer = color -> {
// apple = new Apple("green", 170); // 錯誤:不可修改引用指向的對象
apple.setColor(color);
};
consumer.accept("green");
System.out.println(apple);
}
}
方法引用
當我們使用Lambda表達式去實現某個功能時,若恰巧存在某個方法可以實現這個功能,就可以用方法引用來表示這個方法。
從上面的定義不難看出來,方法引用的本質是特定Lambda表達式的表現形式。是一種語法糖。方法引用並未定義新的功能,只是Lambda表達式的一種更簡潔的表達,具有更強的可讀性。
從名字上來看,方法引用屬於引用的一種。而我們知道引用類型數據代表的是對實際值的引用,其本身並不存放任何實際值,方法引用也是如此。方法引用表示對某個方法的引用,其本身並不具有該方法的功能實現。
格式
方法引用的格式如下:
類名(對象名):: 方法名
::是域操作符,表示對方法的引用。方法名後面不需要括號。下面通過一個例子來演示方法引用:
public class Student {
private String name;
private int score;
public Student(String name, int score) {
super();
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
@Override
public String toString() {
return "Student [name=" + name + ", score=" + score + "]";
}
public static int compareByScore(Student s1, Student s2) {
return s1.getScore() - s2.getScore();
}
}
public class TestMethodReference1 {
public static List<Student> students = Arrays.asList(new Student("zhangsan", 64),
new Student("lisi", 85), new Student("wangwu", 71), new Student("zhaoliu", 82));
public static void main(String[] args) {
students.sort(Student::compareByScore);
System.out.println(students);
}
}
上面的例子中,我們想要調用list的sort()方法對集合中的元素排序,而sort()方法使用Comparator接口作爲參數:
public interface List<E> extends Collection<E> {
default void sort(Comparator<? super E> c) { ... }
...
}
之前提到過:JDK8將Comparator註解爲函數式接口,所以我們可以使用Lambda表達式表示Comparator接口的一個實現:
students.sort((Student s1, Student s2) -> s1.getScore() - s2.getScore());
而此時Student類中恰好存在compareScore()方法,可以實現上面Lambda表達式的功能,那麼就可以用方法引用來引用該方法:
students.sort(Student::compareByScore);
三類方法引用
方法引用可以分爲三類:靜態方法引用、實例方法引用和構造方法引用。
靜態方法引用
格式: 類名 :: 靜態方法名
這類方法引用比較好理解——相當於把調用靜態方法的.替換成::(注意,這裏的用詞是相當於,方法調用和方法引用之間沒有任何關係,它們是兩種完全不相同的東西),TestMethodReference1中使用的就是此類方法引用。
實例方法引用
格式1: 對象名 :: 實例方法名
這類方法引用也很容易理解——相當於把調用對象實例方法的.替換成::。
JDK8中Iterable接口新增forEach()方法,該方法使用Consumer接口作爲參數:
public interface Iterable<T> {
default void forEach(Consumer<? super T> action) { ... }
...
}
下面我們來試着使用該方法來打印集合:
public class TestMethodReference2 {
public static void main(String[] args) {
TestMethodReference1.students.forEach(System.out::println);
}
}
看到上面的例子,你可能會覺得奇怪:這裏並沒有出現對象,爲什麼可以使用方法引用呢?其實out就是定義在System類中一個對象:
public final class System {
public final static PrintStream out = null;
...
}
而println()方法正是PrintStream類中的成員方法:
public class PrintStream extends FilterOutputStream implements Appendable, Closeable {
public void println(Object x) { ... }
...
}
這裏我們希望有一個方法可以實現打印對象的功能,而實例對象out正好可以提供println()方法,因此可以使用方法引用。
格式2: 類名 :: 實例方法名
這類方法引用是比較難理解的,我們通過一個例子講解。向Student類中添加下面的方法:
public int compareByScore2(Student s1) {
return this.getScore() - s1.getScore();
}
public class TestMethodReference3 {
public static void main(String[] args) {
TestMethodReference1.students.sort(Student::compareByScore2);
TestMethodReference1.students.forEach(System.out::println);
}
}
通過TestMethodReference1的例子,我們知道sort()方法使用Comparator接口作爲參數。然而Comparator接口的compare()方法有兩個參數,而Student類的compareByScore2()方法卻只有一個參數,這裏爲什麼可以使用方法引用表示compareByScore2()方法呢?
這就是這類方法引用難以理解的地方。首先實例方法肯定需要通過對象來調用,那麼這個對象是從哪兒來的呢?我們知道方法引用對應Lambda表達式,Lambda表達式的第一個參數就會成爲調用實例方法的對象,其餘參數則會作爲該實例方法的參數傳遞。如下圖所示:
下面再演示一個例子強化理解:
public class TestMethodReference4 {
public static void main(String[] args) {
List<String> list = Arrays.asList("zhangsan", "lisi", "wangwu", "zhaoliu");
list.sort(String::compareToIgnoreCase);
list.forEach(System.out::println);
}
}
String類的compareToIgnoreCase()方法定義如下:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
public int compareToIgnoreCase(String str) {
return CASE_INSENSITIVE_ORDER.compare(this, str);
}
...
}
構造方法引用
格式1: 類名 :: new
這類方法引用只能用於構造方法。下面通過一個例子演示:
public class TestMethodReference5 {
public static void main(String[] args) {
// Supplier<Student> supplier = Student::new; // 報錯: Student類中沒有無參構造方法
BiFunction<String, Integer, Student> bf = Student::new;
Student student = bf.apply("zhangsan", 64);
System.out.println(student);
}
}
上面的例子中,試圖通過方法引用表示Student類的無參構造方法,但是由於我們在Student類定義了有參構造方法,所以該類中不存在無參構造方法。因此這裏使用方法引用表示Student類的無參構造方法會出現錯誤信息,方法引用只能表示指定類的無參構造方法。
格式2: 類名[] :: new
這類方法引用是數組專屬的。
其實可能你都沒發現,到現在爲止你都沒有見過數組的構造方法。在Java中並不存在數組這個類,它是一種即時創建的類型。數組的構造方法只有一個int類型參數,該參數表示數組的長度。下面的例子是數組構造方法引用:
public class TestMethodReference6 {
public static void main(String[] args) {
IntFunction<int[]> fun = int[]::new;
int[] arr = fun.apply(5); // 創建長度爲5的數組
System.out.println(arr.length);
}
}
到此爲止,關於Lambda表達式、函數式接口和方法引用就全部介紹完了。不過這些只是所有Java8新特性的基礎,下一篇文章將進入Java8新特性最重要部分的學習——流。
參考:
https://blog.csdn.net/yangyifei2014/article/details/80068265