JAVA8 Lambda表示式完全解析
JAVA8 新特性
在學習JAVA8 Lambda之前,必須先了解一下JAVA8中與Lambda相關的新特性,不然對於一些概念會感到比較陌生。
1、 介面的預設方法和靜態方法
Java 8允許我們給介面新增一個預設方法,用default
修飾即可。預設方法可以重寫。
public interface IMyInterface {
void onMethond(String str);//這是一個抽象方法
default String onDefalutMethod(){//這是一個預設方法
return "這是一個預設方法";
}
}
//重寫預設方法
public class MyImpl1 implements IMyInterface {
@Override
public void onMethond(String str) {
// TODO Auto-generated method stub
}
@Override
public String onDefalutMethod() {
return "重寫預設方法";
}
}
//不重寫預設方法
public class MyImpl2 implements IMyInterface {
@Override
public void onMethond(String str) {
// TODO Auto-generated method stub
}
}
此外Java 8還允許我們給介面新增一個靜態方法,用static
修飾即可。
public interface IMyInterface {
void onMethond(String str);//這是一個抽象方法
default String onDefalutMethod(){//這是一個預設方法
return "這是一個預設方法";
}
static String onStaticMethod(){
return "這是一個靜態方法";
}
}
2、 函式式介面(Functional Interface)
什麼叫函式式介面?他和普通介面有什麼區別?
“函式式介面”是指僅僅只包含一個抽象方法的介面(可以包含預設方法和靜態方法),其他特徵和普通介面沒有任何區別,Java中Runnalbe,Callable等就是個函式式介面;。我們可以給一個符合函式式介面新增@FunctionalInterface
註解,這樣就顯式的指明該介面是一個函式式介面,如果不是,編譯器會直接提示錯誤。當然你也可以不用新增此註解。新增的好處在於,由於Lambda表示式只支援函式式介面,如果恰好這個介面被應用於Lambda表示式,某天你手抖不小心添加了個抽象方法,編譯器會提示錯誤。
//顯式指明該介面是函式式介面
@FunctionalInterface
public interface IMyInterface {
void onMethond(String str);//這是一個抽象方法
default String onDefalutMethod(){//這是一個預設方法
return "這是一個預設方法";
}
static String onStaticMethod(){
return "這是一個靜態方法";
}
}
3、方法與建構函式引用
Java 8 允許你使用::
關鍵字來引用已有Java類或物件的方法或構造器。::
的誕生和Lambda一樣都是來簡化匿名內部類的寫法的,所以::
必須配合函式式介面一起用。使用::
操作符後,會返回一個函式式介面物件,這個介面可以自己定義,也可以直接使用系統提供的函式式介面,系統提供的後面會單獨介紹。
假如有個Person類如下,以下的例子都以這個Person類為基礎講解。
public class Person {
private String name;
private int age;
public Person(){
}
public Person(String name,int age){
this.name=name;
this.age=age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public static String show(String str){
System.out.println("----->輸入"+str);
return "----->返回"+str;
}
}
- 使用
::
關鍵字初始化建構函式,返回的是一個函式式介面,這個介面可以自己定義,也可以直接使用系統提供的函式式介面。現在先來定義一個函式式介面用於獲取Person例項。
@FunctionalInterface
public interface PersonSupply {
Person get();
}
接下來寫一個介面的實現,如果按照常規寫法,寫出來的程式碼一般會是如下形式。
PersonSupply sp=new PersonSupply{
public Person get(){
Person person=new Person();
return person;
}
}
Person person=sp.get();//獲取例項
而自從Java8引入::
關鍵字後,就可以簡化為一行程式碼,簡直不要太簡潔。
PersonSupply sp=Person::new; //介面實現
Person person=sp.get();//獲取例項
當然為了使這個介面更通用,我們可以定義成如下形式
@FunctionalInterface
public interface PersonSupply<T> {
T get();
}
PersonSupply<Person> sp=Person::new;
Person person=sp.get();
java8自帶的Supplier
介面就是這樣實現的,後面會做介紹。直接使用Supplier
如下
Supplier<Person> sp=Person::new;
Person person=sp.get();
這種簡便寫法同樣支援帶參建構函式,首先寫一個函式式介面,由於需要引數所以get()裡面輸入引數,返回Person,如下。
@FunctionalInterface
public interface PersonSupply<P extends Person> {
P get(String name, int age);
}
常規寫法如下:
PersonSupply<Person> sp=new PersonSupply<Person>{
public Person get(String name, int age);{
Person person=new Person(name,age);
return person;
}
}
Person person=sp.get("maplejaw",20);
簡便寫法如下:
PersonSupply<Person> sp=Person::new;
Person person=sp.get("maplejaw",20);
- 使用
::
關鍵字引用靜態方法
同樣,寫一個函式式介面,該靜態方法需要引數,所以get()裡傳入引數,需要返回引數,所以返回String。
@FunctionalInterface
public interface PersonFactory {
String get(String str);
}
PersonFactory pf=Person::show;
pf.get("哈哈哈");
PersonFactory pf=Person::show;
等價於下面
PersonFactory pf=new PersonFactory{
public String get(String str){
return Person.show(str);
}
}
- 使用
::
關鍵字引用普通方法比較特殊。
如果要呼叫的方法沒有引數,可以用Class::method
形式呼叫,但是這時需要傳入一個Person例項,這時函式式介面這樣寫,在get()中傳入Person類的例項,返回String。
@FunctionalInterface
public interface PersonFactory {
String get(Person person);
}
PersonSupply<Person> sp=Person::new;
Person person=sp.get("maplejaw", 20);
PersonFactory pf=Person::getName;
System.out.println("--->"+pf.get(person));
PersonFactory pf=Person::getName;
等價於下面
PersonFactory pf=new PersonFactory{
public String get(Person person){
return person.getName();
}
}
也可以以instance::method
形式呼叫。這時get()不需要傳參。返回String。
@FunctionalInterface
public interface PersonFactory {
String get();
}
PersonSupply<Person> sp=Person::new;
Person person=sp.get("maplejaw", 20);
PersonFactory pf=person::getName;
System.out.println("--->"+pf.get());
PersonFactory pf=person::getName;
等價於下面
Person person=new Person("maplejaw",20);
PersonFactory pf=new PersonFactory{
public String get( ){
return person.getName();
}
}
如果要呼叫的方法有引數,必須用instance::method
形式呼叫,這時函式式介面這樣寫,set傳入引數。
@FunctionalInterface
public interface PersonFactory {
void set(String name);
}
PersonSupply<Person> sp=Person::new;
Person person=sp.get("maplejaw", 20);
PersonFactory pf=person::setName;
pf.set("maplejaw");
PersonFactory pf=person::setName;
等價於下面
Person person=new Person("maplejaw",20);
PersonFactory pf=new PersonFactory{
public void set(String name){
return person.setName(name);
}
}
4、JAVA8 API內建的函式式介面
還記得前面提到的Supplier
函式式介面嗎,它就是API內建的函式式介面之一,下面將介紹一些常用的內建函式式介面。
- Supplier
Supplier 提供者,不接受引數,有返回值
Supplier<Person> Supplier=new Supplier<Person>() {
@Override
public Person get() {
return new Person();
}
};
Supplier<Person> sp=Person::new;
- Function 一個引數,一個返回值。常用於資料處理。
Function<String, Integer> function=new Function<String, Integer>(){
@Override
public Integer apply(String s) {
return Integer.parseInt(s);
}
} ;
Function<String, Integer> function=Integer::parseInt;
- Consumer 消費者,只有一個引數,沒有返回值
Consumer<String> consumer=new Consumer<String>() {
@Override
public void accept(String t) {
}
};
- Comparator 比較類
Comparator<Integer> comparator=new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
// TODO Auto-generated method stub
return o1-o2;
}
};
- Predicate
Predicate 介面,抽象方法只有一個引數,返回boolean型別。該介面包含多種預設方法來將Predicate組合成其他複雜的邏輯(比如:與,或,非):
Predicate<String> predicate=new Predicate<String>() {
@Override
public boolean test(String t) {
return t.startsWith("h");
}
};
boolean b=predicate.test("hahaha");//判斷是否符合條件
Predicate<String> predicate = String::isEmpty;
boolean b=predicate.test("hahaha");//判斷是否符合條件
- UnaryOperator 接收一個引數,返回一個引數,且引數型別相同
UnaryOperator<String> unaryOperator=new UnaryOperator<String>() {
@Override
public String apply(String s) {
// TODO Auto-generated method stub
return s;
}
};
- BinaryOperator 接收兩個引數,返回一個引數,且引數型別相同
BinaryOperator<String> binaryOperator=new BinaryOperator<String>() {
@Override
public String apply(String t, String u) {
// TODO Auto-generated method stub
return t+u;
}
};
5、三個API
- Optional
Optional 這是個用來防止NullPointerException異常而引入的。Optional提供很多有用的方法,這樣我們就不用顯式進行空值檢測。
Optional<String> optional = Optional.of("給你一個值");
optional.isPresent(); //判斷是否為空值
optional.get(); //獲取值 ,如果空值直接拋異常。
optional.orElse("返回空值"); //獲取值 ,如果空值返回指定的值。
Stream(流)
最新新增的Stream API(java.util.stream)把真正的函數語言程式設計風格引入到Java中。這是目前為止對Java類庫最好的補充,因為Stream API可以極大提供Java程式設計師的生產力,讓程式設計師寫出高效率、乾淨、簡潔的程式碼。
接觸過RxJava的可能對下面的程式碼風格比較眼熟,Stream 的建立需要指定一個數據源,比如 java.util.Collection的子類,List或者Set, Map不支援。Stream的操作可以序列執行或者並行執行。
怎麼用Stream?Stream必須有資料來源。那就給一個數據源。List<String> list = new ArrayList<>(); list.add("ddd2"); list.add("aaa2"); list.add("bbb1"); list.add("aaa1"); list.add("aaa3"); list.add("bbb3"); list.add("ccc"); list.add("bbb2"); list.add("ddd1");
forEach list新增的for迴圈方法,forEach是一個最終操作(只能放在最後)
//遍歷列印資料 list.forEach(new Consumer<String>() { @Override public void accept(String t) { System.out.println("---->"+t); } });
列印結果如下
---->ddd2 ---->aaa2 ---->bbb1 ---->aaa1 ---->aaa3 ---->bbb3 ---->ccc ---->bbb2 ---->ddd1
filter 過濾
list.stream() .filter(new Predicate<String>() { @Override public boolean test(String t) { return t.startsWith("a"); } }) .forEach(new Consumer<String>() { @Override public void accept(String t) { System.out.println("---->"+t); } });
列印結果如下
---->aaa2 ---->aaa1 ---->aaa3
Sort 排序
list.stream() .sorted()//排序,如果不實現Comparator介面,則按預設規則排序 .filter(new Predicate<String>() { @Override public boolean test(String t) { return t.startsWith("a"); } }) .forEach(new Consumer<String>() { @Override public void accept(String t) { System.out.println("---->"+t); } }); list.stream() .sorted(new Comparator<String>() { @Override public int compare(String o1, String o2) { // TODO Auto-generated method stub return o1.compareTo(o2); } }) .filter(new Predicate<String>() { @Override public boolean test(String t) { return t.startsWith("a"); } }) .forEach(new Consumer<String>() { @Override public void accept(String t) { System.out.println("---->"+t); } });
map
list.stream() .sorted(new Comparator<String>() { @Override public int compare(String o1, String o2) { // TODO Auto-generated method stub return o1.compareTo(o2); } }) .filter(new Predicate<String>() { @Override public boolean test(String t) { return t.startsWith("a"); } }) .map(new Function<String, String>() { @Override public String apply(String t) { // TODO Auto-generated method stub return t+"--->被我處理過了"; } }) .forEach(new Consumer<String>() { @Override public void accept(String t) { System.out.println("---->"+t); } });
---->aaa1--->被我處理過了 ---->aaa2--->被我處理過了 ---->aaa3--->被我處理過了
Match 匹配,是一個最終操作
boolean b= list.stream() .anyMatch(new Predicate<String>() { @Override public boolean test(String t) { // TODO Auto-generated method stub return t.startsWith("a"); } }); System.out.println("----->"+b);//true boolean b= list.stream() .allMatch(new Predicate<String>() { @Override public boolean test(String t) { // TODO Auto-generated method stub return t.startsWith("a"); } }); System.out.println("----->"+b);//false
Count 計數,是一個最終操作
long count = list.stream() .filter(new Predicate<String>() { @Override public boolean test(String t) { return t.startsWith("a"); } }) .count(); System.out.println(count); // 3
Reduce 規約,最終操作
Optional<String> optional = list .stream() .sorted() .reduce(new BinaryOperator<String>() { @Override public String apply(String t, String u) { return t+u; } }); System.out.println("---->"+optional.get()); //---->aaa1aaa2aaa3bbb1bbb2bbb3cccddd1ddd2
findFirst 提取第一個,最終操作
Optional<String> optional = list .stream() .sorted() .findFirst(); System.out.println("---->"+optional.get()); //aaa1
parallelStream(並行流)
並行化之後和之前的程式碼區別並不大。並且並行操作下,速度會比序列快。但是需要注意的是不用在並行流下排序,並行流做不到排序。
list.parallelStream()
.filter(new Predicate<String>() {
@Override
public boolean test(String t) {
// TODO Auto-generated method stub
return t.startsWith("a");
}
})
.forEach(new Consumer<String>() {
@Override
public void accept(String t) {
// TODO Auto-generated method stub
System.out.println("--->"+t);
}
});
list.parallelStream()
.sorted()
.forEach(new Consumer<String>() {
@Override
public void accept(String t) {
// TODO Auto-generated method stub
System.out.println("--->"+t);
}
});
//打印出來的並沒有排序
Lambda 表示式
在上面你是不是覺得::
有時候挺好用的?可以不用再new介面,再也不用寫煩人的匿名內部類了,比如
Supplier<Person> sp=Person::new;
但是::
的使用場景還是比較有限的。Lambda表示式的誕生就是為了解決匿名內部類中飽受詬病的問題的。
什麼是Lambda表示式
Lambda表示式是Java8的一個新特性,它提供了一種更加清晰和簡明的方式使用函式式介面(以前被叫作單一方法介面)。使用Lambda表示式能夠更加方便和簡單的使用匿名內部類,比如對於集合的遍歷、比較、過濾等等。Lambda表示式格式
(type1 arg1, type2 arg2…) -> { body }
每個lambda都包括以下三個部分:
引數列表:(type1 arg1, type2 arg2…)
箭頭: ->
方法體:{ body }方法體既可以是一個表示式,也可以是一個語句塊:
- 表示式:表示式會被執行然後返回執行結果。
- 語句塊:語句塊中的語句會被依次執行,就像方法中的語句一樣,return語句會把控制權交給匿名方法的呼叫者。
表示式函式體適合小型lambda表示式,它消除了return關鍵字,使得語法更加簡潔。
以下是一些例子
(int a, int b) -> { return a + b; }
( a, b) -> { return a + b; }
( a, b) -> a+b
() -> System.out.println("s")
(String s) -> { System.out.println(s); }
- 一個 Lambda 表示式可以有零個或多個引數
- 引數的型別既可以明確宣告,也可以根據上下文來推斷。例如:(int a)與(a)效果相同
- 所有引數需包含在圓括號內,引數之間用逗號相隔。例如:(a, b) 或 (int a, int b)
- 空圓括號代表引數集為空。例如:()->{System.out.println(“s”);};
- 當只有一個引數,且其型別可推導時,圓括號()可省略。
- 如果 Lambda 表示式的主體只有一條語句,花括號{}可省略,且不能以分號結尾
- 如果 Lambda 表示式的主體包含一條以上語句,則表示式必須包含在花括號{}中,每條語句必須以分號結尾。
Lambda表示式可以用來簡化內部類寫法,比如
//常規程式碼
Runnable runable=new Runnable() {
@Override
public void run() {
System.out.println("--->");
}
};
//Lambda表示式
Runnable runable=()->{System.out.println("--->");};
//啟動一個執行緒
new Thread(()->{System.out.println("--->");}).start();
還記得前面介紹的Predicate介面嗎?現在我們再來用Lambda表示式寫一遍。
//一般寫法
Predicate<String> predicate=new Predicate<String>() {
@Override
public boolean test(String t) {
return t.startsWith("h");
}
};
//Lambda寫法
Predicate<String> predicate=(String s)->{s.startsWith("h");};
你現在是不是覺得Lambda表示式太神奇了?居然可以寫出這麼簡潔的程式碼。
還記得內部類使用區域性變數時需要把變數宣告為final嗎,Lambda表示式則不需要。不過雖然不用宣告final,但也不允許改變值。
String s="sss";
new Thread(()->{
System.out.println(s);
}).start();
此外,內部類引用外部類也不用使用MainActivity.this,這種操蛋的程式碼了。
在Lambda表示式中this,指的就是外部類,因為根本就沒有內部類的概念啊。
btn.setOnClickListener(()->{
Toast.makeText(this,"xxx",Toast.LENGTH_SHORT).show();
});
現在回過頭來把前面Stream中的程式碼用Lambda表示式再寫一遍。
list.stream()
.sorted((s1,s2)->s1.compareTo(s2))
.filter((s)->s.startsWith("a"))
.map((s)->s+"被我處理過了")
.forEach(s->System.out.println(s));
程式碼簡潔的簡直讓人窒息。但是能不能更簡潔一點呢?當然是可以的,首先我們檢查一下哪裡可以替換成::
關鍵字,然後作如下替換,是不是更簡潔了。關於替換規則,請看前面的介紹。
list.stream()
.sorted((s1,s2)->s1.compareTo(s2))
.filter((s)->s.startsWith("a"))
.map((s)->s+"被我處理過了")
.forEach(System.out::println);
列印結果如下
aaa1被我處理過了
aaa2被我處理過了
aaa3被我處理過了
- 顯示指定目標型別
如果Lambda表示式的目標型別是可推導的,就不用指定其型別,如果Lambda表示式的引數型別是可以推導,就不用指定引數型別。因為編譯器可以根據上下文自動推匯出其型別,然後進行隱式轉換。但是,有些場景編譯器是沒法推導的。比如下面這樣的,如果不顯示指定型別,編譯器就會提示錯誤
//介面一
public interface IMyInterface1 {
void opreate(String str);
}
//介面二
public interface IMyInterface2 {
void opreate(int i);
}
//Person類
public class Person類 {
private String name;
private int age;
public Test(String name, int age) {
this.name = name;
this.age = age;
}
public void opreate(IMyInterface1 inter){
inter.opreate(name);
}
public void opreate(IMyInterface2 inter){
inter.opreate(age);
}
}
//這樣寫是錯誤的,因為編譯器無法推匯出其目標型別
new Test("maplejaw",20).opreate((name)->System.out.println(name));
解決辦法有兩個
一、指定引數型別,但是如果兩個介面的引數型別是一樣的,就只能顯示指定目標型別。
new Test("maplejaw",20).opreate((String name)->System.out.println(name));
二、指定目標型別
new Test("maplejaw",20).opreate((IMyInterface1) (s)->System.out.println(s));
new Test("maplejaw",20).opreate((IMyInterface1) System.out::println);
由於目標型別必須是函式式介面,所以如果想賦值給Object物件時,也必須顯示轉型。
Object runnable=(Runnable)()->{System.out.print("--->");};
關於Lambda表示式的介紹到此為止,想更深入瞭解推薦【深入理解Java 8 Lambda】這篇文章。
最後
當初學習Lambda表示式的時候,由於網上的資料比較零散,且直接用了JAVA8的新API來做演示,由於對新API不是很熟導致學習的時候走了一些彎路,看得一頭霧水。所以決定把我的學習路線給記錄下來,或許可以幫助部分人。
Lambda表示式是把雙刃劍,讓程式碼簡潔的同時,降低了程式碼的可讀性。但是作為程式設計師,追逐新技術的腳步不能停下。