はじめに
今回の記事ではJavaのStringについて深掘りしていきたいと思います。
今所属しているチームのWikiで「文字列を扱うときはStringよりStringBuilderを使おう」という記事を読んでみて、わからないことやもっと勉強したいことがあって記事に書きます。なので今回と次の記事はJavaの文字列に関して勉強した内容になります。
Stringの特徴
Stringはオブジェクト
プログラムは「変数」を扱う際、その変数のデータタイプを指定する。
Javaでは、データ型を大きく分けて基本データ型(primitive type)と参照型(reference type)がある。
その中String
は参照型(reference type)である。
つまり、Stringはほかの参照型オブジェクトと同じようにStack領域ではなく、Heap領域にデータが保存されます。
int age = 19; // 基本データ型(primitive type)
String name = "kim"; // 参照型(reference type)
stack領域には変数名であるnameと"kim"というデータが保存されたHeapの場所(アドレス)が保持されます。
StringはImmutable(不変オブジェクト)
Immutableとは作成後にその状態を変えることのできないオブジェクトのことです。
基本的にJavaでStringで保存されたデータは変更できません。
下記のコードを見ると変数str
が参照している"Hi"
が+演算子で" Kim"
を追加することで、name変数に"Hi Kim"
でデータが入れ替わります。しかし、実際にHeapメモリでは"Hi Kim"
が新たに作られそのアドレスをstackのstr変数がまた参照する仕組みになっています。
String name = "Hi";
name += " Kim";
System.out.pringln("name"); // Hi Kim
ここで"Hi"
は参照が切れてガベージコレクションによって解放される。
nameの参照アドレスが変わったのはhashCode()
で確認できます。
String name = "Hi";
System.out.println(name.hashCode()); // 2337
name += " Kim";
System.out.println(name.hashCode()); // -2135669968
同じ変数のアドレスを出力するにも関わらずアドレスの値は全く違うことが分かります。
このようにStringはデータを入れ替わる際、新たにデータを作成し、そのアドレスを変数に代入することでデータを入れ替えています。
StringがImmutableのはなぜ?
ここで疑問が浮かびます。なぜ色んなデータタイプの中で、StringだけImmutableなのか?
その理由は大きく3つあります。
- メモリの節約ができる(String Constant Pool)
例えば下記のように各変数に同じデータを代入すると考えます。
String one = "1";
String temp = "1";
String first = "1";
Javaはこの際、"1"
のデータ3つ作ってそれぞれの変数に代入することではなく、最初1回だけ"1"
をHeapメモリのString Constant Poolに保存します。そのあとほかの変数が"1"
を使うときはString Constant Poolのデータを参照することでメモリの使用量を減らせます。
しかし、これには一つの問題点があります。
first = "first";
first変数のデータを"first"
に変えると、String Constant Poolの"1"
が"first"
に変わり、"1"
を参照していたすべての変数のデータが変わるSide Effectが生じます。
幸いにJavaはStringをImmutableで扱っているため、上記のようなSide Effectの問題はありません。
String Constant PoolについてはStringのnew()演算子とリテラルで説明します。
- セキュリティ
例えばDBのusernameとpasswordやソケット通信でのhostとportなどの情報がStringで渡されるので、ハッカーの攻撃からデータが変わることを防げます。
- マルチスレッド環境でも安全(Synchronization)
データがImmutableであればマルチスレッド環境で数十数百のスレッドがStringデータに参照してもデータを変えることはできないためThread Safeが保証される。
Stringのnew()演算子とリテラル
JavaでString変数を宣言するときにリテラル
とnew()演算子
を使う方法があります。
String str1 = "リテラル"
String str2 = new String("new演算子")
コードで見ると宣言方法が違うだけだと思うかもしれませんが、実際にはメモリに保存される方法も違います。
new()演算子でStringを宣言するとHeapメモリに保存されますが、リテラルでStringを宣言するとString Constant Poolに保存されます
下記のように文字列を宣言してみます
String str1 = "Hi";
String str2 = "Hi";
String str3 = new String("Hi");
String str4 = new String("Hi");
このコードを図で表現すると次のようになる。
メモリの節約ができる(String Constant Pool)に記述したようにstr1とstr2変数は同じString Constant Poolのアドレスを参照している。
文字列比較 ==、equals()
Javaではint
とboolean
などの基本データタイプは==
演算子で比較します。
しかし、Javaを勉強してみるとStringのようなオブジェクトを比較するときは==
ではなく、equals()
というメソッドを使うようになったいます。
コードでStringを比較してみます。
// リテラル
String str1 = "Hi";
String str2 = "Hi";
// new()演算子
String str3 = new String("Hi");
String str4 = new String("Hi");
// リテラル - リテラル 比較
System.out.println(str1==str2); // true
// リテラル - new()演算子 比較
System.out.println(str1==str3); // false
System.out.println(str1.equals(str3)); // true
// new()演算子 - new()演算子 比較
System.out.println(str3==str4); // false
System.out.println(str3.equals(str4)); // true
このような結果が出たのは==
演算子とequals()
メソッドの差を見てみると、
-
==
演算子は比較する二つの対象のアドレスを比較する。 -
equals()
メソッドは比較する二つの対象のデータそのものを比較する。
System.out.println(str1==str2)
は同じString Constant Poolに保存されているデータを参照するため、trueを返す。
一方でSystem.out.println(str3==str4)
はHeapメモリでそれぞれ別のメモリ領域に保存されてデータを参照するため、falseを返します。
(変数が参照しているアドレスは任意のアドレスを使いました。)
intern()メソッド
恐らくJavaを勉強してもintern()メソッドを知らない方もいらっしゃるはずです。(実際に私もJava Silver資格の勉強をするとき知りました。)
Stringをリテラルで宣言する場合、内部処理でStringのintern()
メソッドが呼び出されます。
intern()
メソッドはStringをリテラルで宣言するとき、文字列がString Constant Poolに存在するかを確認します。存在するとそのPoolに保存されている文字列のアドレスを返し、なければ文字列をPoolに保存してそのアドレスを返します。
つまり、intern()
メソッドを使うと==
演算子を使って文字列の比較が可能となります。
// リテラル
String str1 = "kim";
// new()演算子
String str2 = new String("kim");
System.out.println(str1 == str2); // false
System.out.println(System.identityHashCode(str1)); // 2003749087
System.out.println(System.identityHashCode(str2)); // 1324119927
// intern()メソッドでPoolの中の"kim"のアドレスをreturn
str2 = str1.intern();
System.out.println(str1 == str2); // true
System.out.println(System.identityHashCode(str1)); // 2003749087
System.out.println(System.identityHashCode(str2)); // 2003749087
各str1とstr2変数はリテラルとnew()演算子で宣言されています。この場合、==
演算子で比較するとfalseが出力されます。
次にstr2をstr1.intern()で再代入すると、str1の参照しているアドレスが代入され、==
演算子で比較するとtrueが出力されます。
しかし、intern()
メソッドにもデメリットはあります。
- まず、Stringオブジェクトを作ります。
- Stringのequals()でString Constant Poolに代入する文字列を探します。(時間がかかる)
- String Constant Poolに保存されると、ガベージコレクションの処理対象から除外されます。(メモリ管理X)
これらを考えながら、コードを書くように気を付けましょう。
以上です。
おわりに
2回目の記事でした。最後までご覧いただきありがとうございます。
ただの興味でいろいろ探して記事を書きましたが、かなりの長文になりました。
勉強すればするほど私のIT知識の足りなさを感じます。毎日少しずつ勉強するほかありませんね。。。
急がずにプログラミングの基礎から学んでいきたいと思います。あと、今より日本語が上手になりますように。。。
次回の記事はString、StringBuilder、StringBufferに関する記事になります。
ありがとうございます。
参考サイト
Oracle Java Silver 黒本
https://truehong.tistory.com/54
https://madplay.github.io/post/java-string-literal-vs-string-object
https://tech.pjin.jp/blog/2020/12/28/java_03_06_string/
https://readystory.tistory.com/139
https://shanepark.tistory.com/330