¿Cómo comprobar si una secuencia de Java 8 está vacía?

96

¿Cómo puedo verificar si a Streamestá vacío y lanzar una excepción si no lo está, como una operación no terminal?

Básicamente, estoy buscando algo equivalente al código a continuación, pero sin materializar el flujo intermedio. En particular, la verificación no debe ocurrir antes de que la secuencia sea consumida realmente por una operación de terminal.

public Stream<Thing> getFilteredThings() {
    Stream<Thing> stream = getThings().stream()
                .filter(Thing::isFoo)
                .filter(Thing::isBar);
    return nonEmptyStream(stream, () -> {
        throw new RuntimeException("No foo bar things available")   
    });
}

private static <T> Stream<T> nonEmptyStream(Stream<T> stream, Supplier<T> defaultValue) {
    List<T> list = stream.collect(Collectors.toList());
    if (list.isEmpty()) list.add(defaultValue.get());
    return list.stream();
}
Cefalópodo
fuente
23
No puedes tener tu pastel y comértelo también, y literalmente, en este contexto. Tienes que consumir la transmisión para saber si está vacía. Ese es el punto de la semántica de Stream (pereza).
Marko Topolnik
Se consumirá eventualmente, en este punto debería ocurrir la verificación
Cephalopod
11
Para comprobar que el flujo no está vacío, debe intentar consumir al menos un elemento. En ese momento el arroyo ha perdido su "virginidad" y no se puede volver a consumir desde el principio.
Marko Topolnik

Respuestas:

24

Si puede vivir con capacidades paralelas limitadas, la siguiente solución funcionará:

private static <T> Stream<T> nonEmptyStream(
    Stream<T> stream, Supplier<RuntimeException> e) {

    Spliterator<T> it=stream.spliterator();
    return StreamSupport.stream(new Spliterator<T>() {
        boolean seen;
        public boolean tryAdvance(Consumer<? super T> action) {
            boolean r=it.tryAdvance(action);
            if(!seen && !r) throw e.get();
            seen=true;
            return r;
        }
        public Spliterator<T> trySplit() { return null; }
        public long estimateSize() { return it.estimateSize(); }
        public int characteristics() { return it.characteristics(); }
    }, false);
}

Aquí hay un código de ejemplo que lo usa:

List<String> l=Arrays.asList("hello", "world");
nonEmptyStream(l.stream(), ()->new RuntimeException("No strings available"))
  .forEach(System.out::println);
nonEmptyStream(l.stream().filter(s->s.startsWith("x")),
               ()->new RuntimeException("No strings available"))
  .forEach(System.out::println);

El problema con la ejecución en paralelo (eficiente) es que admitir la división de la Spliteratorrequiere una forma segura de subprocesos para notar si alguno de los fragmentos ha visto algún valor de una manera segura para subprocesos. Luego, el último de los fragmentos en ejecución tryAdvancedebe darse cuenta de que es el último (y tampoco pudo avanzar) en lanzar la excepción apropiada. Entonces no agregué soporte para dividir aquí.

Holger
fuente
33

Las otras respuestas y comentarios son correctos en el sentido de que para examinar el contenido de un flujo, se debe agregar una operación de terminal, "consumiendo" así el flujo. Sin embargo, se puede hacer esto y convertir el resultado de nuevo en una secuencia, sin almacenar en búfer todo el contenido de la secuencia. Aqui hay un par de ejemplos:

static <T> Stream<T> throwIfEmpty(Stream<T> stream) {
    Iterator<T> iterator = stream.iterator();
    if (iterator.hasNext()) {
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false);
    } else {
        throw new NoSuchElementException("empty stream");
    }
}

static <T> Stream<T> defaultIfEmpty(Stream<T> stream, Supplier<T> supplier) {
    Iterator<T> iterator = stream.iterator();
    if (iterator.hasNext()) {
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false);
    } else {
        return Stream.of(supplier.get());
    }
}

Básicamente, convierta la secuencia en un Iteratorpara invocarla hasNext()y, si es cierto, convierta la parte Iteratorposterior en un Stream. Esto es ineficiente porque todas las operaciones posteriores en la secuencia pasarán por el Iterador hasNext()ynext() métodos , lo que también implica que la secuencia se procesa de manera efectiva de forma secuencial (incluso si luego se convierte en paralelo). Sin embargo, esto le permite probar la transmisión sin almacenar en búfer todos sus elementos.

Probablemente haya una forma de hacer esto usando un en Spliteratorlugar de un Iterator. Esto potencialmente permite que el flujo devuelto tenga las mismas características que el flujo de entrada, incluida la ejecución en paralelo.

