最近使用lambda表達式,感覺使用起來非常舒服,箭頭函數極大增強了代碼的表達能力。于是決心花點時間深入地去研究一下java8的函數式。
一、lambda表達式
先po一個最經典的例子——線程
public static void main(String[] args) {// Java7new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(i);}}}).start();// Java8new Thread(() -> {for (int i = 0; i < 100; i++) {System.out.println(i);}}).start();
}
復制代碼
第一次接觸lambda表達式是在創建線程時,比較直觀的感受就是lambda表達式相當于匿名類的語法糖,emm~,真甜。不過事實上,lambda表達式并不是匿名類的語法糖,而且經過一段時間的使用,感覺恰恰相反,在使用上匿名類更像是Java中lambda表達式的載體。
使用場景
下面的一些使用場景均為個人的一些體會,可能存在不當或遺漏之處。
1. 簡化匿名類的編碼
上面的創建線程就是一個很好簡化編碼的例子,此處就不再重復。
2. 減少不必要的方法創建
在Java中,我們經常會遇到這樣一種場景,某個方法只會在某處使用且內部邏輯也很簡單,在Java8之前我們通常都會創建一個方法,但是事實上我們經常會發現這樣寫著寫著,一個類中的方法可能會變得非常龐雜,嚴重影響閱讀體驗,進而影響編碼效率。但是如果使用lambda表達式,那么這個問題就可以很容易就解決掉了。
一個簡單的例子,如果我們需要在一個函數中多次打印時間。(這個例子可能有些牽強,但是實際上還是挺常遇見的)
public class FunctionMain {public static void main(String[] args) {TimeDemo timeDemo = new TimeDemo();timeDemo.createTime = System.currentTimeMillis();timeDemo.updateTime = System.currentTimeMillis() + 10000;outputTimeDemo(timeDemo);}private static void outputTimeDemo(TimeDemo timeDemo) {Function timestampToDate = timestamp -> {DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");return df.format(new Date(timestamp));};System.out.println(timestampToDate.apply(timeDemo.createTime));System.out.println(timestampToDate.apply(timeDemo.updateTime));}interface Function {String apply(long timestamp);}
}class TimeDemo {long createTime;long updateTime;
}
復制代碼
在這段代碼的outputTimeDemo中我們可以看到,對于時間戳轉換的內容,我們并沒有額外創建一個方法,而是類似于創建了一個變量來表達。不過,這個時候出現了另一個問題,雖然我們少創建了一個方法,但是我們卻多創建了一個接口Function,總有種因小失大的感覺, 不過這個問題,我們在后面的java.util.function包部分可以找到答案。
3. 事件處理
一個比較常見的例子就是回調。
public static void main(String[] args) {execute("hello world", () -> System.out.println("callback"));
}private static void execute(String s, Callback callback) {System.out.println(s);callback.callback();
}@FunctionalInterface
interface Callback {void callback();
}
復制代碼
在這里,可以發現一點小不同,就是Callback多了一個注解@FunctionalInterface,這個注解主要用于編譯期檢查,如果我們的接口不符合函數式接口的要求,那編譯的時候就會報錯。不加也是可以正常執行的。
4. stream中使用
這個在后面的stream中詳解。
java.util.function包
在之前的例子中,我們發現使用lambda表達式的時候,經常需要定義一些接口用來輔助我們的編碼,這樣就會使得本應輕量級的lambda表達式又變得重量級。那是否存在解決方案呢?其實Java8本身已經為我們提供了一些常見的函數式接口,就在java.util.function包下面。
接口 | 描述 |
---|---|
Function<T,R> | 接受一個輸入參數,返回一個結果 |
Supplier<T> | 無參數,返回一個結果 |
Consumer<T> | 接受一個輸入參數,并且不返回任何結果 |
BiFunction<T,U,R> | 接受兩個輸入參數的方法,并且返回一個結果 |
BiConsumer<T,U> | 接受兩個輸入參數的操作,并且不返回任何結果 |
此處列出最基本的幾個,其他的都是在這些的基礎上做了一些簡單的封裝,例如IntFunction<R>就是對Function<T,R>的封裝。上面的這些函數式接口已經可以幫助我們處理絕大多數場景了,如果有更復雜的情況,那就得我們自己定義接口了。不過遺憾的是在java.util.function下沒找到無參數無返回結果的接口,目前我找到的方案就是自己定義一個接口或者直接使用Runnable接口。
使用示例
public static void main(String[] args) {Function<Integer, Integer> f = x -> x + 1;System.out.println(f.apply(1));BiFunction<Integer, Integer, Integer> g = (x, y) -> x + y;System.out.println(g.apply(1, 2));
}
復制代碼
lambda表達式和匿名類的區別
lambda表達式雖然使用時和匿名類很相似,但是還是存在那么一些區別。
1. this指向不同
lambda表達式中使用this指向的是外部的類,而匿名類中使用this則指向的是匿名類本身。
public class FunctionMain {private String test = "test-main";public static void main(String[] args) {new FunctionMain().output();}private void output() {Function f = () -> {System.out.println("1:-----------------");System.out.println(this);System.out.println(this.test);};f.outputThis();new Function() {@Overridepublic void outputThis() {System.out.println("2:-----------------");System.out.println(this);System.out.println(this.test);}}.outputThis();}interface Function {String test = "test-function";void outputThis();}
}
復制代碼
如上面這段代碼,輸出結果如下
所以如果想使用lambda表達式的同時去訪問原類中的變量、方法的是做不到的。
2. 底層實現不同
編譯
從編譯結果來看,兩者的編譯結果完全不同。
首先是匿名類的方式,代碼如下:
import java.util.function.Function;public class ClassMain {public static void main(String[] args) {Function<Integer, Integer> f = new Function<Integer, Integer>() {@Overridepublic Integer apply(Integer integer) {return integer + 1;}};System.out.println(f.apply(1));}
}
復制代碼
編譯后的結果如下:
可以看到ClassMain在編譯后生成了兩個class,其中ClassMain$1.class就是匿名類生成的class。
那么接下來,我們再來編譯一下lambda版本的。代碼和編譯結果如下:
import java.util.function.Function;public class FunctionMain {public static void main(String[] args) {Function<Integer, Integer> f = x -> x + 1;System.out.println(f.apply(1));}
}
復制代碼
在這里我們可以看到FunctionMain并沒有生成第二個class文件。
字節碼
更進一步,我們打開他們的字節碼來尋找更多的細節。首先依然是匿名類的方式
在Code-0這一行,我們可以看到匿名類的方式是通過new一個類來實現的。
接下來是lambda表達式生成的字節碼,
在lambda表達式的字節碼中,我們可以看到我們的lambda表達式被編譯成了一個叫做lambda$main$0
的靜態方法,接著通過invokedynamic的方式進行了調用。
3. lambda表達式只能替代部分匿名類
lambda表達式想要替代匿名類是有條件的,即這個匿名類實現的接口必須是函數式接口,即只能有一個抽象方法的接口。
性能
由于沒有實際測試過lambda表達式的性能,且我使用lambda更多是基于編碼簡潔度的考慮,因此本文就不探討性能相關問題。
關于lambda表達式和匿名類的性能對比可以參考官方ppt www.oracle.com/technetwork…
二、Stream API
Stream API是Java8對集合類的補充與增強。它主要用來對集合進行各種便利的聚合操作或者批量數據操作。
1. 創建流
在進行流操作的第一步是創建一個流,下面介紹幾種常見的流的創建方式
從集合類創建流
如果已經我們已經有一個集合對象,那么我們可以直接通過調用其stream()方法得到對應的流。如下
List<String> list = Arrays.asList("hello", "world", "la");
list.stream();
復制代碼
利用數組創建流
String[] strArray = new String[]{"hello", "world", "la"};
Stream.of(strArray);
復制代碼
利用可變參數創建流
Stream.of("hello", "world", "la");
復制代碼
根據范圍創建數值流
IntStream.range(0, 100); // 不包含最后一個數
IntStream.rangeClosed(0, 99); // 包含最后一個數
復制代碼
BufferReader.lines()
對于BufferReader而言,它的lines方法也同樣可以創建一個流
File file = new File("/Users/cayun/.m2/settings.xml");
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
br.lines().forEach(System.out::println);
br.close();
復制代碼
2. 流操作
在Stream API中,流的操作有兩種:Intermediate和Terminal
Intermediate:一個流可以后面跟隨零個或多個 intermediate 操作。其目的主要是打開流,做出某種程度的數據映射/過濾,然后返回一個新的流,交給下一個操作使用。這類操作都是惰性化的(lazy),就是說,僅僅調用到這類方法,并沒有真正開始流的遍歷。 Terminal:一個流只能有一個 terminal 操作,當這個操作執行后,流就被使用“光”了,無法再被操作。所以這必定是流的最后一個操作。Terminal 操作的執行,才會真正開始流的遍歷,并且會生成一個結果,或者一個 side effect。
除此以外,還有一種叫做short-circuiting的操作
對于一個 intermediate 操作,如果它接受的是一個無限大(infinite/unbounded)的 Stream,但返回一個有限的新 Stream。 對于一個 terminal 操作,如果它接受的是一個無限大的 Stream,但能在有限的時間計算出結果。
常見的流操作可以如下歸類:
Intermediate map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
Terminal forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
Short-circuiting anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
常見的流操作詳解
1. forEach
forEach可以說是最常見的操作了,甚至對于List等實現了Collection接口的類可以不創建stream而直接使用forEach。簡單地說,forEach就是遍歷并執行某個操作。
Stream.of("hello", "world", "a", "b").forEach(System.out::println);
復制代碼
2. map
map也同樣是一個非常高頻的流操作,用來將一個集合映射為另一個集合。下面代碼展示了將[1,2,3,4]映射為[1,4,9,16]
IntStream.rangeClosed(1, 4).map(x -> x * x).forEach(System.out::println);
復制代碼
除此之外,還有一個叫做flatMap的操作,這個操作在映射的基礎上又做了一層扁平化處理。這個概念可能比較難理解,那舉個例子,我們需要將["hello", "world"]轉換成[h,e,l,l,o,w,o,r,l,d],可以嘗試一下使用map,那你會驚訝地發現,可能結果不是你想象中的那樣。如果不信可以執行下面這段代碼,就會發現map與flatMap之間的區別了,
Stream.of("hello", "world").map(s -> s.split("")).forEach(System.out::println);
System.out.println("--------------");
Stream.of("hello", "world").flatMap(s -> Stream.of(s.split(""))).forEach(System.out::println);
復制代碼
3. filter
filter則實現了過濾的功能,如果只需要[1,2,3,4,5]中的奇數,可以如下,
IntStream.rangeClosed(1, 5).filter(x -> x % 2 == 1).forEach(System.out::println);
復制代碼
4. sorted和distinct
其中sorted表示排序,distinct表示去重,簡單的示例如下:
Integer[] arr = new Integer[]{5, 1, 2, 1, 3, 1, 2, 4}; // 千萬不要用int
Stream.of(arr).sorted().forEach(System.out::println);
Stream.of(arr).distinct().forEach(System.out::println);
Stream.of(arr).distinct().sorted().forEach(System.out::println);
復制代碼
5. collect
在流操作中,我們往往需求是從一個List得到另一個List,而不是直接通過forEach來打印。那么這個時候就需要使用到collect了。依然是之前的例子,將[1,2,3,4]轉換成[1,4,9,16]。
List<Integer> list1= Stream.of(1, 2, 3, 4).map(x -> x * x).collect(Collectors.toList());// 對于IntStream生成的流需要使用mapToObj而不是map
List<Integer> list2 = IntStream.rangeClosed(1, 4).mapToObj(x -> x * x).collect(Collectors.toList());
復制代碼
3. 補充
并行流
除了普通的stream之外還有parallelStream,區別比較直觀,就是stream是單線程執行,parallelStream為多線程執行。parallelStream的創建及使用基本與stream類似,
List<Integer> list = Arrays.asList(1, 2, 3, 4);
// 直接創建一個并行流
list.parallelStream().map(x -> x * x).forEach(System.out::println);
// 或者將一個普通流轉換成并行流
list.stream().parallel().map(x -> x * x).forEach(System.out::println);
復制代碼
不過由于是并行執行,parallelStream并不保證結果順序,同樣由于這個特性,如果能使用findAny就盡量不要使用findFirst。
使用parallelStream時需要注意的一點是,多個parallelStream之間默認使用的是同一個線程池,所以IO操作盡量不要放進parallelStream中,否則會阻塞其他parallelStream。
三、Optional
Optional的引入是為了解決空指針異常的問題,事實上在Java8之前,Optional在很多地方已經較為廣泛使用了,例如scala、谷歌的Guava庫等。
在實際生產中我們經常會遇到如下這種情況,
public class FunctionMain {public static void main(String[] args) {Person person = new Person();String result = null;if (person != null) {Address address = person.address;if (address != null) {Country country = address.country;if (country != null) {result = country.name;}}}System.out.println(result);}
}class Person {Address address;
}class Address {Country country;
}class Country {String name;
}
復制代碼
每每寫到這樣的代碼,作為編碼者一定都會頭皮發麻,滿心地不想寫,但是卻不得不寫。這個問題如果使用Optional,或許你就能找到你想要的答案了。
Optional的基本操作
1. 創建Optional
Optional.empty(); // 創建一個空Optional
Optional.of(T value); // 不接受null,會報NullPointerException異常
Optional.ofNullable(T value); // 可以接受null
復制代碼
2. 獲取結果
get(); // 返回里面的值,如果值為null,則拋異常
orElse(T other); // 有值則返回值,null則返回other
orElseGet(Supplier other); // 有值則返回值,null則由提供的lambda表達式生成值
orElseThrow(Supplier exceptionSupplier); // 有值則返回值,null則拋出異常
復制代碼
3. 判斷是否為空
isPresent(); // 判斷是否為空
復制代碼
到這里,我們可能會開始考慮怎么用Optional解決引言中的問題了,于是思考半天,寫出了這樣一段代碼,
public static void main(String[] args) {Person person = new Person();String result = null;Optional<Person> per = Optional.ofNullable(person);if (per.isPresent()) {Optional<Address> address = Optional.ofNullable(per.get().address);if (address.isPresent()) {Optional<Country> country = Optional.ofNullable(address.get().country);if (country.isPresent()) {result = Optional.ofNullable(country.get().name).orElse(null);}}}System.out.println(result);
}
復制代碼
啊嘞嘞,感覺不僅沒有使得代碼變得簡單,反而變得更加復雜了。那么很顯然這并不是Optional的正確使用方法。接下來的部分才是Optional的正確使用方式。
4. 鏈式方法
在Optional中也有類似于Stream API中的鏈式方法map、flatMap、filter、ifPresent。這些方法才是Optional的精髓。此處以最典型的map作為例子,可以看看map的源碼
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {Objects.requireNonNull(mapper);if (!isPresent())return empty();else {return Optional.ofNullable(mapper.apply(value));}
}
復制代碼
源碼很簡單,可以看到對于null情況仍然返回null,否則返回處理結果。那么此再來思考一下引言的問題,那就可以很簡單地改寫成如下的寫法,
public static void main(String[] args) {Person person = new Person();String result = Optional.ofNullable(person).map(per -> per.address).map(address -> address.country).map(country -> country.name).orElse(null);System.out.println(result);
}
復制代碼
哇哇哇,相比原先的null寫法真真是舒服太多了。
map與flatMap的區別
這兩者的區別,同樣使用一個簡單的例子來解釋一下吧,
public class FunctionMain {public static void main(String[] args) {Person person = new Person();String name = Optional.ofNullable(person).flatMap(p -> p.name).orElse(null);System.out.println(name);}
}class Person {Optional<String> name;
}
復制代碼
在這里使用的不是map而是flatMap,稍微觀察一下,可以發現Person中的name不再是String類型,而是Optional<String>類型了,如果使用map的話,那map的結果就是Optional<Optional<String>>了,很顯然不是我們想要的,flatMap就是用來將最終的結果扁平化(簡單地描述,就是消除嵌套)的。
至于filter和ifPresent用法類似,就不再敘述了。
四、其他一些函數式概念在Java中的實現
由于個人目前為止也只是初探函數式階段,很多地方了解也不多,此處只列舉兩個。(注意:下面的部分應用函數與柯里化對應的是scala中的概念,其他語言中可能略有偏差)
部分應用函數(偏應用函數)
部分應用函數指的是對于一個有n個參數的函數f,但是我們只提供m個參數給它(m < n),那么我們就可以得到一個部分應用函數,簡單地描述一下,如下
在這里就是
的一個部分應用函數。
BiFunction<Integer, Integer, Integer> f = (x, y) -> x + y;
Function<Integer, Integer> g = x -> f.apply(1, x);
System.out.println(g.apply(2));
復制代碼
柯里化
柯里化就是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,并且返回接受余下的參數而且返回結果的新函數的技術。換個描述,如下
Java中對柯里化的實現如下,
Function<Integer, Function<Integer, Integer>> f = x -> y -> x + y;
System.out.println(f.apply(1).apply(2));
復制代碼
因為Java限制,我們不得不寫成f.apply(1).apply(2)
的形式,不過視覺上的體驗與直接寫成f(1)(2)
相差就很大了。
柯里化與部分應用函數感覺很相像,不過因為個人幾乎未使用過這兩者,因此此處就不發表更多見解。
參考
[1] java.util.stream 庫簡介
[2] Java 8 中的 Streams API 詳解
[3] 了解、接受和利用Java中的Optional(類)
[4] 維基百科-柯里化
[5] 維基百科-λ演算