高手問答第 305 期 —— 如何使用 lambda 表達式提升開發效率?

OSC噠噠 發布于 08/22 15:54
閱讀 11K+
收藏 24

從數據到大模型應用,11 月 25 日,杭州源創會,共享開發小技巧

Java8的一個大亮點是引入Lambda表達式,使用它設計的代碼會更加簡潔。當開發者在編寫Lambda表達式時,也會隨之被編譯成一個函數式接口。

OSCHINA 本期高手問答 (8 月 23 日 - 8 月 29 日) 我們請來嘉賓 阿超老師 來和大家一起探討關于Lambda和Stream 的問題,將以【如何使用lambda表達式提升開發效率】為切入點展開討論。

可討論的問題包括但不限于:
  • lambda表達式的應用場景
  • Stream的應用場景
  • Lambda/Stream的進一步封裝
除了上述三個范圍,你也可以將討論的內容外延到函數式編程的整個領域(不限于編程語言),包括各大開源項目中對其的封裝、應用等等,還可以專注于開源的orm框架 Mybatis-Plus的源碼、實踐等細節。
 

嘉賓介紹

阿超,00后全棧開發,dromara組織成員、hutool團隊成員、mybatis-plus團隊成員、stream-query項目作者,參與貢獻的開源項目包括不限于apache-shenyu、apache-streampark等。

個人主頁:https://gitee.com/VampireAchao/

為了鼓勵踴躍提問,問答結束后我們會從提問者中抽取 5 名幸運會員,贈予 開源項目stream-query的開源周邊T恤,由阿超親自設計!

Lambda表達式

https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

簡單來說:就是把我們的函數(方法)作為參數傳遞、調用等

例子:自定義函數式接口(用jdk自帶的函數式接口也可以)

import java.io.Serializable;

/**
 * 可序列化的Functional
 *
 * @author VampireAchao
 */
@FunctionalInterface
public interface Func<T, R> extends Serializable {

    /**
     * 調用
     *
     * @param t 參數
     * @return 返回值
     */
    R apply(T t);
}

我們定義一個類可以去實現該接口

/**
 * 可序列化的函數式接口實現類
 *
 * @author VampireAchao
 */
public class FuncImpl implements Func<Object, String> {
    /**
     * 調用
     *
     * @param o 參數
     * @return 返回值
     */
    @Override
    public String apply(Object o) {
        return o.toString();
    }
}

到此為止,都非常的簡單

這里就有個問題:假設我有很多的地方需要不同的類去實現Func,我就得每次都去寫這么一個類,然后實現該接口并重寫方法

這樣很麻煩!因此我們使用匿名內部類

        Func<String, Integer> func = new Func<String, Integer>() {
            /**
             * 調用
             *
             * @param s 參數
             * @return 返回值
             */
            @Override
            public Integer apply(String s) {
                return s.hashCode();
            }
        };

我們可以看到,使用了匿名內部類后不用每次去新建這個類了,只需要在調用的地方,new一下接口,創建一個匿名內部類即可

但這樣還有個問題,我們每次都要寫這么一大幾行代碼,特別麻煩

由此而生,我們有了lambda這種簡寫的形式

https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html#syntax

        Func<String, String> func1 = (String s) -> {
            return s.toUpperCase();
        };

如果只有一行,我們可以省略掉中括號以及return

        Func<String, String> func2 = (String s) -> s.toUpperCase();

我們可以省略掉后邊的參數類型

        Func<String, String> func3 = s -> s.toUpperCase();

如果我們滿足特定的形式,我們還可以使用方法引用(雙冒號)的形式縮寫

        Func<String, String> func4 = String::toUpperCase;

這里除了我們的參數->返回值寫法:s->s.toUpperCase(),還有很多種

例如無參數帶返回值寫法()->"yes"、無參無返回值寫法()->{}等等

而方法引用這種寫法有如下幾種:

https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html

package org.dromara.streamquery;

import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.Supplier;

/**
 * 語法糖——方法引用
 *
 * @author VampireAchao
 */
public class MethodReferences {

    public static Object staticSupplier() {
        return "whatever";
    }

    public Object instanceSupplier() {
        return "whatever";
    }

    public Object anonymousInstanceFunction() {
        return "whatever";
    }

