java基礎總結(六十八)--Java8中的函數式編程

目錄

1.Lambda表達式與函數式接口

1.Lambda表達式簡介

2.Lambda表達式的用法

3.函數式接口簡介

4.默認方法與靜態方法

5.四大常用的函數式接口

2.方法引用

1.構造器引用

2.靜態方法引用

3.任意對象的方法引用

4.特定對象的方法引用

3.Optional

1.Optional的三種構造方式

2.Optional的錯誤使用

3.Optional的常見用法

4.Stream

1.Stream簡介

2.常用惰性求值方法

3.常用及早求值方法

5.Collector

1.轉換成其他集合

2.轉換成值

3.數據分塊

4.數據分組

5.字符串

6.組合收集器

7.定製收集器


 

作者:劉仁鵬
參考資料:

  1. 使用 Java8 Optional 的正確姿勢
  2. 《Java8函數式編程》Richard Warburton 著 王羣峯 譯

1.Lambda表達式與函數式接口

1.Lambda表達式簡介

  • Lambda表達式的作用:使得Java可以把 函數 像對象一樣作爲語言的 一等公民 來對待。從而:
    1. 能夠編寫出 可讀性更強、更加緊湊、抽象級別更高的代碼
    2. 易於編寫出可在多核CPU上高效運行、線程安全 的代碼
    3. 在編寫 回調函數 和事件處理程序時,可以擺脫匿名內部類的冗繁
    4. ...
  • Java8的lambda表達式其實是 匿名類語法糖 ,本質上仍然是對象。
  • 函數成爲 一等公民 的含義:可以將函數 賦值 給變量,可以在 參數 中傳遞函數,可以讓一個方法的 返回值 是函數...
    即,凡是對象可以出現的地方,函數 都可以出現。

2.Lambda表達式的用法

  • 一個lambda表達式可由 用逗號分隔的參數列表->符號函數體三部分組成,例如:

 

Arrays.asList("a", "b", "c").forEach((String item) -> System.out.println(item));
  • 因爲 參數 item的 類型 可由編譯器 推測 出來,因此也可簡寫成:

 

Arrays.asList("a", "b", "c").forEach(item -> System.out.println(item));
  • 上面的兩種寫法,等價於在Java8之前的版本中,使用 匿名類 的實現:

 

Arrays.asList("a", "b", "c").forEach(
        new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        }
);
  • 如果lambda的函數體 非單一語句 ,需要把函數體放在 一對花括號 中:

 

Arrays.asList("a", "b", "c").forEach((String item) -> {
    System.out.println(item);
    System.out.println(item);
});
  • lambda可以使用外部作用域的 常量 (如果是變量而非常量,會 隱式地 轉爲常量,建議顯式地聲明爲常量):

 

public class Test {

    private int num = 1;

    public static void main(String[] args) {
        String separator = ",";
        Test test = new Test();
        Arrays.asList("a", "b", "c").forEach(
                item -> System.out.println(item + separator + test.num));
        //separator = ";"; 
        //編譯期錯誤 Variable used in lambda expression should be final or effectively final
        //test = new Test(); 
        //編譯期錯誤 Variable used in lambda expression should be final or effectively final
    }

}
  • lambda有可能會返回一個值。返回值 的類型也可由編譯器 推斷 出來。如果lambda的 函數體只有一行 的話,可以不顯式使用 return語句,下面兩個代碼是等價的:

 

Arrays.asList("a", "b", "c").sort((s1, s2) -> s1.compareTo(s2));

Arrays.asList("a", "b", "c").sort((s1, s2) -> {
    return s1.compareTo(s2);
});

3.函數式接口簡介

  • 所有的lambda表達式都可以替換成匿名類的實現方式,但並非所有的匿名類都能用lambda來表示。那麼什麼樣的匿名類一定能用lambda來表示呢?那就是 實現於只有一個abstract方法的接口的匿名類 (不算Object中的方法,以及static方法和default方法)。而這種特殊的接口,在Java8中稱爲 函數式接口 ,用註解 @FunctionalInterface 表示:

 

@FunctionalInterface
public interface TestFunctional{
    void method();
}
  • 但爲什麼只有這樣的接口,才能和lambda配合使用呢?其實不難理解。因爲lambda本質是 沒有顯式重寫方法省略了重寫方法名 的匿名類,而一個lambda只能對一個方法進行重寫。
  • Java中所有 lambda表達式類型 都是某個具體的函數式接口
  • 一個 函數式接口 即使不加 @FunctionalInterface 註解,也可以與lambda配合使用,但這樣的函數式接口是 容易出錯 的:如有某個人在接口定義中增加了另一個方法,這時,這個接口就不再是函數式的了,並且編譯過程也會失敗。爲了克服函數式接口的這種 脆弱性 並且能夠 明確聲明 接口作爲函數式接口的意圖,建議顯式使用該註解

4.默認方法與靜態方法

  • Java 8用 默認方法靜態方法 這兩個新概念來擴展接口的聲明。
  • 默認方法 使用 default 關鍵字定義,默認方法與抽象方法不同,不需要被實現類來具體實現,但是可以被實現類繼承或重寫。默認方法的出現使Java可以 在擴展接口功能的同時保證向後兼容 (只要是Java1到Java7寫出的代碼,在Java8中依然可以編譯通過)。例如Collection中的stream方法,如果Java8沒有對默認方法的支持,那麼所有的子類都需要對stream方法提供實現,顯然無法保證向後兼容。
  • 靜態方法 使用 static 關鍵字定義,與一般java類中的靜態方法一樣。靜態方法的出現是爲了 讓工具方法和相關的類或接口放在一起,而不是放到另一個工具類中。例如下面的代碼,如果Java8沒有對靜態方法的支持,那麼ofAll方法必須放到另外的工具類Rulers中,顯然不如直接放到Ruler中友好。
  • 示例:

 

@FunctionalInterface
public interface Ruler<T> {

    /**
     * 校驗T
     */
    void check(T checkTarget);

    /**
     * 或操作
     */
    @SuppressWarnings("unchecked")
    default Ruler<T> or(Ruler<T>... rulers) {
        return checkTarget -> {
            try {
                check(checkTarget);
            } catch (CheckException e) {
                ofAll(rulers).check(checkTarget);
            }
        };
    }

    /**
     * Ruler整合
     */
    @SafeVarargs
    static <T> Ruler<T> ofAll(Ruler<T>... rulers) {
        return (checkTarget -> Arrays.stream(rulers).forEach(ruler -> ruler.check(checkTarget)));
    }

}

5.四大常用的函數式接口

  • 消費型接口 Consumer<T>

 

/**
 * @name 消費型接口
 * @use Consumer<T>
 * @param T 傳入參數
 * @fun 接受一個參數 無返回值
 * */
Consumer<String> con = (str) -> System.out.println(str);
con.accept("我是消費型接口!");
//輸出:我是消費型接口!
  • 供給型接口 Supplier<R>

 

/**
 * @name 供給型接口
 * @use Supplier<R>
 * @param R 返回值類型
 * @fun 無參數 有返回值
 * */
Supplier<Date> supp = () -> new Date();
Date date = supp.get();
System.out.println("當前時間:" + date);
//輸出:當前時間:Wed Jul 04 08:05:10 CST 2018
  • 函數型接口 Function<T,R>

 

/**
 * @name 函數型接口
 * @use Function<T,R>
 * @param T 傳入參數
 * @return R 返回值類型
 * @fun 接受一個參數 有返回值
 * */
Function<String, String> fun = (str) -> "hello," + str;
String str = fun.apply("tom");
System.out.println(str);
//輸出:hello,tom
  • 斷定型接口 Predicate<T>

 

/**
 * @name 斷定型接口
 * @use Predicate<T>
 * @param T 傳入參數
 * @return Boolean 返回一個Boolean型值
 * @fun 接受一個參數 返回Boolean型值
 * */
Predicate<Integer> pre = (num) -> num > 0;
Boolean flag = pre.test(10);
System.out.println(flag);
//輸出:true
  • 還有更多功能豐富的函數式接口,可自行了解。

2.方法引用

