HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

詰めが甘いObject#equals()のオーバーライド

「もれなくダブりなく」とは、書籍「プログラマの数学」に書かれていることだが、そんな「条件がもれてしまった」という爆弾コード。

public class Xxx {
    public boolean equals(Object o) {
        if (!(o instanceof Xxx)) {
            return false;
        }
        Xxx that = (Xxx) o;
        return
            getAaa().equals(that.getAaa()) &&
            ((getBbb()==null && that.getBbb()==null) || getBbb().equals(that.getBbb())) &&
            getCcc().equals(that.getCcc());
    }
}

で、Xxxクラスの仕様

  • String getAaa():nullを返さない。
  • String getBbb():nullを返す可能性がある。
  • String getCcc():nullを返さない。

さて、このequals()メソッドの実装には問題があります。パッと見てわかりますか?

・・・

答えは、仕様の二番目にある「getBbb():nullを返す可能性がある」に起因する問題。

getBbb()に対する条件判定の部分に注目し、"this.getBbb()"と"that.getBbb()"の値について見てみます。

  • "this.getBbb()"と"that.getBbb()"が両方nullの場合
    • 前半の条件「getBbb()==null && that.getBbb()==null」がtrueとなるので、全体としてtrueになります。
  • "this.getBbb()"と"that.getBbb()"が両方nullでない場合
    • 前半の条件はfalseとなるので、後半の条件判定に移りますが、結果はString#equals()メソッドの結果となります。
  • "this.getBbb()"がnullでなく、"that.getBbb()"がnullの場合
    • 前半の条件はfalseとなるので、後半の条件判定に移りますが、x.equals(null) の結果なのでfalseとなります。
  • では、"this.getBbb()"がnullで、"that.getBbb()"がnullでない場合は?
    • 前半の条件はfalseとなるので、後半の条件判定ですが、"this.getBbb()"はnullであるのに、そのequals()メソッドを呼ぼうとしています。結果として、NullPointerExceptionが発生してしまいます。

値がnullとなる可能性のある2値の比較に関する詰めの甘さが招いたこの爆弾コードですが、このようなケースでは以下が正しい判定条件です。

this.getBbb() == null && that.getBbb() == null
    ||
this.getBbb() != null && this.getBbb().equals(that.getBbb())

もちっとバイト数を減らして(謎)以下のようなのでもOK。

this.getBbb() == that.getBbb()
    ||
this.getBbb() != null && this.getBbb().equals(that.getBbb())

「this.getBbb() == that.getBbb()」という条件では、両方がnullの場合はtrueになるし、両方がnullでない同一のオブジェクトの場合、equals()メソッドの実装規約によりtrueを返すべし、となっているので、「==」で判定しても結果は同じ、ということで。