Javadoc of Collector pokazuje, jak zbierać elementy strumienia do nowej listy. Czy istnieje jedna linijka, która dodaje wyniki do istniejącej tablicy ArrayList?
Javadoc of Collector pokazuje, jak zbierać elementy strumienia do nowej listy. Czy istnieje jedna linijka, która dodaje wyniki do istniejącej tablicy ArrayList?
Odpowiedzi:
UWAGA: odpowiedź nosida pokazuje, jak dodać do istniejącej kolekcji za pomocą forEachOrdered()
. Jest to przydatna i skuteczna technika modyfikowania istniejących kolekcji. Moja odpowiedź dotyczy tego, dlaczego nie powinieneś używać a Collector
do modyfikowania istniejącej kolekcji.
Krótka odpowiedź brzmi: nie , przynajmniej nie generalnie, nie powinieneś używać a Collector
do modyfikowania istniejącej kolekcji.
Powodem jest to, że kolekcje są zaprojektowane do obsługi równoległości, nawet w przypadku kolekcji, które nie są bezpieczne wątkowo. Sposób, w jaki to robią, polega na tym, że każdy wątek działa niezależnie na swoim własnym zbiorze wyników pośrednich. Sposób, w jaki każdy wątek pobiera własną kolekcję, polega na wywołaniu metody, Collector.supplier()
która jest wymagana, aby za każdym razem zwracać nową kolekcję.
Te zbiory wyników pośrednich są następnie scalane, ponownie w sposób ograniczony wątkowo, aż do uzyskania pojedynczej kolekcji wyników. To jest ostateczny wynik collect()
operacji.
Kilka odpowiedzi od Baldera i assylias sugerowało użycie, Collectors.toCollection()
a następnie przekazanie dostawcy, który zwraca istniejącą listę zamiast nowej. Narusza to wymóg dostawcy, zgodnie z którym za każdym razem zwraca nową, pustą kolekcję.
To zadziała w prostych przypadkach, jak pokazują przykłady w ich odpowiedziach. Jednak zakończy się niepowodzeniem, zwłaszcza jeśli strumień jest uruchamiany równolegle. (Przyszła wersja biblioteki może ulec zmianie w nieprzewidziany sposób, co spowoduje jej niepowodzenie, nawet w przypadku sekwencyjnym).
Weźmy prosty przykład:
List<String> destList = new ArrayList<>(Arrays.asList("foo"));
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
newList.parallelStream()
.collect(Collectors.toCollection(() -> destList));
System.out.println(destList);
Kiedy uruchamiam ten program, często otrzymuję plik ArrayIndexOutOfBoundsException
. Dzieje się tak, ponieważ wiele wątków działa na ArrayList
strukturze danych niebezpiecznej dla wątków. OK, zsynchronizujmy to:
List<String> destList =
Collections.synchronizedList(new ArrayList<>(Arrays.asList("foo")));
To już nie zawiedzie z wyjątkiem. Ale zamiast oczekiwanego wyniku:
[foo, 0, 1, 2, 3]
daje takie dziwne wyniki:
[foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0]
Jest to wynikiem operacji akumulacji / scalania ograniczonych wątkami, które opisałem powyżej. W przypadku strumienia równoległego każdy wątek wywołuje dostawcę, aby uzyskać własną kolekcję na potrzeby akumulacji pośredniej. Jeśli przekażesz dostawcę, który zwraca tę samą kolekcję, każdy wątek dołącza swoje wyniki do tej kolekcji. Ponieważ nie ma uporządkowania między wątkami, wyniki zostaną dołączone w dowolnej kolejności.
Następnie, gdy te kolekcje pośrednie są scalane, w zasadzie następuje scalenie listy ze sobą. Listy są łączone za pomocą List.addAll()
, co mówi, że wyniki są niezdefiniowane, jeśli kolekcja źródłowa zostanie zmodyfikowana podczas operacji. W tym przypadku ArrayList.addAll()
wykonuje operację kopiowania tablicy, więc w końcu się powiela, co jest chyba czymś, czego można by się spodziewać. (Zauważ, że inne implementacje List mogą mieć zupełnie inne zachowanie). W każdym razie to wyjaśnia dziwne wyniki i zduplikowane elementy w miejscu docelowym.
Możesz powiedzieć: „Po prostu upewnię się, że mój strumień jest uruchamiany sekwencyjnie” i napiszemy taki kod
stream.collect(Collectors.toCollection(() -> existingList))
tak czy siak. Odradzałbym to robić. Jeśli kontrolujesz strumień, na pewno możesz zagwarantować, że nie będzie on działał równolegle. Spodziewam się, że pojawi się styl programowania, w którym zamiast kolekcji będą przekazywane strumienie. Jeśli ktoś przekaże Ci strumień i użyjesz tego kodu, zakończy się niepowodzeniem, jeśli strumień będzie równoległy. Co gorsza, ktoś może podać Ci sekwencyjny strumień i ten kod będzie działał dobrze przez chwilę, przejdzie wszystkie testy itp. Następnie, po jakimś dowolnym czasie, kod w innym miejscu systemu może się zmienić, aby używać równoległych strumieni, co spowoduje, że Twój kod złamać.
OK, po prostu pamiętaj, aby wywołać sequential()
dowolny strumień, zanim użyjesz tego kodu:
stream.sequential().collect(Collectors.toCollection(() -> existingList))
Oczywiście będziesz pamiętać o tym za każdym razem, prawda? :-) Powiedzmy, że tak. Następnie zespół wykonawczy będzie się zastanawiał, dlaczego wszystkie ich starannie przygotowane równoległe implementacje nie zapewniają żadnego przyspieszenia. I po raz kolejny prześledzą to do twojego kodu, który wymusza sekwencyjne działanie całego strumienia.
Nie rób tego.
toCollection
metody za każdym razem zwracał nową i pustą kolekcję, przekonuje mnie, że tego nie robię. Naprawdę chcę złamać kontrakt javadoc dotyczący podstawowych klas Java.
forEachOrdered
. Efekty uboczne obejmują dodawanie elementów do istniejącej kolekcji, niezależnie od tego, czy zawiera już elementy. Jeśli chcesz elementy strumienia jest umieszczony w nowym zbierania, wykorzystywania collect(Collectors.toList())
lub toSet()
lub toCollection()
.
O ile widzę, wszystkie inne odpowiedzi do tej pory wykorzystywały kolektor do dodawania elementów do istniejącego strumienia. Istnieje jednak krótsze rozwiązanie, które działa zarówno w przypadku strumieni sekwencyjnych, jak i równoległych. Możesz po prostu użyć metody forEachOrdered w połączeniu z odwołaniem do metody.
List<String> source = ...;
List<Integer> target = ...;
source.stream()
.map(String::length)
.forEachOrdered(target::add);
Jedynym ograniczeniem jest to, że źródło i cel to różne listy, ponieważ nie możesz wprowadzać zmian w źródle strumienia, dopóki jest on przetwarzany.
Należy pamiętać, że to rozwiązanie działa zarówno w przypadku strumieni sekwencyjnych, jak i równoległych. Jednak współbieżność nie jest korzystna. Odwołanie do metody przekazane do forEachOrdered będzie zawsze wykonywane sekwencyjnie.
forEach(existing::add)
jako możliwość w odpowiedzi dwa miesiące temu . Powinienem był również dodać forEachOrdered
…
forEachOrdered
zamiast forEach
?
forEachOrdered
działa zarówno dla strumieni sekwencyjnych, jak i równoległych . W przeciwieństwie do tego, forEach
może wykonywać przekazany obiekt funkcji współbieżnie dla równoległych strumieni. W takim przypadku obiekt funkcji musi być odpowiednio zsynchronizowany, np. Za pomocą pliku Vector<Integer>
.
target::add
. Bez względu na to, z których wątków wywoływana jest metoda, nie ma wyścigu danych . Spodziewałbym się, że to wiesz.
Krótka odpowiedź brzmi: nie (lub nie powinno). EDYCJA: tak, to możliwe (patrz odpowiedź assylias poniżej), ale czytaj dalej. EDIT2: ale spójrz na odpowiedź Stuarta Marksa z jeszcze jednego powodu, dla którego nadal nie powinieneś tego robić!
Dłuższa odpowiedź:
Celem tych konstrukcji w Javie 8 jest wprowadzenie do języka pewnych koncepcji programowania funkcjonalnego ; W programowaniu funkcjonalnym struktury danych zwykle nie są modyfikowane, zamiast tego tworzone są nowe ze starych za pomocą przekształceń, takich jak mapowanie, filtrowanie, zwijanie / zmniejszanie i wiele innych.
Jeśli musisz zmodyfikować starą listę, po prostu zbierz zmapowane elementy do nowej listy:
final List<Integer> newList = list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
a potem zrób list.addAll(newList)
- znowu: jeśli naprawdę musisz.
(lub utwórz nową listę łączącą starą i nową i przypisz ją z powrotem do list
zmiennej - jest to trochę bardziej w duchu FP niż addAll
)
Co do API: mimo że API na to pozwala (zobacz odpowiedź assyliasa), powinieneś starać się tego unikać, przynajmniej ogólnie. Najlepiej nie walczyć z paradygmatem (FP) i starać się go raczej nauczyć niż z nim walczyć (chociaż Java generalnie nie jest językiem FP) i uciekać się do „brudniejszych” taktyk tylko wtedy, gdy jest to absolutnie konieczne.
Naprawdę długa odpowiedź: (tj. Jeśli uwzględnisz wysiłek znalezienia i przeczytania intro / książki FP zgodnie z sugestią)
Aby dowiedzieć się, dlaczego modyfikowanie istniejących list jest ogólnie złym pomysłem i prowadzi do trudniejszego w utrzymaniu kodu - chyba że modyfikujesz zmienną lokalną, a Twój algorytm jest krótki i / lub trywialny, co wykracza poza zakres kwestii utrzymania kodu —Znajdź dobre wprowadzenie do programowania funkcjonalnego (jest ich setki) i zacznij czytać. „Wstępne” wyjaśnienie mogłoby wyglądać mniej więcej tak: jest bardziej rozsądne matematycznie i łatwiejsze do uzasadnienia, że nie należy modyfikować danych (w większości części programu) i prowadzi do wyższego poziomu i mniej technicznego (a także bardziej przyjaznego dla człowieka, gdy mózg odejście od myślenia imperatywnego w starym stylu) definicje logiki programu.
Erik Allik już podał bardzo dobre powody, dla których najprawdopodobniej nie będziesz chciał zbierać elementów strumienia do istniejącej listy.
W każdym razie, jeśli naprawdę potrzebujesz tej funkcjonalności, możesz skorzystać z poniższej linijki.
Ale jak wyjaśnia Stuart Marks w swojej odpowiedzi, nigdy nie powinieneś tego robić, jeśli strumienie mogą być równoległe - używaj na własne ryzyko ...
list.stream().collect(Collectors.toCollection(() -> myExistingList));
Musisz tylko odnieść się do swojej oryginalnej listy, aby była tą, która Collectors.toList()
zwraca.
Oto demo:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Reference {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
System.out.println(list);
// Just collect even numbers and start referring the new list as the original one.
list = list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(list);
}
}
A oto jak możesz dodać nowo utworzone elementy do swojej oryginalnej listy w jednej linii.
List<Integer> list = ...;
// add even numbers from the list to the list again.
list.addAll(list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList())
);
To właśnie zapewnia ten paradygmat programowania funkcjonalnego.
Chciałbym połączyć starą listę i nową listę jako strumienie i zapisać wyniki na liście miejsc docelowych. Działa dobrze również równolegle.
Posłużę się przykładem zaakceptowanej odpowiedzi udzielonej przez Stuarta Marksa:
List<String> destList = Arrays.asList("foo");
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
destList = Stream.concat(destList.stream(), newList.stream()).parallel()
.collect(Collectors.toList());
System.out.println(destList);
//output: [foo, 0, 1, 2, 3, 4, 5]
Mam nadzieję, że to pomoże.
Collection
”