<<前ページ目次次ページ>>
18.温故知新
〜toStringとequals〜
 すべてのクラスは、そのスーパークラスをたどっていくと、java.lang.Objectに行き着きます。ということはご先祖様であるクラスObjectの財産は把握しておかねばなりません。その中でも基本となるメソッドtoStringとequalsについて見ていきましょう。


オリジナルはどうなってる?

 java.lang.Object、つまり標準ライブラリのパッケージjava.langの中のクラスObjectが特別のクラスであることは、第14章でやりました。すべてのクラスのスーパークラスであるということは、そこで定義されているメソッドはどのクラスでも使えるということで、これらについて知っておく必要があります。また基本のメソッドequalsとtoStringについては、必要に応じてオーバーライドして、自分で作成したクラス用に書き換える方法も知らなければなりません。では第9章のクラスMyRectangleでこの二つのメソッドをオリジナルのまま使用した例を見てみましょう。

[MyRectangle.java]
public class MyRectangle {
	double width;
	double height;

	MyRectangle(double w, double h) {
		this.width = w;
		this.height = h;
	}
}
[Main.java]
public class Main {
	public static void main(String[] args) {
		MyRectangle a, b, c;                   
		a = new MyRectangle(3.0, 5.5);   //@ 
		System.out.println(a.toString()); 
				
		b = new MyRectangle(3.0, 5.5);  //A
		c = a;                           //B
		System.out.println(a.equals(b) + " " + a.equals(c));
	}
}
 クラスMyRectangle中のこの章に関係のないメソッドはすべて略しました。さてクラスMainの@からBでやっていることは大丈夫ですか。第9章の長方形はいくつ?でやりましたが、復習のため図を載せておきます。


 @の次の出力ですが、こんな形になります。

   MyRectangle@131f71a

 メソッドtoStringは、オブジェクトの情報を文字列で返すメソッドです。a.toString()で、オブジェクトa(変数aの参照するオブジェクト)、図の黄色いところの情報が求められます。最初はオブジェクトaがどのクラスから生成されたか、@に続いてオブジェクトのハッシュコードが16進で出力されます。ハッシュコードについてはそのうち解説することにして、これでは肝心のインスタンス変数widthとheightの値が出てきません。今ひとつ黄色いオブジェクトの情報が伝わってこないので、後ほどMyRectangleの中でオーバーライドしてみましょう。

 次はBの次の出力ですが、こうなります。

   false true

 メソッドequalsはオブジェクト同士の比較を行います。a.equals(b)で、オブジェクトaが引数で指定されたオブジェクトbと等しいかどうかを調べます。つまりオブジェクトaとbは等しくないけれど、aとcは等しいという結果になったわけです。
 クラスObjectで定義されたオリジナルのequalsは、二つのオブジェクトを「==」で比較します。a.equals(b)の場合、a==bを調べるのです。変数aには、黄色いオブジェクトがどこにあるかという情報、参照値が入っています。aとbは別のオブジェクトを参照しているので、その中身はちがう参照値が入っているため、結果がfalseになった、というわけです。a==cはtrueなので、a.equals(c)はtrueでになります。オブジェクトの中身はこの際関係してきません。もしオブジェクトaとオブジェクトbのように、インスタンス変数の値が完全に一致するオブジェクトどうしは等しい、としたければ、equalsもMyRectangleの中でオーバーライドしてやらねばなりません。
 
 
こまめにAPI

 クラスObjectについて調べたいときは、APIを見ましょう。最初のページは次のようになっています。


 パッケージ名一覧で「java.lang」を選ぶと、パッケージjava.langのクラスの一覧が下に表示されるので、ここから「Object」選んでください。右にクラスObjectの解説が表示されます。索引で「Object」を直接引いてもかまいません。
 だんだん使用するクラスも増えてくるので、APIはこまめに見る習慣をつけておくといいでしょう。お気に入りに入れておきましょう。
 


