Apakah ada cara ringkas untuk beralih di atas aliran dengan indeks di Java 8?


382

Apakah ada cara ringkas untuk beralih di atas aliran sementara memiliki akses ke indeks di arus?

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};

List<String> nameList;
Stream<Integer> indices = intRange(1, names.length).boxed();
nameList = zip(indices, stream(names), SimpleEntry::new)
        .filter(e -> e.getValue().length() <= e.getKey())
        .map(Entry::getValue)
        .collect(toList());

yang tampaknya agak mengecewakan dibandingkan dengan contoh LINQ yang diberikan di sana

string[] names = { "Sam", "Pamela", "Dave", "Pascal", "Erik" };
var nameList = names.Where((c, index) => c.Length <= index + 1).ToList();

Apakah ada cara yang lebih ringkas?

Lebih lanjut tampaknya zip telah dipindahkan atau dihapus ...


2
Apa intRange()? Belum menemukan metode ini di Java 8 sampai sekarang.
Rohit Jain

@RohitJain Mungkin an IntStream.rangeClosed(x, y).
assylias

2
Sebagai komentar sampingan, tantangan 4 terlihat lebih baik (IMO) denganList<String> allCities = map.values().stream().flatMap(list -> list.stream()).collect(Collectors.toList());
assylias

3
Ya, ziptelah dihapus, bersama dengan aliran dua nilai eksperimental dengan berbagai cara disebut BiStreamatau MapStream. Masalah utama adalah bahwa untuk melakukan ini secara efektif, Java benar-benar membutuhkan tipe (atau tuple) yang diketik secara struktural. Karena tidak memiliki satu, mudah untuk membuat kelas Pair atau Tuple generik - sudah dilakukan berkali-kali - tetapi semuanya terhapus dengan tipe yang sama.
Stuart Marks

3
Oh, masalah lain dengan kelas Pair atau Tuple generik adalah mengharuskan semua primitif untuk dikotak.
Stuart Marks

Jawaban:


435

Cara terbersih adalah mulai dari aliran indeks:

String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};
IntStream.range(0, names.length)
         .filter(i -> names[i].length() <= i)
         .mapToObj(i -> names[i])
         .collect(Collectors.toList());

Daftar yang dihasilkan hanya berisi "Erik".


Salah satu alternatif yang terlihat lebih akrab ketika Anda terbiasa dengan loop adalah mempertahankan counter ad hoc menggunakan objek yang bisa berubah, misalnya AtomicInteger:

String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};
AtomicInteger index = new AtomicInteger();
List<String> list = Arrays.stream(names)
                          .filter(n -> n.length() <= index.incrementAndGet())
                          .collect(Collectors.toList());

Perhatikan bahwa menggunakan metode terakhir pada aliran paralel dapat pecah karena item tidak perlu diproses "dalam urutan" .


28
Menggunakan atom dengan cara ini bermasalah dengan aliran paralel. Pertama, urutan pemrosesan elemen tidak harus sama dengan urutan elemen terjadi dalam array awal. Dengan demikian, "indeks" yang ditetapkan menggunakan atom mungkin tidak akan cocok dengan indeks array yang sebenarnya. Kedua, meskipun atom-thread-safe, Anda mungkin menghadapi pertentangan di antara banyak utas yang memperbarui atom, menurunkan jumlah paralelisme.
Stuart Marks

1
Saya mengembangkan solusi yang mirip dengan yang dilakukan oleh @assylias. Untuk menghindari masalah dengan aliran paralel @StuartMarks yang disebutkan, saya pertama kali membuat aliran paralel yang diberikan secara berurutan, melakukan pemetaan dan mengembalikan keadaan paralel. public static <T> Stream<Tuple2<Integer, T>> zipWithIndex(Stream<T> stream) { final AtomicInteger index = new AtomicInteger(); final Function<T, Tuple2<Integer, T>> zipper = e -> Tuples.of(index.getAndIncrement(), e); if (stream.isParallel()) { return stream.sequential().map(zipper).parallel(); } else { return stream.map(zipper); } }
Daniel Dietrich

4
@DanielDietrich Jika Anda pikir itu memecahkan pertanyaan, Anda harus mempostingnya sebagai jawaban daripada komentar (dan kode akan lebih mudah dibaca juga!).
assylias

3
@DanielDietrich Maaf, jika saya membaca kode itu dengan benar, itu tidak akan berfungsi. Anda tidak dapat memiliki segmen berbeda dari pipa yang berjalan secara paralel vs berurutan. Hanya yang terakhir parallelatau sequentialyang dihormati ketika operasi terminal dimulai.
Stuart Marks

4
Demi keadilan, "jalan terbersih" dicuri dari jawaban @ Stuart.
Vadzim

70

API Java 8 stream tidak memiliki fitur untuk mendapatkan indeks elemen stream serta kemampuan untuk zip stream bersama. Ini sangat disayangkan, karena membuat aplikasi tertentu (seperti tantangan LINQ) lebih sulit daripada yang seharusnya.

Namun, sering ada penyelesaian masalah. Biasanya ini dapat dilakukan dengan "mengarahkan" aliran dengan rentang integer, dan mengambil keuntungan dari fakta bahwa elemen asli sering dalam array atau dalam koleksi yang dapat diakses oleh indeks. Misalnya, masalah Tantangan 2 dapat diselesaikan dengan cara ini:

String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};

List<String> nameList =
    IntStream.range(0, names.length)
        .filter(i -> names[i].length() <= i)
        .mapToObj(i -> names[i])
        .collect(toList());

Seperti yang saya sebutkan di atas, ini mengambil keuntungan dari fakta bahwa sumber data (array nama) secara langsung dapat diindeks. Jika tidak, teknik ini tidak akan berhasil.

Saya akan mengakui bahwa ini tidak memenuhi maksud Tantangan 2. Meskipun demikian, ini menyelesaikan masalah dengan cukup efektif.

EDIT

Contoh kode saya sebelumnya digunakan flatMapuntuk memadukan operasi filter dan pemetaan, tetapi ini rumit dan tidak memberikan keuntungan. Saya telah memperbarui contoh per komentar dari Holger.


7
Bagaimana dengan IntStream.range(0, names.length).filter(i->names[i].length()<=i).mapToObj(i->names[i])? Itu bekerja tanpa tinju ...
Holger

1
Hm, ya, mengapa saya pikir saya perlu menggunakannya flatMap?
Stuart Marks

2
Akhirnya meninjau kembali ini ... Saya mungkin digunakan flatMapkarena ini semacam sekering operasi penyaringan dan pemetaan menjadi satu operasi, tetapi ini benar-benar tidak memberikan keuntungan. Saya akan mengedit contohnya.
Stuart Marks

Stream.of (Array) akan membuat antarmuka streaming untuk sebuah array. Efektif membuatnya menjadi Stream.of( names ).filter( n -> n.length() <= 1).collect( Collectors.toList() );Kurang unboxing, dan alokasi memori lebih sedikit; karena kami tidak membuat aliran rentang lagi.
Code Eyez

44

Sejak jambu 21, Anda bisa menggunakannya

Streams.mapWithIndex()

Contoh (dari dokumen resmi ):

Streams.mapWithIndex(
    Stream.of("a", "b", "c"),
    (str, index) -> str + ":" + index)
) // will return Stream.of("a:0", "b:1", "c:2")

3
Juga, orang-orang Guava belum menerapkan untukEachWithIndex (mengambil konsumen daripada fungsi), tetapi ini masalah yang ditugaskan: github.com/google/guava/issues/2913 .
John Glassmyer

25

Saya telah menggunakan solusi berikut dalam proyek saya. Saya pikir ini lebih baik daripada menggunakan objek yang bisa berubah atau rentang integer.

import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
import java.util.stream.Collector.Characteristics;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static java.util.Objects.requireNonNull;


public class CollectionUtils {
    private CollectionUtils() { }

