背景
java8推出的集合操作流stream極大的方便了我們的開發,但stream支持的功能十分豐富,我們經常使用到的並不多。在開發中也發現有些人對stream並不熟悉,或者只會使用簡單的功能。
舉例來說 在 Java 7 中,如果要發現 type 為 grocery 的所有交易,然后返回以交易值降序排序好的交易 ID 集合,我們需要這樣寫:
List<Transaction> groceryTransactions = new Arraylist<>();
for(Transaction t: transactions){
if(t.getType() == Transaction.GROCERY){
groceryTransactions.add(t);
}
}
Collections.sort(groceryTransactions, new Comparator(){
public int compare(Transaction t1, Transaction t2){
return t2.getValue().compareTo(t1.getValue());
}
});
List<Integer> transactionIds = new ArrayList<>();
for(Transaction t: groceryTransactions){
transactionsIds.add(t.getId());
}
而在 Java 8 使用 Stream,代碼更加簡潔易讀;而且使用並發模式,程序執行速度更快。
List<Integer> transactionsIds = transactions.parallelStream().
filter(t -> t.getType() == Transaction.GROCERY).
sorted(comparing(Transaction::getValue).reversed()).
map(Transaction::getId).
collect(toList());
包括我自己對一些stream的api也不太熟悉,在使用時需要現去查詢相關的api資料。所以本文對在開發中經常使用到的集合類操作stream的api進行總結,並介紹利用idea開發工具調試stream時的小技巧。
stream介紹
1. 什么是流?
Stream 不是集合元素,它不是數據結構並不保存數據,它是有關算法和計算的,它更像一個高級版本的 Iterator。原始版本的 Iterator,用戶只能顯式地一個一個遍歷元素並對其執行某些操作;高級版本的 Stream,用戶只要給出需要對其包含的元素執行什么操作,比如 “過濾掉長度大於 10 的字符串”、“獲取每個字符串的首字母”等,Stream 會隱式地在內部進行遍歷,做出相應的數據轉換。
Stream 就如同一個迭代器(Iterator),單向,不可往復,數據只能遍歷一次,遍歷過一次后即用盡了,就好比流水從面前流過,一去不復返。
而和迭代器又不同的是,Stream 可以並行化操作,迭代器只能命令式地、串行化操作。顧名思義,當使用串行方式去遍歷時,每個 item 讀完后再讀下一個 item。而使用並行去遍歷時,數據會被分成多個段,其中每一個都在不同的線程中處理,然后將結果一起輸出。Stream 的並行操作依賴於 Java7 中引入的 Fork/Join 框架(JSR166y)來拆分任務和加速處理過程。
Stream 的另外一大特點是,數據源本身可以是無限的。
2. 流的構成
獲取一個數據源(source)→ 數據轉換→執行操作獲取想要的結果。每次轉換原有 Stream 對象不改變,返回一個新的 Stream 對象(可以有多次轉換),這就允許對其操作可以像鏈條一樣排列,變成一個管道。
有多種方式生成 Stream Source:
- 從 Collection 和數組
Collection.stream()
Collection.parallelStream()
Arrays.stream(T array) or Stream.of() - 從 BufferedReader
java.io.BufferedReader.lines() - 靜態工廠
java.util.stream.IntStream.range()
java.nio.file.Files.walk() - 自己構建
java.util.Spliterator - 其它
Random.ints()
BitSet.stream()
Pattern.splitAsStream(java.lang.CharSequence)
JarFile.stream()
3.流的操作類型
- Intermediate:一個流可以后面跟隨零個或多個 intermediate 操作。其目的主要是打開流,做出某種程度的數據映射/過濾,然后返回一個新的流,交給下一個操作使用。這類操作都是惰性化的(lazy),就是說,僅僅調用到這類方法,並沒有真正開始流的遍歷。
- Terminal:一個流只能有一個 terminal 操作,當這個操作執行后,流就被使用“光”了,無法再被操作。所以這必定是流的最后一個操作。Terminal 操作的執行,才會真正開始流的遍歷,並且會生成一個結果,或者一個 side effect。
在對於一個 Stream 進行多次轉換操作 (Intermediate 操作),每次都對 Stream 的每個元素進行轉換,而且是執行多次,這樣時間復雜度就是 N(轉換次數)個 for 循環里把所有操作都做掉的總和嗎?其實不是這樣的,轉換操作都是 lazy 的,多個轉換操作只會在 Terminal 操作的時候融合起來,一次循環完成。我們可以這樣簡單的理解,Stream 里有個操作函數的集合,每次轉換操作就是把轉換函數放入這個集合中,在 Terminal 操作的時候循環 Stream 對應的集合,然后對每個元素執行所有的函數。
還有一種操作被稱為 short-circuiting。用以指:
對於一個 intermediate 操作,如果它接受的是一個無限大(infinite/unbounded)的 Stream,但返回一個有限的新 Stream。
對於一個 terminal 操作,如果它接受的是一個無限大的 Stream,但能在有限的時間計算出結果。
當操作一個無限大的 Stream,而又希望在有限時間內完成操作,則在管道內擁有一個 short-circuiting 操作是必要的。
stream使用
1. 流的構造與轉換
最常使用到的就是集合作為流的source。
Stream stream;
//由單獨的值構成
Stream<String> strStream = Stream.of("one", "two", "three", "four");
//由數組構成
String [] strArray = new String[] {"a", "bb", "c"};
stream = Stream.of(strArray);
stream = Arrays.stream(strArray);
//由集合構成,最常用了
List<String> list = Arrays.asList(strArray);
stream = list.stream();
//對於基本數值型,目前有三種對應的包裝類型的Stream:IntStream、LongStream、DoubleStream
IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);
IntStream.range(1, 3).forEach(System.out::println);
IntStream.rangeClosed(1, 3).forEach(System.out::println);
進階:還可以自己構造流
- Stream.generate
Stream.generate通過實現 Supplier 接口,你可以自己來控制流的生成。這種情形通常用於隨機數、常量的 Stream,或者需要前后元素間維持着某種狀態信息的 Stream。由於它是無限的,在管道中,必須利用 limit 之類的操作限制 Stream 大小。
//生成100以內的15個隨機整數,用來構造測試隨機數不失為一種簡便的方式
Stream.generate(() -> new Random().nextInt(100)).limit(15).forEach(System.out::println);
//Another way
IntStream.generate(() -> (int) (System.nanoTime() % 100)).
limit(15).forEach(System.out::println);
//random其實提供了更方便的ints()方法
new Random().ints().limit(15).forEach(System.out::println);
Stream.generate() 還接受自己實現的 Supplier。例如在構造海量測試數據的時候,用某種自動的規則給每一個變量賦值,用來構造測試數據很方便!
@Test
public void testSupplier(){
Stream.generate(new PersonSupplier()).
limit(10).
forEach(p -> System.out.println(p.getName() + ", " + p.getAge() + ", " + p.getHeight()));
}
private class PersonSupplier implements Supplier<PersonDto> {
private int index = 0;
private Random random = new Random();
@Override
public PersonDto get() {
return new PersonDto( "xiao" + index, index++, random.nextInt(190));
}
}
- Stream.iterate
iterate 跟 reduce 操作很像,接受一個種子值,和一個 UnaryOperator(例如 f)。然后種子值成為 Stream 的第一個元素,f(seed) 為第二個,f(f(seed)) 第三個,以此類推。在 iterate 時候管道必須有 limit 這樣的操作來限制 Stream 大小。
Stream.iterate(0, n -> n + 3).limit(10). forEach(x -> System.out.print(x + " "));.
2. 流的操作
流可以轉換為數組、集合等
Stream<String> stream = Stream.of("one", "two", "three", "four");
// 1. 轉換為數組
String[] strArray1 = stream.toArray(String[]::new);
// 2. 轉換為集合
List<String> list1 = stream.collect(Collectors.toList());
List<String> list2 = stream.collect(Collectors.toCollection(ArrayList::new));
Set set1 = stream.collect(Collectors.toSet());
Stack stack1 = stream.collect(Collectors.toCollection(Stack::new));
// 3. 轉為String
String str = stream.collect(Collectors.joining(",")).toString();
@Data
@Accessors(chain = true)
public class OriginalDto {
private String id;
private String regionId;
private LocalDateTime deviceTime;
private Double noiseValue;
private Double noiseStatus;
}
List<OriginalDto> originalDtoList = new ArrayList<>(10);
//獲取originalDtoList中所有id的集合
List<String> originalDtoIdList = originalDtoList.stream().map(OriginalDto::getId).collect(Collectors.toList());
//根據originalDtoList的deviceTime進行分組
Map<LocalDateTime, List<OriginalDto>> dateTimeListMap= originalDtoList.stream().collect(Collectors.groupingBy(OriginalDto::getDeviceTime));
//求每個deviceTime分組對應的noiseValue的和
Map<LocalDateTime, Double> doubleMap = originalDtoList.stream().collect(
Collectors.groupingBy(OriginalDto::getDeviceTime, Collectors.summingDouble(OriginalDto::getNoiseValue)));
//獲取originalDtoList中所有噪音值的平均值,此時mapToDouble轉換為Double流,也可以求和,最大值,最小值,去重,排序,計數等
OptionalDouble avgNoiseOptional = originalDtoList.stream().mapToDouble(OriginalDto::getNoiseValue).average();
//對originalDtoList中元素按照deviceTime進行排序
originalDtoList = originalDtoList.stream().sorted(Comparator.comparing(OriginalDto::getDeviceTime)).collect(Collectors.toList());
//從idList到dayDataDto集合
List<DayDataDto> dayDataList = originalDtoIdList.stream().map(id -> {
DayDataDto dayDataDto = new DayDataDto();
dayDataDto.setId(id);
dayDataDto.setDate(LocalDate.now());
return dayDataDto;
}).collect(Collectors.toList());
//滿足noiseStatus=0的regionId前3個的集合
List<String> regionId = originalDtoList.stream().filter(originalDto -> originalDto.getNoiseStatus() == 0)
.map(OriginalDto::getRegionId).limit(3).collect(Collectors.toList());
`
在idea中如何調試stream
stream方便了我們的開發,但是在調試時卻十分不友好,幸好在idea2018之后的版本中,我們也可以查看調試stream各元素的變化。

點擊上圖中紅框所示按鈕即可進入stream,查看各元素變化,如下圖查看各元素排序情況:
而且idea中還支持調試時在當前上下文中執行表達式,這個調試時炒雞炒雞方便,如

總結
- 本文沒有介紹stream的理論知識,只對編碼開發中經常使用到的集合操作stream進行了總結介紹,方便進一步提高開發效率;
- stream存在流內部出現bug調試不便,和后期維護性差的缺點,所以不建議在stream內寫太復雜的業務邏輯,要保持代碼的邏輯清晰性和添加必要的注釋。
