Java8 函數式編程——基礎篇

函數式編程

一種編程範式,它將電腦運算視爲函數運算,並且避免使用程序狀態以及易變對象。其中,λ演算(lambda calculus)爲該語言最重要的基礎。

特性

函數是“第一等公民”

函數與其他數據類型一樣,可以賦值給其他變量,也可以作爲參數,也可以作爲返回值

不可變性

像閉包一樣,傳入的自由變量是不可變的,降低數據的不一致性。同時也只返回新的值,不修改變量狀態,沒有“副作用”。

爲什麼使用

  • 代碼簡潔,開發快速
  • 易於理解,降低風險
  • 易於並行
  • 延遲執行

例: 根據用戶名批量並行獲取工號

Before

public List<String> listEmployIds(List<String> usernames) throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(usernames.size());
        ConcurrentHashMap<String, UserDTO> mapping = new ConcurrentHashMap<>();
        for (String username : usernames) {
            ThreadPoolUtil.execute(() -> {
                UserDTO user = userService.getUserDetail(username);
                mapping.put(username, user);
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        List<String> employIds = new ArrayList<>(usernames.size());
        for (UserDTO user : mapping.values()) {
            employIds.add(user.getEmployeeId());
        }
        return employIds;
    }

After

public List<String> listEmployIds(List<String> usernames) throws Exception {
      return usernames.parallelStream().map(userService::getUserDetail).map(UserDTO::getEmployeeId);

基礎點

lambda表達式

函數式編程的基礎,用於創建函數式接口的實現對象。函數式接口就是隻包含一個抽象方法的接口,如Runnable@FunctionalInterface註解用於提供約束。

變量作用域

lambda表達式可以理解爲匿名內部類的“語法糖”,但略有些不同。

  • “捕獲”傳入參數,複製到生成的實例中
  • 自由變量必須爲有效final
  • 與內部類不同,lambda表達式方法體中的變量與上層嵌套代碼塊有着相同的作用域,this也指向外層的對象
public class MyTest {
    public void test() {
        int i = 0;
        new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 8; // 正確
                System.out.println(this); //com.didi.oe.controller.MyTest$1@16275d20 名字爲1的匿名內部類
            }
        }).start();

        new Thread(() -> {
            // int i = 8; // Variable 'i' is already defined in the scope
            System.out.println(this); // com.didi.oe.controller.MyTest@12b968dd
        }).start();
    }
}

Stream

  • 不存儲元素,元素依然保存在源集合之中
  • 不改變源集合
  • 延遲執行

並行流

  • 通過parallelStream()stream().parallel()開啓並行流,使用sequential()切回串行流
  • 並行返回結果與串行時相同,可通過unordered()放棄有序加快速度。

異常

  • 異常將拋給lambda表達式的調用者
  • 並行流中只要有一個線程拋出異常則強行結束所有線程並拋出異常

開發中的使用場景

Optional

Optional對象是對類型T對象的封裝,也可以表示爲空對象。可理解爲對單對象進行Stream封裝。比直接指向T類型的引用更加安全(正確使用的情況下)。

        // 不恰當的使用
        Optional<T> opt = ...;
        if (opt.isPresent()) {
            opt.get().someMethod();
        }
        // 正解
        opt.ifPresent(value -> {
            value.someMethod();
        });

ifNotPresent的處理方式:

    // java9以上
    opt.ifPresentOrElse(value -> value.someMethod(), () -> logger.error("…"));
 
    // java8
    opt.map(value -> {
            value.someMethod();
            return value;
        }).orElseGet(() -> {
            // operation
            return null;
        });

存在問題:表達式中無法直接返回跳出父方法
可通過異常代替。

默認值:

        opt.orElse("");
        opt.orElseThrow(ResourceNotFoundException::new);
		// 注意以下兩種寫法,前者可以延遲執行
        opt.orElseGet(() -> RandomStringUtils.random(1));
        opt.orElse(RandomStringUtils.random(1));

流式處理

Optional 的 map() 與 flatMap() 方法

簡化的源碼如下,本質都是爲了返回Optional,以支持流式調用。

    public Optional map(Function mapper) {
        if (!isPresent())
            return empty();
        else {
            return Optional.ofNullable(mapper.apply(value));
        }
    }

    public Optional flatMap(Function mapper) {
        if (!isPresent())
            return empty();
        else {
            return mapper.apply(value);
        }
    }

示例:

        // 先前的寫法
        JSONObject json = new JSONObject();
        JSONObject user = json.getJSONObject("user");
        if (user != null) {
            JSONObject address = json.getJSONObject("address");
            if (user != address) {
                String street = json.getString("street");
                if (street != null) {
                    return street;
                } else {
                    return "not found";
                }
            }
        }
        
        // Optional寫法
        return Optional.ofNullable(json).map(jsonObject -> jsonObject.getJSONObject("user"))
            .map(jsonObject ->jsonObject.getJSONObject("address"))
            .map(jsonObject -> jsonObject.getString("street")).orElse("not found");
       // 使用Class解析的情況
        return Optional.ofNullable(resopnse).map(Response::getUser).map(User::getAddress).map(Address::getStreet)
            .orElse("not found");

優勢

  • 更加安全,顯式地提醒對空指針的處理
  • 簡化代碼

使用時機

  • Optional的預期用途主要是作爲方法返回類型。獲取返回值後,可以根據值是否存在進行不同的業務邏輯處理(建議後續業務中統一使用Optional)
  • 不要將其用作類中的字段,及controller類返回值,因爲序列化需特殊支持
  • 不要將其用作構造函數和方法的參數,這會導致不必要的複雜代碼

聚合之坑

常見使用方式

        List<UserDTO> users = new ArrayList<>();
        users.stream().map(UserDTO::getEmployeeId).collect(Collectors.toList());
        users.stream().map(UserDTO::getEmployeeId).collect(Collectors.joining(","));
        users.stream().collect(Collectors.toMap(UserDTO::getUsername, Function.identity()));

坑1

Collectors.toMap存在多個相同key時,將會拋出IllegalStateException異常。需顯式指定:

       users.stream().collect(Collectors.toMap(UserDTO::getUsername, UserDTO::getEmployeeId, (oldValue, newValue) -> newValue));
        

坑2

Collectors.toMap的value爲空時拋出NullPointerException異常,這是OpenJDK的已知BUG,解決方案:

		// 自己實現聚合邏輯,還可以替換默認的HashMap類型,如果LinkedHashMap
        users.stream().collect(HashMap::new, (map, user)-> map.put(user.getUsername(), user.getEmployeeId()), HashMap::putAll);

其他

性能

http://worldcomp-proceedings.com/proc/p2015/SER2509.pdf

函數式編程(Functional Programming)與 面向對象編程(Object-oriented Programming)

兩種不同的看待事物的方式。
FP以lambda爲基礎,強調在邏輯處理中不變性的重要性。所謂不變性就是把一切“狀態”都消除。由確定的輸入經過確定的一組函數處理得到的最終結果

OOP把對象作爲程序的基本單元,強調object之間的消息傳遞。類對象通過繼承、多態、消息等機制達到靈活性和擴展性。

沒有銀彈。

業務領域建模,這個過程就是多個獨立的“對象”在相互協作的結果。因此OOP在這個層面上對這個流程進行抽象是很合適的。

當對一組數據做加工時,先查詢,然後聚合,聚合後排序,再join,再排序,再聚合,再轉換(map)得到最終的結果。這個過程,用FP的函數就很自然。

其他知識點

  • 花樣排序(二級排序,null情況等)
  • stream分組、分片
  • 函數式接口類別,實際使用
  • parallelStream機制(forkJoin)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章