提高Java開發生產力,我選Stream API,真香啊

来源:https://www.cnblogs.com/coderacademy/p/18020463
-Advertisement-
Play Games

本文深入介紹了Java 8的Stream API,包括創建、中間操作、終端操作等,強調了並行流在大數據處理中的性能提升。提供清晰實用的示例,為讀者理解流式計算提供有益指導。 ...


Java 8 引入的Stream API提供了一種新的數據處理方式,它以聲明式、函數式的編程模型,極大地簡化了對集合、數組或其他支持數據源的操作。Stream可以被看作是一系列元素的流水線。允許你高效地對大量數據執行複雜的過濾、映射、排序、聚合等操作,而無需顯式地使用迴圈或者臨時變數。Stream API的設計理念主要包括兩個方面:鏈式調用惰性求值。鏈式調用允許我們將多個操作連接在一起,形成一個流水線,而惰性求值意味著只有在真正需要結果的時候才執行計算,從而避免了不必要的計算開銷。

接下來我們就來盤點一下日常開發中常用的一些Stream API。

創建Stream

  • 集合創建
List<String> list = new ArrayList<>(); 
// 串列流
Stream<String> stream = list.stream();
// 並行流
Stream<String> parallelStream = list.parallelStream();
  • 數組創建
String[] strs = new String[3];  
Stream<String> stream = Arrays.stream(strs);
  • 使用Stream.of(T...values)創建
Stream<String> stream = Stream.of("Apple", "Orange", "Banana");
  • 使用Stream.generate()創建流
// 生成一個無限流,通過limit()限制元素個數  
Stream<Double> randomStream = Stream.generate(Math::random).limit(5);
  • 使用Stream.iterate()創建流
// 生成一個等差數列,通過limit()限制元素個數 
Stream<Integer> integerStream = Stream.iterate(0, n -> n + 2).limit(5);
  • 使用IntStream、LongStream、DoubleStream創建原始類型流
// 使用IntStream創建  
IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4]  
  
// 使用LongStream創建  
LongStream longStream = LongStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5]

IntStream我們使用的地方還是比較多的,比如我們按照下標遍歷一個集合時,同常的做法是:for(int i = 0; i < list.size(); i++){},我們可以使用IntStream去改造一下,IntStream.rangeClosed(0, list.size()).forEach();

中間操作

中間操作是構建流水線的一部分,用於對流進行轉換和處理,但它們並不會觸發實際的計算。

  • 過濾操作(filter)
    過濾操作用於篩選流中的元素,保留滿足指定條件的元素。Stream<T> filter(Predicate<? super T> predicate)filter接受一個謂詞Predicate,我們可以通過這個謂詞定義篩選條件,Predicate是一個函數式介面,其包含一個test(T t)方法,該方法返回boolean。
private static void filterTest(){  
    List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape");  
    // 過濾長度大於5的水果  
    List<String> filteredFruits = fruits.stream().filter(fruit -> fruit.length() > 5).collect(Collectors.toList());  
    System.out.println("長度大於5的水果: "+ filteredFruits);  
}

private static void filterTest(List<Student> students){  
    List<Student> filterStudents = students.stream()  
            .filter(student -> Objects.equals("武漢大學", student.getSchool()))  
            .collect(Collectors.toList());  
  
    filterStudents.forEach(System.out::println);  
}

列印結果:
image.png

  • 映射操作(map/flatMap)
    映射操作用於對流中的每個元素進行轉換。他有map以及flatMap兩種操作。map就是基本的映射操作,對每個元素進行提取轉換。
// 將實體層映射成學生姓名字元串  
List<String> names = students.stream()  
        .map(Student::getName)  
        .collect(Collectors.toList());

// 將字元串轉大寫。
List<String> upperList = Lists.newArrayList("hello", "world", "stream", "api").stream().map(String::toUpperCase).collect(Collectors.toList());

日常開發中map操作我們用的非常多,比如資料庫中查詢出來的DO實體,我們需要轉換為VO返回給前端頁面展示,這時候我們可以使用map進行轉換操作:

List<StudentDO> studentDOList = studentMapper.listStudents();

List<StudentVO> studentVOList = studentDOList.stream().map(studentDO -> {
	StudentVO studentVO = StudentVO.builder().studentNo(studentDO.getId())
	.studentName(studentDO.getName()).build();
	return studentVO;
}).collect(Collectors.toList());

而flatMap的作用略微特殊,它用於將一個元素映射為一個流,然後將所有流連接成一個流。這在處理嵌套結構或集合中的元素是另一個集合的情況下非常有用。

List<List<String>> nestedWords = Arrays.asList(
    Arrays.asList("Java", "Kotlin"),
    Arrays.asList("Python", "Ruby"),
    Arrays.asList("JavaScript", "TypeScript")
);

// 使用 flatMap 將嵌套的 List<String> 轉換為一個扁平的 List<String>, 結果將是包含所有單詞的扁平流
List<String> wordList = nestedWords.stream()  
        .flatMap(List::stream).collect(Collectors.toList());

System.out.println(wordList);

// 列印結果: [Java, Kotlin, Python, Ruby, JavaScript, TypeScript]

flatMap在使用時,通常會涉及到處理複雜的數據結構,比如處理嵌套的對象集合或者進行數據的扁平化。

@Data
@Builder
class Student {  
    private String name;  
    private List<Integer> grades;  
}

@Data
@Builder
class ClassRoom {  
    private List<Student> studentList;  
}

@Data
@Builder
class School {  
    private List<ClassRoom> classRoomList;  
}

School school = School.builder()  
        .classRoomList(Lists.newArrayList(  
                ClassRoom.builder().studentList(Lists.newArrayList(  
                        Student.builder().name("Alice").gradeList(Lists.newArrayList(90, 85, 88)).build(),  
                                  Student.builder().name("Bob").gradeList(Lists.newArrayList(78, 92, 80)).build()  
                )).build(),  
                ClassRoom.builder().studentList(Lists.newArrayList(  
                        Student.builder().name("Charlie").gradeList(Lists.newArrayList(95, 89, 91)).build(),  
                        Student.builder().name("David").gradeList(Lists.newArrayList(82, 87, 79)).build()  
                )).build()  
        ))  
        .build();  
  
// 使用 flatMap 扁平化處理獲取所有學生的所有課程成績  
List<Integer> allGrades = school.getClassRoomList().stream()  
        .flatMap(classroom -> classroom.getStudentList().stream())  
        .flatMap(student -> student.getGradeList().stream())  
        .collect(Collectors.toList());  
  
System.out.println(allGrades);
// 列印結果:[90, 85, 88, 78, 92, 80, 95, 89, 91, 82, 87, 79]
  • mapToInt操作
    mapToInt 是 Stream API 中的一種映射操作,專門用於將元素映射為 IntStream。通過 mapToInt,你可以將流中的元素映射為 int 類型,從而進行更專門化的操作,例如數值計算。
int totalAge2 = students.stream().mapToInt(Student::getAge).sum();

類似的還有mapToLongmapToDouble 操作,這兩個操作類似於 mapToInt,分別用於將流中的元素映射為 LongStreamDoubleStream

  • 排序操作(sorted)
    排序操作用於對流中的元素進行排序。
List<String> cities = Lists.newArrayList("New York", "Tokyo", "London", "Paris");

// 對城市按字母順序排序
List<String> sortedStream = cities.stream().sorted().collect(Collectors.toList());  

對於集合中對象的排序,sorted要求待比較的元素必須實現Comparable介面。

@Data  
@Builder  
static class Student implements Comparable<Student>{  
    private String name;  
    private Integer age;  
      
    @Override  
    public int compareTo(Student other) {  
        return other.getAge()-this.getAge();  
    }  
}

List<String> sortedList = students.stream()  
        .sorted()  
		.map(Student::getName()) 
        .collect(Collectors.toList());    

如果沒有實現,就需要將比較器作為參數傳遞給sorted(Comparator<? super T> comparator)

@Data  
@Builder  
static class Student {  
    private String name;  
    private Integer age;
}

List<String> sortedList = students.stream()  
        .sorted((student1,student2) -> student2.getAge() - student1.getAge())  
        .map(Student::getName()) 
        .collect(Collectors.toList());    
  • 去重操作(distinct)
    去重操作用於去除流中的重覆元素。distinct基於Object.equals(Object)實現。
List<Integer> numbers = Lists.newArrayList(1, 2, 3, 2, 4, 5, 3, 6);  
// 去除重覆的數字  
List<Integer> distinctList = numbers.stream().distinct().collect(Collectors.toList());

