Страницы

Поиск по вопросам

среда, 10 октября 2018 г.

Откуда NullPointerException в тернарном операторе

Вот есть у меня метод, который отлично работает:
public static Integer update(Integer val) { if (val == null) return val; else return val+1; }
Передашь null — вернёт null, а иначе вернёт число на единицу больше.
Решил я его сократить, воспользовавшись тернарным оператором ?:. Чтобы точно не накосячить я даже не стал это делать вручную, а попросил Eclipse:

Получился такой метод:
public static Integer update(Integer val) { return val == null ? val : val+1; }
И он после этого перестал работать! Если передаёшь null, он не возвращает null, а падает с NullPointerException! Почему так? Вроде бы тернарный оператор — то же самое, что if/else?


Ответ

Дело в том, что тернарный оператор -- это выражение, которое имеет определённый конкретный тип возвращаемого значения, поэтому обе его ветки должны возвращать объект этого типа.
Для выражения val == null ? val : val+1 компилятор выводит тип int, потому что во второй ветке производится сложение объекта Integer со значением типа int, которое преобразуется в val.intValue() + 1, т.е. выполняется unboxing переменной val. Так как вторая ветка имеет тип int, для результата первой ветки тоже будет предпринята попытка выполнить unboxing, и в итоге метод будет выглядеть следующим образом:
public static Integer update(Integer val) { return Integer.valueOf(val == null ? val.intValue() : val.intValue() + 1); }
Легко заметить, что если val будет иметь значение null, то возникнет исключение NullPointerException

Это можно исправить двумя способами:
Явно обозначить, что вторая часть выражения имеет тип Integer. Это позволит сделать так, чтобы не требовался unboxing первой ветки (т.е. результатом всего выражения будет Integer). Это то, что предлагает IntelliJ IDEA при попытке рефакторинга первоначального выражения:
public static Integer update(Integer val) { return val == null ? val : Integer.valueOf(val + 1); }
В этом случае будет сгенерирован следующий код:
public static Integer update(Integer val) { return val == null ? val : Integer.valueOf(val.intValue() + 1); }
т.е. unboxing будет выполнен только во второй ветке, после чего результат будет снова упакован в Integer, а в первой ветке уже имеется Integer, поэтому можно не выполнять с ним никаких действий и значение null пройдёт без приключений. Показать, что мы просто возвращаем null в первой ветке:
public static Integer update(Integer val) { return val == null ? null : val + 1; }
В этом случае boxing второй ветки писать явно не требуется, так как компилятор сразу будет знать, что выражение не может иметь примитивный тип. Это преобразуется в следующий код:
public static Integer update(Integer val) { return val == null ? null : Integer.valueOf(val.intValue() + 1); }
Как видно, boxing и unboxing производятся только во второй части выражения. Я считаю, что этот вариант предпочтительнее ещё и потому, что здесь явно видно, что мы возвращаем null, а не просто val, и не требуется раскручивать выражение для понимания этого факта.

Как можно догадаться, оператор if ... else ... подобным поведением не обладает и в случае с ним такой проблемы не возникает, достаточно уже, что все return возвращают объекты, приводимые к типу Integer

Комментариев нет:

Отправить комментарий