Dlaczego Iterable <T> nie zapewnia metod stream () i parallelStream ()?

241

Zastanawiam się, dlaczego Iterableinterfejs nie zapewnia metod stream()i parallelStream(). Rozważ następującą klasę:

public class Hand implements Iterable<Card> {
    private final List<Card> list = new ArrayList<>();
    private final int capacity;

    //...

    @Override
    public Iterator<Card> iterator() {
        return list.iterator();
    }
}

Jest to realizacja ręku jak można mieć karty w ręku podczas gry karcianej Game.

Zasadniczo otacza a List<Card>, zapewnia maksymalną pojemność i oferuje kilka innych przydatnych funkcji. Lepiej jest go wdrożyć bezpośrednio jako List<Card>.

Teraz, dla wygody, pomyślałem, że fajnie byłoby zaimplementować Iterable<Card>takie, że możesz użyć ulepszonych pętli for, jeśli chcesz nad nimi zapętlić. (Moja Handklasa zapewnia również get(int index)metodę, dlatego Iterable<Card>moim zdaniem jest to uzasadnione).

IterableInterfejs zapewnia następujące (w lewo na javadoc)

public interface Iterable<T> {
    Iterator<T> iterator();

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

Teraz możesz uzyskać strumień z:

Stream<Hand> stream = StreamSupport.stream(hand.spliterator(), false);

Tak więc na prawdziwe pytanie:

  • Dlaczego Iterable<T>nie zapewnia domyślnych metod, które się wdrożą stream()i parallelStream()nie widzę nic, co uczyniłoby to niemożliwym lub niechcianym?

Znalazłem podobne pytanie: Dlaczego Stream <T> nie implementuje Iterable <T>?
Co dziwnie sugeruje, że można to zrobić na odwrót.

skiwi
źródło
1
Myślę, że to dobre pytanie do listy mailingowej Lambda .
Edwin Dalorzo
Dlaczego to dziwne, że chcesz iterować po strumieniu? Jak inaczej możesz break;wykonać iterację? (Ok, Stream.findFirst()może to być rozwiązanie, ale może nie spełniać wszystkich potrzeb ...)
glglgl
Zobacz także Konwertuj Iterable na strumień za pomocą JDK Java 8, aby uzyskać praktyczne rozwiązania.
Vadzim

Odpowiedzi:

298

To nie było pominięciem; szczegółowo omówiono listę EG w czerwcu 2013 r.

Ostateczna dyskusja grupy ekspertów jest zakorzeniona w tym wątku .

Chociaż wydawało się to „oczywiste” (początkowo nawet dla grupy ekspertów), stream()wydawało się , że ma to sens Iterable, fakt, że Iterablebył tak ogólny, stał się problemem, ponieważ oczywisty podpis:

Stream<T> stream()

nie zawsze było to, czego chciałeś. Niektóre rzeczy, które były Iterable<Integer>raczej mają ich sposób strumień zwracać IntStream, na przykład. Ale umieszczenie stream()metody tak wysoko w hierarchii uniemożliwiłoby to. Zamiast więc zrobiliśmy to naprawdę łatwe do wykonania Streamze związku Iterable, dostarczając spliterator()metody. Implementacja stream()w Collectionjest po prostu:

default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}

Każdy klient może uzyskać żądany strumień Iterablez:

Stream s = StreamSupport.stream(iter.spliterator(), false);

W końcu doszliśmy do wniosku, że dodawanie stream()do Iterablebyłoby błędem.

