Javaでは,まず非常に原始的なprogramを書いておいて, それを拡張したものを追加していくという方法でprogramを開発する ことがよくあります。
appletが良い例です。appletは
public class MyApplet extends java.applet.Applet { ... }のように,必ず java.applet.Applet classを拡張(extend) する形で定義します。
この場合,
つまり,必要なすべてのメソッドを自分で定義しなくても, 親の定義を(いわばデフォールトの定義として)継承するので, プログラムが簡単になるのです。
別の例として, Java言語開発チームが作った java.io パッケージの中で, 出力関係のクラスを詳しく調べてみましょう。
まず,一番簡単な出力のためのクラス OutputStream が, 次のように定義されています。大部分のメソッドは中身は空です。
public abstract class OutputStream { public abstract void write(int b) throws IOException; public void write(byte b[]) throws IOException { ... } public void write(byte b[], int off, int len) throws IOException { ... } public void flush() throws IOException { } public void close() throws IOException { } }
この abstract という形容詞は, 中身がまだ空であるという意味です。
次に,この OutputStream を拡張する形で, FileOutputStream クラスや, FilterOutputStream クラスが定義されています。 たとえばソースファイル FilterOutputStream.java を見ると,
public class FilterOutputStream extends OutputStream { protected OutputStream out; public FilterOutputStream(OutputStream out) { ... } public void write(int b) throws IOException { ... } public void write(byte b[]) throws IOException { ... } public void write(byte b[], int off, int len) throws IOException { ... } public void flush() throws IOException { ... } public void close() throws IOException { ... } }のようになっています。 今度は,中身が空のメソッドはありません。
さらに,この FilterOutputStream を拡張する形で, BufferedOutputStream が定義されています。 ソースファイル BufferedOutputStream.java を見ると,
public class BufferedOutputStream extends FilterOutputStream { protected byte buf[]; protected int count; public BufferedOutputStream(OutputStream out) { ... } public BufferedOutputStream(OutputStream out, int size) { ... } public synchronized void write(int b) throws IOException { ... } public synchronized void write(byte b[], int off, int len) throws IOException { ... } public synchronized void flush() throws IOException { ... } }となっています。
上にあげた三つのクラスは,いずれも全く同じメソッドを含んでいます。
このように,全く同じメソッドを含むクラスについては,たとえば
BufferedOutputStream ostr = new BufferedOutputStream(...);とする代わりに
OutputStream ostr = new BufferedOutputStream(...);のように, 親の OutputStream 型の変数に入れてしまってもかまわいません。 これでも,ostr.write(x) とした場合, ちゃんと子の BufferedOutputStream の write() メソッド が使われます。
しかし,子にあって親にないメソッドがある場合は別です。
たとえば PrintStream クラスは,
public class PrintStream extends FilterOutputStream { private boolean autoflush; private boolean trouble; public PrintStream(OutputStream out) { ... } public PrintStream(OutputStream out, boolean autoflush) { ... } public void write(int b) { ... } public void write(byte b[], int off, int len) { ... } public void flush() { ... } public void close() { ... } public boolean checkError() { ... } public void print(Object obj) { ... } synchronized public void print(String s) { ... } synchronized public void print(char s[]) { ... } ...(以下略)... }のように,OutputStream クラスにないたくさんのメソッドが 定義されています。この場合,
OutputStream ostr = new PrintStream(...);としてもエラーになりませんが, これでは OutputStream になくて PrintStream にあ る println() などの便利なメソッドが使えなくなります。 この場合はちゃんと
PrintStream ostr = new PrintStream(...);と書く必要があります。
親クラスと子クラスが同じ名前・同じ引数のメソッドを含む場合は, 親クラスのメソッドは子クラスのメソッドで上書きされます。
これに対して,親クラスと子クラスが同じ名前の変数を含む場合は, 両方の変数が共存します。
たとえば次のような親・子・孫の三つのクラスがあったとしましょう。
class Test1 { // 親 int x = 100; void foo() { x += 1; } } class Test2 extends Test1 { // 子 int x = 1000; void foo() { x += 2; } } class Test3 extends Test2 { // 孫 int x = 10000; void foo() { x += 3; } }
このとき,まず
class Test { public static void main(String args[]) { Test3 r = new Test1(); // エラー } }はできません。 子クラスの型の変数 r に親クラスのインスタンスを代入することは できないのです。
しかし,
class Test { public static void main(String args[]) { Test1 s = new Test1(); // 大丈夫 Test1 t = new Test3(); // 大丈夫 } }はできます。つまり,親に子を代入できるけれど, 子に親を代入できないのです。
この場合,s は Test1 クラスのインスタンス を Test1 クラスの変数に代入しているので, s.x や s.foo() とすれば Test1 クラスの ものが使われることは明らかです。
しかし,
Test1 t = new Test3();のように代入したとき,
じつは,上の例
Test1 t = new Test3();では,メソッドは Test3 クラスのものしかありませんが, 変数は三つとも存在するのです。
キャストするのが面倒なら,
class Test { public static void main(String args[]) { Test1 t1; Test2 t2; Test3 t3; t1 = t2 = t3 = new Test3(); // ... } }のように,オブジェクトを入れるための三つの変数を作り, どれにも Test3 クラスのインスタンスを入れれば, t1.x と t2.x と t3.x は別々のものになります。 しかし, t1.foo() と t2.foo() と t3.foo() は, まったく同じものです。