本教程詳細闡述了如何利用Java 8及更高版本的Stream API,將包含嵌套列表(如List
1. 問題背景與挑戰
在處理復雜數據結構時,我們經常遇到需要根據嵌套對象(如列表中的列表)的屬性來對外部對象進行分組的需求。例如,給定一個trip對象列表,每個trip包含一個employee對象列表,目標是創建一個map
直接嘗試使用Collectors.groupingBy對Trip流進行分組,并嘗試從Trip中獲取員工ID列表作為鍵,通常會導致編譯錯誤或不符合預期的結果。這是因為groupingBy期望一個單一的、可作為鍵的值,而不是一個流或列表。例如,將t.getEmpList().stream().map(Employee::getEmpId)作為groupingBy的分類函數,會導致鍵類型為Stream
2. 解決方案核心思路:扁平化與輔助對象
解決此問題的關鍵在于:
- 扁平化流: 將Stream
轉換為一個更細粒度的流,其中每個元素能夠直接關聯到員工ID和對應的Trip。 - 輔助對象: 引入一個臨時的數據結構(如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
trips.stream() .flatMap(trip -> trip.getEmpList().stream() // 將每個Trip的empList轉換為Stream<Employee> .map(emp -> new TripEmployee(emp.getEmpId(), trip)) // 將每個Employee映射為TripEmployee ) // 此時流的類型為 Stream<TripEmployee>
flatMap操作在這里至關重要,它將一個Stream
4.2 步驟二:分組聚合 (groupingBy 與 mapping)
在得到Stream
- 分類函數: 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解決從嵌套列表中提取數據并進行復雜分組的問題,使得代碼更具表達力和維護性。