1. 程式人生 > >Java Lambda表示式及方法引用

Java Lambda表示式及方法引用

Lambda

Lambda表示式是Java SE 8中一個重要的新特性。允許你通過表示式來代替功能介面,其幾乎解決了匿名內部類帶來的所有問題。

其實Lambda表示式的本質是一個”語法糖”,由編譯器推斷並幫你轉換包裝為常規的程式碼,因此你可以使用更少的程式碼來實現同樣的功能。

語法糖(Syntactic sugar),也譯為糖衣語法,是由英國電腦科學家彼得·蘭丁發明的一個術語,指計算機語言中新增的某種語法,這種語法對語言的功能並沒有影響,但是更方便程式設計師使用。語法糖讓程式更加簡潔,有更高的可讀性。

匿名內部類的問題

java閉包中的匿名內部類使用的非常廣泛。匿名內部類使用的最常見的場景就是事件處理器了。其次匿名內部類還常被用在多執行緒的程式中,我們通常寫匿名內部類,而不是建立 Runnable/Callable 介面的實現類。

雖然匿名類到處都在使用,但是他們還是有很多問題。

  1. 第一個主要問題是複雜。這些類讓程式碼的層級看起來很亂很複雜,也稱作 Vertical Problem 。
  2. 第二,他們不能訪問封裝類的非 final 成員,this 這個關鍵字將變得很有迷惑性。

如果一個匿名類有一個與其封裝類相同的成員名稱,內部變數將會覆蓋外部的成員變數,在這種情況下,外部的成員在匿名類內部將是不可見的,甚至不能通過 this 關鍵字來訪問。因為 this 關鍵字值得是匿名類物件本身而不是他的封裝類的物件。例子如下:

public void anonymousExample() {
    String nonFinalVariable =
"Non Final Example"; String variable = "Outer Method Variable"; new Thread(new Runnable() { String variable = "Runnable Class Member"; public void run() { String variable = "Run Method Variable"; //下面註釋句子編譯會出錯 //System.out.println("->" + nonFinalVariable);
System.out.println("->" + variable); System.out.println("->" + this.variable); } }).start(); } //輸出 ->Run Method Variable ->Runnable Class Member

Functional Interfaces

在我們進一步探討 lambda 表示式之前,讓我們來看一看 Functional Interfaces。
簡單來說,Functional Interfaces 是一個只有單個抽象方法的介面。(但是可以有其他方法,例如有其他繼承Object而來的方法)。

大多數回撥介面都是 Functional Interfaces。例如 Runnable,Callable,Comparator 等等。以前被稱作 SAM(Single Abstract Method)

官方原文如下:
(The‘Single’method can exist in the form of multiple abstract methods that are inherited from superinterfaces. But in that case the inherited methods should logically represent a single method or it might redundantly declare a public method that is provided by classes like Object, e.g. toString.)

interface Runnable { void run(); }
// 1.Functional
interface Foo { boolean equals(Object obj); }
//2. Not functional; equals是Object類的方法
interface Bar extends Foo {int compare(String o1, String o2); }
//3. Functional; Bar有一個抽象的不是object類的方法
interface Comparator {
 boolean equals(Object obj);
 int compare(T o1, T o2);
}
//4. Functional; Comparator有一個抽象的不是object類的方法,且equals是object的方法
interface Foo {int m();   Object clone(); }
//5. Not functional; 方法Object.clone並不是public的
interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
//6. Functional: 雖然有兩個方法,但是這兩個方法的宣告是完全一樣的,在Z中只當做一個方法

PS:Object中的clone()是protected的,而介面中的方法只能是public,所以在介面中宣告Object clone();會被當做一個新的方法,在上面的第4個介面便包含了兩個自己宣告的方法,所以不是functional interface。

Lambda表示式

lambda表示式允許你通過表示式來代替上面的Functonal介面。 lambda表示式就和方法一樣,它提供了一個正常的引數列表和一個使用這些引數的主體(body,可以是一個表示式或一個程式碼塊)。

基本語法:
(parameters) -> expression 或 (parameters) ->{ statements; }
例如 :
(String s)-> s.lengh;
() ->{ return true; }
(int x, int y) -> { x + y; }

我們上邊說過,匿名類的一個主要問題是是程式碼的層級看起來很亂,也就是 Vertical Problem 了,Lamdba 表示式實際上就是匿名類,只不過他們的結構更輕量,更短。Lambda 表示式看起來像方法。他們有一個正式的引數列表和這些引數的塊體表達。

public class FirstLambdaExpression {
    public String variable = "Class Level Variable";
    public static void main(String[] arg) {
        new FirstLambdaExpression().anonymousExample();
    }
    public void anonymousExample() {
        String nonFinalVariable = "Non Final Example";
        String variable = "Outer Method Variable";
        new Thread(() -> {
            //下面註釋句子編譯會出錯
            //String variable = "Run Method Variable"
            System.out.println("->" + variable);
            System.out.println("->" + this.variable);
        }).start();

    }
}

//輸出
->Outer Method Variable
->Class Level Variable

你可以比較一些使用 Lambda 表示式和使用匿名內部類的區別。我們可以清楚的說,使用 Lambda 表示式的方式寫匿名類解決了變數可見性的問題。Lambda 表示式不允許建立覆蓋變數。

通常的 Lambda 表示式的語法包括一個引數列表,箭頭關鍵字”->”最後是主體。主體可以是表示式(單行語句)也可以是多行語句塊。如果是表示式,將被計算後返回,如果是多行的語句塊,就看起來跟方法的語句塊很相似了,可以使用 return 來指定返回值。

為什麼選擇這個特殊的語法形式呢,因為目前 C# 和 Scala 中通常都是這種樣式,也算是 Lambda 表示式的通用寫法。這樣的語法設計基本上解決了匿名類的複雜性。但是與此同時他也是非常靈活的,例如,如果方法體是單個表示式,大括號和 return 語句都是不需要的。表示式的結果就是作為他自己的返回值。這種靈活性可以保持程式碼簡潔。

lambda例子

接下來我們再看看一些lambda的例子:

//首先定義一個人物列表players
String[] atp = {"Rafael Nadal", "Novak Djokovic",
                "David Ferrer", "Roger Federer",
                "Andy Murray", "Tomas Berdych"};
List<String> players = Arrays.asList(atp);
//***要迴圈逐個輸出列表中的名字***//
// 1、可以使用List的foreach函式,裡面的引數是Comsunmer型別,foreach有一個迴圈會逐個呼叫Comsumer的accept方法。可以可以自己去看原始碼得知。
players.forEach(new Consumer<String>() {
    @Override
    public void accept(String player) {
        System.out.print(player + "; ");
    }
});

//2、 因為Comsuer是一個Fuctional Interface,所以可以使用 lambda 表示式呼叫,下面式子功能跟上面完全一致。
players.forEach((player) -> System.out.print(player + "; "));

//3、Comsunmer也可以單獨使用lambda表示式新建例項再傳進foreach函式
Consumer<String> consumer = (player) -> System.out.print(player + "; ");
players.forEach(consumer);
//***根據 name首字母 排序 players***//
//1、使用Arrays的sort方法排序,有個Comparator型別的引數作為排序規則。
Arrays.sort(players, new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return (s1.compareTo(s2));
    }
});

//2、同樣的Compartor也是一個Functional Interface,所以可以使用Lambda代替
Arrays.sort(players, (String s1, String s2) -> (s1.compareTo(s2)));

//3、同樣的Compartor可以單獨使用lambda表示式新建例項傳進sort函式
Comparator compatator = (String s1, String s2) -> (s1.compareTo(s2));
Arrays.sort(players, compatator);

仔細看看 lambda 表示式,目標介面型別不在表示式中完全沒有宣告到。

一個很明顯的問題來了,為什麼 Lambda 表示式不需要一個指定的方法名呢?

Lambda 表示式只能用於 functional interface ,而 functional interface 只有一個方法。
而且編譯器會根據所需型別、引數個數、函式返回型別幫助推斷 lambda 表示式的型別。

下面例子有點繁雜,大家要耐心看:

//下面定義兩個functional interface
interface intTypeInterface { int test(); }
interface StringTypeInterface { String test(); }
interface paramInterface { String test(String param); }

class Test {
    //兩個同名不同引數不同返回型別的函式
    //函式返回int型別的介面
    void invoke(intTypeInterface a) { System.out.println("intType"); }
    //函式返回String型別的介面
    void invoke(StringTypeInterface b)  { System.out.println("StringType");}
    //函式返回String型別,但是有兩個引數的介面
    void invoke(paramInterface b)  { System.out.println("param");}


    //***如果呼叫invoke函式會呼叫哪一個?***//

    public static void main(String[] args) {
        Test test = new Test();

        //根據 1 返回型別得知應例項化intTypeInterface介面
        test.invoke( () -> 1 );
        //根據 "String" 返回型別得知應例項化StringTypeInterface介面
        test.invoke( () -> "String" );
        //根據函式引數得知應例項化paramInterface介面
        test.invoke( (String s) -> "String" );
}

可以看出,編譯器是根據invoke裡面的引數個數和型別去呼叫符合的invoke函式,而invoke函式裡面的引數是什麼型別的是根據lambda匹配到了什麼而確定。
很明顯,這裡的lambda表示式是根據引數個數,函式返回類型進行匹配確定需要例項化哪個介面。

List list = Arrays.asList(
            (Callable) ()->"callable 1",
            (Callable) ()->"callable 2",
            (Callable) ()->"callable 3");

Lambda表示式可以顯式的轉換為指定的目標型別,只要跟所需的型別相容。看一下下面的程式,我實現了三種Callable,而且都將其轉換為Callable型別。

匹配總結:

Lambda 表示式必須有一個目標型別,而他們可以適配任意可能的目標型別。當目標型別是一個介面的時候,下面的條件必須滿足,才能編譯正確:

  1. 介面應該是一個 functional interface
  2. 表示式的型別和引數數量必須與 functional interface 中宣告的一致
  3. 返回值型別必須相容 functional interface 中方法的返回值型別
  4. 丟擲的異常表示式必須相容 functional interface 中方法的丟擲異常宣告

PS :
一、由於編譯器可以通過目標型別的宣告中得知引數型別和個數,所以在 Lambda 表示式中,可以省略引數型別宣告。

Comparator c = (s1, s2) -> s1.compareToIgnoreCase(s2);

二、如果目標型別中宣告的方法只接收一個引數(很多時候都是這樣的),那麼引數的小括號也是可以不寫的,例如:

players.forEach(player -> System.out.print(player + "; "));

方法引用

Lambda 表示式允許我們定義一個匿名的方法,並將它作為 Functional interface 的一個例項。方法引用跟 Lambda 表示式很像,他們都需要一個目標型別,但是不同的是方法引用不提供方法的實現,他們引用一個已經存在的類或者物件的方法。
類似如下結構:

System::getProperty
String::length
Integer::compareTo
ArrayList::new

上面的語句展示了方法和建構函式的引用的通用語法。::符號叫做雙冒號操作符(double colon operator),用來表示對某個類或物件的某個方法的引用。

class Dog {
  //定義了一個類方法speak
    static void speak() {
        System.out.println("我就是一條狗");
    }
    public static void main(String[] args) {
    //用一個執行緒去執行Dog的speak方法
    new Thread(new Runnable() {
    @Override
    public void run() {
     Dog.speak();
    }
    });
    //1、使用lambda表示式
    new Thread(() -> Dog.speak()).start();
    //2、使用方法引用
    new Thread(Dog::speak()).start;
    }
}

new Thread()需要一個Runnable物件,我們用lambda表示式() -> Dog.speak() 例項化了Runnable物件。
而這裡方法引用只用了一個Dog::speak(),為什麼可以這樣呢?同樣的原因,方法引也是用在Functional Interface裡面,其只有一個方法,所以我們可以連方法的結構都不寫直接讓編譯器去識別例項化需要型別的物件。

//前面例子我們用sort函式和Comparator對players按照名字進行排序
Arrays.sort(players, new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return (s1.compareTo(s2));
    }
});

//這裡我們建立一個類包含了一個比較函式
class Util {
    public static int mycompare(String s1, String s2) {
        return (s1.compareTo(s2));
    }
}

//我們用自己寫的mycompare函式排序
Arrays.sort(players, new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return Util.mycompare(s1, s2);
    }
});

//**這裡便可以用方法引用的方式,功能同上**//
Arrays.sort(players, Util::mycompare);

其實上面的

sort 方法的第二個引數是 Comparator 型別的,但是我們卻傳遞了 Util 的一個靜態方法引用。重要的問題就在這了,我既沒有讓 Util 實現 Comparable 介面,也沒有寫一個獨立的 Comparator 類,但是輸出確實沒有任何問題。

讓我們來看一看這是為什麼。

Arrays.sort 方法期望一個 Comparator 的例項,而這個 Comparator 是一個 functional interface ,這就意味著他只有一個方法 compare 了。compare方法有兩個都是String型別的引數,而且返回型別是int。而這裡與Util的mycompare函式引數的型別、數量,返回值都是相同的,只有他們的名字是不一樣的。這時候我們就可以建立一個方法引用,並將它傳遞給 sort 作為第二個引數。

當有多個相同的名稱的方法的時候,編譯器會根據目標型別選擇最佳的匹配。為了搞明白,來看一個例子:

class MyComparator {
    //同樣的名字,不同引數
    public static int myCompare(String s1, String s2) {
         return s1.compareTo(s2);
    }
    public static int myCompare(Integer int1, Integer int2) {
         return int1 - int2;
    }
}
//建立兩個陣列
String[] strings= {"Rafael Nadal", "Novak Djokovic", "Roger Federer"};
Integer[] ints = {1 , 4, 8, 2, 3, 8, 6};

//使用myComparator的函式排序
MyComparator comparator = new MyComparator();
Arrays.sort(strings, comparator::myCompare);
Arrays.sort(ints, comparator::myCompare);

這裡,兩行程式碼中的方法引用宣告都是相同的(comparator::myCompare),唯一不同的是我們傳入的陣列,這裡編譯器會幫助我們檢查第一個引數,並且智慧的找到合適的引用方法。

所以我們知道對於方法引用,匹配的是函式引數的型別、數量,返回值,程式會進行匹配以呼叫合適的方法。

方法應用 :
靜態方法 : 類名::方法名
例項方法 : 物件::方法名

上面ints陣列的排序也可以寫成 :

Arrays.sort(ints, Integer::compareTo);

是不是很他媽神奇,明明Integer中的compareTo()函式不是static的,我們卻直接使用類名引用了。

答案是:這種型別的語句允許使用在一些特定的型別中。Integer和String是某種資料型別,而對於資料型別來說,這種語句是允許的。

如果我們將 Util 的方法 myCompare 變成非靜態的,然後這樣使用:Util::myCompare,就會出編譯錯誤:No Suitable Method Found。

構造方法引用

構造方法引用是 JavaSE 8 的一個新的特性。我們可以構造一個構造方法的引用,並且將它作為引數傳遞給目標型別。

當我們使用方法引用的時候,我們引用一個已有的方法使用他們。同樣的,在使用構造方法引用的時候,我們建立一個已有的構造方法的引用。

結構是 : 構造方法引用的語法類名::new,這看起來很像方法引用。這種構造方法的引用可以分配給目標 functional interface 的例項。一個類可能有多個構造方法,在這種情況下,編譯器會檢查 functional interfaces 的型別,最終找到最好的匹配。

public class ConstructorReference {
    public static void main(String[] ar){
        //會把 Myclass的 例項傳遞給 MyInterface 的例項
        MyInterface in = MyClass::new;
        System.out.println("->"+in.getMeMyObject());
    }
}
//Functional Interface
interface MyInterface{
    MyClass getMeMyObject();
}
class MyClass{
    MyClass(){}
}

//輸出 [email protected]

那怎樣例項化一個帶引數的構造方法引用?

public class ConstructorReference {
    public static void main(String[] ar){
        EmlpoyeeProvider provider = Employee::new;
        Employee emp = provider.getMeEmployee("John", 30);
        System.out.println("->Employee Name: "+emp.name);
        System.out.println("->Employee Age: "+emp.age);
   }
}
interface EmlpoyeeProvider{
    Employee getMeEmployee(String s, Integer i);
}
class Employee{
    String name;
    Integer age;
    Employee (String name, Integer age){
        this.name = name;
        this.age = age;
    }
}

//輸出:
->Employee Name: John
->Employee Age: 30

相關推薦

Java Lambda表示式方法引用

Lambda Lambda表示式是Java SE 8中一個重要的新特性。允許你通過表示式來代替功能介面,其幾乎解決了匿名內部類帶來的所有問題。 其實Lambda表示式的本質是一個”語法糖”,由編譯器推斷並幫你轉換包裝為常規的程式碼,因此你可以使用更少的

Java——Lambda表示式方法引用內建函式式介面

1.Lambda表示式 面向物件的基礎上支援函數語言程式設計 1.1 約束: 介面有且僅有一個抽象方法,如果存在兩個抽象方法,則無法使用函數語言程式設計。 介面有且僅有一個抽象方法,且想要轉化為lambda表示式,加註解 @FunctionalInterface

java8的lambda表示式方法引用(一)

當前很多公司的java開發環境都升級到jdk8以上了。lambda表示式是java8中最重要的更新,其目的是為了配合隨著並行運算流行起來的所謂“函式式”程式設計改進而來的語法糖。既然是語法糖,那麼其實不用這些lambda表示式也是可以實現原有功能的,只不過看起來程式碼行數多一

lambda表示式方法引用----java

1.概念 ---- 什麼是方法引用???     對於每一個java類來說,它們都主要有三種方法,即普通方法、靜態方法和構造方法。而方法引用就是利用函式式介面+lambda表示式(這裡的lambda表示式並非前面提到的帶"->“符號的表示式,而是使用雙冒號”::

Java 8 新特性:Lambda 表示式方法引用Lambda 表示式補充版)

