Obtenha o último elemento de Stream / List em uma linha

118

Como posso obter o último elemento de um fluxo ou lista no código a seguir?

Onde data.careasestá um List<CArea>:

CArea first = data.careas.stream()
                  .filter(c -> c.bbox.orientationHorizontal).findFirst().get();

CArea last = data.careas.stream()
                 .filter(c -> c.bbox.orientationHorizontal)
                 .collect(Collectors.toList()).; //how to?

Como você pode ver, pegar o primeiro elemento, com uma certa filter, não é difícil.

No entanto, obter o último elemento em uma linha é uma verdadeira dor:

  • Parece que não posso obtê-lo diretamente de a Stream. (Só faria sentido para fluxos finitos)
  • Também parece que você não pode obter coisas como first()e last()da Listinterface, o que é realmente uma dor.

Não vejo nenhum argumento para não fornecer um método first()e last()na Listinterface, pois os elementos lá são ordenados e, além disso, o tamanho é conhecido.

Mas de acordo com a resposta original: Como obter o último elemento de um finito Stream?

Pessoalmente, isso é o mais próximo que consegui:

int lastIndex = data.careas.stream()
        .filter(c -> c.bbox.orientationHorizontal)
        .mapToInt(c -> data.careas.indexOf(c)).max().getAsInt();
CArea last = data.careas.get(lastIndex);

No entanto, envolve o uso de um indexOfem todos os elementos, o que geralmente você não deseja, pois pode prejudicar o desempenho.

skiwi
fonte
10
O Guava fornece o Iterables.getLastque leva Iterable, mas é otimizado para funcionar List. Uma implicância é que ele não tem getFirst. A StreamAPI em geral é terrivelmente anal, omitindo muitos métodos de conveniência. O LINQ do C #, por contraste, tem o prazer de fornecer .Last()e até mesmo .Last(Func<T,Boolean> predicate), embora ofereça suporte a Enumerables infinitos também.
Aleksandr Dubinsky
@AleksandrDubinsky votou positivamente, mas uma nota para os leitores. StreamA API não é totalmente comparável, LINQjá que ambas são feitas em um paradigma muito diferente. Não é pior ou melhor, é apenas diferente. E, definitivamente, alguns métodos estão ausentes, não porque os desenvolvedores de oráculos sejam incompetentes ou mesquinhos :)
dia
1
Para um verdadeiro one-liner, este segmento pode ser útil.
quantum

Respostas:

185

É possível obter o último elemento com o método Stream :: reduce . A lista a seguir contém um exemplo mínimo para o caso geral:

Stream<T> stream = ...; // sequential or parallel stream
Optional<T> last = stream.reduce((first, second) -> second);

Essas implementações funcionam para todos os fluxos ordenados (incluindo fluxos criados a partir de listas ). Para fluxos não ordenados , por razões óbvias não é especificado qual elemento será retornado.

A implementação funciona para fluxos sequenciais e paralelos . Isso pode ser surpreendente à primeira vista e, infelizmente, a documentação não afirma isso explicitamente. No entanto, é um recurso importante dos streams, e tento esclarecê-lo:

  • O Javadoc para o método Stream :: reduce afirma que " não está restrito a executar sequencialmente " .
  • O Javadoc também requer que a "função acumuladora deve ser uma função associativa , não interferente e sem estado para combinar dois valores" , o que é obviamente o caso da expressão lambda (first, second) -> second.
  • O Javadoc para operações de redução declara: "As classes de streams têm várias formas de operações de redução geral, chamadas reduce () e collect () [..]" e "uma operação de redução construída corretamente é inerentemente paralelizável , desde que as funções ) usados ​​para processar os elementos são associativos e sem estado . "

A documentação para os coletores intimamente relacionados é ainda mais explícita: "Para garantir que as execuções sequenciais e paralelas produzam resultados equivalentes , as funções do coletor devem satisfazer as restrições de identidade e associatividade ."


De volta à pergunta original: o código a seguir armazena uma referência ao último elemento na variável laste lança uma exceção se o fluxo estiver vazio. A complexidade é linear no comprimento do fluxo.

CArea last = data.careas
                 .stream()
                 .filter(c -> c.bbox.orientationHorizontal)
                 .reduce((first, second) -> second).get();
nosid
fonte
Boa, obrigado! A propósito, você sabe se é possível omitir um nome (talvez usando a _ou semelhante) nos casos em que não precisa de um parâmetro? Então seria:.reduce((_, current) -> current) se apenas essa sintaxe fosse válida.
skiwi
2
@skiwi você pode usar qualquer nome de variável válido, por exemplo: .reduce(($, current) -> current)ou .reduce((__, current) -> current)(sublinhado duplo).
assylias
2
Tecnicamente, pode não funcionar para nenhum stream. A documentação que você aponta, bem como para Stream.reduce(BinaryOperator<T>)não faz nenhuma menção se reduceobedece a ordem de encontro, e uma operação de terminal é livre para ignorar a ordem de encontro, mesmo se o fluxo for ordenado. À parte, a palavra "comutativo" não aparece nos javadocs do Stream, portanto, sua ausência não nos diz muito.
Aleksandr Dubinsky
2
@AleksandrDubinsky: Exatamente, a documentação não menciona comutativa , pois não é relevante para a operação de redução . A parte importante é: "[..] Uma operação de redução construída corretamente é inerentemente paralelizável, desde que as funções usadas para processar os elementos sejam associativas [..]."
nosid
2
@Aleksandr Dubinsky: claro, não é uma “questão teórica da especificação”. Faz a diferença entre reduce((a,b)->b)ser uma solução correta para obter o último elemento (de um fluxo ordenado, é claro) ou não. A declaração de Brian Goetz faz questão, além disso, a documentação da API afirma que reduce("", String::concat)é uma solução ineficiente, mas correta para concatenação de strings, o que implica na manutenção da ordem do encontro. A intenção é bem conhecida, a documentação tem que ser atualizada.
Holger
42

Se você tem uma coleção (ou mais um Iterable geral), pode usar o Google Guava's

Iterables.getLast(myIterable)

como oneliner útil.

Peti
fonte
1
E você pode facilmente converter um fluxo em um iterável:Iterables.getLast(() -> data.careas.stream().filter(c -> c.bbox.orientationHorizontal).iterator())
shmosel
10

Um forro (sem necessidade de fluxo;):

Object lastElement = list.get(list.size()-1);
nimo23
fonte
30
Se a lista estiver vazia, este código será lançado ArrayIndexOutOfBoundsException.
Dragon
8

Guava tem método dedicado para este caso:

Stream<T> stream = ...;
Optional<T> lastItem = Streams.findLast(stream);

É equivalente a, stream.reduce((a, b) -> b)mas os criadores afirmam que tem um desempenho muito melhor.

Da documentação :

O tempo de execução desse método será entre O (log n) e O (n), com melhor desempenho em fluxos divisíveis de forma eficiente.

Vale a pena mencionar que se o stream estiver desordenado este método se comporta como findAny().

k13i
fonte
1
@ZhekaKozlov meio que ... Holger mostrou algumas falhas aqui
Eugene
0

Se você precisar obter o último número N de elementos. O fechamento pode ser usado. O código abaixo mantém uma fila externa de tamanho fixo até que o fluxo chegue ao fim.

    final Queue<Integer> queue = new LinkedList<>();
    final int N=5;
    list.stream().peek((z) -> {
        queue.offer(z);
        if (queue.size() > N)
            queue.poll();
    }).count();

Outra opção poderia ser usar a operação de redução usando a identidade como uma fila.

    final int lastN=3;
    Queue<Integer> reduce1 = list.stream()
    .reduce( 
        (Queue<Integer>)new LinkedList<Integer>(), 
        (m, n) -> {
            m.offer(n);
            if (m.size() > lastN)
               m.poll();
            return m;
    }, (m, n) -> m);

    System.out.println("reduce1 = " + reduce1);
Himanshu Ahire
fonte
-1

Você também pode usar a função skip () conforme abaixo ...

long count = data.careas.count();
CArea last = data.careas.stream().skip(count - 1).findFirst().get();

é super simples de usar.

Parag Vaidya
fonte
Nota: você não deve confiar no "salto" do stream ao lidar com coleções enormes (milhões de entradas), porque o "salto" é implementado pela iteração de todos os elementos até que o enésimo número seja alcançado. Tentei. Fiquei muito decepcionado com o desempenho, em comparação com uma operação simples de obter por índice.
java.is.for.desktop
1
também se a lista estiver vazia, ele lançaráArrayIndexOutOfBoundsException
Jindra Vysocký