Como verificar se um Java 8 Stream está vazio?

95

Como posso verificar se a Streamestá vazio e lançar uma exceção se não estiver, como uma operação não terminal?

Basicamente, estou procurando algo equivalente ao código abaixo, mas sem materializar o fluxo intermediário. Em particular, a verificação não deve ocorrer antes que o fluxo seja realmente consumido por uma operação 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ópode
fonte
23
Você não pode ter seu bolo e comê-lo - e literalmente, neste contexto. Você tem que consumir o fluxo para descobrir se está vazio. Esse é o ponto da semântica do Stream (preguiça).
Marko Topolnik
Será consumido eventualmente, neste momento a verificação deve ocorrer
Cefalópode
11
Para verificar se o fluxo não está vazio, você deve tentar consumir pelo menos um elemento. Nesse ponto, o fluxo perdeu sua "virgindade" e não pode ser consumido novamente desde o início.
Marko Topolnik

Respostas:

24

Se você pode viver com capacidades paralelas limitadas, a seguinte solução 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);
}

Aqui está um exemplo de código usando-o:

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

O problema com a execução paralela (eficiente) é que o suporte à divisão do Spliteratorrequer uma maneira segura de thread para perceber se algum dos fragmentos viu algum valor de maneira segura. Então, o último dos fragmentos em execução tryAdvanceprecisa perceber que é o último (e também não pode avançar) a lançar a exceção apropriada. Portanto, não adicionei suporte para divisão aqui.

Holger
fonte
33

As outras respostas e comentários estão corretos no sentido de que para examinar o conteúdo de um fluxo, é necessário adicionar uma operação de terminal, "consumindo" assim o fluxo. No entanto, pode-se fazer isso e transformar o resultado de volta em um fluxo, sem armazenar todo o conteúdo do fluxo. Aqui estão alguns exemplos:

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

Basicamente, transforme o stream em um Iteratorpara chamá hasNext()-lo e, se verdadeiro, transforme as Iteratorcostas em um Stream. Isso é ineficiente porque todas as operações subsequentes no fluxo passarão pelos métodos hasNext()e do Agente Iterativo next(), o que também implica que o fluxo é efetivamente processado sequencialmente (mesmo se posteriormente for tornado paralelo). No entanto, isso permite que você teste o fluxo sem armazenar em buffer todos os seus elementos.

Provavelmente existe uma maneira de fazer isso usando um em Spliteratorvez de um Iterator. Isso permite que o fluxo retornado tenha as mesmas características do fluxo de entrada, incluindo a execução em paralelo.

Stuart Marks
fonte
1
Não acho que exista uma solução sustentável que ofereça suporte ao processamento paralelo eficiente, pois é difícil oferecer suporte à divisão, no entanto, tendo estimatedSizee characteristicspode até melhorar o desempenho de thread único. Acontece que eu escrevi a Spliteratorsolução enquanto você estava postando a Iteratorsolução ...
Holger
3
Você pode pedir ao stream por um Spliterator, chamar tryAdvance (lambda) onde seu lambda captura qualquer coisa passada a ele e, em seguida, retornar um Spliterator que delega quase tudo ao Spliterator subjacente, exceto que cola o primeiro elemento de volta no primeiro pedaço ( e corrige o resultado de estimativaSize).
Brian Goetz
1
@BrianGoetz Sim, foi o que pensei, só não me preocupei em passar pelo trabalho de lidar com todos esses detalhes.
Stuart Marks
3
@Brian Goetz: Isso é o que eu quis dizer com “muito complicado”. Chamar tryAdvanceantes do Streamfaz isso transforma a natureza preguiçosa do Streamem um fluxo "parcialmente preguiçoso". Isso também implica que a busca pelo primeiro elemento não é mais uma operação paralela, já que você precisa dividir primeiro e fazer tryAdvancenas partes divididas simultaneamente para fazer uma operação paralela real, pelo que eu entendi. Se a única operação do terminal for findAnyou semelhante, isso destruiria toda a parallel()solicitação.
Holger
2
Portanto, para obter suporte paralelo completo, você não deve chamar tryAdvanceantes do fluxo e deve envolver cada parte dividida em um proxy e reunir as informações "hasAny" de todas as operações simultâneas por conta própria e garantir que a última operação simultânea emita a exceção desejada se o o riacho estava vazio. Muitas coisas ...
Holger
18

Isso pode ser suficiente em muitos casos

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

Você deve realizar uma operação de terminal no Stream para que qualquer um dos filtros seja aplicado. Portanto, você não pode saber se ele estará vazio até que você o consuma.

O melhor que você pode fazer é encerrar o Stream com uma findAny()operação de terminal, que irá parar quando encontrar qualquer elemento, mas se não houver nenhum, terá que iterar em toda a lista de entrada para descobrir isso.

Isso só ajudaria se a lista de entrada tivesse muitos elementos e um dos primeiros passasse pelos filtros, já que apenas um pequeno subconjunto da lista teria que ser consumido antes de você saber que o Fluxo não está vazio.

Claro, você ainda terá que criar um novo fluxo para produzir a lista de saída.

Eran
fonte
7
anyMatch(alwaysTrue()), eu acho que é o mais próximo de hasAny.
Marko Topolnik
1
@MarkoTopolnik Acabei de verificar a referência - o que eu tinha em mente era findAny (), embora anyMatch () também funcionasse.
Eran
3
anyMatch(alwaysTrue())corresponde perfeitamente à semântica pretendida do seu hasAny, dando-lhe um em booleanvez de Optional<T>--- mas estamos dividindo os cabelos aqui :)
Marko Topolnik
1
Note alwaysTrueé um predicado Goiaba.
Jean-François Savard
10
anyMatch(e -> true)então.
FBB
5

Eu acho que deve ser o suficiente para mapear um booleano

No código é:

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
fonte
1
Se isso é tudo de que você precisa, a resposta de kenglxn é melhor.
Dominykas Mostauskis
é inútil, ele duplica Collection.isEmpty ()
Krzysiek
@Krzysiek não é inútil se você precisar filtrar a coleção. No entanto, concordo com Dominykas sobre a resposta de kenglxn é melhor
Hertzu
É porque também é duplicadoStream.anyMatch()
Krzysiek
4

Seguindo a ideia de Stuart, isso poderia ser feito da Spliteratorseguinte forma:

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

Acho que isso funciona com fluxos paralelos, pois a stream.spliterator()operação encerrará o fluxo e, em seguida, o reconstruirá conforme necessário

Em meu caso de uso, eu precisava de um valor padrão em Streamvez de um valor padrão. isso é muito fácil de mudar se não for o que você precisa

phoenix7360
fonte
Não consigo descobrir se isso afetaria significativamente o desempenho com fluxos paralelos. Provavelmente deveria testá-lo se este for um requisito
phoenix7360
Desculpe não ter percebido que o @Holger também tinha uma solução com SpliteratorGostaria de saber como os dois se comparam.
phoenix7360