    /**
     * Converts an {@link java.util.Iterator} to {@link java.util.stream.Stream}.
     */
    public static <T> Stream<T> iterate(Iterator<? extends T> iterator) {
        int characteristics = Spliterator.ORDERED | Spliterator.IMMUTABLE;
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, characteristics), false);
    }

    /**
     * Zips the specified stream with its indices.
     */
    public static <T> Stream<Map.Entry<Integer, T>> zipWithIndex(Stream<? extends T> stream) {
        return iterate(new Iterator<Map.Entry<Integer, T>>() {
            private final Iterator<? extends T> streamIterator = stream.iterator();
            private int index = 0;

            @Override
            public boolean hasNext() {
                return streamIterator.hasNext();
            }

            @Override
            public Map.Entry<Integer, T> next() {
                return new AbstractMap.SimpleImmutableEntry<>(index++, streamIterator.next());
            }
        });
    }

    /**
     * Returns a stream consisting of the results of applying the given two-arguments function to the elements of this stream.
     * The first argument of the function is the element index and the second one - the element value. 
     */
    public static <T, R> Stream<R> mapWithIndex(Stream<? extends T> stream, BiFunction<Integer, ? super T, ? extends R> mapper) {
        return zipWithIndex(stream).map(entry -> mapper.apply(entry.getKey(), entry.getValue()));
    }

    public static void main(String[] args) {
        String[] names = {"Sam", "Pamela", "Dave", "Pascal", "Erik"};

        System.out.println("Test zipWithIndex");
        zipWithIndex(Arrays.stream(names)).forEach(entry -> System.out.println(entry));

        System.out.println();
        System.out.println("Test mapWithIndex");
        mapWithIndex(Arrays.stream(names), (Integer index, String name) -> index+"="+name).forEach((String s) -> System.out.println(s));
    }
}

+1 - mampu menerapkan fungsi yang "menyisipkan" elemen setiap indeks N menggunakan StreamSupport.stream()dan iterator khusus.
ach

13

Selain protonpack, SeOO jOOλ menyediakan fungsionalitas ini (dan oleh ekstensi library yang membangunnya seperti cyclops-react , saya adalah penulis perpustakaan ini).

Seq.seq(Stream.of(names)).zipWithIndex()
                         .filter( namesWithIndex -> namesWithIndex.v1.length() <= namesWithIndex.v2 + 1)
                         .toList();

Seq juga mendukung hanya Seq.of (nama) dan akan membangun JDK Stream di bawah selimut.

Persamaan reaksi sederhana akan terlihat seperti

 LazyFutureStream.of(names)
                 .zipWithIndex()
                 .filter( namesWithIndex -> namesWithIndex.v1.length() <= namesWithIndex.v2 + 1)
                 .toList();

Versi reaksi sederhana lebih disesuaikan untuk pemrosesan asinkron / bersamaan.


John saya melihat hari ini perpustakaan Anda, saya kagum sekaligus bingung.
GOXR3PLUS

12

Hanya untuk kelengkapan, inilah solusi yang melibatkan pustaka StreamEx saya :

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
EntryStream.of(names)
    .filterKeyValue((idx, str) -> str.length() <= idx+1)
    .values().toList();

Di sini kami membuat EntryStream<Integer, String>yang memperluas Stream<Entry<Integer, String>>dan menambahkan beberapa operasi tertentu seperti filterKeyValueatau values. Juga toList()shortcut digunakan.


kerja bagus; adakah jalan pintas untuk .forEach(entry -> {}) ?
Steve Oh

2
@SteveOh jika saya memahami Anda mempertanyakan benar, maka ya, Anda dapat menulis .forKeyValue((key, value) -> {}).
Tagir Valeev

8

Saya menemukan solusi di sini ketika Stream dibuat dari daftar atau array (dan Anda tahu ukurannya). Tetapi bagaimana jika Stream dengan ukuran yang tidak diketahui? Dalam hal ini coba varian ini:

public class WithIndex<T> {
    private int index;
    private T value;

    WithIndex(int index, T value) {
        this.index = index;
        this.value = value;
    }

    public int index() {
        return index;
    }

    public T value() {
        return value;
    }

    @Override
    public String toString() {
        return value + "(" + index + ")";
    }

    public static <T> Function<T, WithIndex<T>> indexed() {
        return new Function<T, WithIndex<T>>() {
            int index = 0;
            @Override
            public WithIndex<T> apply(T t) {
                return new WithIndex<>(index++, t);
            }
        };
    }
}

Pemakaian:

public static void main(String[] args) {
    Stream<String> stream = Stream.of("a", "b", "c", "d", "e");
    stream.map(WithIndex.indexed()).forEachOrdered(e -> {
        System.out.println(e.index() + " -> " + e.value());
    });
}

6

Dengan Daftar Anda dapat mencoba

List<String> strings = new ArrayList<>(Arrays.asList("First", "Second", "Third", "Fourth", "Fifth")); // An example list of Strings
strings.stream() // Turn the list into a Stream
    .collect(HashMap::new, (h, o) -> h.put(h.size(), o), (h, o) -> {}) // Create a map of the index to the object
        .forEach((i, o) -> { // Now we can use a BiConsumer forEach!
            System.out.println(String.format("%d => %s", i, o));
        });

Keluaran:

0 => First
1 => Second
2 => Third
3 => Fourth
4 => Fifth

1
Sebenarnya ide yang bagus, tetapi string :: indexOf mungkin agak mahal. Saran saya adalah menggunakan: .collect (HashMap :: new, (h, s) -> h.put (h.size (), s), (h, s) -> {}) . Anda cukup menggunakan metode size () untuk membuat indeks.
gil.fernandes

@ gil.fernandes Terima kasih atas sarannya. Saya akan mengedit.
V0idst4r

3

Tidak ada cara untuk beralih Streamsementara memiliki akses ke indeks karena Streamtidak seperti apa pun Collection. A Streamhanyalah jalur pipa untuk membawa data dari satu tempat ke tempat lain, sebagaimana dinyatakan dalam dokumentasi :

Tidak ada penyimpanan. Aliran bukanlah struktur data yang menyimpan elemen; sebaliknya, mereka membawa nilai-nilai dari sumber (yang bisa berupa struktur data, generator, saluran IO, dll) melalui pipa operasi komputasi.

Tentu saja, saat Anda mengisyaratkan pertanyaan Anda, Anda selalu dapat mengonversikannya Stream<V>menjadi Collection<V>, seperti a List<V>, di mana Anda akan memiliki akses ke indeks.


2
Ini tersedia dalam bahasa / alat lain. Ini hanyalah nilai tambahan yang diteruskan ke fungsi peta
Lee Campbell

Tautan Anda ke dokumentasi rusak.
Usman Mutawakil

3

Dengan https://github.com/poetix/protonpack Anda dapat melakukannya zip:

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};

List<String> nameList;
Stream<Integer> indices = IntStream.range(0, names.length).boxed(); 

nameList = StreamUtils.zip(indices, stream(names),SimpleEntry::new)
        .filter(e -> e.getValue().length() <= e.getKey()).map(Entry::getValue).collect(toList());                   

System.out.println(nameList);

3

Jika Anda tidak keberatan menggunakan perpustakaan pihak ketiga, Eclipse Collections telah zipWithIndexdan forEachWithIndextersedia untuk digunakan di banyak jenis. Berikut adalah serangkaian solusi untuk tantangan ini baik untuk tipe JDK dan tipe Eclipse Collections zipWithIndex.

String[] names = { "Sam", "Pamela", "Dave", "Pascal", "Erik" };
ImmutableList<String> expected = Lists.immutable.with("Erik");
Predicate<Pair<String, Integer>> predicate =
    pair -> pair.getOne().length() <= pair.getTwo() + 1;

// JDK Types
List<String> strings1 = ArrayIterate.zipWithIndex(names)
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings1);

List<String> list = Arrays.asList(names);
List<String> strings2 = ListAdapter.adapt(list)
    .zipWithIndex()
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings2);

// Eclipse Collections types
MutableList<String> mutableNames = Lists.mutable.with(names);
MutableList<String> strings3 = mutableNames.zipWithIndex()
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings3);

ImmutableList<String> immutableNames = Lists.immutable.with(names);
ImmutableList<String> strings4 = immutableNames.zipWithIndex()
    .collectIf(predicate, Pair::getOne);
Assert.assertEquals(expected, strings4);

MutableList<String> strings5 = mutableNames.asLazy()
    .zipWithIndex()
    .collectIf(predicate, Pair::getOne, Lists.mutable.empty());
Assert.assertEquals(expected, strings5);

Inilah solusi menggunakan forEachWithIndexsebagai gantinya.

MutableList<String> mutableNames =
    Lists.mutable.with("Sam", "Pamela", "Dave", "Pascal", "Erik");
ImmutableList<String> expected = Lists.immutable.with("Erik");

List<String> actual = Lists.mutable.empty();
mutableNames.forEachWithIndex((name, index) -> {
        if (name.length() <= index + 1)
            actual.add(name);
    });
Assert.assertEquals(expected, actual);

Jika Anda mengubah lambda ke kelas batin anonim di atas, maka semua contoh kode ini akan berfungsi di Java 5 - 7 juga.

Catatan: Saya pengendara untuk Eclipse Collections


2

Jika Anda menggunakan Vavr (sebelumnya dikenal sebagai Javaslang), Anda dapat memanfaatkan metode khusus:

Stream.of("A", "B", "C")
  .zipWithIndex();

Jika kami mencetak konten, kami akan melihat sesuatu yang menarik:

Stream((A, 0), ?)

Ini karena Streamsmalas dan kami tidak memiliki petunjuk tentang item berikutnya dalam arus.


1

Jika Anda mencoba untuk mendapatkan indeks berdasarkan predikat, coba ini:

Jika Anda hanya peduli dengan indeks pertama:

OptionalInt index = IntStream.range(0, list.size())
    .filter(i -> list.get(i) == 3)
    .findFirst();

Atau jika Anda ingin menemukan beberapa indeks:

IntStream.range(0, list.size())
   .filter(i -> list.get(i) == 3)
   .collect(Collectors.toList());

Tambahkan .orElse(-1);jika Anda ingin mengembalikan nilai jika tidak menemukannya.


1

Berikut ini kode dari AbacusUtil

Stream.of(names).indexed()
      .filter(e -> e.value().length() <= e.index())
      .map(Indexed::value).toList();

Pengungkapan : Saya pengembang AbacusUtil.


1

Anda dapat menggunakan IntStream.iterate()untuk mendapatkan indeks:

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
List<String> nameList = IntStream.iterate(0, i -> i < names.length, i -> i + 1)
        .filter(i -> names[i].length() <= i)
        .mapToObj(i -> names[i])
        .collect(Collectors.toList());

Ini hanya berfungsi untuk Java 9 ke atas di Java 8 Anda dapat menggunakan ini:

String[] names = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
List<String> nameList = IntStream.iterate(0, i -> i + 1)
        .limit(names.length)
        .filter(i -> names[i].length() <= i)
        .mapToObj(i -> names[i])
        .collect(Collectors.toList());

0

Anda dapat membuat kelas dalam statis untuk merangkum pengindeks seperti yang perlu saya lakukan dalam contoh di bawah ini:

static class Indexer {
    int i = 0;
}

public static String getRegex() {
    EnumSet<MeasureUnit> range = EnumSet.allOf(MeasureUnit.class);
    StringBuilder sb = new StringBuilder();
    Indexer indexer = new Indexer();
    range.stream().forEach(
            measureUnit -> {
                sb.append(measureUnit.acronym);
                if (indexer.i < range.size() - 1)
                    sb.append("|");

                indexer.i++;
            }
    );
    return sb.toString();
}

0

Pertanyaan ini ( Cara Streaming untuk mendapatkan indeks boolean pencocokan elemen pertama ) telah menandai pertanyaan saat ini sebagai duplikat, jadi saya tidak bisa menjawabnya di sana; Saya menjawabnya di sini.

Berikut ini adalah solusi umum untuk mendapatkan indeks yang cocok yang tidak memerlukan perpustakaan eksternal.

Jika Anda punya daftar.

public static <T> int indexOf(List<T> items, Predicate<T> matches) {
        return IntStream.range(0, items.size())
                .filter(index -> matches.test(items.get(index)))
                .findFirst().orElse(-1);
}

Dan menyebutnya seperti ini:

int index = indexOf(myList, item->item.getId()==100);

Dan jika menggunakan koleksi, coba yang ini.

   public static <T> int indexOf(Collection<T> items, Predicate<T> matches) {
        int index = -1;
        Iterator<T> it = items.iterator();
        while (it.hasNext()) {
            index++;
            if (matches.test(it.next())) {
                return index;
            }
        }
        return -1;
    }

0

Salah satu cara yang mungkin adalah dengan mengindeks setiap elemen pada aliran:

AtomicInteger index = new AtomicInteger();
Stream.of(names)
  .map(e->new Object() { String n=e; public i=index.getAndIncrement(); })
  .filter(o->o.n.length()<=o.i) // or do whatever you want with pairs...
  .forEach(o->System.out.println("idx:"+o.i+" nam:"+o.n));

Menggunakan kelas anonim di sepanjang aliran tidak digunakan dengan baik sementara sangat berguna.


0

Anda tidak perlu map harus
itu adalah lambda terdekat dengan contoh LINQ:

int[] idx = new int[] { 0 };
Stream.of( names ).filter( name -> name.length() <= idx[0]++ ).collect( Collectors.toList() );

0
String[] namesArray = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
String completeString
         =  IntStream.range(0,namesArray.length)
           .mapToObj(i -> namesArray[i]) // Converting each array element into Object
           .map(String::valueOf) // Converting object to String again
           .collect(Collectors.joining(",")); // getting a Concat String of all values
        System.out.println(completeString);

OUTPUT: Sam, Pamela, Dave, Pascal, Erik

String[] namesArray = {"Sam","Pamela", "Dave", "Pascal", "Erik"};

IntStream.range(0,namesArray.length)
               .mapToObj(i -> namesArray[i]) // Converting each array element into Object
               .map(String::valueOf) // Converting object to String again
               .forEach(s -> {
                //You can do various operation on each element here
                System.out.println(s);
               }); // getting a Concat String of all 

Untuk Mengumpulkan dalam Daftar:

String[] namesArray = {"Sam","Pamela", "Dave", "Pascal", "Erik"};
 List<String> namesList
                =  IntStream.range(0,namesArray.length)
                .mapToObj(i -> namesArray[i]) // Converting each array element into Object
                .map(String::valueOf) // Converting object to String again
                .collect(Collectors.toList()); // collecting elements in List
        System.out.println(listWithIndex);

Solusi dari pertanyaan di atas diharapkan menjadi Listsatu elemen yang mengandung Erik .
Kaplan

Saya telah menambahkan contoh untuk dikumpulkan dalam Daftar juga.
Arpan Saini

0

Seperti yang dikatakan jean-baptiste-yunès, jika aliran Anda didasarkan pada Daftar java kemudian menggunakan AtomicInteger dan metode incrementementAndGet adalah solusi yang sangat baik untuk masalah dan integer yang dikembalikan sesuai dengan indeks dalam Daftar asli selama Anda jangan gunakan aliran paralel.


0

Jika Anda memerlukan indeks di forEach maka ini menyediakan cara.

  public class IndexedValue {

    private final int    index;
    private final Object value;

    public IndexedValue(final int index, final Object value) { 
        this.index = index;
        this.value = value;
    }

    public int getIndex() {
        return index;
    }

    public Object getValue() {
        return value;
    }
}

Kemudian gunakan sebagai berikut.

@Test
public void withIndex() {
    final List<String> list = Arrays.asList("a", "b");
    IntStream.range(0, list.size())
             .mapToObj(index -> new IndexedValue(index, list.get(index)))
             .forEach(indexValue -> {
                 System.out.println(String.format("%d, %s",
                                                  indexValue.getIndex(),
                                                  indexValue.getValue().toString()));
             });
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.