Dapatkan elemen terakhir Stream / List dalam satu baris

118

Bagaimana saya bisa mendapatkan elemen terakhir dari aliran atau daftar dalam kode berikut?

Dimana data.careasa List<CArea>:

CArea first = data.careas.stream()
                  .filter(c -> c.bbox.orientationHorizontal).findFirst().get();

CArea last = data.careas.stream()
                 .filter(c -> c.bbox.orientationHorizontal)
                 .collect(Collectors.toList()).; //how to?

Seperti yang Anda lihat, mendapatkan elemen pertama, dengan pasti filter, tidaklah sulit.

Namun mendapatkan elemen terakhir dalam satu baris adalah rasa sakit yang nyata:

  • Sepertinya saya tidak bisa mendapatkannya langsung dari a Stream. (Ini hanya masuk akal untuk aliran yang terbatas)
  • Tampaknya Anda juga tidak bisa mendapatkan hal-hal seperti first()dan last()dari Listantarmuka, yang sangat merepotkan.

Saya tidak melihat argumen untuk tidak menyediakan metode first()dan last()di Listantarmuka, karena elemen di sana, diurutkan, dan terlebih lagi ukurannya diketahui.

Tetapi sesuai jawaban aslinya: Bagaimana cara mendapatkan elemen terakhir dari sebuah finite Stream?

Secara pribadi, ini yang paling dekat yang bisa saya dapatkan:

int lastIndex = data.careas.stream()
        .filter(c -> c.bbox.orientationHorizontal)
        .mapToInt(c -> data.careas.indexOf(c)).max().getAsInt();
CArea last = data.careas.get(lastIndex);

Namun itu melibatkan, menggunakan indexOfdi setiap elemen, yang biasanya tidak Anda inginkan karena dapat mengganggu kinerja.

skiwi
sumber
10
Guava menyediakan Iterables.getLastyang membutuhkan Iterable tetapi dioptimalkan untuk digunakan List. Hewan peliharaan yang mengesalkan adalah dia tidak punya getFirst. The StreamAPI pada umumnya adalah mengerikan anal, menghilangkan banyak metode kenyamanan. C #'s LINQ, dengan konstrast, dengan senang hati menyediakan .Last()dan bahkan .Last(Func<T,Boolean> predicate), meskipun mendukung Enumerables yang tak terbatas juga.
Aleksandr Dubinsky
@AleksandrDubinsky memberi suara positif, tetapi satu catatan untuk pembaca. StreamAPI tidak sepenuhnya dapat dibandingkan LINQkarena keduanya dilakukan dalam paradigma yang sangat berbeda. Itu tidak lebih buruk atau lebih baik itu hanya berbeda. Dan pasti beberapa metode tidak ada bukan karena oracle devs tidak kompeten atau jahat :)
fasth
1
Untuk one-liner sejati, utas ini mungkin berguna.
kuantum

Jawaban:

185

Anda bisa mendapatkan elemen terakhir dengan metode Stream :: reduce . Daftar berikut berisi contoh minimal untuk kasus umum:

Stream<T> stream = ...; // sequential or parallel stream
Optional<T> last = stream.reduce((first, second) -> second);

Implementasi ini berfungsi untuk semua aliran yang diurutkan (termasuk aliran yang dibuat dari Daftar ). Untuk aliran yang tidak berurutan , untuk alasan yang jelas tidak ditentukan elemen mana yang akan dikembalikan.

Implementasinya berfungsi untuk aliran sekuensial dan paralel . Sekilas mungkin hal itu mengejutkan, dan sayangnya dokumentasinya tidak menyatakannya secara eksplisit. Namun, ini adalah fitur aliran yang penting, dan saya mencoba menjelaskannya:

  • Javadoc untuk metode Stream :: reduce menyatakan bahwa itu " tidak dibatasi untuk dieksekusi secara berurutan " .
  • Javadoc juga mensyaratkan bahwa "fungsi akumulator harus merupakan fungsi asosiatif , tidak mengganggu , tanpa status untuk menggabungkan dua nilai" , yang jelas merupakan kasus untuk ekspresi lambda(first, second) -> second .
  • Javadoc untuk operasi reduksi menyatakan: "Kelas aliran memiliki beberapa bentuk operasi reduksi umum, yang disebut reduksi () dan kumpulkan () [..]" dan "operasi pengurangan yang dibangun dengan benar secara inheren dapat diparalelkan , selama fungsinya ) digunakan untuk memproses elemen bersifat asosiatif dan tanpa kewarganegaraan . "

Dokumentasi untuk Kolektor yang terkait erat bahkan lebih eksplisit: "Untuk memastikan bahwa eksekusi sekuensial dan paralel menghasilkan hasil yang setara , fungsi kolektor harus memenuhi batasan identitas dan asosiatif ."


Kembali ke pertanyaan awal: Kode berikut menyimpan referensi ke elemen terakhir dalam variabel lastdan melontarkan pengecualian jika aliran kosong. Kompleksitasnya linier sepanjang aliran.

CArea last = data.careas
                 .stream()
                 .filter(c -> c.bbox.orientationHorizontal)
                 .reduce((first, second) -> second).get();
nosid
sumber
Bagus, terima kasih! Apakah Anda tahu apakah mungkin untuk menghilangkan nama (mungkin dengan menggunakan _atau serupa) dalam kasus di mana Anda tidak memerlukan parameter? Jadi akan menjadi: .reduce((_, current) -> current)jika hanya itu sintaks yang valid.
skiwi
2
@skiwi Anda dapat menggunakan nama variabel legal apa pun, misalnya: .reduce(($, current) -> current)atau .reduce((__, current) -> current)(garis bawah ganda).
assylias
2
Secara teknis, ini mungkin tidak berfungsi untuk aliran apa pun. Dokumentasi yang Anda tunjuk, dan juga untuk Stream.reduce(BinaryOperator<T>)tidak menyebutkan jika reducemematuhi perintah pertemuan, dan operasi terminal bebas untuk mengabaikan perintah pertemuan bahkan jika aliran dipesan. Selain itu, kata "komutatif" tidak muncul di Stream javadocs, jadi ketiadaannya tidak memberi tahu kita banyak.
Aleksandr Dubinsky
2
@AleksandrDubinsky: Sebenarnya, dokumentasi tidak menyebutkan komutatif , karena tidak relevan untuk operasi pengurangan . Bagian yang penting adalah: "[..] Operasi pengurangan yang dibangun dengan benar secara inheren dapat diparalelkan, selama fungsi yang digunakan untuk memproses elemen bersifat asosiatif [..]."
nosid
2
@ Aleksandr Dubinsky: tentu saja, ini bukan "pertanyaan teoretis tentang spesifikasi". Itu membuat perbedaan antara reduce((a,b)->b)menjadi solusi yang tepat untuk mendapatkan elemen terakhir (dari aliran yang teratur, tentu saja) atau tidak. Pernyataan Brian Goetz membuat satu poin, lebih lanjut dokumentasi API menyatakan bahwa reduce("", String::concat)ini adalah solusi yang tidak efisien tetapi benar untuk penggabungan string, yang menyiratkan pemeliharaan urutan pertemuan. Maksudnya adalah terkenal, dokumentasi harus menyusul.
Holger
42

Jika Anda memiliki Koleksi (atau lebih umum Iterable) Anda dapat menggunakan Google Guava

Iterables.getLast(myIterable)

sebagai oneliner berguna.

Peti
sumber
1
Dan Anda dapat dengan mudah mengubah aliran menjadi aliran yang dapat diubah:Iterables.getLast(() -> data.careas.stream().filter(c -> c.bbox.orientationHorizontal).iterator())
shmosel
10

Satu liner (tidak perlu streaming;):

Object lastElement = list.get(list.size()-1);
nimo23
sumber
30
Jika daftar kosong, kode ini akan dibuang ArrayIndexOutOfBoundsException.
Naga
8

Jambu biji memiliki metode khusus untuk kasus ini:

Stream<T> stream = ...;
Optional<T> lastItem = Streams.findLast(stream);

Ini setara dengan stream.reduce((a, b) -> b)tetapi pencipta mengklaim itu memiliki kinerja yang jauh lebih baik.

Dari dokumentasi :

Runtime metode ini akan berada di antara O (log n) dan O (n), berperforma lebih baik pada streaming yang dapat dipisahkan secara efisien.

Perlu disebutkan bahwa jika aliran tidak diurutkan, metode ini akan berfungsi findAny().

k13i
sumber
1
@ZhekaKozlov semacam ... Holger menunjukkan beberapa kekurangannya di sini
Eugene
0

Jika Anda perlu mendapatkan jumlah N elemen terakhir. Penutupan bisa digunakan. Kode di bawah ini mempertahankan antrian eksternal dengan ukuran tetap sampai, aliran mencapai akhir.

    final Queue<Integer> queue = new LinkedList<>();
    final int N=5;
    list.stream().peek((z) -> {
        queue.offer(z);
        if (queue.size() > N)
            queue.poll();
    }).count();

Pilihan lainnya adalah menggunakan operasi pengurangan menggunakan identitas sebagai Antrian.

    final int lastN=3;
    Queue<Integer> reduce1 = list.stream()
    .reduce( 
        (Queue<Integer>)new LinkedList<Integer>(), 
        (m, n) -> {
            m.offer(n);
            if (m.size() > lastN)
               m.poll();
            return m;
    }, (m, n) -> m);

    System.out.println("reduce1 = " + reduce1);
Himanshu Ahire
sumber
-1

Anda juga dapat menggunakan fungsi skip () seperti di bawah ini ...

long count = data.careas.count();
CArea last = data.careas.stream().skip(count - 1).findFirst().get();

ini sangat mudah digunakan.

Parag Vaidya
sumber
Catatan: Anda tidak boleh mengandalkan "lewati" aliran saat menangani koleksi besar (jutaan entri), karena "lewati" diterapkan dengan mengulang semua elemen hingga angka ke-N tercapai. Sudah mencobanya. Sangat kecewa dengan kinerjanya, dibandingkan dengan operasi get-by-index yang sederhana.
java.is.for.desktop
1
juga jika daftar kosong, itu akan melemparArrayIndexOutOfBoundsException
Jindra Vysocký