Pourquoi findFirst () lance-t-il une NullPointerException si le premier élément qu'il trouve est nul?

89

Pourquoi cela jette- java.lang.NullPointerExceptiont-il un ?

List<String> strings = new ArrayList<>();
        strings.add(null);
        strings.add("test");

        String firstString = strings.stream()
                .findFirst()      // Exception thrown here
                .orElse("StringWhenListIsEmpty");
                //.orElse(null);  // Changing the `orElse()` to avoid ambiguity

Le premier élément stringsest null, qui est une valeur parfaitement acceptable. De plus, findFirst()retourne une option , ce qui est encore plus logique pour findFirst()pouvoir gérer nulls.

EDIT: mis orElse()à jour le pour être moins ambigu.

sans fin
la source
4
null n'est pas une valeur parfaitement acceptable ... utilisez plutôt ""
Michele Lacorte
1
@MicheleLacorte bien que j'utilise Stringici, que faire si c'est une liste qui représente une colonne dans la base de données? La valeur de la première ligne de cette colonne peut être null.
neverendingqs
Oui, mais en java, null n'est pas acceptable ... utilisez une requête pour définir null dans db
Michele Lacorte
10
@MicheleLacorte, nullest une valeur parfaitement acceptable en Java, d'une manière générale. En particulier, c'est un élément valide pour un ArrayList<String>. Comme toute autre valeur, cependant, il y a des limites à ce qui peut être fait avec. «Ne jamais utiliser null» n'est pas un conseil utile, car vous ne pouvez pas l'éviter.
John Bollinger
@NathanHughes - Je soupçonne qu'au moment où vous appelez findFirst(), vous ne voulez rien faire d'autre.
neverendingqs

Réponses:

72

La raison en est l'utilisation de Optional<T>dans le retour. Facultatif n'est pas autorisé à contenir null. Essentiellement, il n'offre aucun moyen de distinguer les situations «ce n'est pas là» et «c'est là, mais c'est réglé null».

C'est pourquoi la documentation interdit explicitement la situation lorsque nullest sélectionné dans findFirst():

Jette:

NullPointerException - si l'élément sélectionné est null

Sergey Kalinichenko
la source
3
Il serait simple de suivre si une valeur existe avec un booléen privé à l'intérieur des instances de Optional. Quoi qu'il en soit, je pense que je suis à la limite des déclamations - si le langage ne le supporte pas, il ne le supporte pas.
neverendingqs
2
@neverendingqs Absolument, utiliser un booleanpour différencier ces deux situations serait parfaitement logique. Pour moi, il semble que l'utilisation d' Optional<T>ici était un choix discutable.
Sergey Kalinichenko
1
@neverendingqs Je ne peux pas penser à une alternative intéressante à cela, à part rouler votre propre null , ce qui n'est pas idéal non plus.
Sergey Kalinichenko
1
J'ai fini par écrire une méthode privée qui obtient l'itérateur de n'importe quel Iterabletype, vérifie hasNext()et renvoie la valeur appropriée.
neverendingqs
1
Je pense que cela a plus de sens si findFirstrenvoie une valeur facultative vide dans le cas de PO
Danny
47

Comme indiqué précédemment , les concepteurs d'API ne supposent pas que le développeur souhaite traiter les nullvaleurs et les valeurs absentes de la même manière.

Si vous souhaitez toujours le faire, vous pouvez le faire explicitement en appliquant la séquence

.map(Optional::ofNullable).findFirst().flatMap(Function.identity())

au flux. Le résultat sera une option vide dans les deux cas, s'il n'y a pas de premier élément ou si le premier élément l'est null. Donc, dans votre cas, vous pouvez utiliser

String firstString = strings.stream()
    .map(Optional::ofNullable).findFirst().flatMap(Function.identity())
    .orElse(null);

pour obtenir une nullvaleur si le premier élément est absent ou null.

Si vous souhaitez faire la distinction entre ces cas, vous pouvez simplement omettre l' flatMapétape:

Optional<String> firstString = strings.stream()
    .map(Optional::ofNullable).findFirst().orElse(null);
System.out.println(firstString==null? "no such element":
                   firstString.orElse("first element is null"));

Ce n'est pas très différent de votre question mise à jour. Il suffit de remplacer "no such element"par "StringWhenListIsEmpty"et "first element is null"par null. Mais si vous n'aimez pas les conditions, vous pouvez également y parvenir comme:

String firstString = strings.stream().skip(0)
    .map(Optional::ofNullable).findFirst()
    .orElseGet(()->Optional.of("StringWhenListIsEmpty"))
    .orElse(null);

Maintenant, ce firstStringsera nullsi un élément existe mais est nullet ce sera "StringWhenListIsEmpty"quand aucun élément n'existe.

