Поиск…


замечания

Значение null является значением по умолчанию для неинициализированного значения поля, тип которого является ссылочным типом.

Исключение NullPointerException (или NPE) - это исключение, которое возникает при попытке выполнить ненадлежащую операцию над ссылкой на null объект. К таким операциям относятся:

  • вызов метода экземпляра на null целевом объекте,
  • доступ к полю null целевого объекта,
  • пытаясь проиндексировать null массив или получить доступ к его длине,
  • используя ссылку на null объект в качестве мьютекса в synchronized блоке,
  • литье ссылки на null объект,
  • unboxing ссылку на null объект и
  • бросая ссылку на null объект.

Наиболее распространенные первопричины для NPE:

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

Примеры обычно используемых методов, возвращающих значение null включают:

  • Метод get(key) в API Map возвращает null если вы вызываете его с помощью ключа, который не имеет сопоставления.
  • getResource(path) и getResourceAsStream(path) в API ClassLoader и Class API возвращают значение null если ресурс не может быть найден.
  • Метод get() в Reference API возвращает null если сборщик мусора очистил ссылку.
  • Различные методы getXxxx в API-интерфейсах Java EE возвращают значение null если вы getXxxx извлечь необязательный параметр запроса, атрибут сеанса или сеанса и т. Д.

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

Pitfall - Ненужное использование примитивных оберток может привести к NullPointerExceptions

Иногда программисты, которые являются новыми Java, будут использовать примитивные типы и обертки взаимозаменяемо. Это может привести к проблемам. Рассмотрим этот пример:

public class MyRecord {
    public int a, b;
    public Integer c, d;
}

...
MyRecord record = new MyRecord();
record.a = 1;               // OK
record.b = record.b + 1;    // OK
record.c = 1;               // OK
record.d = record.d + 1;    // throws a NullPointerException

Наш класс MyRecord 1 использует инициализацию по умолчанию для инициализации значений в своих полях. Таким образом, когда мы записываем new запись, поля a и b будут установлены на ноль, а поля c и d будут установлены в null .

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

Есть несколько способов взглянуть на это:

  • Если поля c и d должны быть примитивными оболочками, то либо мы не должны полагаться на инициализацию по умолчанию, либо мы должны тестировать значение null . Для прежнего это правильный подход, если для полей в null состоянии не существует определенного значения.

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

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


1 - Этот класс не является примером хорошей практики кодирования. Например, у хорошо продуманного класса не было бы публичных полей. Однако это не относится к этому примеру.

Pitfall - использование null для представления пустого массива или коллекции

Некоторые программисты считают, что это хорошая идея, чтобы сэкономить место, используя null для представления пустого массива или коллекции. Хотя это правда, что вы можете сэкономить небольшое пространство, обратная сторона заключается в том, что он делает ваш код более сложным и более хрупким. Сравните эти две версии метода суммирования массива:

Первая версия - это то, как вы обычно кодируете метод:

/**
 * Sum the values in an array of integers.
 * @arg values the array to be summed
 * @return the sum
 **/
public int sum(int[] values) {
    int sum = 0;
    for (int value : values) {
        sum += value;
    }
    return sum;
}

Вторая версия - это то, как вам нужно закодировать метод, если вы привыкли использовать null для представления пустого массива.

/**
 * Sum the values in an array of integers.
 * @arg values the array to be summed, or null.
 * @return the sum, or zero if the array is null.
 **/
public int sum(int[] values) {
    int sum = 0;
    if (values != null) {
        for (int value : values) {
            sum += value;
        }
    }
    return sum;
}

Как вы можете видеть, код немного сложнее. Это напрямую связано с решением использовать null таким образом.

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


Урок здесь состоит в том, чтобы использовать пустые массивы и пустые списки, когда это то, что вы имеете в виду.

int[] values = new int[0];                     // always empty
List<Integer> list = new ArrayList();          // initially empty
List<Integer> list = Collections.emptyList();  // always empty

Недостаток пространства небольшой, и есть другие способы свести его к минимуму, если это стоит того.

Pitfall - «Создание хороших» неожиданных нулей

В StackOverflow мы часто видим такой код в ответах:

public String joinStrings(String a, String b) {
    if (a == null) {
        a = "";
    }
    if (b == null) {
        b = "";
    }
    return a + ": " + b;
}

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

Это лучшая практика? Короче: Нет.

Есть некоторые основополагающие предположения, которые необходимо поставить под сомнение, прежде чем мы сможем сказать, если это хорошая идея сделать это в наших joinStrings :

Что значит для «a» или «b» быть нулевым?

Значение String может быть равно нулю или больше символов, поэтому у нас уже есть способ представления пустой строки. null означает что-то другое, чем "" ? Если нет, то проблематично иметь два способа представления пустой строки.

Был ли null получен из неинициализированной переменной?

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

Указывает ли нуль значение «не знаю» или «отсутствует»?

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

Если это ошибка (или ошибка дизайна), мы должны «сделать хорошо»?

