Javaのthisとは?this/this()の意味や使い方
Javaでのthisは、使われる箇所によって以下の二つの意味があります。
- this:自分自身のインスタンスを指す変数
- this():コンストラクタ内から別のコンストラクタを呼び出す際のメソッド名
この記事ではそれぞれの説明を初級者向けに行いますので、ぜひしっかり理解していただければと思います。
なお、thisを理解するには、Javaにおける「インスタンス」の概念の理解が前提となります。
もしインスタンスの理解に自信がない方は、先に参考書やインターネット上のドキュメントで、基礎的な理解をしておいてください。
目次
1.変数のthis
1-1.thisは自分自身のインスタンスを指す変数
thisとは、自分自身のインスタンスを指す変数です。変数を使って、その変数が指すインスタンスのフィールドやメソッドを参照したり呼び出せるのと同じように、自分自身のインスタンスを明示的に指し示し、そのフィールドやメソッドを使いたい時に使う変数です。
// oという変数を経由して、変数が指すインスタンスのメソッドを呼び出す Object o = new Object(); o.toString(); // thisObjという変数を経由して、自分自身のインスタンスのメソッドを呼び出す Object thisObj = this; thisObj.toString(); // ↑とほぼ同じ意味。thisという変数を経由して、自分自身のインスタンスのメソッドを呼び出す this.toString();
thisの意味から当然なのですが、thisが指すモノはインスタンスごとに違います。例えば以下のプログラムでその違いの確認ができます(※)。
※Object.toString()の標準実装はインスタンスの保存先アドレスを表現したものなので、同じインスタンスかどうかの区別に使えます。
class ThisSample1 { void printThis() { System.out.println(this); // プログラム自体は同じだが… } public static void main(String args[]) { // インスタンスを2つ生成 ThisSample1 sample1 = new ThisSample1(); ThisSample1 sample2 = new ThisSample1(); // thisが指すものはインスタンスごとに違う sample1.printThis(); // → ThisSample1@70dea4e sample2.printThis(); // → ThisSample1@5c647e05 } }
インスタンスのコンテキスト(※)では、thisの有無にかかわらず現在のインスタンスのフィールド・メソッドを参照・実行できますので、明確な理由なくthisを付ける意味は薄いです。
※インスタンスメソッドやコンストラクタなど、インスタンスの持ち物としての文脈で実行されるコード。
class ThisSample2 { int field = 123; Object objField = new Object(); void instanceMethod() { System.out.println("instanceMethod"); } void thisTest() { System.out.println(field == this.field); // true、同じフィールドを指しているため System.out.println(objField == this.objField); // 同上 instanceMethod(); // "instanceMethod"がprintされる this.instanceMethod(); // 同上、同じメソッドを呼び出しているため } public static void main(String[] args) { new ThisSample2().thisTest(); } }
ですから、thisが付けられるからと、全てのフィールド参照・メソッド呼び出しにthisを付けると、プログラムが冗長になります。Java向けの開発環境やエディタではフィールド・メソッドなどへは色分けをしてくれるのでなおさらです。ただし、コーディングルールとして付けることになっている場合があります。
1-2.thisの使い方
1-2-1.使い方の例①:フィールドとローカル変数の名前を同じにしたい時
thisが最も使われるケースは、フィールドとローカル変数の名称を同じにしたい時です。Javaでの変数名はローカル変数が優先されるので、変数名が重複する場合、フィールドへはthisを付けて区別します。なお、これはsetterと呼ばれるパターンではほぼ必須です。
class ThisSample3 { int field = 123; void instanceMethod() { int field = 456; // フィールド名と同じローカル変数を宣言 System.out.println(field); // →456 System.out.println(this.field); // →123、フィールドを指定したい場合はthisを経由する } // 最もよく使われるパターン(setterパターン) void setField(int field) { this.field = field; } }
1-2-2.使い方の例②:自分自身のインスタンスをメソッドの戻り値としたい場合
thisは自分自身のインスタンスをメソッドの戻り値としたい時にも使います。これはJavaプログラミングでのイディオムの一つでもあり、いろいろな所で見かけます。
thisを戻すことでメソッドチェーンが使えるようになり、直接的にはプログラムの行数を減らせます。これは標準APIのStringBuilderを使う際のイメージが分かりやすいでしょう。
class ThisSample4 { ThisSample4 getThis() { return this; } public static void main(String[] args) { ThisSample4 sample = new ThisSample4(); sample.getThis().getThis().getThis().getThis(); // getThis()を延々と続けて書ける } } // StringBuilderでのメソッドチェーンの例 StringBuilder sb = new StringBuilder(); sb.append("A").append("B").append("C"); // StringBuilderのappendはthisを戻すので1行で書ける sb.append(1); // このように書いてもいいが、3行必要になってしまう sb.append(2); sb.append(3);
この考え方を推し進めると、以下のようなものとなります。各メソッドは単機能としながらも、何かのまとまった処理を連続して行うイメージです。このような記述をするAPIやフレームワークも多いのですが、興味がある方は「Fluent interface(流れるようなインターフェイス)」で検索すると更なる知識を得られるでしょう。
new SomeClass().method1().method2().method3();
1-3.thisを使えるところ、使えないところ
1-3-1.thisはインスタンスのスコープで使える
thisは、Java 11の時点では以下のところで使えます。
- ①メソッドの中(インスタンスメソッド、デフォルトメソッド(Java 8以降)、ラムダ式(Java 8以降))
- ②コンストラクタの中
- ③インスタンスイニシャライザの中
- ④フィールド初期化時の右辺値の中
- ⑤Receiver Parameterとして(Java 8以降)
thisを使えるところの例としては以下のとおりです。無名クラスや内部クラスでのthisは少し注意が必要です(詳細は後述します)。
import java.util.function.Supplier; class ThisSample5 implements ThisSample5Interface { int field = 123; final int finalField = this.field * 100; // ④フィールド初期化時の右辺値 // ③インスタンスイニシャライザ { System.out.println("Instance Initialzer: " + this); } // ②コンストラクタ ThisSample5() { System.out.println("Constructor(default): " + this); } // ①-1 インスタンスメソッド void instanceMethod() { System.out.println("Instance Method: " + this); // 無名クラス Supplier<Integer> s1 = new Supplier<Integer>() { int anonymousClassField = 100; public Integer get() { System.out.println("Anonymous Class: " + this); // 無名クラスのインスタンス System.out.println("Enclosing Instance: " + ThisSample5.this); // 無名クラスのエンクロージングインスタンス return this.anonymousClassField; } }; s1.get(); // 内部クラス Supplier<Integer> s2 = new ThisSampleInnerClass(); s2.get(); // ①-3 ラムダ式 Supplier<Integer> s3 = () -> { System.out.println("Lambda: " + this); return this.finalField; }; s3.get(); } // ⑤Receiver Paramter void instanceMethod2(ThisSample5 this) { System.out.println("Receiver Parameter: " + this); } // 内部クラス class ThisSampleInnerClass implements Supplier<Integer> { int innerClassField = 12345; public Integer get() { System.out.println("Inner Class: " + this); // 内部クラスのインスタンス System.out.println("Enclosing Instance: " + ThisSample5.this); // 内部クラスのエンクロージングインスタンス return this.innerClassField; } } public static void main(String[] args) { ThisSample5 sample = new ThisSample5(); System.out.println("Caller: " + sample.toString()); sample.instanceMethod(); sample.instanceMethod2(); sample.defaultMethod(); } } interface ThisSample5Interface { // ①-2 デフォルトメソッド default void defaultMethod() { System.out.println("Default Method: " + this); } }
実行結果サンプル:
Instance Initialzer: ThisSample5@4411d970 Constructor(default): ThisSample5@4411d970 Caller: ThisSample5@4411d970 Instance Method: ThisSample5@4411d970 Anonymous Class: ThisSample5$1@60f82f98 Enclosing Instance: ThisSample5@4411d970 Inner Class: ThisSample5$ThisSampleInnerClass@7f690630 Enclosing Instance: ThisSample5@4411d970 Lambda: ThisSample5@4411d970 Receiver Parameter: ThisSample5@4411d970 Default Method: ThisSample5@4411d970
1-3-2.thisはクラスのスコープでは使えない
thisを使えるのはインスタンスのコンテキスト内だけです。つまり、クラスのコンテキスト、言い換えればstaticなコンテキスト内(staticメソッド・staticイニシャライザなど)ではthisを使うとコンパイルエラーになります。staticなコンテキスト内では、紐付いているインスタンスがないからです。
class ThisSample6 { int instanceField = 1; static staticField = this.instanceField * 100; // コンパイルエラー!! // staticイニシャライザ static { System.out.println(this); // コンパイルエラー!! } // staticメソッド static void staticMethod(ThisSample6 this) { // コンパイルエラー!! System.out.println(this); // コンパイルエラー!! } // staticな内部クラス static class InnerClass { void instanceMethod() { System.out.println(ThisSample6.this); // コンパイルエラー!! } } }
2.メソッドのthis()
2-1.this()は自分自身のコンストラクタを呼ぶ時に使う
Javaでプログラムを書いていると、あるコンストラクタから、オーバーロードした別のコンストラクタを明示的に呼びたい時があります。その時にthis()を使います。
コンストラクタの中から呼べるコンストラクタのメソッド名は“this”で固定なので、オーバーロードしたコンストラクタの内のどれを呼び出すかは、コンストラクタの引数で区別します。
以下の例で言うと、③→②→①の順番にコンストラクタが呼ばれ、①→②→③の順番で実行されていきます。
class ThisSample7 { String name; int amount; // ①デフォルトコンストラクタ ThisSample7() { this.name = "名無しの権兵衛"; this.amount = 0; } // ②オーバーロードしたコンストラクタ(その1) ThisSample7(String name) { this(); // ①のコンストラクタを呼び出す this.name = name; } // ③オーバーロードしたコンストラクタ(その2) ThisSample7(String name, int amount) { this(name); // ②のコンストラクタを呼び出す this.amount = amount; } public static void main(String[] args) { new ThisSample7("山田太郎", 1000); } }
2-2.this()の使い方/コンストラクタの使い分け
this()の良くある使い道は、複数の引数を持つ共通的なコンストラクタを用意しておいて、呼び出すコンストラクタに応じて設定値を変えたりデフォルト値を設定する、というものです。
それによりインスタンスの初期化処理を一つのコンストラクタにまとめられるので、プログラムが分かりやすくなりますし、保守性も高まります。
この例だと、フィールドの実質的な初期化はコンストラクタ③で行っています。①と②はデフォルト値を引数に、③を呼び出しているだけです。これによりフィールドの初期化処理の記述を③だけに出来ています。
class ThisSample8 { String name; int amount; // ①デフォルトコンストラクタ ThisSample8() { this("名無しの権兵衛", 10); // デフォルト値で③を呼び出す } // ②オーバーロードしたコンストラクタ(その1) ThisSample8(String name) { this(name, 20); // 引数を使いつつ、③を呼び出す } // ③オーバーロードしたコンストラクタ(その2) ThisSample8(String name, int amount) { // フィールドの初期化はこのコンストラクタだけで行う this.name = name; this.amount = amount; } void printProperty() { System.out.println(String.format("私の名前は%sです。所持金は%d円です。", name, amount)); } public static void main(String[] args) { ThisSample8 sample1 = new ThisSample8(); sample1.printProperty(); // → 名無しの権兵衛、10円 ThisSample8 sample2 = new ThisSample8("山田太郎"); sample2.printProperty(); // → 山田太郎、20円 ThisSample8 sample3 = new ThisSample8("鈴木次郎", 1000); sample3.printProperty(); // → 鈴木次郎、1000円 } }
3.thisに関する少し進んだ知識
3-1.【中級者向け】内部クラスのエンクロージングインスタンスを指すthis
内部クラスのインスタンス内から、そのインスタンスが紐づいているクラスのインスタンス(エンクロージングインスタンス)の参照を得たい場合は、少々特殊なthisの指定の仕方をします。
class ThisSample9 { int field = 123; class InnerClass { int field = 456; void innerClassMethod() { System.out.println(field); // → 456 System.out.println(this.field); // → 456、ここでのthisはInnerClassのインスタンス System.out.println(ThisSample9.this.field); // →123、クラスを明示的に指定してthisを得る } } public static void main(String[] args) { (new ThisSample9().new InnerClass()).innerClassMethod(); } }
内部クラスの中から「クラス名.this」とすると、そのクラスのインスタンスを得られます。ややこしいのですが、イメージとしては以下のようにインスタンスが親子状に関係付けられていると考えてもらえればいいと思います。
- ThisSample9のインスタンス
- ┗InnerClassのインスタンス
3-2.【中級者向け】synchronizedブロックでのthis
排他制御のため、以下の書き方をしているソースコードを多く見かけました。これは排他制御を行う際の一つのイディオムです。
synchronized(this) { // 排他実行したい処理 }
これは、自分自身のインスタンスを排他制御のモニターとして使用しています。ですので、このsynchronizedブロックは、1インスタンスごとに必ず1つのスレッドからしか実行されないことが保証されます。
なお、今では排他制御にはjava.util.concurrentにある各種Lockクラスを使うことが多いので、synchronizedブロックを使うケースは減りました。ですが、昔の書籍やソースコードを読むこともあるでしょうから、意味はしっかり理解しておきたいものです。
3-3.【上級者向け】スーパークラスでのthis
継承関係のあるクラスでthisを使う時は注意が必要です。thisが実際に指しているものが何かを、メソッドのオーバーライドの観点から意識する必要があるからです。
以下の例は、Javaでのポリモーフィズムの特徴とthisをしっかり理解していないと、直感に反する動きをしていると思われるかもしれません。どういう値が表示されるか予想してみてください。
class ThisSample10Parent { // フィールド① int instanceField = 123; // メソッド① void methodOverride() { System.out.println("Parent.methodOverride"); System.out.println("this is " + this); System.out.println("this.instanceField is " + this.instanceField); } void methodOfSuperClass() { System.out.println("Parent.methodOfSuperClass"); System.out.println("this is " + this); // ここでのthisは何か? System.out.println("this.instanceField is " + this.instanceField); // フィールド①と②のどちらか? this.methodOverride(); // メソッド①と②のどちらが呼ばれるか? // this.methodOfSubclass(); // メソッド③はここから呼び出せるか? } } class ThisSample10Child extends ThisSample10Parent { // フィールド② int instanceField = 456; // メソッド② void methodOverride() { System.out.println("Child.methodOverride"); System.out.println("this is " + this); System.out.println("this.instanceField is " + this.instanceField); } // メソッド③ void methodOfSubclass() { System.out.println("Child.methodOfSubclass"); } public static void main(String[] args) { ThisSample10Child sample = new ThisSample10Child(); sample.methodOfSuperClass(); } }
ThisSample10Childの実行結果例
- Parent.methodOfSuperClass ←スーパークラスのメソッドが呼ばれた
- this is ThisSample10Child@4411d970 ←thisはサブクラスのインスタンスを指す
- this.instanceField is 123 ←this.instanceFieldはスーパークラスのフィールドを指す
- Child.methodOverride ←サブクラスのオーバーライド済みメソッド②が実行された
- this is ThisSample10Child@4411d970 ←ここでのthisは、当然サブクラスのインスタンスを指す
- this.instanceField is 456 ←this.instanceFieldはサブクラスのフィールドを指す
サブクラスから呼び出された場合、スーパークラスでのthisは、サブクラスのインスタンスを指します。ですので、メソッドをオーバーライドした場合に呼ばれるメソッドの実体は、サブクラスのものです(this経由でなくてもそうです)。
そして、スーパークラスを直接newした場合のthisは、当然ながらスーパークラスのインスタンスを指します。
これこそがポリモーフィズムの作用です。thisが指すインスタンスの実体はコンテキストに応じて変わりますが、それをJava仮想マシンが把握しており、インスタンスに紐づけられたメソッドを適切に呼び分けているのです。
注意すべきはスーパークラス内のthis.instanceFieldが指すフィールドは、サブクラスから呼び出された場合でもスーパークラスのものだということです。Javaではフィールドはポリモーフィズムの対象とはならないからです。
4.まとめ
thisは自分自身のインスタンスを指す変数で、this()は自分自身のクラスのコンストラクタを呼び出す時に使います。
この記事ではそれぞれの良くある使い方、注意すべきことを紹介しましたが、いずれも効果的に使えば読みやすいプログラムを作れるものです。
そして、thisの使い方・考え方を突き詰めていくと、Javaでのオブジェクト指向の考え方がうっすらと見えてきます。関連するsuper/super()も含めてしっかりと理解して、一つ上のレベルのJavaプログラマーを目指しましょう!!
コメント