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

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

継承と変数・メソッド






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

継承と変数・メソッド

では、クラスの継承ではなくて、そのクラスに含まれるメソッドや変数がどのように「継承」されるのかを見ていこう。その変数やメソッドが少なくとも protected である限り(要するに private で宣言されていない限り)、「継承」によって、派生クラスでは基底クラスの変数やメソッドがあたかも派生クラスで定義されているかのように使うことができる。これはオブジェクト指向の当然の機能であるが、この解説ではこれを次のように区別しようと思う。つまり、

真の継承
動的に参照されること。つまりコンパイル時に、派生クラスの中の同名要素の、どれが参照されるのかは決定されずに、実行時にそれが生成された真の所属クラスに従って解決される。言い替えれば、派生クラスによって「上書き」されるもの。
隠蔽
静的に参照されること。つまりコンパイル時に、その型の要素として決定され、定義されている通りにその型の要素としてのみ参照される。だから、派生クラスによって決して「上書き」されない。

このように区別して見たときには、「真に継承」されるのは、Java では「インスタンス・メソッド」だけである。後の要素はすべて「隠蔽」である。ちなみに C++ では、デフォルトではすべての要素は「隠蔽」され、特に virtual で指定したメソッドのみが「真に継承」されるという仕様であるが、Java の「インスタンス・メソッド」(要するにクラスメソッドではないメソッド)はデフォルトで「真に継承」される。

インスタンス変数

逆に言えばインスタンス変数は一切継承されない。つまり派生クラスに同名変数があったとしても、それは一切「上書き」されない。インスタンス変数は所属クラスによってコンパイル時に完全に決定され、動的には一切変更されない。これは逆に言えば、インスタンス変数名はすべてクラス固有であり、クラス内で変数名をすべて統一的に変更したとしても、基底クラス、派生クラスには特に super (親クラスを示すインスタンス)によってそれを参照しない限り、一切影響がない。

実例で見よう。

public class myClass {
        public int x = 10;
        public int getX() { return x; } /* ←この x は myClass の x である */
}

public class mySubClass extends myClass {
        public int x = 20;
        public int getX() { return x; } /* ←この x は mySubClass の x である */
}

と、同一のインスタンス変数があったとしても、それらは完全に区別され、上書きされることはない。参照解決のルールは、単にCであったような「スコープ解決」ルールであり、ある変数はまずその所属クラスで定義されているものとして解決され、もし所属クラスで定義されていない場合にのみ、基底クラスに所属しているかどうかを順次上に辿って検索して解決されていく。これが「隠蔽」であり、感覚的には派生クラスの「名前」によって基底クラスの同一の「名前」が「覆い隠されてしまう」に過ぎないのである。

インスタンス・メソッド

しかし、インスタンス・メソッドは上書きされる。この違いは次のコードで明らかになる。

        mySubClass mcc = new mySubClass();
        myClass mac = (myClass)mcc;  /* キャストはなくて可 */
        System.out.println( mcc.x + mac.x ); /* 10+20=30 */
        System.out.println( mcc.getX() + mac.getX() );
        /* 両方とも、mySubClass.getX() == 20 が呼び出される */

つまり、インスタンス変数として参照された場合には、「今そのインスタンスが所属するクラス」のインスタンス変数として解決される(System.out.println( mcc.x + mac.x ); → 10+20=30)。しかし、インスタンス・メソッドとして参照された場合には、次のロジックで動作するのである。

        System.out.println( mcc.getX() + mac.getX() );
  1. インスタンス mcc の所属クラスは myConcreteClass である。
  2. だから、myConcreteClass のメソッド getX() を呼び出す。
  3. そうすると、myConcreteClass.getX() は、myConcreteClass のインスタンス変数 x を参照し、その値を返す(== 10)。
  4. インスタンス mac の所属クラスは表面上 myAbstractClass である。
  5. しかし、実はこのインスタンスは myConcreateClass として生成され、この場合には mcc と同じインスタンスである。
  6. だから、呼び出される getX() メソッドは myAbstractClass のそれではなく、派生クラスの myConcreteClass.getX() である。
  7. そうすると、myConcreteClass.getX() は、myConcreteClass のインスタンス変数 x を参照し、その値を返す(== 10)。
  8. だから双方とも myConcreteClass のインスタンス変数 x を参照し、その結果を足して表示する。(== 20)

それゆえ、インスタンス・メソッドは「真に継承」される。つまり、そのインスタンスが生成された時のクラスをインスタンス自身が「憶えて」いて、いくら基底クラス(や interface型)にキャストされて見かけのクラスが異なっていようとも、正しく「生成されたクラスのメソッド」が呼び出されるのである。これが「多相」の基盤であり、オブジェクト指向言語のさまざまなトリックの基盤になっている。

特にサブクラス内で親クラスの処理を引続き行いたい場合には、親クラスのポインタを示す super を使って、次のようにする。

public class mySubClass extends myClass {
        ..................
        private int getX() { return super.getX() + this.x; }
                                    /* == 10        == 20 */
}

もう一度まとめよう。同一識別名のインスタンス・メソッドは「上書きされる」が、インスタンス変数は派生クラスの変数名によって「隠蔽」されるに過ぎないのである。

クラスのフィールド変数宣言

通常、メソッドのトップレベルに書かれるフィールド変数宣言の形式は次の通り。

修飾子 型 変数名 [ = 初期化子 ];

変数は次の修飾子を取ることができ、順番は問わない。

public, protected, private
アクセス修飾子は通例通り。インスタンス変数は継承によって隠蔽されるに過ぎないから、親クラスの同名変数と矛盾するアクセス修飾子であっても問題はない。

final
これは継承による上書きを禁止するのではなくて、初期化以外の代入を禁止する。正規の初期化式によるものでなくても良く、代入回数を1回に制限することで実装されている。これは上書き禁止と同様に、コンパイラの最適化を促進する。

static
これは「クラス変数」というものであり、すぐ次で解説する。

transient
一時的で、恒久的に保存する内容ではない変数を指示する。つまりこれは、そのオブジェクトをシリアライズする(オブジェクトの標準形式でストリームに書き出す)時に、不要なフィールドを指示する。特にシリアライズに気を配るのでなければ、気にしなくて良い。詳しくは「組み込みクラスライブラリ・パッケージ〜java.io.*」で解説する。

volatile
Cの volatile とほぼ同じである。つまりこれは最適化に関する指示であり、この変数に対する参照で過度にレジスタで保持し続けない(アクセス毎にちゃんと変数領域を参照する)ように最適化すべきことをコンパイラに伝える。Java の場合、大まかなCとは違ってこの volatile 修飾がされた変数アクセスについてのコンパイラ動作が詳細に定義されているが、これは専門的に過ぎるので解説しない。しかし、マルチスレッド環境での実行を前提とすると、別スレッドによる volatile 変数に対する変更が、即座にこのスレッドの参照に反映されて、変更されたことを見落とすことがなくなる、というメリットがある。Java はマルチスレッドで動作するので、Cの場合よりもずっと有用である。



copyright by K.Sugiura, 1996-2006