Filtrar o stream de Java para 1 e apenas 1 elemento

Eu estou tentando usar o Java 8 Stream s para encontrar elementos em um LinkedList . Eu quero garantir, no entanto, que há uma e apenas uma correspondência com os critérios de filtro.

Pegue este código:

 public static void main(String[] args) { LinkedList 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; } } 

Este código encontra um User base em seu ID. Mas não há garantias de quantos User correspondem ao filtro.

Alterando a linha do filtro para:

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

Vai lançar um NoSuchElementException (bom!)

Eu gostaria de lançar um erro se houver várias correspondências, no entanto. Existe uma maneira de fazer isso?

Crie um Collector personalizado

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

Usamos Collectors.collectingAndThen para construir o nosso Collector desejado

  1. Coletando nossos objects em uma List com o Collectors.toList() .
  2. Aplicando um finalizador extra no final, que retorna o único elemento – ou lança um IllegalStateException se list.size != 1 .

Usado como:

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

Você pode personalizar esse Collector quanto quiser, por exemplo, dar a exceção como argumento no construtor, ajustá-lo para permitir dois valores e mais.

Uma alternativa – sem dúvida menos elegante – solução:

Você pode usar um ‘workaround’ que envolva peek() e um AtomicInteger , mas na verdade você não deveria estar usando isso.

O que você poderia fazer é apenas coletá-lo em uma List , assim:

 LinkedList users = new LinkedList<>(); users.add(new User(1, "User1")); users.add(new User(2, "User2")); users.add(new User(3, "User3")); List resultUserList = users.stream() .filter(user -> user.getId() == 1) .collect(Collectors.toList()); if (resultUserList.size() != 1) { throw new IllegalStateException(); } User resultUser = resultUserList.get(0); 

Por uma questão de perfeição, aqui está o ‘one-liner’ correspondente à excelente resposta do @prunge:

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

Isso obtém o único elemento correspondente do stream, jogando

  • NoSuchElementException caso o stream esteja vazio ou
  • IllegalStateException no caso de o stream conter mais de um elemento correspondente.

Uma variação dessa abordagem evita lançar uma exceção antecipadamente e, em vez disso, representa o resultado como um Optional contendo o elemento único ou nada (vazio) se houver zero ou vários elementos:

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

Uau, tamanha complexidade! 🙂 As outras respostas que envolvem escrever um Coletor customizado são provavelmente mais eficientes (como as de Louis Wasserman , +1), mas se você quiser brevidade, sugiro o seguinte:

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

Em seguida, verifique o tamanho da lista de resultados.

A Guava fornece MoreCollectors.onlyElement() que faz a coisa certa aqui. Mas se você tiver que fazer isso sozinho, você pode criar seu próprio Collector para isso:

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

… ou usando seu próprio tipo de Holder vez de AtomicReference . Você pode reutilizar esse Collector quanto quiser.

Use o MoreCollectors.onlyElement() ( JavaDoc ) do Guava.

Ele faz o que você quer e lança um IllegalArgumentException se o stream consistir em dois ou mais elementos e um NoSuchElementException se o stream estiver vazio.

Uso:

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

A operação “escape hatch”, que permite fazer coisas estranhas que não são suportadas pelos streams, é pedir por um Iterator :

 Iterator 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 tem um método conveniente para pegar um Iterator e obter o único elemento, jogando se houver zero ou vários elementos, o que poderia replace as linhas n-1 inferiores aqui.

Atualizar

Boa sugestão no comentário do @Holger:

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

Resposta original

A exceção é lançada por Optional#get , mas se você tiver mais de um elemento que não ajudará. Você pode coletar os usuários em uma coleção que aceita apenas um item, por exemplo:

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

Que lança um java.lang.IllegalStateException: Queue full , mas que parece muito hacky.

Ou você poderia usar uma redução combinada com um opcional:

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

A redução retorna essencialmente:

  • null se nenhum usuário for encontrado
  • o usuário se apenas um for encontrado
  • lança uma exceção se mais de um for encontrado

O resultado é então empacotado em um opcional.

Mas a solução mais simples provavelmente seria simplesmente coletar em uma coleção, verificar se seu tamanho é 1 e obter o único elemento.

Uma alternativa é usar redução: (este exemplo usa strings, mas pode ser aplicado facilmente a qualquer tipo de object, incluindo User )

 List 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  BinaryOperator thereCanBeOnlyOne() { return (a, b) -> {throw new RuntimeException("Duplicate elements found: " + a + " and " + b);}; } 

Então, para o caso com o User você teria:

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

Guava tem um Collector para isso chamado MoreCollectors.onlyElement() .

Podemos usar o RxJava (biblioteca de extensão reativa muito poderosa)

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

O operador único lança uma exceção se nenhum usuário ou mais de um usuário for encontrado.

Se você não se importa em usar uma biblioteca de terceiros, o SequenceM de cyclops-streams (e o LazyFutureStream de simple- react ) ambos têm operadores single & singleOptional.

singleOptional lança uma exceção se houver 0 ou mais de 1 elementos no Stream, caso contrário, retornará o valor único.

  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 return Optional.empty se não houver valores ou mais de um valor no Stream.

  Optional result = SequenceM.fromStream(Stream.of("x")) .singleOptional(); //Optional["x"] Optional result = SequenceM.of().singleOptional(); // Optional.empty Optional result = SequenceM.of(1,2,3).singleOptional(); // Optional.empty 

Divulgação – Eu sou o autor de ambas as bibliotecas.

Como Collectors.toMap(keyMapper, valueMapper) usa uma fusão de lançamento para manipular várias inputs com a mesma chave, é fácil:

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

Você receberá uma IllegalStateException para chaves duplicadas. Mas no final eu não tenho certeza se o código não seria ainda mais legível usando um if .

Usando um Collector :

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

Uso:

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

Retornamos um Optional , já que geralmente não podemos supor que a Collection contenha exatamente um elemento. Se você já sabe que este é o caso, ligue:

 User user = result.orElseThrow(); 

Isso coloca o fardo de manipular o erro no chamador - como deveria.

Eu estou usando esses dois coletores:

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

Eu fui com a abordagem direta e apenas implementei a coisa:

 public class CollectSingle implements Collector, BiConsumer, Function, Supplier { T value; @Override public Supplier supplier() { return this; } @Override public BiConsumer accumulator() { return this; } @Override public BinaryOperator combiner() { return null; } @Override public Function finisher() { return this; } @Override public Set 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; } } 

com o teste JUnit:

 public class CollectSingleTest { @Test public void collectOne( ) { List 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 lst = new ArrayList<>(); lst.add(7); lst.add(8); Integer o = lst.stream().collect( new CollectSingle<>()); } } 

Esta implementação não é threadsafe.

Usando reduzir

Essa é a maneira mais simples e flexível que encontrei (com base na resposta @prunge)

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

Desta forma você obtém:

  • o Opcional – como sempre com seu object ou Optional.empty () se não estiver presente
  • Exceção (eventualmente com seu tipo / mensagem personalizada) se houver mais de um elemento

Você já tentou isso

  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.