web-dev-qa-db-ja.com

完全一致を行わないequalsメソッドにロジックを含めることは良い考えですか?

大学のプロジェクトで学生を支援しながら、Javaフィールドでアドレスのクラスを定義したJavaが提供するエクササイズに取り組みました:

_number
street
city
zipcode
_

また、数値と郵便番号が一致する場合、equalsロジックはtrueを返すように指定しました。

Equalsメソッドはオブジェクト間の正確な比較のみを行うべきであると以前教えられました(ポインターをチェックした後)。これは私にはある程度の意味がありますが、それらが与えられたタスクと矛盾します。

部分一致でlist.contains()のようなものを使用できるようにロジックをオーバーライドする理由はわかりますが、これがコーシャと見なされているのか、そうでないのはなぜですか?

35
William Dunne

2つのオブジェクトの同等性の定義

同等性は、任意の2つのオブジェクトに対して任意に定義できます。誰かが好きなように定義することを禁止する厳密なルールはありません。ただし、実装されているもののドメインルールにとって意味がある場合、平等はしばしば定義されます。

等価関係契約 に従うことが期待されます:

  • これはreflexiveです。null以外の参照値xの場合、x.equals(x)はtrueを返します。
  • これはsymmetricです。null以外の参照値xおよびyの場合、x.equals(y)はy.equals( x)trueを返します。
  • これはtransitiveです。null以外の参照値x、y、zの場合、x.equals(y)がtrueを返し、y.equals( z)はtrueを返し、x.equals(z)はtrueを返す必要があります。
  • これはconsistentです。null以外の参照値xおよびyの場合、x.equals(y)の複数の呼び出しは常にtrueを返すか、一貫してfalseを返します。オブジェクトの等値比較で使用される情報が変更されていない場合。
  • Null以外の参照値xの場合、x.equals(null)はfalseを返す必要があります。

あなたの例では、おそらく、郵便番号と番号が同じである2つの住所を区別する必要はありません。次のコードが機能することを期待するのに完全に妥当なドメインがあります。

_Address a1 = new Address("123","000000-0","Street Name","City Name");
Address a2 = new Address("123","000000-0","Str33t N4me","C1ty N4me");
assert a1.equals(a2);
_

あなたが言及したように、これはそれらが異なるオブジェクトであることを気にしない場合に役立ちます-あなたはそれらが保持する値のみを気にします。おそらく、郵便番号+番地は正しい住所を特定するのに十分であり、残りの情報は「余分」であり、その追加情報が平等ロジックに影響を与えたくないでしょう。

これは、ソフトウェアにとって完全に優れたモデリングになる可能性があります。この動作を確認するためのドキュメントまたは単体テストがいくつかあり、パブリックAPIがこの使用を反映していることを確認してください。


hashCode()をお忘れなく

実装に関連するもう1つの詳細は、多くの言語がハッシュコードの概念を頻繁に使用しているという事実です。これらの言語、Javaを含め、通常、次の命題を想定しています:

X.equals(y)の場合、x.hashCode()とy.hashCode()は同じです。

以前と同じリンクから:

等しいオブジェクトは等しいハッシュコードを持つ必要があることを示すhashCodeメソッドの一般規約を維持するために、このメソッド(等しい)がオーバーライドされるときは常にhashCodeメソッドをオーバーライドする必要があることに注意してください。

同じhashCodeを持つことは、2つのオブジェクトが等しいことを意味しないことに注意してください!

その意味で、同等性を実装するときは、上記のプロパティに続くhashCode()も実装する必要があります。このhashCode()は、データ構造によって効率性と操作の複雑さの上限を保証するために使用されます。

優れたハッシュコード関数を思いつくのは難しく、それ自体がトピック全体です。理想的には、2つの異なるオブジェクトのhashCodeは異なるか、インスタンスオカレンス間で均等に分散している必要があります。

ただし、次の単純な実装は、「優れた」ハッシュ関数ではありませんが、等価プロパティを満たしていることに注意してください。

_public int hashCode() {
    return 0;
}
_

ハッシュコードを実装するより一般的な方法は、等価性を定義するフィールドのハッシュコードを使用して、それらにバイナリ演算を行うことです。あなたの例では、郵便番号と番地。それはしばしば次のように行われます:

_public int hashCode() {
    return this.zipCode.hashCode() ^ this.streetNumber.hashCode();
}
_

あいまいな場合は、明快さを選択してください

ここで私は、平等に関して何を期待すべきかについて区別します。平等に関する人々の期待は人によって異なります。 最小驚きの原則 に従うことを検討している場合は、設計をより適切に説明するために他のオプションを検討できます。

それらのどれが等しいと見なされるべきですか?

_Address a1 = new Address("123","000000-0","Street Name","City Name");
Address a2 = new Address("123","000000-0","Str33t N4me","C1ty N4me");
assert a1.equals(a2); // Are typos the same address?
_
_Address a1 = new Address("123","000000-0","John Street","SpringField");
Address a2 = new Address("123","000000-0","John St.","SpringField");
assert a1.equals(a2); // Are abbreviations the same address?
_
_Vector3 v1 = new Vector3(1.0f, 1.0f, 1.0f);
Vector3 v2 = new Vector3(1.0f, 1.0f, 1.0f);
assert v1.equals(v2); // Should two vectors that have the same values be the same?
_
_Vector3 v1 = new Vector3(1.00000001f, 1.0f, 1.0f);
Vector3 v2 = new Vector3(1.0f, 1.0f, 1.0f);
assert v1.equals(v2); // What is the error tolerance?
_

真実または偽りのそれぞれについてケースを作成できます。疑問がある場合は、ドメインのコンテキストでより明確な別の関係を定義できます。

たとえば、isSameLocation(Address a)を定義できます。

_Address a1 = new Address("123","000000-0","John Street","SpringField");
Address a2 = new Address("123","000000-0","John St.","SpringField");

System.out.print(a1.equals(a2)); // false;
System.out.print(a1.isSameLocation(a2)); // true;
_

または、ベクターの場合はisInRangeOf(Vector v, float range)

_Vector3 v1 = new Vector3(1.000001f, 1.0f, 1.0f);
Vector3 v2 = new Vector3(1.0f, 1.0f, 1.0f);

System.out.print(v1.equals(v2)); // false;
System.out.print(v1.isInRangeOf(v2, 0.01f)); // true;
_

このようにして、平等の設計意図をより適切に説明し、コードが実際に何をするかについての将来の読者の期待に反することを避けます。 (わずかに異なるすべての答えを見て、例の等価関係に関して人々の期待がどのように変化するかを確認することができます)

89
Albuquerque

それは、タスクの目的が演算子のオーバーライドを調査して理解することである大学の課題のコンテキストにあります。これは、その時点で価値のあるエクササイズとして表示されるようにするのに十分な暗黙の目的を持つ割り当ての例のようです。

ただし、これが私によるコードレビューである場合、これを重大な設計上の欠陥としてマークアップします。

問題はこれです。それは明らかに正しく見える構文的にクリーンなコードを有効にします:

if (driverLocation.equals(parcel.deliveryAddress)) { parcel.deliver(); }

そして、他のユーザーのコメントに基づいて、このコードは、郵便番号が通りに固有であるブラジルで正しい結果を生成します。ただし、この仮定が有効ではなくなった米国でこのソフトウェアを使用してみた場合、このコードは正しいように見えます。

これが次のように実装されている場合:

if (Address.isMatchNumberAndZipcode(driverLocation, parcel.deliveryAddress)) {
  parcel.deliver();
}

それから数年後、別のブラジルの開発者にコードベースが与えられ、ソフトウェアがカリフォルニアの新しい顧客の間違った住所に小包を配達すると言われたとき、今や壊れた仮定はコードで明白であり、決定ポイントで見ることができます配達するかどうか-これは、メンテナンスプログラマーが小包が間違った住所に配達された理由を確認するために最初に見る可能性が高い場所です。