Stuart Marks
fuente
1
No creo que exista una solución mantenible que admita un procesamiento paralelo eficiente, ya que es difícil admitir la división, sin embargo, tiene estimatedSizee characteristicsincluso podría mejorar el rendimiento de un solo subproceso. Simplemente sucedió que escribí la Spliteratorsolución mientras usted publicaba la Iteratorsolución ...
Holger
3
Puede pedirle al flujo un Spliterator, llamar a tryAdvance (lambda) donde su lambda captura todo lo que se le pasa y luego devolver un Spliterator que delega casi todo al Spliterator subyacente, excepto que pega el primer elemento de nuevo al primer fragmento ( y corrige el resultado de estimadoSize).
Brian Goetz
1
@BrianGoetz Sí, eso fue lo que pensé, pero todavía no me he molestado en pasar por el trabajo preliminar de manejar todos esos detalles.
Stuart Marks
3
@Brian Goetz: Eso es lo que quise decir con "demasiado complicado". Llamar tryAdvanceantes que el Streamhace convierte la naturaleza perezosa del Streamen una corriente "parcialmente perezosa". También implica que buscar el primer elemento ya no es una operación en paralelo, ya que tienes que dividir primero y hacerlo tryAdvanceen las partes divididas al mismo tiempo para hacer una operación en paralelo real, según tengo entendido. Si la única operación del terminal es findAnyo similar, destruiría toda la parallel()solicitud.
Holger
2
Por lo tanto, para un soporte paralelo completo, no debe llamar tryAdvanceantes de que lo haga la transmisión y debe envolver cada parte dividida en un proxy y recopilar la información "hasAny" de todas las operaciones simultáneas por su cuenta y asegurarse de que la última operación simultánea arroje la excepción deseada si el el arroyo estaba vacío. Un montón de cosas ...
Holger
19

Esto puede ser suficiente en muchos casos.

stream.findAny().isPresent()
Kenglxn
fuente
15

Debe realizar una operación de terminal en la secuencia para que se aplique cualquiera de los filtros. Por lo tanto, no puede saber si estará vacío hasta que lo consuma.

Lo mejor que puede hacer es finalizar la transmisión con un findAny() operación de terminal, que se detendrá cuando encuentre cualquier elemento, pero si no hay ninguno, tendrá que iterar sobre toda la lista de entrada para averiguarlo.

Esto solo lo ayudaría si la lista de entrada tiene muchos elementos, y uno de los primeros pasa los filtros, ya que solo un pequeño subconjunto de la lista tendría que consumirse antes de saber que Stream no está vacío.

Por supuesto, aún tendrá que crear una nueva secuencia para producir la lista de salida.

Eran
fuente
7
Hay anyMatch(alwaysTrue()), creo que es el más cercano a hasAny.
Marko Topolnik
1
@MarkoTopolnik Acabo de verificar la referencia; lo que tenía en mente era findAny (), aunque anyMatch () también funcionaría.
Eran
3
anyMatch(alwaysTrue())coincide perfectamente con la semántica prevista de su hasAny, dándole unaboolean lugar de Optional<T>--- pero estamos dividiendo los pelos aquí :)
Marko Topolnik
1
Note alwaysTruees un predicado de Guayaba.
Jean-François Savard
10
anyMatch(e -> true)luego.
FBB
5

Creo que debería ser suficiente para mapear un booleano

En código esto es:

boolean isEmpty = anyCollection.stream()
    .filter(p -> someFilter(p)) // Add my filter
    .map(p -> Boolean.TRUE) // For each element after filter, map to a TRUE
    .findAny() // Get any TRUE
    .orElse(Boolean.FALSE); // If there is no match return false
Luis Roberto
fuente
1
Si esto es todo lo que necesita, la respuesta de kenglxn es mejor.
Dominykas Mostauskis
es inútil, duplica Collection.isEmpty ()
Krzysiek
@Krzysiek no es inútil si necesita filtrar la colección. Sin embargo, estoy de acuerdo con Dominykas en que la respuesta de kenglxn es mejor
Hertzu
Es porque también se duplicaStream.anyMatch()
Krzysiek
4

Siguiendo la idea de Stuart, esto podría hacerse con algo Spliteratorasí:

static <T> Stream<T> defaultIfEmpty(Stream<T> stream, Stream<T> defaultStream) {
    final Spliterator<T> spliterator = stream.spliterator();
    final AtomicReference<T> reference = new AtomicReference<>();
    if (spliterator.tryAdvance(reference::set)) {
        return Stream.concat(Stream.of(reference.get()), StreamSupport.stream(spliterator, stream.isParallel()));
    } else {
        return defaultStream;
    }
}

Creo que esto funciona con stream.spliterator()transmisiones paralelas ya que la operación terminará la transmisión y luego la reconstruirá según sea necesario

En mi caso de uso, necesitaba un valor predeterminado en Streamlugar de uno predeterminado. eso es bastante fácil de cambiar si esto no es lo que necesita

phoenix7360
fuente
No puedo averiguar si esto afectaría significativamente el rendimiento con flujos paralelos. Probablemente debería probarlo si esto es un requisito
phoenix7360
Lo siento, no me di cuenta de que @Holger también tenía una solución con SpliteratorMe pregunto cómo se comparan los dos.
phoenix7360