JAVA學習筆記 15 - 函數式接口、Lambda表達式和方法引用

本文是Java基礎課程的第十五課。主要介紹在JDK8中,Java引入的部分新特性,包括函數式接口、Lambda表達式和方法引用。這些新特性使得Java能夠在按照面向對象思想進行開發的基礎上,融合函數式編程的許多優點。學習掌握並熟練使用這些新特性,可以使Java代碼更簡潔、清晰、優雅

一、函數式接口

1、什麼是函數式接口

JDK8中新增了許多有趣的特性,比如前文中已經介紹過的新的日期時間API、接口默認方法等。函數式接口也是JDK8中提出的新特性之一。

函數式接口Functional Interface),也被稱爲功能性接口,簡單的理解就是指有且只有一個抽象方法的接口

JDK8爲了規範函數式接口定義,提供了@FunctionalInterface註解,編譯器會檢查被@FunctionalInterface註解標註的接口是否是一個合法的函數式接口。

一個規範函數式接口應該遵循以下規則

  1. 有且僅有一個抽象方法可以有其他非抽象方法重新顯式聲明java.lang.Object相同非最終公開(由public修飾且沒有被final修飾)的方法聲明可以忽略
  2. @FunctionalInterface註解(但如果某接口只有一個抽象方法,有沒有標註@FunctionalInterface註解並不影響它是一個函數式接口的事實,@FunctionalInterface註解只是觸發編譯器的檢查,起到強制保證該接口是一個合法函數式接口的作用)。

函數式接口是新版本的Java之所以能夠支持函數式編程基礎之一函數式接口核心思維如下:

  1. 規範且僅規範一種行爲
  2. 該行爲通過函數式接口的實現類可以方便進行實現
  3. 近似得將該行爲當作調用方法參數使用,事實上是將函數式接口實現類對象作爲方法實際參數進行傳參。

2、聲明與應用函數式接口

2.1、聲明函數式接口

聲明一個函數式接口非常簡單,按照聲明接口正常語法,並讓該接口符合函數式接口規範即可
下面是一個示例:
Action接口的源碼:

package com.codeke.java.test;

/**
 * 行爲接口
 */
@FunctionalInterface
public interface Action<T> {
    /**
     * 接收一個輸入參數並進行處理的方法,該方法無返回值
     * @param t 需要接收的參數
     */
    void accept(T t);
}

說明:

  • 本例中聲明瞭一個泛型的函數式接口,名稱爲Action,該接口只包含一個抽象方法accept(T t),並且該接口標註了@FunctionalInterface註解。
  • 該接口的作用即是規範一種行爲,該行爲需要接收一個參數、不返回任何結果,該行爲由接口中唯一的抽象方法accept(T t)來聲明,使用時由具體的實現類來實現。

2.2、應用函數式接口

在之前介紹集合的章節中,通過編程實現了一個簡易的集合,包含MyList接口和以數組爲基礎的集合實現類MyArrayList。這裏結合函數式接口,簡化簡易集合的遍歷操作。

下面是一個示例:
MyList接口的源碼:

package com.codeke.java.test;

/**
 * 簡易的List接口
 */
public interface MyList<T> {

    // 向集合中添加元素
    boolean add(T o);

    // 獲取指定下標處的元素
    T get(int index);

    // 移除並返回集合中指定下標處的元素
    T remove(int index);

    // 獲取集合中元素的個數
    int size();

    /**
     * 遍歷處理的方法
     * @param action 消費者
     */
    default void forEach(Action<T> action) {
        for(int i = 0; i < this.size(); i ++){
            T t = this.get(i);
            action.accept(t);
        }
    }
}

MyArrayList類的源碼同之前章節中編程實現簡易集合的示例。
測試類Test類的源碼:

package com.codeke.java.test;

public class Test {
    public static void main(String[] args) {
        // 實例化一個自定義實現的集合對象,並賦值給一個MyList接口類型的變量
        MyList<Integer> list = new MyArrayList<>();
        // 添加元素
        list.add(11);
        list.add(32);
        list.add(152);
        list.add(87);
        list.add(76);
        list.add(90);
        list.add(6);
        // 使用MyList接口中聲明的forEach(Action<T> action)方法遍歷
        list.forEach(new Action<Integer>() {
            @Override
            public void accept(Integer num) {
                System.out.print(num + " ");
            }
        });
    }
}

執行輸出結果:

11 32 152 87 76 90 6 

說明:

  • 本例的MyList接口中,聲明瞭一個默認方法forEach(Action<T> action),該方法的參數是一個Action接口類型的變量,在調用該方法時需要傳入一個Action接口的實現類對象,之後,在forEach(Action<T> action)方法中遍歷當前集合,並將集合中的每一個元素交由Action接口規範的行爲,即accept(T t)方法進行處理。
  • 本例的Test測試類main方法中,使用了匿名內部類實例化了一個的Action接口實現類對象,並將該對象作爲forEach(Action<T> action)方法的實際參數進行傳參,看起來,猶如在調用集合listforEach(Action<T> action)方法時,將一種行爲(本例中是System.out.print(num + " "))作爲方法參數而使用。

3、JDK中的函數式接口

函數式接口雖然是JDK8中才提出的特性,但之前版本的JDK中其實已經存在一些事實上的函數式接口,JDK8對它們進行了規範,標註@FunctionalInterface註解。它們中常見的一些如下:

接口名稱 抽象方法 返回值類型
java.util.Comparator<T> compare(T o1, T o2) int
java.lang.Runnable run() void
java.util.concurrent.Callable<V> call() V
java.io.FileFilter accept(File pathname) boolean
java.nio.file.PathMatcher matches(Path path) boolean

JDK8除了規範以前存在函數式接口之外,還提供了一系列函數式接口,它們都在java.util.function下,主要分爲以下幾種

  1. 消費接口
  2. 生產接口
  3. 功能接口
  4. 操作接口
  5. 斷言接口

3.1、消費接口

消費型函數式接口的名稱中都帶有Consumer字樣,該型接口中的抽象方法均爲accept()(不同的消費型函數式接口中accept()方法的參數列表不同),這些accept()方法規範“消費給定數據或對象”的行爲,這種消費行爲通常不需要返回值,故這些accept()方法的返回值類型均爲void

常見消費型函數式接口如下:

