Это плохая идея, если equals (null) вместо NullPointerException выбрасывает?

Контракт equals в отношении null выглядит следующим образом:

Для любого ненулевого опорного значения x, x.equals(null) должен return false.

Это довольно странно, потому что если o1 != null и o2 == null, то мы имеем:

o1.equals(o2) // returns false
o2.equals(o1) // throws NullPointerException

Тот факт, что o2.equals(o1) throws NullPointerException - это хорошо, потому что он предупреждает нас об ошибке программиста. И тем не менее, эта ошибка не была бы решена, если по разным причинам мы просто переключили ее на o1.equals(o2), которая просто "молчала бы" вместо этого.

Итак, вопросы:

  • Почему это хорошая идея, что o1.equals(o2) должен return false вместо метания NullPointerException?
  • Было бы плохой идеей, если, по возможности, мы переписываем контракт так, чтобы anyObject.equals(null) всегда бросал NullPointerException вместо этого?

По сравнению с Comparable

В отличие от этого, Comparable контракт говорит:

Обратите внимание, что null не является экземпляром какого-либо класса, а e.compareTo(null) должен бросать NullPointerException, хотя e.equals(null) возвращает false.

Если NullPointerException подходит для compareTo, почему это не для equals?

Связанные вопросы


Чистый семантический аргумент

Это фактические слова в Object.equals(Object obj):

Указывает, является ли какой-либо другой объект "равным" этому.

А что такое объект?

Объекты JLS 4.3.1

Объект - это экземпляр класса или массив.

Контрольные значения (часто просто ссылки) являются указателями на эти объекты и специальной ссылкой null, которая ссылается на отсутствие объекта.

Мой аргумент из этого угла действительно прост.

  • equals проверяет, равен ли какой-либо другой объект "<" t228 >
  • null ссылка не дает другого объекта для теста
  • Поэтому equals(null) должен бросать NullPointerException

Ответы

Ответ 1

К вопросу о том, является ли эта асимметрия непоследовательной, я думаю, что нет, и я отсылаю вас к этому древнему дзен-ксан:

  • Спросите любого человека, если он так же хорошо, как и следующий человек, и каждый скажет "да".
  • Спросите любого человека, если он так же хорош, как никто, и каждый скажет "нет".
  • Не спрашивайте никого, если он так хорош, как любой человек, и вы никогда не получите ответ.

В этот момент компилятор достиг просветления.

Ответ 2

Исключение действительно должно быть исключительной ситуацией. Нулевой указатель может не быть ошибкой программиста.

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

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

Ответ 3

Подумайте, как .equals относится к == и .compareTo относится к операторам сравнения > , <, > =, < =.

Если вы собираетесь утверждать, что использование .equals для сравнения объекта с NULL должно бросать NPE, тогда вам нужно сказать, что этот код также должен бросить его:

Object o1 = new Object();
Object o2 = null;
boolean b = (o1 == o2); // should throw NPE here!

Разница между o1.equals(o2) и o2.equals(o1) заключается в том, что в первом случае вы сравниваете что-то с нулевым, аналогично o1 == o2, тогда как во втором случае метод equals равен никогда не выполнялся, поэтому никакого сравнения не происходит вообще.

Что касается контракта .compareTo, сравнение ненулевого объекта с нулевым объектом похоже на попытку сделать это:

int j = 0;
if(j > null) { 
   ... 
}

Очевидно, это не скомпилируется. Вы можете использовать auto-unboxing для компиляции, но при сравнении вы получаете NPE, что согласуется с контрактом .compareTo:

Integer i = null;
int j = 0;
if(j > i) { // NPE
   ... 
}

Ответ 4

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

private static final String CONSTANT_STRING = "Some value";
String text = getText();  // Whatever getText() might be, possibly returning null.

Как бы то ни было, я могу сделать.

if (CONSTANT_STRING.equals(text)) {
    // do something.
}

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

if (text != null && text.equals(CONSTANT_STRING)) {
    // do something.
}

Является ли это достаточно хорошей причиной для того, чтобы поведение было таким, каким оно есть? Я не знаю, но это полезный побочный эффект.

Ответ 5

Если вы учитываете объектно-ориентированные концепции и рассматриваете все роли отправителя и получателя, я бы сказал, что поведение удобно. См. В первом случае, когда вы спрашиваете объект, если он никому не равен. Он ДОЛЖЕН сказать "НЕТ, Я НЕ".

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

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

Как один последний момент. Исключение с нулевым указателем следует поднимать, когда в вашем коде возникает ошибка. Однако, спрашивая объект, если он никто, не следует считать программным недостатком. Я думаю, что вполне нормально спросить объект, если он не является нулевым. Что делать, если вы не контролируете источник, который предоставляет вам объект? и этот источник отправляет вам null. Вы проверили бы, является ли объект нулевым и только потом увидите, равны ли они? Разве не было бы более интуитивно просто сравнить эти два, и что бы ни делал второй объект, сравнение будет проводиться без исключений?

