2017年10月7日土曜日

4: クラスメンバーアクセス解決の厳密なルールを復習しよう

<このシリーズの、前の記事 | このシリーズの目次 | このシリーズの、次の記事>

本文 START

任意の表現についてどのクラスメンバーがアクセスされるかを決定する、例外なく正しいルールを復習しよう

話題: Javaプログラミング言語

以下の理解は厳密には正しくない

-Hypothesizer

クラスメンバーアクセス解決のルールについての私の理解は正確ではなかった。

-Rebutter

君の理解はどのようなものだったのか?

-Hypothesizer

私の理解は、インスタンスメソッドについては、インスタンスタイプが即、呼ばれるメソッドを決めるというものだった。

-Rebutter

もっと説明してくれ。

-Hypothesizer

例えば、ベースクラス、'BaseClass'とサブクラス、'ExtendedClass'があるとする。

-Rebutter

いいだろう。

-Hypothesizer

'ExtendedClass'のインスタンスは、タイプ、'BaseClass'の変数で参照することができる。

-Rebutter

その変数を'l_anObject'と名付けよう。

-Hypothesizer

この変数を使ってあるメソッド、例えば'aMethod'を'l_anObject.aMethod ()'のように呼んだとき、私の理解は、ただ、「インスタンスタイプ、'ExtendedClass'だけが問題であり、変数タイプは関係ない」というものだった。

-Rebutter

ああ、それは単純に過ぎるだろう、そういう単純化した理解は快いものではあるが。

-Hypothesizer

「変数タイプが何であろうが、インスタンスがアクセスされるわけであり、そのインスタンスだけが問題だ。だから、'ExtendedClass'が指定した名前のメソッドを持っていれば、それが、呼ばれるものであろう」と私は思っていた。

-Rebutter

それは、一つの理想かもしれないが、Javaプログラム言語はそのように作られていない。

-Hypothesizer

以下の例は、私の理解がいかに間違っていたかを示している。

@Java Source Code
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 ();
 }
}
-Rebutter

君の不正確な理解によると、'Test1ExtendedClass'のメソッドが呼ばれることになるだろうが、Javaはそのようには動かない。

-Hypothesizer

別の直感的であるが、不正確な理解は、メソッドを継承することを、サブクラスが、継承したメソッドを自分のものとして持っているかのようのに思い描くイメージだ。

例えば、'Test2BaseClass'があるメソッド、'printValueA'を持っており、'Test2ExtendedClass'が'Test2BaseClass'を、以下のように拡張するとする。

@Java Source Code
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 ();
 }
}
-Rebutter

'Test2ExtendedClass'は'printValueA'を継承したが、それでも、'printValueA'は、'Test2BaseClass'のものであって、'Test2ExtendedClass'のものではない。あたかも、'Test2ExtendedClass'が以下のようになったかのようなイメージを持ったら、

@Java Source Code
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));
 }
}

それは間違いだ。

-Hypothesizer

上にイメージしたような状態になっているのであれば、上書きされた値がプリントされるだろうが、実際にはそうではない。

実際、世俗の世界では、相続(インヘリタンス)は、相続者が遺産を所持することになることを意味するが、Javaでは、相続(インヘリタンス)は、相続者に遺産が見えて、相続者が遺産を使えることを意味するに過ぎない。

-Rebutter

それらの誤った理解は、直感的なイリュージョンだが、我々がなぜそうしたイリュージョンに陥りがちかは理解できる。おそらく、そうしたイリュージョンは、望まれていたが、プログラム言語の実装上の諸事情のために実現されなかったものなのだろう。

そこで、クラスメンバーアクセス解決の厳密なルールを復習しよう

-Hypothesizer

上記の理解はシンプルだが、一部のケースでは問題を起こす。ここでは、厳密なルールを復習したい。直感的ではあるが例外をはらんだ、厳密ではないイメージではなく。

-Rebutter

試してみよう。

-Hypothesizer

我々が求めるのは、任意の表現に対して、どのクラスメンバーがアクセスされるかを決定するルールだ。

例えば、上の表現、"l_anObject.aMethod ()"に対して、'Test1ExtendedClass'の'aMethod'がアクセスされるのか、それとも'Test1BaseClass'の'aMethod'がアクセスされるのか?

-Rebutter

いいだろう。

第1に、表現中の、メンバー名への修飾を特定しなければならない

-Hypothesizer

第1に、表現中の、メンバー名への修飾を見なければならない。

表現が'l_anObject.aMethod ()'の時、'l_anObject'が、メンバー名、'aMethod'への修飾だ。

どんな場合も、何らかの修飾がなければならない。というのも、ただ'aMethod ()'とだけ言ったのでは、コンパイラもランタイムも、それがどの'aMethod'なのか判断できないから。

修飾が明示的に述べられていない場合は、暗黙的な修飾がある。その場合は、その暗黙的修飾を知らなければならない。

-Rebutter

修飾が暗黙である場合、誤判断をよりしがちなようだ。修飾が見えないので、事態を奔放に想像しがちだ。

-Hypothesizer

そう。上の、'Test2BaseClass'中の'printValueA'の中で、's_valueA'への暗黙の修飾は'Test2BaseClass'だ。修飾が明示的に述べられていれば、誤判断もしないだろうが、修飾が見えないので、'Test2ExtendedClass'の値がアクセスされるのではないかという期待を抱きがちだ。

-Rebutter

だから、修飾が暗黙である場合、その修飾を意識することが助けになる。

-Hypothesizer

表現がスタティックコンテキストの中にある場合は、暗黙の修飾は、その表現を包含するクラスであり、表現がインスタンスコンテキストの中にある場合は、暗黙の修飾は、'this'だ。

'this'が使える任意の場所がインスタンスコンテキストであり、その他がスタティックコンテキストだ。例えば、スタティックメソッドの中は、スタティックコンテキストであり、インスタンスメソッドの中は、インスタンスコンテキストだ。

-Rebutter

オーケー。

第2に、修飾のクラスを知らなければならない

-Hypothesizer

第2に、修飾のクラスを知らなければならない。修飾のクラスはインスタンスクラスのことではないことに注意しよう。

-Rebutter

'l_anObject'の場合、修飾のクラスは、'Test1BaseClass'であって、'Test1ExtendedClass'(インスタンスクラス)ではない。

-Hypothesizer

上で学んだように、我々は、インスタンスクラスへ即ジャンプするということはできない。修飾のクラスが常にスターティングポイントだ。

-Rebutter

いいだろう。

-Hypothesizer

修飾は、ただの変数やただのクラスよりも複雑なものかもしれないが、常に1つのクラスを持っている。

-Rebutter

例えば、修飾は、'(new Test1ExtendedClass ())'や'((Test1BaseClass) new Test1ExtendedClass ())'といった、一種のObjectかクラスを戻す任意の表現かもしれない。

-Hypothesizer

そう。その第1の例のクラスは'Test1ExtendedClass'であり、第2の例のクラスは'Test1BaseClass'だ。私の言っている意味は分かるだろう。

-Rebutter

分かる。

-Hypothesizer

'this'のクラスは、表現を包含するクラスであることに注意しよう。

-Rebutter

ここでも、我々は、インスタンスクラスについて話しているのではなく、変数タイプについて話している。

-Hypothesizer

'this'という変数は、我々自身で定義したものではないが、我々は、それが以下のように暗黙に定義されていると考えるべきだ。

@Java Source Code
class XXX {
 private XXX this;
}
-Rebutter

そのように思われる。'this'は、サブクラスのインスタンスを参照できるが、'this'の変数タイプは、サブクラスへ、魔法のように変化したりはしない。変数タイプは'XXX'として固定されている。

第3に、修飾がクラスを表わすのであれば、そのクラスのメンバーがアクセスされる(その場合の解決は完結する)

-Hypothesizer

第3に、修飾がクラスを表わす(インスタンスではなく)のであれば、そのクラスのメンバーがアクセスされる(もちろん、メンバーはそのクラスそのもので定義されておらず、スーパークラスから継承されているかもしれないが、私の言う意味は分かるだろう)。

-Rebutter

修飾が'Test2BaseClass'の場合、それは、クラスを表わしている。対して、修飾が'l_anObject'の場合、それは、インスタンスを表している。

-Hypothesizer

そうだ。

修飾がクラスの場合、もちろん、指定したメンバーはスタティックでなければならない。さもなければ、コンパイルエラーが起きる。

-Rebutter

見るべきインスタンスが存在しないので、メンバーを探す場所は、修飾のクラスそのものしかない。

第4に、メンバーが、修飾のクラスからインスタンスクラスへ向けてどのようにオーバーライドされているかを知らなければならない

-Hypothesizer

今は、修飾がインスタンスを表している場合のみを扱っている。

-Rebutter

他の場合については、解決は、この前のセクションで既に完結している。

-Hypothesizer

メンバーが、修飾のクラスから(もちろん、メンバーは、修飾のクラスで定義されておらず、スーパークラスから継承されているかもしれない)インスタンスクラスへ向けてどのようにオーバーライドされているかを我々は知らなければならない。

-Rebutter

「オーバーライド」という用語の定義について、若干の混乱があるかもしれない。

-Hypothesizer

ああ、あるかもしれない。「オーバーライド」の公式な定義が何なのか私は知らないが、「オーバーライド」で我々が何を意味しているかを明確にしよう。

-Rebutter

すると、我々の定義は、公式なものや他の誰かのものとは異なっているかもしれない。

-Hypothesizer

かもしれない。しかし、我々は我々の定義を使う。というのも、他の定義(そういうものがもしあるとして)は我々の目的の役に立たないから。

第1に、我々は、シャドーイングは、オーバーライドに含まない。

-Rebutter

すると、フィールドはシャドーイングされるのであって、決して、オーバーライドされることはない。

-Hypothesizer

そうだ。

第2に、サブクラスから見えないメソッドは、決してオーバーライドされない。

-Rebutter

例えば、以下を考えてみよう。

@Java Source Code
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'のメソッドはオーバーライドされている。

-Hypothesizer

'ClassB'は、'ClassA'のメソッドとは無関係の新たなメソッドを作ったのだ。

-Rebutter

分かった。

-Hypothesizer

どんなフィールドもどんなスタティックメソッドもオーバーライドされることはないことを知っておく必要がある。

-Rebutter

インスタンスメソッドだけがオーバーライドされることができる。

-Hypothesizer

修飾のクラスからインスタンスクラスへのオーバーライドの連続するリンクの最後のクラスを特定したら、その最後のクラスのメンバーがアクセスされる。

-Rebutter

すると、リンクは修飾のクラスから始めて連続していなければならないわけだ。

-Hypothesizer

それが、まず最初に、修飾のクラスを見なければならない理由だ。インスタンスクラスを即見て、インスタンスクラスのメソッドがアクセスされると判断してはいけない。

-Rebutter

上の例では、インスタンスクラスが'ClassC'である場合、インスタンスクラスのメソッドは'ClassB'のメソッドをオーバーライドしているが、'( (ClassA) new ClassC ()).aMethod ()'という表現に対して'ClassC'のメソッドが呼ばれると言うことはできない。

-Hypothesizer

そうだ。

実際には、修飾のクラスのメソッドがパブリックかプロテクテッドの場合、シグネチャが同じサブクラスのメソッドは修飾のクラスのメソッドをオーバーライドしていることが保証される。なぜなら、アクセス権限は、継承のチェーンの中で弱めることができないから。

-Rebutter

だから、オーバーライドのリンクが途中のどこかで断ち切られることはない。

-Hypothesizer

しかし、修飾のクラスのメソッドのアクセス権限が「未指定」(同じパッケージからのみ見えることを意味する)の場合、インスタンスクラスは、修飾のクラスと同じパッケージ内にいなくても、修飾のクラスのメソッドをオーバーライドしているかもしれない。

-Rebutter

例えば、以下を考えてみよう。

@Java Source Code
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 ();
 }
}

結果は以下のとおりだ。

@Output
From Test3ExtendedExtendedClass.
-Hypothesizer

'Test3ExtendedClass'がメソッドをパブリックにして、'Test3ExtendedExtendedClass'から見えるようにしたことが重要だ。そのため、'aMethod'は'Test3BaseClass'から'Test3ExtendedExtendedClass'まで連続的にオーバーライドされている。

-Rebutter

したがって、どのメソッドが呼ばれるかを知るには、中間にいるサブクラスも見なければならない。修飾のクラスとインスタンスクラスだけではなく。

-Hypothesizer

他方で、「インスタンスクラスは、修飾のクラスと同じパッケージ内にいても修飾のクラスのメソッドをオーバーライドしていないかもしれない」と私は思ったのだが、なぜだか、そうではなさそうだ。

-Rebutter

ふむ?. . . なぜだ?私もそう思った。

-Hypothesizer

この例を考えてみよう。

@Java Source Code
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 ();
 }
}

結果は以下のとおりだ。

@Output
From Test4ExtendedExtendedClass.

「未指定」アクセス権限メソッドの奇妙なオーバーライド

-Rebutter

ふむ?ふーむ、一体何が起きているのだろう?

-Hypothesizer

「'Test4BaseClass'の'aMethod'は'Test4ExtendedClass'から見えないから、'Test4ExtendedClass'はそれをオーバーライドしていない。'Test4ExtendedClass'の'aMethod'は、'Test4BaseClass'の'aMethod'とは関係ない新しく作られたメソッドだ。」と私は思ったんだが。

-Rebutter

そのはずだ。

-Hypothesizer

そして、「'Test4ExtendedExtendedClass'は'Test4ExtendedClass'の'aMethod'を見るから、'Test4ExtendedClass'の'aMethod'をオーバーライドしている。だから、'Test4BaseClass'の'aMethod'はオーバーライドされていない。結果は、'From Test4BaseClass.'のはずだ。」と思ったわけ。

-Rebutter

ふーむ、何が起きているか見え始めてきた。

-Hypothesizer

何が起きているのだ?

-Rebutter

'Test4BaseClass'の'aMethod'は、'Test4ExtendedClass'の'aMethod'によってシャドーイングされていないので、'Test4ExtendedExtendedClass'から見えている。

-Hypothesizer

ふむ?. . . ふーむ、それはそうだ。'Test4ExtendedClass'が行なったのはシャドーイングではない。見えないものをシャドーイングはできない。実際、'Test4BaseClass'の'aMethod'と'Test4ExtendedClass'の'aMethod'は全くの別物であって、両方が'Test4ExtendedExtendedClass'から同一のシグネチャで見える。

Javaはそういう事態を避けるために多重継承を禁止しているが、抜け穴があるようだ。

-Rebutter

それでは、どちらのメソッドを'Test4ExtendedExtendedClass'はオーバーライドしているのか?

-Hypothesizer

上の'Test4TestClass'を以下のように変えてみよう。

@Java Source Code
package test.classmemberaccessresolutiontest1;

import test.classmemberaccessresolutiontest2.Test4ExtendedClass;

public class Test4TestClass {
 public void test() {
  ( (Test4BaseClass) new Test4ExtendedExtendedClass ()).aMethod ();
  ( (Test4ExtendedClass) new Test4ExtendedExtendedClass ()).aMethod ();
 }
}

結果は以下のとおりだ。

@Output
From Test4ExtendedExtendedClass.
From Test4ExtendedExtendedClass.
-Rebutter

うーん、'Test4ExtendedExtendedClass'は両方をオーバーライドしているようだ。

'Test4ExtendedExtendedClass'がメソッドを'public'と宣言しなかったらどうなるのか?

-Hypothesizer

コンパイラは以下のエラーを出す。

@Output
error: aMethod() in Test4ExtendedExtendedClass cannot override aMethod() in Test4ExtendedClass
attempting to assign weaker access privileges; was public
-Rebutter

ははあ . . .、両方をオーバーライドしなければならない。

'Test4ExtendedExtendedClass'からオーバーライドを除いて、'test'メソッドの最後に以下を追加してみよう。

@Java Source Code
  (new Test4ExtendedExtendedClass ()).aMethod ();

この行の出力はどうなるか?

-Hypothesizer

実は、以下のとおりだ。

From Test4ExtendedClass.

-Rebutter

ふーむ . . .、'Test4ExtendedClass'の'aMethod'が優先しているようだ。しかし、どちらにしても、'Test4BaseClass'の'aMethod'もオーバーライドされているという事実は変わらないようだ。

オーバーライドのリンクが途中で断ち切られている例

-Hypothesizer

オーバーライドのリンクが途中で断ち切られている例を挙げよう。

@Java Source Code
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 ();
 }
}

結果は以下のとおりだ。

@Output
From Test5ExtendedClass.
-Rebutter

結果は、我々が予測したとおりだ。'Test5ExtendedClass'の'aMethod'は、'Test5ExtendedExtendedClass'によってオーバーライドされていない。'Test5ExtendedExtendedClass'からは見えないからだ。

このように、我々は、修飾のクラスからスタートして、インスタンスクラスへ向けてオーバーライドのリンクをたどらなければならない。

'@Override'アノテーションを使うべき

-Hypothesizer

我々は意図的に'@Override'アノテーションを使用しなかった(トラップを事前に明かさないため)が、我々は、ミスを避けるため、'@Override'を使うべきだ。

本文 END

<このシリーズの、前の記事 | このシリーズの目次 | このシリーズの、次の記事>