Почему Optional.get () без вызова isPresent () плох, а не iterator.next ()?

23

При использовании нового Java8 streams api для поиска одного конкретного элемента в коллекции я пишу такой код:

    String theFirstString = myCollection.stream()
            .findFirst()
            .get();

Здесь IntelliJ предупреждает, что get () вызывается без предварительной проверки isPresent ().

Тем не менее, этот код:

    String theFirstString = myCollection.iterator().next();

... не дает предупреждения.

Есть ли какое-то глубокое различие между этими двумя методами, которое делает потоковый подход более «опасным» в некотором смысле, что крайне важно никогда не вызывать get () без вызова isPresent () в первую очередь? Поглядывая вокруг, я нахожу статьи, рассказывающие о том, как программисты небрежны с Optional <>, и предполагаю, что они могут вызывать get () всякий раз, когда.

Дело в том, что я хотел бы, чтобы на меня было сгенерировано исключение как можно ближе к месту, где мой код содержит ошибки, что в этом случае будет тем, где я вызываю get (), когда нет элемента, который нужно найти. Я не хочу писать бесполезные эссе, такие как:

    Optional<String> firstStream = myCollection.stream()
            .findFirst();
    if (!firstStream.isPresent()) {
        throw new IllegalStateException("There is no first string even though I totally expected there to be! <sarcasm>This exception is much more useful than NoSuchElementException</sarcasm>");
    }
    String theFirstString = firstStream.get();

... разве есть опасность провоцировать исключения из get (), о которых я не знаю?


Прочитав ответ Карла Билефельда и комментарий Халка, я понял, что мой код исключения был немного неуклюжим, вот что-то лучше:

    String errorString = "There is no first string even though I totally expected there to be! <sarcasm>This exception is much more useful than NoSuchElementException</sarcasm>";
    String firstString = myCollection.stream()
            .findFirst()
            .orElseThrow(() -> new IllegalStateException(errorString));

Это выглядит более полезным и, вероятно, станет естественным во многих местах. Я все еще чувствую, что хочу иметь возможность выбирать один элемент из списка, не имея дело со всем этим, но это может быть просто потому, что мне нужно привыкнуть к такого рода конструкциям.

Фрэнк ТВ
источник
7
Часть размещения исключений из вашего последнего примера может быть написана гораздо более кратко, если вы используете orElseThrow - и всем читателям будет ясно, что вы намереваетесь создать исключение.
Халк

Ответы:

12

Прежде всего, учтите, что проверка сложна и, вероятно, требует относительно много времени. Это требует некоторого статического анализа, и между этими двумя вызовами может быть немного кода.

Во-вторых, идиоматическое использование двух конструкций очень разное. Я программирую полный рабочий день в Scala, который использует Optionsгораздо чаще, чем Java. Я не могу вспомнить ни одного случая, когда мне нужно было использовать a .get()на Option. Вы должны почти всегда возвращать Optionalиз своей функции или использовать orElse()вместо этого. Это гораздо лучший способ обработки пропущенных значений, чем создание исключений.

Поскольку .get()при нормальном использовании его редко следует вызывать, для среды IDE имеет смысл запустить сложную проверку, когда она видит, .get()чтобы проверить, правильно ли она использовалась. Напротив, iterator.next()это правильное и повсеместное использование итератора.

Другими словами, дело не в том, что одно плохо, а другое нет, дело в том, что один является общим случаем, который имеет фундаментальное значение для его работы и с большой вероятностью вызовет исключение во время тестирования разработчика, а другой редко используется. случай, который может быть недостаточно реализован, чтобы вызвать исключение во время тестирования разработчика. Это те случаи, о которых вы хотите, чтобы IDE предупреждали вас.

Карл Билефельдт
источник
Может быть, мне нужно больше узнать об этом дополнительном бизнесе - я в основном рассматривал java8 как хороший способ сопоставления списков объектов, которые мы много делаем в нашем веб-приложении. Таким образом, вы должны отправлять дополнительные <> везде? Это займет некоторое привыкание, но это еще один пост SE! :)
Фрэнк Фрэнк
Фрэнк @ TV. Меньше, что они должны быть повсюду, и более того, они позволяют вам писать функции, которые преобразуют значение содержащегося в них типа, и вы цепляете их с помощью mapили flatMap. OptionalЭто в основном отличный способ избежать необходимости ставить все в скобки с nullчеками.
Морген
15

