Deep Side of Java〜Java 言語再入門 第2回 〜 Java 文法を中心に

オブジェクト指向に関する仕様

継承とインターフェイス






継承とインターフェイス

継承はクラス名の後に「extends 基底クラス」を最大1つ指定できる。この「extends 〜」がない場合には、Object クラスを継承するものとされる。Java は多重継承を認めていないので、親クラスは1つしかない。

それに対して、「implements interface名」はコンマで区切っていくつでも指定できる。interface は、実装されるべきメソッド名だけ(正確にはそれと初期化されている「定数」変数)を指定できる、一種の抽象クラスである。しかし、interface 名によって、そのクラスが特定のメソッドを実装していることを保証できるのであり、それゆえ interface 参照型はクラスであるかのように振舞うことができる。

このような継承は「多相」と関連して、複雑な様相を呈する。つまり、代入可能性の問題である。

継承と多相

あるクラスに属するとして宣言された変数に対して、同じ型のインスタンスが代入可能であることは言うまでもない。

    SomeClass sc = new SomeClass();
    SomeClass p = sc;

「継承」があると、この関係は拡張される。サブクラスは親クラスのすべての変数・メソッドを備えている。イメージ的には構造体を入れ子にして内部に含んだようなイメージを持っても良いだろう。だから、親クラスに属するとして宣言された変数には、サブクラスのインスタンスが代入可能である。なぜなら、そのインスタンスが親クラスのものであろうと、サブクラスのものであろうと、あるべきメソッドや公開変数(親クラスのメソッドや公開変数)は、同じで(サブクラスにもちゃんとある)からである。

    ChildClass cc = new ChildClass();
    ParentClass pc = new ParentClass();
    ParentClass p = cc;
    p = pc;

これは逆に言えば、同じクラスから派生する複数のサブクラスでも、それらは親クラスに代入される限り、見かけ上は区別できないことになる。しかし、具体的なメソッド呼び出しで呼び出されるメソッドは、親クラスのものではなくて、そのインスタンスが生成されたクラスのメソッドである。これが「多相」である。

   Child1Class c1c = new Child1Class(); /* すべて ParentClass から派生 */
   Child2Class c2c = new Child2Class();
   Child3Class c3c = new Child3Class();
   ParantClass [] pc = { c1c, c2c, c3c }; /* 親クラスによる配列定義(一種の代入) */
   for( int i = 0; i < 3; i++ ) {
      pc[i].someMethod();  /* 基底クラスに someMethod があるから、サブクラスにもある。
                              しかし、実際に呼び出されるのは、サブクラスで上書きして
                              再定義されたものかもしれない。*/
   }

抽象クラス

継承の元になる基底クラスは、単に継承元になるだけで、それ自身のインスタンス化を想定しないクラスもありうる。このような場合に、インスタンス化を「拒む」宣言ができる。これが abstract class 宣言である。また、このようなクラスでは、当然メソッドの実体定義は不可能である。それゆえ、そのメソッドの引数や戻り値を定めた宣言だけをしたい。これが「抽象メソッド」と呼ばれるものである。だから、抽象クラスは次のように宣言し、これを継承した「具象クラス」で初めてインスタンスを作り、利用することができるようになる。

public abstract class MyAbstractClass {
    private int x;
    public void setX( int x ){ this.x = x; }  /* 具象メソッド。ちゃんと定義されている */
    public abstract int nextX();      /* 抽象メソッド */
}

しかし、抽象クラスも基底クラスであり、インスタンスは作れなくても抽象基底クラスの変数は定義できる。それに代入されるのは、その抽象基底クラスを継承した具象クラスでなくてはならない。

     MyConcreteClass1  mcc1;  /* 両方とも MyAbstractClass を継承している */
     MyConcreteClass2  mcc2;
     MyAbstractClass   mac;   /* 抽象基底クラスの型を持つ変数 */

     mcc1 = new MyConcreteClass1();
     mcc2 = new MyConcreteClass2();
     if( x % 2 == 0 ) mac = mcc1; else mac = mcc2;  /* どちらも代入できる */
     int val = mac.nextX();  /* nextX メソッドは基底クラスで定義されている! */

抽象基底クラスで名前のみ定義された nextX メソッドは、具象クラスではちゃんと実装しなければならない(実装しなければ、その派生クラスも抽象クラスにならなくてはならない)。だから、抽象基底クラスの型に代入可能なインスタンスは、その抽象基底クラスから派生した具象クラスであり、それゆえ、抽象基底クラスで(抽象的であれ、具象的であれ)定義されたメソッドなどは、それがインスタンス化されたクラスでは完全に実装されている保証があることになる。だから、抽象基底クラスの型で宣言された変数に対して、基底クラスで定義されたメソッドを実行できるのである。

これは単なる派生の機能ではなくて、実は設計の概念と結び付けて理解すべきである。つまり、

  1. なすべき仕事の中で、汎用的で本質的な内容と、具体的なアプリケーションで求められる偶然的な内容とを分離する。
  2. 汎用的で本質的な内容は、実際には具体的な実装に依存するのかもしれないが、そのインターフェイスだけは統一して定めることができるかもしれない。
  3. 汎用的で本質的な内容を、基底クラスとして定義する。
  4. もし、その本質的な内容が実装依存であるが、インターフェイスだけはあらかじめ定めておくことができるのならば、そのメソッドは抽象メソッドとし、基底クラスは抽象基底クラスで定義する。
  5. 実際の仕事をするクラスは、その基底クラスを継承した具象クラスとして定義する。

このようにやれば、すべてのそのクラス階層で共通する機能は、基底クラスで実装できる。基底クラスでインターフェイスしか定義できない抽象メソッドを呼び出しても良いのである。そして、基底クラスは具象クラスの設計のための「テンプレート」として、どんなメソッドを実装しなければならないかを定めるガイドラインにもなる。それゆえ、少なくとも基底クラスは再利用可能であり、たとえ派生クラスが再利用可能でなくても、新しい派生クラスを定義する時にガイドラインはあることになる。これがクラス設計の根本である。

interface と多相

interface 型は次のように宣言する。アクセス修飾子 public の意味はクラスと変わらない。

public interface myInterface {
        int getVar();
        void setVar( int n );
}

これは実質上次の抽象クラスと同じである。勿論、抽象クラスは interface と異なり、abstract ではない具象メソッドも定義できれば、定数ではないメンバ変数も定義できる。

public abstract class myAbstractClass {
        public abstract int getVar();
        public abstract void setVar( int n );
}

interface を implements するクラスは次のようになる。

public class HaveInterfaceClass extends SomeClass implements myInterface {
        int xxx;
        public void getXXX() { return xxx; }
        int getVar() { return 1; }  /* とか何とか、定義しないとコンパイルが通らない */
        void setVar() { } /* 定義があれば良いのだから、空でもかまわない。*/

次のようにして使うことができる。

        HaveInterfaceClass hic = new HaveInterfaceClass();
        myInterface mi = hic;
        int i = mi.getVar();

要するに interface は抽象基底クラスと大した違いはない。では、なぜこんなものがあるのだろうか? 大きな違いは次のとおり。

