ある先生が言いました。「Stringクラスの初期化は,String クラス名 = 文字列リテラル;
」だと。
ある先生が言いました。「Javaでの文字列比較はequalsメソッドを使いましょう」と。
ある学生が言いました。「JavaのStringクラスはnew
せず使えます」と。
ある学生が言いました。「Javaでの文字列比較はequalsメソッドで行います」と。
私は思いました。「なんで?」と。学び始めた当初の私には理由がわからない「こうあるべし」に,そこはかとない気持ち悪さがありました。
環境
- java version "9.0.4"
うわあ,このPCバージョン上げ忘れとる...
襲い掛かるequalsメソッドの謎:等価演算子で比較?
文字列比較のequalsメソッドには,先頭にこのような記述があります。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
// 以下省略
}
同一インスタンスであればtrue,その後,もにょもにょして(雑ですが趣旨から離れるのでご容赦を)一致すればtrueを返す仕組みです。
参照型であるStringの「同一文字列か否か」の判定にわざわざ「同一インスタンスか否か」の判定を行う等価演算子を組み込む理由とは何ぞやというのが本日のお話です。
実験
public class StringCheck {
public static void main(String[] args) {
String stringLiteral = "I love Elixir";
String stringNew = new String("I love Elixir");
if (stringLiteral == "I love Elixir") System.out.println("literal is equal");
else System.out.println("literal is not equal");
if (stringNew == "I love Elixir") System.out.println("new is equal");
else System.out.println("new is not equal");
}
}
// 出力結果
// literal is equal
// new is not equal
文字列の比較はequalsで行いましょうという説明に対する切ない気持ちが生まれました。
なぜ文字列リテラルは等価演算子が使用可能なのか
Moreover, a string literal always refers to the same instance of class String.
Oracleさんの有難いお話
「文字列リテラルは、常にStringクラスの同じインスタンスを参照します。」
だから,等価演算子で比較できるのですね。そういう作りなのですね素敵。(単純)
equalsメソッドもわざわざややこしい比較をする前に,同一インスタンスの可能性を除去してしまえた方が効率がいいという判断なのでしょう。
class OtherString {
String string = "I love Elixir";
}
public class StringCheck {
public static void main(String[] args) {
String stringLiteral = "I love Elixir";
OtherString other = new OtherString();
if (stringLiteral == other.string) System.out.println("true");
else System.out.println("false");
}
}
// 出力:true やっぱり同一インスタンスの模様
Stringクラスの再確認
学び始めたあの頃,「Stringクラスのインスタンスは生成されるたびに新しいメモリ領域を確保する」と教わりました。それが,new String()
した結果なのでしょう。上記の実験でもfalseを返しています。
Stringクラスの概要はこちら。
Instances of class String represent sequences of Unicode code points.
A String object has a constant (unchanging) value.
String literals are references to instances of class String.
The string concatenation operator + implicitly creates a new String object when the result is not a constant expression.
Oracleさんの有難いお話
素直にGoogle翻訳様の力を借りたところ,
- Stringクラスのインスタンスは,Unicodeコードポイントのシーケンスを表します。
- Stringオブジェクトは,定数(不変)の値を持ちます。
- 文字列リテラルは,Stringクラスのインスタンスへの参照です。
- 文字列連結演算子+は,結果が定数式でない場合は暗黙的に新しいStringオブジェクトを作成します。
とのこと。
if (stringLiteral + "." == stringLiteral + ".") System.out.println("true");
else System.out.println("false");
// 出力:false 連結すると新しいインスタンスが生成されるので,別もの扱い
ならば,文字列リテラルはどのように同一インスタンスを参照するのでしょうか。
JavaヒープメモリとStringプール
ざっくり説明すると,変数などを用いデータを確保するために,JVMはOSからメモリ領域の一部を借りてきて使用しています。
Javaヒープメモリは借りてきたメモリのさらに一部で,生成したインスタンスのデータなどを保持しておくための領域です。このヒープメモリを効率よく使うためにも,Garbage Colector(GC)なんかが裏で頑張って動いてくれるっていうわけですね。
Stringクラスは特別なクラスで,Javaヒープメモリの中にStringプールという領域を確保してもらえています。Stringプールには作ったStringクラスのインスタンスをせっせせっせと溜めておきます。
文字列リテラルは,どのようにして同一インスタンスの参照を得ているか
デザインパターンに「Flyweightデザインパターン」というものがあります。インスタンスを共有することでリソースを無駄なく使うという類の考え方で,Stringプールもこの考え方なのでしょう。プログラムを軽くするのが目標です。
ダブルクォートで指定された文字列リテラルは,まずStringプールに同一の文字列を表すインスタンスが存在しないかを探ります。同一の文字列を表すインスタンスが見つかれば,そのインスタンスの参照を利用します。見つからなければ新しいインスタンスを生成します。
無制限に新しいインスタンスを生成するとコストがかかりすぎるために,こういう仕組みになっているのですね。そして,ここでインスタンスの共有が行われるため,equalsメソッドではまず等価演算子を使っているのでしょう。
また,new String()
した場合は,無条件に新しいインスタンスを作成します。new
している時点で「何かしらの理由があって新しいインスタンスが必要」と判断してくれるということです。親切です。
また,能動的にnew
した場合はStringプールではなくJavaヒープメモリに領域が確保されます。他のクラスでnew
した時と同じ挙動になるわけです。
なお,new
したStringクラスのインスタンスもinternメソッドを利用することでStringプールに登録するなどが可能です。
そう考えると,最初に感じていた「なんでStringだけ...」という気持ち悪さもなくなりますね。万歳。
まとめ
- 基本的に文字列は
String クラス名 = 文字列リテラル;
でいい。 - 等価演算子で期待する結果が返るとは確約できないので,文字列比較はequalsメソッドで統一しておけば安心。
- インターンと言われると,インターンシップのイメージが先立ちますが,"intern"という単語には「抑留する」という意味もあるらしく,闇を感じました。