Filter Java Stream op 1 en slechts 1 element

Ik probeer Java 8 te gebruiken Streams om elementen in een LinkedListte vinden. Ik wil echter garanderen dat er één en slechts één overeenkomst is met de filtercriteria.

Neem deze code:

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

Deze code vindt een Userop basis van hun ID. Maar er zijn geen garanties hoeveel User‘s overeenkwamen met het filter.

De filterregel wijzigen in:

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

Gooit een NoSuchElementException(goed!)

Ik zou echter willen dat het een foutmelding geeft als er meerdere overeenkomsten zijn. Is er een manier om dit te doen?


Antwoord 1, autoriteit 100%

Maak een aangepaste 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);
            }
    );
}

We gebruiken Collectors.collectingAndThenom onze gewenste Collectorte bouwen door

  1. Onze objecten verzamelen in een Listmet het Collectors.toList()-verzamelprogramma.
  2. Aan het einde een extra finisher toepassen, die het enkele element retourneert — of een IllegalStateExceptiongenereert als list.size != 1.

Gebruikt als:

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

Je kunt deze Collectorvervolgens zo veel aanpassen als je wilt, bijvoorbeeld de uitzondering als argument in de constructor geven, het aanpassen om twee waarden toe te staan, en meer.

Een alternatieve — aantoonbaar minder elegante — oplossing:

Je kunt een ‘oplossing’ gebruiken die peek()en een AtomicIntegeromvat, maar dat zou je eigenlijk niet moeten gebruiken.

Wat u in plaats daarvan kunt doen, is het gewoon in een Listverzamelen, zoals dit:

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

Antwoord 2, autoriteit 60%

Voor de volledigheid is hier de ‘one-liner’ die overeenkomt met het uitstekende antwoord van @prunge:

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

Dit haalt het enige overeenkomende element uit de stream, werpend

  • NoSuchElementExceptionin het geval dat de stream leeg is, of
  • IllegalStateExceptionvoor het geval de stream meer dan één overeenkomend element bevat.

Een variatie op deze aanpak voorkomt dat er vroegtijdig een uitzondering wordt gegenereerd en geeft in plaats daarvan het resultaat weer als een Optionaldie ofwel het enige element bevat, of niets (leeg) als er nul of meerdere elementen zijn:

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

Antwoord 3, autoriteit 36%

De andere antwoorden die betrekking hebben op het schrijven van een aangepaste Collectorzijn waarschijnlijk efficiënter (zoals Louis Wasserman’s, +1), maar als je het kort wilt houden, raad ik het volgende aan:

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

Verifieer vervolgens de grootte van de resultatenlijst.

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

Antwoord 4, autoriteit 34%

Guavabiedt MoreCollectors.onlyElement()die het juiste doet hier. Maar als je het zelf moet doen, kun je hiervoor je eigen Collectorgebruiken:

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

…of uw eigen Holder-type gebruiken in plaats van AtomicReference. Je kunt die Collectorzo vaak hergebruiken als je wilt.


Antwoord 5, autoriteit 26%

Gebruik MoreCollectors.onlyElement()(Broncode).

Het doet wat je wilt en genereert een IllegalArgumentExceptionals de stream uit twee of meer elementen bestaat, en een NoSuchElementExceptionals de stream leeg is.

Gebruik:

import static com.google.common.collect.MoreCollectors.onlyElement;
User match =
    users.stream().filter((user) -> user.getId() < 0).collect(onlyElement());

Antwoord 6, autoriteit 13%

De “escape hatch”-bewerking waarmee je rare dingen kunt doen die anders niet door streams worden ondersteund, is om een ​​Iteratorte vragen:

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 heeft een handige methode om een ​​Iteratorte nemen en het enige element te krijgen, waarbij wordt gegooid als er nul of meerdere elementen zijn, die de onderste n-1 regels hier zouden kunnen vervangen.


Antwoord 7, autoriteit 10%

Bijwerken

Leuke suggestie in reactie van @Holger:

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

Oorspronkelijk antwoord

De uitzondering wordt gegenereerd door Optional#get, maar als je meer dan één element hebt, helpt dat niet. U kunt de gebruikers verzamelen in een verzameling die slechts één item accepteert, bijvoorbeeld:

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

die een java.lang.IllegalStateException: Queue fullgooit, maar dat voelt te hacky.

Of u kunt een korting gebruiken in combinatie met een optionele:

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

De verlaging keert in wezen terug:

  • null als er geen gebruiker is gevonden
  • de gebruiker als er maar één wordt gevonden
  • geeft een uitzondering als er meer dan één wordt gevonden

Het resultaat wordt dan verpakt in een optionele.

Maar de eenvoudigste oplossing zou waarschijnlijk zijn om gewoon naar een verzameling te verzamelen, te controleren of de grootte 1 is en het enige element te krijgen.


Antwoord 8, autoriteit 8%

Ik denk dat deze manier eenvoudiger is:

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

Antwoord 9, autoriteit 7%

Een alternatief is om reductie te gebruiken:
(in dit voorbeeld worden tekenreeksen gebruikt, maar kan gemakkelijk worden toegepast op elk objecttype, inclusief 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);};
}

Dus voor het geval met Userzou je hebben:

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

Antwoord 10, autoriteit 5%

Verminderen gebruiken

Dit is de eenvoudigere en flexibelere manier die ik heb gevonden (gebaseerd op het antwoord van @prunge)

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

Op deze manier verkrijgt u:

  • de Optionele – zoals altijd met uw object of Optional.empty()indien niet aanwezig
  • de uitzondering (met eventueel JOUW aangepast type/bericht) als er meer dan één element is

Antwoord 11, autoriteit 4%

Guavaheeft hiervoor een Collectorgenaamd MoreCollectors.onlyElement().


Antwoord 12, autoriteit 3%

Een Collector:

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

Gebruik:

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

We retourneren een Optional, aangezien we meestal niet kunnen aannemen dat de Collectionprecies één element bevat. Als je al weet dat dit het geval is, bel dan:

User user = result.orElseThrow();

Dit legt de last van het afhandelen van de fout bij de beller – zoals het hoort.


Antwoord 13

We kunnen RxJavagebruiken (zeer krachtige reactieve extensiebibliotheek)

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

De enkeleoperatorgenereert een uitzondering als er geen gebruiker of meer dan wordt er één gebruiker gevonden.


Antwoord 14

Omdat Collectors.toMap(keyMapper, valueMapper)een throwing merge gebruikt om meerdere items met dezelfde sleutel te verwerken, is het eenvoudig:

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

Je krijgt een IllegalStateExceptionvoor dubbele sleutels. Maar uiteindelijk weet ik niet zeker of de code niet nog leesbaarder zou zijn met een if.


Antwoord 15

Ik gebruik die twee verzamelprogramma’s:

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

Antwoord 16

Als je het niet erg vindt om een ​​bibliotheek van derden te gebruiken, SequenceMvan cyclops-streams(en LazyFutureStreamvan simple-react) beide hebben een enkele & enkeleOptionele operators.

singleOptional()genereert een uitzondering als er 0of meer dan 1elementen in de Streamzijn, anders wordt de enkele waarde geretourneerd.

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()retourneert Optional.empty()als er geen waarden zijn of meer dan één waarde in de 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

Openbaarmaking – ik ben de auteur van beide bibliotheken.


Antwoord 17

List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
Integer value  = list.stream().filter((x->x.intValue()==8)).findFirst().orElse(null);

Ik heb Integer-type gebruikt in plaats van primitiefomdat het een null-pointer-uitzondering zal hebben. je moet gewoon met deze uitzondering omgaan… ziet er beknopt uit, denk ik;)


Antwoord 18

Verminderen en optioneel gebruiken

Van Fabio Bonfantereactie:

public <T> T getOneExample(Collection<T> collection) {
    return collection.stream()
        .filter(x -> /* do some filter */)
        .reduce((x,y)-> {throw new IllegalStateException("multiple");})
        .orElseThrow(() -> new NoSuchElementException("none"));
}

Antwoord 19

Als je Guava of Kotlin niet gebruikt, is hier een oplossing gebaseerd op de antwoorden van @skiwi en @Neuron.

users.stream().collect(single(user -> user.getId() == 1));

of

users.stream().collect(optional(user -> user.getId() == 1));

waarbij singleen Optionalstatisch geïmporteerde functies zijn die corresponderende verzamelprogramma’s retourneren.

Ik redeneerde dat het er beknopter uit zou zien als de filterlogica in de collector was verplaatst. Ook zou er niets in de code breken als je de string zou verwijderen met .filter.

De kern van de code https://gist.github.com/overpas/ccc39b75f17a1c65682c071045c1a079


Antwoord 20

Ik heb zelf een voorbeeldcode geprobeerd en hier is de oplossing daarvoor.

User user = Stream.of(new User(2), new User(2), new User(1), new User(2))
            .filter(u -> u.getAge() == 2).findFirst().get();

en de gebruikersklasse

class User {
    private int age;
public User(int age) {
    this.age = age;
}
public int getAge() {
    return age;
}
public void setAge(int age) {
    this.age = age;
 }
}

Other episodes