Одна из интерпретаций кода заключается в том, что мы «делаем хороший» неожиданный null , используя пустую строку на своем месте. Правильная ли стратегия? Было бы лучше позволить NullPointerException , а затем поймать исключение дальше по стеку и зарегистрировать его как ошибку?

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

Является ли это эффективным / хорошим для качества кода?

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

В итоге

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

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

Pitfall - Возвращение null вместо исключения исключения

Некоторые Java-программисты имеют общее отвращение к выбросам или распространению исключений. Это приводит к следующему коду:

public Reader getReader(String pathname) {
    try {
        return new BufferedReader(FileReader(pathname));
    } catch (IOException ex) {
        System.out.println("Open failed: " + ex.getMessage());
        return null;
    }

}

Так в чем проблема?

Проблема в том, что getReader возвращает значение null в качестве специального значения, чтобы указать, что Reader не может быть открыт. Теперь возвращаемое значение нужно протестировать, чтобы проверить, не является ли оно значением null до его использования. Если тест не учитывается, результатом будет исключение NullPointerException .

Здесь есть три проблемы:

  1. IOException было обнаружено слишком рано.
  2. Структура этого кода означает, что существует риск утечки ресурса.
  3. Затем был null потому что не было доступно «реальное» Reader .

На самом деле, предполагая, что исключение нужно было поймать раньше, было несколько альтернатив возврату null :

  1. Можно было бы реализовать класс NullReader ; например, когда операции API ведут себя так, как если бы читатель уже находился в позиции «конец файла».
  2. С Java 8 можно было бы объявить getReader как возвращающий Optional<Reader> .

Pitfall - не проверяет, не инициализирован ли поток ввода-вывода при его закрытии

Чтобы предотвратить утечку памяти, не следует забывать закрыть входной поток или выходной поток, чья работа выполнена. Обычно это делается с помощью заявления try catch finally без части catch :

void writeNullBytesToAFile(int count, String filename) throws IOException {
    FileOutputStream out = null;
    try {
        out = new FileOutputStream(filename);
        for(; count > 0; count--)
            out.write(0);
    } finally {
        out.close();
    }
}

Хотя приведенный выше код может выглядеть невинно, у него есть недостаток, который может сделать отладку невозможной. Если строка, где out инициализирована ( out = new FileOutputStream(filename) ), генерирует исключение, тогда out будет null когда out.close() , что приводит к неприятному исключению NullPointerException !

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

void writeNullBytesToAFile(int count, String filename) throws IOException {
    FileOutputStream out = null;
    try {
        out = new FileOutputStream(filename);
        for(; count > 0; count--)
            out.write(0);
    } finally {
        if (out != null)
            out.close();
    }
}

Еще лучший подход - try -with-resources, так как он автоматически закрывает поток с вероятностью 0, чтобы выбросить NPE без необходимости блока finally .

void writeNullBytesToAFile(int count, String filename) throws IOException {
    try (FileOutputStream out = new FileOutputStream(filename)) {
        for(; count > 0; count--)
            out.write(0);
    }
}

Pitfall - использование «нотации Yoda», чтобы избежать NullPointerException

Многие примеры кода, размещенные в StackOverflow, включают в себя следующие фрагменты:

if ("A".equals(someString)) {
    // do something
}

Это предотвращает или предотвращает возможное исключение NullPointerException в случае, если someString имеет значение null . Кроме того, можно утверждать, что

    "A".equals(someString)

лучше, чем:

    someString != null && someString.equals("A")

(Это более красноречиво, и в некоторых случаях это может быть более эффективным. Однако, как мы утверждаем ниже, краткость может быть отрицательной.)

Тем не менее, реальная ловушка использует тест Йоды, чтобы избежать NullPointerExceptions в качестве привычки.

Когда вы пишете "A".equals(someString) вы на самом деле «делаете хорошо», когда someString имеет значение null . Но в качестве другого примера ( Pitfall - «Создание хороших» неожиданных нулей ) объясняет, что «создание хороших» null значений может быть вредным по целому ряду причин.

Это означает, что условия Йоды не являются «лучшей практикой» 1 . Если не ожидается null , лучше разрешить NullPointerException , чтобы вы могли получить отказ единичного теста (или отчет об ошибке). Это позволяет вам найти и исправить ошибку, которая вызвала появление неожиданного / нежелательного null .

Условия Yoda должны использоваться только в тех случаях, когда ожидается null потому что объект, который вы тестируете, исходит из API, который документирован как возвращающий null . И, возможно, лучше использовать один из менее привлекательных способов выражения теста, потому что это помогает выделить null тест тому, кто просматривает ваш код.


1 - Согласно Википедии : «Лучшие методы кодирования - это набор неформальных правил, которые сообщество разработчиков программного обеспечения со временем узнало, что может помочь улучшить качество программного обеспечения». , Использование нотации Yoda этого не достигает. Во многих ситуациях это делает код хуже.



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow