1. 程式人生 > >深入理解Java Lambda表示式(全網之最)

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

本文將結合書本和網路教程,闡述自己對於Lambda表示式的理解,如有偏差,歡迎指正...

目錄

方法引用:

 技術的進步,循序漸進;慢下來,紮紮實實;用過度的功夫,才能理解表面膚淺的深度

什麼是Lambda表示式?

可以將Lambda表示式理解為一個匿名函式; Lambda表示式允許將一個函式作為另外一個函式的引數; 我們可以把 Lambda 表示式理解為是一段可以傳遞的程式碼(將程式碼作為實參),也可以理解為函數語言程式設計,將一個函式作為引數進行傳遞

為什麼要引入Lambda表示式?

這就好像小強看到小明的手裡拿了一把玩具手槍,自己也想擁有一把一樣。當java程式設計師看到其他語言的程式設計師(如JS,Python)在使用閉包或者Lambda表示式的時候,於是開始吐槽世界上使用最廣的語言居然不支援函數語言程式設計。千呼萬喚,Java8推出了Lambda表示式。

Lambda表示式能夠讓程式設計師的程式設計更加高效

讓我們先來看一段程式碼:

package com.isea.java;
public class TestLambda {
       public static void main(String[] args) {
       Thread thread = new Thread(new MyRunnable());
       thread.start();
       thread.close();
    }
}
class MyRunnable implements Runnable{
	    @Override
        public void run() {
            System.out.println("Hello");
        }
}

為了使這段程式碼變得更加簡潔,可以使用匿名內部類重構一下(注意程式碼中的註釋)

package com.isea.java;
public class TestLambda {
    public static void main(String[] args) {
        new Thread(new Runnable() {
        //這裡的new Runnable()是一個匿名的[實現了Runnable介面內部類]物件,匿名物件沒有名字,只能用父類或者是父介面指向.
        //這裡可以看出,我們把一個重寫的run()方法傳入了一個建構函式中。
            @Override
            public void run() {
                System.out.println("Hello");
            }
        }).start();
    }
}

而上面的這段程式碼,不是最簡單的,還可以進一步簡化

package com.isea.java;
public class TestLambda {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello")).start();
    }
}//這裡new Thread(() -> System.out.println("Hello"));同樣實現了將一段程式碼傳入了構造方法中

怎麼樣?是不是很酷?我想上面的三段程式碼已經成功的想你展示了Lambda表示式能夠讓程式設計師的程式設計更叫高效這話是真的!假如之前沒有接觸過函數語言程式設計的話,即便已經體會到了Lambda表示式能夠減少程式碼量,省掉不少手力,也為這邏輯,感到很費腦力。孰能生巧,還記得當年打9*9的乘法表的時候,也覺得很難不是麼?

Lambda表示式的語法:

([Lambda引數列表,即形參列表]) -> {Lambda體,即方法體}

特點:使用 "->"將引數和實現邏輯分離;( ) 中的部分是需要傳入Lambda體中的引數;{ } 中部分,接收來自 ( ) 中的引數,完成一定的功能。

Lambda表示式的分類

  • 無參無返回值
package com.isea.java;
public class TestLambda {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello"));
    }
}//()中無引數,也不能省略;{}中只有一句話,建議省略。
  • 有參無返回值
package com.isea.java;
import java.util.ArrayList;
public class TestLambda {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("AAAAA");
        list.add("BBBBB");
        list.add("CCCCC");
        list.add("DDDDD");
//形參的型別是確定的,可省略;只有一個形參,()可以省略;
        list.forEach(t -> System.out.print(t + "\t"));
        //列印結果:AAAAA	BBBBB	CCCCC	DDDDD
    }
}

public void forEach(Consumer<? super E> action)

forEach()  功能等同與增強型for迴圈 這個方法來自於Iterable介面,Collection介面繼承了這個介面,List又繼承了Collection介面,而ArrayList是List的實現類;forEach函式,指明該函式需要傳入一個函式,而且是有引數沒有返回值的函式,而Consumer介面中正好有且僅有一個這樣的有參無返回值的抽象方法**。接下來,我們會了解到這是使用Lambda的必要條件。

void accept(T t);//來自原始碼

  • 無參有返回值
