JDK8中包含了許多內建的Java中常用到函數接口,比如Comparator或者Runnable接口,這些接口都增加了@FunctionalInterface注解以便能用在lambda上。
name
|
type
|
description
|
Consumer
|
Consumer< T >
|
接收T對象,不返回值
|
Predicate
|
Predicate< T >
|
接收T對象並返回boolean
|
Function
|
Function< T, R >
|
接收T對象,返回R對象
|
Supplier
|
Supplier< T >
|
提供T對象(例如工廠),不接收值
|
UnaryOperator
|
UnaryOperator
|
接收T對象,返回T對象
|
BinaryOperator
|
BinaryOperator
|
接收兩個T對象,返回T對象
|
標注為@FunctionalInterface的接口是函數式接口,該接口只有一個自定義方法。注意,只要接口只包含一個抽象方法,編譯器就默認該接口為函數式接口。lambda表示是一個命名方法,將行為像數據一樣進行傳遞。
Collection中的新方法
該方法的簽名為void forEach(Consumer<? super E> action),作用是對容器中的每個元素執行action指定的動作,其中Consumer是個函數接口,里面只有一個待實現方法void accept(T t)。注意,這里的Consumer不重要,只需要知道它是一個函數式接口即可,一般使用不會看見Consumer的身影。
list.forEach(item -> System.out.println(item));
該方法簽名為boolean removeIf(Predicate<? super E> filter),作用是刪除容器中所有滿足filter指定條件的元素,其中Predicate是一個函數接口,里面只有一個待實現方法boolean test(T t),同樣的這個方法的名字根本不重要,因為用的時候不需要書寫這個名字。
// list中元素類型String list.removeIf(item -> item.length() < 2);
List.replaceAll()
該方法簽名為void replaceAll(UnaryOperator<E> operator),作用是對每個元素執行operator指定的操作,並用操作結果來替換原來的元素。
// list中元素類型String list.replaceAll(item -> item.toUpperCase());
List.sort()
該方法定義在List接口中,方法簽名為void sort(Comparator<? super E> c),該方法根據c指定的比較規則對容器元素進行排序。Comparator接口我們並不陌生,其中有一個方法int compare(T o1, T o2)需要實現,顯然該接口是個函數接口。
// List.sort()方法結合Lambda表達式 ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); list.sort((str1, str2) -> str1.length()-str2.length());
Map.forEach()
該方法簽名為void forEach(BiConsumer<? super K,? super V> action),作用是對Map中的每個映射執行action指定的操作,其中BiConsumer是一個函數接口,里面有一個待實現方法void accept(T t, U u)。
map.forEach((key, value) -> System.out.println(key + ": " + value));
Stream API
認識了幾個Java8 Collection新增的幾個方法,在了解下Stream API,你會發現它在集合數據處理方面的強大作用。常見的Stream接口繼承關系圖如下:
- 調用Collection.stream()或者Collection.parallelStream()方法
- 調用Arrays.stream(T[] array)方法
- 無存儲。stream不是一種數據結構,它只是某種數據源的一個視圖。本質上stream只是存儲數據源中元素引用的一種數據結構,注意stream中對元素的更新動作會反映到其數據源上的。
- 為函數式編程而生。對stream的任何修改都不會修改背后的數據源,比如對stream執行過濾操作並不會刪除被過濾的元素,而是會產生一個不包含被過濾元素的新stream。
- 惰式執行。stream上的操作並不會立即執行,只有等到用戶真正需要結果的時候才會執行。
- 可消費性。stream只能被“消費”一次,一旦遍歷過就會失效,就像容器的迭代器那樣,想要再次遍歷必須重新生成。
對Stream的操作分為2種,中間操作與結束操作,二者的區別是,前者是惰性執行,調用中間操作只會生成一個標記了該操作的新的stream而已;后者會把所有中間操作積攢的操作以pipeline的方式執行,這樣可以減少迭代次數。計算完成之后stream就會失效。
操作類型
|
接口方法
|
中間操作
|
concat() distinct() filter() flatMap() limit() map() peek() skip() sorted() parallel() sequential() unordered()
|
結束操作
|
allMatch() anyMatch() collect() count() findAny() findFirst() forEach() forEachOrdered() max() min() noneMatch() reduce() toArray()
|
stream方法
forEach()
stream的遍歷操作。
filter()
函數原型為Stream<T> filter(Predicate<? super T> predicate),作用是返回一個只包含滿足predicate條件元素的Stream。
distinct()
函數原型為Stream<T> distinct(),作用是返回一個去除重復元素之后的Stream。
sorted()
排序函數有兩個,一個是用自然順序排序,一個是使用自定義比較器排序,函數原型分別為Stream<T> sorted()和Stream<T> sorted(Comparator<? super T> comparator)。
map()
函數原型為<R> Stream<R> map(Function<? super T,? extends R> mapper),作用是返回一個對當前所有元素執行執行mapper之后的結果組成的Stream。直觀的說,就是對每個元素按照某種操作進行轉換,轉換前后Stream中元素的個數不會改變,但元素的類型取決於轉換之后的類型。
List<Integer> list = CollectionUtil.newArrayList(1, 2, 3, 4); list.stream().map(item -> String.valueOf(item)).forEach(System.out::println); flapmap()
和map類似,不同的是每個元素轉換得到的是stream對象,會把子stream對象壓縮到父集合中。
List<List<String>> list3 = Arrays.asList( Arrays.asList("aaa", "bb", "ccc"), Arrays.asList("aa", "bbb", "ccc")); list3.stream().flatMap(Collection::stream).collect(Collectors.toList());
reduce 和 collect
reduce的作用是從stream中生成一個值,sum()、max()、min()、count()等都是reduce操作,將他們單獨設為函數只是因為常用。
// 找出最長的單詞 Stream<String> stream = Stream.of("I", "love", "you", "too"); Optional<String> longest = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2);
collect方法是stream中重要的方法,如果某個功能沒有在Stream接口中找到,則可以通過collect方法實現。
// 將Stream轉換成容器或Map Stream<String> stream = Stream.of("I", "love", "you", "too"); List<String> list = stream.collect(Collectors.toList()); // Set<String> set = stream.collect(Collectors.toSet()); // Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length));
諸如String::length的語法形式稱為方法引用,這種語法用來替代某些特定形式Lambda表達式。如果Lambda表達式的全部內容就是調用一個已有的方法,那么可以用方法引用來替代Lambda表達式。方法引用可以細分為四類。引用靜態方法 Integer::sum,引用某個對象的方法 list::add,引用某個類的方法 String::length,引用構造方法 HashMap::new。
Stream Pipelines原理
ArrayList<String> list = CollectionUtil.newArrayList("I", "love", "you"); list.stream() .filter(s -> s.length() > 1) .map(String::toUpperCase) .sorted() .forEach(System.out::println);
上面的代碼和下面的功能一樣,不過下面的代碼便於打斷點調試。
ArrayList<String> list = CollectionUtil.newArrayList("I", "love", "you"); list.stream() .filter(s -> { return s.length() > 1; }) .map(s -> { return s.toUpperCase(); }) .sorted() .forEach(s -> { System.out.println(s); });
首先filter方法了解一下:
// ReferencePipeline @Override public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) { Objects.requireNonNull(predicate); return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE, StreamOpFlag.NOT_SIZED) { // 生成state對應的Sink實現 @Override Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) { return new Sink.ChainedReference<P_OUT, P_OUT>(sink) { @Override public void begin(long size) { downstream.begin(-1); } @Override public void accept(P_OUT u) { if (predicate.test(u)) downstream.accept(u); } }; } }; }
filter方法返回一個StatelessOp實例,並實現了其opWrapSink方法,可以肯定的是opWrapSink方法在之后某個時間點會被調用,進行Sink實例的創建。從代碼中可以看出,filter方法不會進行真正的filter動作(也就是遍歷列表進行filter操作)。
filter方法中出現了2個新面孔,StatelessOp和Sink,既然是新面孔,那就先認識下:
abstract class AbstractPipeline<E_IN, E_OUT, S extends BaseStream<E_OUT, S>> extends PipelineHelper<E_OUT> implements BaseStream<E_OUT, S>
StatelessOp繼承自AbstractPipeline,lambda的流處理可以分為多個stage,每個stage對應一個AbstractPileline和一個Sink。
Stream流水線組織結構示意圖如下:
圖中通過Collection.stream()方法得到Head也就是stage0,緊接着調用一系列的中間操作,不斷產生新的Stream。這些Stream對象以雙向鏈表的形式組織在一起,構成整個流水線,由於每個Stage都記錄了前一個Stage和本次的操作以及回調函數,依靠這種結構就能建立起對數據源的所有操作。這就是Stream記錄操作的方式。
Stream上的所有操作分為兩類:中間操作和結束操作,中間操作只是一種標記,只有結束操作才會觸發實際計算。中間操作又可以分為無狀態的(Stateless)和有狀態的(Stateful),無狀態中間操作是指元素的處理不受前面元素的影響,而有狀態的中間操作必須等到所有元素處理之后才知道最終結果,比如排序是有狀態操作,在讀取所有元素之前並不能確定排序結果。
有了AbstractPileline,就可以把整個stream上的多個處理操作(filter/map/...)串起來,但是這只解決了多個處理操作記錄的問題,還需要一種將所有操作疊加到一起的方案。你可能會覺得這很簡單,只需要從流水線的head開始依次執行每一步的操作(包括回調函數)就行了。這聽起來似乎是可行的,但是你忽略了前面的Stage並不知道后面Stage到底執行了哪種操作,以及回調函數是哪種形式。換句話說,只有當前Stage本身才知道該如何執行自己包含的動作。這就需要有某種協議來協調相鄰Stage之間的調用關系。這就需要Sink接口了,Sink包含的方法如下:
方法名
|
作用
|
void begin(long size)
|
開始遍歷元素之前調用該方法,通知Sink做好准備。
|
void end()
|
所有元素遍歷完成之后調用,通知Sink沒有更多的元素了。
|
boolean cancellationRequested()
|
是否可以結束操作,可以讓短路操作盡早結束。
|
void accept(T t)
|
遍歷元素時調用,接受一個待處理元素,並對元素進行處理。Stage把自己包含的操作和回調方法封裝到該方法里,前一個Stage只需要調用當前Stage.accept(T t)方法就行了。
|
有了上面的協議,相鄰Stage之間調用就很方便了,每個Stage都會將自己的操作封裝到一個Sink里,前一個Stage只需調用后一個Stage的accept()方法即可,並不需要知道其內部是如何處理的。當然對於有狀態的操作,Sink的begin()和end()方法也是必須實現的。比如Stream.sorted()是一個有狀態的中間操作,其對應的Sink.begin()方法可能創建一個存放結果的容器,而accept()方法負責將元素添加到該容器,最后end()負責對容器進行排序。Sink的四個接口方法常常相互協作,共同完成計算任務。實際上Stream API內部實現的的本質,就是如何重載Sink的這四個接口方法。
回到最開始地方的代碼示例,map/sorted方法流程大致和filter類似,這些操作都是中間操作。重點關注下forEach方法:
// ReferencePipeline @Override public void forEach(Consumer<? super P_OUT> action) { evaluate(ForEachOps.makeRef(action, false)); } // ... -> // AbstractPipeline @Override final <P_IN, S extends Sink<E_OUT>> S wrapAndCopyInto(S sink, Spliterator<P_IN> spliterator) { copyInto(wrapSink(Objects.requireNonNull(sink)), spliterator); return sink; } @Override final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) { // 各個pipeline的opWrapSink方法回調 for ( @SuppressWarnings("rawtypes") AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) { sink = p.opWrapSink(p.previousStage.combinedFlags, sink); } return (Sink<P_IN>) sink; } @Override final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) { if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) { // sink各個方法的回調 wrappedSink.begin(spliterator.getExactSizeIfKnown()); spliterator.forEachRemaining(wrappedSink); wrappedSink.end(); } else { copyIntoWithCancel(wrappedSink, spliterator); } }
forEach()流程中會觸發各個Sink的操作,也就是執行各個lambda表達式里的邏輯了。到這里整個lambda流程也就完成了。
Java lambda 原理
Java lambda 一眼看上去有點像匿名內部類的簡化形式,但是二者確有着本質的差別。匿名內部類經編譯后會生成對應的class文件,格式為XXX$n.class;而lambda代碼經過編譯后生成一個private方法,方法名格式為lambda$main$n。
// Application.main 方法中代碼 ArrayList<String> list = CollectionUtil.newArrayList("I", "love", "you"); list.forEach(new Consumer<String>() { @Override public void accept(String s) { System.out.println(s); } }); list.forEach(System.out::println);
以上代碼就會產生一個Application$1.class文件和一個lambda$main$0的方法。既然lambda實現不是內部類,那么在lambda中this就代表的當前所在類實例。
// Application.main 方法中代碼 ArrayList<String> list = CollectionUtil.newArrayList("I", "love", "you"); list.forEach(item -> { System.out.println(item); });
通過javap -c -p Application.class查看以上代碼對應的字節碼:

