Javaのクラス継承時の振る舞いについて大きな勘違いをしていた。
注:Java以外のオブジェクト指向言語についも同様であるかは確認していない。
例えば、自動車クラスを定義する。
public static class Car { String name = "車両"; public boolean equals(Object obj){ return obj instanceof Car && name.equals(((Car)obj).name); } }
自動車クラスを継承して普通自動車クラスを定義する。
public static class Futusha extends Car { String haiki = "1000cc"; public boolean equals(Object obj){ return super.equals(obj) && obj instanceof Futusha && haiki.equals(((Futusha)obj).haiki); } }
メソッド引数で、親クラスとしてやりとりをして、実際はCarだったりFutushaだったりすると困ったことになる場合がある。
public static Car getCar(boolean isFutu){ if(isFutu){ return new Futusha(); }else{ return new Car(); } }
オーバーライドされたメソッドは、キャストしても外部からベースメソッドを参照できない。
public static void main(String []args){ Car car = getCar(false); Car futu = getCar(true); System.out.println("car.equals(futu) -> "+car.equals(futu)); System.out.println("futu.equals(car) -> "+futu.equals(car)); }
コンソール car.equals(futu) -> true futu.equals(car) -> false
実際に上記のようなケースに遭遇し、一向にequalsがtrueにならないという状況に陥った。
キャスト済みなら当然親クラスのメソッドがコールされると思っていたためである。
上記スレッド内では、クラス設計がそもそも間違っていると書かれている。
個人的には、抽象クラスや親クラスを継承するというのは、追加拡張するイメージであって、追加拡張前のメソッドも当然参照できるもの、という気がしていた。
そうでなければ、継承した瞬間に、ベースクラスが死んでしまうからだ。
しかし、Javaの継承という行為は、追加拡張ではなく、改変であり、インスパイアであり、コピペとも云えよう。
インタフェースが同じだからキャストできるだけで、中身は完全に別物なのだ。
継承の度にオーバーライドされるhashCodeメソッドが、常に最後にオーバーライドされたものをコールするからこそコレクションオブジェクトが機能出来るとも云えるが・・・
ちなみに、このケースでは、インスタンスメソッドのequalsだけでなく、static メソッドのequalsを定義し、インスタンスメソッドからstatic メソッドへ委譲することで解決した。
親クラスのequalsをコールしたい場合、キャストではなく、明示的にstatic メソッドをコールする。
※これが表題の解である。
public static void main(String []args){ Car car = getCar(false); Car futu = getCar(true); System.out.println("car.equals(futu) -> "+car.equals(futu)); System.out.println("futu.equals(car) -> "+futu.equals(car)); System.out.println("Car.equals(car, futu) -> "+Car.equals(car, futu)); System.out.println("Futusha.equals(car, futu) -> "+Futusha.equals(car, futu)); } public static class Car { String name = "車両"; public boolean equals(Object obj){ return Car.equals(this, obj); } public static boolean equals(Object obj1, Object obj2){ //引数の型はObjectではなくCarでもよい return (obj1 == obj2) || ( obj1 instanceof Car && obj2 instanceof Car && ((Car)obj1).name.equals(((Car)obj2).name) ); } } public static class Futusha extends Car { String haiki = "1000cc"; public boolean equals(Object obj){ return Futusha.equals(this, obj); } public static boolean equals(Object obj1, Object obj2){ return (obj1 == obj2) || ( obj1 instanceof Futusha && obj2 instanceof Futusha && Car.equals(obj1, obj2) && ((Futusha)obj1).haiki.equals(((Futusha)obj2).haiki) ); } }
コンソール car.equals(futu) -> true futu.equals(car) -> false Car.equals(car, futu) -> true Futusha.equals(car, futu) -> false
この方法であれば、もし staticメソッドをオーバーライドされても、期待した通り動作する。
実はもう一つ、勘違いしていたことがある。
当たり前だが親クラスの内部からオーバーライドされたメソッドをコールすると、子クラスのメソッドに飛ばされる。
たとえthisをつけてもオーバーライド前の自身のメソッドを呼び出すことは出来ない。
public class HelloWorld{ public static void main(String []args){ Car car = getCar(false); Car futu = getCar(true); car.echo(); futu.echo(); } public static Car getCar(boolean isFutu){ if(isFutu){ return new Futusha(); }else{ return new Car(); } } public static class Car { String name = "車両"; public void echo(){ //"クラス名>車両"が出力されることを期待している System.out.println(this.getClass().getName() + ">" + this.toString()); //thisはCarクラスとは限らない } public String toString(){ return name; } } public static class Futusha extends Car { String haiki = "1000cc"; public String toString(){ return name + "(" + haiki + ")"; } } }
コンソール HelloWorld$Car>車両 HelloWorld$Futusha>車両(1000cc)
つまり、親クラスは親クラス、継承クラスは継承クラス。
継承したのに、よく似た非なるもの。
もはや他人である。
やっぱJavaにも構造体ほしいな
クラスじゃ事故が起きる