| VM | JDK と JRE |
| Language | オブジェクトとインスタンス this と super static フィールドアクセス |
| IO | Serializable の実装 |
| Swing | MetalLookAndFeel イメージパネル |
| JavaBeans | プロパティ名について XMLEncoder で保存 |
| その他 | 正規表現テストアプリ 秀丸の強調表示 Ant のインストール 文字セット変換 Ant タスク XAMPP + Tomcat |
|
キーワード this と super について |
Java のキーワードの this, super に関する内容です。
インスタンスメソッドはオブジェクトの振る舞いとしてそれぞれのオブジェクトに属するものなので、インスタンスメソッドの実行時には必ずそのメソッドを実行しているオブジェクトが存在します。 インスタンスメソッド内の this はこのメソッドを実行しているオブジェクトの参照を表します。 実行時になるまで分からない実行オブジェクトの参照をソースコードにおいて this で表し、実行時には this を実行オブジェクトの参照に置き換えたり、単純名でのインスタンス変数やインスタンスメソッドへのアクセスを実行オブジェクトに対して行うことで、それぞれのオブジェクトの振る舞いの違いを1つのメソッド定義で実現しています。 このような動作を実現するためにインスタンスメソッドの実行時には JVM 内部では引数と共に実行オブジェクトの参照も渡されています。 (単純名でのインスタンス変数やインスタンスメソッドへのアクセスはインナークラスの場合には外側のクラスのインスタンスに対するアクセスとなる場合がありますが、それに関しては後述しています)
下はインスタンスメソッド内の this が実行オブジェクトの参照に置き換えられる事を確認するプログラムです。
public class Test {
public Test getReference() {
return this;
}
public static void main(String[] args) {
Test test1 = new Test();
Test test2 = new Test();
System.out.println(test1 == test2);
System.out.println(test1 == test1.getReference());
System.out.println(test2 == test2.getReference());
}
}
Test を実行すると
false true trueと標準出力に表示されます。
main メソッドでは下のように Test オブジェクトとその Test オブジェクトの getReference メソッド内の this の参照を比較しています。
test1 == test1.getReference()
Test#getReference の実装は以下です。
public Test getReference() {
return this;
}
前述のようにインスタンスメソッドの実行時に JVM 内部では引数の他に実行オブジェクトの参照も渡されているので、この場合には getReference の実行時にはローカル変数 test1
が保持する Test オブジェクトの参照が渡されています。
そして getReference メソッド内の this は実行時に渡された
Test オブジェクトの参照に置き換えられてリターンされます。
test1.getReference() の戻り値と test1 の参照比較は当然等しくなり true が表示されます。
test2.getReference()
この呼び出しでは test1 と同様に getReference メソッド内の this は test2
が保持する参照に置き換えられます。
次に単純名によるインスタンス変数やインスタンスメソッドへのアクセスと this を使って限定した場合のアクセスについて触れたいと思います。 オブジェクトは状態と振る舞いを持ち、状態はそのオブジェクトのクラスと Object クラスまでたどったすべての上位クラスに宣言されているインスタンス変数 (パッケージアクセスや private 指定されているものも含む) の値であり、それぞれのオブジェクトごとにそれらの変数の値を格納する為の領域を保持しています。 振る舞いの方はインスタンスメソッドを実行した際の動作を指します。 よってインスタンス変数はいずれかのオブジェクトに属しており、 インスタンスメソッドもいずれかのオブジェクトの振る舞いとして実行されます。 そのためにインスタンス変数やインスタンスメソッドにアクセスする為にはアクセス対象のオブジェクトを指定する必要があります。 インスタンスメソッドの場合にはこのアクセス対象のオブジェクトがメソッドの実行オブジェクトになります。 これらのアクセス対象となるオブジェクトの指定は参照型の変数、あるいはオブジェクトを表す式とそれに続くドットで行います。
String foo = "foo";
String sub = foo.substring(1);
sub = new String("bar").substring(1);
int pointX = new java.awt.Point(10, 20).x;
インスタンスメソッド内において、属するオブジェクトを指定しない単純名によるインスタンス変数やインスタンスメソッドへのアクセスはトップクラスのインスタンスの場合にはそのメソッドの実行オブジェクトに対するアクセスになります。
よってトップクラスのインスタンスのインスタンスメソッド内の単純名によるインスタンス変数やインスタンスメソッドへのアクセスの前には明示的に
"this."
を付ける事ができます。
(例外として、定数式の定義に絡んで switch 文の case
に
コンスタント変数
を指定する場合に this を付ける事はできません)
インナークラスのインスタンスの場合には外側のクラスのインスタンスの変数やメソッドに単純名でアクセスできます。 この為インナークラスのインスタンスのインスタンスメソッド内における単純名によるインスタンス変数やインスタンスメソッドへのアクセスは実行オブジェクトに対するアクセスとは限りません。 この場合にアクセス対象となるオブジェクトの決定はコンパイル時に行われます。
Test#getReference はインスタンスメソッドの戻り値として this を使っている例でしたが、下の Square クラスはそれ以外の this の使用例です。
public class Square {
private int width;
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getArea() {
int width = getWidth();
return width * width;
}
}
setWidth メソッドで this が使用されていますがこれは引数やローカル変数にインスタンス変数と同じ名前を使用するとそれらがインスタンス変数より優先される為です。
つまり setWidth メソッド内で単に width と記述すると、すべて引数の width
と解釈されてしまう為、
this を使うことでメソッドを実行しているオブジェクトのインスタンス変数の方であることを明示しています。
getArea メソッドではローカル変数 width が宣言されているのでこのメソッド内の width はすべてインスタンス変数ではなく、ローカル変数の方の width になります。
Square クラスはトップクラスなのでインスタンスメソッド内における単純名による変数やメソッドへのアクセスはすべて実行オブジェクトに対して行われます。 よってこれらの単純名の前には "this." を付けられます。 Square クラスで可能な限り this を明示すると以下のようになります。
public class Square {
private int width;
public int getWidth() {
return this.width;
}
public void setWidth(int width) {
this.width = width;
}
public int getArea() {
int width = this.getWidth();
return width * width;
}
}
this は下のように synchronized ブロックでモニタとして使用される事もあります。
synchronized (this) {
...
}
この場合の this もインスタンスメソッド内の this と同じくブロック内の処理を実行するオブジェクト
(synchronized ブロックを含んでいるインスタンスメソッドやコンストラクタやインスタンス初期化子の実行オブジェクト)
の参照を表します。
キーワード this はインスタンスメソッド内の他、コンストラクタ内、インスタンス変数初期化子 (宣言時の代入) やインスタンス初期化子 (インスタンス初期化ブロック) でも使用できます。 コンストラクタ内の this は2通りの使われ方があります。 1つはオーバーロードされている別のコンストラクタの呼び出しです。 引数の無いコンストラクタの呼び出しは以下です。
this();
コンストラクタが this という名前のインスタンスメソッドであるかのように呼び出します。
この別のコンストラクタの呼び出しは最初に一度だけ行えます。
コンストラクタの最初の部分以外で別のコンストラクタを呼び出している場合にはコンパイルエラーとなります。
もう1つのコンストラクタ内の this はインスタンスメソッドと同様にそのコンストラクタを実行しているオブジェクトの参照を表します。 インスタンス変数の宣言時の初期化式やインスタンス初期化子の実行はコンストラクタと連動して行われており、それらにおける this もコンストラクタを実行しているオブジェクトの参照を表します。
下はこれらに関するプログラムです。
public class Sub extends Super {
private int value = this.show(1);
{
this.value = this.show(2);
this.value2 = this.show(3);
}
private int value2 = this.show(4);
public Sub() {
this(5);
System.out.println("Sub#constructor()");
}
public Sub(int value) {
System.out.println("Sub#constructor(int)");
this.value = this.show(value);
}
private int show(int value) {
System.out.println("called show : " + value);
return value;
}
}
public class Super {
public Super() {
System.out.println("Super#constructor()");
}
}
public class Main {
public static void main(String[] args) {
new Sub();
}
}
Sub クラスでは this を可能な限り明示しています。
Main の実行結果は以下のように標準出力に表示されます。
Super#constructor() called show : 1 called show : 2 called show : 3 called show : 4 Sub#constructor(int) called show : 5 Sub#constructor()順を追ってみていきます。
public static void main(String[] args) {
new Sub();
}
main メソッドでは Sub のインスタンスを生成しているだけです。
Sub
クラスのロード後にインスタンスが1つ生成され、引数の無い方のコンストラクタが実行されます。
コンストラクタを実行しようとしているオブジェクトはその時点で Object クラスまでたどったすべてのスーパークラスのインスタンス変数の値を格納する領域やメソッドにアクセスする為の情報などをメモリ上に保持しています。 つまりオブジェクトとして完成するのはコンストラクタ実行後ですが、コンストラクタを実行する時には実行オブジェクトの参照は存在します。 生成直後のオブジェクトのインスタンス変数は宣言時に初期化するしないに関わらず、 boolean 型は false に、char 型は '' (空文字 '\u0000') に、それ以外のプリミティブ型は 0 に、参照型は null に初期化されており、その状態でコンストラクタが実行される事になります。 またインスタンスメソッドの呼び出しと同様に JVM 内部ではコンストラクタの実行の際にも引数の他にコンストラクタを実行するオブジェクトの参照が渡されコンストラクタ内の this はその実行オブジェクトの参照に置き換えられて処理されます。
public Sub() {
this(5);
System.out.println("Sub#constructor()");
}
1行目の this はオーバーロードされている方のコンストラクタの呼び出しなので参照の置き換えなどは行われず、引数の 5 と共に実行オブジェクトの参照を渡してもう一方のコンストラクタを実行します。
public Sub(int value) {
System.out.println("Sub#constructor(int)");
this.value = show(value);
}
こちらのコンストラクタでは同じクラスの別のコンストラクタの呼び出しも明示的なスーパークラスのコンストラクタの呼び出しも行われていません。
この場合には最初にスーパークラスのデフォルトコンストラクタ (引数のないコンストラクタ) が実行されます。
一般に、あるクラスのコンストラクタを実行する際にはそれが Object クラスでない限りスーパークラスのコンストラクタが先に実行されます。ソースコードにおいてコンストラクタに明示的なスーパークラスのコンストラクタの呼び出しが記述されていなければスーパークラスのデフォルトコンストラクタが実行されます。
もしもスーパークラスにデフォルトコンストラクタが無い場合 (引数を取るコンストラクタしか宣言していない場合) にコンストラクタにおけるスーパークラスのコンストラクタの呼び出しを省略した場合にはコンパイル時にエラーとなります。
Super クラスのデフォルトコンストラクタは次のようになっています。
public Super() {
System.out.println("Super#constructor()");
}
この実行により標準出力に
Super#constructor()が表示されます。
Super クラスのコンストラクタ実行後には Sub クラスのコンストラクタに実装されている処理の前にインスタンス変数の宣言時の初期化式やインスタンス初期化子が実行されます。 これらの実行は上から宣言されている順に行われます。
private int value = this.show(1);
{
this.value = this.show(2);
this.value2 = this.show(3);
}
private int value2 = this.show(4);
最初に value に this.show(1) の戻り値が代入され、その際に標準出力に
called show : 1が表示されます。この this はコンストラクタを実行しているオブジェクトの参照であり、インスタンスメソッド内の this と同じ使われ方です。
次にインスタンス初期化子 (インスタンス初期化ブロック) があるのでこのブロック内が実行されます。 インスタンス初期化子は普通はコンストラクタで事が足りることもあり、あまり使われることはないと思います。匿名クラスを作成する場合にはコンストラクタを宣言できないのでインスタンス初期化子が必要になる場合があります。またインスタンス初期化子内では代入における左辺としてならインスタンス初期化子より後方で宣言されたインスタンス変数が使用できます。 このインスタンス初期化子の実行により標準出力に
called show : 2 called show : 3が表示されます。このインスタンス初期化子内で使用される this もコンストラクタを実行しているオブジェクトの参照に置き換えられます。
続いて value2 に this.show(4) の戻り値が代入され、その際に標準出力に
called show : 4が表示されます。
スーパークラスのコンストラクタの実行とインスタンス変数の宣言時の初期化やインスタンス初期化子の実行が済んでからようやく Sub のコンストラクタに実装された処理が行われます。
System.out.println("Sub#constructor(int)");
this.value = show(value);
まず標準出力に
Sub#constructor(int)が表示され、次に show(5) の呼び出しにより
called show : 5が表示されます。 そして制御は最初に呼び出されたデフォルトコンストラクタに戻り
Sub#constructor()が表示されます。
以上 this というよりもインスタンスの初期化に関する内容になってしまいましたが
this に関してはオーバーロードされている別のコンストラクタを呼び出す際に使う場合以外はインスタンスメソッド内の this と同じ使われ方なので特に説明する部分はありませんでした。
重要なのはインスタンス変数の宣言時の初期化式やインスタンス初期化子がコンストラクタに先だって行われるのではなく、コンストラクタ内でスーパークラスのコンストラクタ実行に続いて処理され、その際に this はコンストラクタを実行しているオブジェクトの参照に置き換えられる事です。
Sub クラスのコンストラクタまでの部分のソースコードは以下のように実装しても、まったく同じ動作になります。
private int value;
private int value2;
public Sub() {
this(5);
System.out.println("Sub#constructor()");
}
public Sub(int value) {
this.value = this.show(1);
this.value = this.show(2);
this.value2 = this.show(3);
this.value2 = this.show(4);
System.out.println("Sub#constructor(int)");
this.value = this.show(value);
}
ここまでコンストラクタ内からオーバーロードされている別のコンストラクタを呼び出す際の this とインスタンスメソッドやコンストラクタを実行しているオブジェクトの参照を表す this を紹介しました。 このインスタンスメソッドやコンストラクタを実行しているオブジェクトの参照を表す方の this はクラスを明示する事ができます。
public class Foo {
private int value;
public Foo(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
明示可能な this をすべて明示すると
public class Foo {
private int value;
public Foo(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
}
this のクラスを明示すると
public class Foo {
private int value;
public Foo(int value) {
Foo.this.value = value;
}
public int getValue() {
return Foo.this.value;
}
}
となります。インナークラス (内部クラス) に関しては Java言語規定 第3版 8.1.3 (日本語訳) において以下のように定義されています。
このページで使っているインナークラスとはこの内部クラスのことで、ネストされているクラスで static 宣言しているものやインターフェイスにネストされているクラス、また static ブロックや static メソッド、クラス変数初期化子におけるローカルクラスや匿名クラスは含みません。8.1.3 Inner Classes and Enclosing Instances
An inner class is a nested class that is not explicitly or implicitly declared static.
内部クラス(inner class)は,明示的又は暗黙にstaticと宣言されない入れ子クラスとする。
最初にインナークラスのインスタンスを生成するコードを下の Outer と Outer.Inner クラスを使って紹介します。
public class Outer {
private int value;
public Outer(int value) {
this.value = value;
}
class Inner {
int getValue() {
return value;
}
}
}
Outer.Inner クラスの getValue メソッドでは外側の Outer クラスのインスタンス変数
value にアクセスしています。
インスタンス変数はオブジェクトに属しているので実行時に
Outer クラスのインスタンス変数にアクセスするためにはその変数を保持する
Outer オブジェクトの参照が必要になります。
このようなアクセスを実現する為にインナークラスのインスタンスは内部的に外側のクラスのインスタンスの参照を保持しています。
ここではこのインナークラスのインスタンスが内部的に保持している外側のクラスのインスタンスをエンクロージングオブジェクト (enclose → 取り囲む) と呼びます。
エンクロージングオブジェクトはインナークラスのインスタンスを生成する際に指定しなければなりません。Outer outer = new Outer(100); Outer.Inner inner = outer.new Inner();しかしインナークラスのインスタンスが外側のクラス以外の場所で生成されるような設計が必要になることはあまりないので上記のようなコードはめったに見ることはないと思います。 外側のクラスである Outer 内で Outer.Inner オブジェクトを生成する場合には
public class Outer {
private int value;
private Inner inner = this.new Inner();
public Outer(int value) {
this.value = value;
}
Inner getInner() {
return this.new Inner();
}
class Inner {
int getValue() {
return value;
}
}
}
となります。フィールド inner の初期化子の this は Outer
クラスのコンストラクタを実行する Outer
オブジェクトに置き換えられ、 getInner メソッド内の this はメソッドを実行している
Outer オブジェクトに置き換えられます。
これらの this は通常省略されています。
Outer.Inner クラスにおいてエンクロージングオブジェクトである
Outer オブジェクトの参照を表す場合には
Outer.this と記述します。
よって Inner#getValue メソッド内の value
は属するオブジェクトを明示すると以下のようになります。
int getValue() {
return Outer.this.value;
}
以下のようにした場合にはコンパイルエラーとなります。
int getValue() {
return this.value;
}
Outer.Inner クラスにおいて単に
this
とすると
Inner.this
と解釈され Outer.Inner クラスには value というフィールドは宣言されておらず、また継承もしていないのでコンパイルエラーとなります。
単純名によるフィールドアクセスではコンパイラが外側のクラスも探してアクセス対象を決定します。
下は Outer.this を明示しなければならない例です。
public class Outer {
private int value;
Inner inner = new Inner();
public void setValue(int value) {
this.value = value;
}
class Inner {
private int value;
Inner() {
value = Outer.this.value;
}
int getValue() {
return value;
}
int getOuterValue() {
return Outer.this.value;
}
Outer getOuter() {
return Outer.this;
}
}
}
冗長ですが this とそのクラスをすべて明示すると以下のようになります。
public class Outer {
private int value;
Inner inner = Outer.this.new Inner();
public void setValue(int value) {
Outer.this.value = value;
}
class Inner {
private int value;
Inner() {
Inner.this.value = Outer.this.value;
}
int getValue() {
return Inner.this.value;
}
int getOuterValue() {
return Outer.this.value;
}
Outer getOuter() {
return Outer.this;
}
}
}
インナークラスがいくつ入れ子になっていてもエンクロージングオブジェクトの参照の表現は同じです。
public class Top {
class Middle {
class Bottom {
Top getTop() {
return Top.this;
}
Middle getMiddle() {
return Middle.this;
}
Bottom getBottom() {
return this;
}
}
}
}
インナークラス以外、つまりトップクラス、ネストされたクラスで static 宣言しているクラス、インターフェイスにネストされたクラス、 static ブロックまたは static メソッド内のローカルクラスや匿名クラス、クラス変数初期化子における匿名クラスのインスタンスはエンクロージングオブジェクトを持ちません。 インスタンスメソッド、コンストラクタ、インスタンス初期化子内のローカルクラスや匿名クラス、あるいはインスタンス変数初期化子における匿名クラスのインスタンスはインスタンスメソッドやコンストラクタを実行しているオブジェクトをエンクロージングオブジェクトとして内部的に保持します。 ローカルクラスや匿名クラスにおけるエンクロージングオブジェクトの参照の表現もクラスにネストされたインナークラスと同じです。
スーパークラスとサブクラスに同名の変数が存在する場合に this をキャストする必要が生じる場合があります。
オブジェクトはそのオブジェクトのクラスと Object クラスまでたどったすべてのスーパークラスのインスタンス変数の値を保持していますが、スーパークラスとサブクラスに同じ名前のフィールドが存在することや、あるクラスのフィールド名と実装しているインターフェイスに宣言されているフィールド名との重複などは許されています。 スーパークラスの private フィールドなどは通常ドキュメントに記述はありませんし、ソースコードが無ければその存在が分かりませんのでフィールド名の重複が起きてしまうのは仕方のない事と言えると思います。 ただし同じクラス内での重複した名前のフィールドの宣言は型や static 、非 static に関わらずコンパイルエラーとなります。
スーパークラスとサブクラスで同名のフィールドの存在を許可している以上それらを区別する手段が用意されているはずです。 オブジェクトを通したフィールドアクセスではコンパイル時のオブジェクトの型によって アクセスするフィールドが変わってきます。 あるクラスとそのスーパークラスで重複したフィールド名が存在する場合にはアクセスしたい方のフィールドを宣言したクラスをコンパイル時の型にすることでフィールドを限定できます。 以下はコンパイル時のオブジェクトの型とアクセスするフィールドの決定に関するプログラムです。
public class First {
protected int value = 1;
}
public class Second extends First {
protected int value = 2;
}
public class Third extends Second {
protected int value = 3;
}
public class Main {
static First toFirst(Second second) {
System.out.println(second.value);
return second;
}
public static void main(String[] args) {
Third third = new Third();
Second second = third;
First first = third;
System.out.println(third.value + " " +
second.value + " " +
first.value);
System.out.println(third.value + " " +
((Second)third).value + " " +
((First)third).value);
System.out.println(toFirst(third).value);
}
}
実行結果は以下です。
3 2 1 3 2 1 2 1
First, Second, Third は継承関係にあり、それぞれに value というフィールドが宣言されています。 よって Third クラスのインスタンスは First で宣言した value と Second で宣言した value と Third で宣言した value の 3 つの value というフィールドを保持します。 main メソッドでは Third オブジェクトのコンパイル時の型を変更することでこれら3つのフィールドの値を取得しています。 オブジェクトのコンパイル時の型とはソースコードに記述されている静的な型で変数や仮引数、戻り値の型、あるいはキャスト演算子でキャストされた型などです。
順に main のコードを追っていきます。
Third third = new Third();
まずThird クラスのインスタンスを生成しています。
Third 型のローカル変数 third が保持する Third
オブジェクトは状態として以下のように3つの value の値を保持しています。
( ) 内は value を宣言したクラスです。
Second second = third;
First first = third;
そしてそれらの変数を通してフィールドの値を取得して標準出力に出力します。
System.out.println(third.value + " " +
second.value + " " +
first.value);
変数を通したアクセスでは変数の型がコンパイル時の型になるので third
のコンパイル時の型は Third となり、 second は Second で first は
First となり、それらのクラスに宣言されている value
の値が取得されるので標準出力に以下が表示されます。
3 2 1次にキャストによる型の変換ですが
System.out.println(third.value + " " +
((Second)third).value + " " +
((First)third).value);
Third 型の変数 third を Second や First
にキャストしていますが、変数に限らずオブジェクトの参照を表す式をキャストしている場合はコンパイラはキャスト後の型をコンパイル時の型とします。
よって (Second)third のコンパイル時の型は Second 型、
(First)third は First 型になるので標準出力に以下が表示されます。
3 2 1最後はメソッドの戻り値や仮引数の場合です。
System.out.println(toFirst(third).value);
メソッドの戻り値や仮引数も宣言されている型がコンパイル時の型となります。
toFirst メソッドは
static First toFirst(Second second) {
System.out.println(second.value);
return second;
}
となっており、まず仮引数の Second 型の second はコンパイル時の型も Second
ですから third が参照している Third オブジェクトの持つ Second
クラスで宣言された value の値が標準出力に出力されます。
2そして戻り値の型は First なので toFirst(third) の結果の型は First となるので標準出力に以下が表示されます。
1
this もキャストによってコンパイル時の型を変更できます。
下は this をキャストすることによって、隠蔽しているスーパークラスのフィールドにアクセスしている例です。
public class First {
protected int value = 1;
}
public class Second extends First {
protected int value = 2;
}
public class Third extends Second {
protected int value = 3;
public void show() {
System.out.println(value + " " +
((Second)this).value + " " +
((First)this).value);
}
public void setValue(int value) {
this.value = value;
}
public void setSecondValue(int value) {
((Second)this).value = value;
}
public void setFirstValue(int value) {
((First)this).value = value;
}
}
public class Main {
public static void main(String[] args) {
Third third = new Third();
third.show();
third.setValue(30);
third.setSecondValue(20);
third.setFirstValue(10);
third.show();
}
}
実行結果は以下です。
3 2 1 30 20 10以上フィールドアクセスにおける this のキャストに関する内容でしたが、クラスを作成する際にアクセス可能な上位クラスのフィールドと同じ名前のフィールドをわざわざ宣言する必要があるケースを思いつきませんでした。 反対にクラス作成者以外の人がそのソースコードを読んだ場合に混乱の原因となり、あまり良いことは無いと思われます。
フィールドアクセスと違い、インスタンスメソッドにアクセスする場合に this
をキャストするのは、ほとんどの場合に無意味です。
ポリモーフィズムによりインスタンスメソッドがオーバーライドされている場合にはその上書きしている方のメソッドが実行される為、ほとんどの場合に this をキャストしてコンパイル時の型を変更しても実行されるインスタンスメソッドは同じだからです
(static メソッドの場合にはコンパイル時の型で実行するメソッドが変わってきますが
static メソッドにはクラス名を使ってアクセスするべきです)。
"ほとんど" というのは例外があるからです。
下のプログラムはその例外のサンプルです。
public class Foo {
private String getName() {
return "Foo";
}
static class Bar extends Foo {
public String getName() {
return "Bar";
}
public void show() {
System.out.println(this.getName() + '\n' +
((Foo)this).getName());
}
}
public static void main(String[] args) {
Bar bar = new Bar();
bar.show();
}
}
実行結果は以下です。
Bar FooBar インスタンスにおける this.getName() と ((Foo)this).getName() では実行されるメソッドが違っています。 まず Bar#getName は private 指定されている Foo#getName をオーバーライドしていません。 private メソッドは暗黙のうちに final なのでオーバーライドできません。 上位クラスの private メソッドと同じシグニチャで戻り値の型も同じメソッドを定義しても オーバーライドにはなりませんし、 private メソッドの呼び出しではポリモーフィズムによるオーバーライドメソッドの動的検索も行われません。 よってコンパイル時の型が Foo である ((Foo)this) の getName を呼び出した場合には Foo#getName メソッドが実行されるのは当然です。 問題はネストされたクラスの特性により、通常アクセスできないはずのスーパークラスの private メソッドにアクセスできる事にあります。 Bar が Foo にネストされたクラスでなければ private な Foo#getName にはアクセスできないので Bar において ((Foo)this).getName() というコードがあればコンパイルエラーになります。 しかし Bar は Foo にネストして宣言されているので private な Foo#getName メソッドにアクセス可能となり、それが this のキャストにより実行されるメソッドが違ってくるという妙な現象を起こしています。
上の例以外にも同じクラスにネストされた2つのクラスが継承関係にある場合など、 あるトップクラスの宣言内ではどの private メンバにもアクセスできるので同じような現象が起きます。 (フィールドに対するアクセスの例がフィールドアクセスのページにあります)
キーワード super は Object クラスでは使えませんが、それ以外は this と同じ場所で使用可能で、インスタンスメソッドやコンストラクタ、インスタンス初期化子 (初期化ブロック) の中やインスタンス変数初期化子 (インスタンス変数宣言時の右辺の初期化式) の中です。
コンストラクタ内の super() または super(arg1 ...) はスーパークラスのコンストラクタの呼び出しを行います。 スーパークラスのコンストラクタの呼び出しはコンストラクタの最初にしか記述できません。 それ以外の場所ではコンパイルエラーになります。 コンストラクタ内の this でも触れましたが Object 以外のクラスではコンストラクタの一番最初の部分ではそのクラスのオーバーロードされているコンストラクタかスーパークラスのコンストラクタの呼び出しを記述する必要があります。 それらが記述されていないコンストラクタを持つクラスのソースはコンパイルの際にデフォルトコンストラクタの呼び出し
super();を行うバイトコードがコンパイラによってコンストラクタの最初の部分に挿入されたクラス (ファイル) が生成されます。
フィールドにアクセスする場合の super は this をスーパークラスの型にキャストするのと等価です。
public class A {
protected String name = "A";
}
public class B extends A {
protected String name = "B";
public void showSuper() {
System.out.println(((A)this).name + ' ' + super.name);
}
public static void main(String[] args) {
new B().showSuper();
}
}
実行結果は
A Aとなり、フィールドにアクセスする場合に限っては ((A)this) と super はまったく同じで、どちらもフィールドにアクセスするコンパイル時の型を A にします。
ネストされたクラスにおけるエンクロージングオブジェクトに対する super の使い方は以下です。
public class A {
protected String name = "A";
}
public class B extends A {
protected String name = "B";
class C {
String name = "C";
public void showName() {
System.out.println(name + ' ' + B.this.name + ' ' + B.super.name);
}
}
public static void main(String[] args) {
new B().new C().showName();
}
}
実行結果は
C B Aとなります。
super を使ったインスタンスメソッドへのアクセスはフィールドへのアクセスとは違い this をスーパークラスの型にキャストしてインスタンスメソッドの呼び出しを行うのとはまったく異なる結果になります。
public class A {
protected String getName() {
return "A";
}
}
public class B extends A {
protected String getName() {
return "B";
}
public void showSuper() {
System.out.println(((A)this).getName());
System.out.println(super.getName());
}
}
public class C extends B {
protected String getName() {
return "C";
}
}
public class Main {
public static void main(String[] args) {
new C().showSuper();
}
}
Main の実行結果は標準出力に
C Aが表示されます。
System.out.println(((A)this).getName());
の部分ではポリモーフィズムにより C#getName が実行されます。
つまりインスタンスメソッドの呼び出しの際に this
をキャストしてコンパイル時の型を変更しても、コンパイラによって生成されるバイトコードはポリモーフィズムを実現するオーバーライドに対応した命令になるので最終的に実行されるメソッドはキャストの有無に関わらず同じメソッドとなります。 (これに関する例外は
this をキャストする に記述しています)
System.out.println(super.getName());
こちらの super を使ってインスタンスメソッドを呼び出す場合には、コンパイラによって生成されるバイトコードはコンパイル時の型でメソッドを検索して最初に見つかったメソッドを実行するタイプの命令 (invokespecial) となります。
上の例の場合には B 内で super.getName() というコードを記述しているので
B のスーパークラスの
A の getName を検索して実行する命令となります。
getName メソッドは A から最上位クラスの Object までのどこかにあるはずで、少なくともコンパイル時には存在する事がチェックされています。
検索は A から Object の方向に順に getName メソッドを探していき、最初に見つかった getName
メソッドが実行されます。
上の例では A に getName が宣言されているのでそれが実行されます。
注意が必要なのは super を使ったメソッドアクセスにおいては super が表す型はコンパイル時に決定し、 実行オブジェクトの "オブジェクトのクラス" (実行時の型) は関係ないということです。main は以下のようになっています。
new C().showSuper();
C オブジェクトの showSuper を呼び出しています。
showSuper メソッドは B クラスに以下のように宣言されています。
public void showSuper() {
System.out.println(((A)this).getName());
System.out.println(super.getName());
}
C オブジェクトを実行オブジェクトとするこのメソッド呼び出しでは
super が表す型は
C のスーパークラスの B に実行時に決定されるのではなく、ソースコードにおいて
super が記述されているクラス
B のスーパークラスの A にコンパイル時に決定されます。