Lambda

2023-04-05 18:00:27

Lambda

前言 之前在學校,老師說,最好不要使用jdk8的一些新特性....程式碼閱讀不方便。
然後我天真的以為,是新特性不好用,是新特性閱讀體驗不好,所以,我就從未使用,也從未了解。
直到參加工作,發現了同事使用新特性,跟同事交流了這個新特性的事情,才知道是大學老師怕我們糊塗,於是在假日深入研究了一下

從JDK1.8開始為了簡化使用者進行程式碼的開發,專門提供有Lambda表示式的支援,利用此操作可以實現函數式的程式設計,對於函數語言程式設計比較著名的語言有:Haskell、Scala,利用函數式的程式設計可以避免掉物件導向程式設計之中一些繁瑣的處理問題。

Lambda入門小案例:

這是我自己寫的工具方法

public static int calculateNum(IntBinaryOperator operator){
        int a = 10;
        int b = 20;
        return operator.applyAsInt(a, b);
    }

在用這個方法的時候,發現引數的型別是 IntBinaryOperator ,那麼就直接檢視原始碼

發現有個 方法叫applyAsInt ,含義就是說 接收 int 型別的 left、right 兩個引數,最終我要返回得是 int 型別

那麼我先用一個老式得方式來呼叫 這個 calculateNum 工具方法

       //  calculateNum 原始寫法
        /*
            * IntBinaryOperator   是一個介面   在 idea 中,如果先寫常規的語法的話,此時我想要用 Lambda 的話  ,
        */
        int param = calculateNum(new IntBinaryOperator() {
            @Override
            public int applyAsInt(int left, int right) {
                return left + right;
            }
        });

這是老式得寫法了。

(小技巧:如果你使用得IDEA 那麼可以使用 Alt + enter 快速生成匿名內部類 轉變為Lambda表示式 或者 是Lambda表示式 生成匿名內部類)

那麼我使用Lambda如何寫呢?

        int params = calculateNum( ( int a,int b) -> {
            return a+b;
        });

函數式介面 Functional Interface

丟擲一個疑問:在我們書寫一段 Lambda 表示式後(比如上一章節中匿名內部類的 Lambda 表示式縮寫形式),Java 編譯器是如何進行型別推斷的,它又是怎麼知道重寫的哪個方法的?

需要說明的是,不是每個介面都可以縮寫成 Lambda 表示式。只有那些函數式介面(Functional Interface)才能縮寫成 Lambda 表示式。

那麼什麼是函數式介面(Functional Interface)呢?

所謂函數式介面(Functional Interface)就是隻包含一個抽象方法的宣告。針對該介面型別的所有 Lambda 表示式都會與這個抽象方法匹配。

注意:你可能會有疑問,Java 8 中不是允許通過 defualt 關鍵字來為介面新增預設方法嗎?那它算不算抽象方法呢?答案是:不算。因此,你可以毫無顧忌的新增預設方法,它並不違反函數式介面(Functional Interface)的定義。

總結一下:只要介面中僅僅包含一個抽象方法,我們就可以將其改寫為 Lambda 表示式。為了保證一個介面明確的被定義為一個函數式介面(Functional Interface),我們需要為該介面新增註解:@FunctionalInterface。這樣,一旦你新增了第二個抽象方法,編譯器會立刻丟擲錯誤提示。

範例程式碼:

@FunctionalInterface
interface Converter<F, T> {
    T convert(F from);
}

注意:上面的範例程式碼,即使去掉 @FunctionalInterface 也是好使的,它僅僅是一種約束而已。

Lambda錯誤範例

interface IMessage {
    public void send(String str);
    public void say();
}

public class JavaDemo {
    public static void main(String[] args) {
        IMessage i = (str) -> {
            System.out.println(str);
        };
        i.send("www.baidu.com");
    }
}

Lambda表示式的幾種格式

  • 方法沒有引數: () -> {};
  • 方法有引數::(引數,…,引數) -> {};

demo

package com.yhn.Lambda;