明らかでないロジックがオペレーターのオーバーロードに隠れていると、コードの修正に時間がかかります。この問題をこのコードでキャッチするには、デバッガーがコードをステップ実行するセッションが必要になります。

42
Michael Shaw

平等は文脈の問題です。 2つのオブジェクトが等しいと見なされるかどうかは、関係する2つのオブジェクトの場合と同様に、コンテキストの問題です。

したがって、コンテキストではifは都市と通りを無視しても意味があり、郵便番号と番号のみに基づいて平等を実装しても問題はありません。 (コメントの1つで指摘されたように、郵便番号と番号areはブラジルの住所を一意に識別するのに十分です。)

もちろん、それに応じてhashCodeもオーバーロードすることを確認するなど、同等性をオーバーロードするための適切なルールに従うようにしてください。

25
Jörg W Mittag

等値演算子は、2つのオブジェクトが等しいと見なす場合にのみ、2つのオブジェクトが等しいと見なします。

繰り返します。あなたが役立つと考えるあらゆる考慮事項のため。

ソフトウェア開発者は、ここで運転席にいます。明らかな要件との整合性(a = a、a = bはb + a、a = bおよびb = cはa = cを意味します)とハッシュ関数との整合性は別として、等値演算子は好きなものにすることができます。

3
gnasher729

多くの回答が得られましたが、私の意見はまだ出ていません。

かつて、equalsメソッドはオブジェクト間の正確な比較のみを行うべきだと教えられました

ルールの説明とは別に、この定義は、人々がequalityについて話すときに、彼らの直感から推測するものです。平等は文脈に依存するとの回答もある。すべてのフィールドが一致しなくても、オブジェクトは同等であるという意味で正しいです。しかし、「等しい」という共通の理解を過度に再定義してはなりません。

トピックに戻りますが、同じ場所を指している場合、私には別の住所と同じです。

ドイツでは、たとえば郊外に名前が付けられている場合など、都市の仕様が異なる場合があります。次に、郊外SUBの住所の都市は、「主要都市」のみ、または「主要都市、SUB」または「SUB」のみとして指定できます。メインの都市名を付けることは問題ないので、都市のすべてのストリート名とそれに割り当てられた郊外は一意である必要があります。

ここでは、都市名が異なる場合でも、郵便番号は都市を示すのに十分です。
しかし、郵便番号が1つの有名な通りを指している場合を除いて、通りを離れることは一意ではありません。
したがって、2つのアドレスが、無視されたフィールドで構成されている異なる場所を指すことができる場合、それらを等しいと見なすことは直観的ではありません。

すべてではなく一部のみを必要とするユースケースがある場合は、それを行う比較メソッドに適切な名前を付ける必要があります。 「等しい」メソッドは1つだけあり、こっそりと「特別な1つのユースケースでのみ等しい」に変換すべきではありませんが、誰もそれを見ることができません。

つまり、説明した理由から私は言います...

これはコーシャと見なされているのでしょうか

誤って通りの名前が重要ではない場所にいる場合、知識がなければ:いいえ、そうではありません。
そのような場所で使用されるだけでなく、何かをプログラムしたい場合:いいえ、そうではありません。
生徒に正しいことをしてもらい、コードをわかりやすく論理的に保つ感覚を与えたい場合:そうではありません。

2
puck

与えられた要件は人間の感覚と矛盾しますが、オブジェクトプロパティのサブセットのみが「一意」の意味を定義できるようにしても問題ありません。

ここでの問題は、equals()hashcode()の間に技術的な関係があるため、2つのオブジェクトについて、そのタイプのabが:
if a.equals(b) then a.hashcode()==b.hashcode()
一意性条件を定義するプロパティのサブセットがある場合は、同じサブセットを使用してhashcode()の戻り値を計算する必要があります。

結局のところ、要件に対するより適切なアプローチは、ComparableまたはカスタムのisSame()メソッドを実装することだったかもしれません。