方法引用 文 | 莫若吻      (注:此文乃個人查詢資料然後學習總結的,若有不對的地方,請大家指出,非常感謝!) 1.方法引用簡述 方法引用是用來直接訪問類或者例項的已經存在的方法或

JavaLambda表示式方法引用和構造器引用

方法引用: 首先看 Timer t = new Timer(1000, System.out::println); 表示式 System.out::println 就是一個方法引用,等價於Lambda

javaSE新特性——介面定義加強、Lambda表示式方法引用

一、介面定義加強 首先我們回顧一下之前學習過的介面。讓它要求我們的介面只能夠由抽象方法、全域性常量組成。但是我們今天講解一下接口裡面除了這兩種元素以外還可以有普通方法和static方法。但是這樣的特性是在我們JDK1.8以後才有的。 1.可以使用default來定義普通方法,需要通過物件呼叫

Lambda表示式--Java8的新功能案例詳解(2) Lambda表示式方法引用

Lambda表示式與內部類相比有很多限制,比如只能為函式式介面建立例項,但是Lambda表示式大大簡化了程式碼的書寫。 Lambda表示式的方法引用主要分為下面幾類: 1.引用類方法 2.引用特定物件的例項方法 3.引用某類物件的例項方法 4.引用構造方法 下面通過幾個例項

深度解讀Java8-lambda表示式方法引用

先看個例子 import java.util.ArrayList; import java.util.Arrays; import static java.util.Comparator.comparing; import java.util.Comparator; im

Java 8 中的方法引用

時間 情況 arrays 抽象 以及 eth ted 方式 消費 一、原理概要 lambda 表示式,可以作為某些匿名內部類的替代。主要目的是調用該內部類中的方法,而該方法的實現(重寫)由 lambda表示式決定。 通常,我們可能不關心匿名內部類中的具體方法(被重寫的方法)

java lambda表示式的簡單應用

一、多引數的情況: (Type1 param1, Type2 param2, ..., TypeN paramN) -> { statment1; statment2; //............. return statmentM; } 二、單引數的情況: pa

淺談Java 8中的方法引用(Method References)

  本人接觸Java 8的時間不長,對Java 8的一些新特性略有所知。Java 8引入了一些新的程式設計概念,比如經常用到的 lambda表示式、Stream、Optional以及Function等,讓人耳目一新。這些功能其實上手並不是很難,根據別人的程式碼抄過來改一下,並不要知道內部的實現原理,也可以很熟

簡單理解Java Lambda表示式的形成

Lambda表示式由於語法表示比較另類,初看時會不太理解。Java官方文件提供了從內嵌類到匿名類,再到Lambda表示式簡潔清晰的描述,很有助於理解。 內嵌類 (nested/inner class) Java支援類內嵌其它類定義,即在一個類中定義另一個類,如下,在OuterClass中定義

初識Java8的lambda表示式Stream API

首先,引入一個模擬的專案,公司要求將年齡35歲以上的人員過濾出來。 將人員資料建模,有姓名、年齡、收入: public class Employee { private String name; private int age; private double salary; …

C# 委託,匿名方法lambda表示式使用方法

在 2.0 之前的 C# 版本中,宣告委託的唯一方法是使用命名方法。 C# 2.0 引入了匿名方法,而在 C# 3.0 及更高版本中,Lambda 表示式取代了匿名方法,作為編寫內聯程式碼的首選方式。 有一種情況下,匿名方法提供了 Lambda 表示式中所沒有的功能。

java Lambda表示式

https://colobu.com/2016/03/02/Java-Stream/ https://blog.csdn.net/IO_Field/article/details/54971761 http://www.runoob.com/java/java8-streams.

java Lambda表示式學習筆記

Lambda表示式的不同形式 Runnable noArguments = () -> System.out.println("Hello World");➊ ActionListener oneArgument = event -> System.out.p

深入理解Java Lambda表示式(全網之最)

本文將結合書本和網路教程,闡述自己對於Lambda表示式的理解,如有偏差,歡迎指正... 目錄 方法引用:  技術的進步,循序漸進;慢下來,紮紮實實;用過度的功夫,才能理解表面膚淺的深度 什麼是Lambda表示式? 可以將Lamb

Java - lambda表示式入門

關於lambda表示式            Lambda 是Jdk8推出的一個新特性,允許把函式作為一個方法的引數(函式作為引數傳遞進方法中)。            L

Java——Lambda表示式

一、Lambda表示式——函數語言程式設計 Lambda是JDK1.8推出的重要新特性。很多開發語言都開始支援函數語言程式設計,其中最具備代表性的就是haskell。 傳統面向物件開發 interface IMyInterface{ void print();