Javaの文字列は不変で、一度宣言されると変更されない

Javaの文字列は不変です。つまり、一度宣言された値はもう変更できません。文字列変数はヒープメモリ上の文字列値への参照を格納しており、これは私たちがプログラムの実行中によく行うように変更することができます。

リテラル文字列が宣言されると、ヒープメモリの特定の場所に保存されます。これを「文字列プール」と呼びます。

これをもっとクリアにするために、私たちのJavaクラスで新しい文字列変数を宣言しましょう。

public class Application {
    public static void main(String[] args) {
        String myFirstString = "Hello";
    }
}

文字列変数を宣言してリテラルを使って初期化すると、私たちのアプリケーションに割り当てられたメモリで何が起こるかです。

画像挿入

もう一つの文字列変数を同じリテラル属性を使って宣言すると、文字列プールに新しい場所は作られません。なぜなら、JVMはすでに「Hello」として宣言された文字列値が存在することを知っているからです。

コード

public class Application {
    public static void main(String[] args) {
        String myFirstString = "Hello";
        String mySecondString = "Hello";
    }
}

メモリを見ると:

画像挿入

はじめに、文字列プールは空です。myFirstStringを宣言した後、JVMは中を見て「Hello」という値を検索します。もしまだ存在しないなら、JVMはその値を格納してアドレスを返します。すでに存在している場合は、その値のアドレスを返します。

myFirstStringmySecondStringが同じメモリの場所を指していることを確認するために、==演算子で比較することができます。これは、同じアドレスを持っていればtrueを返します。

コード

public class Application {
    public static void main(String[] args) {
        String myFirstString = "Hello";
        String mySecondString = "Hello";

        System.out.println("Has same address: " + (myFirstString == mySecondString));
    }
}

結果

Has same address: true

もし両方の値を変えたら、「Hello」はメモリから削除されませんが、そのアドレスではなく、新しく宣言された値で文字列プールに新しい場所が割り当てられるだけです。

コード

public class Application {
    public static void main(String[] args) {
        String myFirstString = "Hello";
        String mySecondString = "Hello";

        myFirstString = "World";
        mySecondString = "World";

        System.out.println("Has same address: " + (myFirstString == mySecondString));
        System.out.println("Has same address as \"Hello\" value: " + (myFirstString == "Hello"));
    }
}

結果

Has same address: true
Has same address as "Hello" value: false

メモリ表現

画像挿入

ここでは、変数の値の変更後も保存されていた値が変わらなかった不変性がわかりますし、新しい値で文字列プールの別の場所が占められたこともわかります。

newキーワードで文字列変数を宣言すると、「Hello」という値は文字列プールではなく、newキーワードで文字列が宣言されるたびにヒープスペースに保存されます。

コード

public class Application {
    public static void main(String[] args) {
        String myLiteralString = "Hello";
        String myNewString = new String("Hello");

        System.out.println("Has same address: " + (myLiteralString == myNewString));
    }
}

結果

Has same address: false

メモリ表現

画像挿入

その値を文字列プールに割り当てるために、String.intern()メソッドを手動で使うことができます。

コード

public class Application {
    public static void main(String[] args) {
        String myLiteralString = "Hello";
        String myNewString = new String("Hello");

        System.out.println("Has same address: " + (myLiteralString == myNewString));

        myNewString = myNewString.intern();
        System.out.println("Has same address after intern: " + (myLiteralString == myNewString));
    }
}

結果

Has same address: false
Has same address after intern: true

メモリ表現

画像挿入

利点

  • 文字列プールの使用により、キャッシュとして機能するためアプリケーションのパフォーマンスが向上しますし、JVMが消費するメモリも少なくなります。
  • 文字列値の再利用性があります。
  • プロジェクトの実行中に変更できない文字列(パスワードなど)に対して、より高いセキュリティを提供します。

欠点

  • 文字列が変更されるたびに、新しい値がメモリに割り当てられ、古い値も存在し続けて余分なスペースを使用します。変化の激しい文字列変数を扱う場合、これはメモリの問題になる可能性があります。

JavaのStringBuilderを使えば、文字列を可変的に使用することが可能です。このリソースを使うと文字列値は文字列プールには保存されず、割り当てられた値は同じアドレスで変更され、再割り当てされることはありません。

もちろん、Immutableな文字列が問題を解決できないケースもあり、パフォーマンスの問題になることもあります。あなたのケースに最適な適用を理解する必要があります

この文字列プールに関するトピックは、ScalerBaeldungCoding with JohnVogellaを基にしています。

以上です。フィードバック、改善点、修正など大歓迎です。

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/marcosgofavaretto/strings-in-java-are-immutable-once-declared-they-cannot-be-changed-4p8p