Java Akışını 1 ve yalnızca 1 öğeye filtreleme


230

Bir Streamöğeleri bulmak için Java 8 s kullanmaya çalışıyorum LinkedList. Bununla birlikte, filtre kriterlerine bir ve tek bir eşleşme olduğunu garanti etmek istiyorum.

Bu kodu al:

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;
    }
}

Bu kod User, kimlikleri temel alınarak bulunur. Ancak Userfiltrenin kaç tane ile eşleştiğinin garantisi yoktur .

Filtre hattını şu şekilde değiştirme:

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

Bir firlatirmisiniz NoSuchElementException(iyi!)

Yine de birden fazla eşleşme varsa bir hata atmasını istiyorum. Bunu yapmanın bir yolu var mı?


count()bir terminal işlemidir, bu yüzden bunu yapamazsınız. Akış daha sonra kullanılamaz.
Alexis C.

Tamam, teşekkürler @ZouZou. Bu yöntemin ne yaptığından tam olarak emin değildim. Neden hayır Stream::size?
ryvantage

7
@ryvantage Bir akış yalnızca bir kez kullanılabildiğinden: boyutunun hesaplanması, üzerinde "yineleme" anlamına gelir ve bundan sonra akışı artık kullanamazsınız.
assylias

3
Vay. Bu yorum Streamdaha önce yaptığımdan çok daha fazlasını anlamama yardımcı oldu ...
ryvantage

2
Bu, LinkedHashSet(ekleme siparişinin korunmasını istediğinizi varsayarak) veya bir süre HashSetboyunca kullanmanız gerektiğini fark ettiğiniz zamandır . Koleksiyonunuz yalnızca tek bir kullanıcı kimliği bulmak için kullanılıyorsa, neden diğer tüm öğeleri topluyorsunuz? Her zaman benzersiz olması gereken bir kullanıcı kimliği bulmanız gerekecek bir potansiyel varsa, neden bir set değil bir liste kullanmalısınız? Geriye doğru programlıyorsunuz. İş için doğru koleksiyonu kullanın ve kendinizi bu baş ağrısından
kurtarın

Yanıtlar:


192

Özel oluştur 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);
            }
    );
}

Biz kullanmak Collectors.collectingAndThenbizim istenilen inşa etmek Collectoryoluyla

  1. Toplayıcı Listile nesnelerimizi Collectors.toList()toplamak.
  2. Sonunda tek bir öğeyi döndüren - veya bir IllegalStateExceptionif atar list.size != 1.

Olarak kullanılır:

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

Daha sonra Collectorbunu istediğiniz kadar özelleştirebilirsiniz ; örneğin, istisnayı yapıcıda argüman olarak verin, iki değere izin vermek için değiştirin ve daha fazlasını yapabilirsiniz.

Alternatif - tartışmasız daha az zarif - bir çözüm:

Bir 'geçici çözüm' kullanabilirsiniz peek()ve bir AtomicInteger, ama gerçekten bunu kullanmamalısınız.

Yapabileceğiniz şey onu sadece a'da toplamaktır List, şöyle:

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);

25
Guava'lar Iterables.getOnlyElementbu çözümleri kısaltır ve daha iyi hata mesajları sağlar. Zaten Google Guava kullanan diğer okuyuculara bir ipucu olarak.
Tim Büthe

2
bu fikri bir sınıfa tamamladım - gist.github.com/denov/a7eac36a3cda041f8afeabcef09d16fc
denov

1
@LonelyNeuron Lütfen kodumu düzenlemeyin. Beni dört yıl önce yazdığım tüm cevabımı doğrulamam gereken bir duruma sokuyor ve şu anda bunun için zamanım yok.
skiwi

2
@skiwi: Lonely'nin düzenlemesi yararlı ve doğruydu, bu yüzden gözden geçirdikten sonra yeniden kararlaştırdım. Bugün bu yanıtı ziyaret eden kullanıcılar cevaba nasıl geldiğinizi umursamıyorlar, eski sürümü ve yeni sürümü ve Güncellenmiş bölümünü görmeleri gerekmiyor . Bu, yanıtınızı daha kafa karıştırıcı ve daha az yardımcı hale getirir. Gönderiyi son duruma getirmek çok daha iyidir ve insanlar nasıl oynandığını görmek isterse gönderi geçmişini görüntüleyebilirler.
Martijn Pieters

1
@skiwi: Cevaptaki kod kesinlikle yazdıklarınız. Düzenleyicinin yaptığı tüm yayınınızı temizlemek, yalnızca yayındasingletonCollector() kalan sürüm tarafından kullanılmayan tanımın önceki bir sürümünü kaldırmak ve adını yeniden adlandırmaktı toSingleton(). Java akış uzmanlığım biraz paslı, ancak yeniden adlandırma bana yardımcı oluyor. Bu değişikliği gözden geçirmek 2 dakika sürdü. Düzenlemeleri incelemek için zamanınız yoksa, gelecekte bir başkasından, belki de Java sohbet odasında bunu yapmasını istemenizi önerebilir miyim ?
Martijn Pieters

119

Tamlık uğruna, @ prunge'nin mükemmel cevabına karşılık gelen 'tek astar':

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

Bu, tek eşleme elemanını akıştan alır,

  • NoSuchElementException akışın boş olması durumunda veya
  • IllegalStateException akışın birden fazla eşleşen öğe içermesi durumunda.

Bu yaklaşımın bir varyasyonu, bir istisnayı erkenden atmaktan kaçınır ve bunun yerine sonucu Optional, tek öğeyi içeren veya sıfır veya birden çok öğe varsa hiçbir şey (boş) olarak temsil eder :

Optional<User> user1 = users.stream()
        .filter(user -> user.getId() == 1)
        .collect(Collectors.reducing((a, b) -> null));

3
Bu cevaptaki ilk yaklaşımı seviyorum. Özelleştirme amaçları için, son dönüştürmek mümkündür get()içinorElseThrow()
arin

1
Bunun özlülüğünü ve her çağrıldığında gereksiz bir Liste örneği oluşturmaktan kaçınmasını seviyorum.
LordOfThePigs

83

Bir gelenek yazmayı içeren diğer cevaplar Collectormuhtemelen daha verimlidir ( Louis Wasserman , +1 gibi), ancak kısalık istiyorsanız, aşağıdakileri öneririm:

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

Ardından sonuç listesinin boyutunu doğrulayın.

if (result.size() != 1) {
  throw new IllegalStateException("Expected exactly one user but got " + result);
User user = result.get(0);
}

5
limit(2)Bu çözümün anlamı nedir ? Ortaya çıkan listenin 2 veya 100 olması ne fark eder? 1'den büyükse.
Mart'ta ryvantage

18
İkinci bir eşleşme bulursa derhal durur. Sadece daha fazla kod kullanarak tüm süslü koleksiyoncular bunu yapar. :-)
Stuart Marks

10
Collectors.collectingAndThen(toList(), l -> { if (l.size() == 1) return l.get(0); throw new RuntimeException(); })
Lukas Eder

1
Javadoc bu konuda Sınırın param diyor: maxSize: the number of elements the stream should be limited to. Peki, .limit(1)bunun yerine olmamalı .limit(2)mı?
alexbt

5
@alexbt Sorun ifadesi, tam olarak bir (daha fazla, daha az değil) eşleşen öğe olduğundan emin olmaktır. Kodumdan sonra, result.size()1'e eşit olduğundan emin olmak için test edilebilir. 2 ise, birden fazla eşleşme vardır, bu bir hatadır. Kod yerine limit(1), birden fazla eşleşme, tek bir öğeyle sonuçlanır; bu, tam olarak bir eşleşme olduğundan ayırt edilemez. Bu OP'nin endişe duyduğu bir hatayı kaçırır.
Stuart Marks

67

Guava sağlar MoreCollectors.onlyElement()Burada doğru şeyi yapan. Ancak bunu kendiniz yapmanız gerekiyorsa, bunun için kendiniz yapabilirsiniz 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);
}

... veya Holderyerine kendi türünüzü kullanarak AtomicReference. Bunu Collectoristediğiniz kadar tekrar kullanabilirsiniz .


@ skiwi'nin singletonCollector'u bundan daha küçük ve takip edilmesi daha kolaydı, bu yüzden ona çek verdim. Ama cevapta fikir birliği görmek güzel: bir gelenek Collectoryoluydu.
ryvantage

1
Yeterince adil. Kısacası kısmi olmayı değil, hızı hedefliyordum.
Louis Wasserman

1
Evet? Seninki neden daha hızlı?
ryvantage

3
Çoğunlukla bir all-up tahsis etmek Listtek değişkenli bir referanstan daha pahalıdır.
Louis Wasserman

1
@ LouisWasserman, hakkındaki son güncelleme cümlesi MoreCollectors.onlyElement()aslında ilk olmalı (ve belki de tek :))
Piotr Findeisen

46

Guava MoreCollectors.onlyElement()( JavaDoc ) kullanın .

İstediğinizi yapar ve IllegalArgumentExceptionakış iki veya daha fazla öğeden oluşuyorsa NoSuchElementExceptionve akış boşsa a atar .

Kullanımı:

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

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

2
Diğer kullanıcılar için not: MoreCollectorshenüz yayınlanmamış (2016-12 itibariyle) yayınlanmamış sürüm 21'in bir
parçasıdır

2
Bu cevap yukarı çıkmalıdır.
Emdadul Sawon

31

Akımlar tarafından başka şekilde desteklenmeyen garip şeyler yapmanızı sağlayan "kaçış kapağı" işlemi aşağıdakileri istemektir 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();
}

Guava'nın, Iteratortek n öğeyi almak ve almak için uygun bir yöntemi vardır, sıfır veya birden fazla öğe varsa, buradaki alt n-1 çizgilerinin yerini alabilir.


4
Guava'nın yöntemi: Iterators.getOnlyElement (Yineleyici <T> yineleyici).
anre

23

Güncelleme

@Holger tarafından yapılan yorumda güzel öneri:

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

Orijinal cevap

İstisna atılır Optional#get, ancak yardımcı olmayacak birden fazla öğeniz varsa. Kullanıcıları yalnızca bir öğeyi kabul eden bir koleksiyonda toplayabilirsiniz, örneğin:

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

ki bu atar java.lang.IllegalStateException: Queue full, ama bu çok acayip geliyor.

Veya isteğe bağlı olarak birleştirilmiş bir azaltma kullanabilirsiniz:

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();

Azaltma esasen şunu döndürür:

  • hiçbir kullanıcı bulunamazsa null
  • yalnızca bir tane bulunursa kullanıcı
  • birden fazla bulunursa istisna atar

Sonuç daha sonra isteğe bağlı olarak sarılır.

Ancak en basit çözüm muhtemelen bir koleksiyona toplamak, boyutunun 1 olup olmadığını kontrol etmek ve tek öğeyi almak olacaktır.


1
nullKullanmayı önlemek için bir kimlik öğesi ( ) eklerdim get(). Ne yazık ki, reducesizin düşündüğünüz gibi çalışmıyor, Streamiçinde nullunsurlar olan bir düşünün , belki de onu kapsadığınızı düşünüyorsunuz, ama ben olabilirim [User#1, null, User#2, null, User#3], şimdi burada yanlış olmadıkça, bence bir istisna atmayacak.
skiwi

2
@Skiwi null elemanlar varsa, filtre önce bir NPE atar.
assylias

2
Eğer dere geçemez biliyoruz yana nullolan tüm konu işlemi kılacak kimlik değeri argüman kaldırarak, azaltma işlevine nulleskimiş işlevinde: reduce( (u,v) -> { throw new IllegalStateException("More than one ID found"); } )iş yapar ve daha da iyisi, zaten bir döner Optionalçağırmak için gerekliliğini eliding, Optional.ofNullableüzerinde sonuç.
Holger

15

Bir alternatif, redüksiyon kullanmaktır: (bu örnek, dizeleri kullanır ancak aşağıdakiler dahil herhangi bir nesne türüne kolayca uygulanabilir 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);};
}

Yani Usersenin için dava olurdu:

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

8

Azaltma kullanma

Bu bulduğum daha basit ve esnek bir yol (@ prunge yanıtına dayanarak)

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

Bu şekilde elde edersiniz:

  • İsteğe bağlı - her zaman nesnenizle olduğu gibi veya Optional.empty()yoksa
  • Birden fazla öğe varsa İstisna (sonunda SİZİN özel türünüz / iletinizle birlikte)

6

Bu şekilde daha basit olduğunu düşünüyorum:

User resultUser = users.stream()
    .filter(user -> user.getId() > 0)
    .findFirst().get();

4
Sadece ilk bulmak ama dava birden fazla olduğunda da istisna atmak oldu
lczapski

5

Kullanarak 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()
    );
}

Kullanımı:

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

OptionalGenellikle Collectionbir öğeyi içerdiğini varsayamayacağımız için bir döndürüriz . Durumun bu olduğunu zaten biliyorsanız, arayın:

User user = result.orElseThrow();

Bu, arayana hatayı elden çıkarma yükünü getirir - olması gerektiği gibi.



1

Biz kullanabilirsiniz RxJava (çok güçlü reaktif uzatma kütüphanesi)

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();

Tek operatör Kullanıcı ya da daha sonra bir kullanıcı bulunursa bir istisna atar.


Doğru cevap, bir engelleme akışının veya koleksiyonun ilklendirilmesi muhtemelen (kaynaklar açısından) çok ucuz değildir.
Karl Richter

1

Gibi Collectors.toMap(keyMapper, valueMapper)aynı anahtarla birden fazla giriş işlemek için kullanımlara bir atma birleşme kolaydır:

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();

IllegalStateExceptionYinelenen anahtarlar için bir alacaksınız . Ama sonunda kod kullanarak bir daha okunabilir olmaz emin değilim if.


1
Güzel çözüm! Ve eğer yaparsanız .collect(Collectors.toMap(user -> "", Function.identity())).get(""), daha genel bir davranışınız olur.
glglgl

1

Bu iki toplayıcıyı kullanıyorum:

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);
}

Temiz! > 1 öğe için ve 0 öğe için NoSuchElementException` (in ) değerini onlyOne()atar . IllegalStateExceptionOptional::get
simon04

simon04 @ Bir almaya yöntemleri aşılmasına neden olabilir Supplierait (Runtime)Exception.
Xavier Dury

1

Eğer, bir 3. parti kitaplığı kullanarak sakıncası yoksa SequenceMgelen Tepegöz-akışları (ve LazyFutureStreamgelen basit-tepki ) hem tek ve singleOptional operatörümüz var.

singleOptional()öğesinde 0veya daha fazla 1öğe varsa bir istisna atar Stream, aksi takdirde tek değeri döndürür.

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()Optional.empty()değerinde veya birden fazla değer yoksa döndürür 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

Açıklama - Her iki kütüphanenin de yazarıyım.


0

Doğrudan yaklaşımla gittim ve sadece şeyi uyguladım:

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;
}


} 

JUnit testi ile:

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<>());
}

}

Bu uygulama threadsafe değildir .


0
User match = users.stream().filter((user) -> user.getId()== 1).findAny().orElseThrow(()-> new IllegalArgumentException());

5
Bu kod, sorunun nasıl ve neden çözüldüğüne dair bir açıklama da dahil olmak üzere soruyu çözebilir, ancak gönderinizin kalitesini artırmaya yardımcı olabilir ve muhtemelen daha fazla oyla sonuçlanır. Sadece şimdi soran kişi için değil, gelecekte okuyucular için soruyu cevapladığınızı unutmayın. Lütfen açıklama eklemek için cevabınızı düzenleyin ve hangi sınırlamaların ve varsayımların geçerli olduğunu belirtin.
David Buck

-2

Bunu denedin mi

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.

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


3
count()Terminal operasyonu olduğu için kullanımının iyi olmadığı söylendi .
Ocak'ta ryvantage

Bu gerçekten bir teklifse, lütfen kaynaklarınızı ekleyin
Neuron
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.