Trouver le premier élément par prédicat

504

Je viens de commencer à jouer avec les lambdas Java 8 et j'essaie d'implémenter certaines des choses auxquelles je suis habitué dans les langages fonctionnels.

Par exemple, la plupart des langages fonctionnels ont une sorte de fonction de recherche qui opère sur des séquences, ou des listes qui renvoie le premier élément, pour lequel le prédicat est true. La seule façon que je peux voir pour y parvenir dans Java 8 est:

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

Cependant, cela me semble inefficace, car le filtre analysera toute la liste, du moins à ma connaissance (ce qui pourrait être faux). Y a-t-il une meilleure façon?

siki
la source
53
Ce n'est pas inefficace, l'implémentation de Java 8 Stream est évaluée paresseusement, donc le filtre est appliqué uniquement au fonctionnement du terminal. Même question ici: stackoverflow.com/questions/21219667/stream-and-lazy-evaluation
Marek Gregor
1
Cool. C'est ce que j'espérais que ça ferait. Sinon, cela aurait été un flop de conception majeur.
siki
2
Si votre intention est vraiment de vérifier si la liste contient un tel élément (et non de distinguer le premier de plusieurs éventuellement), .findAny () peut théoriquement être plus efficace dans un contexte parallèle, et bien sûr, communique plus clairement cette intention.
Joachim Lous
Comparé à un simple cycle forEach, cela créerait beaucoup d'objets sur le tas et des dizaines d'appels de méthode dynamique. Bien que cela n'affecte pas toujours le résultat net de vos tests de performances, dans les points chauds, cela fait une différence de s'abstenir de l'utilisation triviale de Stream et de constructions lourdes similaires.
Agoston Horvath

Réponses:

720

Non, le filtre n'analyse pas l'intégralité du flux. Il s'agit d'une opération intermédiaire, qui renvoie un flux paresseux (en fait, toutes les opérations intermédiaires renvoient un flux paresseux). Pour vous convaincre, vous pouvez simplement faire le test suivant:

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

Quelles sorties:

will filter 1
will filter 10
10

Vous voyez que seuls les deux premiers éléments du flux sont réellement traités.

Vous pouvez donc suivre votre approche qui est parfaitement bien.

Alexis C.
la source
37
Comme note, j'ai utilisé get();ici parce que je sais quelles valeurs je fournis au pipeline de flux et donc qu'il y aura un résultat. En pratique, vous ne devez pas utiliser get();, mais orElse()/ orElseGet()/ orElseThrow()(pour une erreur plus significative au lieu d'un NSEE) car vous ne savez peut-être pas si les opérations appliquées au pipeline de flux aboutiront à un élément.
Alexis C.
31
.findFirst().orElse(null);par exemple
Gondy
20
N'utilisez pas orElse null. Cela devrait être un anti-modèle. Tout est inclus dans l'option, alors pourquoi devriez-vous risquer un NPE? Je pense que le traitement facultatif est la meilleure façon. Testez simplement l'Optionnel avec isPresent () avant de l'utiliser.
BeJay
@BeJay je ne comprends pas. que dois-je utiliser à la place orElse?
John Henckel
3
@JohnHenckel Je pense que ce que signifie BeJay, c'est que vous devez le laisser en tant que Optionaltype, ce qui .findFirstrevient. L'une des utilisations d'Optional est d'aider les développeurs à éviter d'avoir à traiter avec nullseg au lieu de vérifier myObject != null, vous pouvez vérifier myOptional.isPresent()ou utiliser d'autres parties de l'interface Optional. Est-ce que c'est plus clair?
AMTerp
102

Cependant cela me semble inefficace, car le filtre va parcourir toute la liste

Non, il ne le fera pas - il "se cassera" dès que le premier élément satisfaisant le prédicat sera trouvé. Vous pouvez en savoir plus sur la paresse dans le package de flux javadoc , en particulier (c'est moi qui souligne):

De nombreuses opérations de flux, telles que le filtrage, le mappage ou la suppression des doublons, peuvent être implémentées paresseusement, ce qui expose les opportunités d'optimisation. Par exemple, "trouver la première chaîne avec trois voyelles consécutives" n'a pas besoin d'examiner toutes les chaînes d'entrée. Les opérations de flux sont divisées en opérations intermédiaires (génératrices de flux) et opérations terminales (génératrices de valeur ou d'effets secondaires). Les opérations intermédiaires sont toujours paresseuses.

wha'eve '
la source
5
Cette réponse m'a été plus informative et explique le pourquoi, pas seulement le comment. Je n'ai jamais de nouvelles opérations intermédiaires qui sont toujours paresseuses; Les flux Java continuent de me surprendre.
kevinarpe
30
return dataSource.getParkingLots()
                 .stream()
                 .filter(parkingLot -> Objects.equals(parkingLot.getId(), id))
                 .findFirst()
                 .orElse(null);

J'ai dû filtrer un seul objet d'une liste d'objets. J'ai donc utilisé cela, j'espère que cela aide.

CodeShadow
la source
MIEUX: puisque nous recherchons une valeur de retour booléenne, nous pouvons le faire mieux en ajoutant une vérification nulle: return dataSource.getParkingLots (). Stream (). Filter (parkingLot -> Objects.equals (parkingLot.getId (), id)) .findFirst (). orElse (null)! = null;
shreedhar bhat
1
@shreedharbhat Vous ne devriez pas avoir à le faire .orElse(null) != null. Utilisez plutôt les API facultatives, .isPresentc'est-à - dire .findFirst().isPresent().
AMTerp
@shreedharbhat tout d'abord OP ne cherchait pas une valeur de retour booléenne. Deuxièmement, s'ils l'étaient, il serait plus .stream().map(ParkingLot::getId).anyMatch(Predicate.isEqual(id))
simple
13

En plus de la réponse d' Alexis C , si vous travaillez avec une liste de tableaux dans laquelle vous ne savez pas si l'élément que vous recherchez existe, utilisez ceci.

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

Ensuite, vous pouvez simplement vérifier si un est null.

Ifesinachi Bryan
la source
1
Vous devez corriger votre exemple. Vous ne pouvez pas affecter null à un entier simple. stackoverflow.com/questions/2254435/can-an-int-be-null-in-java
RubioRic
J'ai édité votre message. 0 (zéro) peut être un résultat valide lorsque vous recherchez dans une liste d'entiers. Type de variable remplacé par Integer et valeur par défaut par null.
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);
        }
    }
}
aillusions
la source
0

Réponse One-Liner améliorée: Si vous recherchez une valeur de retour booléenne, nous pouvons le faire mieux en ajoutant isPresent:

return dataSource.getParkingLots().stream().filter(parkingLot -> Objects.equals(parkingLot.getId(), id)).findFirst().isPresent();
shreedhar bhat
la source
Si vous voulez une valeur de retour booléenne, vous devez utiliser anyMatch
AjaxLeung