Dlaczego Stream <T> nie implementuje Iterable <T>?

261

W Javie 8 mamy klasę Stream <T> , która, co ciekawe, ma metodę

Iterator<T> iterator()

Można się więc spodziewać, że zaimplementuje interfejs Iterable <T> , który wymaga dokładnie tej metody, ale tak nie jest.

Kiedy chcę iterować po strumieniu za pomocą pętli foreach, muszę zrobić coś takiego

public static Iterable<T> getIterable(Stream<T> s) {
    return new Iterable<T> {
        @Override
        public Iterator<T> iterator() {
            return s.iterator();
        }
    };
}

for (T element : getIterable(s)) { ... }

Czy coś mi umyka?

roim
źródło
7
nie wspominając, że pozostałe 2 metody iterowalne (forEach i spliterator) są również w strumieniu
njzk2
1
jest to konieczne, aby przejść Streamdo starszych interfejsów API, które oczekująIterable
ZhongYu,
11
Dobre IDE (np. IntelliJ) poprosi cię o uproszczenie kodu getIterable()doreturn s::iterator;
ZhongYu,
23
W ogóle nie potrzebujesz metody. Jeśli masz Stream i chcesz Iterable, po prostu przekaż stream :: iterator (lub, jeśli wolisz, () -> stream.iterator ()) i gotowe.
Brian Goetz,
6
Niestety nie mogę pisać for (T element : stream::iterator), więc nadal wolałbym, aby Stream również zaimplementowałby Iterablelub metodę toIterable().
Thorsten

Odpowiedzi:

197

Ludzie pytali już o to samo na liście mailingowej ☺. Głównym powodem jest to, że Iterable ma również powtarzalną semantię, podczas gdy Stream nie.

Myślę, że głównym powodem jest to, że Iterableoznacza to możliwość ponownego użycia, podczas gdy Streamjest to coś, co można wykorzystać tylko raz - bardziej jak Iterator.

Jeśli zostanie Streamrozszerzony, Iterableistniejący kod może być zaskoczony, gdy otrzyma komunikat, Iterablektóry generuje Exceptionpo raz drugi for (element : iterable).

kennytm
źródło
22
Co ciekawe, w Javie 7 istniały już pewne iterowalne wersje, np. DirectoryStream: Chociaż DirectoryStream rozszerza Iterable, nie jest to Iterable ogólnego przeznaczenia, ponieważ obsługuje tylko jeden Iterator; wywołanie metody iteratora w celu uzyskania drugiego lub kolejnego iteratora zgłasza IllegalStateException. ( openjdk.java.net/projects/nio/javadoc/java/nio/file/… )
podróż
31
Niestety, nie ma nic w dokumentacji Iterabledotyczącej tego, czy iteratornależy zawsze, czy nie, można wywoływać wielokrotnie. To coś, co powinni tam umieścić. Wydaje się, że jest to bardziej standardowa praktyka niż formalna specyfikacja.
Lii
7
Jeśli zamierzają użyć wymówek, można by pomyśleć, że mogliby przynajmniej dodać metodę asIterable () - lub przeciążenia dla wszystkich metod, które przyjmują tylko Iterable.
Trejkaz
25
Być może najlepszym rozwiązaniem byłoby sprawienie, aby Foreach Java zaakceptował Iterable <T>, a także potencjalnie Stream <T>?
pożarł elysium
3
@Lii Standardowe praktyki są jednak dość mocne.
biziclop
160

Aby przekonwertować Streamna a Iterable, możesz to zrobić

Stream<X> stream = null;
Iterable<X> iterable = stream::iterator

Aby przekazać Streammetodę, która oczekuje Iterable:

void foo(Iterable<X> iterable)

po prostu

foo(stream::iterator) 

jednak prawdopodobnie wygląda śmiesznie; może lepiej być trochę bardziej wyraźnym

foo( (Iterable<X>)stream::iterator );
ZhongYu
źródło
66
Możesz również użyć tego w pętli for(X x : (Iterable<X>)stream::iterator), choć wygląda brzydko. Naprawdę cała ta sytuacja jest po prostu absurdalna.
Aleksandr Dubinsky
24
@HRJIntStream.range(0,N).forEach(System.out::println)
MikeFHay
11
W tym kontekście nie rozumiem składni podwójnego dwukropka. Jaka jest różnica między tym, stream::iteratora stream.iterator()tym samym sprawia, że ​​pierwsze jest akceptowalne dla Iterabledrugiego, ale nie drugie?
Daniel C. Sobral,
20
Odpowiadając sobie: Iterableto interfejs funkcjonalny, więc wystarczy przekazać funkcję, która go implementuje.
Daniel C. Sobral,
5
Warto zauważyć, że to się zepsuje, jeśli kod odbierający spróbuje ponownie użyć Iterable (na przykład dwie osobne pętle dla każdej), na odpowiedź kennyTM . To technicznie psuje specyfikację. Możesz użyć strumienia tylko raz; nie można wywołać dwukrotnie BaseStream :: iterator. Pierwsze połączenie kończy strumień. Według JavaDoc: „To jest operacja terminalowa”. Chociaż to obejście jest wygodne, nie należy przekazywać wynikowego Iterable do kodu, który nie jest pod twoją kontrolą.
Zenexer
10

Chciałbym zauważyć, że StreamEximplementuje Iterable(i Stream), a także wiele innych niesamowicie niesamowitych funkcji, których brakuje Stream.

Aleksandr Dubinsky
źródło
8

Możesz użyć strumienia w forpętli w następujący sposób:

Stream<T> stream = ...;

for (T x : (Iterable<T>) stream::iterator) {
    ...
}

(Uruchom ten fragment tutaj )

(Używa to obsady interfejsu Java 8).

(Jest to opisane w niektórych powyższych komentarzach (np. Aleksandr Dubinsky ), ale chciałem wyciągnąć je w odpowiedzi, aby było bardziej widoczne.)

Bogaty
źródło
Prawie wszyscy muszą na to spojrzeć dwa lub trzy razy, zanim zdadzą sobie sprawę, jak się nawet kompiluje. To jest absurdalne. (Jest to uwzględnione w odpowiedzi na komentarze w tym samym strumieniu komentarzy, ale chciałem tutaj dodać komentarz, aby był bardziej widoczny.)
ShioT
7

kennytm opisał, dlaczego niebezpieczne jest traktowanie Streamjako „a” Iterable, a Zhong Yu zaproponował obejście, które pozwala na użycie „tak Streamjak” Iterable, choć w niebezpieczny sposób. Jest to możliwe, aby uzyskać najlepsze z obu światów: wielokrotnego użytku Iterablez punktu A Stream, który spełnia wszelkie gwarancje dokonane przez Iterablespecyfikację.

Uwaga: SomeTypenie jest tutaj parametrem typu - należy go zastąpić odpowiednim typem (np. String) Lub zastosować odbicie

Stream<SomeType> stream = ...;
Iterable<SomeType> iterable = stream.collect(toList()):

Jest jedna poważna wada:

Korzyści z leniwej iteracji zostaną utracone. Jeśli planujesz natychmiastowe iterowanie wszystkich wartości w bieżącym wątku, wszelkie koszty ogólne będą nieistotne. Jeśli jednak planujesz iterować tylko częściowo lub w innym wątku, ta natychmiastowa i pełna iteracja może mieć niezamierzone konsekwencje.

Dużą zaletą jest oczywiście to, że można ponownie użyć Iterable, podczas gdy (Iterable<SomeType>) stream::iteratorzezwala się tylko na jedno użycie. Jeśli kod odbierający będzie wielokrotnie iterował kolekcję, jest to nie tylko konieczne, ale prawdopodobnie korzystne dla wydajności.

Zenexer
źródło
1
Czy próbowałeś skompilować kod przed odpowiedzią? To nie działa
Tagir Valeev
1
@TagirValeev Tak, zrobiłem. Musisz zastąpić T odpowiednim typem. Skopiowałem przykład z działającego kodu.
Zenexer
2
@TagirValeev Przetestowałem to ponownie w IntelliJ. Wygląda na to, że IntelliJ czasami się myli z powodu tej składni; Tak naprawdę nie znalazłem wzoru. Jednak kod kompiluje się dobrze, a IntelliJ usuwa komunikat o błędzie po kompilacji. Myślę, że to tylko błąd.
Zenexer
1
Stream.toArray()zwraca tablicę, a nie an Iterable, więc ten kod nadal się nie kompiluje. ale może to być błąd w zaćmieniu, ponieważ IntelliJ wydaje się to kompilować
benez
1
@Zenexer Jak udało Ci się przypisać tablicę do Iterable?
radiantRazor
3

Streamnie implementuje Iterable. Ogólne zrozumienie Iterablejest wszystkim, co można powtarzać, często od nowa. Streammogą nie być odtwarzane.

Jedynym sposobem, w jaki mogę wymyślić, gdzie można odtworzyć iterację opartą na strumieniu, jest odtworzenie strumienia. Używam Supplierponiżej, aby utworzyć nową instancję strumienia, za każdym razem, gdy tworzony jest nowy iterator.

    Supplier<Stream<Integer>> streamSupplier = () -> Stream.of(10);
    Iterable<Integer> iterable = () -> streamSupplier.get().iterator();
    for(int i : iterable) {
        System.out.println(i);
    }
    // Can iterate again
    for(int i : iterable) {
        System.out.println(i);
    }
Ashish Tyagi
źródło
2

Jeśli nie przeszkadza przy użyciu bibliotek stron trzecich cyklop reagują definiuje Stream, który implementuje zarówno Stream i iterable i replayable zbyt (rozwiązując problem kennytm opisane ).

 Stream<String> stream = ReactiveSeq.of("hello","world")
                                    .map(s->"prefix-"+s);

lub:

 Iterable<String> stream = ReactiveSeq.of("hello","world")
                                      .map(s->"prefix-"+s);

 stream.forEach(System.out::println);
 stream.forEach(System.out::println);

[Ujawnienie Jestem wiodącym twórcą Cyclops-reag]

John McClean
źródło
0

Nie idealnie, ale zadziała:

iterable = stream.collect(Collectors.toList());

Nie idealne, ponieważ będzie pobrać wszystkie elementy ze strumienia i umieścić je w to List, co nie jest dokładnie to, co Iterablei Streamchodzi. Powinny być leniwi .

yegor256
źródło
-1

Możesz iterować wszystkie pliki w folderze, korzystając z Stream<Path>tego:

Path path = Paths.get("...");
Stream<Path> files = Files.list(path);

for (Iterator<Path> it = files.iterator(); it.hasNext(); )
{
    Object file = it.next();

    // ...
}
BullyWiiPlaza
źródło