Це погана ідея, якщо equals (null) замість цього викидає NullPointerException?


75

Договір щодо, що 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перевіряє, чи є якийсь інший об'єкт "рівним"this
  • nullпосилання не дає жодного іншого об'єкта для тесту
  • Тому equals(null)слід кидатиNullPointerException

2
коментуючи тут, що відомо, що на Java, з рівними (), присутніми на самій вершині ієрархії ОО, неможливо поважати договір рівних ні для чого, крім найпростішого випадку (тобто, коли ви цього не робите ОО взагалі). Думка про те, що існує таке поняття, як непорушений контракт Java дорівнює () , марення. Ми йдемо набагато далі: за замовчуванням equals () і hashCode () кидають UOE. Якщо ви хочете використовувати ці методи, ви повинні задокументувати, як ви вирішуєте
SyntaxT3rr0r

1
8 голосів і 3 обраних на моє запитання, пов’язані з безперечною розбитістю рівних тут: stackoverflow.com/questions/2205565 Справа в тому, що "загальноприйнята рівність мудрості" просто не працює. Це говорять не тільки такі люди, як Джошуа Блох та Мартін Одерський, але ви можете використовувати логіку, щоб довести цей факт. Ви просто не можете виконати OOA / OOD для перекладу ООП і сподіватися повторно використати концепцію рівності Java: для мене це фундаментальний недолік мови, яка дорівнює , присутня в Object. Звичайно, люди, котрі п'ють кухонну допомогу Гослінга, не погодяться. Нехай вони сперечаються з Блохом
SyntaxT3rr0r

1
моя остання думка така: у багатьох випадках мова йде не про кидання NPE або повернення false: мова йде про кидання величезного великого UnsupportedOperationException, і це недолік Java, який дозволяє викликати рівних для об'єктів, які не повинні мати саме це поняття рівності в перше місце. Відомий останнє слово: UnsupportedOperationException :)
SyntaxT3rr0r

Відповіді:


105

Щодо питання, чи ця асиметрія несуперечлива, я думаю, що ні, і я звертаюся до цього древнього дзен-кхана:

  • Запитайте будь-якого чоловіка, чи він настільки хороший, як наступний, і кожен скаже так.
  • Запитайте будь-якого чоловіка, чи хороший він як ніхто, і кожен скаже ні.
  • Не питайте нікого, чи це так добре, як будь-який чоловік, і ви ніколи не отримаєте відповіді.

У той момент упорядник досяг просвітлення.


6
Цікаво, чи справді про це думали дизайнери мови програмування Java, чи вони просто не помітили асиметрії ... ;-)
Jesper,

1
Коан не пишеться ко-ан, і, мабуть, над макросом є макрон.
Digital Impermanence

Гаразд, я виправлю це, оскільки це, комічно, майже моя найпопулярніша відповідь. Потрібно правильно зрозуміти.
Шон Оуен,

19

Винятком справді має бути виняткова ситуація. Нульовий покажчик може не бути помилкою програміста.

Ви цитували існуючий контракт. Якщо ви вирішите піти проти конвенції, через весь цей час, коли кожен розробник Java очікує, що рівне поверне false, ви зробите щось несподіване і небажане, що зробить ваш клас парією.

Я не міг не погодитися більше. Я б не переписував рівне, щоб постійно видавати виняток. Я б замінив будь-який клас, який це зробив, якби я був його клієнтом.


Мені подобаються міркування у питанні (ви не можете використовувати змінну, якщо вона є null) - але @duffymo має рацію, і підтримка його відповіді походить від того, як сам код Java реалізує .equals()для багатьох класів (перевірте вихідний код). Спочатку перевіряється рівність за допомогою ==, а потім перевіряється нерівність за допомогою instanceOf. За визначенням обидві ці перевірки повернуть логічне значення без NullPointerException- тобто даний об'єкт не використовується в .equals()методі, поки не буде доведено, що він не є null. Тому, коли це можливо, вам слід писати власні класи, щоб поводитись однаково.
PMorganCA

8

Подумайте, як .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) { 
   ... 
}

Очевидно, це не буде скомпільовано. Ви можете використовувати автоматичне розпаковування, щоб зробити його компіляцією, але ви отримуєте NPE під час порівняння, що узгоджується з контрактом .compareTo:

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

