Разработка программ. Мои заметки.
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, строго следовать его законам, следить за кодом, и хоть немного уменьшить последствия проблемы. Если состав небольшой и опытный, будет достаточно несложных, но письменно задекларированных соглашений.