Класс Optional предназначен для использования, когда неизвестно, присутствует ли элемент, который он содержит. Существует предупреждение, предупреждающее программистов о том, что существует дополнительный возможный путь к коду, который они могут корректно обработать. Идея состоит в том, чтобы вообще не создавать исключений. Представленный вами вариант использования не предназначен для него, и поэтому предупреждение не имеет к вам отношения - если вы действительно хотите, чтобы было сгенерировано исключение, продолжайте и отключите его (хотя только для этой строки, а не глобально - вам следует Принятие решения о том, обрабатывать или нет не существующее дело в каждом отдельном случае, и предупреждение напомнит вам сделать это.

Возможно, аналогичное предупреждение должно быть сгенерировано для итераторов. Тем не менее, это просто никогда не было сделано, и добавление одного сейчас, вероятно, вызовет слишком много предупреждений в существующих проектах, чтобы быть хорошей идеей.

Также обратите внимание, что создание собственного исключения может быть более полезным, чем просто исключение по умолчанию, так как включает в себя описание того, какой элемент должен был существовать, но не может быть очень полезным при отладке на более позднем этапе.

Жюль
источник
6

Я согласен, что в этом нет существенной разницы, однако я хотел бы указать на больший недостаток.

В обоих случаях у вас есть тип, который предоставляет метод, который не гарантированно работает, и вы должны вызывать другой метод до этого. Предупреждение вашей IDE / компилятора / etc о том или ином случае зависит только от того, поддерживает ли он этот случай и / или настроен ли он для этого.

Тем не менее, обе части кода являются запахами кода. Эти выражения намекают на основные недостатки дизайна и дают хрупкий код.

Конечно, вы правы в том, что хотите быстро потерпеть неудачу, но решение, по крайней мере для меня, не может состоять в том, чтобы просто отмахнуться от него и выбросить руки в воздух (или исключение в стек).

Может быть известно, что ваша коллекция на данный момент непуста, но все, на что вы действительно можете положиться, это ее тип: Collection<T>- и это не гарантирует вам не пустоту. Даже если вы знаете, что теперь он не пустой, измененное требование может привести к изменению исходного кода, и внезапно ваш код попадет в пустую коллекцию.

Теперь вы можете отодвинуться и сказать другим, что вы должны получить непустой список. Вы можете быть счастливы, что быстро обнаружите эту «ошибку». Тем не менее, у вас сломан кусок программного обеспечения и вам нужно изменить код.

Сравните это с более прагматичным подходом: поскольку вы получаете коллекцию, и она может быть пустой, вы заставляете себя иметь дело с этим случаем, используя Optional # map, #orElse или любой другой из этих методов, кроме #get.

Компилятор выполняет для вас как можно больше работы, так что вы можете полагаться как можно больше на получение статического контракта Collection<T>. Когда ваш код никогда не нарушает этот контракт (например, через одну из этих вонючих цепочек вызовов), ваш код становится гораздо менее хрупким по отношению к изменяющимся условиям среды.

В приведенном выше сценарии изменение, приводящее к пустой коллекции ввода, не будет иметь никакого значения для вашего кода. Это просто еще один действительный ввод и, следовательно, все еще обрабатывается правильно.

Стоимость этого заключается в том, что вам приходится тратить время на более качественные разработки, а не а) рисковать хрупким кодом или б) неоправданно усложнять вещи, создавая исключения.

И последнее замечание: поскольку не все IDE / компиляторы предупреждают о вызовах iterator (). Next () или option.get () без предварительной проверки, такой код обычно помечается в обзорах. Что бы это ни стоило, я бы не позволил такому коду разрешить проходить обзор и требовать чистого решения.

Фрэнк
источник
Я думаю, что могу немного согласиться с запахом кода - я сделал приведенный выше пример, чтобы сделать короткий пост. Другим более обоснованным случаем может быть выбор обязательного элемента из списка, что не является странным явлением в приложении IMO.
Телевидение Фрэнк
Пятно на. «Выберите элемент со свойством p из списка» -> list.stream (). Filter (p) .findFirst () .. и снова мы вынуждены задать вопрос «а что, если его там нет?» .. ежедневный хлеб с маслом. Если это мандаторный и "только там" - почему вы спрятали его в кучу других вещей в первую очередь?
Фрэнк
4
Потому что, возможно, это список, который загружается из базы данных. Возможно, список представляет собой статистическую коллекцию, которая была собрана в какой-то другой функции: мы знаем, что у нас есть объекты A, B и C, я хочу найти количество чего угодно для B. Множество допустимых (и в моем приложении, общих) случаев.
Телевидение Фрэнк
Я не знаю о вашей базе данных, но моя не гарантирует как минимум один результат на запрос. То же самое для статистических сборников - кто сказал, что они всегда будут непустыми? Я согласен с тем, что достаточно просто рассуждать о том, что должен быть тот или иной элемент, но я предпочитаю правильную статическую типизацию, а не документацию или, что еще хуже, какую-то информацию из обсуждения.
Фрэнк