  1. 抽象基底クラスから派生する場合には、ただ1つの親クラスしか持てないが、interface はいくつでも「装備」することができる。
  2. 1つの抽象基底クラスで共通して扱うことのできるクラスは、その基底クラスから派生した「兄弟」クラスでなくてはならないが、interface は単に指定されたメソッドを実装することを「要求」するだけなので、継承関係から言えば無関係なクラスを同じ interface 型変数に代入できる!
  3. だから、特定のメソッドが存在することを保証する手段として、あるメソッドを定義した interface を定義し、無関係なクラスにその interface を implements し、特定のメソッドが定義されている「クラス」として共通に扱うことができる。これはGUIを実現する AWT で、頻繁に使われる「委譲イベントモデル」(デザインパターンで言えば Observer パターン)で採用されている方法である。

特にこの3番目の比重は高い。つまり、感覚的には一種のキャストのためのベースとして使われるのである。下のサンプルは簡単な AWT プログラムであり、 Button クラスの addActionListener メソッドの引数は ActionListener interface 型である。addActionListener メソッドは、ボタンが押された時に呼び出される、ボタン動作を定義するコールバック関数を「登録する」メソッドである。だから、その引数はボタン動作を定義するコールバック関数を「備えた」クラスのインスタンスでなくてはならない。しかし、クラスとしてはコールバック関数である public void actionPerformed( ActionEvent ae); を備えているクラスがないかわりに、これの定義が ActionListener interface で定義されており、「ボタンが押されたこと」を受け取りたいクラスはこのクラスを implements する。

/* MyFrame クラスがボタンが押された結果を受け取りたい。だから actionPerformed 
   メソッドが定義されている ActionListener interface を implements しておく */
class MyFrame extends Frame implements ActionListener {
    MyFrame() {
        Label ml = new Label( "下のボタンを押すとこのラベルも変わる" );
        Button b = new Button( "押して!" );
        /* Button クラスでは public void addActionListener(ActionListener l);
           を使って、コールバック関数を持つクラスを登録する。this == MyFrame は
           ActionListener を implements しているので、actionPerformed メソッド
           が存在する(定義し忘れるとコンパイルエラー)ことが保証されている。
           だから、ActionListener「型」として引数に代入可能である。*/
        b.addActionListener( this );
        add( ml );
        add( b );
        show();
    }

    /* コールバック関数 */
    public void actionPerformed( ActionEvent ae ) {
        ml.setText( "ボタンが押された結果、ラベルが変わった!" );
    }
}

コールバック関数を持つのはどのクラスでも良い。そのクラスをマークするのは ActionListener interface を implements しているかどうかで判定できるのである。

class MyLabel extends Label implements ActionListener {
    /* コールバック関数 */
    public void actionPerformed( ActionEvent ae ) {
        setText( "ボタンが押された結果、ラベルが変わった!" );
    }
}

class MyFrame extends Frame {
    MyFrame() {
        MyLabel ml = new MyLabel( "下のボタンを押すとこのラベルも変わる" );
        Button b = new Button( "押して!" );
        b.addActionListener( ml ); /* MyLabel クラスに actionPerformed がある */
        add( ml );
        add( b );
        show();
    }
}

あるいは少々技巧的に...

class MyButton extends Button implements ActionListener {
    Label lab;
    MyButton( String s, Label l ) { 
       super( s ); lab = l;  /* ← Label を保存しておく */
       addActionListener( this ); /* MyButton クラスに actionPerformed がある */
    }
    

    /* コールバック関数 */
    public void actionPerformed( ActionEvent ae ) {
        lab.setText( "ボタンが押された結果、ラベルが変わった!" );
    }
}

class MyFrame extends Frame {
    MyFrame() {
        Label ml = new Label( "下のボタンを押すとこのラベルも変わる" );
        MyButton b = new MyButton( "押して!", ml );
        /* あるいは
        b.addActionListener( b ); かもしれない */
        add( ml );
        add( b );
        show();
    }
}

以上のバリエーションのすべてで、Button をクリックすると、Button にはそのコールバック関数が装備された ActionListener 型のクラスが登録されているので、そのコールバック関数 actionPerformed を呼び出すことが可能であることを確認して欲しい。つまり、ActionListener interface を装備することによって、上記の例では MyFrame クラス、MyLabel クラス、MyButton クラスの3つのクラスを同じ ActionListener「型」として扱うことができたわけである。このように「無関係な」(ホントは以上3つのクラスは無関係ではないが...)クラスを同じ「型」として interface が仮にまとめ上げたと言えるだろう。この感覚はCで言えば型キャストに近いのである。→ Humor

しかし、当然 interface は実装クラスの「抽象的な基盤」としてあらかじめ定義しておく抽象基底クラスと共通する側面も無視できない。だから、ある基底クラスを、抽象基底クラスとして定義するのか、インターフェイスとして定義するのかは、実際のクラス構成を見ながら決めていく必要がある。また、interface も extends によって別な interface を継承することができる。既存の interface に更に別なインターフェイスを加えて定義することができるのである。



copyright by K.Sugiura, 1996-2006