類型 示例
構造器引用 Class::new
靜態方法引用 Class::static_method
任意對象的方法引用 instance::method
特定對象的方法引用 Class::method
  • 下面,我們以定義了4個方法的Car這個類爲例子,區分Java8中支持的4種不同的方法引用。

 

public static class Car {
    public static Car create(final Supplier<Car> supplier) {
        return supplier.get();
    }

    public static void collide(final Car car) {
        System.out.println("Collided " + car.toString());
    }

    public void follow(final Car another) {
        System.out.println("Following the " + another.toString());
    }

    public void repair() {
        System.out.println("Repaired " + this.toString());
    } 
}

1.構造器引用

  • 第一種方法引用是構造器引用,它的語法是Class::new,或者更一般的Class<T>::new。請注意構造器 沒有參數

 

final Car car1 = Car.create(() -> new Car()); //lambda表達式
final Car car2 = Car.create(Car::new); //方法引用

2.靜態方法引用

  • 第二種方法引用是靜態方法引用,它的語法是Class::static_method。請注意這個方法 接受一個Car類型的參數

 

final List<Car> cars = Arrays.asList(car1, car2);
cars.forEach((Car it) -> Car.collide(it)); //lambda表達式
cars.forEach(Car::collide); //方法引用

3.任意對象的方法引用

  • 第三種方法引用是特定類的任意對象的方法引用,它的語法是Class::method。請注意,這個方法 沒有參數

 

cars.forEach((Car it) -> it.repair()); //lambda表達式
cars.forEach(Car::repair); //方法引用

4.特定對象的方法引用

  • 第四種方法引用是特定對象的方法引用,它的語法是instance::method。請注意,這個方法 接受一個Car類型的參數

 

final Car police = new Car();
cars.forEach((Car it) -> police.follow(it)); //lambda表達式
cars.forEach(police::follow); //方法引用

3.Optional

  • Java8引入 Optional 來通過一系列的 鏈式調用優雅地 解決 null安全問題

1.Optional的三種構造方式

  • Optional.of(obj) :它要求傳入的obj不能是null值, 否則會拋出NPE。
  • Optional.empty() :返回一個持有null的Optional實例。
  • Optional.ofNullable(obj) :它以一種智能的,寬容的方式來構造一個Optional實例。傳null進到就得到Optional.empty(),非null就調用Optional.of(obj)。

2.Optional的錯誤使用

  1. 使用 isPresent() 方法:
    isPresent() 與 obj != null 沒有任何分別,並不會使代碼變得優雅

  2. 使用 get() 方法:
    沒有 isPresent() 作鋪墊的 get() 調用在IDEA中會收到警告

    Reports calls to java.util.Optional.get() without first checking with a isPresent() call if a value is available. If the Optional does not contain a value, get() will throw an exception
    調用 Optional.get() 前不事先用 isPresent() 檢查值是否可用. 假如 Optional 不包含一個值, get() 將會拋出一個異常

  3. Optional 類型作爲 類/實例屬性方法參數 時:
    把 Optional 類型用作屬性或是方法參數在 IntelliJ IDEA 中是強力不推薦的

    Reports any uses of java.util.Optional<T>, java.util.OptionalDouble, java.util.OptionalInt, java.util.OptionalLong or com.google.common.base.Optional as the type for a field or a parameter. Optional was designed to provide a limited mechanism for library method return types where there needed to be a clear way to represent “no result”. Using a field with type java.util.Optional is also problematic if the class needs to be Serializable, which java.util.Optional is not.
    使用任何像 Optional 的類型作爲字段或方法參數都是不可取的. Optional 只設計爲類庫方法的, 可明確表示可能無值情況下的 返回類型 . Optional 類型不可被序列化, 用作字段類型會出問題的

  4. 錯誤示例:

 

//不要這麼寫:
Optional<User> userOpt = selectUserById(id);
if (userOpt.isPresent()) {
    return userOpt.get().getName();
} else {
    return "none";
}

//這其實與我們以前不使用Optional時的代碼沒有任何區別:
User user = getUserById(id);
if (user != null) {
    return user.getName();
} else {
    return "none";
}

