はじめに
JavaSilverを勉強中に、文字列のコンスタントプールの仕組みが分かったようで分からなかったので、文字列インスタンスがいつどこでどのように生成されるのかを理解したくなりました。完全初心者なので間違っているところがあれば訂正して頂けたら嬉しいです。
コンスタントプールとは
文字列インスタンスを生成するとき、同じ内容の文字列インスタンスが既に存在するなら新しいインスタンスを生成するのではなく既存のインスタンスを使い回すことでメモリを節約する仕組みのこと。文字列型はimmutableで内容を変更されないことが保証されるので、安全に使い回すことができる。
文字列インスタンス生成で使用する領域
文字列インスタンスを生成するとき、文字列リテラルや文字列インスタンスがどのような領域を使用するのかを理解する。
文字列はコンパイルされるとクラスファイルに定数として文字列リテラルが登録される。この時点ではインスタンスではないので、一つの文字列情報として管理される。この文字列の登録情報を一元管理する場所がコンスタントプールである。コンパイルされたクラスファイルを実行してインスタンスを生成すると、ヒープ領域にインスタンスを格納するが、文字列インスタンスの場合はヒープ領域の中でも特にインターンプールという場所で特別に管理される。
文字列のコンパイルと実行
"sushi"という同じ内容の文字列を2つ生成した時、どのような流れになるかを理解する。
String s1 = "sushi";
String s2 = "sushi";
System.out.println(s1 == s2);// true
1行目
String s1 = "sushi";
- コンパイル時に"sushi"という文字列情報をコンスタントプールに登録する
- 実行時に"sushi"という文字列インスタンスがインターンプールに存在するか確認する
- 存在していないので、インターンプールに文字列インスタンスを生成する
- 変数s1はそのインターンプール内のインスタンスを参照する
2行目
String s2 = "sushi";
- コンスタントプールに"sushi"というリテラルが存在しているか確認する(コンパイル時)
- 存在していたので、インターンプール内に"sushi"という内容のインスタンスを探しに行く
- 変数s2はインターンプール内にある既存のインスタンスへの参照値を格納する
よって変数s1,s2は同じインターンプールのインスタンスを参照し、==での比較はtrueとなる。
文字列は不変型
s1とs2に同じインスタンスへの参照値が格納されている状態で、s1に"udon"というリテラルを代入してみる。
String s1 = "sushi";
String s2 = "sushi";
System.out.println(s1 == s2);// true
s1 = "udon";
System.out.println("s1:" + s1);// udon
System.out.println("s2:" + s2);// sushi
s1に"udon"という文字列リテラルを代入するということは、"udon"という内容の文字列インスタンスを新しく生成し、その参照値をs1に格納するということである。s1が参照している"sushi"インスタンスがの内容が"udon"に書き換わるわけではない。よって同じインスタンスへの参照をしていたs2には影響せず、s2は"sushi"のままである。
いろんなパターンを考えてみる
String s1 = "sushi";
String s2 = "su" + "shi";
System.out.println(s1 == s2);//true
文字列リテラル同士の演算はコンパイラが"sushi"と認識しコンスタントプールに既存かどうかの確認をすることができるので、新しいインスタンスを生成しない
String s1 = "su";
String s2 = s1 + "shi";
System.out.println(s1 == s2);//false
変数同士の演算はコンパイルの時点で変数s1を評価できず、新しいインスタンスを生成する
new演算子で文字列インスタンスを生成する(非推奨)
Stringクラスのコンストラクタに文字列リテラルを渡し、newでインスタンスを生成する流れを理解する。このやり方は非推奨であるが、JavaSilverでは出題される。
String s2 = new String("sushi");
まず、右辺のnew String("sushi") の実行順序について確認する。new演算子を評価する前に、引数で渡される "sushi" という文字列リテラルの評価が先に実行されることに注意する。
1.リテラルの評価
- コンスタントプールに"sushi"という文字列リテラルが存在するか確認し、存在されていないので登録する
- インターンプールに"sushi"という文字列インスタンスが存在するか確認し、存在されていないのでヒープ領域でインスタンスを生成し、それをインターンプールに登録する
2.newの実行
- Stringクラスのコンストラクタにインターンプールのインスタンスが渡され、new演算子でヒープ領域内に新たに別のインスタンスを生成する
- スタック領域の変数s1はヒープ領域に生成したインスタンスを参照する
new演算子でインスタンスを生成すると、ヒープ領域内に同じ"sushi"という内容の文字列インスタンスが2つ存在することになり、変数s1が参照するインスタンスは、インターンプールにあるインスタンスを元に生成した全く新しい別のインスタンスである。
次に、newで生成する前に"sushi"というインスタンスが既に存在した場合の流れを理解する
String s1 = "sushi";
String s2 = new String("sushi");
System.out.println(s1 == s2);//false
1行目
String s1 = "sushi";
- コンスタントプールに"sushi"が登録される
- インターンプールに"sushi"という文字列インスタンスを生成する
- s1はそのインスタンスを参照する
2行目
String s2 = new String("sushi");
- コンスタントプールに"sushi"という文字列リテラルが存在するのを確認する
- インターンプールで"sushi"という文字列インスタンスが存在するのを確認する
- そのインスタンスを元に、newで新しい別のインスタンスを生成する
- 変数s2は新しいインスタンスを参照する
変数s1,s2はヒープ領域に存在する異なる2つのインスタンスをそれぞれ参照するので、==での比較はfalseになる
次に、newによる生成がリテラルによる生成より先だった場合の流れを理解する
String s1 = new String("sushi");
String s2 = "sushi";
System.out.println(s1 == s2);//false
1行目
String s1 = new String("sushi");
- コンスタントプールに"sushi"という文字列リテラルが存在するか確認し、存在されていないので登録する
- インターンプールに"sushi"という文字列インスタンスが存在するか確認し、存在されていないのでヒープ領域でインスタンスを生成し、それをインターンプールに登録する
- Stringクラスのコンストラクタにインターンプールのインスタンスが渡され、new演算子でヒープ領域内に新たに別のインスタンスを生成する
- スタック領域の変数s1はヒープ領域に生成したインスタンスを参照する
2行目
String s2 = "sushi";
- コンスタントプールに"sushi"という文字列リテラルが存在するのを確認する
- インターンプールに"sushi"という文字列インスタンスが存在するのを確認する
- 変数s2はインターンプールに既に存在しているインスタンスを参照する
変数s1,s2はヒープ領域に存在する異なる2つのインスタンスをそれぞれ参照するので、==での比較はfalseになる