Constant pool: #1 = Methodref #12.#36 // java/lang/Object."<init>":()V #2 = Class #37 // java/lang/String #3 = String #38 // I #4 = String #39 // love #5 = String #40 // you #6 = Methodref #41.#42 // cn/hutool/core/collection/CollectionUtil.newArrayList:([Ljava/lang/Object;)Ljava/util/ArrayList; #7 = InvokeDynamic #0:#48 // #0:accept:()Ljava/util/function/Consumer; #8 = Methodref #49.#50 // java/util/ArrayList.forEach:(Ljava/util/function/Consumer;)V #9 = Fieldref #51.#52 // java/lang/System.out:Ljava/io/PrintStream; #10 = Methodref #53.#54 // java/io/PrintStream.println:(Ljava/lang/String;)V #11 = Class #55 // com/luo/demo/Application #12 = Class #56 // java/lang/Object #13 = Utf8 <init> #14 = Utf8 ()V #15 = Utf8 Code #16 = Utf8 LineNumberTable #17 = Utf8 LocalVariableTable #18 = Utf8 this #19 = Utf8 Lcom/luo/demo/Application; #20 = Utf8 main #21 = Utf8 ([Ljava/lang/String;)V #22 = Utf8 args #23 = Utf8 [Ljava/lang/String; #24 = Utf8 list #25 = Utf8 Ljava/util/ArrayList; #26 = Utf8 LocalVariableTypeTable #27 = Utf8 Ljava/util/ArrayList<Ljava/lang/String;>; #28 = Utf8 lambda$main$0 #29 = Utf8 (Ljava/lang/String;)V #30 = Utf8 item #31 = Utf8 Ljava/lang/String; #32 = Utf8 SourceFile #33 = Utf8 Application.java #34 = Utf8 RuntimeVisibleAnnotations #35 = Utf8 Lorg/springframework/boot/autoconfigure/SpringBootApplication; #36 = NameAndType #13:#14 // "<init>":()V #37 = Utf8 java/lang/String #38 = Utf8 I #39 = Utf8 love #40 = Utf8 you #41 = Class #57 // cn/hutool/core/collection/CollectionUtil #42 = NameAndType #58:#59 // newArrayList:([Ljava/lang/Object;)Ljava/util/ArrayList; #43 = Utf8 BootstrapMethods #44 = MethodHandle #6:#60 // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; #45 = MethodType #61 // (Ljava/lang/Object;)V #46 = MethodHandle #6:#62 // invokestatic com/luo/demo/Application.lambda$main$0:(Ljava/lang/String;)V #47 = MethodType #29 // (Ljava/lang/String;)V #48 = NameAndType #63:#64 // accept:()Ljava/util/function/Consumer; #49 = Class #65 // java/util/ArrayList #50 = NameAndType #66:#67 // forEach:(Ljava/util/function/Consumer;)V #51 = Class #68 // java/lang/System #52 = NameAndType #69:#70 // out:Ljava/io/PrintStream; #53 = Class #71 // java/io/PrintStream #54 = NameAndType #72:#29 // println:(Ljava/lang/String;)V #55 = Utf8 com/luo/demo/Application #56 = Utf8 java/lang/Object #57 = Utf8 cn/hutool/core/collection/CollectionUtil #58 = Utf8 newArrayList #59 = Utf8 ([Ljava/lang/Object;)Ljava/util/ArrayList; #60 = Methodref #73.#74 // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; #61 = Utf8 (Ljava/lang/Object;)V #62 = Methodref #11.#75 // com/luo/demo/Application.lambda$main$0:(Ljava/lang/String;)V #63 = Utf8 accept #64 = Utf8 ()Ljava/util/function/Consumer; #65 = Utf8 java/util/ArrayList #66 = Utf8 forEach #67 = Utf8 (Ljava/util/function/Consumer;)V #68 = Utf8 java/lang/System #69 = Utf8 out #70 = Utf8 Ljava/io/PrintStream; #71 = Utf8 java/io/PrintStream #72 = Utf8 println #73 = Class #76 // java/lang/invoke/LambdaMetafactory #74 = NameAndType #77:#81 // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; #75 = NameAndType #28:#29 // lambda$main$0:(Ljava/lang/String;)V #76 = Utf8 java/lang/invoke/LambdaMetafactory #77 = Utf8 metafactory #78 = Class #83 // java/lang/invoke/MethodHandles$Lookup #79 = Utf8 Lookup #80 = Utf8 InnerClasses #81 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; #82 = Class #84 // java/lang/invoke/MethodHandles #83 = Utf8 java/lang/invoke/MethodHandles$Lookup #84 = Utf8 java/lang/invoke/MethodHandles { public com.luo.demo.Application(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 12: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/luo/demo/Application; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=2, args_size=1 0: iconst_3 1: anewarray #2 // class java/lang/String 4: dup 5: iconst_0 6: ldc #3 // String I 8: aastore 9: dup 10: iconst_1 11: ldc #4 // String love 13: aastore 14: dup 15: iconst_2 16: ldc #5 // String you 18: aastore 19: invokestatic #6 // Method cn/hutool/core/collection/CollectionUtil.newArrayList:([Ljava/lang/Object;)Ljava/util/ArrayList; 22: astore_1 23: aload_1 24: invokedynamic #7, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer; 29: invokevirtual #8 // Method java/util/ArrayList.forEach:(Ljava/util/function/Consumer;)V 32: return LineNumberTable: line 15: 0 line 16: 23 line 19: 32 LocalVariableTable: Start Length Slot Name Signature 0 33 0 args [Ljava/lang/String; 23 10 1 list Ljava/util/ArrayList; LocalVariableTypeTable: Start Length Slot Name Signature 23 10 1 list Ljava/util/ArrayList<Ljava/lang/String;>; private static void lambda$main$0(java.lang.String); descriptor: (Ljava/lang/String;)V flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC Code: stack=2, locals=1, args_size=1 0: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 7: return LineNumberTable: line 17: 0 line 18: 7 LocalVariableTable: Start Length Slot Name Signature 0 8 0 item Ljava/lang/String; }
通過字節碼可以看出,調用lambda方法時使用了invokedynamic,該字節碼命令是為了支持動態語言特性而在Java7中新增的。Java的lambda表達式實現上也就借助於invokedynamic命令。
字節碼中每一處含有invokeDynamic指令的位置都稱為“動態調用點”,這條指令的第一個參數不再是代表方法調用符號引用的CONSTANT_Methodref_info常亮,而是變成為JDK7新加入的CONSTANT_InvokeDynamic_info常量,從這個新常量中可得到3項信息:引導方法(Bootstrap Method,此方法存放在新增的BootstrapMethods屬性中)、方法類型和名稱。引導方法是有固定的參數,並且返回值是java.lang.invoke.CallSite對象,這個代表真正要執行的目標方法調用。根據CONSTANT_InvokeDynamic_info常量中提供的信息,虛擬機可以找到並執行引導方法,從而獲得一個CallSite對象,最終調用要執行的目標方法。
從上述mian方法的字節碼可見,有一個invokeDynamic指令,他的參數為第7項常量(第二個值為0的參數HotSpot中用不到,占位符):
invokedynamic #7, 0 // InvokeDynamic #0:accept ()Ljava/util/function/Consumer;
常量池中第7項是#7 = InvokeDynamic #0:#48 // #0:accept:()Ljava/util/function/Consumer;,說明它是一項CONSTANT_InvokeDynamic_info常量,常量值中前面的#0表示引導方法取BootstrapMethods屬性表的第0項,而后面的#48表示引用第48項類型為CONSTANAT_NameAndType_info的常量,從這個常量中可以獲取方法名稱和描述符,即accept方法。
BootstrapMethods: 0: #44 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; Method arguments: #45 (Ljava/lang/Object;)V #46 invokestatic com/luo/demo/Application.lambda$main$0:(Ljava/lang/String;)V #47 (Ljava/lang/String;)V
上圖是在lambda代碼中打斷點時的調用棧信息,如果在這里的lambda中打印當前所屬class,就是Application類,也印證了前面分析的lambda代碼會生成一個private方法。
從調用棧的信息來看,是在accept方法中調用lambda對應的private方法(ambda$main$0)的,但是這里的accept方法是屬於什么對象呢?從圖中看是一串數字字符串,這里可以理解成一個Consumer接口的實現類即可,每個lambda表達式可以理解成在一個新的Consumer實現類中調用的即可。使用命令jmap -histo查看JVM進程類和對象信息可以看到這一行信息:
600: 1 16 com.luo.demo.Application$$Lambda$5/1615039080
參考資料: