クラスの継承
クラスの継承は、オブジェクト指向プログラミングで非常によく使われる機能です。
この機能を使うと、新しいクラスを定義するときに一から定義するのではなく、既に完成しているクラスの定義を流用して定義できるようになります。
またクラスの継承機能は、前章で説明したインターフェースの機能も含んでいます。
例えば何か新しいクラスを定義するとき、まったく一からプログラムを書くのではなく、どこからか、ひな形になるクラス定義を探してきて、その内容を修正して新しいクラスを定義することがよくあります。
クラスの継承機能を使うと、このような一連の修正を行き当たりばったりでなく、おこなうことができます。
例えば、これまで Turtle クラスを使って画面に図形を描いてきました。
描けるのは実線だけでしたが、これを改造して点線も描けるようにしてみましょう。
まずクラスの継承機能を使わない場合のプログラムを示します。
public class SmartTurtle {
public Turtle turtle;
public int length;
public SmartTurtle() {
turtle = new Turtle();
length = 10;
}
public void penDown() { turtle.penDown(); }
public void penUp() { turtle.penUp(); }
public void rotate(int r) { turtle.rotate(r); }
public void move(int x, int y) { turtle.move(x, y); }
public void setLength(int len) { length = len; }
public void go(int d) {
int length2 = length * 2;
turtle.penDown();
while (d > length2) {
turtle.go(length);
turtle.penUp();
turtle.go(length);
turtle.penDown();
d -= length2;
}
turtle.go(d);
}
}
この SmartTurtle クラスは、実際には Turtle クラスを改造したものというよりは、Turtle オブジェクトを制御する部品を表すクラスである、と考えることができます。
go メソッドと setLength メソッド以外のメソッドは、全て呼び出されると Turtle クラスの対応するメソッドを呼び出すだけのメソッドです。
SmartTurtle クラスの go メソッドは呼び出されると、Turtle クラスの
go メソッドのように距離 d
だけ前進しますが、その際に実線ではなく点線を描きます。
このメソッドは距離 d 前進するときに、フィールド length が表す距離だけ線を描きながら前進し、次に同じ距離を線を描かずに前進、ふたたび同じ距離を線を描きながら前進、次にまた ... と、これを全部で距離 d だけ移動するまで繰り返します。
一方、setLength メソッドは、点線の間隔を決めるフィールド length の値を変更するためのメソッドです。
これは Turtle クラスに定義されていないメソッドです。
この SmartTurtle クラスを使って三角形を描くプログラムを下に示します。
public class Example {
public static void main(String[] args) {
SmartTurtle t = new SmartTurtle();
t.setLength(5);
t.move(10, 200);
t.penDown();
t.go(200);
t.rotate(120);
t.go(200);
t.rotate(120);
t.go(200);
}
}
上で示した SmartTurtle クラスの定義では、改造の元となる Turtle
クラスのオブジェクトを、SmartTurtle オブジェクトが含む形になっています。
このような定義でも、Turtle クラスを元に新しいクラスを定義する、という目的は達せられますが、定義をよく見ると、penDown や rotate、move
などのメソッドの定義が目につきます。
これらのメソッドは、Turtle オブジェクトの同名のメソッドを呼び出し、処理を代わりに実行させているだけです。
このように処理を他のオブジェクトのメソッドに代行してもらうことを delagate
(委任) といいますが、これらのメソッドをひとつずつ定義するのは、いかにも面倒です。
もし Turtle クラスの定義を直接編集してよいのなら、上のような SmartTurtle
クラスを定義するのはやめて、Turtle クラスに setLength メソッドを追加し、go
メソッドを修正する方が、元々の目的を直接的に実現できます。
しかし Turtle クラスの定義を直接編集すると、go
メソッドの処理内容が変わってしまいますので、Turtle
クラスを使って書かれた他のプログラムが正しく動かなくなってしまいます。
このような状況で便利なのが、クラスの継承です。
この機能を使うと、元になるクラスからの差分を書くだけで新しいクラスを定義することができます。
クラスの継承機能を使うように SmartTurtle クラスの定義を書き直しましょう。
public class SmartTurtle extends Turtle {
public int length;
public SmartTurtle() {
super();
length = 10;
}
public void setLength(int len) { length = len; }
public void go(int d) {
int length2 = length * 2;
this.penDown();
while (d > length2) {
super.go(length);
this.penUp();
super.go(length);
this.penDown();
d -= length2;
}
super.go(d);
}
}
1 行目のクラス名に続けて extends (拡張する)
というキーワードと、元になるクラスの名前を書きます。
キーワードの末尾に s がつくことに注意してください。
extend ではありません。
元になるクラスのことをスーパークラス (super class) または親クラスといいます。
逆に SmartTurtle クラスは Turtle クラスのサブクラス (sub class) または子クラスである、ということもあります。
SmartTurtle クラスと Turtle クラスの関係を表すのに、SmartTurtle
クラスは Turtle クラスを拡張 (extend) している、ということもあります。
しかしオブジェクト指向プログラミング一般では、同じことを
SmartTurtle
クラスは Turtle クラスを継承する (inherit) する、ということが多いようです。
Java 言語では拡張も継承も同じことを意味しますが、しかしオブジェクト指向プログラミング一般では、継承という用語の方が一般的です。
実装を表す implements の後ろには複数のインタフェース名を書けましたが、継承を表す extends の後ろには 1 つしかクラス名を書くことができません。
一方、extends と implements を混在させることはできます。
例えば、
public class SortedIntList extends IntList implements SortedSet {
// 以下省略
}
と書くこともできます。
これは SortedIntList クラスは IntList クラスを継承し、SortedSet
インタフェースを実装していることを意味します。
なお extends と混在しているときも、implements
に続けて複数のインタフェースをカンマで区切って並べることもできます。
さて SmartTurtle クラスの定義は、Turtle クラスを継承していますから、2 つのクラスを融合したものになります。
SmartTurtle クラスのフィールドやメソッドは、Turtle クラスのフィールドやメソッドに、SmartTurtle クラスで独自に定義したものを加えたものです。
Turtle クラスのフィールドやメソッドの定義を手でコピーして SmartTurtle
クラスの定義に書き加えた場合と同様の結果になります。
実際に手で定義をコピーせずに継承機能を使うと、メソッドの定義がプログラム中の
1 カ所にまとまりますから、プログラムの保守が容易になります。
例えば rotate メソッドの定義を手でコピーしたとして、後で
rotate メソッドの定義に誤りがあることに気づいたとしましょう。
当然、誤りを修正するときは全ての rotate メソッドの定義のコピーをもれなく修正しなければなりません。
コピーの数が少なければよいのですが、プログラムが大きくなって多数の rotate メソッドの定義のコピーができてしまうと、修正も大変になります。
万一、修正もれがあると、直したはずの誤りが直っていないということになり、プログラムの作成者を混乱させてしまうかもしれません。
継承機能を使うと、このような問題はおきません。
rotate メソッドの定義はプログラムの中では 1 カ所だけだからです。
修正するときも 1 カ所だけですみます。
Turtle クラスのメソッドやフィールドは、SmartTurtle クラスにそのままコピーされますが、go メソッドだけは例外です。
このメソッドは SmartTurtle でも Turtle クラスでも定義されています。
その場合、Turtle クラスの go メソッドは SmartTurtle クラスにコピーされません。
SmartTurtle クラスで独自に定義された go メソッドが使われます。
このようにスーパークラスで定義されているメソッドを、サブクラスで独自に定義しなおすことを、メソッドをオーバーライド (override) する、といいます。
go メソッドの中では、同じオブジェクトのメソッドを呼ぶときに this
を用いています。
これまで説明してきたように、この this は省略可能です。
go メソッドの中では this の他に super という特別な変数も使われています。
例えば
super.go(length2);
は super の代わりに this と書いた場合と同様、そのオブジェクト自身の go
メソッドを呼べ、という意味です。
しかし this と異なり、呼ばれる go メソッドはスーパクラスである Turtle
クラスに定義されている go メソッドです。
SmartTurtle クラスで定義されている go メソッドが再帰的に呼び出されるわけではありません。
Turtle クラスの go メソッドは実線を描きますから、結局、上の例のメソッド呼び出しにより、長さ length2
の実線が描かれます。
変数 this と super を使い分けなければいけないのは、go のようなスーパークラスとサブクラスとで定義が違うメソッドを呼び出すときだけです。
例えば penDown や move のような、それ以外のメソッドを呼ぶ場合は、2 つの変数を使い分ける必要はありません。
this と書くか、省略してしまうのがよいでしょう。
メソッドのオーバーライド (override) とオーバーロード (overload)
の違いに注意してください。
オーバーライドは、スーパークラスで定義されているメソッドを上書きして、独自のメソッドを定義することです。
一方、オーバーロードは、同じ名前のメソッドを、シグネチャだけ変えて複数の種類、二重、三重に定義することです。
オーバーロードされたメソッドを、オーバーライドすることも可能です。
例えば Turtle クラスの go メソッドはオーバーロードされています。
このクラスには int 型の引数をとる go メソッドだけでなく、double
型の引数をとる go メソッドも定義されています。
SmartTurtle クラスでは int 型の引数をとる go
メソッドをオーバーライドして定義していますが、オーバーライドされるのは
Turtle クラスの int 型の引数をとるメソッドだけです。
double 型の引数をとる go メソッドは、別個のメソッドとして
SmartTurtle クラスにコピーされます。
つまり Turtle クラスには
void go(int d)
void go(double d)
という 2 つのメソッドが定義されていますが、SmartTurtle クラスでも、
void go(int d) 点線を描くように再定義
void go(double d) Turtle クラスの定義をそのままコピー
と、2 つのメソッドが定義されます。
オーバーライドされるのは引数の型や個数 (シグネチャ) が一致するメソッドだけです。
Turtle クラスの両方の go メソッドをオーバーライドするには、SmartTurtle
クラスでも 2 つの go メソッドを定義しなければなりません。
double 型の引数をとる go メソッドは、SmartTurtle クラスでオーバーライドされないので、次のようなプログラムを書くと実線で三角形が描かれます。
public class Example {
public static void main(String[] args) {
SmartTurtle t = new SmartTurtle();
t.setLength(5);
t.move(10, 200);
t.penDown();
t.go(200.0);
t.rotate(120);
t.go(200.0);
t.rotate(120);
t.go(200.0);
}
}
go メソッドの引数が 200 ではなく、200.0 と浮動小数点数、つまり double
型になっています。
これにより、int 型ではなく double 型の引数をとる go メソッドが呼び出されます。
このメソッドは、Turtle メソッドに定義されているものそのままのコピーですから、点線ではなく、実線で三角形が描かれます。
クラスの定義にはコンストラクタを書くことができます。
コンストラクタは new 演算子によってオブジェクトが作成されたときに暗黙のうちに呼び出される特別なメソッドです。
SmartTurtle のような他のクラスを継承しているクラスの場合、コンストラクタの定義には注意が必要です。
コンストラクタは主にフィールドに初期値を代入するのに使われます。
したがって他のクラスを継承しているときは、スーパークラスのコンストラクタも呼び出さないと、一部の (スーパークラスで定義されている) フィールドに正しい初期値が代入されなくなってしまいます。
スーパークラスのコンストラクタは、メソッドと異なりサブクラスにコピーされません。
したがって、サブクラスのコンストラクタの最初の行は、必ずスーパークラスのコンストラクタの呼び出しでなければなりません。
SmartTurtle クラスのコンストラクタは次のようなものでした。
public SmartTurtle() {
super(); // 省略可能
length = 10;
}
1 行目の super() が、スーパークラスのコンストラクタの呼び出しです。
もしスーパークラスのコンストラクタが引数をとるときは、super
に続く括弧の中に引数を並べて書きます。
Turtle のコンストラクタは引数をとらないので、上の例では括弧の中を空にします。
super を使ってスーパークラスのコンストラクタを呼び出せるのは、コンストラクタの最初だけです。
コンストラクタの最後の行に super() と書いても呼び出せません。
またメソッドの中に super() と書いて、スーパークラスのコンストラクタを呼び出すことはできません。
スーパークラスのコンストラクタが引数をとらないときは、1
行目のスーパークラスのコンストラクタの呼び出しを省略できます。
つまり SmartTurtle クラスのコンストラクタは
public SmartTurtle() {
length = 10;
}
と書くこともできます。
このように書いても、
super();
と書いてあるものと解釈されて、スーパークラスのコンストラクタが引数なしで呼び出されます。
実は全てのクラスには必ずコンストラクタを定義しなければなりません。
もしコンストラクタが定義されていない場合は、スーパークラスのコンストラクタを引数なしで呼び出すだけのコンストラクタが、暗黙のうちに定義されます。
例えば、もし SmartTurtle クラスにコンストラクタが 1 つも定義されていないと、
public SmartTurtle() {
super();
}
というコンストラクタが定義されているものと解釈されます。
ただし、スーパークラスには引数なしのコンストラクタが定義されていなければなりません。
そうでない場合は、SmartTurtle クラスのコンストラクタの定義を省略することはできません。
この規則は extends によって他のクラスを継承していない場合にも当てはまります。
例えば
public class Point {
public int x, y;
}
この Point クラスの定義にはコンストラクタがありません。
しかし暗黙のうちにコンストラクタが定義されるので、実際には、次のように Point
クラスを定義したことになります。
public class Point {
public int x, y;
public Point() {
super();
}
}
コンストラクタはスーパークラスのコンストラクタを引数なしで呼び出します。
ところが Point クラスには extends によるスーパークラスの指定がありません。
Java 言語では、クラス定義に extends によるスーパークラスの指定がないときは、Object クラスがスーパークラスになります。
したがって上の Point クラスのコンストラクタは、Object クラスのコンストラクタを引数なしで呼び出します。
Object クラスは String クラスと同様、あらかじめ定義されているクラスです。
extends を省略すると必ず Object クラスがスーパークラスになるので、Object
クラスは直接または間接に全てのクラスのスーパークラスになります。
例えば SmartTurtle クラスのスーパクラスは Turtle ですが、Turtle クラスのスーパークラスは実は Object クラスです。
結局、Object クラスは間接的に SmartTurtle クラスのスーパークラスになります。
メソッド同様、コンストラクタもオーバーロードすることが可能です。
シグネチャ、すなわち引数の個数や型さえ変えれば、複数のコンストラクタを同一のクラスに定義することができます。
new 演算子でオブジェクトが作成されるときに、どのコンストラクタが呼ばれるかは、new 演算子に続く実引数列によって決まります。
その実引数列に合致する引数をとるコンストラクタが選ばれて呼び出されます。
例えば
public class Point {
public int x, y, z;
public Point() {
x = y = z = 0;
}
public Point(int x0, int y0) {
x = x0;
y = y0;
z = 0;
}
public static void main(String[] args) {
Point p = new Point();
Point q = new Point(3, 4);
System.out.println("p.x = " + p.x);
System.out.println("q.x = " + q.x);
}
}
のように Point クラスが定義されていたとします。
すると main メソッドの中で、変数 p が表す Point オブジェクトが作成されるときには、引数なしのコンストラクタが呼ばれます。
一方、変数 q が表すオブジェクトが作成されるときは、int 型の値を 2 つ引数にとるコンストラクタが呼ばれます。
上の Point クラスの定義では、どちらのコンストラクタでも最初の super() の呼び出しが省略されています。
コンストラクタの中では最初にスーパークラスのコンストラクタを呼ばなければなりませんが、コンストラクタがオーバーロードされている場合は、同じクラスの別なコンストラクタを呼んで、それに代えることもできます。
例えば、次のようなコンストラクタを Point クラスに追加できます。
public class Point {
public Point(int x0, int y0, int z0) {
this(x0, y0); // 他のコンストラクタの呼び出し
z = z0;
}
// 以下同じ
}
最初の行は、同じ Point クラスのコンストラクタを呼び出します。
呼ばれるのは int 型の引数を 2 つ引数にとるコンストラクタです。
スーパークラスのコンストラクタを呼ぶときには super を使いますが、同じクラスの別なコンストラクタを呼ぶときには this を代わりに使います。
extends を使ってスーパークラスを指定すると、スーパークラスをインタフェースのように使うことができます。
インタフェースは、いくつかのクラスを包含する型を表しました。
このような型を、一般的には、ポリモルフィック・タイプ (polymorphic type)
といいます。
これは複数の形態をもつ型という意味で、しばしば多態型あるいは多相型と訳されます。
インタフェースはポリモルフィック・タイプを定義する一番直接的な方法ですが、スーパークラスもポリモルフィック・タイプとして使えます。
包含されるのは、全てのサブクラスです。
サブクラスのサブクラスも包含されます。
例えば、Figure クラスのサブクラスが Polygon クラスで、Polygon
クラスのサブクラスが Rectangle クラスとします。
その場合、Rectangle クラスは Figure クラスの間接的なサブクラスとなり、Figure
クラスが表す型に包含されます。
スーパークラスの型の変数には、サブクラスのオブジェクトの参照番号を代入することができます。
例えば SmartTurtle クラスは Turtle クラスのサブクラスですから、次のようなプログラムを書くことができます。
public class Example {
public static void main(String[] args) {
Turtle t;
if (args.length > 0 && args[0].equals("dot")) {
SmartTurtle st = new SmartTurtle();
st.setLength(5);
t = st; // Turtle 型の変数 t へ代入
}
else
t = new Turtle();
t.move(10, 200);
t.penDown();
t.go(200);
t.rotate(120);
t.go(200);
t.rotate(120);
t.go(200);
}
}
このプログラムは、コマンド行引数が dot であるとき点線で、そうでないとき実線で三角形を描きます。
普通に
java Example
と実行すれば実線で、
java Example dot
とコマンド行引数をつけて実行すれば点線で三角形を描きます。
変数 t の型は Turtle ですが、t には SmartTurtle オブジェクトの参照番号を代入することができます。
作成した SmartTurtle オブジェクトの参照番号を変数 t
に代入する前に、いったん変数
st に代入し、setLength メソッドを呼び出しています。
これは、setLength メソッドが SmartTurtle クラスにだけ定義されているメソッドなので、
t.setLength(5);
のように、変数 t が表すオブジェクトの setLength メソッドを呼び出すことはできないからです。
変数 t の型は Turtle なので、呼び出せるのは Turtle クラスに定義されているメソッドだけです。
Turtle クラスで定義されていない setLength メソッドは呼び出せません。
変数 t が実際にどのクラスのオブジェクトを表しているかは無関係です。
仮に変数 t が SmartTurtle オブジェクトを表していても、setLength
メソッドを呼び出すことはできません。
一方、go メソッドを呼び出す場合は、変数 t が表しているオブジェクトのクラスが重要になります。
go メソッドは Turtle クラスで定義されていますが、SmartTurtle
クラスでもオーバーライドされています。
この場合、
t.go(200);
を実行すると、変数 t が表すオブジェクトのクラスに定義されている go
メソッドが呼ばれます。
変数 t が Turtle オブジェクトを表していれば Turtle クラスの実線を描く
go メソッドが、SmartTurtle オブジェクトを表していれば SmartTurtle クラスの点線を描く go メソッドが呼ばれます。
変数 t の型は常に Turtle ですが、呼び出される go メソッドは Turtle クラスの
go メソッドとは限りません。
go のようにオーバーライドされているメソッドを呼び出すと、変数が表すオブジェクトのクラスに応じて、異なるメソッドが選択されます。
これを一般に、メソッド・ディスパッチ (method dispatch) といいます。
この機構があるため、Turtle オブジェクトを使って三角形を描く場合と、SmartTurtle
オブジェクトを使って三角形を描く場合とで、プログラムの後半を共通にすることができます。
同じことはインタフェースを使っても実現可能ですが、継承を使うと、別途インタフェースを定義せずに、簡単にポリモルフィック・タイプを利用することができます。
SmartTurtle オブジェクトを表しているのが変数ではなく、メソッドの引数の場合も同様です。
public class Example {
public static void drawTriangle(Turtle t) {
t.move(10, 200);
t.penDown();
t.go(200);
t.rotate(120);
t.go(200);
t.rotate(120);
t.go(200);
}
public static void main(String[] args) {
drawTriangle(new SmartTurtle());
}
}
drawTriangle メソッドの中で引数 t の型は Turtle ですが、go
メソッドを呼ぶと、引数 t が表すオブジェクトのクラスに定義されている
go メソッドが呼ばれます。
つまり、この場合は、SmartTurtle の go
メソッドが呼ばれ、点線で三角形が描かれます。
メソッド・ディスパッチは、スーパークラスのメソッドの中でも有効です。
例えば次の 2 つのクラス Point と Point3D を考えてください。
Point3D クラスは Point クラスを継承しています。
public class Point {
public int x, y;
public Point(int x0, int y0) {
x = x0;
y = y0;
}
public void print() {
System.out.println(this.toString());
}
public String toString() {
return "(" + x + ", " + y + ")";
}
}
public class Point3D extends Point {
public int z;
public Point3D(int x0, int y0, int z0) {
super(x0, y0);
z = z0;
}
public String toString() {
return "(" + x + ", " + y + ", " + z + ")";
}
public static void main(String[] args) {
Point p = new Point(1, 2);
p.print(); // (1, 2) と表示
Point3D q = new Point3D(3, 4, 5);
q.print(); // (3, 4, 5) と表示
Point r = new Point3D(6, 7, 8);
r.print(); // (6, 7, 8) と表示
}
}
toString メソッドは、そのオブジェクトが表す座標を文字列にしたものを返します。
Point クラスは 2 次元座標、Point3D クラスは 3 次元座標を表し、座標の文字列表現がそれぞれ異なるので、Point3D クラスは toString メソッドをオーバーライドして独自のメソッドを定義しています。
問題は Point クラスの print メソッドです。
このメソッドは、this が表すオブジェクト (つまり自分自身)
の toString メソッドを呼び出し、返ってきた文字列を画面に表示します。
このとき呼び出される toString メソッドは、メソッド・ディスパッチにより、
this が表すオブジェクトのクラスに定義されている toString メソッドです。
上の main メソッドの中では、変数 p、q、r が表すオブジェクトの print
メソッドをそれぞれ呼び出しています。
print メソッドの中からどの toString メソッドが呼び出されるか、それぞれの場合について考えてみましょう。
変数の型 表すオブジェクト print メソッド toString メソッド
変数 p Point Point Point Point
変数 q Point3D Point3D Point Point3D
変数 r Point Point3D Point Point3D
まず変数 p は Point オブジェクトを表します。
したがって、この変数が表すオブジェクトの print メソッドを呼び出すと、Point
クラスに定義された print メソッドが呼ばれます。
このメソッドの中では this も
Point オブジェクトを表しますから、Point クラスに定義されている toString メソッドが呼ばれます。
一方、変数 q は Point3D オブジェクトを表します。
しかし、この変数が表すオブジェクトの print メソッドを呼び出すと、Point
クラスに定義された print メソッドが呼ばれます。
これは Point3D クラスで print メソッドを定義していないからです。
print メソッドの中では変数 this の型は Point ですが、this が表すオブジェクトは
Point3D オブジェクトです。
したがって print メソッドの中で this が表すオブジェクトの toString
メソッドを呼ぶと、Point3D クラスに定義されている toString
メソッドが呼ばれます。
変数 r の型は Point ですが、この変数は Point3D オブジェクトを表します。
変数 r が表すオブジェクトの print メソッドを呼び出すと、変数 q
の場合と同様、Point クラスに定義された print メソッドが呼ばれます。
print メソッドの中では変数 this の型はやはり Point ですが、this
が表すオブジェクトは Point3D オブジェクトです。
したがって print メソッドの中で this が表すオブジェクトの toString
メソッドを呼ぶと、Point3D クラスに定義されている toString
メソッドが呼ばれます。
print メソッドは Point クラスに定義されているので、print メソッドの中では変数
this は常に Point 型です。
したがって this が表すオブジェクトについて呼び出せるのは、Point
クラスに定義されたメソッドだけです。
一方、変数 this が表すオブジェクトのクラスは、どのオブジェクトの
print メソッドを呼んだかに応じて異なります。
Point3D オブジェクトの print メソッドを呼んだ場合は、変数 this
は Point3D オブジェクトを表します。
したがって print メソッドの中で toString メソッドを呼ぶと、this
が表すオブジェクトのクラスによって異なる toString メソッドが呼ばれます。
ポリモルフィック・タイプの変数を使う場合、その変数の型と、その変数が表すオブジェクトのクラスが必ずしも一致しません。
その場合、
インタフェースを定義するときに、別なインタフェースを extends を使って継承することができます。
継承されたインタフェースがスーパーインタフェース、継承したインタフェースがサブインタフェースとなります。
インタフェースを継承すると、スーパーインタフェースで定義されていたメソッドやフィールドは、サブインタフェースにそのままコピーされます。
public interface IntSet {
void add(int value);
boolean find(int value);
int size();
int get(int index);
}
public interface UpdatableIntSet extends IntSet {
void set(int index, int value);
}
public class IntArray implements UpdatableIntSet {
public int[] array;
public int num;
public IntArray() {
array = new int[8];
num = 0;
}
public void add(int value) {
if (num >= array.length) {
int[] newArray = new int[array.length + 8];
for (int i = 0; i < array.length; ++i)
newArray[i] = array[i];
array = newArray;
}
array[num++] = value; // 末尾に追加
}
public boolean find(int value) {
for (int i = 0; i < num; ++i)
if (array[i] == value)
return true;
return false;
}
public int size() { return num; }
public int get(int index) {
return array[index];
}
public void set(int index, int value) {
array[index] = value;
}
public static void main(String[] args) {
IntArray array = new IntArray();
UpdatableIntSet uset = array;
IntSet set = array;
UpdatableIntSet uset2 = (UpdatableIntSet)set;
uset.add(3);
System.out.println(uset.find(3)); // true
uset.set(0, 4);
System.out.println(uset.find(3)); // false
}
}
この例では、IntArray クラスが UpdatableIntSet インタフェースを実装しています。
UpdatableIntSet は IntSet インタフェースを継承しているので、UpdatableIntSet
は 5 つのメソッド add、find、size、get、set を定義していることになります。
これを実装している IntArray クラスは、それら 5 つのメソッドを全て定義しなければなりません。
スーパーインタフェースが表す型は、サブインタフェースを包含します。
またサブインタフェースを実装するクラスは、同時にスーパーインタフェースも実装したことになります。
したがって、IntSet インタフェースが表す型は UpdatableIntSet インタフェースと
IntArray クラスを包含します。
IntArray クラスの main メソッドの中では、IntArray 型の変数 array
の値を UpdatableIntSet 型の変数 uset や IntSet 型の変数 set
に代入しています。
これは IntArray クラスが UpdatableIntSet や IntSet インタフェースを実装しているからです。
一方、IntSet 型の変数 set の値を直接 UpdatableIntSet 型の変数 uset に代入することはできません。
このような場合はキャスト演算で、型を合わせなければなりません。
UpdatableIntSet uset2 = (UpdatableIntSet)set;
変数 set が表すオブジェクトのクラスが UpdatableIntSet を実装していれば、この代入は成功して、変数 set の値が uset2 へ代入されます。
ポリモルフィックな型の変数が、実際にどのクラスのオブジェクトを表しているか調べたいときには、instanceof 演算子を使います。
この演算子を使えば、あるオブジェクトのクラスが、指定したクラスまたはそのサブクラスであるか否かを調べることができます。
この演算子は
式 instanceof クラス名
という形で使います。
「クラス名」の部分は、インタフェース名でもかまいません。
instanceof の計算結果は、演算子の左側の式が表すオブジェクトのクラス C
が、次のような条件を満たすとき true、そうでないとき false となります。
instanceof の計算結果は、演算子の左側の式の型とは無関係です。
例えば
public class Point {
public int x, y;
public Point(int x0, int y0) {
x = x0;
y = y0;
}
}
public class Point3D extends Point {
public int z;
public Point3D(int x0, int y0, int z0) {
super(x0, y0);
z = z0;
}
public static void main(String[] args) {
Object p = new Point3D(3, 4, 5);
System.out.println(p instanceof Point); // true
System.out.println(p instanceof Point3D); // true
}
}
Object クラスは全てのクラスのスーパークラスでした。
この例では、Object クラスを継承しているのが Point クラスで、Point
クラスを継承しているのが Point3D です。
変数 p の型は Object ですが、実際には Point3D
オブジェクトを表すので、main メソッド中のそれぞれの instanceof の結果は共に
true となります。
Copyright (C) 2003 by Shigeru Chiba, All rights reserved.