    public static void main(String[] args) {
        // 引用構造函數
        Supplier<MethodReferences> conSup = () -> new MethodReferences();
        conSup = MethodReferences::new;
        // 數組構造函數引用
        IntFunction<int[]> intFunction = value -> new int[value];
        // intFunc == new int[20];
        int[] intFuncResult = intFunction.apply(20);
        // 引用靜態方法
        Supplier<Object> statSup = () -> staticSupplier();
        statSup = MethodReferences::staticSupplier;
        Object statSupResult = statSup.get();
        // 引用特定對象的實例方法
        Supplier<Object> instSup = new MethodReferences()::instanceSupplier;
        instSup = new MethodReferences()::instanceSupplier;
        Object instSupResult = instSup.get();
        // 引用特定類型的任意對象的實例方法
        Function<MethodReferences, Object> anonInstFunc = streamDemo -> streamDemo.anonymousInstanceFunction();
        anonInstFunc = MethodReferences::anonymousInstanceFunction;

    }

}

順便放幾個常用的,jdk自帶的函數式接口寫法

package org.dromara.streamquery;

import java.math.BigDecimal;
import java.util.function.*;

/**
 * 常用的幾個函數式接口寫法
 *
 * @author VampireAchao
 */
class Usual {

    public static Consumer<Object> consumer() {
        // 有參數無返回值
        return o -> {
        };
    }

    public static Function<Integer, Object> function() {
        // 有參數有返回值
        return o -> o;
    }

    public static Predicate<Object> predicate() {
        // 有參數,返回值為boolean
        return o -> true;
    }

    public static Supplier<Object> supplier() {
        // 無參數有返回值
        return Object::new;
    }

    public static BiConsumer<String, Integer> biConsumer() {
        // 倆參數無返回值
        return (q, o) -> {
        };
    }

    public static BiFunction<Integer, Long, BigDecimal> biFunction() {
        // 倆參數,有返回值
        return (q, o) -> new BigDecimal(q).add(BigDecimal.valueOf(o));
    }

    public static UnaryOperator<Object> unaryOperator() {
        // 一個參數,返回值類型和參數一樣
        return q -> q;
    }

    public static BinaryOperator<Object> binaryOperator() {
        // 倆參數和返回值類型保持一致
        return (a, o) -> a;
    }

}

Stream

Java 8 API添加了一個新的抽象稱為流Stream,可以讓你以一種聲明的方式處理數據。方法全是傳入函數作為參數,來達到我們的目的。

https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html

        // 聲明式編程是告訴計算機需要計算“什么”而不是“如何”去計算
        // 現在,我想要一個List,包含3個數字6
        List<Integer> sixSixSix =
                // 我想要:
                Stream
                        // 數字6
                        .generate(() -> 6)
                        // 3個
                        .limit(3)
                        // 最后收集起來轉為List
                        .collect(Collectors.toList());
        sixSixSix.forEach(System.out::print);

Stream 使用一種類似用 SQL 語句從數據庫查詢數據的直觀方式來提供一種對 Java 集合運算和表達的高階抽象。

        // 就像sql里的排序、截取
        // 我要把傳入的list逆序,然后從第五個(元素下標為4)開始取值,取4條
        abc = abc.stream()
                // 排序(按照自然順序的逆序)
                .sorted(Comparator.reverseOrder())
                // 從下標為4開始取值
                .skip(4)
                // 取4條
                .limit(4)
                // 最后收集起來轉為List
                .collect(Collectors.toList());
        System.out.println("我要把傳入的list逆序,然后從第五個(元素下標為4)開始取值,取4條");
        abc.forEach(System.out::print);
        System.out.println();

Stream API可以極大提高Java程序員的生產力,讓程序員寫出高效率、干凈、簡潔的代碼。

    /**
     * 老辦法實現一個list,存儲3個6
     *
     * @return [6, 6, 6]
     */
    private static List<Integer> oldSix() {
        // 老辦法
        List<Integer> sixSixSix = new ArrayList<>(3);
        sixSixSix.add(6);
        sixSixSix.add(6);
        sixSixSix.add(6);
        System.out.println("老辦法實現一個list,存儲3個6");
        for (Integer integer : sixSixSix) {
            System.out.print(integer);
        }
        System.out.println();
        return sixSixSix;
    }

    /**
     * 新方法實現一個list,存儲3個6
     *
     * @return [6, 6, 6]
     */
    private static List<Integer> newSix() {
        List<Integer> sixSixSix = Stream.generate(() -> 6).limit(3).collect(Collectors.toList());
        System.out.println("新方法實現一個list,存儲3個6");
        sixSixSix.forEach(System.out::print);
        System.out.println();
        return sixSixSix;
    }

這種風格將要處理的元素集合看作一種流, 流在管道中傳輸, 并且可以在管道的節點上進行處理, 比如篩選, 排序,聚合等。

        // 管道中傳輸,節點中處理
        int pipe = abc.stream()
                // 篩選
                .filter(i -> i > 'G')
                // 排序
                .sorted(Comparator.reverseOrder())
                .mapToInt(Object::hashCode)
                // 聚合
                .sum();
        System.out.println("將26個字母組成的集合過濾出大于'G'的,逆序,再獲取hashCode值,進行求和");
        System.out.println(pipe);

元素流在管道中經過中間操作(intermediate operation)的處理,最后由最終操作(terminal operation)得到前面處理的結果。

        // 將26個大寫字母Character集合轉換為String然后轉換為小寫字符
        List<String> terminalOperation = abc.stream()
                // 中間操作(intermediate operation)
                .map(String::valueOf).map(String::toLowerCase)
                // 最終操作(terminal operation)
                .collect(Collectors.toList());
        System.out.println("26個大寫字母Character集合,轉換成String然后轉換為小寫字符,收集起來");
        terminalOperation.forEach(System.out::print);
        System.out.println();

OSChina 高手問答一貫的風格,不歡迎任何與主題無關的討論和噴子。

下面歡迎大家就 Lambda和Stream 相關問題阿超老師提問,直接回帖提問既可。

加載中
0
yaosaya
yaosaya

高手問答第 305 期 —— 如何使用 lambda 表達式提升開發效率?

@handy-git @Createsequence @asphalt520 @時光TM @clearsky1991 

恭喜以上5位網友分別獲得 開源項目 stream-query 的開源周邊 T 恤(M到3XL可選) 一件。

請于9月7日前登陸賬號, 私信 @yaosaya 告知快遞信息(格式:姓名+電話+地址),過期視為自動放棄哦~

2
瘋狂的獅子Li

@快樂阿超 阿超為什么鐘意于寫lambda 這東西有什么優劣??

快樂阿超
快樂阿超
可以用更少的代碼,實現更多的細節
handy-git
handy-git
阿超舉的那個例子也很好 可以極大提高 Java 程序員的生產力,讓程序員寫出高效率、干凈、簡潔的代碼。
1
快樂阿超
快樂阿超

借用Createsequence提出的問題【能介紹一下 Collector 嗎?它是五個方法是干嘛的?三個泛型又代表什么意思?

首先介紹下Collector:

https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.html

Collector通常是作為Stream的collect方法的參數,通過使用Collectors可以創建一些預設的Collector例如上方提到的

list.stream().collect(Collectors.toList())

則是將流轉換收集起來為List集合,Collectors還預設了非常多的常用收集器,基本滿足大多數場景如Collectors.toMap、Collectors.groupingBy等,此處回答不做過多展開,繼續回答一下Collector的五個方法,除了characteristics以外,其他四個方法均返回函數式接口

1. supplier ,意義是用于指定初始值,也可用于指定具體類型,可以傳入lambda表達式如:

HashMap::new、()-> new ArrayList(88)等

2. accumulator,返回值是BiConsumer類型,意義為傳入具體的收集操作,例如此處的lambda包含兩個參數為

map(需要收集到的map)和item(每一個元素)

邏輯為將item.toString作為key,item作為value放入map

(map, item) -> {
    map.put(item.toString(), item);
}

3. combiner,返回BinaryOperator類型的函數式接口,是一個可選的參數,可選不一定代表可以傳入null,而指的是lambda中可以隨便寫,例如傳入(lastMap, curMap) -> null

這個參數的意義是在“并行流”場景下,將多線程返回的多個結果進行合并,如果是“串行流”,則不會調用lambda中的方法。此處講一下并行流和串行流:

因為Stream流只有在 結束操作(collect、reduce、forEach等) 時才會真正觸發執行以往的 中間操作 (filter、map、flatMap等)

它分為串行流和并行流 并行流會使用拆分器(java.util.Spliterator)將操作拆分為多個異步任務(java.util.concurrent.ForkJoinTask)執行 這些異步任務默認使用(java.util.concurrent.ForkJoinPool)線程池進行管理

