Почему findFirst () выдает исключение NullPointerException, если первый найденный элемент имеет значение NULL?

89

Почему это бросает 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

Первый элемент в stringsIS null, который является вполне приемлемым значением. Более того, findFirst()возвращает Optional , что имеет еще больше смысла для findFirst()обработки nulls.

РЕДАКТИРОВАТЬ: обновлено, orElse()чтобы быть менее двусмысленным.

бесконечный
источник
4
null не совсем приемлемое значение ... используйте вместо этого ""
Мишель Лакорте
1
@MicheleLacorte, хотя я здесь использую String, что, если это список, представляющий столбец в БД? Значение первой строки для этого столбца может быть null.
neverendingqs 08
Да, но в java null неприемлемо .. используйте запрос для установки null в db
Мишель Лакорте
10
@MicheleLacorte, nullвообще говоря, вполне приемлемое значение для Java. В частности, это допустимый элемент для ArrayList<String>. Однако, как и с любым другим значением, есть ограничения на то, что с ним можно сделать. «Никогда не употребляйте null» - бесполезный совет, поскольку вы не можете его избежать.
Джон Боллинджер
@NathanHughes - Я подозреваю, что к тому времени, когда вы позвоните findFirst(), вам больше нечего делать.
neverendingqs 08

Ответы:

72

Причина этого - использование Optional<T>в возврате. Необязательно содержать null. По сути, он не предлагает возможности различать ситуации «это не там» и «это есть, но он установлен null».

Вот почему в документации явно запрещена ситуация, когда nullвыбрано в findFirst():

Броски:

NullPointerException - если выбранный элемент null

Сергей Калиниченко
источник
3
Было бы просто отследить, существует ли значение с частным логическим значением внутри экземпляров Optional. В любом случае, я думаю, что я граничу с разглагольствованием - если язык его не поддерживает, он не поддерживает его.
neverendingqs 08
2
@neverendingqs Безусловно, использование a booleanдля различения этих двух ситуаций имело бы смысл. На мой взгляд, использование Optional<T>здесь было сомнительным выбором.
Сергей Калиниченко
1
@neverendingqs Я не могу придумать какой-либо красивой альтернативы этому, кроме разворачивания собственного нуля , что тоже не идеально.
Сергей Калиниченко
1
В итоге я написал частный метод, который получает итератор любого Iterableтипа, проверяет hasNext()и возвращает соответствующее значение.
neverendingqs 08
1
Я думаю , что это имеет смысл , если findFirstвозвращает пустое факультативное значение в случае , если ЗП
Danny
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> .

Возможен вариант, позволяющий разрешить Optional с нулевым значением без добавления логического флага - просто добавьте объект-дозорный. (может даже использоватьthis качестве дозорного; см. Throwable.cause)

Решение о том, что Optional не может обернуть null, не основано на стоимости выполнения. Это была очень обсуждаемая проблема, и вам нужно покопаться в списках рассылки. Решение не для всех убедительно.

В любом случае, поскольку Optional не может обернуть нулевое значение, он загоняет нас в угол в таких случаях, как findFirst. Они, должно быть, рассудили, что нулевые значения очень редки (даже считалось, что Stream должен блокировать нулевые значения), поэтому удобнее генерировать исключение для нулевых значений, а не для пустых потоков.

Обходной путь - бокс null, например

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

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

(Говорят, решение каждой проблемы ООП - ввести другой тип :)

Чжун Ю
источник
1
Другой Boxтип создавать не нужно . Сам Optionalтип может служить этой цели. См. Мой ответ для примера.
Хольгер
@Holger - да, но это может сбивать с толку, поскольку это не предназначение Optional. В случае OP nullэто допустимое значение, как и любое другое, без особой обработки. (пока не поздно :)
ZhongYu