Java8新特性(一) Lambda表達式、函數式接口與方法引用

導航

引例

Lambda表達式

格式

格式簡化

函數式接口

@FunctionalInterface

四大核心函數式接口

Predicate

Consumer

Supplier

Function

改進

Lambda表達式與變量捕獲

方法引用

格式

三類方法引用

靜態方法引用

實例方法引用

構造方法引用


引例

有這樣一位農場主,他經營着一片蘋果園。某天這位農場主突發奇想,他想找出果園裏所有的綠蘋果。這種簡單的要求,我們可以很輕鬆的幫他實現:

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表達式可以根據傳入參數自動推導該參數的類型(類型推導),所以參數列表中的的參數類型可以省略。參數列表簡化規則如下:

  1. 參數類型可以省略
  2. 若參數列表只存在一個參數,參數列表的括號可以和參數類型一起省略
  3. 若參數列表爲空,則參數列表的括號不可以省略

下面通過幾個例子演示上述規則:

(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"); }    //  錯誤:參數列表爲空,不可以省略括號

方法實現簡化規則如下:

  1. 方法實現有多條語句,方法體必須使用花括號
  2. 方法實現只有一條語句,方法體可以省略花括號(若省略花括號,語句末尾的分號也要一起省略)
  3. 方法實現只有一條語句且這條語句包含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");
	}
	
}

看到上面的代碼你可能會覺得很奇怪,不是說函數式接口只能聲明一個抽象方法嗎?怎麼我們定義的函數式接口存在這麼多方法?確實函數式接口只允許聲明一個抽象方法,但是除抽象方法之外函數式接口中還可以聲明以下兩種方法

  1. default方法和靜態方法(具體可以參考這篇文章:Java8新特性(五) default
  2. 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接口類似數學中的函數:r=f(t),給出入參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

https://blog.csdn.net/sun_promise/article/details/51190256

https://segmentfault.com/a/1190000012269548

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章