Brian Goetz
źródło
8
Przede wszystkim bardzo dziękuję za odpowiedź na pytanie. Nadal jestem ciekawy, dlaczego Iterable<Integer>(myślę, że mówisz?) Chciałbyś zwrócić IntStream. Czy zatem iterowalność raczej nie byłaby PrimitiveIterator.OfInt? A może masz na myśli inny przypadek użycia?
skiwi
139
Dziwne wydaje mi się, że powyższa logika została rzekomo zastosowana do Iterable (nie mogę mieć stream (), ponieważ ktoś może chcieć zwrócić IntStream), podczas gdy nie zastanawiałem się nad dodaniem dokładnie tej samej metody do Collection ( Mogę też chcieć, aby stream () integretu mojej kolekcji <Integer> zwrócił również IntStream.) Bez względu na to, czy były obecne na obu, czy nieobecne na obu, ludzie prawdopodobnie po prostu poradziliby sobie z życiem, ale ponieważ są obecne na jednym, a nieobecne na drugi staje się rażącym pominięciem ...
Trejkaz
6
Brian McCutchon: To ma dla mnie więcej sensu. Wygląda na to, że ludzie zmęczyli się kłótniami i postanowili grać bezpiecznie.
Jonathan Locke
44
Chociaż ma to sens, istnieje powód, dla którego nie istnieje alternatywna Stream.of(Iterable)metoda statyczna , która przynajmniej uczyniłaby tę metodę racjonalnie możliwą do wykrycia przez przeczytanie dokumentacji API - jako kogoś, kto nigdy tak naprawdę nie pracował z elementami wewnętrznymi strumieni, których nigdy nie nawet spojrzał na to StreamSupport, co jest opisane w dokumentacji jako zapewniające „operacje niskiego poziomu”, które są „głównie dla twórców bibliotek”.
Jules
9
Całkowicie zgadzam się z Jules. zamiast StreamSupport.stream (iter.spliterator (), false) należy dodać metodę statyczną Stream.of (iterowalny iter) lub Stream.of (iter iterator);
user_3380739,
23

Przeprowadziłem dochodzenie na kilku listach mailingowych projektu lambda i myślę, że znalazłem kilka interesujących dyskusji.

Jak dotąd nie znalazłem zadowalającego wyjaśnienia. Po przeczytaniu tego wszystkiego doszedłem do wniosku, że to tylko pominięcie. Ale widać tutaj, że omawiano go kilka razy na przestrzeni lat podczas projektowania interfejsu API.

Lambda Libs Spec Experts

Dyskusję na ten temat znalazłem na liście dyskusyjnej Lambda Libs Spec Experts :

W części Iterable / Iterator.stream () Sam Pullara powiedział:

Pracowałem z Brianem nad tym, w jaki sposób można wdrożyć funkcjonalność limitu / podstrumienia [1], a on zasugerował, że konwersja do Iteratora jest właściwą drogą do tego. Myślałem o tym rozwiązaniu, ale nie znalazłem żadnego oczywistego sposobu, aby wziąć iterator i zamienić go w strumień. Okazuje się, że tam jest, wystarczy przekonwertować iterator na spliterator, a następnie przekonwertować spliterator na strumień. To skłania mnie do ponownego rozważenia, czy powinniśmy mieć te zawieszone bezpośrednio w jednym z Iterable / Iterator, czy w obu.

Sugeruję, aby przynajmniej mieć go na Iteratorze, abyś mógł swobodnie poruszać się między dwoma światami, a także byłoby łatwo go odkryć, niż robić:

Streams.stream (Spliterators.spliteratorUnknownSize (iterator, Spliterator.ORDERED))

A potem Brian Goetz odpowiedział :

Myślę, że Sam miał na myśli to, że istnieje wiele klas bibliotecznych, które dają ci iterator, ale niekoniecznie musisz pisać własnego spliteratora. Wszystko, co możesz zrobić, to strumień połączeń (spliteratorUnknownSize (iterator)). Sam sugeruje zdefiniowanie Iterator.stream (), aby zrobić to za Ciebie.

Chciałbym zachować metody stream () i spliterator () jako przeznaczone dla autorów bibliotek / zaawansowanych użytkowników.

I później

„Biorąc pod uwagę, że pisanie Spliteratora jest łatwiejsze niż pisanie Iteratora, wolałbym po prostu napisać Spliteratora zamiast Iteratora (Iterator jest taki z lat 90.”)

Jednak brakuje ci sensu. Istnieją setki klas, które już dają ci Iterator. I wiele z nich nie jest przystosowanych do spliteratora.

Poprzednie dyskusje na liście mailingowej Lambda

Być może nie jest to odpowiedź, której szukasz, ale na liście dyskusyjnej projektu Lambda zostało to krótko omówione. Być może pomaga to w szerszej dyskusji na ten temat.

Według słów Briana Goetza w Streams from Iterable :

Cofając się ...

Istnieje wiele sposobów tworzenia strumienia. Im więcej informacji na temat opisywania elementów, tym więcej funkcjonalności i wydajności może zapewnić biblioteka strumieni. W kolejności od co najmniej do większości informacji są to:

Iterator

Iterator + rozmiar

Spliterator

Spliterator, który zna jego rozmiar

Spliterator, który zna jego rozmiar, a ponadto wie, że wszystkie podziały znają swój rozmiar.

(Niektórzy mogą być zaskoczeni, gdy stwierdzimy, że możemy wydobyć paralelizm nawet z głupiego iteratora w przypadkach, gdy Q (praca na element) nie jest łatwe.)

Gdyby Iterable miał metodę stream (), po prostu owinąłby Iterator Spliteratorem, bez informacji o rozmiarze. Ale większość rzeczy, które są iterowalne , zawiera informacje o rozmiarze. Co oznacza, że ​​obsługujemy niedobór strumieni. To nie jest takie dobre.

Jedną z wad praktyki API opisanej tutaj przez Stephena, polegającej na akceptowaniu Iterable zamiast Collection, jest to, że zmuszasz rzeczy przez „małą rurkę”, a zatem odrzucasz informacje o rozmiarze, gdy może to być przydatne. To dobrze, jeśli robisz to tylko dla każdego, ale jeśli chcesz zrobić więcej, lepiej jest, jeśli możesz zachować wszystkie potrzebne informacje.

Domyślne ustawienie dostarczone przez Iterable byłoby naprawdę gówniane - odrzuciłoby rozmiar, mimo że zdecydowana większość Iterable zna te informacje.

Sprzeczność?

Chociaż wygląda na to, że dyskusja opiera się na zmianach, które Grupa Ekspertów wprowadziła do początkowego projektu Strumieni, który początkowo był oparty na iteratorach.

Mimo to warto zauważyć, że w interfejsie takim jak Collection metoda stream jest zdefiniowana jako:

default Stream<E> stream() {
   return StreamSupport.stream(spliterator(), false);
}

Może to być dokładnie ten sam kod użyty w interfejsie Iterable.

Dlatego powiedziałem, że ta odpowiedź prawdopodobnie nie jest zadowalająca, ale nadal interesująca w dyskusji.

Dowody refaktoryzacji

Kontynuując analizę na liście mailingowej, wygląda na to, że metoda splitIterator była pierwotnie w interfejsie Collection, aw pewnym momencie w 2013 roku przenieśli ją do Iterable.

Pociągnij splitIterator z kolekcji do Iterable .

Wnioski / teorie?

Są zatem szanse, że brak metody w Iterable jest tylko pominięciem, ponieważ wygląda na to, że powinni również przenieść metodę stream, gdy przenieśli splitIterator z Collection do Iterable.

Jeśli istnieją inne powody, nie są one oczywiste. Ktoś inny ma inne teorie?

Edwin Dalorzo
źródło
Doceniam twoją odpowiedź, ale nie zgadzam się z uzasadnieniem. W tej chwili, że nadpisać spliterator()z Iterable, wówczas wszystkie kwestie nie są stałe i można trywialnie wdrożenia stream()i parallelStream()..
skiwi
@skiwi Dlatego powiedziałem, że to prawdopodobnie nie jest odpowiedź. Próbuję tylko dodać do dyskusji, ponieważ trudno jest ustalić, dlaczego grupa ekspertów podjęła decyzje, które podjęła. Wydaje mi się, że wszystko, co możemy zrobić, to spróbować zrobić kryminalistykę na liście mailingowej i sprawdzić, czy możemy wymyślić jakieś powody.
Edwin Dalorzo
1
@skiwi Przejrzałem inne listy mailingowe i znalazłem więcej dowodów na dyskusję i być może kilka pomysłów, które pomagają w postawieniu diagnozy.
Edwin Dalorzo
Dzięki za twój wysiłek powinienem naprawdę nauczyć się, jak skutecznie dzielić listy mailowe. Pomogłoby to, gdyby można je było wizualizować w jakiś ... nowoczesny sposób, np. Forum lub coś takiego, ponieważ czytanie e-maili zawierających zwykły tekst z cytatami nie jest do końca skuteczne.
skiwi
6

Jeśli znasz rozmiar, którego możesz użyć, java.util.Collectionktóry zapewnia stream()metodę:

public class Hand extends AbstractCollection<Card> {
   private final List<Card> list = new ArrayList<>();
   private final int capacity;

   //...

   @Override
   public Iterator<Card> iterator() {
       return list.iterator();
   }

   @Override
   public int size() {
      return list.size();
   }
}

I wtedy:

new Hand().stream().map(...)

Napotkałem ten sam problem i byłem zaskoczony, że moją Iterableimplementację można bardzo łatwo rozszerzyć na AbstractCollectionimplementację, po prostu dodając size()metodę (na szczęście miałem rozmiar kolekcji :-)

Należy również rozważyć zastąpienie Spliterator<E> spliterator().

Udo
źródło