import java.util.function.Function;
import java.util.function.IntBinaryOperator;
import java.util.function.IntConsumer;
import java.util.function.IntPredicate;
/**

    一、什麼情況下可以使用 Lambda 表示式?
        Lambda表示式如果要想使用,那麼必須要有一個重要的實現要求:SAM(Single Abstract Method),介面中只有一個抽象方法。
        以IMessage介面為例,這個介面裡面只是提供有一個send()方法,
         interface IMessage {
         public void send(String str);
         }
        除此之外沒有任何其他方法定義,所以這樣的介面就被稱為函數式介面,
        而只有函數式介面才能被Lambda表示式所使用。


    二、 Lambda 表示式的格式
        Lambda 表示式在Java語言中引入了一個操作符**「->」**,該操作符被稱為Lambda操作符或箭頭操作符。它將Lambda分為兩個部分:
        左側:指定了Lambda表示式需要的所有引數
        右側:制定了Lambda體,即Lambda表示式要執行的功能。
        方法沒有引數: () -> {};
        方法有引數::(引數,…,引數) -> {};

        省略規則
          引數型別可以省略
          方法體只有一句程式碼時大括號return和唯一一句程式碼的分號可以省略
          方法只有一個引數時小括號可以省略
          以上這些規則都記不住也可以省略不記
 *
 */

public class LambdaDemo {

    public static void main(String[] args) {
        printNum2(value -> value % 2 == 0, value -> value>4);

        /* ------------------------------------------------------------------------------------------ */

        //  calculateNum 原始寫法
        /*
            * IntBinaryOperator   是一個介面   在 idea 中,如果先寫常規的語法的話,此時我想要用 Lambda 的話  ,
            * 可以使用 Alt + enter 快速生成匿名內部類 轉變為Lambda表示式 或者 是Lambda表示式 生成匿名內部類
        */
        int param = calculateNum(new IntBinaryOperator() {
            @Override
            public int applyAsInt(int left, int right) {
                return left + right;
            }
        });


        // 使用 Lambda 寫法
        int params = calculateNum( ( int a,int b) -> {
            return a+b;
        });

        /* ------------------------------------------------------------------------------------------ */

        /*
         *  printNum    IntPredicate 介面只有一個方法 test
         */
        printNum(new IntPredicate() {
            @Override
            public boolean test(int value) {
                return value%2==0;
            }
        });

        printNum( (int a) -> {
            return a%2 == 0;
        });

        /* ------------------------------------------------------------------------------------------ */

        // 先解釋  Function<T, R> 介面  - >  Function<String, R>
        // Function 介面 有個方法叫 apply   含義就是說   R 傳什麼型別 , 我最終都是要返回的是 T 型別  在此處是  String 型別


        //  此時 我只需要 設定一個泛型即可
        Integer result  = typeConver(new Function<String, Integer>() {

            @Override
            public Integer apply(String s) {
                return Integer.valueOf(s);
            }
        });

        Integer results  = typeConver( (String s) ->{
            return Integer.valueOf(s);
        });

        // 擴充套件
        String s1 = typeConver(s -> {
            return s + "new";
        });


        /* ------------------------------------------------------------------------------------------ */

        foreachArr(new IntConsumer() {
            @Override
            public void accept(int value) {
                System.out.println(value);
            }
        });

        foreachArr((int value ) -> {
            System.out.println(value);
        });

        // 進行省略
        foreachArr( value -> System.out.println(value));


        /* ------------------------------------------------------------------------------------------ */
    }



    public static void foreachArr(IntConsumer consumer){
        int[] arr = {1,2,3,4,5,6,7,8,9,10};
        for (int i : arr) {
            consumer.accept(i);
        }
    }




    public static <R> R typeConver(Function<String,R> function){
        String str = "1235";
        R result = function.apply(str);
        return result;
    }



    public static void printNum2(IntPredicate predicate,IntPredicate predicate2){
        int[] arr = {1,2,3,4,5,6,7,8,9,10};
        for (int i : arr) {
            if(predicate.and(predicate2).test(i)){
                System.out.println(i);
            }
        }
    }

