はじめに
現在、私が進行しているJavaの勉強会でまとめた内容です。
内容に不正確な点があれば、ご指摘いただけるとありがたいです。
- 韓国人として、日本語とコンピュータの勉強を同時に行うために、ここに文章を書いています
- 翻訳ツールの助けを借りて書いた文章なので、誤りがあるかもしれません
勉強する内容
- クラスを定義する方法
- オブジェクトを作成する方法(
new
キーワードの理解) - メソッドを定義する方法
- コンストラクタを定義する方法
-
this
キーワードの理解
クラスとは何でしょうか?
- 現実世界の「オブジェクト」をコードの世界で表現するための設計図です。
- オブジェクトが持つ属性(フィールド)や機能(メソッド)を定義します
オブジェクト(インスタンス)とは何でしょうか?
- クラスを基に生成された、メモリ上の実体を指します
アクセス修飾子について
- クラス(インスタンス)やその属性(フィールド)、機能(メソッド)を理解する前に、以下のようなアクセス修飾子について学ぶ必要があります
アクセス修飾子 | クラス内 | 同じパッケージ内 | サブクラス | 外部クラス |
---|---|---|---|---|
public |
○ | ○ | ○ | ○ |
protected |
○ | ○ | ○ | × |
default |
○ | ○ | × | × |
private |
○ | × | × | × |
使用箇所
対象 | public |
protected |
default |
private |
---|---|---|---|---|
クラス | ○ | × | ○ | × |
フィールド | ○ | ○ | ○ | ○ |
メソッド | ○ | ○ | ○ | ○ |
コンストラクタ | ○ | ○ | ○ | ○ |
アクセス修飾子の説明
-
public
: すべてのクラスからアクセス可能です -
protected
: 同じパッケージ内のすべてのクラスおよびサブクラスからアクセス可能です - (default) パッケージ・プライベート: アクセス修飾子を明示しない場合の設定で、同じパッケージ内でのみアクセス可能です
-
private
: 同じクラス内でのみアクセス可能であり、他のクラスからはアクセスできません
package example;
public class Example {
public int publicField = 1; // どこからでもアクセス可能
protected int protectedField = 2; // 同じパッケージ内およびサブクラスからアクセス可能
int defaultField = 3; // 同じパッケージ内でのみアクセス可能
private int privateField = 4; // 同じクラス内でのみアクセス可能
public Example() {} // どこからでもインスタンス生成可能
protected Example(String name) {}// 同じパッケージ内およびサブクラスでインスタンス生成可能
Example(int value) {} // 同じパッケージ内でのみインスタンス生成可能
private Example(boolean flag) {} // 同じクラス内でのみインスタンス生成可能
}
アクセス修飾子はなぜ使うのか?
- カプセル化: データの隠蔽とオブジェクト間の相互作用を最小限に抑えるため
- 保守性: 外部からアクセス可能な範囲を明確に定義することで、コードの安定性を確保するため
-
拡張性:
protected
を活用し、サブクラスから必要なメンバーのみアクセス可能にすることで柔軟な設計を可能にするため
クラスを定義する方法
public class Human{
// クラスの内容 ..
}
このようにクラスを定義します。
クラスの中には以下のようなものを定義することができます。
先ほど述べたように、アクセス修飾子を使用してクラスへのアクセスを制限することが可能です。
public: どこからでもアクセス可能
class Human{
// クラスの内容 ..
}
もし次のように定義した場合、同じパッケージ内でのみアクセス可能です。
-
(default)
: 同じパッケージ内でのみアクセス可能
その他のキーワードとしては以下のものがあります:
-
public
: どこからでもアクセス可能 -
(default)
: 同じパッケージ内でのみアクセス可能 -
final
: 継承が不可能なクラスとして定義 -
abstract
: 抽象クラスとして宣言
これらについての詳細は、次回の記事「継承とパッケージ」で取り上げる予定です。
なお、先ほど述べた {}
で囲まれた部分(ローカル変数として扱われる範囲)には、インスタンスのライフサイクル中に存在するコードが定義されます。
ネストクラス (Nested Class)
Javaでは、クラスの中に別のクラスを定義することができます。
これを ネストクラス と呼びます。
public class OuterClass {
// 静的ネストクラス
static class StaticNestedClass {
// 静的ネストクラスの内容
}
// インナークラス
class InnerClass {
// インナークラスの内容
}
}
ネストクラスは、次の2種類に分類されます。
- 静的ネストクラス
- 非静的ネストクラス(インナークラス)
- 非静的(non-static): インナークラス(inner class) と呼ばれます。
- 静的(static): 静的ネストクラス(static nested class) と呼ばれます
非静的ネストクラスは、囲んでいるクラスの他のメンバー(private
を含む)にもアクセスすることができます。
次のようにコードを記述して実行してみると、privateField
に正常にアクセスし、出力されることを確認できます。
それが可能な理由は、インナークラスが外部クラスのコンテキスト(Context)を共有しているためです。
上記のコード例にある OuterClass.java
を javac
でコンパイルしてみると、次のようになります
OuterClass$InnerClass.class
と OuterClass.class
という2つのクラスファイルが生成されることを確認できます。
これら2つのバイトコードを確認すると、次のようになっています(長いため、写真ではなくテキストで記載します)。
~/Documents/java_study/main ❯ javap -c OuterClass$InnerClass.class at 03:02:34 PM
Compiled from "OuterClass.java"
public class OuterClass {
public OuterClass();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #7 // String Private Field
7: putfield #9 // Field privateField:Ljava/lang/String;
10: return
public static void main(java.lang.String[]);
Code:
0: new #10 // class OuterClass
3: dup
4: invokespecial #15 // Method "<init>":()V
7: astore_1
8: new #16 // class OuterClass$InnerClass
11: dup
12: aload_1
13: dup
14: invokestatic #18 // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;
17: pop
18: invokespecial #24 // Method OuterClass$InnerClass."<init>":(LOuterClass;)V
21: astore_2
22: aload_2
23: invokevirtual #27 // Method OuterClass$InnerClass.printPrivateField:()V
26: return
}
~/Documents/java_study/main ❯ javap -c OuterClass.class at 03:02:43 PM
Compiled from "OuterClass.java"
public class OuterClass {
public OuterClass();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #7 // String Private Field
7: putfield #9 // Field privateField:Ljava/lang/String;
10: return
public static void main(java.lang.String[]);
Code:
0: new #10 // class OuterClass
3: dup
4: invokespecial #15 // Method "<init>":()V
7: astore_1
8: new #16 // class OuterClass$InnerClass
11: dup
12: aload_1
13: dup
14: invokestatic #18 // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;
17: pop
18: invokespecial #24 // Method OuterClass$InnerClass."<init>":(LOuterClass;)V
21: astore_2
22: aload_2
23: invokevirtual #27 // Method OuterClass$InnerClass.printPrivateField:()V
26: return
}
上記のバイトコードを見ると、OuterClass
は内部クラス(OuterClass$InnerClass
)を明示的に参照していることが分かります。
また、main
メソッドには内部クラスのインスタンスを生成し、そのメソッドを呼び出している部分が含まれています。
8: new #16 // class OuterClass$InnerClass
...
23: invokevirtual #27 // Method OuterClass$InnerClass.printPrivateField:()V
一方、内部クラスは常に自分の外部クラスに依存しています。
OuterClass$InnerClass
を確認すると、外部クラスのインスタンスを参照していることが分かります。
invokespecial #24 // Method OuterClass$InnerClass."<init>":(LOuterClass;)V
さらに、内部クラスが外部クラスの private
フィールドやメソッドにアクセスできることも確認できます。
getfield #9 // Field OuterClass.privateField:Ljava/lang/String;
ネストクラスを使用する理由
-
論理的に一箇所でのみ使用されるクラスをグループ化する方法:
- もしあるクラスが他の特定のクラスでのみ有用である場合、そのクラスを対象のクラス内に含め、両者を一緒に維持することが理にかなっています。
- このような ヘルパークラス(helper class) をネストすることで、パッケージがより簡潔になります。
-
カプセル化を向上させる:
- 2つのトップレベルクラス A と B を考えてみましょう。B が本来
private
として宣言されるべき A のメンバーにアクセスする必要がある場合、B を A 内に隠すことで、A のメンバーをprivate
に保ちながらも B がアクセスできるようになります - また、B 自体も外部から隠すことが可能です
public class A { private String privateFiled = "haroya"; public class B { public void printPrivateFiled() { System.out.printlnf("print : " + privateFiled); } } public static void main(String[] args) { A a = new A(); A.B b = a.new B(); b.printPrivateFiled(); // Aの private メンバーにアクセス }
- 2つのトップレベルクラス A と B を考えてみましょう。B が本来
-
コードの可読性と保守性を向上させる:
トップレベルクラス内に小さなクラスをネストすることで、そのコードが使用される場所により近くなり、可読性と保守性が向上します
ローカルクラス
Javaでは、メソッド、コンストラクタ、またはブロックの内部でもクラスを定義することができます。
これらはそのローカルスコープ({ }
内部)でのみ使用可能で、メソッドのローカル変数のように動作します。
以下はその例として作成したコードです。
public void sayHello() {
class HelloWorld {
void printMessage() {
System.out.println("Hello, World!");
}
}
HelloWorld hello = new HelloWorld();
hello.printMessage();
}
コードをご覧いただくと、次のように sayHello()
メソッド内で class HelloWorld
を定義していることが分かります。
これを ローカルクラス と呼びます。
バイトコードで分析すると、次のようなことが分かります。
以前のネストクラスと同様に、クラスファイルが2つ生成されましたが、
~/Documents/java_study/main ❯ javap -c Haroya.class at 03:37:38 PM
Compiled from "Haroya.java"
public class Haroya {
public Haroya();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void sayHello();
Code:
0: new #7 // class Haroya$1HelloWorld
3: dup
4: aload_0
5: invokespecial #9 // Method Haroya$1HelloWorld."<init>":(LHaroya;)V
8: astore_1
9: aload_1
10: invokevirtual #12 // Method Haroya$1HelloWorld.printMessage:()V
13: return
public static void main(java.lang.String[]);
Code:
0: new #15 // class Haroya
3: dup
4: invokespecial #17 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #18 // Method sayHello:()V
12: return
}
~/Documents/java_study/main ❯ javap -c Haroya\$1HelloWorld at 03:37:50 PM
Compiled from "Haroya.java"
class Haroya$1HelloWorld {
final Haroya this$0;
Haroya$1HelloWorld(Haroya);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:LHaroya;
5: aload_0
6: invokespecial #7 // Method java/lang/Object."<init>":()V
9: return
void printMessage();
Code:
0: getstatic #13 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #19 // String Hello, World!
5: invokevirtual #21 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
.class
ファイルは次のように生成されることを確認できます。
以前と同様にクラスごとにファイルが生成されましたが、ローカルクラスのファイルを詳しく見ると、
final Haroya this$0;
Haroya$1HelloWorld(Haroya);
Code:
0: aload_0
1: aload_1
外部クラスの参照がコンストラクタに含まれており、
javac
コンパイラがローカルクラスの安全性を保証するために、
final Haroya this$0;
が宣言されていることを確認できます。
これを要約すると以下の通りです
Inner ClassとLocal Classの違い
Inner Class(内部クラス)
- 外部クラスの情報を暗黙的に把握しています。
- 内部クラスは外部クラスのすべてのメンバー(
private
を含む)に直接アクセス可能です。 - 外部クラスのインスタンスを明示的に参照しなくても、外部クラスのメンバーにアクセスできます。
- 内部クラスは外部クラスのすべてのメンバー(
Local Class(ローカルクラス)
- 外部クラスの参照を明示的に保存します。
- ローカルクラスは
this$0
フィールドを通じて外部クラスの情報に間接的にアクセスします。- 外部クラスの参照をコンストラクタの引数のように受け取り、利用します。
- 定義されたブロック内のローカル変数や引数をキャプチャして使用します(ただし、それらは
final
または「事実上のfinal
」である必要があります)。
- ローカルクラスは
Effectively Finalとは?
Java 8で導入された概念で、変数が明示的に final
として宣言されていなくても、初期化後に値が変更されない変数を指します。
void satHello(){
String hello = "こんにちは";
hello = "안녕하세요";
}
次のように値が変更される場合、final
ではありません。
void satHello(){
String hello = "こんにちは";
}
以下のようなコードでは、ローカル変数のスコープ内で hello
が一度値を割り当てられ、その後スコープが終了するまで変更されないことが確認できます。
これを実質的な final
、すなわち Effectively Final と呼びます。
public class Haroya {
public void sayHello() {
String hello = "こんにちは";
class HelloWorld {
void printMessage() {
System.out.println(hello);
}
}
HelloWorld helloworld = new HelloWorld();
helloworld.printMessage();
}
public static void main(String[] args) {
Haroya haroya = new Haroya();
haroya.sayHello();
}
}
以前に作成したローカルクラスのコードを少し修正した例は以下の通りです。
hello
という変数は、sayHello()
メソッド内で値が割り当てられ、
public void sayHello() {
String hello = "こんにちは";
class HelloWorld {
void printMessage() {
System.out.println(hello);
}
}
ローカルクラスでは、一度も値の変更が行われませんでした。
この状態を 実質的な final
状態(Effectively Final) と呼ぶことができます。
では、このコードを次のように変更した場合はどうなるでしょうか?
class HelloWorld {
void printMessage() {
hello = "안녕하세요";
System.out.println(hello);
}
}
もし次のように内部で値を変更した場合、
次のようなエラーが発生します。
なぜなら、以前説明したように、オブジェクトの参照をコピーして使用しているためです。
スタック内のメソッドは命令が終了するとライフサイクルに従って消えますが、
ヒープメモリに生成されたオブジェクトはGC(ガベージコレクション)が実行されるまで再利用可能です。
例えば、複数のスレッドを生成してヒープ上のオブジェクトを参照する場合、
その変数が final
でないと値が変更され続けるため、オブジェクトの整合性を保証できません。
そのため、Javaでは final
を使用して値をコピーする方法が採用されています。
以下のコードは、ThreadExample
クラスの例で、ローカル変数がスレッド間で共有される状況を示しています。
public class ThreadExample {
public void method() {
String message = "こんにちは"; // スタック上のローカル変数
// 1. 最初のスレッド
new Thread(() -> {
// message がヒープにコピーされて保存される
System.out.println("スレッド1: " + message);
}).start();
// 2. 二番目のスレッド
new Thread(() -> {
// 同じ message を使用
System.out.println("スレッド2: " + message);
}).start();
// メソッドが終了してもスレッドは実行を継続する!
}
}
上記の例のようなコードは許容されますが、ローカルスコープ({}
)内で値を変更しようとすると、
new Thread(() -> {
System.out.println("スレッド1 " + message);
message = "안녕하세요";
}).start();
2番目のスレッドでは、message
の一貫性を保証することができません。
このような理由から、Javaでは Effectively Final という概念が導入されています。
匿名クラス(Anonymous Class)
interface Printable {
void print();
}
class OuterClass {
void method() {
// インターフェースやクラスをその場で実装
Printable printable = new Printable() {
public void print() {
System.out.println("匿名クラス");
}
};
printable.print();
}
}
このように、Javaでは匿名クラスが導入されています。
匿名クラスは Innerクラス の一種であり、
一度だけ使用するオブジェクトを簡単に生成・破棄することで、
コードの保守性を向上させる役割を担っています。
その他の継承に関する内容...
class Human extends Animal implements HumanInterface{}
このように、スーパークラスやインターフェースから情報を継承することができます。
これについては、次回の記事で「継承」について詳しく説明する際に記載する予定です。
属性(フィールド)
- インスタンス変数: オブジェクトごとに個別に生成される
public class Person {
private String name; // インスタンス変数
private int age; // インスタンス変数
private final String id; // 定数(final)のインスタンス変数
private transient String secret; // シリアライズ対象外のフィールド
}
- 静的(static)変数: クラスごとに1つ生成され、すべてのオブジェクトで共有される
private static int count = 0;
静的(static)変数
すべてのオブジェクトが共有するとはどういう意味でしょうか?
public class Test {
private static int count = 0;
public Test() {
count++;
}
public int getCount() {
return count;
}
}
次のように定義した場合、
public class Main {
public static void main(String[] args) {
Test test1 = new Test();
Test test2 = new Test();
System.out.println("test1 : " + test1.getCount());
System.out.println("test2 : " + test2.getCount());
}
}
以下のコードを呼び出すと、
どちらの場合も、count
は2と出力されます。
Testクラスのバイトコードを見ると、getStatic
や putStatic
という記述が出てきますが、
JVM標準文書を検索してみると、より詳しい情報を得ることができました。
getStatic
を例に取ると、静的(static)フィールドを取得してスタックにプッシュする命令と記載されていました。
なぜなら、JVM内で Method Area に静的変数(static フィールド)が既に保存されているため、
クラスレベルでメモリに一つだけ存在し、その値を取得してスタック(Javaの命令実行、Javaはスタックベース)に載せるためです。
要約すると、JVMの Method Area に静的(static)フィールドが保存されているため、
クラスレベルで一つだけ存在し、必要に応じてスタックに載せて使用されるということです。
フィールドに関する整理
-
Method Area(Static Area)
-
static
メンバーが保存される領域 - プログラム開始時に生成され、終了時に破棄される
- すべてのスレッドで共有される
-
static
変数、static
メソッド、クラス情報などが保存される
-
-
Heap
- インスタンス(オブジェクト)が保存される領域
-
new
演算子で生成されたオブジェクトやインスタンス変数が保存される - GC(ガベージコレクション)の対象
メソッド
メソッドは次のように定義します。
public void printHello() {
System.out.println("hello");
}
-
静的(static)メソッド
- 静的メソッドは、先ほどの
static
フィールドと同様にメソッド領域にロードされます。 - 通常のインスタンスメソッドとの違いは、クラスに結びついているため、クラスインスタンスとは関係なく呼び出すことができる点です。
- 静的メソッドは、先ほどの
public class Test {
private static int count = 0;
public static int getCount() {
return ++count;
}
}
次のように定義して、
public class Main {
public static void main(String[] args) {
System.out.println("no instance 1 : " + Test.getCount());
System.out.println("no instance 2 : " + Test.getCount());
}
}
次のようにインスタンスを生成せずにコードを実行すると、
静的メソッドがインスタンスフィールドにアクセスできない理由は?
package org.example;
public class Test {
private int count = 0;
public static int getCount() {
return ++count;
}
}
次のように、インスタンスフィールドである private int count = 0;
に、static getCount()
はアクセスできません。
その理由は、静的(static)メソッドは Method Area にあるクラスを直接呼び出すのに対し、
インスタンスフィールドは特定のインスタンス(Heap)で生成されるためです。
error: non-static variable count cannot be referenced from a static context
return ++count;
^
実行してみると、次のようなコンパイルエラーが発生することを確認できます。
初期化ブロック
-
インスタンス初期化ブロック
- オブジェクトが生成されるたびに実行されるブロック
- コンストラクタよりも優先して実行されます
public class Test {
private static int count = 0;
{
count += 1;
System.out.println("instance block");
}
public int getCount() {
return count;
}
}
次のように { }
内で宣言することができます。
public class Main {
public static void main(String[] args) {
Test test1 = new Test();
System.out.println(test1.getCount());
Test test2 = new Test();
System.out.println(test2.getCount());
}
}
다음과 같은 코드를 실행한다면
次のようなコードを実行すると、{ }
が正しく適用されることを確認できます。
「コンストラクタを使えば良いのでは?」
そうではありません。共通の処理が含まれる場合、{ }
を使用することでボイラープレートコードを削減できます。
public class Test {
private int count;
{
count = 0;
System.out.println("count = " + count);
}
public Test() {
count++;
}
public Test(int count) {
this.count = count;
}
public int getCount() {
return count;
}
}
次のように、インスタンスを 0
に初期化してからコンストラクタを呼び出し、
その後に出力するロジックを作成しました。
public class Main {
public static void main(String[] args) {
Test test1 = new Test();
System.out.println(test1.getCount());
Test test2 = new Test(100);
System.out.println(test2.getCount());
}
}
このコードを記述して実行した結果が以下の通りです。
共通の処理がインスタンス初期化ブロックを経由することが分かります。
共通のコンストラクタ呼び出し前のロジックは、インスタンス初期化ブロックに移しても問題ありません。
-
静的初期化ブロック(static block)
- クラスが最初にロードされる際に一度だけ実行される初期化ブロックです。
package org.example;
public class Test {
private static int count;
static {
count = 0;
System.out.println("count = " + count);
}
public int getCount() {
return ++count;
}
}
次のように、static
で囲まれた { }
初期化ブロックを記述します。
public class Main {
public static void main(String[] args) {
Test test1 = new Test();
System.out.println(test1.getCount());
Test test2 = new Test();
System.out.println(test2.getCount());
}
}
次のようなコードを実行すると、
クラスがロードされる際に、必要な初期化処理が一度だけ実行されることを確認できます。
静的ブロックはシングルトンパターンを使用する際に応用できます。
public class Singleton {
private static final Singleton instance;
// 静的ブロックで初期化
static {
try {
instance = new Singleton();
} catch (Exception e) {
throw new RuntimeException();
}
}
// private コンストラクタ
private Singleton() {
// 初期化作業
}
public static Singleton getInstance() {
return instance;
}
}
コンストラクタ
コンストラクタとは、インスタンスを生成し初期化する際に使用する特殊なメソッドです。
- クラス名と同じ名前を持ち、戻り値の型がありません
package org.example;
public class Test {
private int count;
public Test(){
count = 0;
}
}
- コンストラクタとメソッドは
Overloading
(オーバーローディング) が可能です
package org.example;
public class Test {
private int count;
public Test(){
count = 0;
}
public Test(int count){
this.count = count;
}
}
- コンストラクタを明示しない場合、デフォルトコンストラクタが自動的に追加されます(
javac
のコンパイル時に)
public class Test {}
次のようにコンストラクタのないクラスを記述し、バイトコードを確認してみたところ、
次のように、public org.example.Test()
という引数のないデフォルトコンストラクタが
javac
コンパイラによって追加されていることを確認できます。
Super
以前のデフォルトコンストラクタのみを持つ Test.class
のバイトコードを確認すると、
次のようなコードが含まれていることが分かります。
1: invokespecial #1 // Method java/lang/Object."<init>":()V
すべてのクラスは Object
を親として持ち、すべてのコンストラクタは定数プールに保存されている親クラスのコンストラクタを呼び出します。
言い換えると、super();
が内部的に隠されています。
super();
に関しては、次回の継承についての記事で整理する予定です。
オブジェクトを作成する方法(new
キーワードの理解)
new
キーワードは、次のようにオブジェクト(クラス)のコンストラクタを呼び出して使用します。
Test test = new Test();
-
new はヒープメモリにオブジェクト用の領域を割り当てます。
-
宣言されているが初期化されていない値は、javac コンパイラによって次のように値が割り当てられます。
Data Type | Default Value (for fields) |
---|---|
byte |
0 |
short |
0 |
int |
0 |
long |
0L |
float |
0.0f |
double |
0.0d |
char |
'\u0000' |
String (or any object) |
null |
boolean |
false |
this
キーワードの理解
The Java Virtual Machine uses local variables to pass parameters on method invocation. On class method invocation, any parameters are passed in consecutive local variables starting from local variable 0. On instance method invocation, local variable 0 is always used to pass a reference to the object on which the instance method is being invoked (this in the Java programming language). Any parameters are subsequently passed in consecutive local variables starting from local variable 1.
- Java仮想マシン(JVM)は、メソッドを呼び出す際にローカル変数を使用してパラメータを渡します。
クラスメソッドを呼び出す場合、すべてのパラメータは0番目のローカル変数から順に連続して格納されます。
インスタンスメソッドを呼び出す場合、常に0番目のローカル変数には、呼び出しているオブジェクトの参照(Javaではthis
)が格納されます。
その後、パラメータは1番目のローカル変数から順に格納されます
Javaのインスタンスメソッドやコンストラクタでは、スタックに参照を載せるための this
が含まれています。
package org.example;
public class Test {
private static int staticInt = 0;
private int instanceInt = 0;
public static void staticMethod() {
++staticInt;
}
public void instanceMethod() {
++instanceInt;
}
}
バイトコードを確認すると、
このようになっています。確認すると、静的メソッドやインスタンスには、
0: aload_0
次のような命令が必ず実行されることを確認できます。
これが this
に対応するバイトコードで、その役割は以下の通りです。
インスタンスメソッドを呼び出す場合、常に0番目のローカル変数には、呼び出しているオブジェクトの参照(Javaでは
this
)が格納されます。
- オブジェクトの参照をスタックにロードする際に
this
を使用します。 - インスタンスメソッドは、インスタンスフィールドや他のインスタンスメソッドにアクセスする際、
this
を通じてスタックにロードします。
デフォルトコンストラクタの場合、以前説明したように親クラスのメソッド(Object
のコンストラクタ)を呼び出すため、
this
の参照をスタックにロードします。
ただし、JVMでは this
を自動的に呼び出すため、
public class Test {
private int num = 0;
public Test(int a) {
// this が隠れている
// super も隠れている
num = a;
}
}
次のように代入されます。
public static void staticMethod();
Code:
0: getstatic #13 // Field staticInt:I
3: iconst_1
4: iadd
5: putstatic #13 // Field staticInt:I
8: return
一方で、static
メソッドでは this
を介して呼び出すことはありません。
なぜなら、static
メソッドは既に JVMランタイムの Method Area にロードされており、
スタックにロードする必要がないためです。
公式ドキュメントを参照すると、次のように記載されています:
A method that is declared static is called a class method.
クラスメソッドは特定のオブジェクトへの参照なしに常に呼び出されます。
-
static
と宣言されたメソッドは特定のオブジェクトとは関連せず、クラス自体に属するメソッドです。 - そのため、オブジェクトを作成せずとも直接呼び出すことができます
static メソッド内では
this
やsuper
のようなキーワードは使用できません。
なぜなら、これらのキーワードは特定のオブジェクト(現在のオブジェクト)を指すものですが、static メソッドはオブジェクトなしで呼び出されるためです。
改めて定義すると、this
とは以下の通りです:
-
this
は参照をスタックにロードするために使用されます
JVMのメソッド呼び出し規約によれば、インスタンスメソッドでは最初のローカル変数スロットに this
が格納されます。
スタックに this
をロードすることで、メソッドがオブジェクトの状態に基づいて動作することを可能にしています。
インスタンスメソッドのバイトコード実行時に最初の命令として aload_0
によって this
をスタックに載せるのも、この規約によるものです。
- 内部オブジェクトが存在せず、内部インスタンスにアクセスする必要がない場合、
javac
コンパイラではthis
が生成されません。
(例: オブジェクト参照をスタックにロードする必要がない状況)
public class Test {
public void print() {
System.out.println("Hello World");
}
}
次のように定義した場合(内部でアクセスするインスタンスフィールドが存在しない場合)、
String
は定数プールに格納され、命令は Method Area から取得されるため、
内部インスタンスを参照する必要がありません。
そのため、aload_0
(this
)が生成されません。
その他、this
キーワードを直接参照が必要な箇所で以下のように使用することができます。
- 自分自身を明確にする場合
public class Test {
private int count;
public Test(int count) {
this.count = count;
// this.count = count;
// this.count はインスタンス変数、count は引数
}
}
int count は重複宣言ではありませんか?
ローカル変数についてもう一度確認しましょう。
.2. 自分のインスタンスを参照する場合
public Test increment() {
this.count++;
return this; // 現在のオブジェクトを返す
}
コンストラクタチェイニング
- コンストラクタは特別なメソッドであり、メソッドのようにオーバーロードをサポートします。
-
this
は自分自身のインスタンスを参照するときに使用されます。
これらの2つの特性を利用して、コンストラクタが自分自身の別のコンストラクタを呼び出すことができます。
これを コンストラクタチェイニング と呼びます。
public class コンストラクタチェイニング {
private int count;
private String name;
// デフォルトコンストラクタ
public コンストラクタチェイニング() {
this(0, "default"); // 別のコンストラクタを呼び出し
}
// 引数が1つのコンストラクタ
public コンストラクタチェイニング(int count) {
this(count, "default"); // 別のコンストラクタを呼び出し
}
// 引数が2つのコンストラクタ
public コンストラクタチェイニング(int count, String name) {
this.count = count;
this.name = name;
}
}
メソッドチェイニング
次のように、メソッドチェイニングも可能です。
public class Person {
private String name;
private int age;
public Person setName(String name) {
this.name = name;
return this; // 現在のインスタンスを返すことでメソッドチェイニングが可能
}
public Person setAge(int age) {
this.age = age;
return this; // 現在のインスタンスを返すことでメソッドチェイニングが可能
}
}
// 使用例
Person person = new Person().setName("Alice").setAge(25);
これ以外にも、ラムダ式や動的プロキシのような場面でも使用することができます。
重要な点は、インスタンスの参照をスタックにロードする必要がある場合、
JVMが自動的に追加するか、直接呼び出して使用すればよいということです。
メソッドチェイニングの例「ビルダーパターン」
Lombokの @Builder
アノテーションを付けたコードのバイトコードを展開すると、
次のようにビルダーパターンを活用していることが分かります。
メソッドチェイニングを活用してビルダーパターンを実現していることが確認できます。
public Member(final Long id, final String nickName, final String email, final String profileImageUrl, final LocalDateTime lastLoginAt) {
this.id = id;
this.nickName = nickName;
this.email = email;
this.profileImageUrl = profileImageUrl;
this.lastLoginAt = lastLoginAt;
}
public static class MemberBuilder {
private Long id;
private String nickName;
private String email;
private String profileImageUrl;
private LocalDateTime lastLoginAt;
MemberBuilder() {
}
public MemberBuilder id(final Long id) {
this.id = id;
return this;
}
public MemberBuilder nickName(final String nickName) {
this.nickName = nickName;
return this;
}
public MemberBuilder email(final String email) {
this.email = email;
return this;
}
public MemberBuilder profileImageUrl(final String profileImageUrl) {
this.profileImageUrl = profileImageUrl;
return this;
}
public MemberBuilder lastLoginAt(final LocalDateTime lastLoginAt) {
this.lastLoginAt = lastLoginAt;
return this;
}
public Member build() {
return new Member(this.id, this.nickName, this.email, this.profileImageUrl, this.lastLoginAt);
}
public String toString() {
return "Member.MemberBuilder(id=" + this.id + ", nickName=" + this.nickName + ", email=" + this.email + ", profileImageUrl=" + this.profileImageUrl + ", lastLoginAt=" + this.lastLoginAt + ")";
}
}
次のように、チェイニングを通じてビルダーパターンを実現していることが分かります。
参考文献
一部の簡単なサンプルコードはChatGPTを使用しました。
誤った内容や文法の間違いがありましたら、フィードバックをいただけると幸いです。