接口名稱 抽象方法 返回值類型
Consumer<T> accept(T t) void
BiConsumer<T, U> accept(T t, U u) void
DoubleConsumer accept(double value) void
IntConsumer accept(int value) void
LongConsumer accept(long value) void
ObjDoubleConsumer<T> accept(T t, double value) void
ObjIntConsumer<T> accept(T t, int value) void
ObjLongConsumer<T> accept(T t, long value) void

下面是一個示例:

package com.codeke.java.test;

import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;

public class Test {
    public static void main(String[] args) {
        // 聲明Map類型的變量map,並賦值爲一個HashMap實例
        Map<String, String> map = new HashMap<>();
        // 向map集合中添加鍵值映射數據
        map.put("宋江","呼保義");
        map.put("盧俊義","玉麒麟");
        map.put("吳用","智多星");
        map.put("公孫勝","入雲龍");
        map.put("關勝","大刀");
        // 調用map集合的forEach(BiConsumer<? super K, ? super V> action)方法
        // 該方法需要一個消費型函數式接口BiConsumer<T, U>的實現類對象
        map.forEach(new BiConsumer<String, String>() {
            // 實現accept(T t, U u)方法
            @Override
            public void accept(String k, String v) {
                System.out.println(k + "的綽號是" + v);
            }
        });
    }
}

執行輸出結果:

公孫勝的綽號是入雲龍
盧俊義的綽號是玉麒麟
關勝的綽號是大刀
吳用的綽號是智多星
宋江的綽號是呼保義

說明:

  • 本例演示了藉助消費型函數式接口BiConsumer<T, U>java.util.Map集合的forEach(BiConsumer<? super K, ? super V> action)方法遍歷java.util.Map集合。遍歷的過程中,具體的循環由java.util.Map集合的forEach(BiConsumer<? super K, ? super V> action)方法執行,而循環中每次迭代獲取到的鍵值映射數據要如何進行消費,則由BiConsumer<T, U>接口的accept(T t, U u)方法負責。
  • 在本例的代碼中,通過匿名內部類實現了消費型函數式接口BiConsumer<T, U>,即實現了該接口中的accept(T t, U u)方法,具體實現的行爲是對該方法接收到的參數,即遍歷java.util.Map集合獲取到的一對一對的鍵值映射數據進行打印。

3.2、生產接口

生產型函數式接口的名稱中都帶有Supplier字樣,該型接口中的抽象方法均帶有get字樣,這些方法規範“生產一個數據或對象”的行爲,這些方法根據其所在的接口的不同,都聲明瞭對應的、不同的返回值類型。

常見生產型函數式接口如下:

接口名稱 抽象方法 返回值類型
Supplier<T> get() T
BooleanSupplier getAsBoolean() boolean
DoubleSupplier getAsDouble() double
IntSupplier getAsInt() int
LongSupplier getAsLong() long

下面是一個示例:

package com.codeke.java.test;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.IntSupplier;

public class Test {
    public static void main(String[] args) {
        // 獲取一個集合list,並添加了若干個字符串形式的人名作爲集合元素
        List<String> list = Arrays.asList("林沖","秦明","呼延灼","花榮","柴進");
        // 調用printName(List<String> namelist, IntSupplier indexSupplier)打印一個人名
        printName(list, new IntSupplier() {
            @Override
            public int getAsInt() {
                return new Random().nextInt(list.size());
            }
        });
    }
    /**
     * 打印名字的方法
     * @param namelist 名字集合
     * @param indexSupplier 下標生產者
     */
    private static void printName(List<String> namelist, IntSupplier indexSupplier){
        int index = indexSupplier.getAsInt();
        System.out.println(namelist.get(index));
    }
}

執行輸出結果不再贅述。
說明:

  • 本例中,Test類中除了main方法之外,提供了一個靜態方法printName(List<String> namelist, IntSupplier indexSupplier),該方法在調用是需要傳入一個包含若干字符串的列表nameList,以及一個生產型函數式接口IntSupplier的實現類對象作爲整型數字的生產者,該方法中,藉助IntSupplier接口中的getAsInt()方法獲得一個int類型的值,並將該值作爲下標,訪問集合list中的元素並打印。
  • 在本例的main方法中,通過java.util.Arrays類的asList(T... a)方法獲得一個保存了若干個字符串形式的人名的集合list,並調用printName(List<String> namelist, IntSupplier indexSupplier)方法,調用時使用匿名內部類實現了生產型函數式接口IntSupplier,即實現了該接口中的getAsInt()方法,實現邏輯爲隨機獲取一個[0,list.size())區間內的整數並返回。

3.3、功能接口

功能型函數式接口的名稱中都帶有Function字樣,該型接口中的抽象方法均帶有apply字樣,這些方法規範的行爲可以理解爲“使用某些數據或對象加工轉換成另一類數據或對象”,這些方法根據其所在的接口的不同,都聲明瞭對應的、不同的方法參數列表和返回值類型。

常見功能型函數式接口如下:

接口名稱 抽象方法 返回值類型
Function<T, R> apply(T t) R
BiFunction<T, U, R> apply(T t, U u) R
DoubleFunction<R> apply(double value) R
DoubleToIntFunction applyAsInt(double value) int
DoubleToLongFunction applyAsLong(double value) long
IntFunction<R> apply(int value) R
IntToDoubleFunction applyAsDouble(int value) double
IntToLongFunction applyAsLong(int value) long
LongFunction<R> apply(long value) R
LongToDoubleFunction applyAsDouble(long value) double
LongToIntFunction applyAsInt(long value) int
ToDoubleBiFunction<T, U> applyAsDouble(T t, U u) double
ToDoubleFunction<T> applyAsDouble(T value) double
ToIntBiFunction<T, U> applyAsInt(T t, U u) int
ToIntFunction<T> applyAsInt(T value) int
ToLongBiFunction<T, U> applyAsLong(T t, U u) long
ToLongFunction<T> applyAsLong(T value) long

下面是一個示例:

package com.codeke.java.test;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;