//正確的寫法:
return selectUserById(id).map(User::getName).orElse("none");

3.Optional的常見用法

  • 存在即返回,無則提供默認值
    public T orElse(T other)

 

return opt.orElse(null);
//而不是 return opt.isPresent() ? opt.get() : null;
  • 存在即返回, 無則由函數來產生
    public T orElseGet(Supplier<? extends T> other)

 

return user.orElseGet(() -> fetchAUserFromDatabase()); 
//而不要 return user.isPresent() ? user: fetchAUserFromDatabase();
  • 存在纔對它做點什麼
    public void ifPresent(Consumer<? super T> consumer)

 

user.ifPresent(System.out::println);
 
//而不要
if (user.isPresent()) {
  System.out.println(user.get());
}
  • Optional最重要的用法
    public<U> Optional<U> map(Function<? super T, ? extends U> mapper)
    map方法通常會搭配 orElse orElseGet orElseThrow 方法 使用:

 

return selectUserById(id)
        .map(u -> u.getUsername())
        .map(name -> name.toUpperCase())
        .orElse(null);
           
//在java8之前的版本,等價的寫法是這樣的:
User user = getUserById(id);
if (user != null) {
    String name = user.getUsername();
    if (name != null) {
        return name.toUpperCase();
    } else {
        return null;
    }
} else {
    return null;
}
  • flatMap filter 方法的使用與map類似,這裏就不展開了。

4.Stream

1.Stream簡介

  • Stream是用函數式編程方式,在 集合類 上進行復雜操作的工具:

 

public static void main(String[] args) {
    long count1 = Stream.of(1, 2, 3)
            .filter(it -> it > 2)
            .count();

    long count2 = 0;
    List<Integer> list = Arrays.asList(1, 2, 3);
    for (Integer it : list) {
        if (it > 2) {
            count2++;
        }
    }


    System.out.println("count1:" + count1 + "---count2:" + count2);
    //輸出:count1:1---count2:1
}
  • 上述代碼實現了一種“獲取集合中值大於2的元素個數”的功能。整個過程被 分解 爲兩種更簡單的操作:過濾計數。看似進行了兩次遍歷操作,事實上,類庫巧妙的設計使得只對集合進行了一次遍歷:Stream方法的返回值並不是一個新集合,而是一個Stream對象,是用來創建新集合的 配方(算法)。
  • 像filter這樣只 描述 Stream,而不產生新集合的方法叫做 惰性求值方法;而像count這樣用來從Stream中產生值的方法叫做 及早求值方法
  • 只有調用了 及早求值方法 ,Stream中之前注入的 惰性求值方法 纔會被真正的運行,否則,惰性求值方法 只會像 勢能 一樣,懸而不發:

 

// 勢能存儲
Stream<Integer> potentialEnergy = Stream.of(1, 2, 3)
        .filter(it -> {
            System.out.println("hello");
            return it > 2;
        });
System.out.println("world");

// 勢能釋放
long count = potentialEnergy.count();
System.out.println("count:" + count);

// 嘗試將勢能再次釋放(拋出異常)
long countAgain = potentialEnergy.count();
System.out.println("countAgain:" + countAgain);

//輸出:
//world 
//hello
//hello
//hello
//count:1
//java.lang.IllegalStateException: stream has already been operated upon or closed
  • 判斷一個方法是 惰性求值 的還是 及早求值 的很簡單:看它的 返回值。如果是 Stream 類型的就是惰性求值;否則就是及早求值。
  • 對Stream的使用方式是:形成一個惰性求值的 ,最後調用一個及早求值的方法返回想要的結果,或執行預期的操作。

爲方便演示,這裏定義了一個字段、兩個實體類、一個main方法。下文的示例將放到main方法中,會用到這部分代碼。

 

public class Test {

    public static void main(String[] args) {
        //示例代碼
    }

    private static List<Business> businessList = Arrays.asList(
            new Business(1L, "家政", Arrays.asList(
                    new Category(11L, "保潔"),
                    new Category(12L, "保姆")
            )),
            new Business(2L, "速運", Arrays.asList(
                    new Category(21L, "搬家"),
                    new Category(22L, "貨運")
            ))
    );

    @Data
    @AllArgsConstructor
    static class Business {
        private long id;
        private String name;
        private List<Category> categoryList;
    }

    @Data
    @AllArgsConstructor
    static class Category {
        private long id;
        private String name;
    }
}

2.常用惰性求值方法

  • map:一對一映射

 

businessList.stream()
        .map(Business::getName)
        .forEach(System.out::println);
//輸出:
//家政
//速運
  • flatMap:一對多映射

 

businessList.stream()
        .flatMap(business -> business.getCategoryList().stream())
        .forEach(System.out::println);
//輸出:
//Test.Category(id=11, name=保潔)
//Test.Category(id=12, name=保姆)
//Test.Category(id=21, name=搬家)
//Test.Category(id=22, name=貨運)
  • filter:過濾

 

businessList.stream()
        .filter(business -> business.getId() > 1)
        .forEach(System.out::println);
//輸出:
//Test.Business(id=2, name=速運, categoryList=[Test.Category(id=21, name=搬家), Test.Category(id=22, name=貨運)])

3.常用及早求值方法

  • forEach:對每個元素執行指定操作(演示略)
  • reduce:歸約

 

long sumOfCategoryId = businessList.stream()
        .flatMap(business -> business.getCategoryList().stream())
        .map(Category::getId)
        .reduce((x, y) -> x + y).orElse(0L);
System.out.println(sumOfCategoryId);
//輸出:66
  • collect:收集

 

List categoryIdList = businessList.stream()
        .flatMap(business -> business.getCategoryList().stream())
        .map(Category::getId)
        .collect(Collectors.toList());
System.out.println(categoryIdList);
//輸出:[11, 12, 21, 22]

5.Collector

  • Collector(收集器) 是一種通用的,從流生成複雜值的結構。
  • 標準類庫java.util.stream.Collectors,已經提供了一些常用的收集器
  • 如果標準類庫中的收集器無法滿足需求,也可 定製 一個收集器。
  • 接下來先對標準庫中的收集器做簡要介紹,然後再說明如何定製收集器。

1.轉換成其他集合

  • toList(),toSet(),toCollection()

 

//當希望使用集合對象/實現來收集值時,可以使用toCollection
TreeSet rst = businessList.stream()
        .map(Business::getName)
        .collect(Collectors.toCollection(TreeSet::new));
System.out.println(rst);
//輸出:[家政, 速運]

2.轉換成值

  • 按某種特定順序生成一個值:maxBy(),minBy()

 

//獲取id值最大的業務線
Optional<Business> rst = businessList.stream()
        .collect(Collectors.maxBy(Comparator.comparing(Business::getId)));
        //等價於 .max(Comparator.comparing(Business::getId));
System.out.println(rst.map(Business::getId).orElse(0L));
//輸出:2
  • 實現一些常用的數值計算:averagingXX(),summingXX()

 

//求所有業務線的id均值
Double rst = businessList.stream()
        .collect(Collectors.averagingDouble(Business::getId));
System.out.println(rst);
//輸出:1.5

3.數據分塊

  • 將一個流根據指定的Predicate對象,分解成兩個集合


    圖片.png-66.9kB

    圖片.png-66.9kB

 

//根據業務線id是否大於1分塊
Map<Boolean, List<Business>> rst = businessList.stream()
        .peek(business -> business.setCategoryList(null))
        .collect(Collectors.partitioningBy(business -> business.getId() > 1));
System.out.println(rst);
//輸出:{false=[Test.Business(id=1, name=家政, categoryList=null)], true=[Test.Business(id=2, name=速運, categoryList=null)]}