// 或者去除學生中姓名相同的
List<String> studentNameList = students.stream()
								.map(Student::getName()) 
								.distinct()
						        .collect(Collectors.toList());    

  • 截斷操作(limit)
    截斷操作用於限制流中元素的數量。limit返回包含前n個元素的流,當集合大小小於n時,則返回實際長度。
List<Integer> numbers = Lists.newArrayList(1, 2, 3, 2, 4, 5, 3, 6); 
// 只取前三個數字 
List<Integer> limitedList = numbers.stream().limit(3).collect(Collectors.toList());

// 取土工工程專業的年齡最小的前兩名學生
List<Student> limitStu = students.stream()  
        .filter(student -> Objects.equals("土木工程", student.getMajor())) 
        .sorted((student1,student2) -> student2.getAge() - student1.getAge())  
        .limit(2)  
        .collect(Collectors.toList());
  • 跳過操作(skip)
    跳過操作用於跳過流中的前幾個元素,返回由後面所有元素構造的流,如果n大於滿足條件的集合的長度,則會返回一個空的集合。作用上跟limit相反。
List<Integer> numbers = Lists.newArrayList(1, 2, 3, 2, 4, 5, 3, 6); 
// 跳過前三個數字,返回後面的數字 
List<Integer> limitedList = numbers.stream().skip(3).collect(Collectors.toList());

// 跳過土工工程專業的年齡最小的前兩名學生,取後面的學生
List<Student> limitStu = students.stream()  
        .filter(student -> Objects.equals("土木工程", student.getMajor())) 
        .sorted((student1,student2) -> student2.getAge() - student1.getAge())  
        .skip(2)  
        .collect(Collectors.toList());
  • peek操作
    peek 方法對每個元素執行操作並返回一個新的 Stream。peek 的主要目的是用於調試和觀察流中的元素,通常用於列印調試信息、記錄日誌或其他類似的目的,而不會改變流中元素的結構。
List<String> words = Arrays.asList("apple", "banana", "orange", "grape");  
  
List<String> modifiedWords = words.stream()  
        .filter(word -> word.length() > 5)  
        .peek(word -> System.out.println("Filtered Word: " + word))  
        .map(String::toUpperCase)  
        .peek(word -> System.out.println("Uppercase Word: " + word))  
        .collect(Collectors.toList());

Stream的終端操作

終端操作是對流進行最終計算的操作,執行終端操作後,流將被消耗,不能再被使用。

  • 迭代forEach操作
    forEach 迭代操作,用於對流中的每個元素執行指定的操作。
List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape");

// 使用 forEach 輸出每個水果
fruits.stream().forEach(fruit -> System.out.println(fruit));
// 執行forEach時可省略 stream(),即
fruits.forEach(fruit -> System.out.println(fruit));
// 或
fruits.stream().forEach(System.out::println);
  • 收集操作(collect)
    通過collect()方法結合java.util.stream.Collectors工具類將Stream轉換為另一種形式,例如列表、集合(toList, toSet, toMap)、映射或歸約結果。如上述示例中的:
  1. 收集到List
    使用Collectors.toList()
// 跳過土工工程專業的年齡最小的前兩名學生,取後面的學生
List<Student> limitStu = students.stream()  
        .filter(student -> Objects.equals("土木工程", student.getMajor())) 
        .sorted((student1,student2) -> student2.getAge() - student1.getAge())  
        .skip(2)  
        .collect(Collectors.toList());
  1. 收集到Set
    使用Collectors.toSet()
// 將學生姓名收集到Set
Set<String> studentNameSet = students.stream().map(Student::getName)
		.collect(Collectors.toSet());
  1. List轉Map
    使用Collectors.toMap。日常開發中使用很多。
// 轉換為年齡對應的學生信息  
Map<Integer, Student> studentMap = students.stream().collect(Collectors.toMap(
											Student::getAge, 
											Function.identity(), 
											(e1,e2) -> e1));

這段代碼代表,我們使用年齡作為Map的key,對應學生信息作為value。Function.identity():這是一個提取元素自身的映射函數。(e1, e2) -> e1:這是一個合併衝突的操作。如果在流中存在相同的年齡(相同的鍵),這個函數定義了當出現重覆鍵時應該如何處理。在這裡,我們選擇保留第一個出現的元素,即保留先出現的 Student 對象。當然我們還可以這樣(e1, e2) -> {...}自定義合併衝突策略,例如:

// 轉換為年齡對應的學生信息,如果年齡相同,則取名字較長的  
Map<Integer, Student> studentMap = students.stream().collect(Collectors.toMap(Student::getAge, Function.identity(), (e1,e2) -> {  
    return e1.getName().length() > e2.getName().length() ? e1 : e2;  
}));

如果value的值是一些number,我們也可以做一些加減乘除之類的合併。

日常開發中,這個用法很頻繁。

  1. 字元串拼接:
    使用Collectors.joining(拼接符)
List<Student> students  = Lists.newArrayList(  
        Student.builder().name("Alice").gradeList(Lists.newArrayList(90, 85, 88)).build(),  
        Student.builder().name("Bob").gradeList(Lists.newArrayList(78, 92, 80)).build()  
);  
  
String studentName = students.stream().map(Student::getName).collect(Collectors.joining(","));

// 列印出來:Alice,Bob
  1. 分組
    即按照集合中的元素的某個屬性進行分組,轉換為Map<Object, List<Object>>:
List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape");  
Map<Integer, List<String>> lengthToNamesMap = fruits.stream()  
                    .collect(Collectors.groupingBy(String::length));

// 按照年齡分組  
Map<Integer, List<Student>> studentMap = students.stream().collect(Collectors.groupingBy(Student::getAge));

// 連續進行分組
Map<String,Map<String,List<Student>>> groupsStudent = students.stream()  
        // 先按照學校分組  
        .collect(Collectors.groupingBy(Student::getSchool  
        // 再按照專業分組  
        ,Collectors.groupingBy(Student::getMajor)));
  1. counting()
    counting() 收集器用於計算流中元素的數量。等同於Stream的count()操作。
long studentCount = students.stream().collect(Collectors.counting());
// 效果同等於
long studentCount = students.stream().count();
  1. maxBy()
    maxBy()基於指定的比較器,用於找到流中的最大的元素。等同於Stream的max操作
// 年齡最大的學生
Student olderStudent = students.stream()  
        .collect(Collectors.maxBy((s1,s2) -> s1.getAge()- s2.getAge())).orElse(null);

Student olderStudent2 = students.stream()  
    .collect(Collectors.maxBy(Comparator.comparing(Student::getAge))).orElse(null);

// 等價於stram的max
Student olderStudent = students.stream()
	.max(Comparator.comparing(Student::getAge)).orElse(null);    
  1. minBy()
    minBy()基於指定的比較器,用於找到流中的最小的元素。等同於Stream的min操作。
// 年齡最小的學生
Student youngStudent = students.stream()  
    .collect(Collectors.minBy(Comparator.comparing(Student::getAge))).orElse(null); 

Student youngStudent = students.stream()
	.min(Comparator.comparing(Student::getAge)).orElse(null);
  1. averagingInt
    averagingInt() 收集器用於計算流中元素的平均值。
// 求學生平均年齡
double avgAge = students.stream()  
        .collect(Collectors.averagingInt(Student::getAge));
  1. summarizingInt()
    summarizingInt() 收集器用於計算流中元素的彙總統計信息,包括總數、平均值、最大值和最小值。
// 一次性得到元素個數、總和、均值、最大值、最小值
IntSummaryStatistics summaryStatistics = students.stream().collect(Collectors.summarizingInt(Student::getAge));

System.out.println("總數:" + summaryStatistics.getCount()); 
System.out.println("平均值:" + summaryStatistics.getAverage()); 
System.out.println("最大值:" + summaryStatistics.getMax()); 
System.out.println("最小值:" + summaryStatistics.getMin());
  • partitioningBy()
    將流中的元素按照指定的條件分成兩個部分。在分區中key只有兩種情況:true或false,目的是將待分區集合按照條件一分為二,分區相對分組的優勢在於,我們可以同時得到兩類結果,在一些應用場景下可以一步得到我們需要的所有結果,比如將數組分為奇數和偶數。
// 分為武漢大學學生,非武漢大學學生
Map<Boolean,List<Student>> partStudent = students.stream()  
        .collect(Collectors.partitioningBy(student -> Objects.equals("武漢大學",student.getSchool())));
  • count操作
    count 用於計算流中的元素個數。效果等同於Collectors.counting()
long studentCount = students.stream().count();
// 效果同等於
long studentCount = students.stream().collect(Collectors.counting());

  • max操作
    基於指定比較器,max用於找到流中最大的元素。效果等同於Collectors.maxBy()
Student olderStudent = students.stream()
	.max(Comparator.comparing(Student::getAge)).orElse(null);  

Student olderStudent2 = students.stream()  
    .collect(Collectors.maxBy(Comparator.comparing(Student::getAge))).orElse(null);

  • min操作
    基於指定比較器,min用於找到流中最小的元素。效果等同於Collectors.minBy()
Student youngStudent = students.stream()
	.min(Comparator.comparing(Student::getAge)).orElse(null);
	
// 年齡最小的學生
Student youngStudent = students.stream()  
    .collect(Collectors.minBy(Comparator.comparing(Student::getAge))).orElse(null); 

  • reduce操作
    reduce 用於對流中的元素進行歸約操作,得到一個最終的結果。
// 計算學生的總年齡
int totalAge1 = students.stream()  
        .map(Student::getAge)  
        .reduce(0, (a,b) -> a+b);

// 也可以使用Integer.sum
int totalAge2 = students.stream() 
        .map(Student::getAge)  
        .reduce(0, Integer::sum);

// 也可以不設置初始值0,直接Integer.sum,但是返回的是Optional
int totalAge3 = students.stream()  
       .map(Student::getAge)  
       .reduce(Integer::sum).orElse(0);
  • findFirst操作
    findFirst 用於查找流中的第一個元素。也即list.get(0)
Student firstStu = students.stream()  
        .filter(student -> Objects.equals("土木工程", student.getMajor()))  
        .findFirst().orElse(null);
        

曾經有個小兄弟問我,他有一段代碼類似 Student firstStu = students.get(0)。他們組長讓他優化優化,然後就用了這種方式優化的。

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在編程的世界里,我們經常需要對數據進行迴圈處理,常用的兩種方法就是:for迴圈和foreach迴圈。想象你站在一條裝滿寶貝的傳送帶前,你要親手檢查每一件寶貝。使用for迴圈就像是你親手控制傳送帶的速度和方向,而使用foreach迴圈則是傳送帶自動運轉,你只需專註於寶貝本身。 ...
  • Java 方法重載 方法重載 允許在同一個類中定義多個具有相同名稱的方法,但 參數列表 必須不同。 語法: returnType methodName(parameter1, parameter2, ..., parameterN) { // 方法體 } 示例: public class Main ...
  • TLDR 修飾變數的時候,可以把 constexpr 對象當作加強版的 const 對象:const 對象表明值不會改變,但不一定能夠在編譯期取得結果;constexpr 對象不僅值不會改變,而且保證能夠在編譯期取得結果。如果一個 const 變數能夠在編譯期求值,將其改為 constexpr 能夠 ...
  • 老周一般很少玩游戲,在某寶上買了一堆散件,計劃在過年期間自己做個機械臂耍耍。頭腦中划過一道紫藍色的閃電,想起用游戲手柄來控制機械臂。機械臂是由樹莓派(大草莓)負責控制,然後客戶端通過 Socket UDP 來發送信號。優先考慮在 PC 和手機上測試,就順便折騰一下 XInput API。當然,讀取手 ...
  • 有這樣一個帶有搜索功能的用戶界面需求: 搜索流程如下所示: 這個需求涉及兩個實體: “評分(Rating)、用戶名(Username)”數據與User實體相關 “創建日期(create date)、觀看次數(number of views)、標題(title)、正文(body)”與Story實體相關 ...
  • 常見內置數值類型 數值類型是不可變類型(immutable type),它包括布爾類型、整數、浮點數與複數。 類型 英文名 構造方式 對應關鍵字 構造函數 布爾 Boolean var = True bool bool() 整數 Integer var = 5 int int() 浮點數 Float ...
  • Python 常見內置數據類型 在Python中,常用的類型是這些: Python 中查看數據類型的函數(function)為type()。 >>>text = "Is test a string type object?" >>>print(type(text)) <class 'str'> Py ...
  • ServerCnxnFactory 用於接收客戶端連接、管理客戶端session、處理客戶端請求。 ServerCnxn抽象類 代表一個客戶端連接對象: 從網路讀寫數據 數據編解碼 將請求轉發給上層組件或者從上層組件接收響應 管理連接狀態,比如:enableRecv、sessionTimeout、s ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...