中身がわかれば安心

 ではクラスMyRectangleに、toStringとequalsを追加して、クラスObjectから継承した両メソッドをオーバーライドしてみましょう。ここでの解説は、オーバーライド全般で役立つと思うので、少し細かいですが頑張りましょう。

[新MyRectangle.java]
public class MyRectangle {
        double width;
        double height;

        MyRectangle(double w, double h) {
                this.width = w;
                this.height = h;
        }

        public String toString() {
                return "MyRectangle width=" + this.width + 
            " height=" + this.height;
        }

        public boolean equals(Object obj) {
                if (!(obj instanceof MyRectangle)) {  //@
                        return false;
                }
                
                MyRectangle other = (MyRectangle)obj;  //A 
                
                return other.width == this.width && 
                       other.height == this.height;
        }
}
 クラスMainは前と同じです。太字が追加部分です。

 ではtoStringから。クラスObjectのtoStringは次のように定義されています。これはAPIのクラスObjectのメソッドの解説を見るとわかります。

   public String toString()

オーバーライドも同じ形にします。仮引数名も同じにしておいた方が無難ですね。publicを忘れるとアクセスのレベルが下がるとコンパイラに文句を言われるので気をつけましょう。returnでクラス名と二つのインスタンス変数の値を返すように変更します。出力は次のようになります。

   MyRectangle width=3.0 height=5.5

 どうですか、黄色いオブジェクトの中身までわかってこちらの方がいいですね。

 ところで話は少しそれますが、このような出力もできます。
   System.out.println(a);
これは次と同じになります。
   System.out.println(String.valueOf(a));
つまりクラスStringのメソッドvalueOfが呼ばれるのですが、これはaがnullのとき”null”、そうでないときa.toString()を返します。aがnullのとき
   System.out.println(a.toString());
とすると、nullについてメソッドの呼び出しを行っているので、実行時エラーになるのですが、
   System.out.println(a);
の場合「null」が出力されます。前のmainの最後で
   a = null;
   System.out.println(a);
としてみてください。
 言いたいことは、ソース中、目に見える形でtoStringを呼んでいなくても、このように陰で呼ばれることがある、ということです。どのクラスもメソッドtoStringを継承しているはずなので、それを他で利用しているのです。というわけで影響大なのでくれぐれも慎重にオーバーライドしてください。


 次はequalsです。こちらはちょっと大変。クラスObjectのequalsは次のように定義されています。

   public boolean equals(Object obj)

まず比べる相手の仮引数objの型がObjectである理由を考えましょう。objには、どんなクラスのオブジェクトが渡されてくるかわかりません。どんなものでも代入できるようにするためには、objの型は、おおもとのスーパークラスjava.lang.Objectであればいいですね。「java.lang」はいらないでので(いいですよね)
   Object obj
とすればよいです。これでどんなオブジェクトが実引数に指定されても、objで受け取ることができます。これはよくやるテクニックです。

 次は@でobjのクラスがMyRectangleかどうかを調べます。演算子「instanceof」の登場です。「obj instanceof MyRectangle」は、オブジェクトojbのクラスがMyRectangleであれば、trueになります。

 次はobjのインスタンス変数を、比較のために取り出したいのですが、objの型はこのままではObjectなので、obj.widthなどとすると、そんなものはない、とコンパイルエラーになります。そこでAでMyRectangleに型変換してやると、other.widthなどとできることになります。これでめでたく各インスタンス変数の比較ができます。ああ長かった。今までの知識総動員ですね。
 出力は次のようになり、同一のオブジェクトでなくても、インスタンス変数widthとheightが等しければ、equalsの結果もtrueになるようになりました。

   true true

 一見よさそうですが、問題点が潜んでいます。特に演算子instanceofを使ったところが怪しいので、次はこれを洗い出し、修正してみましょう。


サブクラスが絡んでくると...

 先に進む前に、MyRectangleというクラス名は長いので、Aに変更し、インスタンス変数もint型のxとyにします。このAに先ほどと同じtoStringとequalsを作ります。

