Lambda表達式介紹
Lambda表達式是Java 8中新增的新功能之一,使用lambda表達式可以替代只有一個抽象函數的函數式接口的實現,告別匿名內部類並使代碼簡單易懂。同時配合Stream API,可以提升對集合的迭代、遍歷過濾等操作的並行性和便捷性。Lambda表達式的官方文檔可見:https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html#use-case。下面首先看兩個常見的lambda表達式使用示例,多線程創建:
// Lambda使用示例1
@Test
public void demo1() {
// no lambda
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread run...");
}
}).start();
// lambda
new Thread(() -> {System.out.println("thread run...");}).start();
}
集合的處理(排序):
// lambda使用示例2
@Test
public void demo2() {
List<String> strs = Arrays.asList("websphere", "nginx", "weblogic", "tomcat");
// no lambda
Collections.sort(strs, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
});
System.out.println(strs);
// lambda
Collections.sort(strs, (a, b) -> a.length() - b.length());
System.out.println(strs);
}
因此lambda表達式具有一些特點:
- 體現了函數式編程。
- 實現了參數類型的自動推斷。
- 代碼更加簡潔。
- 易於多核CPU下的並行處理等。
那麼對於學號Lambda表達式需要注意哪些呢?需要學習和了解Java泛型的使用,並注意多練習,否則很容易忘掉(實際上這篇文章就是因爲之前的很多東西都忘了,才進行補充的)
函數式接口
上面開始便提到了lambda表達式是針對函數式接口進行的操作過程,因此任何有函數式接口的地方就可以使用lambda表達式。而什麼是函數式接口呢?函數式接口是隻有一個抽象方法的接口。既然明確了是抽象方法,那麼其他的一些不屬於抽象方法的方法在接口中存在則不影響函數式接口的定義。例如如下:
@FunctionalInterface // 幫助我們註解一個函數式接口
public interface FunctionInterfaceDemo {
int func1();
// Object類中的方法也不算
public int hashCode();
// 默認方法
default int func2() {
return 1;
}
// 靜態方法
static int func3() {
return 1;
}
}
該接口中僅有func1方法爲真正的抽象方法,hashCode方法雖然是抽象方法,但其實際上是Object類中的方法,因此它在這裏並不算抽象方法。而另外的default方法和static方法也不是抽象方法,因此該接口符合函數式接口的定義。此外使用@FunctionalInterface註解可以幫助我們定義函數式接口,加入該註解後,如果接口中不滿足函數式接口,則會報錯。
常用的一些函數式接口
在jdk1.8之前其實也有一些常見的函數式接口,例如如下:
- Runnable
- Callable
- Comparator
jdk1.8則提供了一些有用的函數式接口(rt.jar包中7個重要的函數式接口):
- Supplier 代表一個輸出
- Consumer 代表一個輸入
- BiConsumer 代表兩個輸入
- Function 代表一個輸入,一個輸出(一般輸入和輸出時不同類型的)
- UnaryFunction 代表一個輸入,一個輸出(一般輸入和輸出是相同類型的)
- BiFunction 代表爲兩個輸入,一個輸出(一般輸入和輸出時不同類型的)
- BinaryOperator 代表兩個輸入,一個輸出(一般輸入和輸出是相同類型的)
Lambda表達式詳解
Lambda表達式實際上是對象,是一個函數式接口的實例,而不是方法。
lambda表達式語法
lambda表達式中主要包含兩個部分,一個是函數式接口中抽象方法的參數(args),另一個就是該抽象方法的執行體(body)其中包含該方法的返回值。也就是如下形式:
(函數式接口的args) -> {函數式接口中抽象方法的實現邏輯}
使用圖示如下:
注意:其中()裏面的參數個數由函數式接口中抽象方法的參數個數來決定,其中的參數類型會自動推斷,當然也可以指明。而當只有一個參數時,()可以被省略。當lambdabody中的實現代碼只有一行時,{}和return可以省略。例如如下所示的lambda表達式示例:
() -> {} // 無參,無返回值
() -> {System.out.println(1);}
() -> System.out.println(); // 省略{}
() -> {return 100;} // 無參,有返回值
() -> 100; // 無參,有返回值,省略{}和return
() -> null; // 無參,有返回值(返回null)
(int x) -> {return x + 1;} // 有參,有返回值
(x) -> x + 1; // 有參,有返回值,省略參數類型和{}
x -> x + 1; // 有參,有返回值,上面的簡寫
在對lambda表達式的使用過程中,有如下的幾點需要注意:
- lambda表達式會類型自動推斷,但不能部分省略參數類型,所有參數要麼全部指定類型,要麼全不指定。例如下面的方式是不允許的。
(x, int y) -> {x + y};
- 參數中不能使用final。例如下面的方式是不允許的。
(x, final y) -> {x + y};
- lambda表達式可以強轉爲一個函數式接口。例如下面的方式將lambda表達式強轉爲Supplier接口是可以實現的。
Object obj = (Supplier<?>) () -> "hello";
- 不能將lambda表達式賦值給一個非函數式接口。例如下面的方式將lambda表達式賦值給Object對象是不允許的。
Object o = () -> "hello";
- 不需要也不允許使用throws語句來聲明lambda表達式可能會拋出的異常。
lambda表達式實例
無參無返回值實例:
// 無參,無返回值實例
@Test
public void demo1() {
// 不使用lambda
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("run");
}
};
new Thread(r1).start();
// 使用lambda
Runnable r2 = () -> {System.out.println("run");};
// 可以直接省略大括號
Runnable r3 = () -> System.out.println("run");
}
無參有返回值實例:
// 無參,有返回值實例
@Test
public void demo2() throws Exception {
Callable c1 = new Callable() {
@Override
public Object call() throws Exception {
return "Hello";
}
};
c1.call();
Callable<String> c2 = () -> {return "Hello";};
c2.call();
// 邏輯很簡單時可以直接省略return
Callable<String> c3 = () -> "Hello";
c3.call();
}
有參無返回值實例,返回自定義的函數式接口:
// 有參,有返回值(使用自定義的FunctionInterface)
@Test
public void demo3() {
UserMapper u1 = new UserMapper() {
@Override
public void insert(User user) {
System.out.println("insert user: " + user);
}
};
// 聲明參數類型
UserMapper u2 = (User user) -> {System.out.println("insert user: " + user);};
// 省略參數類型
UserMapper u3 = user -> {System.out.println("insert user: " + user);};
u1.insert(new User());
u2.insert(new User());
u3.insert(new User());
}
// 函數式接口
interface UserMapper {
// 抽象方法
void insert(User user);
}
有參有返回值實例,返回自定義的函數式接口:
// 有參有返回值(使用自定義的FunctionInterface)
@Test
public void demo4() {
OrderMapper o1 = new OrderMapper() {
@Override
public int insert(Order order) {
System.out.println("insert order: " + order);
return 1;
}
};
OrderMapper o2 = (Order order) -> {return 1;};
OrderMapper o3 = (order) -> {return 1;};
OrderMapper o4 = (Order order) -> 1;
OrderMapper o5 = order -> 1;
o1.insert(new Order());
}
interface OrderMapper {
int insert(Order order);
}
具有較複雜方法體的函數式接口實例,這裏用到了上述提到的Function接口,其表示輸入一個值並輸出一個值。
// 方法體中邏輯較爲複雜的情況
@Test
public void demo5() {
// 輸入int,返回int
Function<Integer, Integer> f1 = a -> {
int sum = 0;
for (int i = 0; i <= a; i++) {
sum += i;
}
return sum;
};
System.out.println(f1.apply(10));
}
lambda表達式直接調用其他方法實現抽象方法實例:
當抽象方法無返回值(void)時,在lambda表達式中調用其他方法時(無論是否有返回值),可以直接調用而不用考慮被調用的方法是否有返回值。但如果抽象方法具有返回值(例如int),那麼在lambda表達式中實現方法體時,需要其返回值與抽象方法本身的返回值(int)相一致。
@Test
public void demo6() {
// lambda表達式的實現,執行後面指定的方法
Runnable r1 = () -> get();
Runnable r2 = () -> exec();
Runnable r4 = () -> find();
// Runnable r3 = () -> 100;
Foo f1 = () -> get(); // Foo函數式接口中的get方法有返回值,所以調用具有相同類型返回值的get方法可以
// Foo f2 = () -> exec(); // exec方法沒有返回值,而Foo接口中的get方法有返回值,出現異常
// Foo f3 = () -> find(); // find方法返回值類型與Foo接口中的方法返回值類型不一致
Foo f4 = () -> 100;
}
public int get() { return 1;}
public void exec() {}
public String find() { return " ";}
interface Foo {
int get();
}
使用BiFunction函數式接口實例。方法體的實現爲簡單的計算傳入的兩個String類型變量的長度和並返回。
@Test
public void demo7() {
// 兩個輸入(String,String),一個返回值(Integer)
BiFunction<String, String, Integer> bf = (a, b) -> {
return a.length() + b.length();
};
System.out.println(bf.apply("java", "se"));
}
Lambda表達式中方法的引用
什麼是lambda表達式中的方法的引用?下面先看一下如下的兩個lambda表達式:
@Test
void demo5() {
// 輸入一個字符串,將其轉換爲大寫字母
Function<String, String> fn = (str) -> str.toUpperCase();
System.out.println(fn.apply("admin"));
// Consumer表示一個輸入沒有輸出,下面實現輸出輸入的參數
Consumer<String> consumer = arg -> {System.out.println(arg);};
consumer.accept("hello");
}
上面兩個lambda表達式有如下的特點:
- 第一個lambda表達式,是直接使用了String對象的實例方法。
- 第二個lambda表達式,則是調用了另外的方法來實現抽象接口的方法。
因此在Lambda表達式中,方法引用是直接使用類或實例已經存在的方法(靜態方法、實例方法或構造方法)來實現抽象方法的一種方式。在這種方式中,函數式接口中的抽象方法需要恰好可以使用其他已存在的方法來進行調用實現,而不需要重新實現抽象方法,此時就可以(可能)使用方法引用的方式重新構造lambda表達式。方法應用主要具有如下四種類型:
方法引用類型 | 語法格式 | 對應的lambda表達式 |
---|---|---|
靜態方法引用 | 類名::staticMethod | (args) -> 類名.staticMethod(args) |
實例方法引用 | 對象實例::instanceMethod |
(args) -> new 對象實例.instanceMethod(args) |
對象方法引用 | 類名::instanceMethod | (instance, args) -> new 對象實例.instanceMethod(args) |
構造方法應用 | 類名::new | (args) -> new 類名(args) |
靜態方法引用
靜態方法引用:如果函數式接口的實現恰好可以通過調用一個靜態方法來實現,那麼就可以使用靜態方法引用。
- 語法:類名::staticMethod
可以參考下面的靜態方法引用實例:
public class ReferenceDemo {
@Test
void demo6() {
Supplier<String> s1 = () -> ReferenceDemo.get();
// 等價於上面
Supplier<String> s2 = ReferenceDemo::get;
Supplier<String> s3 = Fun::get;
System.out.println(s1.get());
System.out.println(s2.get());
System.out.println(s3.get());
// 有輸入參數
Consumer<Integer> c1 = (size) -> ReferenceDemo.consumer(size);
Consumer<Integer> c2 = ReferenceDemo::consumer; // 直接換成方法的引用
c1.accept(100);
c2.accept(100);
// 有輸入,也有輸出 (一個輸入,一個輸出)
Function<String, String> f1 = str -> str.toUpperCase(); // 原始的lambda表達式
Function<String, String> f2 = String::toUpperCase; // 調用String類中的靜態方法來進行實現
Function<String, String> f3 = ReferenceDemo::toUpperCase;// 調用自定義的靜態方法來實現
System.out.println(f1.apply("hello"));
System.out.println(f2.apply("hello"));
System.out.println(f3.apply("hello"));
// 兩個輸入,一個輸出
BiFunction<String, String, Integer> bf1 = (str1, str2) -> str1.length() + str2.length();
BiFunction<String, String, Integer> bf2 = ReferenceDemo::length;
System.out.println(bf1.apply("hello", "world"));
System.out.println(bf2.apply("hello", "world"));
}
static String get() { return "hello"; }
static void consumer(Integer size) { System.out.println("size: " + size);}
static String toUpperCase(String str) {return str.toUpperCase();}
static Integer length(String str1, String str2) { return str1.length() + str2.length();}
static class Fun {
public static String get() {
return "hello";
}
}
}
實例方法的引用
實例方法引用:如果函數式接口的實現恰好可以通過調用一個實例的實例方法來實現,那麼就可以使用實例方法引用。
- 語法:instance::instanceMethod
@Test
void demo7() {
Supplier<String> s1 = () -> new ReferenceDemo().put();
Supplier<String> s2 = () -> {return new ReferenceDemo().put();};
Supplier<String> s3 = new ReferenceDemo()::put;
System.out.println(s1.get());
System.out.println(s2.get());
System.out.println(s3.get());
// 沒有輸入,有輸出
Consumer<Integer> c1 = (size) -> new ReferenceDemo().con(size);
Consumer<Integer> c2 = (size) -> {new ReferenceDemo().con(size);};
Consumer<Integer> c3 = new ReferenceDemo()::con;
c1.accept(100);
c2.accept(100);
c3.accept(100);
// 其他的類似
Function<String, Integer> f1 = (str) -> this.toUpper(str); // this表示當前實例
Function<String, Integer> f2 = this::toUpper;
f1.apply("pika_pika");
f2.apply("hello_hello");
}
public String put() {return "hello";}
public void con(Integer size) {System.out.println("size:" + size);}
public Integer toUpper(String str) {return str.length();}
對象方法引用
相較於其他方法引用,對象方法引用是比較複雜的一個。對象方法引用:抽象方法的第一個參數類型剛好是實例方法的類型,抽象方法剩餘的參數恰好可以當做實例方法的參數。如果函數式接口的實現能由上述實例方法調用來實現,那麼就可以使用對象方法引用。
根據該表述,使用對象方法引用時具有如下3個使用條件:
- 抽象方法必需有參數。
- 抽象方法的第一個參數類型和實例方法的類型相同。
- 抽象方法的剩餘參數恰好可以當做實例方法的參數。
- 注意:抽象方法的第一個參數類型最好是自定義的。
語法:類名::instanceMethod(實際上調用的是指定對象的實例方法)。
對象方法引用的語法示意圖如下:
如果上面的形式可以被滿足,那麼該lambda表達式就可以使用如下的對象方法引用來表示:
函數式接口 接口實例 = InstanceClass::method;
根據上述的形式,第一個參數需要參與類型的匹配過程,因此必須存在。所以對於沒有參數的抽象方法無法使用對象方法引用,例如下面的示例:
@Test
void unusable() {
// 如下函數式接口的抽象方法中沒有輸入參數,因此不能使用對象方法引用
Runnable r = () -> {};
Closeable c = () -> {};
Supplier<String> s = () -> "";
}
可以使用對象方法引用的實例如下:
class Too {
public void foo() {
System.out.println("invoking foo function...");
}
}
class Too2 {
public void foo() {
System.out.println("invoking foo function...");
}
public void fo(String str) {
}
}
class Producer {
public Integer fun(String s) {
return 1;
}
public void run(String name, String size) {
}
}
@Test
void demo1() {
// Consumer函數接口中抽象方法沒有參數,而傳入的參數Too類型正好爲方法體中的new Too類型。
// 同時抽象方法中剩餘的參數(無參數)與Too.foo()方法剩餘的參數(無參數)相同,因此可以使用對象方法引用。
Consumer<Too> c1 = (Too too) -> new Too().foo();
Consumer<Too> c2 = Too::foo;
c1.accept(new Too());
c2.accept(new Too());
// 此時傳入的第一個參數類型爲Too,與後續使用new Too2類型不匹配,因此無法使用對象方法引用
Consumer<Too> c3 = (Too too) -> new Too2().foo();
// Consumer<Too> c4 = Too2::foo;
}
@Test
void demo2() {
BiConsumer<Too2, String> c1 = (too2, str) -> new Too2().fo(str);
BiConsumer<Too2, String> c2 = Too2::fo;
BiFunction<Producer, String, Integer> bf1 = (p, s) -> new Producer().fun(s);
BiFunction<Producer, String, Integer> bf2 = Producer::fun;
}
// 不能使用對方方法引用,因爲第一個參數不是自定義的
interface Execute1 {
public void run1(String name, String size);
}
interface Execute2 {
public void run2(Producer p, String name, String size);
}
@Test
void demo3() {
// 如果第一個參數類型不是自定義的,那麼只能使用已有類中的對象方法(從業務邏輯上考慮:這種情況有時無法滿足自己的業務邏輯需要)
Execute1 e1 = (name, size) -> name.equalsIgnoreCase(size);
Execute1 e2 = String::equalsIgnoreCase; // 這種寫法屬於實例方法引用,而不是對象方法引用
// 如果第一個參數類型是自定義的,那麼就可以使用自己的對象方法實現
Execute2 e3 = (p, name, size) -> new Producer().run(name, size);
Execute2 e4 = Producer::run;
}
構造方法引用
構造方法引用:如果函數式接口的實現恰好可以通過調用一個類的構造方法來實現,那麼就可以使用構造方法引用。
- 語法:類名::new
構造方法引用實例如下:
@Test
void demo4() {
Supplier<Person> s1 = () -> new Person();
Supplier<Person> s2 = Person::new;
s1.get();
s2.get();
// 已存在的類,只要有無參構造方法都可以使用
Supplier<List> listSupplier = ArrayList::new;
Supplier<Thread> threadSupplier = Thread::new;
Supplier<Set> setSupplier = HashSet::new;
Supplier<String> stringSupplier = String::new;
}
/**
* 構造方法有參數的情況
*/
@Test
void demo5() {
Consumer<Integer> c1 = (age) -> new Account(age);
Consumer<Integer> c2 = Account::new;
c1.accept(100);
c2.accept(100); // 會調用有參數的構造方法
// 這個不是構造方法引用
Function<String, Integer> fu1 = (str) -> Integer.valueOf(str);
Function<String, Integer> fu2 = Integer::valueOf;
Function<String, Account> fu3 = (str) -> new Account();
Function<String, Account> fu4 = Account::new; // 這樣的使用就需要Account類中有一個參數類型爲String的構造方法,否則不能使用
fu3.apply("test");
fu4.apply("admin");
}
class Person {
public Person() {
System.out.println("new Person()");
}
}
class Account {
public Account() {
System.out.println("Account()");
}
public Account(int age) {
System.out.println("Account(age)");
}
public Account(String name) {
System.out.println("Account(name)");
}
}
Stream API
由於篇幅原因,Stream API方面的內容另起一篇文章:https://blog.csdn.net/yitian_z/article/details/104680964。