Я б стверджував, що існує принципова різниця між операторами рівності (==) та реляційними (<,>, <=,> =), коли мова йде про об'єкти. Реляційні об'єкти не сприймають об'єкти як операнди (навіть якщо вони одного типу), тому замість них використовується метод compareTo. З іншого боку, метод equals не замінює оператор рівності. Оператор рівності все ще може бути використаний на об'єктах (за умови, що вони однотипні) і має іншу мету, ніж метод equals.
Shazz

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.
}

Чи є це достатньо вагомою причиною, щоб поведінка була такою, якою вона є ?? Не знаю, але це корисний побічний ефект.


4

Якщо взяти до уваги об’єктно-орієнтовані концепції та врахувати всі ролі відправника та одержувача, я б сказав, що поведінка зручна. Побачте, у першому випадку ви запитуєте об’єкт, чи він рівний нікому. Йому ПОВИННО сказати "НІ, я не".

Однак у другому випадку ви не маєте посилання ні на кого, отже, ви насправді ні в кого не питаєте. ЦЕ має спричинити виняток, перший випадок - ні.

Я думаю, що це асиметрично лише в тому випадку, якщо ти наче забудеш про орієнтацію об’єкта і ставишся до виразу як до математичної рівності. Однак у цій парадигмі обидва кінці відіграють різні ролі, тож слід очікувати, що порядок має значення.

Як останній момент. Виникнення нульового покажчика слід викликати, коли у вашому коді є помилка. Однак запитувати об’єкт, якщо він ніхто, не слід вважати вадою програмування. Я думаю, що цілком нормально запитати об’єкт, якщо він не нульовий. Що робити, якщо ви не контролюєте джерело, яке надає вам об’єкт? і це джерело надсилає вам нуль. Чи перевірили б ви, чи є об’єкт нульовим, і лише згодом перевірити, чи рівні вони? Чи не було б більш інтуїтивно просто порівняти два, і незалежно від того, яким є другий об’єкт, порівняння буде проводитися без винятків?

Чесно кажучи, я був би озлоблений, якщо метод equals у своєму тілі спеціально поверне виняток нульового покажчика. Equals призначений для використання проти будь-якого об'єкта, тому він не повинен бути таким вимогливим до того, що він отримує. Якби метод equals повернув npe, останнє, що мені здається, було те, що він це зробив спеціально. Особливо враховуючи, що це неперевірений виняток. ЯКЩО ви підняли npe, хлопець мав би пам'ятати, щоб завжди перевіряти на нуль, перш ніж викликати ваш метод, або навіть гірше, оточити виклик рівним у блоці try / catch (боже, я ненавиджу блоки try / catch) Але ну добре. ..


1
Питання "Ви ні з ким не рівні" є дещо сумнівним. З іншого боку, equalsне дається об’єкт - йому дається посилання на об’єкт . Питання насправді полягає в тому, "чи ідентифікує це посилання об’єкт, який вам дорівнює". На таке запитання можна легко відповісти "Ні - оскільки це посилання не ідентифікує жодного об'єкта, воно, безумовно, не ідентифікує еквівалентний мені об'єкт".
supercat

Далі зауважте, що переформульоване питання виправдовує порівняння рівності, що включають будь-яку комбінацію типів об’єктів. Хтось, кого запитують "Чи дорівнює ти червоному кольору", може вважати запитання неправильним, але "Чи посилання [яке ідентифікує червоний колір] стосується об'єкта, який еквівалентний тобі", це не так. Посилання визначає те, що не є рівнозначним людині, тож відповідь "ні".
supercat

Я розумію різницю, яку ви зробили, і з цим погоджуюсь. На практиці ви сказали те, що я кажу, лише з більш точними та технічними умовами. Якщо ви отримаєте протилежну думку з моєї відповіді, то, можливо, я погано висловився.
arg20

Ви досить добре висловились; я хотів запропонувати однозначне переформулювання питання, яке, безперечно, не мало б проблем навіть із нульовим посиланням. Шкода, що немає стислого позначення для виконання перевірки "дорівнює" для пари об'єктів, один або обидва з яких можуть бути нульовими.
supercat