4.數據分組

  • 數據分開只能將數據分成兩部分,而數據分組不受此限制,可以更自然地分割數據(類似與SQL中的 group by 操作)


    圖片.png-61.2kB

    圖片.png-61.2kB

 

    @Data
    @AllArgsConstructor
    static class Custom {
        private long id;
        private String name;
        private String city;
    }
    
    public static void main(String[] args) {
        List<Custom> customList = Arrays.asList(
                new Custom(1L, "張三", "北京"),
                new Custom(2L, "李四", "北京"),
                new Custom(3L, "tom", "紐約"),
                new Custom(4L, "jerry", "紐約"),
                new Custom(5L, "thomas", "紐約")
        );

        //根據城市分組
        Map<String, List<Custom>> rst = customList.stream()
                .collect(Collectors.groupingBy(Custom::getCity));
        System.out.println(rst);
    }
//輸出:{紐約=[Test.Custom(id=3, name=tom, city=紐約), Test.Custom(id=4, name=jerry, city=紐約), Test.Custom(id=5, name=thomas, city=紐約)], 北京=[Test.Custom(id=1, name=張三, city=北京), Test.Custom(id=2, name=李四, city=北京)]}

5.字符串

  • 通過指定 分隔符、前綴、後綴 的方式生成一個格式化後的字符串:joining()

 

String rst = businessList.stream()
        .map(Business::getName)
        .collect(Collectors.joining(",", "[", "]"));
System.out.println(rst);
//輸出:[家政,速運]

6.組合收集器

  • 組合收集器分爲 主收集器下游收集器,主收集器會用到下游收集器,下游收集器是用來生成最終values的 配方

 

List<Custom> customList = Arrays.asList(
        new Custom(1L, "張三", "北京"),
        new Custom(2L, "李四", "北京"),
        new Custom(3L, "tom", "紐約"),
        new Custom(4L, "jerry", "紐約"),
        new Custom(5L, "thomas", "紐約")
);

Map<String, List<Long>> rst = customList.stream()
        .collect(Collectors.groupingBy
                (Custom::getCity,
                        Collectors.mapping(Custom::getId, Collectors.toList())));
System.out.println(rst);
//輸出:{紐約=[3, 4, 5], 北京=[1, 2]}

7.定製收集器

  • 收集器接口的方法介紹
  1. supplier():用來創建容器的工廠方法,和reduce操作的第一個參數類似,是後續操作的 初值圖片.png-38.2kB

    圖片.png-38.2kB

  2. accumulator():該方法的作用和reduce操作的第二個參數一樣,用來 結合 之前操作的結果和當前值,生成並返回新的值。圖片.png-40kB

    圖片.png-40kB

  3. combiner():如果有多個容器,會通過該方法 合併爲一個容器。圖片.png-38.7kB

    圖片.png-38.7kB

  4. finisher():對容器進行 轉換 以得到預期的結果值。finisher.png-35.2kB

    finisher.png-35.2kB

  • 定製一個字符串拼接收集器

 

    public static void main(String[] args) {
        List<Custom> customList = Arrays.asList(
                new Custom(1L, "張三", "北京"),
                new Custom(2L, "李四", "北京"),
                new Custom(3L, "tom", "紐約"),
                new Custom(4L, "jerry", "紐約"),
                new Custom(5L, "thomas", "紐約")
        );

        String rst = customList.parallelStream()
                .map(Custom::getName)
                .collect(new MyStrCollector());

        System.out.println(rst);
        //輸出:[張三,李四,tom,jerry,thomas]
    }

    //泛型含義:<待收集元素的類型,累加器的類型,最終結果的類型>
    static class MyStrCollector implements Collector<String, StringBuilder, String> {

        private final String prefix = "[";
        private final String separator = ",";
        private final String suffix = "]";

        @Override
        public Supplier<StringBuilder> supplier() {
            return StringBuilder::new;
        }

        @Override
        public BiConsumer<StringBuilder, String> accumulator() {
            return (stringBuilder, item) -> {
                stringBuilder.append(separator).append(item);
            };
        }

        @Override
        public BinaryOperator<StringBuilder> combiner() {
            return StringBuilder::append;
        }

        @Override
        public Function<StringBuilder, String> finisher() {
            return (stringBuilder -> prefix + stringBuilder.toString().substring(1) + suffix);
        }

        @Override
        public Set<Characteristics> characteristics() {
            return Collections.emptySet();
        }
    }


end



作者:靈派coder
鏈接:https://www.jianshu.com/p/e0816e6564af
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章