java8(二)lambda表達式手把手學習 一、在哪裏使用lambda? 二、使用函數式接口 三、使用局部變量 四、方法引用 五、實戰

Lambda可以讓你簡單的傳遞一個行爲或者代碼。可以把lambda看作是匿名函數,是沒有聲明名稱的方法,但和匿名類一樣,也可以作爲參數傳遞給一個方法。

可以把Lambda表達式理解爲:簡潔地表示可傳遞的匿名函數的一種方式:它沒有名稱,但它有參數列表、函數主體、返回類型,可能還有一個可以拋出的異常列表。

一、在哪裏使用lambda?

1.1 函數式接口

函數式接口就是隻定義一個抽象方法的接口

Lambda表達式允許你直接以內聯的形式爲函數式接口的抽象方法提供實現,並把整個表達式作爲函數式接口的實例。具體來說,Lambda表達式是函數是接口的具體實現的實例。

通過匿名內部類也可以完成同樣的事情,但是比較笨拙:需要提供一個實現,然後在直接內聯將它的實現實例化。如下面的例子所示,可以清楚地看到lambda的簡潔:

public class TestLambdaAndAnonymity {
    /**
     * 執行方法
     * @param r
     */
    public static void process(Runnable r){
        r.run();
    }
    
    public static void main(String[] args) {
        // 使用匿名類
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("this is r1");
            }
        };

        // 使用Lambda
        Runnable r2 = ()-> System.out.println("this is r2");
        
        process(r1);
        process(r2);
        // 直接將lambda作爲參數傳遞
        process(()-> System.out.println("this is r3"));
    }
}

1.2 函數描述符

函數式接口的抽象方法的簽名基本上就是Lambda表達式的簽名。我們將這種抽象方法叫作函數描述符。

舉個例子:
Runnable 接口可以看作一個什麼也不接受什麼也不返回( void )的函數的簽名,因爲它只有一個叫作 run 的抽象方法,這個方法什麼也不接受,什麼也不返回( void )。

@FunctionalInterface
public interface Runnable {

    public abstract void run();
}

如上個小節的例子:“()-> System.out.println("this is r3")”, () -> void就是這個函數的簽名, 代表了參數列表爲空,且返回 void 的函數。這正是 Runnable 接口所代表的。

舉另一個例子, (Apple,Apple) -> int 代表接受兩個 Apple 作爲參數且返回 int 的函數。

1.3 @FunctionalInterface 又是怎麼回事?

如果你去看看新的Java API,會發現函數式接口帶有 @FunctionalInterface 的標註。這個標註用於表示該接口會設計成一個函數式接口。如果你用 @FunctionalInterface 定義了一個接口,而它卻不是函數式接口的話,編譯器將返回一個提示原因的錯誤。如下:

    @FunctionalInterface
    private interface ITest{
        void test();

        void test1();
    }

表示該接口存在多個抽象方法。

@FunctionalInterface 不是必需的,但對於爲此設計的接口而言,使用它是比較好的做法。

二、使用函數式接口

函數式接口定義且只定義了一個抽象方法。函數式接口很有用,因爲抽象方法的簽名可以描述Lambda表達式的簽名。函數式接口的抽象方法的簽名稱爲函數描述符。所以爲了應用不同的Lambda表達式,你需要一套能夠描述常見函數描述符的函數式接口。

Java 8的庫設計師幫你在 java.util.function 包中引入了幾個新的函數式接口。

2.1 Predicate

java.util.function.Predicate<T> 接口定義了一個名叫 test 的抽象方法,它接受泛型T 對象,並返回一個 boolean 。

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
}

如下篩選華爲手機的例子:

    /**
     * 手機過濾方法,參數是手機list和Predicate<T>函數
     */
    public static List<Phone> phoneFilter(List<Phone> phones, Predicate<Phone> p) {
        List<Phone> results = new ArrayList<>();
        for (Phone phone : phones) {
            if (p.test(phone)) {
                results.add(phone);
            }
        }
        return results;
    }

    // 調用篩選方法
    List<Phone> huaweiPhones = phoneFilter(list, (Phone p) -> "華爲".equals(p.brand));

關於其中的and、or等方法此小節先不考慮。

2.2 Consumer

java.util.function.Consumer<T> 定義了一個名叫 accept 的抽象方法,它接受泛型 T的對象,沒有返回( void )。你如果需要訪問類型 T 的對象,並對其執行某些操作,就可以使用這個接口。

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