拆分后的任務,由于是異步并行執行,所以每個異步任務會返回一個結果,宏觀就是會返回多個結果,最終將這些結果收集起來,所以需要使用combiner,使用例子:

                  (lastMap, curMap) -> {
                            lastMap.putAll(curMap);
                            return lastMap;
                        }

如果只是在串行流(同步場景)使用,這個參數對應的lambda就不會執行

4. finisher,意義為最終轉換,返回Function,是在收集結束后執行,例:

                        map -> {
                            return Collections.unmodifiableMap(map);
                        }

不一定是相同類型,不同類型也可以

                        map -> {
                            return map.values();
                        }

5.  characteristics,表示特征,返回值是一個Set,里面裝著Characteristics類型的枚舉

CONCURRENT允許并發執行的

UNORDERED沒有指定特定的順序的

IDENTITY_FINISH中間步驟和結束步驟一致

這部分主要是用于描述特征,在執行時如果有相應的特征,會走相應的執行流程

 

然后是三個泛型

T: 元素類型,流里的每一個元素的具體類型

A: 中間類型,用于收集元素的對象/集合的類型

R: 最終類型,通常和中間類型一致,但如果有最終操作finisher,會改變最終類型

這里列舉一個完整用法:

        Collector<Object, Map<String, Object>, Map<String, Object>> mapCollector =
                Collector.of(HashMap::new,
                        (map, item) -> map.put(item.toString(), item),
                        (lastMap, curMap) -> {
                            lastMap.putAll(curMap);
                            return lastMap;
                        }, Collections::unmodifiableMap,
                        Collector.Characteristics.IDENTITY_FINISH);
        Map<String, Object> map = Stream.of(1, 2, 3).collect(mapCollector);

 

Createsequence
Createsequence
那不同的 Characteristics 對流的執行會有什么樣的影響呢?比如 IDENTITY_FINISH,會不會影響到調用 finisher ?如果同時存在不同的 Characteristics,它們又會互相影響嗎?
1
iman123
iman123

@快樂阿超 你好,JDK 8 新增的lambda、stream等相關內容在更新越來越頻繁的 JDK 17,21 下有沒有變化,stream里面如果涉及到并行計算,例如求和,可以結合多線程或者虛擬線程提升性能么?

快樂阿超
快樂阿超
目前內置的api還沒有結合虛擬線程,未來應該會
快樂阿超
快樂阿超
還有新增了如toList()代替collect(Collectors.toList())的函數、 新增了能傳入自定義條件結束Stream.iterator迭代的重載等新特性
快樂阿超
快樂阿超
有變化,例如 list.stream().peek(System.out::println).count(); 在java8下會執行打印,在java11時則不會執行peek
1
osc_94315807
osc_94315807

@快樂阿超 如何能說服同事用stream呢?說服領導和同時,從間接性和易讀性上,stream方便很多

金克絲丶爆
金克絲丶爆
換一家單位
快樂阿超
快樂阿超
把這篇文章轉發給他
0
osc_70636502
osc_70636502

請問在日常使用的情況下,在自己的項目中如何充分運用自己寫的函數式接口,與一些設計模式進行配合呢

0
快樂阿超
快樂阿超

可以在策略模式中靈活運用lambda表達式,簡化代碼

例如按照我以往講策略模式blog中的例子:

https://vampireachao.gitee.io/2021/10/16/%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F/

 其中新建WalkingStrategy.java并new

        Navigator navigator = new Navigator(new WalkingStrategy());

可被lambda優化為具體的策略細節

Navigator navigator = new Navigator((s, e) -> Arrays.asList("先倒1", "路寸2", "途涇3"));

 

 
0
滄海紅心

@快樂阿超  受益匪淺??

0
handy-git
handy-git

@快樂阿超 
想使用 自定義函數式接口 解決跨版本兼容問題

例如不同版本的jar api不一致,但是要兼容低版本和高版本就想著用這個

有什么改動最小的方法

0
llllllimm
llllllimm

@快樂阿超 lambda表達式執行如果拋出異常,定位不好定位,怎么處理

快樂阿超
快樂阿超
可以結合全篇日志上下文,lambda在java中的實現就是匿名內部類,異常日志基本沒有區別,簡單來說就是先把日志全部整體看完,再去細入 也可以將debug斷點打入lambda定位排查
OSCHINA
登錄后可查看更多優質內容
返回頂部
頂部
一本久久综合亚洲鲁鲁五月天,无翼乌口工全彩无遮挡H全彩,英语老师解开裙子坐我腿中间