Filtre Java Stream a 1 y solo 1 elemento


230

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

Toma este código:

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

Este código encuentra un Userbasado en su ID. Pero no hay garantías de cuántos Usercoinciden con el filtro.

Cambiar la línea del filtro a:

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

Lanzará un NoSuchElementException(¡bueno!)

Sin embargo, me gustaría que arroje un error si hay varias coincidencias. ¿Hay alguna forma de hacer esto?


count()es una operación de terminal, así que no puedes hacer eso. La secuencia no se puede usar después.
Alexis C.

Ok, gracias @ZouZou. No estaba completamente seguro de lo que hizo ese método. ¿Por qué no hay Stream::size?
ryvantage

77
@ryvantage Debido a que una secuencia solo se puede usar una vez: calcular su tamaño significa "iterar" sobre ella y después de eso ya no se puede usar la secuencia.
Assylias

3
Guau. Ese comentario me ayudó a comprender Streammucho más de lo que lo hice antes ...
ryvantage

2
Esto es cuando se da cuenta de que había necesitado usar un LinkedHashSet(suponiendo que desea preservar el orden de inserción) o HashSettodo el tiempo. Si su colección solo se utiliza para encontrar una única identificación de usuario, ¿por qué está recopilando todos los demás elementos? Si existe la posibilidad de que siempre necesite encontrar alguna identificación de usuario que también deba ser única, ¿por qué usar una lista y no un conjunto? Estás programando al revés. Use la colección adecuada para el trabajo y
ahórrese

Respuestas:


192

Crea una costumbre 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);
            }
    );
}

Usamos Collectors.collectingAndThenpara construir nuestro deseado Collectorpor

  1. Recolectando nuestros objetos en una Listcon el Collectors.toList()coleccionista.
  2. Aplicando un finalizador adicional al final, que devuelve el elemento individual, o arroja un IllegalStateExceptionif list.size != 1.

Usado como:

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

Luego puede personalizarlo Collectortodo lo que desee, por ejemplo, dar la excepción como argumento en el constructor, ajustarlo para permitir dos valores y más.

Una solución alternativa, posiblemente menos elegante:

Puede usar una 'solución' que implique peek()y una AtomicInteger, pero realmente no debería estar usando eso.

Lo que podrías hacer es simplemente recogerlo en un archivo Listcomo este:

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

24
La guayaba Iterables.getOnlyElementacortaría estas soluciones y proporcionaría mejores mensajes de error. Solo como un consejo para otros lectores que ya usan Google Guava.
Tim Büthe


1
@LonelyNeuron Por favor, no edite mi código. Me pone en una situación en la que necesito validar mi respuesta completa, que escribí hace cuatro años, y simplemente no tengo tiempo para eso en este momento.
skiwi

2
@skiwi: la edición de Lonely fue útil y correcta, así que la volví a instalar después de la revisión. A las personas que visitan esta respuesta hoy no les importa cómo llegó a la respuesta, no necesitan ver la versión anterior y la nueva y una sección Actualizada . Eso hace que su respuesta sea más confusa y menos útil. Es mucho mejor poner las publicaciones en un estado final , y si las personas quieren ver cómo se desarrolló todo, pueden ver el historial de publicaciones.
Martijn Pieters

1
@skiwi: El código en la respuesta es absolutamente lo que has escrito. Todo lo que hizo el editor fue limpiar su publicación, solo eliminando una versión anterior de la singletonCollector()definición obsoleta por la versión que permanece en la publicación, y renombrándola a toSingleton(). Mi experiencia en la transmisión de Java está un poco oxidada, pero el cambio de nombre me parece útil. Revisar este cambio me llevó 2 minutos, como máximo. Si no tiene tiempo para revisar las ediciones, ¿puedo sugerirle que pida a otra persona que haga esto en el futuro, tal vez en la sala de chat de Java ?
Martijn Pieters

118

En aras de la exhaustividad, aquí está el 'one-liner' correspondiente 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, arrojando

  • NoSuchElementException en caso de que la secuencia 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 antes de tiempo y, en cambio, representa el resultado como que Optionalcontiene el único elemento o nada (vacío) si hay cero o múltiples elementos:

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

3
Me gusta el enfoque inicial en esta respuesta. Para fines de personalización, es posible convertir el último get()aorElseThrow()
arin

1
Me gusta la brevedad de este, y el hecho de que evita crear una instancia de Lista innecesaria cada vez que se llama.
LordOfThePigs

83

Las otras respuestas que implican escribir una costumbre Collectorson probablemente más eficientes (como Louis Wasserman's , +1), pero si quieres ser breve, te sugiero lo siguiente:

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

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

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

55
¿De qué sirve limit(2)esta solución? ¿Qué diferencia habría si la lista resultante fuera 2 o 100? Si es mayor que 1.
ryvantage

18
Se detiene inmediatamente si encuentra una segunda coincidencia. Esto es lo que hacen todos los coleccionistas sofisticados, simplemente usando más código. :-)
Stuart Marks

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

1
Javadoc dice parámetro de este límite sobre: maxSize: the number of elements the stream should be limited to. Entonces, ¿no debería ser en .limit(1)lugar de .limit(2)?
alexbt

55
@alexbt La declaración del problema es garantizar que haya exactamente un elemento coincidente (no más, no menos). Después de mi código, uno puede probar result.size()para asegurarse de que sea igual a 1. Si es 2, entonces hay más de una coincidencia, por lo que es un error. Si el código lo hiciera limit(1), más de una coincidencia daría como resultado un solo elemento, que no se puede distinguir de que haya exactamente una coincidencia. Esto pasaría por alto un caso de error que preocupaba al OP.
Stuart Marks

67

Guava proporciona lo MoreCollectors.onlyElement()que hace lo correcto aquí. Pero si tiene que hacerlo usted mismo, puede rodar el suyo Collectorpara esto:

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

... o usando su propio Holdertipo en lugar de AtomicReference. Puedes reutilizar eso Collectortanto como quieras.


SingletonCollector de @ skiwi era más pequeño y más fácil de seguir que esto, por eso le di el cheque. Pero es bueno ver un consenso en la respuesta: una costumbre Collectorera el camino a seguir.
ryvantage

1
Lo suficientemente justo. Estaba apuntando principalmente a la velocidad, no a la concisión.
Louis Wasserman

1
¿Si? ¿Por qué es el tuyo más rápido?
ryvantage

3
Principalmente porque asignar un todo Listes más costoso que una sola referencia mutable.
Louis Wasserman

1
@LouisWasserman, la oración de actualización final sobre MoreCollectors.onlyElement()debería ser la primera (y quizás la única :))
Piotr Findeisen

46

Utilice la guayaba MoreCollectors.onlyElement()( JavaDoc ).

Hace lo que quiere y arroja un IllegalArgumentExceptionsi la secuencia consta de dos o más elementos, y un NoSuchElementExceptionsi la secuencia está vacía.

Uso:

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

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

2
Nota para los usuarios de otros: MoreCollectorses parte del aún inédito (a partir de 2016-12) inédita versión 21.
qerub

2
Esta respuesta debería ir arriba.
Emdadul Sawon

31

La operación de "escotilla de escape" que le permite hacer cosas raras que no son compatibles con las transmisiones es solicitar un 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();
}

La guayaba tiene un método conveniente para tomar Iteratory obtener el único elemento, lanzando si hay cero o múltiples elementos, lo que podría reemplazar las líneas n-1 inferiores aquí.


44
Método de guayaba: Iterators.getOnlyElement (iterador Iterator <T>).
anre

23

Actualizar

Buena sugerencia en el comentario de @Holger:

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

Respuesta original

La excepción se produce Optional#get, pero si tiene más de un elemento, eso no ayudará. Puede recopilar 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<User>(1)))
                  .poll();

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

O podría usar una reducción combinada con un 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();

La reducción esencialmente devuelve:

  • nulo si no se encuentra ningún usuario
  • el usuario si solo se encuentra uno
  • lanza una excepción si se encuentra más de uno

El resultado se envuelve en un opcional.

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


1
Agregaría un elemento de identidad ( null) para evitar el uso get(). Lamentablemente, tu reduceno funciona como crees que funciona, considera uno Streamque tenga nullelementos, tal vez pienses que lo cubriste, pero puedo [User#1, null, User#2, null, User#3]hacerlo, ahora creo que no arrojará una excepción, a menos que me equivoque aquí.
skiwi

2
@Skiwi si hay elementos nulos, el filtro lanzará primero un NPE.
Assylias

2
Puesto que usted sabe que la corriente no puede pasar nulla la función de reducción, eliminación del argumento valor de identidad haría que todo el trato con nullla función obsoleta: reduce( (u,v) -> { throw new IllegalStateException("More than one ID found"); } )tiene el trabajo y aún mejor, ya que devuelve una Optional, elidiendo la necesidad de llamar Optional.ofNullablea la resultado.
Holger

15

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

Entonces para el caso con Userusted tendría:

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

8

Usando reducir

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

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

De esta manera obtienes:

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

6

Creo que de esta manera es más simple:

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

44
Solo se encuentra primero, pero el caso también fue lanzar la Excepción cuando es más de uno
lczapski

5

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

Uso:

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

Devolvemos un Optional, ya que generalmente no podemos asumir Collectionque contiene exactamente un elemento. Si ya sabe que este es el caso, llame al:

User user = result.orElseThrow();

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



1

Podemos usar RxJava ( biblioteca de extensión reactiva muy poderosa )

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

El operador único genera una excepción si no se encuentra ningún usuario o más de uno.


Sin embargo, la respuesta correcta, inicializar una secuencia o colección de bloqueo probablemente no sea muy barata (en términos de recursos).
Karl Richter

1

Como Collectors.toMap(keyMapper, valueMapper)utiliza una fusión de lanzamiento para manejar múltiples entradas con la misma clave, es fácil:

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

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


1
Buena solución! Y si lo haces .collect(Collectors.toMap(user -> "", Function.identity())).get(""), tienes un comportamiento más genérico.
glglgl

1

Estoy usando esos dos coleccionistas:

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

¡Ordenado! onlyOne()tira IllegalStateExceptionpara> 1 elementos, y NoSuchElementException` (in Optional::get) para 0 elementos.
simon04

@ simon04 Se podría sobrecargar los métodos para tomar una Supplierde (Runtime)Exception.
Xavier Dury

1

Si no le importa usar una biblioteca de terceros, SequenceMdesde cyclops-streams (y LazyFutureStreamdesde simple-react ), ambos tienen operadores opcionales únicos y únicos.

singleOptional()lanza una excepción si hay 0o más de 1elementos 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 el 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

Divulgación: soy el autor de ambas bibliotecas.


0

Seguí el enfoque directo y simplemente implementé la cosa:

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


} 

con la prueba JUnit:

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

}

Esta implementación no es segura para subprocesos.


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

55
Si bien este código puede resolver la pregunta, incluir una explicación de cómo y por qué esto resuelve el problema realmente ayudaría a mejorar la calidad de su publicación, y probablemente resultaría en más votos positivos. Recuerde que está respondiendo la pregunta para los lectores en el futuro, no solo la persona que pregunta ahora. Edite su respuesta para agregar explicaciones y dar una indicación de qué limitaciones y supuestos se aplican.
David Buck

-2

¿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.

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


3
Se dijo que count()no es bueno usarlo porque es una operación terminal.
ryvantage

Si esto realmente es una cita, por favor agregue sus fuentes
Neuron
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.