    public static void printNum(IntPredicate predicate){
        int[] arr = {1,2,3,4,5,6,7,8,9,10};
        for (int i : arr) {
            if(predicate.test(i)){
                System.out.println(i);
            }
        }
    }

    public static int calculateNum(IntBinaryOperator operator){
        int a = 10;
        int b = 20;
        return operator.applyAsInt(a, b);
    }
}

總結:

​ Lambda 表示式在Java語言中引入了一個操作符「->」,該操作符被稱為Lambda操作符或箭頭操作符。它將Lambda分為兩個部分:
​ 左側:指定了Lambda表示式需要的所有引數
​ 右側:制定了Lambda體,即Lambda表示式要執行的功能。
​ 方法沒有引數: () -> {};
​ 方法有引數::(引數,…,引數) -> {};

​ 省略規則
​ 引數型別可以省略
​ 方法體只有一句程式碼時大括號return和唯一一句程式碼的分號可以省略
​ 方法只有一個引數時小括號可以省略
​ 以上這些規則都記不住也可以省略不記

方法參照

方法參照的三種形式

​ 物件 :: 非靜態方法

​ 類 :: 靜態方法

​ 類 :: 非靜態方法

類參照靜態方法

語法格式: 類名稱::static方法名稱

第一步,我們自定義一個介面,該介面中只有一個抽象方法,是一個函數式介面。

第二步,隨便建立一個類,建立一個方法。這裡要注意,建立的方法返回值型別和形參列表必須和函數式介面中的抽象方法相同。

第三步,建立函數式介面的實現類,我們可以使用方法參照。相當於實現類裡的重寫的方法,就是方法參照的方法。這樣才能方法參照。

public class LambdaDemoLei {
    /**
     *   @FunctionalInterface,主要用於編譯級錯誤檢查,加上該註解,當你寫的介面不符合函數式介面定義的時候,編譯器會報錯。
     */
    @FunctionalInterface
    interface IMessage<T,P> {
        public T transfer(P p);
    }

    static class Supplier{
        public static String getStr(Integer integer) {
            return String.valueOf(integer);
        }
    }
    public static void main(String[] args) {
    //        類參照靜態方法   類名稱::static方法名稱
        IMessage<String, Integer> msg = Supplier::getStr;
        System.out.println(msg.transfer(31415926));
    }
    
    // 輸出 31415926

物件參照非靜態方法

語法格式: 範例化物件::普通方法;

有了類參照靜態方法的基礎,相信大家已經有了一點感覺。

物件參照非靜態方法,和類參照靜態方法一致。要求我們物件參照的方法,返回值和形參列表要和函數式介面中的抽象方法相同。

public class LambdaDemoLei {
    @FunctionalInterface
    interface IMessage1 {
        public double get();
    }

    static class Supplier1{
        private Double salary;

        public Supplier1() {
        }

        public Supplier1(Double salary){
            this.salary = salary;
        }
        public Double getSalary() {
            return this.salary;
        }
    }
    
        public static void main(String[] args) {
//        物件參照非靜態方法   語法格式: 範例化物件::普通方法
        Supplier1 supplier = new Supplier1(9999.9);
        IMessage1 msg1 = supplier::getSalary;
        System.out.println(msg1.get());
    }
    
    // 輸出 9999.9
}

類參照非靜態方法

語法格式: 類::普通方法

類參照普通方法就有點難以理解了。

當抽象方法中有兩個引數,且第一個引數是呼叫者,第二個引數是形參,則可以使用類::實體方法。

package com.yhn.Lambda;

import lombok.Data;

/**
 * @Description
 * @Author TuiMao
 * @Date 2023/4/4 16:19
 * @Version 1.0
 */
@Data
public class Person {
    @FunctionalInterface
    interface IMessage<T, P> {
        // 要看成 T res = p1.compare(p2);
        public T compare(P p1, P p2);
    }

    @FunctionalInterface
    interface IMessage2<T, P, V> {
        //  public T create(P p1, V p2); 符合抽象方法的要求
        public T create(P p1, V p2);
    }



    @FunctionalInterface
    interface IMessage1<T, P, V> {
        // 看成 T res = p1.compare(p2);
        // public int compareTo(String anotherString){} 符合抽象方法的格式
        // int res = str1.compare(str2);
        public T compare(P p1, V p2);
    }


    public Person() {
    }

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    private String name;
    private Integer age;

    public boolean equal(Person per) {
        return this.name.equals(per.getName()) && this.age.equals(per.getAge());
    }

    public static void main(String[] args) {
        Person person1 = new Person("張三", 22);
        Person person2 = new Person("張三", 23);
        // 符合T res = p1.compare(p2);
        IMessage<Boolean, Person> msg = Person::equal;
        System.out.println(msg.compare(person1, person2));

        System.out.println("----------");

        // 類參照普通方法  語法格式: 類::普通方法       當抽象方法中有兩個引數,且第一個引數是呼叫者,第二個引數是形參,則可以使用類::實體方法。
        IMessage1<Integer,String,String> stringCompare = String::compareTo;
        Integer compare = stringCompare.compare("adc", "abd");
        System.out.println(compare);

        System.out.println("----------");

        // 構造參照  語法格式: 類名稱::new
        IMessage2<Person,String,Integer> msg1 = Person::new;
        Person person = msg1.create("張三", 20);
        System.out.println(person);

    }

}

Lambda 存取外部變數及介面預設方法

在本章節中,我們將會討論如何在 lambda 表示式中存取外部變數(包括:區域性變數,成員變數,靜態變數,介面的預設方法.),它與匿名內部類存取外部變數很相似。

存取區域性變數

在 Lambda 表示式中,我們可以存取外部的 final 型別變數,如下面的範例程式碼:

// 轉換器
@FunctionalInterface
interface Converter<F, T> {
    T convert(F from);
}
final int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);

stringConverter.convert(2);     // 3

與匿名內部類不同的是,我們不必顯式宣告 num 變數為 final 型別,下面這段程式碼同樣有效:

int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);

stringConverter.convert(2);     // 3

但是 num 變數必須為隱式的 final 型別,何為隱式的 final 呢?就是說到編譯期為止,num 物件是不能被改變的,如下面這段程式碼,就不能被編譯通過:

int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);
num = 3;

在 lambda 表示式內部改變 num 值同樣編譯不通過,需要注意, 比如下面的範例程式碼:

int num = 1;
Converter<Integer, String> converter = (from) -> {
	String value = String.valueOf(from + num);
	num = 3;
	return value;
};

存取成員變數和靜態變數

上一章節中,瞭解瞭如何在 Lambda 表示式中存取區域性變數。與區域性變數相比,在 Lambda 表示式中對成員變數和靜態變數擁有讀寫許可權:

    @FunctionalInterface
    interface Converter<F, T> {
        T convert(F from);
    }
class Lambda4 {
        // 靜態變數
        static int outerStaticNum;
        // 成員變數
        int outerNum;

        void testScopes() {
            Converter<Integer, String> stringConverter1 = (from) -> {
                // 對成員變數賦值
                outerNum = 23;
                return String.valueOf(from);
            };

            Converter<Integer, String> stringConverter2 = (from) -> {
                // 對靜態變數賦值
                outerStaticNum = 72;
                return String.valueOf(from);
            };
        }
    }

存取介面的預設方法

@FunctionalInterface
interface Formula {
	// 計算
	double calculate(int a);

	// 求平方根
	default double sqrt(int a) {
		return Math.sqrt(a);
	}
}

當時,我們在介面中定義了一個帶有預設實現的 sqrt 求平方根方法,在匿名內部類中我們可以很方便的存取此方法:

Formula formula = new Formula() {
	@Override
	public double calculate(int a) {
		return sqrt(a * 100);
	}
};

但是在 lambda 表示式中可不行:

Formula formula = (a) -> sqrt(a * 100);

帶有預設實現的介面方法,是不能在 lambda 表示式中存取的,上面這段程式碼將無法被編譯通過。

JDK8自帶函數式介面

在JDK1.8之中,提供有Lambda表示式和方法參照,但是你會發現如果由開發者自己定義函數式的介面,往往都需要使用@FunctionalInterface來進行大量的宣告,於是很多的情況下如果為了方便則可以參照系統中提供的函數式介面。