public class Test {
    public static void main(String[] args) {
        // 總成績單
        Map<String, Integer> scoreMap = new HashMap<>();
        scoreMap.put("魯智深", 67);
        scoreMap.put("吳用", 90);
        scoreMap.put("武松", 72);
        scoreMap.put("林沖", 83);
        // 補充成績單
        Map<String, Integer> additionalScoreMap = new HashMap<>();
        additionalScoreMap.put("公孫勝", 88);
        additionalScoreMap.put("花榮", 79);
        additionalScoreMap.put("武松", 75);
        // 將補充成績單彙總到總成績單,涉及兩個方面
        // 1.總成績單中已有的學員根據補充成績單調整成績
        // 2.補充成績單中新增的學員及成績添加到總成績單中
        Set<Map.Entry<String, Integer>> entries = additionalScoreMap.entrySet();
        for (Map.Entry<String, Integer> entry : entries) {
            String name = entry.getKey();
            Integer score = entry.getValue();
            scoreMap.computeIfPresent(name, new BiFunction<String, Integer, Integer>() {
                @Override
                public Integer apply(String s, Integer i) {
                    System.out.printf("之前%s的成績爲%d,現在調整爲%d\n", s, i, score);
                    return score;
                }
            });
            scoreMap.computeIfAbsent(name, new Function<String, Integer>() {
                @Override
                public Integer apply(String s) {
                    System.out.printf("之前沒有%s的成績,現在補錄\n", s);
                    return score;
                }
            });
        }
        // 打印彙總後的成績單
        scoreMap.forEach(new BiConsumer<String, Integer>() {
            @Override
            public void accept(String s, Integer i) {
                System.out.printf("%s的成績爲%d\n", s, i);
            }
        });
    }
}

執行輸出結果:

之前沒有公孫勝的成績,現在補錄
之前沒有花榮的成績,現在補錄
之前武松的成績爲72,現在調整爲75
魯智深的成績爲67
花榮的成績爲79
公孫勝的成績爲88
武松的成績爲75
林沖的成績爲83
吳用的成績爲90

說明:

  • 本例中,使用java.util.Map集合提供了兩個成績單,爲總成績單和補充成績單,首先需要將總成績單中已有的學員根據補充成績單調整成績,其次需要將補充成績單中新增的學員及成績添加到總成績單中。
  • 本例使用了java.util.Map集合中的computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)方法,該方法根據傳入的鍵(key),判斷集合中是否存在對應的值(value),如果存在,則執行參數中傳入的功能型函數式接口BiFunction<T, U, R>的實現類所實現的處理邏輯,並使用返回值作爲新的值(value),存入集合。
  • 本例還使用了java.util.Map集合中的computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)方法,該方法根據傳入的鍵(key),判斷集合中是否存在對應的值(value),如果不存在,則執行參數中傳入的功能型函數式接口Function<T, R>的實現類所實現的處理邏輯,並使用返回值作爲值(value),和鍵(key)一起作爲一組鍵值映射數據,存入集合。

3.4、操作接口

操作型函數式接口的名稱中都帶有Operator字樣,該型函數式接口和功能型函數式接口相似,不同的是,在語意上,功能型函數式接口傾向完成某些功能,而操作型函數式接口傾向完成一些操作或計算。事實上,接口BinaryOperator<T>既是繼承自BiFunction<T, U, R>接口的,很多時候也將操作型函數式接口歸爲功能型函數式接口。操作型函數式接口中的抽象方法也均帶有apply字樣,這些方法根據其所在的接口的不同,都聲明瞭對應的、不同的方法參數列表和返回值類型。

常見操作型函數式接口如下:

接口名稱 抽象方法 返回值類型
BinaryOperator<T> apply(T t1, T t2) T
UnaryOperator<T> apply(T t) T
DoubleBinaryOperator applyAsDouble(double left, double right) double
DoubleUnaryOperator applyAsDouble(double operand) double
IntBinaryOperator applyAsInt(int left, int right) int
IntUnaryOperator applyAsInt(int operand) int
LongBinaryOperator applyAsLong(long left, long right) long
LongUnaryOperator applyAsLong(long operand) long

下面是一個示例:

package com.codeke.java.test;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.function.BinaryOperator;

public class Test {
    public static void main(String[] args) {
    	// 本金1000元,複利率爲10%,逐年計算10年內每年的本金與利息
    	// 聲明並實例化長度爲10的BigDecimal類型數組
        BigDecimal[] nums = new BigDecimal[10];
        // 數組的第一個元素賦值爲本金
        nums[0] = new BigDecimal("1000");
        // 數組其他元素賦值爲年利率
        Arrays.fill(nums, 1, 10, new BigDecimal("0.1"));
        // 藉助Arrays類的parallelPrefix(T[] array, BinaryOperator<T> op)方法完成逐年複利計算
        // 計算操作由操作型函數式接口BinaryOperator<T>的實現類具體實現
        Arrays.parallelPrefix(nums, new BinaryOperator<BigDecimal>() {
            @Override
            public BigDecimal apply(BigDecimal left, BigDecimal right) {
                return left.multiply(right).add(left)
                        .setScale(2, RoundingMode.HALF_UP);
            }
        });
        // 逐年打印
        for (int i = 0; i < nums.length; i++) {
            System.out.printf("第%d年時利息加本金共:%s\n", (i + 1), nums[i]);
        }
    }
}

執行輸出結果:

1年時利息加本金共:10002年時利息加本金共:1100.003年時利息加本金共:1210.004年時利息加本金共:1331.005年時利息加本金共:1464.106年時利息加本金共:1610.517年時利息加本金共:1771.568年時利息加本金共:1948.729年時利息加本金共:2143.5910年時利息加本金共:2357.95

說明:

  • 本例的代碼計算了以1000元爲本金,按10%的複利率在10年間逐年的利息與本金之和。
  • 在本例的main方法中,先聲明並實例化了一個長度爲10java.math.BigDecimal類型的數組nums,數組的第一個元素被賦值爲本金1000,之後的元素被賦值爲複利率10%。之後,調用java.util.Arrays類的parallelPrefix(T[] array, BinaryOperator<T> op)方法,調用該方法時,首先需要傳入被操作的數組,本例中即是數組nums,其次需要傳入一個操作型函數式接口BinaryOperator<T>的實現類對象,該方法會將數組nums中每個元素和其之前的一個元素一起傳入BinaryOperator<T>接口實現類對象的apply(T t1, T t2)方法中,然後使用apply(T t1, T t2)方法的返回值替換數組中相應的元素。藉助parallelPrefix(T[] array, BinaryOperator<T> op)方法的功能,只需在實現BinaryOperator<T>接口的apply(T t1, T t2)方法時,使用複利的計算公式(本金 * 利率)+ 本金,對入參進行計算並返回計算結果,即可完成本例的需求。

3.5、斷言接口

