Filtre Java Stream a 1 y solo 1 elemento

Estoy tratando de usar Java 8 Stream para encontrar elementos en LinkedList . Sin embargo, quiero garantizar que haya una y única coincidencia con los criterios de filtro.

Toma 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 encuentra un User basado en su ID. Pero no hay garantías de cuántos User coinciden con el filtro.

Cambiar la línea de filtro a:

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

NoSuchElementException una NoSuchElementException (¡bien!)

Me gustaría arrojar un error si hay múltiples coincidencias, sin embargo. ¿Hay alguna forma de hacer esto?

Crear un Collector personalizado

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

Utilizamos Collectors.collectingAndThen para construir nuestro Collector deseado por

  1. Recolectar nuestros objetos en una List con el Collectors.toList() .
  2. Aplicando un finalizador extra al final, que devuelve el elemento individual – o arroja una IllegalStateException si list.size != 1 .

Usado como:

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

A continuación, puede personalizar este Collector tanto como desee; por ejemplo, especifique la excepción como argumento en el constructor, modifíquela para permitir dos valores, y más.

Una alternativa, posiblemente menos elegante, solución:

Puedes usar una ‘solución’ que involucra peek() y un AtomicInteger , pero realmente no deberías estar usando eso.

Lo que podrías hacer es simplemente juntarlo en una List , así:

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

En aras de la exhaustividad, aquí está el ‘one-liner’ que corresponde a la excelente respuesta de @prunge:

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

Esto obtiene el único elemento coincidente de la secuencia, lanzando

  • NoSuchElementException en caso de que la transmisión esté vacía, o
  • IllegalStateException en caso de que la secuencia contenga más de un elemento coincidente.

Una variación de este enfoque evita lanzar una excepción anticipadamente y en su lugar representa el resultado como un Optional contiene el elemento único o nada (vacío) si hay cero o múltiples elementos:

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

Las otras respuestas que implican escribir un Collector personalizado son probablemente más eficientes (como Louis Wasserman’s , +1), pero si quiere brevedad, sugeriría lo siguiente:

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

Luego verifica el tamaño de la lista de resultados.

Guava proporciona MoreCollectors.onlyElement() que hace lo correcto aquí. Pero si tiene que hacerlo usted mismo, puede lanzar su propio Collector para esto:

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

… o utilizando su propio tipo de Holder lugar de AtomicReference . Puedes reutilizar ese Collector tanto como quieras.

Utilice los MoreCollectors.onlyElement() ( MoreCollectors.onlyElement() Guava.

Hace lo que desea y arroja una IllegalArgumentException si la transmisión consta de dos o más elementos, y una NoSuchElementException si la transmisión está vacía.

Uso:

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

La operación “escape hatch” que le permite hacer cosas raras que no son soportadas por streams es pedir un 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(); } 

La guayaba tiene un método de conveniencia para tomar un Iterator y obtener el único elemento, lanzar si hay cero o múltiples elementos, que podrían reemplazar las líneas n-1 de abajo aquí.

Actualizar

Buena sugerencia en el comentario de @Holger:

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

Respuesta original

La excepción es lanzada por Optional#get , pero si tiene más de un elemento que no ayudará. Puede recostackr los usuarios en una colección que solo acepta un elemento, por ejemplo:

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

que arroja una java.lang.IllegalStateException: Queue full , pero se siente demasiado hacky.

O puede usar una reducción combinada con una opción:

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

La reducción esencialmente regresa:

  • null si no se encuentra un usuario
  • el usuario si solo se encuentra uno
  • arroja una excepción si se encuentra más de uno

El resultado se envuelve en una opción.

Pero la solución más simple probablemente sería recostackr solo a una colección, verificar que su tamaño sea 1 y obtener el único elemento.

Una alternativa es usar la reducción: (este ejemplo usa cadenas pero podría aplicarse fácilmente a cualquier tipo de objeto, incluido el 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);}; } 

Entonces, para el caso con el User usted tendría:

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

Guava tiene un Collector para esto llamado MoreCollectors.onlyElement() .

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

Devolvemos un Optional , ya que normalmente no podemos suponer que la Collection contenga exactamente un elemento. Si ya sabes que este es el caso, llama:

 User user = result.orElseThrow(); 

Esto pone la carga de manejar el error en la persona que llama, como debería.

Podemos usar RxJava (biblioteca de extensión reactiva muy 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(); 

El operador único arroja una excepción si no se encuentra un usuario o más de un usuario.

Como Collectors.toMap(keyMapper, valueMapper) usa una fusión arrojadiza para manejar múltiples entradas con la misma clave, es 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(); 

Obtendrá una IllegalStateException para claves duplicadas. Pero al final no estoy seguro si el código no sería aún más legible usando un if .

Si no te importa usar una biblioteca de terceros, SequenceM de cyclops-streams (y LazyFutureStream de simple-reactjsr ) ambos tienen operadores únicos y únicos de Opción.

singleOptional() arroja una excepción si hay 0 o más de 1 elementos en el Stream , de lo contrario, devuelve el 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() devuelve Optional.empty() si no hay valores o más de un valor en la 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 

Divulgación: soy el autor de ambas bibliotecas.

Estoy usando esos dos coleccionistas:

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

Fui con el enfoque directo y acabo de implementar la cosa:

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

con la prueba 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 implementación no es insegura.

Usando reducir

Esta es la forma más simple y flexible que encontré (basada en la respuesta @prunge)

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

De esta forma obtienes:

  • el opcional – como siempre con su objeto o Optional.empty () si no está presente
  • la Excepción (eventualmente SU tipo / mensaje personalizado) si hay más de un elemento

Has probado esto

 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.