Честно говоря, я был бы злится, если метод equals в своем теле возвращает цель с нулевым указателем. Равенство предназначено для использования против любого объекта, поэтому он не должен быть настолько придирчивым к тому, что он получает. Если метод equals возвратил npe, то последнее, что было на мой взгляд, состояло бы в том, чтобы это сделало это специально. Специально рассматривая это неконтролируемое исключение. Если вы поднимете npe, парень должен был бы помнить, чтобы всегда проверять значение null перед вызовом вашего метода или, что еще хуже, окружать вызов равным в блоке try/catch (Бог я ненавижу блоки try/catch). Но хорошо...

Ответ 6

Лично я бы предпочел, чтобы он выполнялся так же, как и он.

NullPointerException указывает, что проблема находится в объекте, против которого выполняется операция равенства.

Если NullPointerException использовался, как вы предлагаете, и вы попытались (вроде бессмысленной) операции...

o1.equals(o1) где o1 = null... Выбрасывается ли NullPointerException, потому что ваша функция сравнения завинчена или потому что o1 имеет значение null, но вы этого не понимаете? Крайний пример, я знаю, но с текущим поведением я чувствую, что вы можете легко сказать, где проблема.

Ответ 7

В первом случае o1.equals(o2) возвращает false, потому что o1 не равно o2, что отлично. Во втором случае он выбрасывает NullPointerException, потому что o2 равен null. Нельзя вызвать какой-либо метод на null. Это может быть ограничение языков программирования в целом, но мы должны жить с ним.

Также не рекомендуется бросать NullPointerException, что вы нарушаете контракт для метода equals и делаете вещи более сложными, чем это должно быть.

Ответ 8

Существует много распространенных ситуаций, когда null никоим образом не является исключительным, например. он может просто представлять (не исключительный) случай, когда ключ не имеет значения или иначе обозначает "ничего". Следовательно, выполнение x.equals(y) с неизвестным y также довольно распространено, и необходимость всегда проверять на null сначала будет просто потрачено впустую.

Что касается того, почему null.equals(y) отличается, это ошибка программирования для вызова любого метода экземпляра на нулевой ссылке в Java и поэтому заслуживает исключения. Порядок x и y в x.equals(y) следует выбирать так, чтобы x, как известно, не был null. Я бы сказал, что почти во всех случаях это переупорядочение может быть выполнено на основе того, что известно об объектах заранее (например, из их происхождения или путем проверки на null для других вызовов методов).

Между тем, если оба объекта имеют неизвестный "nullness", тогда другой код почти наверняка требует проверки хотя бы одного из них или не может быть сделано с объектом без риска NullPointerException.

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

Он мог бы быть указан по-разному (т.е. условие equals требует, чтобы аргумент был не null), поэтому это не означает, что ваша аргументация недействительна, но текущая спецификация делает более простой и практичный язык программирования.

Ответ 9

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

Использование методов экземпляра для равенства, сравнения и т.д. обязательно делает расположение асимметричным - немного хлопот для огромного выигрыша полиморфизма. Когда мне не нужен полиморфизм, я иногда создаю симметричный статический метод с двумя аргументами, MyObject.equals(MyObjecta, MyObject b). Затем этот метод проверяет, являются ли один или оба аргумента нулевыми ссылками. Если я специально хочу исключить нулевые ссылки, я создаю дополнительный метод, например. equalsStrict() или аналогичный, который выполняет нулевую проверку перед передачей другому методу.

Ответ 10

Обратите внимание, что контракт является "для любой ненулевой ссылки x". Таким образом, реализация будет выглядеть так:

if (x != null) {
    if (x.equals(null)) {
        return false;
    }
}

x не может быть null считаться равным null, потому что возможно следующее определение equals:

public boolean equals(Object obj) {
    // ...
    // If someMember is 0 this object is considered as equal to null.
    if (this.someMember == 0 and obj == null) {
         return true;
    }
    return false;
}

Ответ 11

Это сложный вопрос. Для обратной совместимости вы не можете этого сделать.

Представьте себе следующий сценарий

void m (Object o) {
 if (one.equals (o)) {}
 else if (two.equals (o)) {}
 else {}
}

Теперь с возвратом equals return else else будет выполняться, но не при вызове исключения.

Кроме того, null не является действительно равным произведению "2", поэтому имеет смысл возвращать false. Тогда, вероятно, лучше настаивать на null.equals( "b" ), чтобы вернуть также false:))

Но это требование делает странное и несимметричное равное отношение.