Holger
la source
Désolé, j'ai réalisé que ma question aurait pu impliquer que je voulais retourner nullpour 1) le premier élément est nullou 2) aucun élément n'existe dans la liste. J'ai mis à jour la question pour supprimer l'ambiguïté.
neverendingqs
1
Dans le troisième extrait de code, un Optionalpeut être attribué à null. Comme il Optionalest supposé être un "type valeur", il ne doit jamais être nul. Et une option ne doit jamais être comparée à ==. Le code peut échouer dans Java 10 :) ou à chaque fois qu'un type de valeur est introduit dans Java.
ZhongYu
1
@ bayou.io: la documentation ne dit pas que les références aux types valeur ne peuvent pas être nullet bien que les instances ne devraient jamais être comparées par ==, la référence peut être testée pour l' nullutilisation ==car c'est la seule façon de la tester null. Je ne vois pas comment une telle transition vers «jamais null» devrait fonctionner pour le code existant, comme l'est même la valeur par défaut de toutes les variables d'instance et éléments de tableau null. L'extrait de code n'est certainement pas le meilleur code, mais il ne s'agit pas non plus de traiter nulls comme des valeurs actuelles.
Holger
voir john rose - il ne peut pas non plus être comparé à l'opérateur «==», même pas avec un nul
ZhongYu
1
Puisque ce code utilise une API générique, c'est ce que le concept appelle une représentation encadrée qui peut être null. Cependant, étant donné qu'un tel changement de langage hypothétique entraînerait le compilateur à émettre une erreur ici (ne pas casser le code silencieusement), je peux vivre avec le fait qu'il devrait éventuellement être adapté pour Java 10. Je suppose que l' StreamAPI va look tout à fait différent alors aussi…
Holger
18

Vous pouvez utiliser java.util.Objects.nonNullpour filtrer la liste avant de trouver

quelque chose comme

list.stream().filter(Objects::nonNull).findFirst();
Mattos
la source
1
Je veux firstStringêtre nullsi le premier élément stringsest null.
Neverendingqs
2
malheureusement, il utilise Optional.ofce qui n'est pas nul sûr. Vous pourriez mapà Optional.ofNullable puis utiliser , findFirstmais vous finirez avec une option de option
Mattos
14

Le code suivant remplace findFirst()par limit(1)et remplace orElse()par reduce():

String firstString = strings.
   stream().
   limit(1).
   reduce("StringWhenListIsEmpty", (first, second) -> second);

limit()ne permet d'atteindre qu'un seul élément reduce. Le BinaryOperatorpassé à reducerenvoie cet élément 1 ou bien "StringWhenListIsEmpty"si aucun élément n'atteint le reduce.

La beauté de cette solution est qu'elle Optionaln'est pas allouée et que le BinaryOperatorlambda n'allouera rien.

Nathan
la source
1

Facultatif est censé être un type "valeur". (lisez les petits caractères en javadoc :) JVM pourrait même tout remplacer Optional<Foo>par juste Foo, en supprimant tous les coûts de boxe et de déballage. Un nullFoo signifie un vide Optional<Foo>.

Il est possible d'autoriser l'option facultative avec une valeur nulle, sans ajouter un indicateur booléen - ajoutez simplement un objet sentinelle. (pourrait même utiliserthis comme sentinelle; voir Throwable.cause)

La décision selon laquelle Facultatif ne peut pas encapsuler null n'est pas basée sur le coût d'exécution. C'était un problème très controversé et vous devez creuser les listes de diffusion. La décision n'est pas convaincante pour tout le monde.

Dans tous les cas, comme Optional ne peut pas envelopper la valeur nulle, cela nous pousse dans un coin dans des cas comme findFirst . Ils ont dû penser que les valeurs nulles sont très rares (il a même été considéré que Stream devrait interdire les valeurs nulles), il est donc plus pratique de lever une exception sur des valeurs nulles plutôt que sur des flux vides.

Une solution de contournement consiste à encadrer null, par exemple

class Box<T>
    static Box<T> of(T value){ .. }

Optional<Box<String>> first = stream.map(Box::of).findFirst();

(Ils disent que la solution à chaque problème de POO est d'introduire un autre type :)

ZhongYu
la source
1
Il n'est pas nécessaire de créer un autre Boxtype. Le Optionaltype lui-même peut servir cet objectif. Voir ma réponse pour un exemple.
Holger le
@Holger - oui, mais cela peut être déroutant car ce n'est pas le but recherché de Optional. Dans le cas de OP, nullest une valeur valide comme une autre, pas de traitement spécial pour elle. (jusqu'à quelque temps plus tard :)
ZhongYu