戻る
変数とインスタンスの区別
オブジェクト指向プログラミングの3つ目の特色は多態性である。「多態」というのは、多くの姿を持つという意味である。
オブジェクト指向プログラミングでいう多態とは、もちろん「オブジェクトが多態である」事を言う。
一般にオブジェクト指向プログラミングというと、カプセル化によるデータの隠蔽や継承だけが強調される傾向にある。
しかし、オブジェクト指向プログラミングの真髄はこの多態性にある。
多態性はカプセル化や継承に比べると、その概念を理解するのが難しい。おまけにポインタに対する理解を要求される。
これからその多態性に入っていく。その踏み込みの第1歩として、次の2つを抑える事から始める。
・ 「変数とインスタンス」の違いを明確に区別する
・ メソッドの再定義
ここでいう「変数」とは、クラス型の変数のことである。
例えば、Aというクラスがあって、そのインスタンスを生成するには次のようにする。
A a = new A( );
このときaがクラスAのインスタンスそのものを表しているように見える。しかし、、aは「変数」であってインスタンスではない。
インスタンスの実体は別のところにあり、変数aはそのありかを指しているに過ぎない。
多態性を理解するには、この変数とインスタンスの違いを明確に認識する必要がある。次にいくつか例を示す。
| A a; | aというクラス型の変数を宣言。aはクラスAのインスタンスの位置(アドレス)を記憶する事が出来る。 aはまだ実体のあるインスタンスを指していない。 |
| a = new A( ); | クラスAのインスタンスを生成して、そのアドレスを変数aに代入する。 (以下、このインスタンスを第1インスタンスという) |
| A a2 = new A( ); | a2というクラス型の変数を;宣言。同時にAというインスタンスを生成し、 そのアドレスを変数a2に代入する。(以下、このインスタンスを第2インスタンスという) |
| a = a2; | a2が指している第2インスタンスのアドレスを変数aに代入する。 この時点で変数a、a2ともに第2インスタンスを指す。 第1インスタンスはなお存在し続けるが、どの変数も指していないので利用する事は出来ない。 |
なぜ「変数とインスタンス」の区別が重要か、
それはクラス型変数には、その型のインスタンスだけでなく、下位のインスタンスを代入できるからである。
そして、その事が多態性と大いに関係してくる。
インスタンスの代入互換性
多態性は、インスタンスの代入互換性と大いに関係がある。インスタンスの代入互換性とは、次のことをいう。
インスタンスの代入互換性 : 上位方の変数に下位型のインスタンスを代入できる。
この代入互換性事態は、多態性と直接は関係ない。
しかし、この代入が可能であるからこそ、多態性が有効に機能する。それをこれから説明していく。
上位型と下位型
まずは、継承に関する用語の整理から。
継承は、子クラスを作る事のみならず、子クラスを継承して孫クラスを作る事もできる。(もちろん、それ以上作る事もできる。)
このとき、あるクラスの上位に当たる祖先クラスを総称して上位型という。
また、下位にある子孫クラスを総称して下位型という。
上位型のインスタンスを上位型インスタンス。下位型のインスタンスを下位型インスタンスという。
代入互換性
代入互換性は、次の2つの意味を持っている。
1: 代入演算子(たとえば=など)で代入出来る。
2: メソッドの引数でデータを渡す事が出来る。
要するに、代入互換性があれば=で代入できる。あるいは、メソッドの引数にして値を渡す事が出来るという事である。
void func(A a) ・・・・・A型の引数を取る
{
…
}
func(b); ・・・・・B型のデータを指定する/代入互換性がなければならない
インスタンスの代入互換性
インスタンスの代入互換性について、具体例を示す。いま、次のようなクラスParentを考える。
class Parent { ・・・・・親クラス
void straight( );
System.out.println("直球: straight
ball");
}
}
このクラスは、straightというメソッドだけを持つ。次にこのParentを継承してChildという子クラスを作る。
class Child extends Parent { ・・・・・子クラス
void curve( ) {
System.out.println("カーブ: curve
ball");
}
}
子クラスChildでは、メソッドcurveを追加している。
これで、インスタンスの代入互換性を説明するための準備が整った。まず、親クラスParent型の変数xを作る。
Parent x;
このxにParentのインスタンスを代入出来るのは(型が同じなので)当然である。
x = new Parent( ); ・・・・・親クラスの変数に親クラスのインスタンスを代入
ところが、インスタンスの代入互換性によると、xに子クラスのインスタンスを代入しても良いというのである。
すなわち、次のような代入をしてもエラーにはならない。
x = new Child( ); ・・・親クラスの変数に子クラスのインスタンスを代入
しかし、その逆は不可能である。
Child y; ・・・・・・・・・・・・yは子クラスの変数
y = new Parent( ); ・・・そこに親クラスのインスタンスを代入
このようなことをすると、次のコンパイルエラーになる。
20: 互換性の無い型
出現: Parent
要求: Child
y = new Parent( );
^
エラー1個
これがインスタンスの代入互換性である。
上位型の変数=下位型のインスタンス(これは○)
下位型の変数=上位型のインスタンス(これは×)
次に、以上をテストする為のサンプルを示す
サンプル:Gokan.java
//
//Gokan.java---インスタンスの代入互換性
//
class Parent {
void straight( );
System.out.println("直球: straight
ball");
}
}
class Child extends Parent {
void curve( );
System.out.println("カーブ curve
ball");
}
}
public class Gokan {
public static void main(String[ ]
args) [
Parent x;
x = new Child( );
x.straight( );
}
}
実行例
$ java Gokan
直球: straight ball
$ _
上位型を下位型に代入できない理由
「上位型変数に下位型インスタンスを代入する事は可能だがその逆は不可」である理由は、少し考えれば当然の事である。
下位型は、上位型のメンバーを追加したものなので、全ての上位型のメンバーを持っている。
●Parent(親クラス)
・・・straight
●Child(子クラス)
・・・straight ・・・・・親クラスから継承したメソッド
・・・curve ・・・・・子クラスで追加したメソッド
したがって、子クラスのインスタンスを親クラスの変数に代入しても、何ら不都合が生じない。
しかし、親クラスのインスタンスを子クラスの変数に代入するのは問題である。
親クラスは、子クラスで追加されたメンバーを持っていない。そのため、追加メンバーを参照する事が出来ない。
Child y; ・・・・・・・・・・・・・yは子クラスの変数
y = new Parent( ); ・・・・そこに親クラスのインスタンスを代入(コンパイルエラーになる)
y.curve( ); ・・・・・・・・・・・子クラスで追加したメソッドを実行。しかし、親クラスのインスタンスはこのメソッドを持っていない。
上位型への回帰
上位型の変数に下位型のインスタンスを代入すると、その変数はあたかも上位型のインスタンスが代入されたかのようにして振舞う。
すなわち、代入されているのが下位型のインスタンス(正しくはそのアドレス)であるにも関わらず、
下位型で追加したメンバーを参照する事ができなくなる。
次に例を示す。
Parent x; ・・・・・・・・・・xは親クラスの変数
x = new Child( ); ・・・・そこに子クラスのインスタンスを代入
x.straight( ); ・・・・・・・これは親クラスのメソッドなので○
x.curve( ); ・・・・・・・・・これは子クラスなので×
もし、このようなコードを書くと次のようなコンパイルエラーになる。
21: シンボルを解釈処理できません。
シンボル: メソッド curve ( )
位置: Parent の クラス
x.curve
^
エラー1個
このように「上位型の変数に下位型のインスタンスを代入すると、その変数はあたかも上位型のインスタンスが代入された」かのように振舞う。
ただし、このことは「多態性」のところで再検討される。
メソッドの再定義
メソッドの代入互換性が明らかになったところで、次にメソッドの再定義に進む。
「再定義」という用語は、前章に出て来た「多重定義」と似ているが、両者は似て非なる用語である。
例として前章のUmeクラスとTakeクラスを使って説明する。
UmeクラスにあるメソッドeatとTakeクラスにあるメソッドeat2は殆ど同じ処理を担当する。
にも関わらず、引数が同じなために同じ名前で使う事が出来なかった。(多重定義できなかった)
しかし、実際はeat→eat2のようにメソッドの名前を変える必要は無い。
つまり、UmeでもTakeでもeatという同じ名前のメソッドを定義する事ができるのである。
この場合、クラスTakeはeatというメソッドを2つ(親クラスのeatと子クラスのeat)持つわけではない。
もし、そうだとしたら、引数で区別する事が出来ないので、どちらのeatが実行されるのか不定になってしまう。
あくまでも、Takeが持つeatは1つだけである。
子クラスで親クラスと同じメソッド(名前も引数も型も同じ)を定義すると、子クラスの内容で親クラスの内容を上書きしてしまう。
この場合、親クラスのインスタンスがeatを実行すれば、親クラスのeatが実行される。
また、子クラスのインスタンスがeatを実行すれば、子クラスのeatが実行される。
Ume u = new Ume( );
u.eat( ); ・・・・・・・・・・・・・uはUmeのインスタンスなのでUmeのeatが実行される
Take t = new Take( );
t. eat( ); ・・・・・・・・・・・・・tはTakeのインスタンスなのでTakeのeatが実行される
このように子クラスで親クラスのメソッドを上書きする事をメソッドのオーバーライドという。(以後は、直感的にメソッドの再定義という用語を使う)
次は、前章のSoup_2.javaをメソッドの再定義を使って書き換えたものである。
サンプル:Soup_3.java
//
//Soup_3.java---メソッドの再定義
//
class Ume {
int select = 0;
String sidhes[ ] = {
"ハンバーグ",
"ステーキ",
"魚",
};
void order(int n) {
if (n <0 || N > 2)
select = 0;
else
select = n;
}
void eat( ) {
System.out.println("Ume.eat:";
System.out.println("料理="
+ dishes[select]);
}
}
class Take extends Ume {
int select2 = 0; //スープの選択
String soup[ ] = {
"コンソメ",
"ポタージュ",
"カボチャスープ",
};
//n1は料理の選択(0-2)、n2はスープの選択(0-2)
void order(int n1, int n2) {
if (n1 < 0 || n1 > 2) {
select = 0;
else
select = n1;
if (n2 < 0 || n2 > 2)
select2 = 0;
else
select2 = n2;
}
void eat( ) {
System.out.println("Take.eat:");
System.out.println("料理="
+ dishes[select]);
System.out.println("スープ="
+ soup[select2]);
}
}
public class Soup_3 {
public static void main(String[ ]
args) {
Ume u = new Ume( );
Take t = new Take( );
u.order(2);
u.eat( );
t.order(1,0);
t.eat( );
}
}
実行例
$ java Soup_3
Ume.eat;
料理=魚
Take.eat:
料理=ステーキ
スープ=コンソメ
$ _
メソッドを検索する順序
メソッドを下位クラスで再定義すると、上位クラスのメソッドは隠される。
これをjavaから見るとどうなるか―すなわりjavaが実行するメソッドをどの順序で決定していくのかをまろめる。
1: そのクラスで全ての引数が一致するメソッドがあればそれを実行する。
例えば、次のようにa.func( )が実行されたとする。aはクラスAのインスタンスなので、クラスAの中でfuncが定義されているかを検索する。
その結果、該当するメソッドがあればソレを実行する。
A a = new A( );
a.func(1,2);
以下、次のように上位のクラスにさかのぼって検索を続ける。
2: 無ければ親クラスを探す
3: それでも無ければ更にその親をさかのぼって探す
それでも該当するメソッドが見つから無い場合は、次のように処理される。
4: それでも見つからない場合はコンパイルエラー
この為、下位クラスで同じ名前のメソッドがあると、上位クラスのメソッドは隠される事になる。
多態性
「インスタンスの代入互換性」と「メソッドの再定義」を組み合わせると、多態性が完成する。
次の2つのクラスで考える。
class A { ・・・・・親クラス
void speak( ) {
System.out.println("親クラスAのspeak(
)を実行中");
}
}
class B extends A { ・・・・・子クラス
void speak( ); { ・・・・・再定義
System.out.println("子クラスBのspeak(
)を実行中");
}
}
子クラスBでメソッドspeakを再定義している。ここまでは、「メソッドの再定義」の話である。
これに「インスタンスの代入互換性」を組み合わせる。すなわち親クラスの変数に子クラスのインスタンスを代入する。
A a; ・・・・・・・・・・aは親クラスA型の変数
a = new B( ); ・・・そこに子クラスのインスタンスを代入
この代入は「インスタンスの代入互換性」から許される。次にメソッドspeakを実行する。
a.speak( );
ここで問題にしたいのが「このspeakは親クラスAのspeakが実行されるのか、それとも子クラスBのspeakが実行されるのか」という事である。
「上位型への回帰」のところの説明によると、
・上位型の変数に下位型インスタンスを代入すると、その変数はあたかも上位型のインスタンスが代入されたかのように振舞う。
という事であった。この説明によると、変数aは親クラスのインスタンスとして振舞う。
したがって、実行されるspeakは親クラスのものという事になる。
そのことを確かめるため、次のサンプルアプリケーションを実行する。
サンプル:Polymor.java
//
//Polymor.java---多態性
//
class A {
void speak( ) {
System.out.println("親クラスAのspeak(
)を実行中");
}
}
class B extends A {
void speak( ) {
System.out.println("子クラスBのspeak(
)を実行中");
}
}
public class Polymor {
public static void main(String[ ]
args) {
A a;
a = new B( );
a.speak( );
}
}
実行例
$ java Polymor
子クラスBのspeak( )を実行中
$ _
この結果を見ると、予想に反して子クラスのspeakが実行されているのが分かる。
仮想メソッド
親クラスの変数を通じてメソッドを参照したにも関わらず、子クラスのメソッドが実行されたのは、j
avaのメソッドがデフォルトで仮想メソッドになるからである。
そのようにならない言語もある。例えばC++の関数は、デフォルトでは仮想にならない。
そのため前項のアプリケーションをC++で書くと、最初に予想したものと同じ結果になる。(親クラスのspeakが実行される。)
仮想メソッドというのは、上位クラスのメソッドが下位クラスで再定義されている時、実行されるメソッドが実行時に決まるようなメソッドの事を言う。
a.speak( )というメソッドの呼び出しは(コンパイル時は)仮の形を取っていて、実際に呼び出されるメソッドは実行時に決まる。
すなわち、実行時にaがどのインスタンスを指しているかによって決まる。そのようなメソッドが仮想メソッドである。
a = new A( );
a.speak( ); ・・・・・(1)
a = new B( );
a.speak( ); ・・・・・(2)
たとえば、この(1)と(2)は全く同じ形をしている。にも関わらず(1)と(2)は異なるメソッドが実行される。
(1)はクラスAのインスタンスが代入されているので、クラスAのspeakが実行される。
しかし、(2)はクラスBのインスタンスが代入されているので、クラスBのインスタンスが実行される。
この例では、何も実行時でなく、コンパイル時に実行するメソッドを(決定しようと思えば)決定する事が出来る。
しかし、コンパイル時には決定できないような例を事項で説明する。
動的リンク
実行されるメソッドが実行時に決まるという「仮想メソッド」についてもう1つ例をあげる。
クラスAとそれを継承したクラスBについては、今の例と同じである。
class A { ・・・・・親クラス
void speak( ) {
System.out.println("親クラスAのSpek(
)を実行中");
}
}
class B extends A { ・・・・・子クラス
void speak( ) {
System.out.println("子クラスBのspeak(
)を実行中");
}
}
これらの親子クラスに対して、次のようなメソッドを作る。
static void call_speak(A x) {
x.speak( );
メソッドcall_speakは親クラスAのインスタンス(のアドレス)を受取る。そして、そのインスタンスのspeakメソッドを呼び出す。
※補: メソッドcall_speakをstaticにしているのは、後でmainメソッドから呼び出す予定があるからである。
mainメソッドはstaticメソッドである。staticメソッドは、同じクラスにある通常メソッドを呼び出す事は出来ない。
参照できるのは、staticメソッドだけである。
このcall_speakに対して、次のようなメソッドを作ったとする。
public static void main(String[ ] args)
{
int n = Integer.parseInt(args[0]); ・・・・・第1引数にnを保存
if (n == 1) ・・・・・第1引数がnなら
call_speak(new A( )); ・・・・・親クラスのインスタンス
else if (n == 2) ・・・・・第1引数が2なら
call_speak(new B( )); ・・・・・子クラスのインスタンス
}
このmainメソッドは、コマンドライン引数の値によりcall_speakに渡すインスタンスの種類を変えている。
すなわち1が指定されたら親クラスのインスタンスを渡し、2が指定されたら子クラスのインスタンスを渡している。
コマンドライン引数を指定するのはユーザーだから、どちらの値が指定されるかは、実行時にならないと分からない。
このことはcall_spaakにとって、どのようなインスタンスがxに代入されているかは、実行時にならないと分からない事を意味する。
static void call_speak(A x) {
x.speak( ); ・・・・・xが親クラスのインスタンスか、子クラスのインスタンスかが不定
つまり、x_speak( )で実行すべきspeakが親クラスのものなのか、子クラスのものなのかをコンパイル時に決める事が出来ない。
では、実際にこのcall_speakはどのように動作するのだろう。以下のサンプルアプリケーションで確認する。
サンプル:Polymor_2.java
//
//Polymor.java---多態性
//
class A {
void speak( ) {
System,out,println("親クラスAのspeak(
)を実行中");
}
}
class B extends A {
void speak( ) {
System.out.println("子クラスBのspeak(
)を実行中");
}
}
public class Polymor_2 {
staticvoid call_speak(A x) {
x,speak( );
}
static void usage( ) {
System,out,peintln("使い方:コマンド名1 or 2");
System.exit(1);
}
public static void main(String[ ]
args) {
if(args.length < 1)
usage( );
int n = Integer.parseInt(args[0]);
if (n == 1)
call_speak(new A( ));
elase if (n == 2)
call_speak(new B( ));
else
usage( );
}
}
実行例
$ java Polymor_2 ・・・・・引数を指定しない
使い方:コマンド名1 or 2 ・・・・・エラーメッセージ
$ java Polymor_2 1 ・・・・・1を指定
親クラスAのspeak( )を実行中
$ java Polymor_2 2 ・・・・・2を指定
子クラスBのspeak( )を実行中
$ _
これを見ると、call_speakは実行時に決められる引数により、ちゃんと動作を変えているのが分かる。
static void call_speak(A x) {
x.speak( ); ・・・・・xのインスタンスにより実行するspeakを変えている。
これは、speakが仮想メソッドであるからに他ならない。仮想メソッドは、コンパイル時に実行されるメソッドを決める事が出来ない。
仮想メソッドを実現するためのリンクの方法を動的リンク、あるいは遅延バイディング、動的バイディングという。
※補: 1つのソースファイルから実行ファイルを作られるまでには、(処理系によって異なるが)コンパイルとリンクの2つの処理が必要である。
多態性
クラス型の変数は、そのクラスの;インスタンスのみならず、下位型のインスタンスを代入する事が出来る。(インスタンスの代入互換性)
しかもメソッドを呼び出すと、そのとき代入されているインスタンスのメソッドが実行される。(仮想メソッド)
A a; ・・・・・aはクラスAの変数
a = new A( ); ・・・・・クラスAのインスタンスを代入
a.speak( ); ・・・・・・・クラスAのspeakが実行される
a = new B( ); ・・・・・下位型であるクラスBのインスタンスを代入
a.speak( ); ・・・・・・・クラスBのspeakが実行される
この例でいうと、aはある時クラスAのインスタンスを指し、またある時はクラスBのインスタンスを指している。
このようにaは時間とともにその姿を変えている。しかも、指すインスタンスによってその動作も変わっている。
a.speak( )はaがどの姿をしているかにより、適切なspeakが実行される。―これが多態性である。
多態性は、ある親クラスを起点にそこから継承された一族を1つの変数で管理することを可能にする。
次へ