java8中stream最實用總結和調試技巧


背景

       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);

進階:還可以自己構造流

  1. 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));
        }
    }
  1. 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中還支持調試時在當前上下文中執行表達式,這個調試時炒雞炒雞方便,如

在這里插入圖片描述

總結

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM