suma - stream java 8 ejemplo
Filtre Java Stream a 1 y solo 1 elemento (15)
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 es lanzada por Optional#get
, pero si tiene más de un elemento que 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 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 recopilar solo a una colección, verificar que su tamaño sea 1 y obtener el único elemento.
Estoy tratando de usar Java 8 Stream
para encontrar elementos en LinkedList
. Sin embargo, quiero garantizar que haya 1 y solo 1 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 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?
Como Collectors.toMap(keyMapper, valueMapper)
usa una fusión arrojadiza 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 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
.
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<User> user1 = users.stream()
.filter(user -> user.getId() == 1)
.collect(Collectors.reducing((a, b) -> null));
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);
}
Fui con el enfoque directo y acabo de implementar 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 insegura.
Guava proporciona MoreCollectors.onlyElement()
que hace lo correcto aquí. Pero si tiene que hacerlo usted mismo, puede lanzar su propio recopilador para 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 utilizando su propio tipo de Holder
lugar de AtomicReference
. Puedes reutilizar ese Collector
tanto como quieras.
Guava tiene un colector para esto llamado MoreCollectors.onlyElement ()
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.
La operación "escape hatch" que le permite hacer cosas raras que no son soportadas por streams es pedir 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 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í.
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 operator único arroja una excepción si no se encuentra un usuario o más de un usuario.
Si no te importa usar una biblioteca de terceros, SequenceM de cyclops-streams (y LazyFutureStream de simple-react ) 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 flujo, 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 return Opcional.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.
Técnicamente hay una "solución alternativa" fea que implica peek()
y un AtomicInteger
, pero realmente no deberías estar usando eso.
Lo que hago en estos casos es simplemente recopilarlo en una lista, como esta:
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);
No conozco una manera de hacer esto en la API, mientras tanto, trabajaré en otro ejemplo que involucre un elemento personalizado.
Actualización , debe crear su propio Collector
para esto:
public static <T> Collector<T, List<T>, T> singletonCollector() {
return Collector.of(
ArrayList::new,
List::add,
(left, right) -> { left.addAll(right); return left; },
list -> {
if (list.size() != 1) {
throw new IllegalStateException();
}
return list.get(0);
}
);
}
Lo que hace es:
- Imita el
Collectors.toList()
. - Aplica un finalizador adicional al final, que arroja una excepción, o si no es una excepción, devuelve el primer elemento de la lista.
Usado como:
User resultUser = users.stream()
.filter(user -> user.getId() > 0)
.collect(singletonCollector());
A continuación, puede personalizar este singletonCollector
tanto como desee, por ejemplo, especifique la excepción como argumento en el constructor, modifíquela para permitir dos valores y más.
Nueva actualización , revisé mi respuesta anterior una vez más para singletonCollector()
, realmente se puede obtener de esta manera:
public static <T> Collector<T, ?, T> singletonCollector() {
return Collectors.collectingAndThen(
Collectors.toList(),
list -> {
if (list.size() != 1) {
throw new IllegalStateException();
}
return list.get(0);
}
);
}
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<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 el User
usted tendría:
User match = users.stream().filter((user) -> user.getId() < 0).reduce(thereCanBeOnlyOne()).get();
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.
Ejemplo:
import static com.google.common.collect.MoreCollectors.onlyElement;
User match =
users.stream().filter((user) -> user.getId() < 0).collect(onlyElement());
ACTUALIZACIÓN (2017-05-30):
Este método ahora está disponible públicamente a partir de Guava 21. Aquí están los últimos JavaDocs en este método.
Wow, tal complejidad! :-) Las otras respuestas que implican escribir un Collector personalizado son probablemente más eficientes (como Louis Wasserman''s , +1), pero si quieres brevedad, te sugiero lo siguiente:
List<User> result = users.stream()
.filter(user -> user.getId() == 1)
.limit(2)
.collect(Collectors.toList());
Luego verifica el tamaño de la lista de resultados.