在系統之中專門提供有一個java.util.functional的開發包,裡面可以直接使用函數式介面,在這個包下面一共有如下幾個核心的介面供我們使用。

功能型函數式介面

介面定義 介面作用 介面使用
@FunctionalInterface
public interface Function<T,R>
消費 T 型別引數,返回 R 型別結果 如下所示

function 相當於是給一個引數,然後返回一個結果。
如果是給兩個引數,返回一個結果,那麼就是 BiFunction。Bi 字首即使 binary 的縮寫。

import java.util.function.*;
/*
  @FunctionalInterface
  T是引數型別
  R是返回型別
  public interface Function<T,R>{
  	public R apply(T t);
  }
 */
class StringCompare {
	// 給一個 String 型別的引數,返回布林型別,符合功能性函數式介面的抽象方法
    public static boolean test(String t) {
        return t == null;
    }
}
public class JavaDemo {
    public static void main(String[] args) {
    	// 直接靜態參照
        Function<String,Boolean> func1 = StringCompare::test;
        System.out.println(func1.apply(null));
    }
}

// true

消費型函數式介面

消費性函數式介面,只能進行資料的處理操作,而沒有返回值

· 在進行系統輸出的時候使用的是:System.out.println();這個操作只是進行資料的輸出和消費,而不能返回,這就是消費性介面。

介面定義 介面作用 介面使用
@FunctionalInterface
public interface Consumer
接收一個 T 型別引數,但是不返回任何東西,消費型介面 如下

其實最常見的消費型介面的實現,就是 System.out.println(xxx) 了。 我們只管往方法中輸入引數,但是並沒有返回任何值。
Consumer 相當於是有來無回,給一個引數,但是無返回。
而如果是兩個引數,無返回,那麼就是 BiConsumer。

public class JavaDemo {
    public static void main(String[] args) {
        Consumer<String> consumer = System.out::println;
        consumer.accept("Hello World!");
    }
}

// Hello World!

當然我們也可以自定義消費性介面

class StringCompare {
	// 接收 StringBuilder ,但是不返回任何資料。
    public void fun(StringBuilder sb) {
        sb.append("World!");
    }
}

public class JavaDemo {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        sb.append("Hello ");
        Consumer<StringBuilder> consumer = new StringCompare()::fun;
        consumer.accept(sb);
        System.out.println(sb.toString());
    }
}

供給型函數式介面

介面定義 介面作用 介面使用
@FunctionalInterface
public interface Supplier
啥也不接受,但是卻返回 T 型別資料,供給型介面 如下

Supplier 相當於是無中生有,什麼也不傳,但是返回一個結果。
像 String 類裡的 toUpperCase() 方法,也是不接受引數,但是返回 String 型別,就可以看成這個供給型函數式介面的一個實現。

public String toUpperCase() {
        return toUpperCase(Locale.getDefault());
    }

import java.util.function.*;
public class Demo01 {
	public static void main(String[] args) {
		Supplier <String> sup = "WWW.BAIDU.COM" :: toLowerCase;
		System.out.println(sup.get());
	}
}

斷言型函數式介面

介面定義 介面作用 介面使用
@FunctionalInterface public interface Predicate 傳入 T 型別引數,返回布林型別,常常用於對入參進行判斷 如下
class StringFilter {
	// 對集合中的資料進行過濾,傳入斷言型介面進行判斷
    public static List<String> filter(List<String> list, Predicate<String> predicate) {
        List<String> stringList = new ArrayList<>();
        for (String str : list) {
            if (predicate.test(str)) {
                stringList.add(str);
            }
        }
        return stringList;
    }

}

public class JavaDemo {
    public static void main(String[] args) {
        List<String> stringList = Arrays.asList("好詩", "好文", "好評", "好漢", "壞蛋", "蛋清", "清風", "風間");
        List<String> filterList = StringFilter.filter(stringList, list -> list.contains("好"));
        System.out.println(filterList);
    }
}

如果JDK本身提供的函數式介面可以被我們所使用,那麼就沒必要重新去定義了。