1
Timothy Truckle

状況によります

それは良い考えですか...?状況によります。たとえば、一度だけで使用されるアプリケーションを開発している場合、たとえば、大学の割り当てで使用することをお勧めします(割り当て後にコードを破棄する場合)確認済み)、または一部の移行ユーティリティ(レガシーデータを一度移行すると、ユーティリティは不要になります)。

しかし、IT業界では多くの場合それは悪い考えです。どうして? @JörgW Mittagは、平等は文脈の問題です...あなたの文脈でそれが理にかなっているなら... と言いました。しかし、多くの場合、同じオブジェクトが多くの異なるコンテキストで使用されており、同等性に関する different のビューがあります。同じエンティティの同等性をどのように異なる方法で定義できるかのほんのいくつかの例:

  • 2つのエンティティの all 属性の同等性
  • 2つのエンティティの主キーの等価性
  • 2つのエンティティの主キーとバージョンの同等性
  • 主キーとバージョンを除くすべての「ビジネス」属性の平等として

equals()で特定のコンテキストのロジックを実装すると、プロジェクト内のチームの多くの開発者が後でこのオブジェクトを他のコンテキストで使用するのが難しくなります。そこにコンテキストが正確に実装されているロジックと、そのコンテキストに依存できるロジックが正確にわからない。場合によっては(@Michael Shawの説明のように)誤って使用することもあれば、ロジックを無視して同じ目的で独自のメソッドを実装することもあります(期待とは異なる動作をする場合があります)。

アプリケーションを使用する場合長時間 2〜3年のように、通常、複数の新しい要件、複数の変更、および複数のコンテキストがあります。そして、おそらく、平等に対する multiple 異なる期待があるでしょう。それが私が提案する理由です:

  • equals()を正式に実装すると、ビジネスコンテキストに接続せずに、すべてのオブジェクト属性が等しいのと同じように、ビジネスロジックがないことになります(もちろん、hashCode/equals規約に従う必要があります)。
  • isPrimaryKeyAndVersionEqual() areBusinessAttributesEqual()のように、コンテキストごとに、このコンテキストの意味での同等性を実装する個別のメソッドを提供します。

次に、特定のコンテキストでオブジェクトを見つけるには、次のように、対応するメソッドを使用します。

if (list.sream.anyMatch(e -> e.isPrimaryKeyAndVersionEqual(myElement))) ...

if (list.sream.anyMatch(e -> e.areBusinessAttributesEqual(myElement))) ...

したがって、コード内のバグが少なくなり、コード分析が容易になり、新しい要件に合わせてアプリケーションを変更することが容易になります。

1
mentallurg

他の人が述べたように、一方で、等式はいくつかの特性を満たす数学的概念にすぎません(たとえば、 アルバカーキ 回答を参照)。一方、そのセマンティックと実装はコンテキストによって決まります。

実装の詳細に関係なく、算術式を表すクラス((1 + 3) * 5など)を例にとります。算術式の標準評価ルールを使用してそのような式のインタープリターを実装する場合、(1 + 3) * 5および10 + 10のそれぞれのインスタンスをequalと見なすことは理にかなっています。ただし、上記のような式にプリティプリンターを実装する場合、インスタンスはequalとは見なされませんが、(1 + 3) * 5(1+3)*5はそうします。

0
michid

他の人が述べたように、オブジェクトの等価性の正確なセマンティクスはビジネスドメインの定義の一部です。この場合、Addressnumberstreetcityzipcodeを含む)のような「一般的な」オブジェクトが非常に狭い平等の定義を持つことは合理的ではないと思います(他の人が述べたように、ブラジルで機能しますが、たとえば米国ではありません)。

代わりに、Addressに等価性の値のようなセマンティクス(すべてのメンバーの等価性によって定義される)を持たせます。その後、私は次のいずれかを行います:

  1. StreeNumberAndZipstreetのみを含み、zipCodeを定義するequalsクラス(_# TODO: bad name_)を作成します。その特定の方法で2つのAddressオブジェクトを比較する場合はいつでも、addressA.streetNumberAndZip().equals(addressB.streetNumberAndZip())、または...
  2. bool equalStreeNumberAndZipCode(Address a, Address b)メソッドを使用してAddressUtilsクラスを作成します。

どちらの場合でも、完全な等価性チェックのためにaddressA.equals(addressB)を使用するアクセス権があります。

オブジェクトのnフィールドの場合、_2^n_の同等性の異なる定義があります(各フィールドをチェックに含めたり、チェックから除外したりできます)。さまざまな方法で同等性を確認する必要がある場合は、_enum AddressComponent_のようなものを使用すると便利な場合もあります。次にbool addressComponentsAreEqual(EnumSet<AddressComponent> equatedComponents, Address a, Address b)を使用できるので、次のようなものを呼び出すことができます

_bool addressAreKindOfEqual = AddressUtils.addressComponentsAreEqual(
    new EnumSet.of(
        AddressComponent.streetNumber, 
        AddressComponent.zipCode,
    ),
    addressA, addressB
);
_

これは明らかにタイピングがはるかに多くなりますが、等価性チェックメソッドの急激な爆発を防ぐことができます。

平等は正しくするために微妙であり、その重要性は一見広範囲に及んでいます。特に、等価演算子を実装するということは、オブジェクトがセットやマップを使ってニースを再生することになっているという意味です。

圧倒的多数の場合、同等性は同一性である必要があります。つまり、オブジェクトが他のオブジェクトと等しいのは、それがである場合同じ住所。恒等関係は常に、適切な等価関係(反射性、推移性など)のすべての条件を尊重します。また、2つのポインターを比較するだけなので、アイデンティティは2つのものを比較する最も速い方法です。同等性関係の規約を尊重することは、同等性の実装に関する最も重要なことの1つです。これを怠ると、診断が困難なことで悪名高いバグに変換されるためです。

等しいを実装する2番目の方法は、型が一致するかどうかを比較してから、オブジェクトのすべての「所有」フィールドを比較することです。これは、多くの場合、すべてのオブジェクトの詳細に再帰します。オブジェクトがequalsを呼び出すデータ構造に入る場合、このアプローチを使用する場合、equalsはおそらくデータ構造がそのほとんどの時間を費やすものになります。他の問題があります:

  • オブジェクトが変更されると、他のオブジェクトとの比較結果も変更されます。これにより、標準クラスが同等性について行うあらゆる種類の仮定が破られます。
  • オブジェクトがクラス/インターフェイス階層にある場合、その階層内の2つのオブジェクトを比較する唯一の健全な方法は、具体的な型が完全に一致するかどうかです(Joshua Blochの優れたを参照してください)効果的なJavaこの詳細については、本を参照してください);
  • できるだけ多くのフィールドを含めることにより、等式関係を非常に厳密にしようとすると、最終的には、等式が「同一」のビジネスロジックに対応しない状況になります。

3番目の方法は、ビジネスロジックに関連するフィールドのみを選択し、残りは無視することです。このアプローチが破られる可能性は、恣意的に1に近くなります。他の人が述べた最初の理由は、oneコンテキストで意味のある比較では、 allコンテキストでは、必ず意味があります。ただし、言語はone形式の等価性を定義することを要求するため、すべてのコンテキストで機能します。アドレスの場合、そのような比較ロジックは存在しません。特別な「2つのアドレスlook同一」メソッドを使用できますが、そのようなメソッドが言語に裏付けられた唯一の真の比較方法であるというリスクはありませんそれは必然的に読者を混乱させるでしょう。

また、私はFalsehoodsプログラマーがアドレスを信じていることを確認することをお勧めします: https://www.mjt.me.uk/posts/falsehoods-programmers-believe-about-addresses/ それは楽しい読み物であり、いくつかの落とし穴を避けるのに役立つかもしれません。

0
Kafein