Encontre o primeiro elemento por predicado

504

Acabei de começar a jogar com o Java 8 lambdas e estou tentando implementar algumas das coisas com as quais estou acostumado nas linguagens funcionais.

Por exemplo, a maioria das linguagens funcionais possui algum tipo de função find que opera em seqüências ou listas que retornam o primeiro elemento, para o qual o predicado é true. A única maneira de conseguir isso no Java 8 é:

lst.stream()
    .filter(x -> x > 5)
    .findFirst()

No entanto, isso me parece ineficiente, pois o filtro varrerá a lista inteira, pelo menos para minha compreensão (o que pode estar errado). Existe uma maneira melhor?

siki
fonte
53
Não é ineficiente, a implementação do Java 8 Stream é avaliada com preguiça; portanto, o filtro é aplicado apenas à operação do terminal. Mesma pergunta aqui: stackoverflow.com/questions/21219667/stream-and-lazy-evaluation
Marek Gregor
1
Legal. Era o que eu esperava que fizesse. Caso contrário, teria sido um grande fracasso no design.
siki
2
Se a sua intenção é realmente verificar se a lista contém esse elemento (e não apenas o primeiro de vários),.
Joachim Lous
Comparado a um ciclo forEach simples, isso criaria muitos objetos no heap e dezenas de chamadas de método dinâmico. Embora isso nem sempre afete a linha de fundo em seus testes de desempenho, nos hot spots, faz diferença abster-se do uso trivial do Stream e de construções semelhantes de peso pesado.
Agoston Horvath

Respostas:

720

Não, o filtro não varre o fluxo inteiro. É uma operação intermediária, que retorna um fluxo lento (na verdade, todas as operações intermediárias retornam um fluxo lento). Para convencê-lo, você pode simplesmente fazer o seguinte teste:

List<Integer> list = Arrays.asList(1, 10, 3, 7, 5);
int a = list.stream()
            .peek(num -> System.out.println("will filter " + num))
            .filter(x -> x > 5)
            .findFirst()
            .get();
System.out.println(a);

Quais saídas:

will filter 1
will filter 10
10

Você vê que apenas os dois primeiros elementos do fluxo são realmente processados.

Então você pode seguir sua abordagem, o que é perfeitamente correto.

Alexis C.
fonte
37
Como observação, usei get();aqui porque sei quais valores alimentam o pipeline de fluxo e, portanto, haverá um resultado. Na prática, você não deve usar get();, mas orElse()/ orElseGet()/ orElseThrow()(para um erro mais significativo em vez de um NSEE), pois talvez você não saiba se as operações aplicadas ao pipeline de fluxo resultarão em um elemento.
Alexis C.
31
.findFirst().orElse(null);por exemplo
Gondy 11/11
20
Não use orElse null. Isso deve ser um anti-padrão. Está tudo incluído no opcional. Por que você deve arriscar um NPE? Eu acho que lidar com o opcional é a melhor maneira. Apenas teste o opcional com isPresent () antes de usá-lo.
precisa saber é o seguinte
@ BeJay eu não entendo. o que devo usar em vez de orElse?
John Henckel
3
@ JohnHenckel Acho que o que BeJay significa é que você deve deixá-lo como um Optionaltipo, que é o que .findFirstretorna. Um dos usos do Opcional é ajudar os desenvolvedores a evitar lidar com nullseg em vez de verificar myObject != null, você pode verificar myOptional.isPresent()ou usar outras partes da interface Opcional. Isso tornou mais claro?
AMTerp
102

No entanto, isso me parece ineficiente, pois o filtro varrerá a lista inteira

Não, não vai - ele irá "quebrar" assim que o primeiro elemento que satisfizer o predicado for encontrado. Você pode ler mais sobre preguiça no pacote stream javadoc , em particular (ênfase minha):

Muitas operações de fluxo, como filtragem, mapeamento ou remoção duplicada, podem ser implementadas com preguiça, expondo oportunidades de otimização. Por exemplo, "encontre a primeira String com três vogais consecutivas" não precisa examinar todas as strings de entrada. As operações de fluxo são divididas em operações intermediárias (produção de fluxo) e operações terminais (produção de valor ou efeito colateral). Operações intermediárias são sempre preguiçosas.

o que
fonte
5
Esta resposta foi mais informativa para mim e explica o porquê, não apenas como. Eu nunca novas operações intermediárias são sempre preguiçosas; Os fluxos Java continuam a me surpreender.
Kevinarpe 6/07
30
return dataSource.getParkingLots()
                 .stream()
                 .filter(parkingLot -> Objects.equals(parkingLot.getId(), id))
                 .findFirst()
                 .orElse(null);

Eu tive que filtrar apenas um objeto de uma lista de objetos. Então eu usei isso, espero que ajude.

CodeShadow
fonte
MELHOR: como procuramos um valor de retorno booleano, podemos fazer isso melhor adicionando verificação nula: return dataSource.getParkingLots (). Stream (). Filter (parkingLot -> Objects.equals (parkingLot.getId (), id)) .findFirst (). orElse (null)! = null;
Shreedhar Bhat
1
@shreedharbhat Você não precisa fazer isso .orElse(null) != null. Em vez disso, use as APIs opcionais, .isPresentie .findFirst().isPresent().
AMTerp
@shreedharbhat antes de tudo, o OP não estava procurando um valor de retorno booleano. Em segundo lugar, se fossem, seria mais limpo escrever.stream().map(ParkingLot::getId).anyMatch(Predicate.isEqual(id))
AjaxLeung
13

Além da resposta de Alexis C , se você estiver trabalhando com uma lista de matrizes, na qual não tem certeza se o elemento que está procurando existe, use-o.

Integer a = list.stream()
                .peek(num -> System.out.println("will filter " + num))
                .filter(x -> x > 5)
                .findFirst()
                .orElse(null);

Então você pode simplesmente verificar se um é null.

Ifesinachi Bryan
fonte
1
Você deve corrigir o seu exemplo. Você não pode atribuir nulo a um int simples. stackoverflow.com/questions/2254435/can-an-int-be-null-in-java
RubioRic
Eu editei sua postagem. 0 (zero) pode ser um resultado válido quando você estiver pesquisando em uma lista de números inteiros. Tipo de variável substituída por Inteiro e valor padrão por nulo.
RubioRic
0

import org.junit.Test;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

// Stream is ~30 times slower for same operation...
public class StreamPerfTest {

    int iterations = 100;
    List<Integer> list = Arrays.asList(1, 10, 3, 7, 5);


    // 55 ms
    @Test
    public void stream() {

        for (int i = 0; i < iterations; i++) {
            Optional<Integer> result = list.stream()
                    .filter(x -> x > 5)
                    .findFirst();

            System.out.println(result.orElse(null));
        }
    }

    // 2 ms
    @Test
    public void loop() {

        for (int i = 0; i < iterations; i++) {
            Integer result = null;
            for (Integer walk : list) {
                if (walk > 5) {
                    result = walk;
                    break;
                }
            }
            System.out.println(result);
        }
    }
}
ilusões
fonte
0

Resposta aprimorada de uma linha: se você estiver procurando por um valor de retorno booleano, podemos fazer isso melhor adicionando isPresent:

return dataSource.getParkingLots().stream().filter(parkingLot -> Objects.equals(parkingLot.getId(), id)).findFirst().isPresent();
shreedhar bhat
fonte
Se você deseja um valor de retorno booleano, use anyMatch
AjaxLeung