Разработка программ. Мои заметки.

February 23, 2017 at 22:17

NullPointerException в Java. Итоги.

9. Ещё немного про Optional и пару советов. Полагаю — умных.

OptionalInt.

Хотелось бы напомнить, что если методу нужно вернуть некий объект. относящийся к одному из трёх обёрточных классов (классы-контейнеры для примитивных типов данных) — Integer, Long и Double. то для таких случаев уже предусмотрены OptionalInt, OptionalLong и OptionalDouble классы. Возникает законный вопрос, почему бы не использовать Optional<Integer>, зачем нужен отдельный OptionalInt? Вопрос хороший. Видимо потому, что в случае взаимодействия с другими интерфейсами вида IntConsumer, IntStream потребовалось бы много лишних преобразований типов и boxing/unbоxing. Возможно мы и вернёмся к этому вопросу как-нибудь позже.

А что есть в других языках?

Есть и в других языках свои неплохие синтаксические конструкции. Облегчающие это дело. Дело борьбы с NullPointerException. Обзорно коснёмся. Для повышения общей эрудиции.

Есть такой интересный оператор — Элвис. Выглядит Элвис-оператор так: “?:“. Он похож на “знаменитую” чёлку Элвиса. Ну если приглядеться и пофантазировать. Кто такой Элвис, полагаю, вы знаете. Традиционно, оператор используется так: x = f() ?: g(); Оператор возвращает левый операнд, если он истинен, иначе возвращает правый. По сути, аналог: x = f() ? f() : g(); но f() два раза не вычисляется.

По законам языка Groovy, null — это false. И получается, что такой оператор возвращает левое значение, если оно не null, или правое, в противном случае. Так же это работает и в языке Kotlin.

В Groovy также есть оператор “?.”. Он более прост. Этот оператор убирает необходимость проверки на null вложенных обращений. Достаточно одному из объектов цепочки быть null и результат операции будет null. Никаких операторов if — не нужно:


    String version = computer?.getSoundcard()?.getUSB()?.getVersion();

Можно и с Элвисом совместить. В этом случае, если кто-то null, то версия будет “UNKNOWN”:


    String version = computer?.getSoundcard()?.getUSB()?.getVersion() ?: "UNKNOWN";

И конечно же нужно помнить о том, что есть функциональные языки программирования. Java только слегка затронула функциональность в версии 8. А ведь есть Haskell, который изначально функциональный язык. А в Java мире есть Scala, которая уже при рождении интегрировала в себя функциональность куда как более широко чем Java 8. Scala относится к миру Java потому как работает на платформе Java. Хотя появилась и реализация под .NET.

Так вот в Haskell имеется тип Maybe, который, по сути, и держит значение которое может быть, а может и не быть. Опционально. Там в принципе отсутствует концепция нулевой ссылки. Аналогичная конструкция в Scala называется Option[T]. И там также фактическое значение типа T может присутствовать, а может и отсутствовать. И вы вынуждены явно убедиться есть там оно или нет.

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

Как снизить вероятность столкнуться с NullPointerException?

Советов можно надавать много и разных. Особенно умничать я не буду, но несколько традиционных всё-таки перечислю. Я бы разбил их на две группы. Первая группа — советы по коду. Им желательно следовать любому разработчику, что называется “на автомате”.

Вторая — советы по организации кода и дисциплине проекта. Для их внедрения необходима некая совокупность стандартов (полиси), которым должны следовать все люди, пишущие код для проекта. Иначе говоря, эти советы дисциплинарны (требуют решения администрации проекта), поскольку в одиночку им следовать не получится. Пение невпопад обычно хуже элементарного молчания.

Начнём с первой группы:

  • Когда сравниваете строку, которая потенциально может содержать null на равенство её некоей константе, то “пляшите” от константы. Это безопаснее. Выглядит вот так:

    String someString = null;  //  Ну допустим, что так

    if( "TheStringConstant".equals( someString ) ) {
        System.err.println( "Так вот будет безопаснее" );
    }    

  • Используйте valueOf() вместо toString() там где это возможно. К примеру для обёрточных классов Integer, Float, Double, BigDecimal:

    BigDecimal balance = getBalance();
    
    System.out.println( String.valueOf( balance ) );  //  Нет NullPointerException
    System.out.println( balance.toString() );  //  А вот тут может и выстрелить!

  • Для предварительной проверки строк используйте популярные сторонние библиотеки, скажем, Apache Сommons (StringUtils). Там есть методы, которые допускает на входе null и позволяют сделать несложную предварительную проверку (но документацию прочесть не забудьте )):

    System.out.println( StringUtils.isEmpty( null ) );  // true
    System.out.println( StringUtils.isBlank( null ) );  // true
    System.out.println( StringUtils.isNumeric( null ) );  // false
    System.out.println( StringUtils.isAllUpperCase( null ) );  // false

  • Будьте осторожны с unboxing в коде. Если объект содержит какое-то поле обёрточного типа (Integer, Float… etc.) и вы лихо, без проверки, анбоксите его в примитивный тип, а оно вдруг было равно null, то вам не избежать NullPointerException:

    Person person = new Person( id, name );
    ...
    int phone = person.getAge();

  • Избегайте возврата null из любых методов. Возвращайте пустой массив, если нет значений. Но массив, а не null. Возвращайте пустую коллекцию, если нет элементов, но не null! И конечно — Optional вам в руки! Про Optional я уже много писал, ну а с коллекциями и так понятно. Если вы должны вернуть коллекцию каких-то элементов, а их не нашлось, не возвращайте null, возвращайте Collections.EMPTY_LIST.

Ну и теперь вторая группа. Вторая группа требует согласования с техническим руководством проекта. Ибо общий подход всегда должен быть такой — лучше безобразно, но единообразно. Лебедь, рак и щука долго не проживут. Заказчик примет решение раньше.

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

  • Используйте аннотации @NotNull and @Nullable. Но только однозначно определитесь чьи реализации аннотаций вы используете. От Guava, от JetBrains, от FindBugs, от Checker Framework, от Lombok, от… В общем, всякий уважающий себя разработчик может назвать несколько разных вариантов реализации этих аннотаций. И в одном проекте, по умному, нужно использовать один подход. И это придётся решать дисциплинарно.

  • Желательно определить некую общую дисциплину для проекта как обращаться с объектами. Как указывать, что они могут быть null, где оговорить тот факт, что тут null не может быть в принципе, а вот тут может… и прочее. Этот набор требований зависит от состава участников проекта. Если много людей с небольшим опытом, то возможно имеет смысл взять некий популярный фреймворк типа FindBugs, строго следовать его законам, следить за кодом, и хоть немного уменьшить последствия проблемы. Если состав небольшой и опытный, будет достаточно несложных, но письменно задекларированных соглашений.

Автор — Владимир Рыбов