詰めが甘い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を返すべし、となっているので、「==」で判定しても結果は同じ、ということで。