オブジェクトを扱うとき最も多く使われうるメソッドの一つがequalsメソッドでしょう。
このメソッドは、オブジェクト間の関係を扱うので使う時、注意が必要です。
今回の規則では、このメソッドを定義するときの守るべきルールを紹介しています。
まず、equalsメソッドのオーバーライドが必要ない場合を並べております。
1.それぞれのオブジェクトが一つのデータを持つ時。
基本形データ(value)のみで構成された変数ではなく、アクティブなオブジェクト(active entity)で構成されたスレッドのようなクラスは、
単純な実行ユニットであるため、Objectのequalsメソッドをそのまま使用しても構いません。
2.クラスで論理同一性の検査を要求しない場合。
例としてjava.util.Randomクラスがあります。
このクラスでのequalsはランダムオブジェクトが同じ乱数列で作成されたのか、いないかを確認することができたはずですが、このクラスを作成した人はそのような機能はクライアントが望まないと思いました。このような時はオーバーライドせず、単にオブゼダートを継承し使って頂けて大丈夫です。
3.上位クラスでオーバーライドしたequalsがサブクラスでも適している場合。
上位から継承したequalsがサブで使用しても無理がない場合、
3.上位クラスでオーバーライドしたequalsがサブクラスでも適している場合。
上位から継承したequalsがサブで使用しても無理がない場合、
equalsをオーバーライドしないままでも大丈夫です。
代表的な例えで、Setクラス(HashSet、TreeSetなど)は、
代表的な例えで、Setクラス(HashSet、TreeSetなど)は、
abstractSetのequalsメソッドをそのまま使います。
Setと同様、MapクラスもAbstractMapのequalsをそのまま使用します。
4.クラスがprivate、またはpackage-privateで宣言されequalsを使う必要がない場合。
内部クラスとして使用するため、クラスをprivateとして宣言する場合に
Setと同様、MapクラスもAbstractMapのequalsをそのまま使用します。
4.クラスがprivate、またはpackage-privateで宣言されequalsを使う必要がない場合。
内部クラスとして使用するため、クラスをprivateとして宣言する場合に
equalsを使う場面が出ません。
この場合、equalsをオーバーライドしないままで結構ですが、
この場合、equalsをオーバーライドしないままで結構ですが、
もしものミスを防ぐために、以下の方法で再定義をしてくれることをお勧めします。
@Override public Boolean equals(Object o){
throw new AssertionError();
}
これらのような場合は、equalsを上書きする必要はありません。
じゃあ、どのようなの場合にオーバーライドすればよういでしょうか?
以下をご覧ください。
2.上位クラスのequalsがサブクラスのニーズを満たしていない場合
通常のデータ(value)クラスの場合が多いです。
データクラスはIntegerやDateように、特定のデータを表現すます。
この時 普通 使用者は、これらのデータのアドレスの同一性(オブゼダート同一性)ななく同一性を比較したいはずです。
この場合、再定義をしてくれれば、これらの要求を満足させることができます。
equalsメソッドをオーバーライドする必要がないデータクラスもあります。
enumクラスが代表的です。
このオブジェクトは、一つのオブジェクトのみを使うように設計されているので、オブゼダート同一性が論理同一性につながります。
通常のデータ(value)クラスの場合が多いです。
データクラスはIntegerやDateように、特定のデータを表現すます。
この時 普通 使用者は、これらのデータのアドレスの同一性(オブゼダート同一性)ななく同一性を比較したいはずです。
この場合、再定義をしてくれれば、これらの要求を満足させることができます。
equalsメソッドをオーバーライドする必要がないデータクラスもあります。
enumクラスが代表的です。
このオブジェクトは、一つのオブジェクトのみを使うように設計されているので、オブゼダート同一性が論理同一性につながります。
1.オブジェクトの同一性ではなく、で、論理同一性を比較しようとする時
当然ながらこのような場合は、Trueが返されます。オブジェクトの同一性が証明されました。
一方、論理同一性の場合には、
各オブジェクトを構成するデータの比較がクライエントの希望を準する状態を意味します。
四角形を構成するときに横二つの線の長さが同じであれば
同じ図形として見たいクライアントがいらっしゃると仮定してみましょう。
この場合、使用者のニーズに合わせてequalsをオーバーライドする必要があります
2.上位クラスのequalsがサブクラスのニーズを満たしていない場合
通常のデータ(value)クラスの場合が多いです。
データクラスはIntegerやDateように、特定のデータを表現すます。
この時 普通 使用者は、これらのデータのアドレスの同一性(オブゼダート同一性)ななく同一性を比較したいはずです。
この場合、再定義をしてくれれば、これらの要求を満足させることができます。
equalsメソッドをオーバーライドする必要がないデータクラスもあります。
enumクラスが代表的です。
このオブジェクトは、一つのオブジェクトのみを使うように設計されているので、オブゼダート同一性が論理同一性につながります。
必要によってequalsをオーバーライドする時の一般的な規則は以下になります。
- 反射性
nullではない変素xについて、「x.equals(x)」このメソッドはtrueを返します。
- 対称性
nullではない変素xとyについて、
x.equals(y)は、y.equals(x)がtrueの場合にのみtrueを返します。
- 推移性
nullではない変素x, y, zについて、
x.equals(y)が真であり、y.equals(z)が真の場合
x.equals(z)も真を返します。
- 一貫性
nullではない変素xとyについて、
equalsを使用して比較される情報に変化がないなら、
何度繰り返しても同じ結果を維持します。
一つずつ検討してみます。
まず、下のコードをご覧ください。
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
if (s == null)
throw new NullPointerException();
this.s = s;
}
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
if (o instanceof String)
return s.equalsIgnoreCase((String) o);
return false;
}
}
コンストラクタに文字列を受け取り、保管するクラスです。
equalsignoreCaseメソッドは、大文字と小文字を区別しないequalsメソッドです。
このクラスのequals定義には問題があります。
CaseInsensitiveStringのequalsメソッドは、Stringオブジェクトの知っていますが
大変なことになります。
CaseInsensitiveString cis = new CaseInsensitiveString("l");
List<CaseInsensitiveString> list = new ArrayList<CaseInsensitiveString>();
list.add(cis);
System.out.println(list.contains(cis));
このような状況でcontainsメソッドを使用すると、どのような結果を得るのです。
一度、私のジャワ環境では、trueが返されますが、JVMのバージョンによってtrueが返されることもあり、falseが返される可能性もあります。
一定の結果を返しできないプログラムは、プログラムとして致命的です。
このため、常に対称性を念頭においてください。
@Override
public boolean equals(Object o) {
return o instanceof CaseInsensitiveString &&((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
上記コードは、
equalsがStringオブジェクトと相互作用しないよう直しました。
また、次のコードを一度ください。
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}
}
2次元空間の座標をとるコードです。
このクラスを継承して座標に続いて色を追加したクラスとequalsを作ってみます。
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
}
equalsは、上位クラスのequalsとカラー情報を選び出す機能をします。
このequals上書きの問題は、順序を変える瞬間、同じ結果を返さないことです。
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
p.eqauls(cp);
cp.equals(p);
上記のコードで考えてみます。
pのequalsを呼び出した瞬間、Javaのポリモーフィズムによって
ColorPointはPointにも扱われるので、if文は軽くパスし
xとyのデータを比べてみて、問題なくtrueを返すことになります。
一方cpのequalsの場合、
PointはColorPointとして扱われないので、if文をパスできずにfalseを返します。
これが対称性が崩れる例になります。
この問題を解決するため、
色情報を無視するコードを絞ても果たして大丈夫でしょうか?
以下は、上記のColorPointのequalsを修正したものです。
@Override public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
if (!(o instanceof ColorPoint))
return o.equals(this);
return super.equals(o) && ((ColorPoint)o).color == color;
}
パラメータの可能性は3つです。
1. Point - >第二ifでパラメータ自身のequalsを呼び出してColorPrintと比べます。
上記の場合には、Pointはcolorを持ってないのでtrueを返します。
2. ColorPrint - >最後のreturnで上位クラス(Point)のequalsと
パラメータのcolorを比べて返します。
3.その他のオブジェクト - >最初のifでfalseを返します。
このように直せば、対称性は維持がされまるが推移性崩れます。
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
p1.equals(p2)とp2.equals(p3)は、trueを返しますが、
p1.equals(p3)はfalseを返します。
p1とp2は色情報を除いて比べたのでtrueだったが、
p1とp3は色情報も検査したためです。
これらの問題は、オブジェクト指向の抽象化の利点を使わないつもりならまだ知らず
継承を使い新しいデータをコンポーネントを追加し
equals規約を破らない方法はありません。
しかし、この本ではこのような悩みを避けることができる
デザインパターンを紹介しています。
public class ColorPoint{
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color){
if(color == null)
throw new NullPointerException();
point = new Point(x,y);
this.color = color;
}
public Point asPoint(){
return point;
}
@Override public boolean equals(Object o){
if (!(o instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
}
JAVAでよく使う継承ではなく、
ColorPrintという名前のオブジェクトを
Pointオブジェクト+ colorオブジェクト属性で構成する方式です。
オブジェクトがequalsに入ると、まずColorPointオブゼダートかを確認をします。
もしPointがequalsに入ってくる場合は、falseを返します。
そしてパラメータのPointと自分のPointを比べた後、
パラメータのcolorと自分のcolorを比較します。
二つ共一致した場合はtrueを、そうではない場合falseを返します。
すべてのニーズを満足させる望ましい1つの方法がない以上、
私はこの構成パターンをとったコードが最善策だと思います。
最後に一貫性についてお話します。
一貫性はほとんどの環境では守られますが
オブジェクトのデータが変化する場合には、崩れます。
代表的な例がjava.net.URLのequalsメソッドです。
このメソッドでは、
URlに対応するホストのIPアドレスを比較してequalsの戻りデータを決定します。
問題は、ホスト名をIPアドレスに変換するためには、
ネットワークに接続する必要がありまするが、これはつねに同じ結果を保証しない点です。
ネット上にサイトのドメイン名は普通変わらないですが、
IPはよく変わります。
この時一貫性が敗れる恐れがあります。このため、注意が必要です。
댓글 없음:
댓글 쓰기