如下例子,分別打印每個值:

public class TestConsumer {

    public static void main(String[] args) {
        forEach(Arrays.asList(1, 2, 3, 4, 5), (Integer i) -> System.out.println(i));
    }

    public static <T> void forEach(List<T> list, Consumer<T> c) {
        for (T t : list) {
            c.accept(t);
        }

    }
}

2.3 Function

java.util.function.Function<T, R> 接口定義了一個叫作 apply 的方法,它接受一個泛型 T 的對象,並返回一個泛型 R 的對象。如果你需要定義一個Lambda,將輸入對象的信息映射到輸出,就可以使用這個接口。

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

如下所示,將String列表的長度映射到Integer列表:

    public static <T, R> List<R> map(List<T> list, Function<T, R> function) {
        List<R> results = new ArrayList<>();
        for (T t : list) {
            results.add(function.apply(t));
        }
        return results;
    }

    public static void main(String[] args) {
        List<Integer> map = map(Arrays.asList("Tom", "Jerry", "XiaoMing"), (String s) -> s.length());
        System.out.println(map);
    }

以上簡單介紹了三種函數式接口的使用方式。

2.4 帶有數據類型的函數式接口

Java類型要麼是引用類型(比如 Byte 、 Integer 、 Object 、 List ),要麼是原始類型(比如 int 、 double 、 byte 、 char )。但是泛型(比如 Consumer<T> 中的 T )只能綁定到引用類型。這是由泛型內部的實現方式造成的。

因此,在Java裏有一個將原始類型轉換爲對應的引用類型的機制。這個機制叫作裝箱(boxing)。相反的操作,也就是將引用類型轉換爲對應的原始類型,叫作拆箱(unboxing)。Java還有一個自動裝箱機制來幫助程序員執行這一任務:裝箱和拆箱操作是自動完成的。

如下所示就是一個裝箱過程,將int類型裝箱成Integer類型:

List<Integer> list = new ArrayList<>();
for (int i = 300; i < 400; i++){
    list.add(i);
}

但這在性能方面是要付出代價的。裝箱後的值本質上就是把原始類型包裹起來,並保存在堆裏。因此,裝箱後的值需要更多的內存,並需要額外的內存搜索來獲取被包裹的原始值。

減少裝箱拆箱是有必要的!

java8爲了減少這樣的問題,針對不同的接口函數提供專門的版本,用來避免自動裝箱操作。

如下舉例1000萬次的循環,分別比較裝箱和不裝箱的耗時有多大:

    public static void main(String[] args) throws InterruptedException {
        Long current = System.currentTimeMillis();

        // 非裝箱
        IntPredicate intPredicate = (int i) -> i % 3 == 0;
        CompletableFuture.runAsync(() -> {
            for (int i = 0; i < 10000000; i++) {
                intPredicate.test(RandomUtil.randomInt(100000));
            }
            System.out.println("線程名稱:IntPredicate" + Thread.currentThread().getName()+"耗時:"+ (System.currentTimeMillis() - current));
        });

        // 裝箱
        Predicate<Integer> predicate = (Integer i) -> i % 3 == 0;
        CompletableFuture.runAsync(() -> {
            for (int i = 0; i < 10000000; i++) {
                predicate.test(RandomUtil.randomInt(100000));
            }
            System.out.println("線程名稱:Predicate" + Thread.currentThread().getName()+"耗時:"+ (System.currentTimeMillis() - current));
        });

        //等待線程執行完成
        Thread.sleep(1000);
    }

結果顯示,使用IntPredicate比Predicate耗時差距在50ms左右:

線程名稱:IntPredicateForkJoinPool.commonPool-worker-1耗時:143
線程名稱:PredicateForkJoinPool.commonPool-worker-2耗時:194

一般來說,針對特殊類型的函數式接口的名稱都需要加上對應的原始類型的前綴。

下面歸納常用函數式接口:

函數式接口 函數式描述符 原始類型特化
Predicate<T> T->boolean IntPredicate
LongPredicate
DoublePredicate
Consumer<T> T->void IntConsumer
LongConsumer
DoubleConsumer
Function<T,R> T->R IntFunction<R>
IntToDoubleFunction
IntToLongFunction
LongFunction<R>
LongToDoubleFunction
LongToIntFunction
DoubleFunction<R>
ToIntFunction<T>
ToDoubleFunction<T>
ToLongFunction<T>
Supplier<T> ()-T BooleanSupplier,IntSupplier
LongSupplier
DoubleSupplier
UnaryOperator<T> T->T IntUnaryOperator
LongUnaryOperator
DoubleUnaryOperator
BinaryOperator<T> (T,T)->T IntBinaryOperator
LongBinaryOperator
DoubleBinaryOperator
BiPredicate<L,R> (L,R)->boolean
BiConsumer<T,U> (T,U)->void ObjIntConsumer<T>
ObjLongConsumer<T>
ObjDoubleConsumer<T>
BiFunction<T,U,R> (T,U)->R ToIntBiFunction<T,U>
ToLongBiFunction<T,U>
ToDoubleBiFunction<T,U>

三、使用局部變量

Lambda可以沒有限
制地捕獲(也就是在其主體中引用)實例變量和靜態變量。但局部變量必須顯式聲明爲 final ,或事實上是 final 。換句話說,Lambda表達式只能捕獲指派給它們的局部變量一次。(注:捕獲實例變量可以被看作捕獲最終局部變量 this 。)

如下代碼將會出現變異錯誤,因爲portBynber變了兩次:

        int portNumber = 1337;
        Runnable r = () -> System.out.println(portNumber);
        portNumber = 31337;

3.1 限制局部變量的原因?

1)實例變量和局部變量背後的實現有一個關鍵不同。實例變量都存儲在堆中,而局部變量則保存在棧上。

如果Lambda可以直接訪問局部變量,而且Lambda是在一個線程中使用的,則使用Lambda的線程,可能會在分配該變量的線程將這個變量收回之後,去訪問該變量。

因此,Java在訪問自由局部變量時,實際上是在訪問它的副本,而不是訪問原始變量。

如果局部變量僅僅賦值一次那就沒有什麼區別了。

2)這一限制不鼓勵你使用改變外部變量的典型命令式編程模式。

這種模式會阻礙很容易做到的並行處理,在java8當中使用的函數式編程可以很輕易的做到並行處理,而傳統的命令式編程卻需要自己去實現多線程與數據的合併。

四、方法引用

4.1 方法引用簡介

方法引用是某些Lambda的快捷寫法。讓你可以重複使用現有的方法定義,並像Lambda一樣傳遞它們。

當你需要使用方法引用時,目標引用放在分隔符 :: 前,方法的名稱放在後面。如下面的例子:

Apple::getWeight

Apple::getWeight 就是引用了 Apple 類中定義的方法 getWeight,方法引用就是Lambda表達式 (Apple a) -> a.getWeight() 的快捷寫法。

你可以把方法引用看作針對僅僅涉及單一方法的Lambda的語法糖,因爲你表達同樣的事情時要寫的代碼更少了。

4.2 構建方法引用

方法引用主要有三類:
1)指向靜態方法的方法引用
如下所示,指向靜態方法parseInt。

    public static void main(String[] args) {
        //靜態方法引用,parseInt
        Function<String, Integer> function = Integer::parseInt;
        Integer apply = function.apply("100");
        System.out.println(apply);
    }

2)指向任意類型實例方法的方法引用
如下所示,指向String的length方法

        //指向任意實例類型方法
        Function<String,Integer> function1 = String::length;
        Integer hello_world = function1.apply("hello world");
        System.out.println(hello_world);

可以這樣理解這句話:你在引用一個對象的方法,而這個對象本身是Lambda的一個參數。

