Ik probeer Java 8 te gebruiken Stream
s om elementen in een LinkedList
te 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 User
op 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.collectingAndThen
om onze gewenste Collector
te bouwen door
- Onze objecten verzamelen in een
List
met hetCollectors.toList()
-verzamelprogramma. - Aan het einde een extra finisher toepassen, die het enkele element retourneert — of een
IllegalStateException
genereert alslist.size != 1
.
Gebruikt als:
User resultUser = users.stream()
.filter(user -> user.getId() > 0)
.collect(toSingleton());
Je kunt deze Collector
vervolgens 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 AtomicInteger
omvat, maar dat zou je eigenlijk niet moeten gebruiken.
Wat u in plaats daarvan kunt doen, is het gewoon in een List
verzamelen, 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
NoSuchElementException
in het geval dat de stream leeg is, ofIllegalStateException
voor 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 Optional
die 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 Collector
zijn 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 Collector
gebruiken:
<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 Collector
zo vaak hergebruiken als je wilt.
Antwoord 5, autoriteit 26%
Gebruik MoreCollectors.onlyElement()
(Broncode).
Het doet wat je wilt en genereert een IllegalArgumentException
als de stream uit twee of meer elementen bestaat, en een NoSuchElementException
als 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 Iterator
te 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 Iterator
te 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 full
gooit, 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 User
zou 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 Collector
genaamd 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 Collection
precies éé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 IllegalStateException
voor 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, SequenceM
van cyclops-streams(en LazyFutureStream
van simple-react) beide hebben een enkele & enkeleOptionele operators.
singleOptional()
genereert een uitzondering als er 0
of meer dan 1
elementen in de Stream
zijn, 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 single
en Optional
statisch 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;
}
}