継承

Javaでは,まず非常に原始的なprogramを書いておいて, それを拡張したものを追加していくという方法でprogramを開発する ことがよくあります。

appletが良い例です。appletは

public class MyApplet extends java.applet.Applet {
    ...
}
のように,必ず java.applet.Applet classを拡張(extend) する形で定義します。

この場合,

といいます。 また,MyApplet で定義しないメソッドは, 親の java.applet.Applet のメソッドの定義を継承します。

つまり,必要なすべてのメソッドを自分で定義しなくても, 親の定義を(いわばデフォールトの定義として)継承するので, プログラムが簡単になるのです。

別の例として, 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) とした場合, ちゃんと子の BufferedOutputStreamwrite() メソッド が使われます。

しかし,子にあって親にないメソッドがある場合は別です。

たとえば 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();               // 大丈夫
    }
}
はできます。つまり,親に子を代入できるけれど, 子に親を代入できないのです。

この場合,sTest1 クラスのインスタンス を Test1 クラスの変数に代入しているので, s.xs.foo() とすれば Test1 クラスの ものが使われることは明らかです。

しかし,

        Test1 t = new Test3();
のように代入したとき, になります。変数とメソッドで帰属が違うのです。 もっとも,t.foo() が参照する x は, t.foo() と同じ Test3 クラスの x です。

じつは,上の例

        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.xt2.xt3.x は別々のものになります。 しかし, t1.foo()t2.foo()t3.foo() は, まったく同じものです。