Java Stream API:將列表中的嵌套列表數據分組映射為Map

Java Stream API:將列表中的嵌套列表數據分組映射為Map

本教程詳細闡述了如何利用Java 8及更高版本的Stream API,將包含嵌套列表(如List中包含List)的數據結構,高效地轉換為以嵌套對象屬性(如員工ID)為鍵、外部對象列表為值的map>。核心方法涉及使用輔助記錄(或類)扁平化流,并結合flatMap、Collectors.groupingBy和Collectors.mapping實現復雜數據聚合。

1. 問題背景與挑戰

在處理復雜數據結構時,我們經常遇到需要根據嵌套對象(如列表中的列表)的屬性來對外部對象進行分組的需求。例如,給定一個trip對象列表,每個trip包含一個employee對象列表,目標是創建一個map>,其中鍵是員工id (empid),值是該員工參與的所有trip列表。

直接嘗試使用Collectors.groupingBy對Trip流進行分組,并嘗試從Trip中獲取員工ID列表作為鍵,通常會導致編譯錯誤或不符合預期的結果。這是因為groupingBy期望一個單一的、可作為鍵的值,而不是一個流或列表。例如,將t.getEmpList().stream().map(Employee::getEmpId)作為groupingBy的分類函數,會導致鍵類型為Stream,而非所需的String

2. 解決方案核心思路:扁平化與輔助對象

解決此問題的關鍵在于:

  1. 扁平化流: 將Stream轉換為一個更細粒度的流,其中每個元素能夠直接關聯到員工ID和對應的Trip。
  2. 輔助對象: 引入一個臨時的數據結構(如Java 16的record或一個簡單的POJO類),用于將每個Employee的empId與其所屬的Trip實例進行綁定。

通過這種方式,我們可以將“一個Trip包含多個Employee”的“一對多”關系,轉換為“一個TripEmployee實例代表一個員工在一次行程中的參與”,從而使得后續的分組操作變得簡單明了。

3. 定義數據模型

首先,我們定義問題中涉及的領域模型:

立即學習Java免費學習筆記(深入)”;

import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;  import java.util.Date; import java.util.List;  @Data @NoArgsConstructor @AllArgsConstructor public class Trip {     private Date startTime;     private Date endTime;     List<Employee> empList; }  @Data @NoArgsConstructor @AllArgsConstructor public class Employee {     private String name;     private String empId; }

為了輔助分組,我們引入一個record(Java 16+)或一個簡單的類來關聯員工ID和行程:

// 使用Java 16+ 的 record public record TripEmployee(String empId, Trip trip) {}  // 對于Java 8-15,可以使用一個普通的類 /* public class TripEmployee {     private String empId;     private Trip trip;      public TripEmployee(String empId, Trip trip) {         this.empId = empId;         this.trip = trip;     }      public String getEmpId() { return empId; }     public Trip getTrip() { return trip; }     // 可以根據需要添加equals, hashCode, toString } */

record的優勢在于其簡潔性,編譯器會自動生成構造函數訪問器、equals()、hashCode()和toString()方法。

4. 使用Stream API進行數據轉換與分組

核心的Stream管道將分為兩步:

4.1 步驟一:扁平化流 (flatMap)

我們首先將Stream扁平化為Stream。對于每個Trip,我們遍歷其內部的empList,為每個Employee創建一個TripEmployee實例,將員工ID與當前Trip關聯起來。

