Filter Java Stream ke 1 dan hanya 1 elemen

230

Saya mencoba menggunakan Java 8 Streams untuk menemukan elemen dalam aLinkedList . Namun saya ingin menjamin bahwa hanya ada satu dan hanya satu yang cocok dengan kriteria filter.

Ambil kode ini:

public static void main(String[] args) {

    LinkedList<User> users = new LinkedList<>();
    users.add(new User(1, "User1"));
    users.add(new User(2, "User2"));
    users.add(new User(3, "User3"));

    User match = users.stream().filter((user) -> user.getId() == 1).findAny().get();
    System.out.println(match.toString());
}

static class User {

    @Override
    public String toString() {
        return id + " - " + username;
    }

    int id;
    String username;

    public User() {
    }

    public User(int id, String username) {
        this.id = id;
        this.username = username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public int getId() {
        return id;
    }
}

Kode ini menemukan Userberdasarkan ID mereka. Tetapi tidak ada jaminan berapa banyakUser yang cocok dengan filter.

Mengubah garis filter ke:

User match = users.stream().filter((user) -> user.getId() < 0).findAny().get();

Akan melempar NoSuchElementException (bagus!)

Saya ingin melontarkan kesalahan jika ada beberapa pertandingan. Apakah ada cara untuk melakukan ini?

ryvantage
sumber
count()adalah operasi terminal sehingga Anda tidak dapat melakukan itu. Aliran tidak dapat digunakan setelah.
Alexis C.
Ok terima kasih @ZouZou. Saya tidak sepenuhnya yakin apa yang dilakukan metode itu. Kenapa tidak ada Stream::size?
ryvantage
7
@ryvantage Karena stream hanya dapat digunakan sekali: menghitung ukurannya berarti "iterating" di atasnya dan setelah itu Anda tidak dapat menggunakan stream lagi.
assylias
3
Wow. Satu komentar itu membantu saya memahami Streamjauh lebih banyak daripada yang saya lakukan sebelumnya ...
ryvantage
2
Ini adalah ketika Anda menyadari bahwa Anda perlu menggunakan LinkedHashSet(dengan asumsi Anda ingin agar penyisipan dipertahankan) atau HashSetsemuanya. Jika koleksi Anda hanya digunakan untuk menemukan id pengguna tunggal, lalu mengapa Anda mengumpulkan semua item lainnya? Jika ada potensi bahwa Anda akan selalu perlu menemukan beberapa id pengguna yang juga perlu unik, lalu mengapa menggunakan daftar dan bukan set? Anda memprogram mundur. Gunakan koleksi yang tepat untuk pekerjaan itu dan selamatkan diri Anda dari sakit kepala ini
smac89

Jawaban:

192

Buat custom Collector

public static <T> Collector<T, ?, T> toSingleton() {
    return Collectors.collectingAndThen(
            Collectors.toList(),
            list -> {
                if (list.size() != 1) {
                    throw new IllegalStateException();
                }
                return list.get(0);
            }
    );
}

Kami menggunakan Collectors.collectingAndThenuntuk membangun yang diinginkan Collectoroleh

  1. Mengumpulkan benda-benda kita ListdenganCollectors.toList() kolektor.
  2. Menerapkan finisher tambahan di akhir, yang mengembalikan elemen tunggal - atau melempar IllegalStateExceptionif list.size != 1.

Digunakan sebagai:

User resultUser = users.stream()
        .filter(user -> user.getId() > 0)
        .collect(toSingleton());

Anda kemudian dapat menyesuaikan ini Collector sebanyak yang Anda inginkan, misalnya memberikan pengecualian sebagai argumen dalam konstruktor, menyesuaikannya untuk memungkinkan dua nilai, dan banyak lagi.

Alternatif - solusi yang bisa dibilang kurang elegan:

Anda dapat menggunakan 'solusi' yang melibatkan peek()danAtomicInteger , tetapi sebenarnya Anda seharusnya tidak menggunakannya.

Apa yang bisa Anda lakukan hanya mengumpulkan itu dalam List, seperti ini:

LinkedList<User> users = new LinkedList<>();
users.add(new User(1, "User1"));
users.add(new User(2, "User2"));
users.add(new User(3, "User3"));
List<User> resultUserList = users.stream()
        .filter(user -> user.getId() == 1)
        .collect(Collectors.toList());
if (resultUserList.size() != 1) {
    throw new IllegalStateException();
}
User resultUser = resultUserList.get(0);
skiwi
sumber
24
Guava's Iterables.getOnlyElementakan mempersingkat solusi ini dan memberikan pesan kesalahan yang lebih baik. Sama seperti tip untuk sesama pembaca yang sudah menggunakan Google Guava.
Tim Büthe
2
saya membungkus ide ini menjadi sebuah kelas - gist.github.com/denov/a7eac36a3cda041f8afeabcef09d16fc
denov
1
@LonelyNeuron Tolong jangan edit kode saya. Itu menempatkan saya dalam situasi di mana saya perlu memvalidasi seluruh jawaban saya, yang telah saya tulis empat tahun lalu, dan saya tidak punya waktu untuk itu sekarang.
skiwi
2
@skiwi: Suntingan Lonely bermanfaat dan benar, jadi saya instal ulang setelah ditinjau. Orang yang mengunjungi jawaban ini hari ini tidak peduli bagaimana Anda sampai pada jawaban, mereka tidak perlu melihat versi lama dan versi baru dan bagian yang Diperbarui . Itu membuat jawaban Anda lebih membingungkan dan kurang membantu. Jauh lebih baik untuk menempatkan posting dalam keadaan akhir , dan jika orang ingin melihat bagaimana semuanya dimainkan mereka dapat melihat riwayat posting.
Martijn Pieters
1
@skiwi: Kode dalam jawaban adalah mutlak apa yang Anda tulis. Yang dilakukan editor hanyalah membersihkan posting Anda, hanya menghapus versi sebelumnya dari singletonCollector()definisi yang sudah usang oleh versi yang tetap ada di posting, dan mengganti namanya menjadi toSingleton(). Keahlian streaming Java saya agak berkarat, tetapi penggantian nama terlihat membantu saya. Meninjau perubahan ini butuh waktu 2 menit, puncak. Jika Anda tidak punya waktu untuk meninjau hasil edit, dapatkah saya menyarankan Anda meminta orang lain untuk melakukan ini di masa depan, mungkin di ruang obrolan Java ?
Martijn Pieters
118

Demi kelengkapan, berikut adalah 'one-liner' yang sesuai dengan jawaban istimewa @ prunge:

User user1 = users.stream()
        .filter(user -> user.getId() == 1)
        .reduce((a, b) -> {
            throw new IllegalStateException("Multiple elements: " + a + ", " + b);
        })
        .get();

Ini mendapatkan elemen pencocokan tunggal dari aliran, melempar

  • NoSuchElementException dalam kasus aliran kosong, atau
  • IllegalStateException dalam hal aliran mengandung lebih dari satu elemen yang cocok.

Variasi dari pendekatan ini menghindari melemparkan pengecualian lebih awal dan sebagai gantinya menyatakan hasilnya sebagai Optionalmengandung elemen tunggal, atau tidak ada (kosong) jika ada nol atau beberapa elemen:

Optional<User> user1 = users.stream()
        .filter(user -> user.getId() == 1)
        .collect(Collectors.reducing((a, b) -> null));
glts
sumber
3
Saya suka pendekatan awal dalam jawaban ini. Untuk keperluan kustomisasi, dimungkinkan untuk mengonversi terakhir get()keorElseThrow()
arin
1
Saya suka singkatnya yang satu ini, dan fakta bahwa ia menghindari membuat contoh instance yang tidak perlu setiap kali dipanggil.
LordOfThePigs
83

Jawaban lain yang melibatkan penulisan kebiasaan Collectormungkin lebih efisien (seperti jawaban Louis Wasserman , +1), tetapi jika Anda ingin singkatnya, saya sarankan yang berikut:

List<User> result = users.stream()
    .filter(user -> user.getId() == 1)
    .limit(2)
    .collect(Collectors.toList());

Kemudian verifikasi ukuran daftar hasil.

if (result.size() != 1) {
  throw new IllegalStateException("Expected exactly one user but got " + result);
User user = result.get(0);
}
Stuart Marks
sumber
5
Apa gunanya limit(2)solusi ini? Apa bedanya jika daftar yang dihasilkan adalah 2 atau 100? Jika lebih besar dari 1.
ryvantage
18
Segera berhenti jika menemukan kecocokan kedua. Inilah yang dilakukan oleh semua kolektor mewah, hanya menggunakan lebih banyak kode. :-)
Stuart Marks
10
Bagaimana kalau menambahkanCollectors.collectingAndThen(toList(), l -> { if (l.size() == 1) return l.get(0); throw new RuntimeException(); })
Lukas Eder
1
Javadoc mengatakan param batas tentang ini: maxSize: the number of elements the stream should be limited to. Jadi, tidak harus itu .limit(1)bukan .limit(2)?
alexbt
5
@alexbt Pernyataan masalah adalah untuk memastikan bahwa ada tepat satu (tidak lebih, tidak sedikit) elemen yang cocok. Setelah kode saya, seseorang dapat menguji result.size()untuk memastikan sama dengan 1. Jika 2, maka ada lebih dari satu kecocokan, jadi ini merupakan kesalahan. Jika bukan kode yang cocok limit(1), lebih dari satu kecocokan akan menghasilkan satu elemen, yang tidak dapat dibedakan dari hanya ada satu kecocokan. Ini akan melewatkan kasus kesalahan yang dikhawatirkan OP.
Stuart Marks
67

Jambu memberikan MoreCollectors.onlyElement()yang melakukan hal yang benar di sini. Tetapi jika Anda harus melakukannya sendiri, Anda bisa melakukannya sendiri Collector:

<E> Collector<E, ?, Optional<E>> getOnly() {
  return Collector.of(
    AtomicReference::new,
    (ref, e) -> {
      if (!ref.compareAndSet(null, e)) {
         throw new IllegalArgumentException("Multiple values");
      }
    },
    (ref1, ref2) -> {
      if (ref1.get() == null) {
        return ref2;
      } else if (ref2.get() != null) {
        throw new IllegalArgumentException("Multiple values");
      } else {
        return ref1;
      }
    },
    ref -> Optional.ofNullable(ref.get()),
    Collector.Characteristics.UNORDERED);
}

... atau menggunakan Holdertipe Anda sendiri, bukan AtomicReference. Anda dapat menggunakannya kembali Collectorsebanyak yang Anda suka.

Louis Wasserman
sumber
singletonCollector @ skiwi lebih kecil dan lebih mudah diikuti daripada ini, itu sebabnya saya memberinya cek. Tetapi baik untuk melihat konsensus dalam jawabannya: kebiasaan Collectoradalah cara untuk pergi.
ryvantage
1
Cukup adil. Saya terutama bertujuan untuk kecepatan, bukan keringkasan.
Louis Wasserman
1
Ya? Mengapa milikmu lebih cepat?
ryvantage
3
Sebagian besar karena mengalokasikan semua-up Listlebih mahal daripada satu referensi yang bisa berubah.
Louis Wasserman
1
@LouisWasserman, kalimat pembaruan terakhir tentang MoreCollectors.onlyElement()seharusnya benar-benar menjadi yang pertama (dan mungkin satu-satunya :))
Piotr Findeisen
46

Gunakan Guava's MoreCollectors.onlyElement()( JavaDoc ).

Itu melakukan apa yang Anda inginkan dan melempar IllegalArgumentExceptionjika aliran terdiri dari dua atau lebih elemen, dan NoSuchElementExceptionjika aliran kosong.

Pemakaian:

import static com.google.common.collect.MoreCollectors.onlyElement;

User match =
    users.stream().filter((user) -> user.getId() < 0).collect(onlyElement());
trevorade
sumber
2
Catatan untuk pengguna lain: MoreCollectorsadalah bagian dari versi yang belum dirilis (pada 2016-12) versi yang belum dirilis 21.
qerub
2
Jawaban ini harus lebih tinggi.
Emdadul Sawon
31

Operasi "escape hatch" yang memungkinkan Anda melakukan hal-hal aneh yang tidak didukung oleh stream adalah meminta Iterator:

Iterator<T> it = users.stream().filter((user) -> user.getId() < 0).iterator();
if (!it.hasNext()) 
    throw new NoSuchElementException();
else {
    result = it.next();
    if (it.hasNext())
        throw new TooManyElementsException();
}

Jambu biji memiliki metode kenyamanan untuk mengambil Iteratordan mendapatkan satu-satunya elemen, melempar jika ada nol atau beberapa elemen, yang bisa menggantikan garis n-1 bawah di sini.

Brian Goetz
sumber
4
Metode Guava: Iterators.getOnlyElement (Iterator <T> iterator).
anre
23

Memperbarui

Saran bagus dalam komentar dari @ Holger:

Optional<User> match = users.stream()
              .filter((user) -> user.getId() > 1)
              .reduce((u, v) -> { throw new IllegalStateException("More than one ID found") });

Jawaban asli

Pengecualian dilemparkan oleh Optional#get, tetapi jika Anda memiliki lebih dari satu elemen yang tidak akan membantu. Anda dapat mengumpulkan pengguna dalam koleksi yang hanya menerima satu item, misalnya:

User match = users.stream().filter((user) -> user.getId() > 1)
                  .collect(toCollection(() -> new ArrayBlockingQueue<User>(1)))
                  .poll();

yang melempar java.lang.IllegalStateException: Queue full, tapi itu terasa terlalu gila.

Atau Anda dapat menggunakan pengurangan yang dikombinasikan dengan opsional:

User match = Optional.ofNullable(users.stream().filter((user) -> user.getId() > 1)
                .reduce(null, (u, v) -> {
                    if (u != null && v != null)
                        throw new IllegalStateException("More than one ID found");
                    else return u == null ? v : u;
                })).get();

Pengurangan pada dasarnya mengembalikan:

  • null jika tidak ada pengguna yang ditemukan
  • pengguna jika hanya satu yang ditemukan
  • melempar pengecualian jika lebih dari satu ditemukan

Hasilnya kemudian dibungkus dalam opsional.

Tetapi solusi paling sederhana mungkin hanya dengan mengumpulkan ke koleksi, periksa bahwa ukurannya adalah 1 dan dapatkan satu-satunya elemen.

assylias
sumber
1
Saya akan menambahkan elemen identitas ( null) untuk mencegah penggunaan get(). Sayangnya Anda reducetidak bekerja seperti yang Anda pikirkan, pertimbangkan Streamyang memiliki nullelemen di dalamnya, mungkin Anda berpikir bahwa Anda menutupinya, tapi saya bisa [User#1, null, User#2, null, User#3], sekarang tidak akan membuang pengecualian saya pikir, kecuali saya salah di sini.
skiwi
2
@Skiwi jika ada elemen null, filter akan melempar NPE terlebih dahulu.
assylias
2
Karena Anda tahu bahwa sungai tidak bisa lewat nullke fungsi pengurangan, menghapus nilai identitas argumen akan membuat seluruh berurusan dengan nulldalam fungsi usang: reduce( (u,v) -> { throw new IllegalStateException("More than one ID found"); } )melakukan pekerjaan dan bahkan lebih baik, itu sudah mengembalikan Optional, eliding keharusan untuk memanggil Optional.ofNullablepada hasil.
Holger
15

Alternatifnya adalah menggunakan reduksi: (contoh ini menggunakan string tetapi bisa dengan mudah diterapkan ke semua jenis objek termasuk User)

List<String> list = ImmutableList.of("one", "two", "three", "four", "five", "two");
String match = list.stream().filter("two"::equals).reduce(thereCanBeOnlyOne()).get();
//throws NoSuchElementException if there are no matching elements - "zero"
//throws RuntimeException if duplicates are found - "two"
//otherwise returns the match - "one"
...

//Reduction operator that throws RuntimeException if there are duplicates
private static <T> BinaryOperator<T> thereCanBeOnlyOne()
{
    return (a, b) -> {throw new RuntimeException("Duplicate elements found: " + a + " and " + b);};
}

Jadi untuk kasus dengan UserAnda akan memiliki:

User match = users.stream().filter((user) -> user.getId() < 0).reduce(thereCanBeOnlyOne()).get();
pangkas
sumber
8

Menggunakan mengurangi

Ini adalah cara sederhana dan fleksibel yang saya temukan (berdasarkan jawaban @prunge)

Optional<User> user = users.stream()
        .filter(user -> user.getId() == 1)
        .reduce((a, b) -> {
            throw new IllegalStateException("Multiple elements: " + a + ", " + b);
        })

Dengan cara ini Anda memperoleh:

  • Opsional - seperti biasa dengan objek Anda atau Optional.empty()jika tidak ada
  • Pengecualian (dengan akhirnya jenis / pesan khusus ANDA) jika ada lebih dari satu elemen
Fabio Bonfante
sumber
6

Saya pikir cara ini lebih sederhana:

User resultUser = users.stream()
    .filter(user -> user.getId() > 0)
    .findFirst().get();
pilladooo
sumber
4
Itu hanya menemukan pertama tetapi kasus itu juga untuk membuang Pengecualian ketika lebih dari satu
lczapski
5

Menggunakan Collector:

public static <T> Collector<T, ?, Optional<T>> toSingleton() {
    return Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.size() == 1 ? Optional.of(list.get(0)) : Optional.empty()
    );
}

Pemakaian:

Optional<User> result = users.stream()
        .filter((user) -> user.getId() < 0)
        .collect(toSingleton());

Kami mengembalikan sebuah Optional, karena kami biasanya tidak dapat menganggap Collectionmengandung persis satu elemen. Jika Anda sudah tahu ini masalahnya, hubungi:

User user = result.orElseThrow();

Ini menempatkan beban menangani kesalahan pada penelepon - sebagaimana mestinya.

Neuron
sumber
1

Kita dapat menggunakan RxJava ( perpustakaan ekstensi reaktif yang sangat kuat )

LinkedList<User> users = new LinkedList<>();
users.add(new User(1, "User1"));
users.add(new User(2, "User2"));
users.add(new User(3, "User3"));

User userFound =  Observable.from(users)
                  .filter((user) -> user.getId() == 1)
                  .single().toBlocking().first();

The tunggal Operator melempar pengecualian jika tidak ada pengguna atau lebih dari satu pengguna ditemukan.

frhack
sumber
Jawaban yang benar, menginisialisasi aliran pemblokiran atau pengumpulan mungkin tidak terlalu murah (dalam hal sumber daya).
Karl Richter
1

Karena Collectors.toMap(keyMapper, valueMapper)menggunakan merger pelemparan untuk menangani banyak entri dengan kunci yang sama, mudah:

List<User> users = new LinkedList<>();
users.add(new User(1, "User1"));
users.add(new User(2, "User2"));
users.add(new User(3, "User3"));

int id = 1;
User match = Optional.ofNullable(users.stream()
  .filter(user -> user.getId() == id)
  .collect(Collectors.toMap(User::getId, Function.identity()))
  .get(id)).get();

Anda akan mendapatkan IllegalStateExceptionkunci duplikat. Tetapi pada akhirnya saya tidak yakin apakah kodenya tidak akan lebih mudah dibaca menggunakan if.

Arne Burmeister
sumber
1
Solusi bagus! Dan jika Anda melakukannya .collect(Collectors.toMap(user -> "", Function.identity())).get(""), Anda memiliki perilaku yang lebih umum.
glglgl
1

Saya menggunakan dua pengumpul tersebut:

public static <T> Collector<T, ?, Optional<T>> zeroOrOne() {
    return Collectors.reducing((a, b) -> {
        throw new IllegalStateException("More than one value was returned");
    });
}

public static <T> Collector<T, ?, T> onlyOne() {
    return Collectors.collectingAndThen(zeroOrOne(), Optional::get);
}
Xavier Dury
sumber
Rapi! onlyOne()melempar IllegalStateExceptionuntuk> 1 elemen, dan NoSuchElementException` (dalam Optional::get) untuk 0 elemen.
simon04
@ simon04 Anda bisa kelebihan metode untuk mengambil Supplierdari (Runtime)Exception.
Xavier Dury
1

Jika Anda tidak keberatan menggunakan perpustakaan pihak ke-3, SequenceMdari cyclops-stream (dan LazyFutureStreamdari reaksi sederhana ), keduanya memiliki operator tunggal & tunggal.

singleOptional()melempar pengecualian jika ada 0atau lebih dari 1elemen dalam Stream, jika tidak mengembalikan nilai tunggal.

String result = SequenceM.of("x")
                          .single();

SequenceM.of().single(); // NoSuchElementException

SequenceM.of(1, 2, 3).single(); // NoSuchElementException

String result = LazyFutureStream.fromStream(Stream.of("x"))
                          .single();

singleOptional()mengembalikan Optional.empty()jika tidak ada nilai atau lebih dari satu nilai di Stream.

Optional<String> result = SequenceM.fromStream(Stream.of("x"))
                          .singleOptional(); 
//Optional["x"]

Optional<String> result = SequenceM.of().singleOptional(); 
// Optional.empty

Optional<String> result =  SequenceM.of(1, 2, 3).singleOptional(); 
// Optional.empty

Pengungkapan - Saya adalah penulis dari kedua perpustakaan.

John McClean
sumber
0

Saya pergi dengan pendekatan langsung dan hanya mengimplementasikannya:

public class CollectSingle<T> implements Collector<T, T, T>, BiConsumer<T, T>, Function<T, T>, Supplier<T> {
T value;

@Override
public Supplier<T> supplier() {
    return this;
}

@Override
public BiConsumer<T, T> accumulator() {
    return this;
}

@Override
public BinaryOperator<T> combiner() {
    return null;
}

@Override
public Function<T, T> finisher() {
    return this;
}

@Override
public Set<Characteristics> characteristics() {
    return Collections.emptySet();
}

@Override //accumulator
public void accept(T ignore, T nvalue) {
    if (value != null) {
        throw new UnsupportedOperationException("Collect single only supports single element, "
                + value + " and " + nvalue + " found.");
    }
    value = nvalue;
}

@Override //supplier
public T get() {
    value = null; //reset for reuse
    return value;
}

@Override //finisher
public T apply(T t) {
    return value;
}


} 

dengan tes JUnit:

public class CollectSingleTest {

@Test
public void collectOne( ) {
    List<Integer> lst = new ArrayList<>();
    lst.add(7);
    Integer o = lst.stream().collect( new CollectSingle<>());
    System.out.println(o);
}

@Test(expected = UnsupportedOperationException.class)
public void failOnTwo( ) {
    List<Integer> lst = new ArrayList<>();
    lst.add(7);
    lst.add(8);
    Integer o = lst.stream().collect( new CollectSingle<>());
}

}

Implementasi ini bukan threadsafe.

gerardw
sumber
0
User match = users.stream().filter((user) -> user.getId()== 1).findAny().orElseThrow(()-> new IllegalArgumentException());
Nitin
sumber
5
Sementara kode ini dapat menyelesaikan pertanyaan, termasuk penjelasan tentang bagaimana dan mengapa ini menyelesaikan masalah akan sangat membantu untuk meningkatkan kualitas posting Anda, dan mungkin menghasilkan lebih banyak suara. Ingatlah bahwa Anda menjawab pertanyaan untuk pembaca di masa depan, bukan hanya orang yang bertanya sekarang. Harap edit jawaban Anda untuk menambahkan penjelasan dan berikan indikasi tentang batasan dan asumsi apa yang berlaku.
David Buck
-2

Sudahkah Anda mencoba ini?

long c = users.stream().filter((user) -> user.getId() == 1).count();
if(c > 1){
    throw new IllegalStateException();
}

long count()
Returns the count of elements in this stream. This is a special case of a reduction and is equivalent to:

     return mapToLong(e -> 1L).sum();

This is a terminal operation.

Sumber: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html

pardeep131085
sumber
3
Dikatakan bahwa count()itu tidak baik untuk digunakan karena ini adalah operasi terminal.
ryvantage
Jika ini benar-benar kutipan, silakan tambahkan sumber Anda
Neuron