3)指向現有實例對象的方法的方法引用
如下所示:

    @Data
    static class Student{
        private String name;
        private Integer age;

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

        //指向現有對象的方法
        Student student = new Student("Jack",10);
        Supplier<String> supplier = student::getName;
        String s = supplier.get();
        System.out.println(s);

與第二條不同,這個方法引用的對象時外部已經存在的對象,如上述例子中的student。

4.3 構造函數引用

對於一個現有構造函數,你可以利用它的名稱和關鍵字 new 來創建它的一個引用:ClassName::new 。它的功能與指向靜態方法的引用類似。

有如下實體類:

    @Data
    static class Student {
        private String name;
        private Integer age;
        private String address;

        public Student() {
        }

        public Student(String name) {
            this.name = name;
        }

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

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

實例:

    /**
     * description: 自定義三個參數的接口

     * @return:
     * @author: weirx
     * @time: 2021/10/19 18:03
     */
    interface ThreeParamsFunction<T, U, P, R> {
        R apply(T t, U u, P p);
    }

    public static void main(String[] args) {
        // 無構造參數 ()->new Student() 轉化成 Student::new
        Supplier<Student> supplier = Student::new;
        Student student = supplier.get();
        // 一個構造參數 (name)->new Student(name) 轉化成 Student::new
        Function<String, Student> function = Student::new;
        function.apply("JACK");
        // 兩個構造參數 (name,age)->new Student(name,age) 轉化成 Student::new
        BiFunction<String, Integer, Student> biFunction = Student::new;
        biFunction.apply("JACK", 10);
        // 三個構造參數,沒有提供三個構造參數的,需要自己寫個接口
        // (name,age,address)->new Student(name,age,address) 轉化成 Student::new
        ThreeParamsFunction<String, Integer, String, Student> threeParamsFunction = Student::new;
        threeParamsFunction.apply("JACK", 10, "Haerbin");
    }

五、實戰

使用List的sort方法對蘋果進行排序:

void sort(Comparator<? super E> c)

5.1 傳遞代碼

根據上面的sort方法構成,我們需要一個Comparator對象對蘋果進行比較。在沒有java8之前,我們需要這樣做,傳遞代碼的方式

import lombok.Data;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

/**
 * @description: 使用List的sort方法,對蘋果進行排序
 * @author:weirx
 * @date:2021/10/20 9:44
 * @version:3.0
 */
public class TestLambda {

    @Data
    static class Apple {
        private Integer weight;

        public Apple(Integer weight) {
            this.weight = weight;
        }
    }

    /**
     * 自定義一個比較類,實現Comparator接口
     */
    public static class CompareApple implements Comparator<Apple> {

        @Override
        public int compare(Apple o1, Apple o2) {
            return o1.getWeight().compareTo(o2.getWeight());
        }
    }

    public static void main(String[] args) {
        List<Apple> apples = Arrays.asList(
                new Apple(10), new Apple(19), new Apple(9), new Apple(22));
        apples.sort(new CompareApple());
    }
}

5.2 匿名類

匿名類實現接口,重寫compare方法,不需要單獨寫實現類了

import lombok.Data;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

/**
 * @description: 使用List的sort方法,對蘋果進行排序
 * @author:weirx
 * @date:2021/10/20 9:44
 * @version:3.0
 */
public class TestLambda {

    @Data
    static class Apple {
        private Integer weight;

        public Apple(Integer weight) {
            this.weight = weight;
        }
    }
    
    public static void main(String[] args) {
        List<Apple> apples = Arrays.asList(
                new Apple(10), new Apple(19), new Apple(9), new Apple(22));
        // 匿名類實現接口,重寫compare方法
        apples.sort(new Comparator<Apple>() {
            @Override
            public int compare(Apple o1, Apple o2) {
                return o1.getWeight().compareTo(o2.getWeight());
            }
        });
    }
}

5.3 使用lambda表達式

這一步將上面的匿名類轉換成了lambda表達式,下面不寫多餘代碼了:

    public static void main(String[] args) {
        List<Apple> apples = Arrays.asList(
                new Apple(10), new Apple(19), new Apple(9), new Apple(22));
        // lambda表達式,替換匿名類
        apples.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
    }

在Comparator中有一個靜態輔助方法comparing,其參數是傳遞一個Funtion<T>,在方法內部進行了比較:

    public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor)
    {
        Objects.requireNonNull(keyExtractor);
        return (Comparator<T> & Serializable)
            (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
    }

所以最終會變成如下的使用方式:

    public static void main(String[] args) {
        List<Apple> apples = Arrays.asList(
                new Apple(10), new Apple(19), new Apple(9), new Apple(22));
        // lambda表達式,替換匿名類
        apples.sort(Comparator.comparing((Apple a) -> a.getWeight()));
    }

5.4 方法引用

最簡潔的實現方式如下所示:

import lombok.Data;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

/**
 * @description: 使用List的sort方法,對蘋果進行排序
 * @author:weirx
 * @date:2021/10/20 9:44
 * @version:3.0
 */
public class TestLambda {

    @Data
    static class Apple {
        private Integer weight;

        public Apple(Integer weight) {
            this.weight = weight;
        }
    }

    public static void main(String[] args) {
        List<Apple> apples = Arrays.asList(
                new Apple(10), new Apple(19), new Apple(9), new Apple(22));
        // 方法引用,替換lambda
        apples.sort(Comparator.comparing((Apple::getWeight)));
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章