來自專輯
我有點兒基礎
java
古時的風箏第 75 篇原創文章 程序員
做者 | 風箏
古時的風箏(ID:gushidefengzheng)
轉載請聯繫受權,掃碼文末二維碼加微信編程
就在今年 Java 25週歲了,可能比在座的各位中的一些少年年齡還大,但使人遺憾的是,居然沒有我大,不由感嘆,Java 仍是過小了。(難道我會說是由於我老了?)數組
而就在上個月,Java 15 的試驗版悄悄發佈了,可是在 Java 界一直有個神祕現象,那就是「你發你發任你發,個人最愛 Java 8」.微信
據 Snyk 和 The Java Magazine 聯合推出發佈的 2020 JVM 生態調查報告顯示,在全部的 Java 版本中,仍然有 64% 的開發者使用 Java 8。另一些開發者可能已經開始用 Java 九、Java 十一、Java 13 了,固然還有一些神仙開發者還在堅持使用 JDK 1.6 和 1.7。數據結構
儘管 Java 8 發佈多年,使用者衆多,可神奇的是居然有不少同窗沒有用過 Java 8 的新特性,好比 Lambda表達式、好比方法引用,再好比今天要說的 Stream。其實 Stream 就是以 Lambda 和方法引用爲基礎,封裝的簡單易用、函數式風格的 API。閉包
Java 8 是在 2014 年發佈的,實話說,風箏我也是在 Java 8 發佈後很長一段時間才用的 Stream,由於 Java 8 發佈的時候我還在 C# 的世界中掙扎,而使用 Lambda 表達式卻很早了,由於 Python 中用 Lambda 很方便,沒錯,我寫 Python 的時間要比 Java 的時間還長。
app
要講 Stream ,那就不得不先說一下它的左膀右臂 Lambda 和方法引用,你用的 Stream API 其實就是函數式的編程風格,其中的「函數」就是方法引用,「式」就是 Lambda 表達式。
框架
Lambda 表達式是一個匿名函數,Lambda表達式基於數學中的λ演算得名,直接對應於其中的lambda抽象,是一個匿名函數,即沒有函數名的函數。Lambda表達式能夠表示閉包。
在 Java 中,Lambda 表達式的格式是像下面這樣dom
// 無參數,無返回值 () -> log.info("Lambda") // 有參數,有返回值 (int a, int b) -> { a+b }
其等價於
log.info("Lambda"); private int plus(int a, int b){ return a+b; }
最多見的一個例子就是新建線程,有時候爲了省事,會用下面的方法建立並啓動一個線程,這是匿名內部類的寫法,new Thread須要一個 implements 自Runnable類型的對象實例做爲參數,比較好的方式是建立一個新類,這個類 implements Runnable,而後 new 出這個新類的實例做爲參數傳給 Thread。而匿名內部類不用找對象接收,直接當作參數。
new Thread(new Runnable() { @Override public void run() { System.out.println("快速新建並啓動一個線程"); } }).run();
可是這樣寫是否是感受看上去很亂、很土,而這時候,換上 Lambda 表達式就是另一種感受了。
new Thread(()->{ System.out.println("快速新建並啓動一個線程"); }).run();
怎麼樣,這樣一改,瞬間感受清新脫俗了很多,簡潔優雅了很多。
Lambda 表達式簡化了匿名內部類的形式,能夠達到一樣的效果,可是 Lambda 要優雅的多。雖然最終達到的目的是同樣的,但其實內部的實現原理卻不相同。
匿名內部類在編譯以後會建立一個新的匿名內部類出來,而 Lambda 是調用 JVM invokedynamic指令實現的,並不會產生新類。
方法引用的出現,使得咱們能夠將一個方法賦給一個變量或者做爲參數傳遞給另一個方法。::雙冒號做爲方法引用的符號,好比下面這兩行語句,引用 Integer類的 parseInt方法。
Function<String, Integer> s = Integer::parseInt; Integer i = s.apply("10");
或者下面這兩行,引用 Integer類的 compare方法。
Comparator<Integer> comparator = Integer::compare; int result = comparator.compare(100,10);
再好比,下面這兩行代碼,一樣是引用 Integer類的 compare方法,可是返回類型卻不同,但卻都能正常執行,並正確返回。
IntBinaryOperator intBinaryOperator = Integer::compare; int result = intBinaryOperator.applyAsInt(10,100);
相信有的同窗看到這裏恐怕是下面這個狀態,徹底不可理喻嗎,也太隨便了吧,返回給誰都能接盤。
先別激動,來來來,如今我們就來解惑,解除蒙圈臉。
Q:什麼樣的方法能夠被引用?
A:這麼說吧,任何你有辦法訪問到的方法均可以被引用。
Q:返回值究竟是什麼類型?
A:這就問到點兒上了,上面又是 Function、又是Comparator、又是 IntBinaryOperator的,看上去好像沒有規律,其實否則。
返回的類型是 Java 8 專門定義的函數式接口,這類接口用 @FunctionalInterface 註解。
好比 Function這個函數式接口的定義以下:
@FunctionalInterface public interface Function<T, R> { R apply(T t); }
還有很關鍵的一點,你的引用方法的參數個數、類型,返回值類型要和函數式接口中的方法聲明一一對應才行。
好比 Integer.parseInt方法定義以下:
public static int parseInt(String s) throws NumberFormatException { return parseInt(s,10); }
首先parseInt方法的參數個數是 1 個,而 Function中的 apply方法參數個數也是 1 個,參數個數對應上了,再來,apply方法的參數類型和返回類型是泛型類型,因此確定能和 parseInt方法對應上。
這樣一來,就能夠正確的接收Integer::parseInt的方法引用,並能夠調用Funciton的apply方法,這時候,調用到的其實就是對應的 Integer.parseInt方法了。
用這套標準套到 Integer::compare方法上,就不難理解爲何便可以用 Comparator<Integer>接收,又能夠用 IntBinaryOperator接收了,並且調用它們各自的方法都能正確的返回結果。
Integer.compare方法定義以下:
public static int compare(int x, int y) { return (x < y) ? -1 : ((x == y) ? 0 : 1); }
返回值類型 int,兩個參數,而且參數類型都是 int。
而後來看Comparator和IntBinaryOperator它們兩個的函數式接口定義和其中對應的方法:
@FunctionalInterface public interface Comparator<T> { int compare(T o1, T o2); } @FunctionalInterface public interface IntBinaryOperator { int applyAsInt(int left, int right); }
對不對,都能正確的匹配上,因此前面示例中用這兩個函數式接口都能正常接收。其實不止這兩個,只要是在某個函數式接口中聲明瞭這樣的方法:兩個參數,參數類型是 int或者泛型,而且返回值是 int或者泛型的,均可以完美接收。
JDK 中定義了不少函數式接口,主要在 java.util.function包下,還有 java.util.Comparator 專門用做定製比較器。另外,前面說的 Runnable也是一個函數式接口。
定義了名稱爲 KiteFunction 的函數式接口,使用 @FunctionalInterface註解,而後聲明瞭具備兩個參數的方法 run,都是泛型類型,返回結果也是泛型。
還有一點很重要,函數式接口中只能聲明一個可被實現的方法,你不能聲明瞭一個 run方法,又聲明一個 start方法,到時候編譯器就不知道用哪一個接收了。而用default 關鍵字修飾的方法則沒有影響。
@FunctionalInterface public interface KiteFunction<T, R, S> { /** * 定義一個雙參數的方法 * @param t * @param s * @return */ R run(T t,S s); }
在 FunctionTest 類中定義了方法 DateFormat,一個將 LocalDateTime類型格式化爲字符串類型的方法。
public class FunctionTest { public static String DateFormat(LocalDateTime dateTime, String partten) { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten); return dateTime.format(dateTimeFormatter); } }
3.用方法引用的方式調用
正常狀況下咱們直接使用 FunctionTest.DateFormat()就能夠了。
而用函數式方式,是這樣的。
KiteFunction<LocalDateTime,String,String> functionDateFormat = FunctionTest::DateFormat; String dateString = functionDateFormat.run(LocalDateTime.now(),"yyyy-MM-dd HH:mm:ss");
而其實我能夠不專門在外面定義 DateFormat這個方法,而是像下面這樣,使用匿名內部類。
public static void main(String[] args) throws Exception { String dateString = new KiteFunction<LocalDateTime, String, String>() { @Override public String run(LocalDateTime localDateTime, String s) { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(s); return localDateTime.format(dateTimeFormatter); } }.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss"); System.out.println(dateString); }
前面第一個 Runnable的例子也提到了,這樣的匿名內部類能夠用 Lambda 表達式的形式簡寫,簡寫後的代碼以下:
public static void main(String[] args) throws Exception { KiteFunction<LocalDateTime, String, String> functionDateFormat = (LocalDateTime dateTime, String partten) -> { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten); return dateTime.format(dateTimeFormatter); }; String dateString = functionDateFormat.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss"); System.out.println(dateString); }
使用(LocalDateTime dateTime, String partten) -> { } 這樣的 Lambda 表達式直接返回方法引用。
爲了說一下 Stream API 的使用,能夠說是大費周章啊,知其然,也要知其因此然嗎,追求技術的態度和姿式要正確。
固然 Stream 也不僅是 Lambda 表達式就厲害了,真正厲害的仍是它的功能,Stream 是 Java 8 中集合數據處理的利器,不少原本複雜、須要寫不少代碼的方法,好比過濾、分組等操做,每每使用 Stream 就能夠在一行代碼搞定,固然也由於 Stream 都是鏈式操做,一行代碼可能會調用好幾個方法。
Collection接口提供了 stream()方法,讓咱們能夠在一個集合方便的使用 Stream API 來進行各類操做。值得注意的是,咱們執行的任何操做都不會對源集合形成影響,你能夠同時在一個集合上提取出多個 stream 進行操做。
咱們看 Stream 接口的定義,繼承自 BaseStream,幾乎全部的接口聲明都是接收方法引用類型的參數,好比 filter方法,接收了一個 Predicate類型的參數,它就是一個函數式接口,經常使用來做爲條件比較、篩選、過濾用,JPA中也使用了這個函數式接口用來作查詢條件拼接。
public interface Stream<T> extends BaseStream<T, Stream<T>> { Stream<T> filter(Predicate<? super T> predicate); // 其餘接口 }
下面就來看看 Stream 經常使用 API。
of
可接收一個泛型對象或可變成泛型集合,構造一個 Stream 對象。
private static void createStream(){ Stream<String> stringStream = Stream.of("a","b","c"); }
empty
建立一個空的 Stream 對象。
concat
鏈接兩個 Stream ,不改變其中任何一個 Steam 對象,返回一個新的 Stream 對象。
private static void concatStream(){ Stream<String> a = Stream.of("a","b","c"); Stream<String> b = Stream.of("d","e"); Stream<String> c = Stream.concat(a,b); }
max
通常用於求數字集合中的最大值,或者按實體中數字類型的屬性比較,擁有最大值的那個實體。它接收一個 Comparator<T>,上面也舉到這個例子了,它是一個函數式接口類型,專門用做定義兩個對象之間的比較,例以下面這個方法使用了 Integer::compareTo這個方法引用。
private static void max(){ Stream<Integer> integerStream = Stream.of(2, 2, 100, 5); Integer max = integerStream.max(Integer::compareTo).get(); System.out.println(max); }
固然,咱們也能夠本身定製一個 Comparator,順便複習一下 Lambda 表達式形式的方法引用。
private static void max(){ Stream<Integer> integerStream = Stream.of(2, 2, 100, 5); Comparator<Integer> comparator = (x, y) -> (x.intValue() < y.intValue()) ? -1 : ((x.equals(y)) ? 0 : 1); Integer max = integerStream.max(comparator).get(); System.out.println(max); }
min
與 max 用法同樣,只不過是求最小值。
findFirst
獲取 Stream 中的第一個元素。
findAny
獲取 Stream 中的某個元素,若是是串行狀況下,通常都會返回第一個元素,並行狀況下就不必定了。
count
返回元素個數。
Stream<String> a = Stream.of("a", "b", "c"); long x = a.count();
peek
創建一個通道,在這個通道中對 Stream 的每一個元素執行對應的操做,對應 Consumer<T>的函數式接口,這是一個消費者函數式接口,顧名思義,它是用來消費 Stream 元素的,好比下面這個方法,把每一個元素轉換成對應的大寫字母並輸出。
private static void peek() { Stream<String> a = Stream.of("a", "b", "c"); List<String> list = a.peek(e->System.out.println(e.toUpperCase())).collect(Collectors.toList()); }
forEach
和 peek 方法相似,都接收一個消費者函數式接口,能夠對每一個元素進行對應的操做,可是和 peek 不一樣的是,forEach 執行以後,這個 Stream 就真的被消費掉了,以後這個 Stream 流就沒有了,不能夠再對它進行後續操做了,而 peek操做完以後,仍是一個可操做的 Stream 對象。
正好藉着這個說一下,咱們在使用 Stream API 的時候,都是一串鏈式操做,這是由於不少方法,好比接下來要說到的 filter方法等,返回值仍是這個 Stream 類型的,也就是被當前方法處理過的 Stream 對象,因此 Stream API 仍然可使用。
private static void forEach() { Stream<String> a = Stream.of("a", "b", "c"); a.forEach(e->System.out.println(e.toUpperCase())); }
forEachOrdered
功能與 forEach是同樣的,不一樣的是,forEachOrdered是有順序保證的,也就是對 Stream 中元素按插入時的順序進行消費。爲何這麼說呢,當開啓並行的時候,forEach和 forEachOrdered的效果就不同了。
Stream<String> a = Stream.of("a", "b", "c"); a.parallel().forEach(e->System.out.println(e.toUpperCase()));
當使用上面的代碼時,輸出的結果多是 B、A、C 或者 A、C、B或者A、B、C,而使用下面的代碼,則每次都是 A、 B、C
Stream<String> a = Stream.of("a", "b", "c"); a.parallel().forEachOrdered(e->System.out.println(e.toUpperCase())); limit
獲取前 n 條數據,相似於 MySQL 的limit,只不過只能接收一個參數,就是數據條數。
private static void limit() { Stream<String> a = Stream.of("a", "b", "c"); a.limit(2).forEach(e->System.out.println(e)); }
上述代碼打印的結果是 a、b。
skip
跳過前 n 條數據,例以下面代碼,返回結果是 c。
private static void skip() { Stream<String> a = Stream.of("a", "b", "c"); a.skip(2).forEach(e->System.out.println(e)); }
distinct
元素去重,例以下面方法返回元素是 a、b、c,將重複的 b 只保留了一個。
private static void distinct() { Stream<String> a = Stream.of("a", "b", "c","b"); a.distinct().forEach(e->System.out.println(e)); }
sorted
有兩個重載,一個無參數,另一個有個 Comparator類型的參數。
無參類型的按照天然順序進行排序,只適合比較單純的元素,好比數字、字母等。
private static void sorted() { Stream<String> a = Stream.of("a", "c", "b"); a.sorted().forEach(e->System.out.println(e)); }
有參數的須要自定義排序規則,例以下面這個方法,按照第二個字母的大小順序排序,最後輸出的結果是 a一、b三、c6。
private static void sortedWithComparator() { Stream<String> a = Stream.of("a1", "c6", "b3"); a.sorted((x,y)->Integer.parseInt(x.substring(1))>Integer.parseInt(y.substring(1))?1:-1).forEach(e->System.out.println(e)); }
爲了更好的說明接下來的幾個 API ,我模擬了幾條項目中常常用到的相似數據,10條用戶信息。
private static List<User> getUserData() { Random random = new Random(); List<User> users = new ArrayList<>(); for (int i = 1; i <= 10; i++) { User user = new User(); user.setUserId(i); user.setUserName(String.format("古時的風箏 %s 號", i)); user.setAge(random.nextInt(100)); user.setGender(i % 2); user.setPhone("18812021111"); user.setAddress("無"); users.add(user); } return users; }
filter
用於條件篩選過濾,篩選出符合條件的數據。例以下面這個方法,篩選出性別爲 0,年齡大於 50 的記錄。
private static void filter(){ List<User> users = getUserData(); Stream<User> stream = users.stream(); stream.filter(user -> user.getGender().equals(0) && user.getAge()>50).forEach(e->System.out.println(e)); /** *等同於下面這種形式 匿名內部類 */ // stream.filter(new Predicate<User>() { // @Override // public boolean test(User user) { // return user.getGender().equals(0) && user.getAge()>50; // } // }).forEach(e->System.out.println(e)); }
map
map方法的接口方法聲明以下,接受一個 Function函數式接口,把它翻譯成映射最合適了,經過原始數據元素,映射出新的類型。
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
而 Function的聲明是這樣的,觀察 apply方法,接受一個 T 型參數,返回一個 R 型參數。用於將一個類型轉換成另一個類型正合適,這也是 map的初衷所在,用於改變當前元素的類型,例如將 Integer 轉爲 String類型,將 DAO 實體類型,轉換爲 DTO 實例類型。
固然了,T 和 R 的類型也能夠同樣,這樣的話,就和 peek方法沒什麼不一樣了。
@FunctionalInterface public interface Function<T, R> { /** * Applies this function to the given argument. * * @param t the function argument * @return the function result */ R apply(T t); }
例以下面這個方法,應該是業務系統的經常使用需求,將 User 轉換爲 API 輸出的數據格式。
private static void map(){ List<User> users = getUserData(); Stream<User> stream = users.stream(); List<UserDto> userDtos = stream.map(user -> dao2Dto(user)).collect(Collectors.toList()); } private static UserDto dao2Dto(User user){ UserDto dto = new UserDto(); BeanUtils.copyProperties(user, dto); //其餘額外處理 return dto; }
mapToInt
將元素轉換成 int 類型,在 map方法的基礎上進行封裝。
mapToLong
將元素轉換成 Long 類型,在 map方法的基礎上進行封裝。
mapToDouble
將元素轉換成 Double 類型,在 map方法的基礎上進行封裝。
flatMap
這是用在一些比較特別的場景下,當你的 Stream 是如下這幾種結構的時候,須要用到 flatMap方法,用於將原有二維結構扁平化。
Stream<String[]>
Stream<Set<String>>
好比下面這個方法,將List<List<User>>扁平處理,而後再使用 map或其餘方法進行操做。
private static void flatMap(){ List<User> users = getUserData(); List<User> users1 = getUserData(); List<List<User>> userList = new ArrayList<>(); userList.add(users); userList.add(users1); Stream<List<User>> stream = userList.stream(); List<UserDto> userDtos = stream.flatMap(subUserList->subUserList.stream()).map(user -> dao2Dto(user)).collect(Collectors.toList()); }
flatMapToInt
用法參考 flatMap,將元素扁平爲 int 類型,在 flatMap方法的基礎上進行封裝。
flatMapToLong
用法參考 flatMap,將元素扁平爲 Long 類型,在 flatMap方法的基礎上進行封裝。
flatMapToDouble
用法參考 flatMap,將元素扁平爲 Double 類型,在 flatMap方法的基礎上進行封裝。
collection
在進行了一系列操做以後,咱們最終的結果大多數時候並非爲了獲取 Stream 類型的數據,而是要把結果變爲 List、Map 這樣的經常使用數據結構,而 collection就是爲了實現這個目的。
就拿 map 方法的那個例子說明,將對象類型進行轉換後,最終咱們須要的結果集是一個 List<UserDto >類型的,使用 collect方法將 Stream 轉換爲咱們須要的類型。
下面是 collect接口方法的定義:
<R, A> R collect(Collector<? super T, A, R> collector);
下面這個例子演示了將一個簡單的 Integer Stream 過濾出大於 7 的值,而後轉換成 List<Integer>集合,用的是 Collectors.toList()這個收集器。
private static void collect(){ Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33); List<Integer> list = integerStream.filter(s -> s.intValue()>7).collect(Collectors.toList()); }
不少同窗表示看不太懂這個 Collector是怎麼一個意思,來,咱們看下面這段代碼,這是 collect的另外一個重載方法,你能夠理解爲它的參數是按順序執行的,這樣就清楚了,這就是個 ArrayList 從建立到調用 addAll方法的一個過程。
private static void collect(){ Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33); List<Integer> list = integerStream.filter(s -> s.intValue()>7).collect(ArrayList::new, ArrayList::add, ArrayList::addAll); }
咱們在自定義 Collector的時候其實也是這個邏輯,不過咱們根本不用自定義, Collectors已經爲咱們提供了不少拿來即用的收集器。好比咱們常常用到Collectors.toList()、Collectors.toSet()、Collectors.toMap()。另外還有好比Collectors.groupingBy()用來分組,好比下面這個例子,按照 userId 字段分組,返回以 userId 爲key,List爲value 的 Map,或者返回每一個 key 的個數。
// 返回 userId:List<User> Map<String,List<User>> map = user.stream().collect(Collectors.groupingBy(User::getUserId)); // 返回 userId:每組個數 Map<String,Long> map = user.stream().collect(Collectors.groupingBy(User::getUserId,Collectors.counting()));
toArray
collection是返回列表、map 等,toArray是返回數組,有兩個重載,一個空參數,返回的是 Object[]。
另外一個接收一個 IntFunction<R>類型參數。
@FunctionalInterface public interface IntFunction<R> { /** * Applies this function to the given argument. * * @param value the function argument * @return the function result */ R apply(int value); }
好比像下面這樣使用,參數是 User[]::new也就是new 一個 User 數組,長度爲最後的 Stream 長度。
private static void toArray() { List<User> users = getUserData(); Stream<User> stream = users.stream(); User[] userArray = stream.filter(user -> user.getGender().equals(0) && user.getAge() > 50).toArray(User[]::new); }
reduce
它的做用是每次計算的時候都用到上一次的計算結果,好比求和操做,前兩個數的和加上第三個數的和,再加上第四個數,一直加到最後一個數位置,最後返回結果,就是 reduce的工做過程。
private static void reduce(){ Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33); Integer sum = integerStream.reduce(0,(x,y)->x+y); System.out.println(sum); }
另外 Collectors好多方法都用到了 reduce,好比 groupingBy、minBy、maxBy等等。
Stream 本質上來講就是用來作數據處理的,爲了加快處理速度,Stream API 提供了並行處理 Stream 的方式。經過 users.parallelStream()或者users.stream().parallel() 的方式來建立並行 Stream 對象,支持的 API 和普通 Stream 幾乎是一致的。
並行 Stream 默認使用 ForkJoinPool線程池,固然也支持自定義,不過通常狀況下沒有必要。ForkJoin 框架的分治策略與並行流處理正好契合。
雖然並行這個詞聽上去很厲害,但並非全部狀況使用並行流都是正確的,不少時候徹底沒這個必要。
必須在多核 CPU 下才使用並行 Stream,聽上去好像是廢話。
在數據量不大的狀況下使用普通串行 Stream 就能夠了,使用並行 Stream 對性能影響不大。
CPU 密集型計算適合使用並行 Stream,而 IO 密集型使用並行 Stream 反而會更慢。
雖然計算是並行的可能很快,但最後大多數時候仍是要使用 collect合併的,若是合併代價很大,也不適合用並行 Stream。
Java 25 週歲了,有多少同窗跟我同樣在用 Java 8,還有多少同窗再用更早的版本,請說出你的故事。
還能夠讀:
別再重複造輪子了,這幾個開源工具庫送你了
隔離作的好,數據操做沒煩惱[MySQL]
公衆號:古時的風箏
一個兼具深度與廣度的程序員鼓勵師,一個本打算寫詩卻寫起了代碼的田園碼農!你可選擇如今就關注我,或者看看歷史文章再關注也不遲。
技術交流還能夠加羣或者直接加我微信。