斷言型函數式接口的名稱中都帶有Predicate字樣,該型接口中的抽象方法均帶有test字樣,這些方法規範的行爲可以理解爲“判斷傳入的數據或對象斷言它們是否符合某種標準”,這些方法根據其所在的接口的不同,都聲明瞭對應的、不同的方法參數列表,但斷言的結果均可以使用一個布爾值表示,故這些方法的返回值類型均爲boolean

常見斷言型函數式接口如下:

接口名稱 抽象方法 返回值類型
Predicate<T> test(T t) boolean
BiPredicate<T, U> test(T t, U u) boolean
DoublePredicate test(double value) boolean
IntPredicate test(int value) boolean
LongPredicate test(long value) boolean

下面是一個示例:

package com.codeke.java.test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class Test {
    public static void main(String[] args) {
        // 聲明並實例化一個ArrayList集合list
        List<String> list = new ArrayList<>();
        // 添加元素
        list.add("魯智深");
        list.add("武松");
        list.add("林沖");
        list.add("吳用");
        list.add("公孫勝");
        // 刪除元素"林沖"及"吳用"
        list.removeIf(new Predicate<String>() {
            @Override
            public boolean test(String s) {
                return "林沖".equals(s) || "吳用".equals(s);
            }
        });
        // 打印集合元素
        System.out.println(Arrays.toString(list.toArray()));
    }
}

執行輸出結果:

[魯智深, 武松, 公孫勝]

說明:

  • 在本例的main方法中,聲明並實例化了一個java.util.ArrayList集合list,並添加了若干字符串形式的人名作爲元素,之後調用了list集合的removeIf(Predicate<? super E> filter)方法,該方法調用時需要傳入一個斷言型函數式接口Predicate<T>的實現類對象,removeIf(Predicate<? super E> filter)方法會對集合中的每一個元素使用Predicate<T>實現類所實現的斷言邏輯進行判斷,如果斷言爲真,則從集合中刪除該元素。

二、Lambda表達式

1、什麼是Lambda表達式

Lamdbda
Lambda表達式也是JDK8中新增的特性之一,可以替換用以實現函數式接口匿名內部類

新版本的Java之所以能夠支持函數式編程Lambda表達式最重要基石Lambda表達式核心思維是:

  1. 通過Lambda表達式簡潔的語法,專注於函數型接口所規範的行爲具體應該做什麼,進一步簡化函數型接口實現
  2. Lambda表達式作爲方法參數使用,即將具體行爲作爲方法實際參數而使用。

2、Lambda表達式之初體驗

上文中在介紹消費型函數式接口時使用過一個示例,這裏再次觀察該示例:

package com.codeke.java.test;

import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;

public class Test {
    public static void main(String[] args) {
        // 聲明Map類型的變量map,並賦值爲一個HashMap實例
        Map<String, String> map = new HashMap<>();
        // 向map集合中添加鍵值映射數據
        map.put("宋江","呼保義");
        map.put("盧俊義","玉麒麟");
        map.put("吳用","智多星");
        map.put("公孫勝","入雲龍");
        map.put("關勝","大刀");
        // 調用map集合的forEach(BiConsumer<? super K, ? super V> action)方法
        // 該方法需要一個消費型函數式接口BiConsumer<T, U>的實現類對象
        map.forEach(new BiConsumer<String, String>() {
            // 實現accept(T t, U u)方法
            @Override
            public void accept(String k, String v) {
                System.out.println(k + "的綽號是" + v);
            }
        });
    }
}

在這一示例中,通過匿名內部類實現了消費型函數式接口BiConsumer<T, U>,具體實現的消費行爲是將accept(String k, String v)方法接收的參數,即遍歷map集合所獲取的一對一對的鍵值映射數據進行打印;如果要使用另外一種消費方式進行消費,比如打印這些鍵值映射數據的字符長度,按照本例的編碼風格,改造如下:

package com.codeke.java.test;

import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;

public class Test {
    public static void main(String[] args) {
        // 聲明Map類型的變量map,並賦值爲一個HashMap實例
        Map<String, String> map = new HashMap<>();
        // 向map集合中添加鍵值映射數據
        map.put("宋江","呼保義");
        map.put("盧俊義","玉麒麟");
        map.put("吳用","智多星");
        map.put("公孫勝","入雲龍");
        map.put("關勝","大刀");
        // 調用map集合的forEach(BiConsumer<? super K, ? super V> action)方法
        // 該方法需要一個消費型函數式接口BiConsumer<T, U>的實現類對象
        map.forEach(new BiConsumer<String, String>() {
            // 實現accept(T t, U u)方法
            @Override
            public void accept(String k, String v) {
                System.out.printf("%s,名字的長度是:%s,綽號長度是:%s\n",
                        k, k.length(), v.length());
            }
        });
    }
}

觀察並比較上文兩段源碼最主要的部分:
在這裏插入圖片描述
不難發現,不論使用什麼樣的行爲來消費數據,只有被綠色下劃線標示出的代碼(形參名稱方法具體實現代碼)纔有可能發生變化,這一部分代碼也正是需要被關注得、具體消費行爲;而被黃色下劃線標示出的代碼是沒有變化的,這一部分代碼或者是固定不變的,或者是編譯器可以通過上下文進行推斷的,這一部分代碼也被稱之爲冗餘的模板代碼

既然冗餘的模板代碼不會隨着消費行爲的變化而變化,那麼如果可以省略它們,勢必代碼會變得簡潔,也更專注於具體行爲Lambda表達式便讓這一設想成爲現實。

下面使用Lambda表達式重構上例中的兩段源碼,初步體驗Lambda表達式
下面是一個示例:

package com.codeke.java.test;

import java.util.HashMap;
import java.util.Map;

public class Test {
    public static void main(String[] args) {
        // 聲明Map類型的變量map,並賦值爲一個HashMap實例
        Map<String, String> map = new HashMap<>();
        // 向map集合中添加鍵值映射數據
        map.put("宋江","呼保義");
        map.put("盧俊義","玉麒麟");
        map.put("吳用","智多星");
        map.put("公孫勝","入雲龍");
        map.put("關勝","大刀");
        // 調用map集合的forEach(BiConsumer<? super K, ? super V> action)方法
        // 使用Lambda表達式作爲方法的參數
        // 第一種消費行爲
        map.forEach((k, v) -> System.out.println(k + "的綽號是" + v));
        // 第二種消費行爲
        map.forEach((k, v) -> System.out.printf("%s,名字的長度是:%s,綽號長度是:%s\n",
                k, k.length(), v.length()));
    }
}

執行輸出結果:

公孫勝的綽號是入雲龍
盧俊義的綽號是玉麒麟
關勝的綽號是大刀
吳用的綽號是智多星
宋江的綽號是呼保義
公孫勝,名字的長度是:3,綽號長度是:3
盧俊義,名字的長度是:3,綽號長度是:3
關勝,名字的長度是:2,綽號長度是:2
吳用,名字的長度是:2,綽號長度是:3
宋江,名字的長度是:2,綽號長度是:3

說明:

  • 本例中的代碼(k, v) -> System.out.println(k + "的綽號是" + v)和代碼(k, v) -> System.out.printf("%s,名字的長度是:%s,綽號長度是:%s\n", k, k.length(), v.length())即是Lambda表達式。
  • 相對於前例中使用的匿名內部類,Lambda表達式使代碼中冗餘的模板代碼消失了,代碼變的簡潔,清晰,重點突出,最重要的是,將具體的行爲當作了方法的參數。
  • 需要注意的是,Lambda表達式並非匿名內部類簡化寫法,其編譯過程實現機制等均與匿名內部類不同

3、進一步瞭解Lambda表達式

3.1、Lambda表達式的語法

Lambda表達式具體的語法格式如下:

(parameters) -> {expression 或 statements}

Lambda表達式的語法結構具有以下特徵:

  • 可以具有零個一個多個參數
  • 參數類型可選可以顯式聲明參數類型也可以編譯器自動從上下文推斷參數類型,如:
    (x, y) -> x – y;				// 省略參數類型
    (int x, int y) -> x + y			// 顯示聲明參數類型
    
  • 參數圓括號()可選,如果只有一個參數不聲明參數類型可以不使用圓括號(),但沒有參數、有多個參數顯式聲明參數類型需要使用圓括號(),如:
    x -> 2 * x;						// 只有一個參數,沒有聲明參數類型,此時可以不使用參數圓括號()
    (int x) -> 3 * x;				// 顯式聲明參數類型,需要使用參數圓括號()
    () -> 5;						// 沒有參數,需要使用參數圓括號()
    (x, y) -> x – y;				// 有一個以上參數,需要使用參數圓括號()   
    
  • 始終有符號->
  • 符號->後的部分爲主體,主體可以包含零條一條多條語句,如:
    x - > {};						// 不包含語句
    x - > {int y = x * 3; System.out.println(y);}			// 包含一條以上語句
    
  • 大括號{}可選,如果主體只包含一條語句可以不使用大括號{},如:
    (x, y) -> x – y;				// 省略大括號{}
    
  • 返回關鍵字return可選,在Lambda表達式需要返回值的情況下,如果主體只有一個表達式,則編譯器會自動返回表達式不需要使用return關鍵字如果使用大括號{},則需要使用return關鍵字指明返回值,如:
    (x, y) -> x – y;	// 如果該Lambda表達式對應的函數式接口所聲明的方法有返回值,則該Lambda表達式返回 x-y 的值
    (x, y) -> {return x - y;};			// 該Lambda表達式返回 x - y 的值
    x - > {int y = x * 3; return y;};	// 該Lambda表達式返回 y 的值
    
  • 還需注意,如果主體使用大括號{},則大括號{}內的每條語句後都需使用分號;結尾

下面是一個示例:

package com.codeke.java.test;

public class Test {
    public static void main(String[] args) {
        // 加法操作,顯式聲明參數類型,使用了(),沒有使用{}和return關鍵字
        MathOperation addition = (int a, int b) -> a + b;
        // 減法操作,不顯式聲明參數類型,使用了(),沒有使用{}和return關鍵字
        MathOperation subtraction = (a, b) -> a - b;
        // 乘法操作,不顯式聲明參數類型,使用了(),使用{}和return關鍵字
        MathOperation multiplication = (a, b) -> {
            return a * b;
        };
        // 除法運算,不顯式聲明參數類型,使用了(),使用{}和return關鍵字
        MathOperation division = (a, b) -> {
            return a / b;
        };

        // 使用Lambda
        System.out.println("8 + 2 = " + operate(8, 2, addition));
        System.out.println("8 - 4 = " + operate(8, 4, subtraction));
        System.out.println("8 x 3 = " + operate(8, 3, multiplication));
        System.out.println("8 / 4 = " + operate(8, 4, division));

        // 用英文打招呼的操作,不顯式聲明參數類型,不使用了()
        GreetingService greetService1 = message ->
                System.out.println("Hello " + message);

        // 用中文打招呼的操作,不顯式聲明參數類型,使用了()
        GreetingService greetService2 = (message) ->
                System.out.println("你好 " + message);

        greetService1.sayMessage("tom");
        greetService2.sayMessage("魯智深");

    }

    // 進行算數運算的方法,需要兩個int類型的操作數和一個操作行爲
    private static int operate(int a, int b, MathOperation mathOperation) {
        return mathOperation.operation(a, b);
    }
}

// 函數式接口,規範一種數學運算操作,
// 需要輸入兩個int類型的操作數,返回一個int類型的操作數
@FunctionalInterface
interface MathOperation {
    int operation(int a, int b);
}

// 函數式接口,規範一個打招呼的行爲
// 需要輸入一個字符串,無返回
@FunctionalInterface
interface GreetingService {
    void sayMessage(String message);
}

執行輸出結果:

8 - 4 = 4
8 x 3 = 24
8 / 4 = 2
Hello tom
你好 魯智深

說明:

  • 本例中聲明瞭兩個函數式接口,MathOperationGreetingService,並在此基礎上演示了Lambda表達式靈活的語法,具體代碼邏輯非常簡單,這裏不再贅述。

3.2、綜合使用Lambda表達式和JDK中的函數式接口

下面結合上文中使用過的部分示例,綜合使用Lambda表達式JDK中的函數式接口,進一步理解和鞏固Lambda表達式的使用。

Lambda表達式和JDK中的消費型函數式接口的綜合使用在初步體驗Lambda表達式時已經舉例,這裏不再舉例。

Lambda表達式和JDK中的生產型函數式接口綜合使用,下面是一個示例:

package com.codeke.java.test;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.IntSupplier;

public class Test {
    public static void main(String[] args) {
        // 獲取一個集合list,並添加了若干個字符串形式的人名作爲集合元素
        List<String> list = Arrays.asList("林沖","秦明","呼延灼","花榮","柴進");
        // 調用printName(List<String> namelist, IntSupplier indexSupplier)打印一個人名
        // 用Lambda表達式替換前文該例中使用的匿名內部類
        printName(list, () -> new Random().nextInt(list.size()));
    }
    /**
     * 打印名字的方法
     * @param namelist 名字集合
     * @param indexSupplier 下標生產者
     */
    private static void printName(List<String> namelist, IntSupplier indexSupplier){
        int index = indexSupplier.getAsInt();
        System.out.println(namelist.get(index));
    }
}

執行輸出結果不再贅述。

Lambda表達式和JDK中的功能型函數式接口綜合使用,下面是一個示例:

package com.codeke.java.test;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class Test {
    public static void main(String[] args) {
        // 總成績單
        Map<String, Integer> scoreMap = new HashMap<>();
        scoreMap.put("魯智深", 67);
        scoreMap.put("吳用", 90);
        scoreMap.put("武松", 72);
        scoreMap.put("林沖", 83);
        // 補充成績單
        Map<String, Integer> additionalScoreMap = new HashMap<>();
        additionalScoreMap.put("公孫勝", 88);
        additionalScoreMap.put("花榮", 79);
        additionalScoreMap.put("武松", 75);
        // 將補充成績單彙總到總成績單,涉及兩個方面
        // 1.總成績單中已有的學員根據補充成績單調整成績
        // 2.補充成績單中新增的學員及成績添加到總成績單中
        Set<Map.Entry<String, Integer>> entries = additionalScoreMap.entrySet();
        for (Map.Entry<String, Integer> entry : entries) {
            String name = entry.getKey();
            Integer score = entry.getValue();
            // 用Lambda表達式替換前文該例中使用的匿名內部類
            scoreMap.computeIfPresent(name, (s, i) -> {
                System.out.printf("之前%s的成績爲%d,現在調整爲%d\n", s, i, score);
                return score;
            });
            // 用Lambda表達式替換前文該例中使用的匿名內部類
            scoreMap.computeIfAbsent(name, s -> {
                System.out.printf("之前沒有%s的成績,現在補錄\n", s);
                return score;
            });
        }
        // 打印彙總後的成績單
        // 用Lambda表達式替換前文該例中使用的匿名內部類
        scoreMap.forEach((s, i) -> System.out.printf("%s的成績爲%d\n", s, i));
    }
}

執行輸出結果不再贅述。

Lambda表達式和JDK中的操作型函數式接口綜合使用,下面是一個示例:

package com.codeke.java.test;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;

public class Test {
    public static void main(String[] args) {
        // 本金1000元,複利率爲10%,逐年計算10年內每年的本金與利息
        // 聲明並實例化長度爲10的BigDecimal類型數組
        BigDecimal[] nums = new BigDecimal[10];
        // 數組的第一個元素賦值爲本金
        nums[0] = new BigDecimal("1000");
        // 數組其他元素賦值爲年利率
        Arrays.fill(nums, 1, 10, new BigDecimal("0.1"));
        // 藉助Arrays類的parallelPrefix(T[] array, BinaryOperator<T> op)方法完成逐年複利計算
        // 計算操作由操作型函數式接口BinaryOperator<T>的實現類具體實現
        // 用Lambda表達式替換前文該例中使用的匿名內部類
        Arrays.parallelPrefix(nums, (left, right) -> left.multiply(right).add(left)
                .setScale(2, RoundingMode.HALF_UP));
        // 逐年打印
        for (int i = 0; i < nums.length; i++) {
            System.out.printf("第%d年時利息加本金共:%s\n", (i + 1), nums[i]);
        }
    }
}

執行輸出結果不再贅述。

Lambda表達式和JDK中的斷言型函數式接口綜合使用,下面是一個示例:

package com.codeke.java.test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        // 聲明並實例化一個ArrayList集合list
        List<String> list = new ArrayList<>();
        // 添加元素
        list.add("魯智深");
        list.add("武松");
        list.add("林沖");
        list.add("吳用");
        list.add("公孫勝");
        // 刪除元素"林沖"及"吳用"
        // 用Lambda表達式替換前文該例中使用的匿名內部類
        list.removeIf(s -> "林沖".equals(s) || "吳用".equals(s));
        // 打印集合元素
        System.out.println(Arrays.toString(list.toArray()));
    }
}

執行輸出結果不再贅述。

結合這些例子,更容易看出,比起使用匿名內部類,用Lambda表達式配合函數式接口來使用,會使相同功能的代碼得到大幅度簡化,並且代碼也更易讀易懂易維護。對於開發人員來說,熟練使用Lambda表達式,會使開發效率得到提高,也更容易寫出簡介清晰優雅代碼

3.3、Lambda表達式與其外部的局部變量

需要注意的是,Lambda表達式可以引用外層局部變量,但是只限於標記final外層局部變量,或事實上final外層局部變量沒有被後面代碼修改,即隱性具有final語義)。

下面是一個示例:

package com.codeke.java.test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        // 申明並初始化一個循環變量
        int index = 1;
        // 將若干名字封裝成一個java.util.ArrayList集合
        List<String> names = new ArrayList<>(Arrays.asList("宋江", "盧俊義", "吳用", "公孫勝"));
        // 使用集合的forEach(Consumer<T> action)方法
        // 使用Lambda表達式輸出每個名字,希望連同循環變量一同輸出
        names.forEach(name -> {
            System.out.println(index + " " + name);
            // index +=1;  // Variable used in lambda expression should be final or effectively final
        });
    }
}

說明:

  • 本例中的代碼在循環輸出集合names中的名字時,希望連同序號(將循環變量index作爲序號)一同輸出。故而希望在實現輸出的Lambda表達式中對外部變量index進行自增操作。然而編譯器會提示一個錯誤Variable used in lambda expression should be final or effectively final,既要麼外部變量index應該被final修飾,要麼變量index在Lambda表達式中不會被修改。

下面是另一個示例:

package com.codeke.java.test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        // 申明並初始化一個只有一個元素的數組,用來存放循環變量
        final int[] index = {1};
        // 將若干名字封裝成一個java.util.ArrayList集合
        List<String> names = new ArrayList<>(Arrays.asList("宋江", "盧俊義", "吳用", "公孫勝"));
        // 使用集合的forEach(Consumer<T> action)方法
        // 使用Lambda表達式輸出每個名字,希望連同循環變量一同輸出
        names.forEach(name -> {
            System.out.println(index[0] + " " + name);
            index[0] += 1;  // 數組index是引用類型,修改數組中某一元素的值時,數組本身的地址沒有發生變化,
        });
    }
}

執行輸出結果:

1 宋江
2 盧俊義
3 吳用
4 公孫勝

說明:

  • 本例的意圖和上例一樣。不同的是,本例使用了一個只有一個元素的int類型數組index來存放循環變量。數組是引用類型,改變數組中某一元素的值並不會影響數組本身的地址。故可以在Lambda表達式中對數組index的第一個元素進行自增,從而實現本例的意圖。

3.4、Lambda表達式中的this關鍵字

前文中提到,Lambda表達式並非匿名內部類簡化寫法,其編譯過程實現機制等均與匿名內部類不同。事實上,Lambda表達式不會被編譯爲對應功能的匿名內部類,在Lambda表達式中使用this關鍵字時,便能體會到這一點。

下面是一個示例:

package com.codeke.java.test;

import java.util.function.Supplier;

public class Test {

    // 主方法
    public static void main(String[] args) {
        Test test = new Test();
        test.testThis();
    }

    /**
     * 測試方法,用來測試匿名內部類和Lambda表達式中的this有何區別
     */
    private void testThis() {
        // 使用匿名內部類實現一個生產者supplier1
        Supplier<String> supplier1 = new Supplier<String>() {
            @Override
            public String get() {
                // 返回this引用的對象的類名稱
                return this.getClass().getName();
            }
        };
        // 將生產者提供的字符串打印在控制檯上
        System.out.println(supplier1.get());

        // 使用Lambda表達式聲明一個生產者supplier2
        Supplier<String> supplier2 = () -> this.getClass().getName();
        // 將生產者提供的字符串打印在控制檯上
        System.out.println(supplier2.get());
    }
}

執行輸出結果:

com.codeke.java.test.Test$1
com.codeke.java.test.Test

說明:

  • 本例的輸出結果中,類名com.codeke.java.test.Test$1中的Test$1是編譯器爲測試類Test中的匿名內部類自動起的名字,而類名com.codeke.java.test.Test即是測試類Test的完全限定名。
  • 通過比較輸出結果,不難發現,Lambda表達式中this的是聲明它的類當前對象,而匿名內部類this的是匿名內部類當前對象

4、關於函數式編程思想的理解

在學習了Java中的函數式接口、Lambda表達式的基礎知識,並在觀察、體會了若干使用這些特性的代碼示例的基礎上,可以初步理解函數式編程思想。大體上的理解,函數就是有輸入量輸出量一套計算方案,也就是“拿什麼東西做什麼事情” 。相對而言,面向對過分強調必須通過對象的形式來做事情”,而函數式思想儘量忽略面向對象的複雜語法——強調做什麼而不是以什麼形式做

粗略的總結爲:

  • 面向對象的思想:做一件事情找一個能解決這個事情的對象調用對象的方法完成事情
  • 函數式編程思想:做一件事情誰去做的並不重要重要的是怎麼做獲得什麼結果不重視形式

三、方法引用

1、什麼是方法引用

方法引用也是JDK8中新增非常有趣的特性之一。方法引用可以認爲是某些特定場景Lambda表達式另一種表現形式

在一些特定場景下,Lambda表達式僅僅調用一個已經存在的方法,而沒有其他多餘動作,在這種情況下,可以使用方法引用簡寫Lambda表達式,使代碼更簡潔易於理解

注意方法引用仍然一個Lambda表達式方法引用操作符雙冒號::

2、方法引用的分類

方法引用具體可以分爲四種形式:即靜態方法引用實例方法引用對象方法引用構造方法引用。這四種形式的方法引用可以簡化的對應Lambda表達式如下表:

類型 語法 對應Lambda表達式
靜態方法引用 Class::static_method (args ...) -> Class.static_method(args ...)
特定對象的實例方法引用 instance::method (args ...) -> instance.method(args ...)
特定類任意對象的實例方法引用 Class::method (instance, args ...) -> instance.method(args ...)
構造方法引用 Class::new (args ...) -> new Class(args ...)

3、方法引用應用舉例

3.1、靜態方法引用

靜態方法引用,下面是一個示例:

package com.codeke.java.test;

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

public class Test {
    public static void main(String[] args) {
        List<Integer> nums = Arrays.asList(13, 4, 31, 5, 19, 2, 25);
        // 對集合中的元素進行排序
        // 使用匿名內部類
        // nums.sort(new Comparator<Integer>() {
        //     @Override
        //     public int compare(Integer num1, Integer num2) {
        //         return Integer.compare(num1, num2);
        //     }
        // });
        // 使用Lambda表達式
        // nums.sort((num1, num2) -> Integer.compare(num1, num2));
        // 靜態方法引用,相當於(num1, num2) -> Integer.compare(num1, num2)
        nums.sort(Integer::compare);
        System.out.println(nums.toString());
    }
}

執行輸出結果:

[2, 4, 5, 13, 19, 25, 31]

說明:

  • 本例中,nums是一個實現了java.util.List接口的集合,可以調用java.util.List集合的方法sort(Comparator c)對其中的元素進行排序。
  • java.util.List集合的方法sort(Comparator c)需要一個實現了java.util.Comparator接口的比較器作爲參數,而java.util.Comparator接口是一個函數式接口;另外,集合nums中存放的是Integer類型的整數,故可以使用java.lang.Integer類的靜態方法compare(int x, int y)來實現比較邏輯。綜上,可以使用Lambda表達式完成比較,代碼如下:
    nums.sort((num1, num2) -> Integer.compare(num1, num2));
    
  • 該Lambda表達式僅僅是調用了java.lang.Integer類的靜態方法compare(int x, int y),需要注意的是,調用compare(int x, int y)方法時的實際參數即是Lambda表達式中傳入的參數num1num2。該Lambda表達式可以使用靜態方法引用簡寫,如下:
    nums.sort(Integer::compare);
    

3.2、特定對象的實例方法引用

特定對象的實例方法引用,下面是一個示例:

package com.codeke.java.test;

import java.io.PrintStream;
import java.util.Arrays;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("宋江", "盧俊義", "公孫勝", "吳用", "關勝", "林沖");
        // 聲明並獲得控制檯輸出對象out
        PrintStream out = System.out;
        // 遍歷輸出集合names中的元素
        // 使用Lambda表達式
        // names.forEach(name -> out.println(name));
        // 特定對象的實例方法引用
        names.forEach(out::println);
    }
}

執行輸出結果:

宋江
盧俊義
公孫勝
吳用
關勝
林沖
  • 本例非常簡單,意圖藉助java.util.List集合的方法forEach(Consumer action)遍歷打印集合中的元素。爲了清晰起見,代碼中聲明並獲得控制檯輸出對象out,它是一個java.io.PrintStream類的實例,println(String x)方法即是java.io.PrintStream類中的一個實例方法。如果使用Lambda表達式完成本例,代碼如下:
    names.forEach(name -> out.println(name));
    
  • 該Lambda表達式僅僅是調用了out對象的實例方法println(String x),需要注意的是,調用println(String x)方法時的實際參數即是Lambda表達式中傳入的參數name。該Lambda表達式可以使用特定對象的實例方法引用簡寫,如下:
    names.forEach(out::println);
    

3.3、特定類任意對象的實例方法引用

特定類任意對象的實例方法引用,下面是一個示例:

package com.codeke.java.test;

import java.util.*;

public class Test {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>(Arrays.asList("宋江", "", "公孫勝", "", "關勝", "林沖"));
        // 刪除集合中的空字符串
        // 使用Lambda表達式
        // names.removeIf(name -> name.isEmpty());
        // 特定類任意對象的實例方法引用
        names.removeIf(String::isEmpty);
        System.out.println(names);
    }
}

執行輸出結果:

[宋江, 公孫勝, 關勝, 林沖]

說明:

  • 本例中,names是一個java.util.ArrayList集合的實例,用來存放若干個人名,但其中部分元素是空字符串,需要將這些空字符串從集合中刪除掉,爲了達到這一目的,可以調用java.util.Collection接口中的默認方法removeIf(Predicate filter)刪除集合中滿足特定條件的元素。
  • removeIf(Predicate filter)方法需要的參數是java.util.function.Predicate接口類型的,而java.util.function.Predicate接口是一個函數式接口;另外,判斷字符串是否爲空字符串可以使用字符串的實例方法isEmpty()。綜上,可以使用Lambda表達式完成本例,代碼如下:
    names.removeIf(name -> name.isEmpty());
    
  • 觀察該Lambda表達式,僅僅調用了字符串的實例方法isEmpty(),而方法的調用者爲Lambda表達式傳入的第一個參數。該Lambda表達式可以用特定類任意對象的實例方法引用來簡寫,如下:
    names.removeIf(String::isEmpty);
    

下面是另一個示例:

package com.codeke.java.test;

import java.util.*;

public class Test {
    public static void main(String[] args) {
        List<Integer> nums = Arrays.asList(13, 4, 31, 5, 19, 2, 25);
        // 對集合中的元素進行排序
        // 使用匿名內部類
        // nums.sort(new Comparator<Integer>() {
        //     @Override
        //     public int compare(Integer num1, Integer num2) {
        //         return num1.compareTo(num2);
        //     }
        // });
        // 使用Lambda表達式
        //  nums.sort((num1, num2) -> num1.compareTo(num2));
        // 特定類任意對象的實例方法引用
        nums.sort(Integer::compareTo);
        System.out.println(nums.toString());
    }
}

執行輸出結果:

[2, 4, 5, 13, 19, 25, 31]

說明:

  • 本例和介紹靜態方法引用時使用的示例非常相似,不同的是,本例實現比較邏輯時沒有使用java.lang.Integer類的靜態方法compare(int x, int y),而是使用java.lang.Integer類的實例方法compareTo(Integer anotherInteger)。Lambda表達式如下:
    nums.sort((num1, num2) -> num1.compareTo(num2));
    
  • 觀察該Lambda表達式,僅僅調用了java.lang.Integer類的實例方法compareTo(Integer anotherInteger),調用時,調用者是Lambda表達式傳入的第一個參數num1,方法的實際參數是Lambda表達式傳入的第二個參數num2。此時,可以使用特定類任意對象的實例方法引用來簡寫該Lambda表達式,如下:
    nums.sort(Integer::compareTo);
    
  • 注意,特定對象的實例方法引用特定類任意對象的實例方法引用區別就在於,Lambda表達式傳入的第一個參數是作爲實例方法實際參數,還是作爲實例方法調用者

3.4、構造方法引用

構造方法引用,下面是一個示例:
Person類的源碼:

package com.codeke.java.test;

public class Person {
    // 名稱
    private String name;
    // 構造方法
    public Person(String name) {
        this.name = name;
    }
    // toString()方法
    @Override
    public String toString() {
        return "Person{" + "name='" + name + '\'' + '}';
    }
}

Test類的源碼:

package com.codeke.java.test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;

public class Test {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("宋江", "盧俊義", "公孫勝", "吳用", "關勝", "林沖");
        // 將存放字符串(人名)的集合轉換成存放Person類對象的集合
        // Lambda表達式
        // List<Person> persons = convertList(names, name -> new Person(name));
        // 構造方法引用
        List<Person> persons = convertList(names, Person::new);
        persons.forEach(System.out::println);
    }
    // 將存放T類型數據的集合按function給定的方式轉換爲存放R類型數據的集合
    private static <T, R> List<R> convertList(List<T> list, Function<T, R> function) {
        List<R> result = new ArrayList<>();
        list.forEach(t -> result.add(function.apply(t)));
        return result;
    }
}

執行輸出結果:

Person{name='宋江'}
Person{name='盧俊義'}
Person{name='公孫勝'}
Person{name='吳用'}
Person{name='關勝'}
Person{name='林沖'}

說明:

  • 本例的Test類中有一個泛型方法convertList(List<T> list, Function<T, R> function),該方法有兩個參數,第一個參數是一個存放T類型數據的集合,第二個參數是一個功能型函數式接口類型的變量function,方法的返回類型爲List<R>。該方法可以按參數function給定的規則,根據存放T類型數據的集合生成一個存放R類型數據的集合並返回。
  • 再觀察本例Test類中的main方法,集合names存放了若干字符串類型的人名,需要以這些字符串類型的人名作爲Person類的name屬性來實例化Person類對象,並獲得存放這些Person類對象的集合persons。藉助convertList(List<T> list, Function<T, R> function)方法,用Lambda表達式完成這一功能,代碼如下:
    List<Person> persons = convertList(names, name -> new Person(name));
    
  • 觀察該Lambda表達式,僅僅是調用了Person類的含參構造方法,調用時方法的實際參數即是Lambda表達式中傳入的參數name。該Lambda表達式可以使用構造方法引用簡寫,如下:
    List<Person> persons = convertList(names, Person::new);
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章