從數據到大模型應用,11 月 25 日,杭州源創會,共享開發小技巧
Java8的一個大亮點是引入Lambda表達式,使用它設計的代碼會更加簡潔。當開發者在編寫Lambda表達式時,也會隨之被編譯成一個函數式接口。
OSCHINA 本期高手問答 (8 月 23 日 - 8 月 29 日) 我們請來了嘉賓 阿超老師 來和大家一起探討關于Lambda和Stream 的問題,將以【如何使用lambda表達式提升開發效率】為切入點展開討論。
-
lambda表達式的應用場景
-
Stream的應用場景
-
Lambda/Stream的進一步封裝
嘉賓介紹
阿超,00后全棧開發,dromara組織成員、hutool團隊成員、mybatis-plus團隊成員、stream-query項目作者,參與貢獻的開源項目包括不限于apache-shenyu、apache-streampark等。
個人主頁:https://gitee.com/VampireAchao/

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 相關問題向阿超老師提問,直接回帖提問既可。
高手問答第 305 期 —— 如何使用 lambda 表達式提升開發效率?
@handy-git @Createsequence @asphalt520 @時光TM @clearsky1991
恭喜以上5位網友分別獲得 開源項目 stream-query 的開源周邊 T 恤(M到3XL可選) 一件。
請于9月7日前登陸賬號, 私信 @yaosaya 告知快遞信息(格式:姓名+電話+地址),過期視為自動放棄哦~
@快樂阿超 阿超為什么鐘意于寫lambda 這東西有什么優劣??
借用Createsequence提出的問題【能介紹一下 Collector 嗎?它是五個方法是干嘛的?三個泛型又代表什么意思?】
首先介紹下Collector:
https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.html
Collector通常是作為Stream的collect方法的參數,通過使用Collectors可以創建一些預設的Collector例如上方提到的
則是將流轉換收集起來為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
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,使用例子:
如果只是在串行流(同步場景)使用,這個參數對應的lambda就不會執行
4. finisher,意義為最終轉換,返回Function,是在收集結束后執行,例:
不一定是相同類型,不同類型也可以
5. characteristics,表示特征,返回值是一個Set,里面裝著Characteristics類型的枚舉
CONCURRENT允許并發執行的
UNORDERED沒有指定特定的順序的
IDENTITY_FINISH中間步驟和結束步驟一致
這部分主要是用于描述特征,在執行時如果有相應的特征,會走相應的執行流程
然后是三個泛型
T: 元素類型,流里的每一個元素的具體類型
A: 中間類型,用于收集元素的對象/集合的類型
R: 最終類型,通常和中間類型一致,但如果有最終操作finisher,會改變最終類型
這里列舉一個完整用法:
@快樂阿超 你好,JDK 8 新增的lambda、stream等相關內容在更新越來越頻繁的 JDK 17,21 下有沒有變化,stream里面如果涉及到并行計算,例如求和,可以結合多線程或者虛擬線程提升性能么?
@快樂阿超 如何能說服同事用stream呢?說服領導和同時,從間接性和易讀性上,stream方便很多
請問在日常使用的情況下,在自己的項目中如何充分運用自己寫的函數式接口,與一些設計模式進行配合呢
可以在策略模式中靈活運用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優化為具體的策略細節
@快樂阿超 受益匪淺??
@快樂阿超
想使用 自定義函數式接口 解決跨版本兼容問題
例如不同版本的jar api不一致,但是要兼容低版本和高版本就想著用這個
有什么改動最小的方法
@快樂阿超 lambda表達式執行如果拋出異常,定位不好定位,怎么處理