Одне, що я хотів би бачити в сучасній об'єктно-орієнтованій мові, до речі, було б засобом декларативно або синтаксично розрізнення випадків, коли посилання інкапсулює ідентичність об'єкта , різні випадки, коли воно інкапсулює значення (з можливістю розрізнення різні комбінації спільного та неділеного, змінного проти лише для читання проти незмінного та безпечного для мутації проти не заважати), а також випадкові випадки, коли посилання має бути суттю для себе. Java дуже слабка в цьому плані, і, на жаль, .NET запозичує значну частину своєї слабкості.
supercat

2

Особисто я волів би, щоб він працював так, як це робить.

NullPointerException ідентифікує , що проблема полягає в об'єкті , проти якого операція дорівнюватиме може бути виконана.

Якщо NullPointerExceptionбуло використано, як ви пропонуєте, і ви спробували (начебто безглуздо) операцію ...

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


2

У першому випадку o1.equals(o2)повертає false, оскільки o1не дорівнює o2, що цілком нормально. У другому випадку він кидає, NullPointerExceptionоскільки o2є null. На a не можна викликати жодного методу null. Це може бути обмеженням мов програмування загалом, але нам доведеться з цим жити.

Також не є гарною ідеєю кидати, що NullPointerExceptionви порушуєте контракт на equalsметод і робите речі більш складними, ніж це повинно бути.


2

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

Що стосується того, чому null.equals(y)різниться, викликати будь-який метод екземпляра за нульовим посиланням у Java є помилкою програмування , і тому гідним винятку. Впорядкування і в повинно бути вибрано таким чином, що , як відомо, не може бути . Я б стверджував, що майже у всіх випадках це переупорядкування можна виконати, виходячи з того, що відомо про об'єкти заздалегідь (наприклад, від їх походження, або перевіряючи наявність інших викликів методів).xyx.equals(y)xnullnull

Тим часом, якщо обидва об'єкти мають невідому "нікчемність", тоді інший код майже напевно вимагає перевірки хоча б одного з них, або з об'єктом неможливо зробити багато, не ризикуючи NullPointerException.

І оскільки це вказано таким чином, помилка програмування полягає в розриві контракту та введенні винятку для nullаргументу equals. І якщо ви розглядаєте альтернативу вимагання викиду викиду, тоді кожна реалізація equalsповинна мати окремий випадок, і кожен виклик equalsбудь-якому потенційно nullоб'єкту повинен перевіряти перед викликом.

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


Ви можете фактично викликати статичні методи за нульовим посиланням, тому ваша відповідь не зовсім точна. Зрозуміло, що це дуже недобре робити.
CurtainDog

1

Зверніть увагу, що контракт "для будь-якого ненульового посилання 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;
}

1

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

Використання методів екземплярів для рівності, порівняння тощо, обов'язково робить розташування асиметричним - трохи клопоту для величезного виграшу поліморфізму. Коли мені не потрібен поліморфізм, я іноді створити симетричний статичний метод з двома аргументами MyObject.equals(MyObjecta, MyObject b). Потім цей метод перевіряє, чи є один або обидва аргументи нульовими посиланнями. Якщо я спеціально хочу виключити нульові посилання, тоді я створюю додатковий метод, наприклад equalsStrict()або подібний, який робить нульову перевірку перед делегуванням до іншого методу.


0

Це каверзне запитання. Для зворотної сумісності ви цього не можете зробити.

Уявіть такий сценарій

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

Тепер із рівним поверненням false буде виконуватися речення else, але не під час створення винятку.

Також null насправді не дорівнює вимові "2", тому має сенс повернути false. Тоді, мабуть, краще наполягати null.equals ("b"), щоб повернути також false :))

Але ця вимога робить дивне та несиметричне відношення рівних.


0

Ви повинні повернути, falseякщо параметр єnull .

Для того, щоб показати , що це стандарт, см ' Objects.equals(Object, Object)від java.util, який виконує асиметричних перевірки нуля на перший параметрі тільки (на який equals(Object)буде називатися). З вихідного коду OpenJDK SE 11 (SE 1.7 містить абсолютно те саме):

public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}

Це також обробляє два nullзначення як рівні.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.