package com.isea.java;
import java.util.Random;
import java.util.stream.Stream;
public class TestLambda {
    public static void main(String[] args) {
        Random random = new Random();
        Stream<Integer> stream = Stream.generate(() ->random.nextInt(100));
        stream.forEach(t -> System.out.println(t));
    }//只有一個return,可以省略return;該方法將會不斷的列印100以內的正整數。
}//Stream.generate()方法建立無限流,該方法要求傳入一個無參有返回值的方法。

public static<T> Stream<T> generate(Supplier<T> s) //來自原始碼

  • 有參有返回值

這個例子,相對較長,請揉揉眼睛,需求:按照學生的姓名對學生進行排序(使用Collator 文字校對器排序)

package com.isea.java;
import java.text.Collator;
import java.util.TreeSet;

public class TestLambda {
    public static void main(String[] args) {
        Collator collator = Collator.getInstance();
        TreeSet<Student> set = new TreeSet<>((s1,s2) -> collator.compare(s1.getName(),s2.getName()));
        set.add(new Student(10,"張飛"));
        set.add(new Student(3,"周瑜"));
        set.add(new Student(1,"宋江"));
        set.forEach(student -> System.out.println(student));
    }
}//這裡的Collator是一個抽象類,但是提供了獲取該類例項的方法getInstance()

class Student{
    private int id;
    private String name;
    
    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

其實,這個例子,也不是很長。到此,我們就講完了Lambda表示式的分類,引數和返回值交叉組合一共四種。

什麼是函式式介面?

即SAM(Single Abstract Method )介面,有且只有一個抽象方法的介面(可以有預設方法或者是靜態方法和從Object繼承來的方法,但是抽象方法有且只能有一個)。 JDK1.8之後,新增@FunctionalInterface表示這個介面是是一個函式式介面,因為有了@functionalInterface標記,也稱這樣的介面為Mark(標記)型別的介面。舉例子:

@FunctionalInterface
public interface Runnable {
	  public abstract void run();
}//來自原始碼

只有函式式介面的變數或者是函式式介面,才能夠賦值為Lambda表示式。這個介面中,可以有預設方法,或者是靜態方法。函式式介面中還可以有Object中覆蓋的方法,也就是equals方法,hashCode方法。 JDK1.8之後,如果是函式式介面,可以新增 @FunctionalInterface表示這是一個函式式介面,舉例:

@FunctionalInterface
java.lang.Runnable{
    void run();
}
	   
@FunctionalInterface
java.lang.Comparator<T>{
	int compare(T o1, T o2);
}

@FunctionalInterface
public interface Function<T, R> {
	R apply(T t);
}

針對上面的例子,比方說這個Runnable介面是支援Lambda表示式,那麼如果有一個方法(比如Thread類的建構函式)需要傳入一個Runnable介面的實現類的話,那麼就可以直接把Lambda表示式寫進去。

換個角度說TreeSet,它有一個建構函式中是要求傳入一個介面型別,如果這個介面型別恰好是函式式介面,那麼直接傳進去一個Lambda表示式即可。

 函式式介面有什麼作用?

函式式介面能夠接受匿名內部類的例項化物件,換句話說,我們可以使用匿名內部類來例項化函式式介面的物件,而Lambda表示式能夠代替內部類實現程式碼的進一步簡化,因此,Lambda表示式和函式式介面緊密的聯絡到了一起,接下來的這句話非常的重要:

每一個Lambda表示式能隱式的給函式式介面賦值

上文中提到的例子:

new Thread(() -> System.out.println("hello")).start();

編譯器會認為Thread()中傳入的是一個Runnable的物件,而我們利用IDEA的智慧感知,滑鼠指向“->”或“()”的時候,會發現這是一個Runnable型別,實際上編譯器會自動將Lambda表示式賦值給函式式介面,在本例中就是Runnable介面。本例中Lambda表示式將列印方法傳遞給了Runnable介面中的run()方法,從而形成真正的方法體。

而且,

引數與返回值是一一對應的,即如果函式式介面中的抽象方法是有返回值,有引數的,那麼要求Lambda表示式也是有返回值,有引數的(餘下類推)

自定義一個函式式介面:

package com.isea.java;
@FunctionalInterface
public interface IMyInterface {
    void study();
}

package com.isea.java;
public class TestIMyInterface {
    public static void main(String[] args) {
        IMyInterface iMyInterface = () -> System.out.println("I like study");
        iMyInterface.study();
    }
}//這裡的Lambda表示式將方法體賦值給函式式介面

四大函式式介面:

有時候後,如果我們呼叫某一個方法,發現這個方法中需要傳入的引數要求是一個函式式的介面,那麼我們可以直接傳入Lambda表示式。這些介面位於java.util.function包下,需要注意一下,java.util包和java.util.function包這兩個包沒有什麼關係,切不可以為function包是java.util包下面的包。

1. 消費型介面:Consumer< T> void accept(T t)有引數,無返回值的抽象方法;

2. 供給型介面:Supplier < T> T get() 無參有返回值的抽象方法;

3. 斷定型介面: Predicate< T> boolean test(T t):有參,但是返回值型別是固定的boolean

4. 函式型介面: Function< T,R> R apply(T t)有參有返回值的抽象方法;

除了這四個之外,在java.util.function包下還有很多函式式介面可供使用。

例子:如果薪資小於10000,漲工資到10000

package com.isea.java;
import java.util.HashMap;
public class TestLambda {
    public static void main(String[] args) {
        HashMap<String,Double> map = new HashMap<>();
        map.put("周瑜",9000.0);
        map.put("宋江",12000.0);
        map.put("張飛",8000.0);

        map.forEach((k,v) -> {
            if (v < 10000.0)
                map.put(k,10000.0);
        });//BiConsumer<T,U>,void apply(T t, U u)
        map.forEach((k,v) -> System.out.print(k + ":" + v + "\t\t"));
    }
}//結果列印:張飛:10000.0		周瑜:10000.0		宋江:12000.0		

方法引用:

當Lambda表示式滿足某種條件的時候,使用方法引用,可以再次簡化程式碼

構造引用:當Lambda表示式是通過new一個物件來完成的,那麼可以使用構造引用。

package com.isea.java;
import java.util.function.Supplier;
public class TestLambda {
    public static void main(String[] args) {
//        Supplier<Student> s = () -> new Student();

        Supplier<Student> s = Student::new;
    }//實際過程:將new Student()賦值給了Supplier這個函式式介面中的那個抽象方法
}

類名::例項方法

Lambda表示式的的Lambda體也是通過一個物件的方法完成,但是呼叫方法的物件是Lambda表示式的引數列表中的一個,剩下的引數正好是給這個方法的實參。

package com.isea.java;
import java.util.TreeSet;
public class TestLambda {
    public static void main(String[] args) {
//        TreeSet<String> set = new TreeSet<>((s1,s2) -> s1.compareTo(s2));
/*  這裡如果使用第一句話,編譯器會有提示:Can be replaced with Comparator.naturalOrder,這句話告訴我們
  String已經重寫了compareTo()方法,在這裡寫是多此一舉,這裡為什麼這麼寫,是因為為了體現下面
  這句編譯器的提示:Lambda can be replaced with method reference。好了,下面的這句就是改寫成方法引用之後:
*/ //類名::例項方法
        TreeSet<String> set = new TreeSet<>(String::compareTo);
        set.add("Hello");
        set.add("isea_you");
       // set.forEach(t -> System.out.println(t));//Hello \n isea_you
        set.forEach(System.out::println);
        //(1)物件::例項方法,Lambda表示式的(形參列表)與例項方法的(實參列表)型別,個數是對應
    }
}

物件::例項方法

上面的程式碼的最後一句已經演示。

類名::靜態方法

package com.isea.java;
import java.util.stream.Stream;
public class TestLambda {
    public static void main(String[] args) {
//        Stream<Double> stream = Stream.generate(() -> Math.random());
//        類名::靜態方法, Lambda表示式的(形參列表)與例項方法的(實參列表)型別,個數是對應
        Stream<Double> stream = Stream.generate(Math::random);
        stream.forEach(System.out::println);
    }
}

第一次認認真真的寫部落格,已經凌晨兩點了,18點開始編輯的,寫的過程中我問自己,做這個事情有用麼?幾度都想放棄了,有沒有用,我也不知道,但是我知道:技術的進步,循序漸進;慢下來,紮紮實實;用過度的功夫,才能理解表面膚淺的深度。

曾經,有好幾份真摯美好的愛情擺在的面前,我都沒有好好珍惜,直到,後來做了程式設計師,才後悔莫及。