<このシリーズの、前の記事 | このシリーズの目次 | このシリーズの、次の記事>
任意の表現についてどのクラスメンバーがアクセスされるかを決定する、例外なく正しいルールを復習しよう
話題: Javaプログラミング言語
クラスメンバーアクセス解決のルールについての私の理解は正確ではなかった。
君の理解はどのようなものだったのか?
私の理解は、インスタンスメソッドについては、インスタンスタイプが即、呼ばれるメソッドを決めるというものだった。
もっと説明してくれ。
例えば、ベースクラス、'BaseClass'とサブクラス、'ExtendedClass'があるとする。
いいだろう。
'ExtendedClass'のインスタンスは、タイプ、'BaseClass'の変数で参照することができる。
その変数を'l_anObject'と名付けよう。
この変数を使ってあるメソッド、例えば'aMethod'を'l_anObject.aMethod ()'のように呼んだとき、私の理解は、ただ、「インスタンスタイプ、'ExtendedClass'だけが問題であり、変数タイプは関係ない」というものだった。
ああ、それは単純に過ぎるだろう、そういう単純化した理解は快いものではあるが。
「変数タイプが何であろうが、インスタンスがアクセスされるわけであり、そのインスタンスだけが問題だ。だから、'ExtendedClass'が指定した名前のメソッドを持っていれば、それが、呼ばれるものであろう」と私は思っていた。
それは、一つの理想かもしれないが、Javaプログラム言語はそのように作られていない。
以下の例は、私の理解がいかに間違っていたかを示している。
package test.classmemberaccessresolutiontest1;
public class Test1BaseClass {
void aMethod () {
System.out.println ("From Test1BaseClass.");
}
}
package test.classmemberaccessresolutiontest2;
import test.classmemberaccessresolutiontest1.Test1BaseClass;
public class Test1ExtendedClass extends Test1BaseClass {
public void aMethod () {
System.out.println ("From Test1ExtendedClass.");
}
}
package test.classmemberaccessresolutiontest1;
import test.classmemberaccessresolutiontest2.Test1ExtendedClass;
public class Test1TestClass {
public void test () {
Test1BaseClass l_anObject = new Test1ExtendedClass ();
l_anObject.aMethod ();
}
}
君の不正確な理解によると、'Test1ExtendedClass'のメソッドが呼ばれることになるだろうが、Javaはそのようには動かない。
別の直感的であるが、不正確な理解は、メソッドを継承することを、サブクラスが、継承したメソッドを自分のものとして持っているかのようのに思い描くイメージだ。
例えば、'Test2BaseClass'があるメソッド、'printValueA'を持っており、'Test2ExtendedClass'が'Test2BaseClass'を、以下のように拡張するとする。
package test.classmemberaccessresolutiontest1;
public class Test2BaseClass {
protected static String s_valueA = "initial value A";
public static void printValueA () {
System.out.println (String.format ("The value A is \"%s\".", s_valueA));
}
}
package test.classmemberaccessresolutiontest1;
public class Test2ExtendedClass extends Test2BaseClass {
protected static String s_valueA = "overwritten value A";
}
package test.classmemberaccessresolutiontest1;
public class Test2TestClass {
public void test () {
Test2ExtendedClass.printValueA ();
}
}
'Test2ExtendedClass'は'printValueA'を継承したが、それでも、'printValueA'は、'Test2BaseClass'のものであって、'Test2ExtendedClass'のものではない。あたかも、'Test2ExtendedClass'が以下のようになったかのようなイメージを持ったら、
package test.classmemberaccessresolutiontest1;
public class Test2ExtendedClass {
protected static String s_valueA = "overwritten value A";
public static void printValueA () {
System.out.println (String.format ("The value A is \"%s\".", s_valueA));
}
}
それは間違いだ。
上にイメージしたような状態になっているのであれば、上書きされた値がプリントされるだろうが、実際にはそうではない。
実際、世俗の世界では、相続(インヘリタンス)は、相続者が遺産を所持することになることを意味するが、Javaでは、相続(インヘリタンス)は、相続者に遺産が見えて、相続者が遺産を使えることを意味するに過ぎない。
それらの誤った理解は、直感的なイリュージョンだが、我々がなぜそうしたイリュージョンに陥りがちかは理解できる。おそらく、そうしたイリュージョンは、望まれていたが、プログラム言語の実装上の諸事情のために実現されなかったものなのだろう。
上記の理解はシンプルだが、一部のケースでは問題を起こす。ここでは、厳密なルールを復習したい。直感的ではあるが例外をはらんだ、厳密ではないイメージではなく。
試してみよう。
我々が求めるのは、任意の表現に対して、どのクラスメンバーがアクセスされるかを決定するルールだ。
例えば、上の表現、"l_anObject.aMethod ()"に対して、'Test1ExtendedClass'の'aMethod'がアクセスされるのか、それとも'Test1BaseClass'の'aMethod'がアクセスされるのか?
いいだろう。
第1に、表現中の、メンバー名への修飾を見なければならない。
表現が'l_anObject.aMethod ()'の時、'l_anObject'が、メンバー名、'aMethod'への修飾だ。
どんな場合も、何らかの修飾がなければならない。というのも、ただ'aMethod ()'とだけ言ったのでは、コンパイラもランタイムも、それがどの'aMethod'なのか判断できないから。
修飾が明示的に述べられていない場合は、暗黙的な修飾がある。その場合は、その暗黙的修飾を知らなければならない。
修飾が暗黙である場合、誤判断をよりしがちなようだ。修飾が見えないので、事態を奔放に想像しがちだ。
そう。上の、'Test2BaseClass'中の'printValueA'の中で、's_valueA'への暗黙の修飾は'Test2BaseClass'だ。修飾が明示的に述べられていれば、誤判断もしないだろうが、修飾が見えないので、'Test2ExtendedClass'の値がアクセスされるのではないかという期待を抱きがちだ。
だから、修飾が暗黙である場合、その修飾を意識することが助けになる。
表現がスタティックコンテキストの中にある場合は、暗黙の修飾は、その表現を包含するクラスであり、表現がインスタンスコンテキストの中にある場合は、暗黙の修飾は、'this'だ。
'this'が使える任意の場所がインスタンスコンテキストであり、その他がスタティックコンテキストだ。例えば、スタティックメソッドの中は、スタティックコンテキストであり、インスタンスメソッドの中は、インスタンスコンテキストだ。
オーケー。
第2に、修飾のクラスを知らなければならない。修飾のクラスはインスタンスクラスのことではないことに注意しよう。
'l_anObject'の場合、修飾のクラスは、'Test1BaseClass'であって、'Test1ExtendedClass'(インスタンスクラス)ではない。
上で学んだように、我々は、インスタンスクラスへ即ジャンプするということはできない。修飾のクラスが常にスターティングポイントだ。
いいだろう。
修飾は、ただの変数やただのクラスよりも複雑なものかもしれないが、常に1つのクラスを持っている。
例えば、修飾は、'(new Test1ExtendedClass ())'や'((Test1BaseClass) new Test1ExtendedClass ())'といった、一種のObjectかクラスを戻す任意の表現かもしれない。
そう。その第1の例のクラスは'Test1ExtendedClass'であり、第2の例のクラスは'Test1BaseClass'だ。私の言っている意味は分かるだろう。
分かる。
'this'のクラスは、表現を包含するクラスであることに注意しよう。
ここでも、我々は、インスタンスクラスについて話しているのではなく、変数タイプについて話している。
'this'という変数は、我々自身で定義したものではないが、我々は、それが以下のように暗黙に定義されていると考えるべきだ。
class XXX {
private XXX this;
}
そのように思われる。'this'は、サブクラスのインスタンスを参照できるが、'this'の変数タイプは、サブクラスへ、魔法のように変化したりはしない。変数タイプは'XXX'として固定されている。
第3に、修飾がクラスを表わす(インスタンスではなく)のであれば、そのクラスのメンバーがアクセスされる(もちろん、メンバーはそのクラスそのもので定義されておらず、スーパークラスから継承されているかもしれないが、私の言う意味は分かるだろう)。
修飾が'Test2BaseClass'の場合、それは、クラスを表わしている。対して、修飾が'l_anObject'の場合、それは、インスタンスを表している。
そうだ。
修飾がクラスの場合、もちろん、指定したメンバーはスタティックでなければならない。さもなければ、コンパイルエラーが起きる。
見るべきインスタンスが存在しないので、メンバーを探す場所は、修飾のクラスそのものしかない。
今は、修飾がインスタンスを表している場合のみを扱っている。
他の場合については、解決は、この前のセクションで既に完結している。
メンバーが、修飾のクラスから(もちろん、メンバーは、修飾のクラスで定義されておらず、スーパークラスから継承されているかもしれない)インスタンスクラスへ向けてどのようにオーバーライドされているかを我々は知らなければならない。
「オーバーライド」という用語の定義について、若干の混乱があるかもしれない。
ああ、あるかもしれない。「オーバーライド」の公式な定義が何なのか私は知らないが、「オーバーライド」で我々が何を意味しているかを明確にしよう。
すると、我々の定義は、公式なものや他の誰かのものとは異なっているかもしれない。
かもしれない。しかし、我々は我々の定義を使う。というのも、他の定義(そういうものがもしあるとして)は我々の目的の役に立たないから。
第1に、我々は、シャドーイングは、オーバーライドに含まない。
すると、フィールドはシャドーイングされるのであって、決して、オーバーライドされることはない。
そうだ。
第2に、サブクラスから見えないメソッドは、決してオーバーライドされない。
例えば、以下を考えてみよう。
public class ClassA {
private void aMethod () {
System.out.println ("From ClassA.");
}
}
public class ClassB extends ClassA {
public void aMethod () {
System.out.println ("From ClassB.");
}
}
public class ClassC extends ClassB {
public void aMethod () {
System.out.println ("From ClassC.");
}
}
'ClassA'のメソッドは、'ClassB'からは見えないので、オーバーライドされているのではない。対して、'ClassB'のメソッドはオーバーライドされている。
'ClassB'は、'ClassA'のメソッドとは無関係の新たなメソッドを作ったのだ。
分かった。
どんなフィールドもどんなスタティックメソッドもオーバーライドされることはないことを知っておく必要がある。
インスタンスメソッドだけがオーバーライドされることができる。
修飾のクラスからインスタンスクラスへのオーバーライドの連続するリンクの最後のクラスを特定したら、その最後のクラスのメンバーがアクセスされる。
すると、リンクは修飾のクラスから始めて連続していなければならないわけだ。
それが、まず最初に、修飾のクラスを見なければならない理由だ。インスタンスクラスを即見て、インスタンスクラスのメソッドがアクセスされると判断してはいけない。
上の例では、インスタンスクラスが'ClassC'である場合、インスタンスクラスのメソッドは'ClassB'のメソッドをオーバーライドしているが、'( (ClassA) new ClassC ()).aMethod ()'という表現に対して'ClassC'のメソッドが呼ばれると言うことはできない。
そうだ。
実際には、修飾のクラスのメソッドがパブリックかプロテクテッドの場合、シグネチャが同じサブクラスのメソッドは修飾のクラスのメソッドをオーバーライドしていることが保証される。なぜなら、アクセス権限は、継承のチェーンの中で弱めることができないから。
だから、オーバーライドのリンクが途中のどこかで断ち切られることはない。
しかし、修飾のクラスのメソッドのアクセス権限が「未指定」(同じパッケージからのみ見えることを意味する)の場合、インスタンスクラスは、修飾のクラスと同じパッケージ内にいなくても、修飾のクラスのメソッドをオーバーライドしているかもしれない。
例えば、以下を考えてみよう。
package test.classmemberaccessresolutiontest1;
public class Test3BaseClass {
void aMethod () {
System.out.println ("From Test3BaseClass.");
}
}
package test.classmemberaccessresolutiontest1;
public class Test3ExtendedClass extends Test3BaseClass {
public void aMethod () {
System.out.println ("From Test3ExtendedClass.");
}
}
package test.classmemberaccessresolutiontest2;
import test.classmemberaccessresolutiontest1.Test3ExtendedClass;
public class Test3ExtendedExtendedClass extends Test3ExtendedClass {
public void aMethod () {
System.out.println ("From Test3ExtendedExtendedClass.");
}
}
package test.classmemberaccessresolutiontest1;
import test.classmemberaccessresolutiontest2.Test3ExtendedExtendedClass;
public class Test3TestClass {
public void test() {
( (Test3BaseClass) new Test3ExtendedExtendedClass ()).aMethod ();
}
}
結果は以下のとおりだ。
From Test3ExtendedExtendedClass.
'Test3ExtendedClass'がメソッドをパブリックにして、'Test3ExtendedExtendedClass'から見えるようにしたことが重要だ。そのため、'aMethod'は'Test3BaseClass'から'Test3ExtendedExtendedClass'まで連続的にオーバーライドされている。
したがって、どのメソッドが呼ばれるかを知るには、中間にいるサブクラスも見なければならない。修飾のクラスとインスタンスクラスだけではなく。
他方で、「インスタンスクラスは、修飾のクラスと同じパッケージ内にいても修飾のクラスのメソッドをオーバーライドしていないかもしれない」と私は思ったのだが、なぜだか、そうではなさそうだ。
ふむ?. . . なぜだ?私もそう思った。
この例を考えてみよう。
package test.classmemberaccessresolutiontest1;
public class Test4BaseClass {
void aMethod () {
System.out.println ("From Test4BaseClass.");
}
}
package test.classmemberaccessresolutiontest2;
import test.classmemberaccessresolutiontest1.Test4BaseClass;
public class Test4ExtendedClass extends Test4BaseClass {
public void aMethod () {
System.out.println ("From Test4ExtendedClass.");
}
}
package test.classmemberaccessresolutiontest1;
import test.classmemberaccessresolutiontest2.Test4ExtendedClass;
public class Test4ExtendedExtendedClass extends Test4ExtendedClass {
public void aMethod () {
System.out.println ("From Test4ExtendedExtendedClass.");
}
}
package test.classmemberaccessresolutiontest1;
public class Test4TestClass {
public void test() {
( (Test4BaseClass) new Test4ExtendedExtendedClass ()).aMethod ();
}
}
結果は以下のとおりだ。
From Test4ExtendedExtendedClass.
ふむ?ふーむ、一体何が起きているのだろう?
「'Test4BaseClass'の'aMethod'は'Test4ExtendedClass'から見えないから、'Test4ExtendedClass'はそれをオーバーライドしていない。'Test4ExtendedClass'の'aMethod'は、'Test4BaseClass'の'aMethod'とは関係ない新しく作られたメソッドだ。」と私は思ったんだが。
そのはずだ。
そして、「'Test4ExtendedExtendedClass'は'Test4ExtendedClass'の'aMethod'を見るから、'Test4ExtendedClass'の'aMethod'をオーバーライドしている。だから、'Test4BaseClass'の'aMethod'はオーバーライドされていない。結果は、'From Test4BaseClass.'のはずだ。」と思ったわけ。
ふーむ、何が起きているか見え始めてきた。
何が起きているのだ?
'Test4BaseClass'の'aMethod'は、'Test4ExtendedClass'の'aMethod'によってシャドーイングされていないので、'Test4ExtendedExtendedClass'から見えている。
ふむ?. . . ふーむ、それはそうだ。'Test4ExtendedClass'が行なったのはシャドーイングではない。見えないものをシャドーイングはできない。実際、'Test4BaseClass'の'aMethod'と'Test4ExtendedClass'の'aMethod'は全くの別物であって、両方が'Test4ExtendedExtendedClass'から同一のシグネチャで見える。
Javaはそういう事態を避けるために多重継承を禁止しているが、抜け穴があるようだ。
それでは、どちらのメソッドを'Test4ExtendedExtendedClass'はオーバーライドしているのか?
上の'Test4TestClass'を以下のように変えてみよう。
package test.classmemberaccessresolutiontest1;
import test.classmemberaccessresolutiontest2.Test4ExtendedClass;
public class Test4TestClass {
public void test() {
( (Test4BaseClass) new Test4ExtendedExtendedClass ()).aMethod ();
( (Test4ExtendedClass) new Test4ExtendedExtendedClass ()).aMethod ();
}
}
結果は以下のとおりだ。
From Test4ExtendedExtendedClass.
From Test4ExtendedExtendedClass.
うーん、'Test4ExtendedExtendedClass'は両方をオーバーライドしているようだ。
'Test4ExtendedExtendedClass'がメソッドを'public'と宣言しなかったらどうなるのか?
コンパイラは以下のエラーを出す。
error: aMethod() in Test4ExtendedExtendedClass cannot override aMethod() in Test4ExtendedClass
attempting to assign weaker access privileges; was public
ははあ . . .、両方をオーバーライドしなければならない。
'Test4ExtendedExtendedClass'からオーバーライドを除いて、'test'メソッドの最後に以下を追加してみよう。
(new Test4ExtendedExtendedClass ()).aMethod ();
この行の出力はどうなるか?
実は、以下のとおりだ。
From Test4ExtendedClass.
ふーむ . . .、'Test4ExtendedClass'の'aMethod'が優先しているようだ。しかし、どちらにしても、'Test4BaseClass'の'aMethod'もオーバーライドされているという事実は変わらないようだ。
オーバーライドのリンクが途中で断ち切られている例を挙げよう。
package test.classmemberaccessresolutiontest1;
public class Test5BaseClass {
void aMethod () {
System.out.println ("From Test5BaseClass.");
}
}
package test.classmemberaccessresolutiontest1;
public class Test5ExtendedClass extends Test5BaseClass {
void aMethod () {
System.out.println ("From Test5ExtendedClass.");
}
}
package test.classmemberaccessresolutiontest2;
import test.classmemberaccessresolutiontest1.Test5ExtendedClass;
public class Test5ExtendedExtendedClass extends Test5ExtendedClass {
void aMethod () {
System.out.println ("From Test5ExtendedExtendedClass.");
}
}
package test.classmemberaccessresolutiontest1;
import test.classmemberaccessresolutiontest2.Test5ExtendedExtendedClass;
public class Test5TestClass {
public void test() {
( (Test5BaseClass) new Test5ExtendedExtendedClass ()).aMethod ();
}
}
結果は以下のとおりだ。
From Test5ExtendedClass.
結果は、我々が予測したとおりだ。'Test5ExtendedClass'の'aMethod'は、'Test5ExtendedExtendedClass'によってオーバーライドされていない。'Test5ExtendedExtendedClass'からは見えないからだ。
このように、我々は、修飾のクラスからスタートして、インスタンスクラスへ向けてオーバーライドのリンクをたどらなければならない。
我々は意図的に'@Override'アノテーションを使用しなかった(トラップを事前に明かさないため)が、我々は、ミスを避けるため、'@Override'を使うべきだ。