それでは、Javaの標準APIの中にあるクラスを使って、クラスの拡張について理解をしてみましょう。あるクラスを拡張して作成されたクラスという例はたくさんありますが、ここではjava.lang.Objectクラスとjava.lang.Stringクラスを使ってみましょう。
本稿では、APIドキュメントを参照します。ここではオンラインのドキュメントを参照していますが、オフライン環境でも確認はできます。APIドキュメントは第1回で「C:\application\jdk6」へインストールしていますから、オフライン環境で確認する場合は、そちらを利用してください。
JDK 6のAPIドキュメントのStringクラスのページを参照すると、java.lang.Stringクラスは、java.lang.Objectクラスを拡張していることが分かります(図4)。JDK 6のAPIドキュメントでは、拡張している関係をこのように表現しているのです。
APIドキュメントを見ると分かりますが、「クラス java.lang.Object から継承されたメソッド」として、cloneやfinalize、getClassといったメソッドが挙げられています。JDK 6のAPIドキュメントのObjectクラスのページも見てみましょう。サンプルプログラムでは、java.lang.Objectクラスにある次の4つのメソッドを使ってみます。
各メソッドの簡単な説明を添えましたが、サンプルの動作を理解するのに必要な最低限のことしか書いてありません。正確なAPIの処理内容については、APIドキュメントを参考するなどしてください。
ここで、再度、JDK 6のAPIドキュメントのStringクラスのページを確認して、Stringクラスもequalsメソッド、hashCodeメソッド、toStringメソッドがある点や、getClassメソッドは「クラスjava.lang.Objectから継承されたメソッド」に含まれるという点に注目してください。
それでは、「sample12.Sample01」という名前のクラスで、これらの動作を確認するサンプルプログラムを作成してみます。フィールドとして、java.lang.Object(以降、「Object」)クラスの配列、java.lang.String(以降、「String」)クラスの配列を持つようにします。コンストラクタでは各フィールドの初期化をして、インデックス0の要素とインデックス2の要素が同じインスタンスを参照するようにしておきます。
また、runメソッドを用意して、そこに主となる処理を記述することにします。プログラム起動用のmainメソッドでは、sample12.Sample01クラスのインスタンスを生成して、そのrunメソッドを呼ぶという単純な処理にしておきます。
package sample12; public class Sample01 { Object[] o = new Object[3]; String[] s = new String[3]; Sample01() { // o[1] は o[0] とは違うインスタンスを参照 // o[0] と o[2] は同じインスタンスを参照 o[0] = new Object(); o[1] = new Object(); o[2] = o[0]; // s[1] は s[0] とは違うインスタンスを参照 // s[0] と s[2] は同じインスタンスを参照 s[0] = new String("s0"); s[1] = new String("s0"); s[2] = s[0]; } // 略 public static void main(String[] args) { new Sample01().run(); } }
情報を表示するために、便利なメソッドをいくつか用意します。ObjectクラスのhashCodeメソッド、toStringメソッドの計算結果を情報を表示するprintObjectInfoメソッドを用意します。各メソッドの結果を出力しているだけです。
void printObjectInfo(String title, Object o) { System.out.println("printObjectInfo:" + title); System.out.println("\t" + o.hashCode()); System.out.println("\t" + o.toString()); }
StringクラスのhashCodeメソッド、toStringメソッドの計算結果を情報を表示するprinStringInfoメソッドも用意します。こちらは、Stringクラスの各メソッドの結果を出力しているだけです。こちらも難しいことは何もありません。
void printStringInfo(String title, String s) { System.out.println("printStringInfo:" + title); System.out.println("\t" + s.hashCode()); System.out.println("\t" + s.toString()); }
Objectクラス、Stringクラスのequalsメソッドの結果を表示するprintEqualメソッドも用意します。あらかじめ用意しておいた配列の各要素についてequalsメソッドで比較をし、その結果を表示します。
void printEqual() { System.out.println("o[0]:o[1]:"+o[0].equals(o[1])); System.out.println("o[0]:o[2]:"+o[0].equals(o[2])); System.out.println("o[1]:o[2]:"+o[1].equals(o[2])); System.out.println("s[0]:s[1]:"+s[0].equals(s[1])); System.out.println("s[0]:s[2]:"+s[0].equals(s[2])); System.out.println("s[1]:s[2]:"+s[1].equals(s[2])); }
Objectクラス、StringクラスのgetClassメソッドを実行するexecGetClassメソッドも用意します。ここで注目してもらいたいのは、Stringクラスについても、「s[0].getClass();」のように、getClassメソッドを呼び出せる点です。StringクラスではgetClassメソッドは「Objectクラスから継承されたメソッド」とAPIドキュメントには記載されていました。このメソッドはStringクラスのソースコードには処理が記述されていません。Objectクラスのソースコードに記述されている処理を利用しているのです。
void execGetClass() { System.out.println("o[0].getClass():" + o[0].getClass()); System.out.println("o[1].getClass():" + o[1].getClass()); System.out.println("o[2].getClass():" + o[2].getClass()); System.out.println("s[0].getClass():" + s[0].getClass()); System.out.println("s[1].getClass():" + s[1].getClass()); System.out.println("s[2].getClass():" + s[2].getClass()); }
最後に、runメソッドです。Objectクラスの配列について、各要素を使ってprintObjectInfoメソッドを実行します。また、Stringクラスの配列について、各要素を使ってprintStringInfoメソッドを実行します。さらに、Stringクラスの配列については、各要素を使ってprintObjectInfoメソッドも実行します。
ここで、「printObjectInfoメソッドは、Objectクラスを引数として受け取るメソッドなのに、なぜだろう」と思いませんか? ObjectクラスとStringクラスは型が違うにもかかわらず、printObjectInfoメソッドの引数として使えるというのは、不思議な感じがしますが、よく考えてみましょう。
インターフェイスのことを思い出してみると、理解できるはずです。同じようなことがインターフェイスを使ったときもできました。そうです、Stringクラスは、Objectクラスを拡張したものなので、このようにObjectクラスと同じものとして扱えます。この点は重要ですから、よく覚えておいてください。
最後に、printEqualメソッドとexecGetClassメソッドを呼び出して終了です。
void run() { for (int i = 0; i < o.length; i++) { printObjectInfo("o[" + i + "]", o[i]); } for (int i = 0; i < s.length; i++) { printStringInfo("s[" + i + "]", s[i]); } for (int i = 0; i < s.length; i++) { printObjectInfo("s[" + i + "]", s[i]); } printEqual(); execGetClass(); }
それでは、プログラムを実行してみましょう。実行する前に、各メソッドの結果がどのようになるか想像してみてください。これまでの説明からどうなるかある程度分かるはずです。想像できたら、実行してください。各メソッドの実行結果が順に出力されるはずです。どうでしょう、あなたの予想は当たりましたか? 量が多いので、sample12.Sample01の各メソッドの計算結果を単位にして説明をします。
最初に、Objectクラスの配列の各要素を使ってprintObjectInfoメソッドを実行した結果です。ハッシュ値は、ご覧のとおり数値です。インスタンスが等しいかどうかを比較する際に利用されるので、Objectクラスとは違うインスタンスでは値が異なります。o[0]とo[1]とo[2]の値を確認しておきましょう。toStringメソッドの結果を見て分かるように、Objectクラスのインスタンスの文字列表現は、「java.lang.Object@ca0b6」のように「クラス名@数値」という表現になっています。
printObjectInfo:o[0] 827574 java.lang.Object@ca0b6 printObjectInfo:o[1] 17510567 java.lang.Object@10b30a7 printObjectInfo:o[2] 827574 java.lang.Object@ca0b6
次に、Stringクラスの配列の各要素を使ってprintStringInfoメソッドを実行した結果です。ハッシュ値に注目をしましょう。Objectクラスとは違って、どれも同じ値です。s[0]とs[1]は、new演算子を使って別々のインスタンスを生成したはずですが、同じ値になっています。toStringメソッドの結果も、Objectクラスとは違います。Stringクラスのインスタンスでは、自身が表している文字列そのものを返してきています。
printStringInfo:s[0] 3613 s0 printStringInfo:s[1] 3613 s0 printStringInfo:s[2] 3613 s0
次に、Stringクラスの配列の各要素を使ってprintObjectInfoメソッドを実行した結果です。この実行結果について、皆さんの予想はどうだったでしょうか。ご覧のとおり、タイトルを表示している部分だけが変わっているだけで、各メソッドの実行結果は、printStringInfoメソッド内のものと同じです。
よく考えると分かりますが、printObjectInfoメソッドへはStringクラスのインスタンスがわたってきているわけですから、StringクラスのhashCodeメソッドやtoStringメソッドが実行されます。printObjectInfoメソッドでは、Objectクラスのインスタンスを使っているかのように処理を記述していますが、実際はここにはObjectクラスを拡張したクラスのインスタンスが来て、それぞれに記述されたhashCodeメソッドやtoStringメソッドが実行されることになるのです。
慣れないと不思議な感じがしますが、このような機能があるために、拡張したクラスを増やしても、従来の処理に修正しなくても済むことが多いので、便利なのです。
printObjectInfo:s[0] 3613 s0 printObjectInfo:s[1] 3613 s0 printObjectInfo:s[2] 3613 s0
sの各要素については、printStringInfoメソッドへはString型のインスタンスとして渡され、printObjectInfoメソッドへはObject型のインスタンスとして渡されました。sの各要素はこのようにどちらの型のインスタンスとしても使えるのです。
この機能を「ポリモーフィズム(polymorphism、多態性)」といいます。あるクラスのインスタンス(この例ではsの各要素)が、そのクラス自身(この例ではjava.lang.String)としての形態を持つと同時に、あるいはスーパークラス(この例では、java.lang.Object)としての形態を持つことから、このようにいいます。オブジェクト指向プログラミング言語で重要な用語ですから、覚えておきましょう。
次に、printEqualメソッドを実行した結果です。こちらは、予想しやすかったのではないでしょうか。以前も紹介しましたが、Stringクラスでは表現している文字列が同じであれば、equalsメソッドの結果はtrueとなります。ここでは、Objectクラスの「o[0]とo[1]」、「o[1]とo[2]」が、equalsメソッドで同値かどうか計算をすると、falseという結果となることについて、理解できれば大丈夫です。
o[0]:o[1]:false o[0]:o[2]:true o[1]:o[2]:false s[0]:s[1]:true s[0]:s[2]:true s[1]:s[2]:true
最後がgetClassメソッドを実行した結果です。ここでは、StringクラスでもgetClassメソッドが使えることが確認できれば十分です。結果は見てのとおり、各インスタンスのクラス情報が取得できていて、同じクラスのインスタンスについては同じ値が出力されています。
o[0].getClass():class java.lang.Object o[1].getClass():class java.lang.Object o[2].getClass():class java.lang.Object s[0].getClass():class java.lang.String s[1].getClass():class java.lang.String s[2].getClass():class java.lang.String
どうでしょう、拡張の元となったクラスと、拡張されたクラスの関係や、機能の差について、何となく理解できたでしょうか。StringクラスはObjectクラスとしても振る舞えるという「型の互換性がある」点は忘れないようにしてください。これが重要ポイントです。
さて、使い方が分かると、「どういった場合にクラスを拡張するといいのか」が分かるようになります。ちょっと難しい内容だったかもしれませんが、よく確認して理解しておくようにしましょう。次ページからは、文法事項を押さえて、自作のクラスを拡張する方法を解説します。
Copyright © ITmedia, Inc. All Rights Reserved.