Чому findFirst () кидає NullPointerException, якщо перший знайдений елемент має значення null?


89

Чому це кидає a java.lang.NullPointerException?

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

Перший елемент в stringsє null, що є цілком прийнятним значенням. Крім того, findFirst()повертає необов’язковий , що має ще більше сенсу для findFirst()можливості обробляти nulls.

РЕДАГУВАТИ: оновлено, orElse()щоб бути менш двозначним.


4
null не є цілком прийнятним значенням ... замість цього використовуйте ""
Мікеле Лакорт

1
@MicheleLacorte, хоча я Stringтут використовую , що якщо це список, який представляє стовпець у БД? Значення першого рядка для цього стовпця може бути null.
neverendingqs

Так, але в Java null неприйнятний .. використовуйте запит, щоб встановити null у db
Мікеле Лакорт

10
@MicheleLacorte null- це цілком прийнятна цінність у Java, загалом кажучи. Зокрема, це допустимий елемент для ArrayList<String>. Однак, як і будь-яке інше значення, існують обмеження щодо того, що з ним можна зробити. "Ніколи не вживати null" - це не корисна порада, оскільки ви не можете її уникнути.
Джон Боллінгер,

@NathanHughes - Я підозрюю, що до того часу, як ти зателефонуєш findFirst(), ти вже нічого не хочеш робити.
neverendingqs

Відповіді:


72

Причиною цього є використання Optional<T>у поверненні. Необов’язково містити null. По суті, він не пропонує ніякого способу розрізнити ситуації "його там немає" і "він там є, але для нього встановлено null".

Ось чому документація прямо забороняє ситуацію, коли nullвона обрана в findFirst():

Кидки:

NullPointerException - якщо вибраним елементом є null


3
Було б просто відстежити, чи існує значення з приватним булевим значенням всередині екземплярів Optional. У будь-якому випадку, я думаю, що межую з викривленням - якщо мова не підтримує це, це не підтримує.
neverendingqs

2
@neverendingqs Абсолютно, використання a booleanдля диференціації цих двох ситуацій було б цілком сенсом. Мені здається, що використання Optional<T>тут було сумнівним вибором.
Сергій Калініченко

1
@neverendingqs Я не можу придумати жодної красиво виглядаючої альтернативи цьому, крім прокатки власного нуля , що теж не є ідеальним.
Сергій Калініченко

1
У підсумку я написав приватний метод, який отримує ітератор будь-якого Iterableтипу, перевіряє hasNext()та повертає відповідне значення.
neverendingqs

1
Я думаю, що має більше сенсу, якщо findFirstповертає порожнє необов’язкове значення у випадку PO
Денні,

47

Як вже обговорювалося , розробники API не припускають, що розробник хоче nullоднаково ставитись до значень та відсутніх значень.

Якщо ви все одно хочете зробити це, ви можете зробити це явно, застосувавши послідовність

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

до потоку. Результат буде порожнім необов’язковим в обох випадках, якщо немає першого елемента або якщо перший елемент є null. Тож у вашому випадку ви можете використовувати

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

отримати nullзначення, якщо перший елемент або відсутній, або null.

Якщо ви хочете розрізнити ці випадки, ви можете просто пропустити flatMapкрок:

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

Це мало чим відрізняється від вашого оновленого запитання. Вам просто потрібно замінити "no such element"на "StringWhenListIsEmpty"і "first element is null"з null. Але якщо вам не подобаються умови, ви можете досягти цього також, як:

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

Тепер firstStringбуде, nullякщо елемент існує, але є, nullі буде, "StringWhenListIsEmpty"коли жодного елемента не існує.


На жаль, я зрозумів, що моє запитання могло натякати на те, що я хочу повернути nullабо 1) перший елемент, nullабо 2) в списку відсутні елементи. Я оновив запитання, щоб усунути двозначність.
neverendingqs

