Jak dodać elementy strumienia Java8 do istniejącej listy

155

Javadoc of Collector pokazuje, jak zbierać elementy strumienia do nowej listy. Czy istnieje jedna linijka, która dodaje wyniki do istniejącej tablicy ArrayList?

codefx
źródło
1
Jest już odpowiedź tutaj . Poszukaj pozycji „Dodaj do istniejącego Collection
Holger

Odpowiedzi:

197

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 Collectordo modyfikowania istniejącej kolekcji.

Krótka odpowiedź brzmi: nie , przynajmniej nie generalnie, nie powinieneś używać a Collectordo 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 ArrayListstrukturze 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.

Stuart Marks
źródło
Świetne wyjaśnienie! - dzięki za wyjaśnienie tego. Zmienię odpowiedź, aby odradzać robienie tego z możliwymi równoległymi strumieniami.
Balder
3
Jeśli pytanie brzmi, czy istnieje jednolinijkowy element służący do dodawania elementów strumienia do istniejącej listy, to krótka odpowiedź brzmi: tak . Zobacz moją odpowiedź. Zgadzam się jednak z Tobą, że używanie Collectors.toCollection () w połączeniu z istniejącą listą jest niewłaściwe.
nosid
Prawdziwe. Myślę, że reszta z nas myślała o kolekcjonerach.
Stuart Marks
Świetna odpowiedź! Kusi mnie, aby skorzystać z rozwiązania sekwencyjnego, nawet jeśli wyraźnie odradzasz, ponieważ jak stwierdzono, musi działać dobrze. Ale fakt, że javadoc wymaga, aby argument dostawcy toCollectionmetody 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.
powiększenie
1
@AlexCurvers Jeśli chcesz, aby strumień miał efekty uboczne, prawie na pewno chcesz go użyć 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().
Stuart Marks
169

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.

nosid
źródło
6
+1 To zabawne, jak wiele osób twierdzi, że nie ma takiej możliwości, jeśli taka istnieje. Przy okazji. Uwzględniłem forEach(existing::add)jako możliwość w odpowiedzi dwa miesiące temu . Powinienem był również dodać forEachOrdered
Holger
5
Czy jest jakiś powód, dla którego użyłeś forEachOrderedzamiast forEach?
członkowie wokół
6
@membersound: forEachOrdereddziała zarówno dla strumieni sekwencyjnych, jak i równoległych . W przeciwieństwie do tego, forEachmoż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>.
nosid
@BrianGoetz: Muszę przyznać, że dokumentacja Stream.forEachOrdered jest trochę nieprecyzyjna. Jednak ja nie widzę żadnej rozsądnej interpretacji niniejszej specyfikacji , w której nie ma się dzieje, zanim relacji między dowolnymi dwoma wezwań 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.
nosid
O ile mi wiadomo, to najbardziej przydatna odpowiedź. W rzeczywistości pokazuje praktyczny sposób wstawiania elementów do istniejącej listy ze strumienia, o co zadawano pytanie (pomimo wprowadzającego w błąd słowa „zbierać”)
Wheezil
12

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 listzmiennej - 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 Kaplun
źródło
@assylias: logicznie rzecz biorąc, nie było to złe, ponieważ istniała część lub ; w każdym razie dodał notatkę.
Erik Kaplun
1
Krótka odpowiedź jest prawidłowa. Zaproponowane jednoliniowe rozwiązania sprawdzą się w prostych przypadkach, ale zawiodą w przypadku ogólnym.
Stuart Marks
Dłuższa odpowiedź jest w większości poprawna, ale projekt API dotyczy głównie równoległości, a mniej programowania funkcjonalnego. Chociaż oczywiście jest wiele rzeczy związanych z FP, które są podatne na równoległość, więc te dwie koncepcje są dobrze dopasowane.
Stuart Marks
@StuartMarks: Interesujące: w jakich przypadkach psuje się rozwiązanie podane w odpowiedzi assyliasa? (i dobre punkty na temat równoległości
wydaje
@ErikAllik Dodałem odpowiedź, która obejmuje ten problem.
Stuart Marks
11

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));
Balder
źródło
ahh, szkoda: P
Erik Kaplun
2
Ta technika zawiedzie okropnie, jeśli strumień będzie prowadzony równolegle.
Stuart Marks
1
Obowiązkiem dostawcy kolekcji byłoby upewnienie się, że nie zawiedzie - np. Poprzez zapewnienie zbierania równoległego.
Balder
2
Nie, ten kod narusza wymaganie metody toCollection (), która polega na tym, że dostawca zwraca nową, pustą kolekcję odpowiedniego typu. Nawet jeśli miejsce docelowe jest bezpieczne dla wątków, scalanie wykonane dla przypadku równoległego spowoduje nieprawidłowe wyniki.
Stuart Marks
1
@Balder Dodałem odpowiedź, która powinna to wyjaśnić.
Stuart Marks
4

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.

Aman Agnihotri
źródło
Chciałem powiedzieć, jak dodać / zebrać do istniejącej listy, a nie tylko ponownie przypisać.
codefx
1
Cóż, technicznie rzecz biorąc, nie można robić tego typu rzeczy w paradygmacie programowania funkcjonalnego, o który chodzi w strumieniach. W programowaniu funkcjonalnym stan nie jest modyfikowany, zamiast tego nowe stany są tworzone w trwałych strukturach danych, dzięki czemu jest bezpieczny dla celów współbieżności i bardziej funkcjonalny. Podejście, o którym wspomniałem, jest tym, co możesz zrobić, lub możesz skorzystać z podejścia obiektowego w starym stylu, w którym iterujesz po każdym elemencie i zachowujesz lub usuwasz elementy według własnego uznania.
Aman Agnihotri
0

targetList = sourceList.stream (). flatmap (List :: stream) .collect (Collectors.toList ());

AS Ranjan
źródło
0

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.

Nikos Stais
źródło