[A.java]
public class A {
	int x, y;

	public A(int x, int y) {
		this.x = x;
		this.y = y;
	}

	public String toString() {
		return "A x=" + this.x + " y=" + this.y;  //@
	}

	public boolean equals(Object obj) {
		if (!(obj instanceof A)) {  //A
			return false;
		}

		A other = (A)obj;

		return other.x == this.x && other.y == this.y;
	}
}
 Aのみでは問題にならなかったことが、サブクラスが絡んでくると、ことはややこしくなってきます。ではサブクラスBにもふたつのメソッドを定義し、Aのメソッドをオーバーライドしてみましょう。このときせっかくスーパークラスAで定義されたメソッドの内容を利用しない手はない、ということで、「super.toString」や「super.equals」といった形で呼び出し、あとはAにないインスタンス変数zについてのみ処理することにします。次のようになります。

[B.java]
public class B extends A {
	int z;
	
	public B(int x, int y, int z) {
		super(x, y);
		this.z = z;
	}
	
	public String toString() {
		return super.toString() + " z=" + z;  //B
	}
	
	public boolean equals(Object obj) {  
		if(!super.equals(obj)) {  //C
			return false;
		}
		
		B other = (B)obj;
		return z == other.z;
	}
}

 さあ、どうなるでしょうか、確かめてみましょう。

[Main.java]
public class Main {
	public static void main(String[] args) {
		A a = new A(10, 20);
		B b = new B(10, 20, 30);
		
		System.out.println(a.toString());  //D
		System.out.println(b.toString());  //E
		
		System.out.println(a.equals(b));  //F
		//System.out.println(b.equals(a));  //G
	}
}
 出力は次のようになります。

   A x=10 y=20
   A x=10 y=20 z=30
   true

 おやおやEではオブジェクトbのクラスがAになっています。さらにFではaとbのクラスがそもそもちがうのに、trueと結果が出ています。Gはコメントアウトしてありますが、Cの下の型変換で実行時のエラーになります。
 クラスBの方は問題なさそうなので、クラスAの方で原因を考えましょう。


サブクラスもOKよ

 まずはtoStringから。これはわかりやすいですね。クラス名がAと固定になっているのでいけません。A中のtoStringをこんなふうにしてみましょう。
        public String toString() {
                return this.getClass().getName() + " x=" + this.x + " y=" + this.y;
        } 
 getClassはクラスObjectのメソッドなので、どんなオブジェクトでも利用可能です。そしてそのオブジェクトがどのクラスから生成されたかを、Class型で返します。(今のところClass 型はあまり気にしないで)つまりb.toString()の場合、クラスB中のtoStringが呼び出されますが、その中でスーパークラスAのtoStringをsuper.toString()の形で呼び出しています。あくまで呼び出しているオブジェクトbはクラスBから生成されたものなので、getClassでクラスBが求められるわけです。これをクラスClassのメソッドgetNameで文字列の「B」に変換します。めでたしめでたしクラス名はBと出力されました。getClass().getName()の使い方はよくするので知っていて損はありません。

 ではequalsはどうでしょう。ここはinstanceofの働きがキーになります。次のようになります。

  x instanceof Y  について
    オブジェクトxのクラスXが、Yの場合 true
    オブジェクトxのクラスXが、Yのスーパークラスでもサブクラスでもない場合 コンパイルエラー
    オブジェクトxのクラスXが、Yのサブクラスの場合 true   (*)
    オブジェクトxのクラスXが、Yのスーパークラスの場合 false
 (*)の場合にtrueになることがこの後問題になります。

 次のプログラムでinstanceofの働きをチェックしておいてください。

[Test.java]
class P {int px;}
class Q extends P {int qx;}
class S {int sx;}
public class Test {
	public static void main(String[] args) {
		P p = new P();
		Q q = new Q();
		P r = new Q();
		S s = new S();
		
		System.out.println((p instanceof P) + " " + 
		                   (p instanceof Q) + " " +
		                   (q instanceof P) + " " +
		                   (r instanceof P) + " " +
		                   (r instanceof Q));
		                  //(s instanceof P) はコンパイルエラー
	}
}

 テスト用のプログラムで、クラスPやQ、Rを別ファイルにするのも面倒くさいので、メソッドmainのあるクラスTestと同じファイルTest.javaに詰め込みました。ひとつのソースファイルに「public」のクラスはひとつだけしか許されないので、P、Q、Rは「public」をつけてはいけません。結果は次のようになります。
   true false true true true
 では先ほどの件に戻ります。a.equals(b)とした場合、equalsの仮引数objにはBのオブジェクトが渡されます。「obj instanceof A」では、BがAのサブクラスなので、結果はtrueになってしまいます。上の(*)の場合ですね。

 b.equals(a)の場合は、Bのequals中のsuper.equals(obj)がtrueで返ってくるので、その次でAのオブジェクトをサブクラスのBに型変換しようとしたところで、エラーになります。

 ずばりobjのクラスが自分自身のクラスと同じかと聞くには、やはりgetClassを使って次のようにします。
   this.getClass() == obj.getClass()
 ただしこれをやるにはobjがnullではまずいので、そのチェックを入れて次のようにequalsを書き換えてみました。
        public boolean equals(Object obj) {
                if(this == obj) {
                        return true;
                }
                
                if(obj == null) {
                        return false;
                }
                
                if(this.getClass() != obj.getClass()) {
                        return false;
                }
                
                A other = (A)obj;

                return other.x == this.x && other.y == this.y;
        }

 最初のif文はthisとobjが同じオブジェクトをさしている場合を考え入れてあります。サブクラスBではこのチェックをした後、B固有のインスタンス変数について調べればよいですね。全体をもう一度載せておきます。自作のクラスで、toStringやequalsをオーバーライドする必要が出てきたときは、このクラスAを参考にしてみてください。

[A.java]
public class A {
	int x, y;

	public A(int x, int y) {
		this.x = x;
		this.y = y;
	}

	public String toString() {
		return this.getClass().getName() + " x=" + this.x + " y=" + this.y;
	}

	public boolean equals(Object obj) {
		if(this == obj) {
			return true;
		}
		
		if(obj == null) {
			return false;
		}
		
		if(this.getClass() != obj.getClass()) {
			return false;
		}
		
		A other = (A)obj;

		return other.x == this.x && other.y == this.y;
	}
}
[B.java]
public class B extends A {
	int z;
	
	public B(int x, int y, int z) {
		super(x, y);
		this.z = z;
	}
	
	public String toString() {
		return super.toString() + " z=" + this.z;
	}
	
	public boolean equals(Object obj) {
		if(!super.equals(obj)) {
			return false;
		}
		
		B other = (B)obj;
		return other.z == this.z;
	}
}
[Main.java]
public class Main {
	public static void main(String[] args) {
		A a = new A(10, 20);
		B b = new B(10, 20, 30);
		
		System.out.println(a.toString());
		System.out.println(b.toString());
		
		System.out.println(a.equals(b));
		System.out.println(b.equals(a));
		
		A c = new B(10, 20, 30);
		System.out.println(b.equals(c));
		System.out.println(c.equals(b));
	}
}
 出力結果は次のようになります。
   A x=10 y=20
   B x=10 y=20 z=30
   false
   false
   true
   true
 equalsは、変数の参照するオブジェクトの比較をするのであって、変数の型は関係ないことを、最後の2行で調べています。変数cの型はA、変数bの型はBですが、オブジェクトcもオブジェクトbも、そのクラスはBでインスタンス変数の値も同じなので、equalsの結果はtrueになります。(変数の型 と オブジェクトのクラス については次の第19章でまとめてあります。)
<<前ページ目次次ページ>>
Copyright (c) 2004-2005 Nagi Imai All Rights Reserved.