在 Java 中透過巢狀列表過濾列表
一、簡介
在本教程中,我們將探討如何在 Java 中過濾包含巢狀清單的清單。當處理複雜的資料結構(例如包含其他清單的物件清單)時,基於特定標準提取特定資訊變得至關重要。
2. 理解問題
我們將使用一個簡單的範例,其中有一個User
類別和一個Order
類別。 User
類別有一個name
和一個Orders
列表,而Order
類別有一個product
和一個price
。我們的目標是根據應用於巢狀訂單的某些條件來過濾使用者清單。
以下是我們的資料模型的結構:
class User {
private String name;
private List<Order> orders;
public User(String name, List<Order> orders) {
this.name = name;
this.orders = orders;
}
// set get methods
}
class Order {
private String product;
private double price;
public Order(String product, double price) {
this.product = product;
this.price = price;
}
// set get methods
}
在此設定中,每個User
可以有多個Order
對象,這些對象為我們提供了根據與訂單相關的某些條件(例如price
過濾使用者所需的詳細資訊。
為了示範過濾邏輯,我們將首先建立一些範例測試資料。在下面的範例中,我們準備兩個Order
物件並將它們與三個User
物件關聯:
Order order1 = new Order("Laptop", 600.0);
Order order2 = new Order("Phone", 300.0);
Order order3 = new Order("Monitor", 510.0);
Order order4 = new Order("Monitor", 200.0);
User user1 = new User("Alice", Arrays.asList(order1, order4));
User user2 = new User("Bob", Arrays.asList(order3));
User user3 = new User("Mity", Arrays.asList(order2));
List users = Arrays.asList(user1, user2, user3);
假設我們要找所有下單價格大於$500
的用戶。在這種情況下,我們希望過濾邏輯會傳回兩個用戶,因為Alice
和Bob
都有符合此標準的訂單。
3. 傳統的循環方法
在 Java 8 引入 Stream API 之前,過濾清單的典型方法是使用傳統的for
迴圈。讓我們以相同的範例並使用巢狀 for 迴圈實作過濾:
double priceThreshold = 500.0;
List<User> filteredUsers = new ArrayList<>();
for (User user : users) {
for (Order order : user.getOrders()) {
if (order.getPrice() > priceThreshold) {
filteredUsers.add(user);
break;
}
}
}
assertEquals(2, filteredUsers.size());
在這種方法中,我們循環遍歷每個User
,然後循環遍歷他們的Orders
列表。一旦我們找到與我們的條件相符的訂單,我們就會將使用者加入到篩選清單中,並使用break
退出內部循環。
這種方法效果很好且易於理解,但需要手動管理嵌套循環。它也缺乏流提供的函數式程式設計風格。
4. 使用 Java Streams 進行過濾
借助 Java 8,我們可以使用 Streams 來獲得更簡潔的程式碼。讓我們使用這種方法來解決與之前相同的問題:我們想要過濾掉下訂單價格超過$500
的用戶。我們可以透過使用 Java Streams 檢查每個用戶的訂單列表,看看它是否包含滿足我們條件的訂單來做到這一點:
double priceThreshold = 500.0;
List<User> filteredUsers = users.stream()
.filter(user -> user.getOrders().stream()
.anyMatch(order -> order.getPrice() > priceThreshold))
.collect(Collectors.toList());
assertEquals(2, filteredUsers.size());
在此程式碼中,我們串流Users
列表,然後對於每個用戶,我們檢查他們的任何訂單的產品價格是否超過$500
。 anyMatch()
方法幫助我們檢查用戶的至少一個訂單是否支付了超過$500
。
5. 多條件過濾
現在,假設我們要過濾訂購「 Laptop
」並且支付超過$500
的用戶。在這種情況下,我們可以在anyMatch()
方法中組合條件:
double priceThreshold = 500.0;
String productToFilter = "Laptop";
List<User> filteredUsers = users.stream()
.filter(user -> user.getOrders().stream()
.anyMatch(order -> order.getProduct().equals(productToFilter)
&& order.getPrice() > priceThreshold))
.collect(Collectors.toList());
assertEquals(1, filteredUsers.size());
assertEquals("Alice", filteredUsers.get(0).getName());
在這裡,我們預計只有user1
會包含在filteredUsers
清單中,因為Alice
是唯一訂購了「 Laptop
」並支付超過$500
用戶。
6. 使用自訂Predicate
另一種方法是將我們的過濾邏輯封裝在自訂Predicate
中。這使得程式碼更具可讀性和可重用性。讓我們定義一個Predicate
來檢查使用者的訂單是否符合我們的條件:
Predicate<User> hasExpensiveOrder = user -> user.getOrders().stream()
.anyMatch(order -> order.getPrice() > priceThreshold);
List<User> filteredUsers = users.stream()
.filter(hasExpensiveOrder)
.collect(Collectors.toList());
assertEquals(2, filteredUsers.size());
此方法透過保持過濾邏輯隔離來提高可讀性,如果需要,謂詞可以重複用於其他過濾操作。
7. 過濾同時保留結構
讓我們考慮這樣的場景:我們希望將所有使用者保留在清單中,但只包含符合特定條件的訂單。我們不是刪除沒有有效訂單的用戶,而是修改他們的訂單清單並僅過濾掉不符合條件的訂單。
在這種情況下,我們需要修改使用者的訂單列表,同時保留user
物件的其餘部分:
List<User> filteredUsersWithLimitedOrders = users.stream()
.map(user -> {
List<Order> filteredOrders = user.getOrders().stream()
.filter(order -> order.getPrice() > priceThreshold)
.collect(Collectors.toList());
user.setOrders(filteredOrders);
return user;
})
.filter(user -> !user.getOrders().isEmpty())
.collect(Collectors.toList());
assertEquals(2, filteredUsersWithLimitedOrders.size());
assertEquals(1, filteredUsersWithLimitedOrders.get(0).getOrders().size());
assertEquals(1, filteredUsersWithLimitedOrders.get(1).getOrders().size());
在這裡,我們使用map()
來修改每個User
物件。對於每個用戶,我們都會過濾他們的訂單列表,只包含滿足價格條件的訂單,然後,我們使用過濾後的結果更新他們的訂單列表。
8. 使用flatMap()
我們可以利用flatMap()
方法來展平巢狀清單並將項目當作單一流處理,而不是使用巢狀流。這種方法透過避免多次呼叫stream()
來簡化巢狀列表的過濾。
讓我們看看如何使用flatMap()
根據Order
清單過濾User
物件:
List<User> filteredUsers = users.stream()
.flatMap(user -> user.getOrders().stream()
.filter(order -> order.getPrice() > priceThreshold)
.map(order -> user))
.distinct()
.collect(Collectors.toList());
assertEquals(2, filteredUsers.size());
在這個方法中,我們使用flatMap()
方法將每個User
轉換為其關聯的Order
物件的流。透過這樣做,我們可以將所有訂單作為單一統一流進行處理。
9. 處理邊緣情況
可能存在某些用戶根本沒有訂單的情況。為了防止在處理沒有任何訂單的使用者時出現潛在的NullPointerExceptions
,我們應該實施檢查以確保訂單不為 null 或空白:
User user1 = new User("Alice", Arrays.asList(order1, order2));
User user2 = new User("Bob", Arrays.asList(order3))
User user3 = new User("Charlie", new ArrayList<>());
List users = Arrays.asList(user1, user2, user3);
List<User> filteredUsers = users.stream()
.filter(user -> user.getOrders() != null && !user.getOrders().isEmpty())
.filter(user -> user.getOrders().stream()
.anyMatch(order -> order.getPrice() > priceThreshold))
.collect(Collectors.toList());
assertEquals(2, filteredUsers.size());
在此範例中,我們首先檢查訂單清單是否不為空且不為空,然後再套用進一步的篩選器。這使我們的程式碼更安全並避免運行時錯誤。
10. 結論
在本文中,我們學習瞭如何在 Java 中根據巢狀列表來過濾列表。我們探索了傳統循環、Java Streams、自訂謂詞以及如何處理邊緣情況。透過使用 Streams,我們可以編寫更乾淨、更有效率的程式碼。
像往常一樣,這裡討論的程式碼可以在 GitHub 上找到。