1
У третьому фрагменті коду Optionalможе бути призначено null. Оскільки Optionalпередбачається, що це "тип значення", воно ніколи не має бути нульовим. І Необов’язково ніколи не слід порівнювати ==. Код може не вдатися в Java 10 :) або коли-небудь тип значення вводиться в Java.
ZhongYu

1
@ bayou.io: у документації не сказано, що посилання на типи значень не можуть бути, nullі хоча екземпляри ніколи не слід порівнювати ==, посилання може бути перевірено на nullвикористання, ==оскільки це єдиний спосіб перевірити його null. Я не бачу, як такий перехід у "ніколи null" повинен працювати для існуючого коду, оскільки навіть значення за замовчуванням для всіх змінних екземпляра та елементів масиву null. Фрагмент, безумовно, не є найкращим кодом, але не є завданням розглядати nulls як поточні значення.
Holger

див. Джон Роуз - і його не можна порівняти з оператором “==”, навіть з нулем
ZhongYu

1
Оскільки цей код використовує загальний API, це те, що концепція називає коробковим представленням, яке може бути null. Однак, оскільки така гіпотетична зміна мови призведе до того, що компілятор видасть тут помилку (не розбиватиме код мовчки), я можу погодитися з тим, що, можливо, її доведеться адаптувати для Java 10. Я гадаю, StreamAPI буде тоді теж виглядати зовсім по-іншому ...
Холгер

18

Ви можете використовувати java.util.Objects.nonNullдля фільтрування списку, перш ніж знаходити

щось на зразок

list.stream().filter(Objects::nonNull).findFirst();

1
Я хочу firstStringбути, nullякщо першим елементом stringsє null.
neverendingqs

2
на жаль, він використовує, Optional.ofщо не є нульовим. Ви могли mapб, Optional.ofNullable а потім скористатися, findFirstале в кінцевому підсумку ви отримаєте Необов’язково Необов’язково
Маттос

14

Наступний замінює код findFirst()з limit(1)і замінює orElse()з reduce():

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

limit()дозволяє досягти лише 1 елементу reduce. BinaryOperatorПередаються reduceповернення , що один елемент або ж , "StringWhenListIsEmpty"якщо ні один з елементів не досягнуті reduce.

Краса цього рішення полягає в тому, що Optionalне виділяється і BinaryOperatorлямбда не збирається нічого виділяти.


1

Необов’язковим є тип "значення". (читайте дрібний шрифт у javadoc :) JVM може навіть замінити все Optional<Foo>просто Foo, видаливши всі витрати на бокс та розпакування. nullFoo означає порожній Optional<Foo>.

Можливий дизайн дозволити необов’язковий з нульовим значенням, не додаючи логічний прапор - просто додайте об’єкт сторожової. (може навіть використовуватися thisяк сторожовий; див. Throwable.cause)

Рішення про те, що Optional не може обертати null, не засноване на вартості виконання. Це було суперечливе питання, і вам потрібно розкопати списки розсилки. Рішення є не переконливим для всіх.

У будь-якому випадку, оскільки необов’язковий варіант не може обернути нульове значення, він відсуває нас у кут у таких випадках findFirst. Вони, мабуть, міркували, що нульові значення дуже рідкісні (навіть вважалося, що Потік повинен забороняти нульові значення), тому зручніше викидати виключення на нульові значення, а не на порожні потоки.

Обхідний шлях полягає у встановленні null, наприклад

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

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

(Кажуть, що рішенням кожної проблеми ООП є введення іншого типу :)


1
Немає необхідності створювати інший Boxтип. OptionalСам тип може служити цій меті. Див. Мою відповідь для прикладу.
Holger

@Holger - так, але це може заплутати, оскільки це не пряма мета необов’язкового. У випадку OP nullє дійсним значенням, як і будь-яке інше, для нього немає спеціального режиму. (до десь пізніше :)
ZhongYu
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.