trips.stream()     .flatMap(trip -> trip.getEmpList().stream() // 將每個Trip的empList轉換為Stream<Employee>         .map(emp -> new TripEmployee(emp.getEmpId(), trip)) // 將每個Employee映射為TripEmployee     )     // 此時流的類型為 Stream<TripEmployee>

flatMap操作在這里至關重要,它將一個Stream>(由map操作生成)扁平化為一個單一的Stream

4.2 步驟二:分組聚合 (groupingBy 與 mapping)

在得到Stream之后,我們就可以使用Collectors.groupingBy進行分組。

  • 分類函數: TripEmployee::empId,這會根據empId進行分組。
  • 下游收集器: 由于我們希望每個empId對應一個List,而當前流中的元素是TripEmployee,我們需要使用Collectors.mapping來提取Trip對象。mapping收集器需要一個映射函數(TripEmployee::trip)和一個最終的下游收集器(Collectors.toList())來將提取出的Trip收集成列表。
    .collect(Collectors.groupingBy(         TripEmployee::empId, // 根據empId進行分組         Collectors.mapping(TripEmployee::trip, // 將TripEmployee映射為Trip             Collectors.toList()) // 將映射后的Trip收集為List     ));

5. 完整示例代碼

以下是包含數據初始化和完整Stream管道的示例:

import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;  import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.stream.Collectors;  // 假設 Trip 和 Employee 類已定義如上  // 輔助記錄 (Java 16+) public record TripEmployee(String empId, Trip trip) {}  public class TripGroupingExample {      public static void main(String[] args) {         // 示例數據         Employee emp1 = new Employee("Alice", "E001");         Employee emp2 = new Employee("Bob", "E002");         Employee emp3 = new Employee("Charlie", "E003");          Trip trip1 = new Trip(new Date(), new Date(), List.of(emp1, emp2));         Trip trip2 = new Trip(new Date(), new Date(), List.of(emp1, emp3));         Trip trip3 = new Trip(new Date(), new Date(), List.of(emp2));         Trip trip4 = new Trip(new Date(), new Date(), List.of(emp3, emp1)); // 再次包含emp1          List<Trip> trips = new ArrayList<>();         trips.add(trip1);         trips.add(trip2);         trips.add(trip3);         trips.add(trip4);          // 使用Stream API生成Map<String, List<Trip>>         Map<String, List<Trip>> empTripsMap = trips.stream()             .flatMap(trip -> trip.getEmpList().stream() // 將每個Trip的empList扁平化為Stream<Employee>                 .map(emp -> new TripEmployee(emp.getEmpId(), trip)) // 將每個Employee映射為TripEmployee             )             .collect(Collectors.groupingBy(                 TripEmployee::empId, // 根據TripEmployee的empId進行分組                 Collectors.mapping(TripEmployee::trip, // 將TripEmployee映射回Trip                     Collectors.toList()) // 將映射后的Trip收集為List             ));          // 打印結果         empTripsMap.forEach((empId, tripList) -> {             System.out.println("Employee ID: " + empId);             tripList.forEach(trip -> System.out.println("  - Trip: " + trip));             System.out.println("---");         });          /* 預期輸出示例 (具體Trip對象內容取決于toString實現和日期)         Employee ID: E001           - Trip: Trip(startTime=..., endTime=..., empList=...) // trip1           - Trip: Trip(startTime=..., endTime=..., empList=...) // trip2           - Trip: Trip(startTime=..., endTime=..., empList=...) // trip4         ---         Employee ID: E002           - Trip: Trip(startTime=..., endTime=..., empList=...) // trip1           - Trip: Trip(startTime=..., endTime=..., empList=...) // trip3         ---         Employee ID: E003           - Trip: Trip(startTime=..., endTime=..., empList=...) // trip2           - Trip: Trip(startTime=..., endTime=..., empList=...) // trip4         ---         */     } }

6. 注意事項與總結

  • Java版本兼容性: 示例中使用了Java 16的record,如果您的項目使用Java 8到Java 15,請使用普通的Java類作為輔助對象(如代碼注釋中所示)。功能上沒有區別,只是record提供了更簡潔的語法。
  • flatMap的重要性: flatMap是處理“一對多”轉換的關鍵操作。它將流中的每個元素映射到一個新的流,然后將這些新的流連接(扁平化)成一個單一的流。在本例中,它將每個Trip轉換為多個TripEmployee實例的流,然后合并這些流。
  • groupingBy與mapping組合: 當需要根據一個屬性分組,但最終值是原始對象或其轉換形式的列表時,Collectors.groupingBy結合Collectors.mapping是一個非常強大的模式。mapping允許你在分組之后,對每個組內的元素進行進一步的轉換和收集。
  • 可讀性: 引入TripEmployee這樣的輔助對象,雖然增加了一個小類,但顯著提高了Stream管道的可讀性和意圖清晰度,避免了使用Map.Entry等通用但語義不明確的結構。

通過上述方法,我們能夠高效且清晰地利用Java Stream API解決從嵌套列表中提取數據并進行復雜分組的問題,使得代碼更具表達力和維護性。

? 版權聲明
THE END
喜歡就支持一下吧
點贊9 分享