Java言語では,まず非常に原始的なプログラムを書いておいて,それを拡張したものを追加していくという方法でプログラムを開発することがよくあります。
アプレットが良い例です。アプレットは
public class MyApplet extends java.applet.Applet { ... }
のように,必ず java.applet.Applet クラスを拡張(extend)する形で定義します。
この場合,
といいます。 また,MyApplet で定義しない変数やメソッドは,親の java.applet.Applet の変数やメソッドの定義を継承します。
つまり,必要なすべての変数やメソッドを自分で定義しなくても,親の定義を(いわばデフォルトの定義として)継承するので,プログラムが簡単になるのです。
別の例として,java.io パッケージの中で,出力関係のクラスを詳しく調べてみましょう。
まず,一番簡単な出力のためのクラス OutputStream が,次のように定義されています。 write(int b) は未定義です。 未定義のものには abstract ということばを付けておきます。
package java.io; public abstract class OutputStream { public abstract void write(int b) throws IOException; // 未定義 public void write(byte b[], int off, int len) throws IOException { // b[off] から b[off + len - 1] までの len バイトを書き出す。 // 上の write(b) を len 回呼び出すだけ。 for (int i = 0; i < len; i++) { write(b[off + i]); } } public void write(byte b[]) throws IOException { // 上の write(b, off, len) を呼び出すだけ。 write(b, 0, b.length); } public void flush() throws IOException { // 何もしない。 } public void close() throws IOException { // 何もしない。 } }
未定義のものが残っているので,クラス自体も abstract になっています。 abstract なクラスは拡張しないと使えません。
この OutputStream クラスを使えるようにするには,
public class MyOutputStream extends OutputStream { public void write(int b) throws IOException { // ここに自分の定義を書いておく } }
のように write(b) の実際の定義を補えばいいのです。
これだけを補えば,配列全体を出力する write(b[])
などは元々の OutputStream クラスに定義されているものが使われます。
つまり,ここで新たに定義した write(b)
を配列の要素の数だけ呼び出して出力します。
もっとも,複数のバイトをまとめて出力するより高速な方法があるなら,write(b[], off, len)
なども自前の定義で上書きすることができます。
つまり,
ということになります。
実際に,FileOutputStream クラスなどは,OutputStream クラスを拡張する形で定義されています。
★ もっとも,byte(b) がどのように定義してあるか調べようとしても,
public native void write(int b) throws IOException;と書いてあるだけです。 native とは,その実装はシステムに依存するので,Java言語ではなく別の方法で作って用意してあるという意味です。
クラスを使うには,まずそのクラスのインスタンスを作ります。 たとえば次のようにします。
import java.io.*; class Test { public static void main(String[] args) throws IOException { // 書くべきデータの準備 byte[] b = new byte[10]; for (int i = 0; i < 10; i++) b[i] = (byte)(10 * i); // FileOutputStream のインスタンス f を作る FileOutputStream f = new FileOutputStream("foo.dat"); f.write(b); // 実際に書く f.close(); // ファイルを閉じる } }
ここで,
FileOutputStream f = new FileOutputStream("foo.dat");
の代わりに
OutputStream f = new FileOutputStream("foo.dat");
のように,作ったインスタンス f を親の OutputStream 型の変数に入れてしまってもかまわいません。 これでも,f.write(b) とした場合,ちゃんと子の FileOutputStream の write(b) メソッドが使われます。
しかし,子にあって親にないメソッドがある場合は別です。
たとえば FileOutputStream クラスは親の OutputStream にない getFD() というメソッドを持っています(これはファイルの記述子file descriptorを調べる命令です)。 これを使うには
OutputStream f = new FileOutputStream("foo.dat"); System.out.println(f.getFD());
ではいけません。ちゃんと
FileOutputStream f = new FileOutputStream("foo.dat");
のようにしなければなりません。
上で述べたように,親クラスと子クラスが同じ名前・同じ引数のメソッドを含む場合は,親クラスのメソッドは子クラスのメソッドで上書きされます。
これに対して,親クラスと子クラスが同じ名前の変数を含む場合は,両方の変数が共存します。
たとえば次のような親・子・孫の三つのクラスがあったとしましょう。
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();
のように代入したとき,
になります。 変数とメソッドで帰属が違うのです。 もっとも,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.x と t2.x と t3.x は別々のものになります。 しかし,t1.foo() と t2.foo() と t3.foo() は,まったく同じものです。
Last modified: 2005-09-22 15:47:18