迭代時修改流中的對象
1. 概述
Java Stream API 提供了各種允許修改流元素的方法。然而,這些方法內的操作必須是互不干擾且無狀態的。否則,這將導致不正確的行為和輸出。
在本教程中,我們將討論修改 Java Stream 中的元素時常見的錯誤以及正確的方法。
2. 更改流元素的狀態
讓我們以Person
類別的列表為例:
public class Person {
private String name;
private String email;
public Person(String name, String email) {
this.name = name;
this.email = email;
}
//standard getters and setters..
}
我們將修改流內Person
元素的電子郵件 ID 並將其轉換為大寫。
2.1.使用forEach()
方法修改
讓我們從一個非常基本的方法開始,透過使用forEach()
方法簡單地迭代列表:
@Test
void givenPersonList_whenUpdatePersonEmailByInterferingWithForEach_thenPersonEmailUpdated() {
personList.stream().forEach(e -> e.setEmail(e.getEmail().toUpperCase()));
personList.stream().forEach(e -> assertEquals(e.getEmail(), e.getEmail().toUpperCase()));
}
在上面的方法中,在迭代Person
物件清單時,每個元素的電子郵件都會轉換為大寫。看似合法,卻違反了不干涉原則。這意味著在流管道中,我們永遠不應該修改原始來源。
除非流源是並發的,否則在流管道執行期間修改流的資料源可能會導致異常、不正確的答案或不一致的行為。
2.2.用peek()
方法修改
現在讓我們來看看peek()
方法。我們常常想用它來修改流中元素的屬性:
@Test
void givenPersonList_whenUpdatePersonEmailByInterferingWithPeek_thenPersonEmailUpdated() {
personList.stream()
.peek(e -> e.setEmail(e.getEmail().toUpperCase()))
.collect(Collectors.toList());
personList.forEach(e -> assertEquals(e.getEmail(), e.getEmail().toUpperCase()));
}
同樣,透過更新來源personList
我們重複了前面部分中提到的相同錯誤。
2.3.用map()
方法修改
forEach()
方法是流管道中的終端操作。但是, map()
與peek()
一樣,是一個傳回Stream
中間操作。在map()
中,我們將建立一個新的Person
對象,其中電子郵件為大寫,然後將其收集到新清單中:
@Test
void givenPersonList_whenUpdatePersonEmailWithMapMethod_thenPersonEmailUpdated() {
List<Person> newPersonList = personList.stream()
.map(e -> new Person(e.getName(), e.getEmail().toUpperCase()))
.collect(Collectors.toList());
newPersonList.forEach(e -> assertEquals(e.getEmail(), e.getEmail().toUpperCase()));
}
在上面的方法中,我們沒有修改原來的清單。相反,我們創建了一個新列表newPersonList
。因此,它是不干擾的。它也是無狀態的,因為它內部的操作結果不會互相影響。大多數情況下,他們獨立運作。無論是順序處理還是並行處理,都建議遵循這些原則。
考慮到不可變性是函數式程式設計的本質之一,我們可以嘗試建立一個不可變的Person
類別:
public class ImmutablePerson {
private String name;
private String email;
public ImmutablePerson(String name, String email) {
this.name = name;
this.email = email;
}
public ImmutablePerson withEmail(String email) {
return new ImmutablePerson(this.name, email);
}
//Standard getters
}
ImmutablePerson
類別沒有任何 setter 方法。但是,它提供了一個withEmail()
方法,該方法傳回一個新的ImmutablePerson
,其中email
為大寫。
現在,讓我們在修改流中的元素時使用它:
@Test
void givenPersonList_whenUpdateImmutablePersonEmailWithMapMethod_thenPersonEmailUpdated() {
List<ImmutablePerson> newImmutablePersonList = immutablePersonList.stream()
.map(e -> e.withEmail(e.getEmail().toUpperCase()))
.collect(Collectors.toList());
newImmutablePersonList.forEach(e -> assertEquals(e.getEmail(), e.getEmail().toUpperCase()));
}
透過這一點,我們正在強制執行不干涉。
3. 從串流中刪除元素
在流程中執行結構變更甚至更加棘手。這是比修改更昂貴的操作,因此如果不小心,可能會導致不一致和不良的結果。讓我們詳細探討一下。
3.1.使用forEach()
方法刪除元素
如果我們想從流中刪除一些元素怎麼辦?例如,讓我們從清單中刪除名為John
的人:
@Test
void givenPersonList_whenRemoveWhileIterating_thenThrowException() {
assertThrows(NullPointerException.class, () -> {
personList.stream().forEach(e -> {
if(e.getName().equals("John")) {
personList.remove(e);
}
});
});
}
我們嘗試在迭代時修改forEach()
方法中列表的結構。令人驚訝的是,這會導致NullPointerException
這與ArrayList
中的forEach()
拋出ConcurrentModificationException:
@Test
void givenPersonList_whenRemoveWhileIteratingWithForEach_thenThrowException() {
assertThrows(ConcurrentModificationException.class, () -> {
personList.forEach(e -> {
if(e.getName().equals("John")) {
personList.remove(e);
}
});
});
}
3.2.使用CopyOnWriteArrayList
刪除元素
CopyOnWriteArrayList
是ArrayList
的線程安全版本。在迭代時可以刪除元素:
@Test
void givenPersonList_whenRemoveWhileIterating_thenPersonRemoved() {
assertEquals(4, personList.size());
CopyOnWriteArrayList<Person> cps = new CopyOnWriteArrayList<>(personList);
cps.stream().forEach(e -> {
if(e.getName().equals("John")) {
cps.remove(e);
}
});
assertEquals(3, cps.size());
}
它可以防止多個執行緒之間的干擾,但成本太高,因為對於每個寫入操作,它都會建立一個快照。
3.3.使用filter()
方法刪除元素
Java Stream API 提供了方法filter()
來以更優雅的方式刪除元素:
@Test
void givenPersonList_whenRemovePersonWithFilter_thenPersonRemoved() {
assertEquals(4, personList.size());
List<Person> newPersonList = personList.stream()
.filter(e -> !e.getName().equals("John"))
.collect(Collectors.toList());
assertEquals(3, newPersonList.size());
}
在上面的方法中, filter()
只允許那些沒有名稱John
Person
物件在管道中向前移動。同樣,過濾方法內使用的謂詞應該是非干擾且無狀態的。它看起來也更簡單、易於理解和排除故障。
4。結論
在本文中,我們了解了修改流中元素的正確方法。重要的是管道處理應該是無幹擾和無狀態的。否則,這可能會導致意想不到的結果。
與往常一樣,本文中使用的程式碼可以在 GitHub 上找到。