Java プログラミングにおけるイミュータブルな String

JavaBeginner
オンラインで実践に進む

はじめに

この実験では、Java プログラミングにおけるイミュータブルな String を使用する利点について学びます。Java の String がデフォルトでイミュータブルである理由と、この特性がアプリケーション全体のパフォーマンスとセキュリティをどのように向上させるかを理解します。この実験の終わりには、Java プログラムでイミュータブルな String を作成し、効果的に使用できるようになります。

Java プロジェクトファイルの作成

このステップでは、~/project ディレクトリに ImmutableStringDemo.java という名前の新しい Java ファイルを作成します。このファイルには、この実験のすべての Java コードが含まれます。

まず、~/project ディレクトリにいることを確認してください。現在のディレクトリは pwd コマンドを使用して確認できます。

pwd

以下のような出力が表示されるはずです。

/home/labex/project

次に、touch コマンドを使用して ImmutableStringDemo.java ファイルを作成します。

touch ImmutableStringDemo.java

ファイルを作成したら、WebIDE エディタで開いて Java コードの記述を開始できます。

String のイミュータビリティと String Pool の理解

Java では、String オブジェクトはイミュータブル(不変)であり、一度作成されるとその値は変更できません。この設計上の選択は、セキュリティ、スレッド安全性、および String プールによるパフォーマンス最適化など、いくつかの利点を提供します。

String プールは、ヒープ内の特別なメモリ領域であり、String リテラルを格納します。String リテラルを作成すると、Java はまず、プール内に同じ String が既に存在するかどうかを確認します。存在する場合、既存の String の参照が返され、メモリが節約されます。存在しない場合、プール内に新しい String が作成され、その参照が返されます。

基本的な Java クラスの構造を追加し、String の作成を実演しましょう。WebIDE で ImmutableStringDemo.java を開き、以下のコードを追加してください。

// ~/project/ImmutableStringDemo.java
public class ImmutableStringDemo {

    public static void main(String[] args) {
        // String リテラルの作成
        String name = "LabexUser";
        System.out.println("Initial name: " + name);

        // String プールを使用する可能性のある String の作成
        String str1 = "Hello";
        String str2 = "Hello"; // これは String プール内の str1 と同じオブジェクトを参照する可能性が高いです

        System.out.println("str1: " + str1);
        System.out.println("str2: " + str2);
        System.out.println("str1 == str2: " + (str1 == str2)); // 同じオブジェクトを参照しているかを確認します

        // 'new' キーワードを使用して String を作成
        // これは、リテラルがプール内に存在する場合でも、常にヒープ内に新しいオブジェクトを作成します
        String str3 = new String("Hello");
        System.out.println("str3: " + str3);
        System.out.println("str1 == str3: " + (str1 == str3)); // 同じオブジェクトを参照しているかを確認します
    }
}

コードを追加したら、ファイルを保存してください。

次に、Java プログラムをコンパイルして実行し、出力を確認します。ターミナルで、以下のコマンドを実行します。

javac ImmutableStringDemo.java
java ImmutableStringDemo

以下のような出力が表示されるはずです。

Initial name: LabexUser
str1: Hello
str2: Hello
str1 == str2: true
str3: Hello
str1 == str3: false

この出力は、str1str2 が String プール内の同じオブジェクトを参照しているのに対し、str3new String() で作成されたもの)は、内容が同一であってもヒープ内の別のオブジェクトであることを示しています。

セキュリティとスレッドセーフティのためのイミュータビリティの強制

イミュータビリティは、Java におけるセキュリティとスレッドセーフティにとって非常に重要です。String がイミュータブルである場合、その値は作成後にプログラムのどの部分からも変更できません。これにより、悪意のあるコードがパスワードや URL のような String に格納された機密データを改変することを防ぎます。

さらに、イミュータブルなオブジェクトは本質的にスレッドセーフです。複数のスレッドは、外部同期を必要とせずに、イミュータブルな String に同時にアクセスできます。なぜなら、一方のスレッドが String を変更している間に別のスレッドがそれを読み取るリスクがないからです。

String に final キーワードを追加し、簡単なスレッドセーフな例を作成することで、これを実証しましょう。

ImmutableStringDemo.java ファイルを以下のように変更します。クラスレベルで final String を宣言し、ThreadSafeTask クラスからアクセスできるようにします。

// ~/project/ImmutableStringDemo.java
import java.util.HashMap;

public class ImmutableStringDemo {

    // セキュリティとスレッドセーフティのために final String を宣言
    private static final String SECURE_PASSWORD = "MySecurePassword123";

    // スレッドセーフティを実証するための内部クラス
    static class ThreadSafeTask implements Runnable {
        private final String password; // パスワードはイミュータブルな String として渡されます

        public ThreadSafeTask(String password) {
            this.password = password;
        }

        @Override
        public void run() {
            System.out.println("Thread " + Thread.currentThread().getName() + " accessing password: " + password);
            // 一部の処理をシミュレート
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    public static void main(String[] args) {
        // ... (name, str1, str2, str3 の前のコードはここにそのまま残ります) ...
        String name = "LabexUser";
        System.out.println("Initial name: " + name);

        String str1 = "Hello";
        String str2 = "Hello";
        System.out.println("str1: " + str1);
        System.out.println("str2: " + str2);
        System.out.println("str1 == str2: " + (str1 == str2));

        String str3 = new String("Hello");
        System.out.println("str3: " + str3);
        System.out.println("str1 == str3: " + (str1 == str3));

        System.out.println("\n--- Demonstrating Security and Thread Safety ---");

        // final String の使用
        System.out.println("Secure Password: " + SECURE_PASSWORD);

        // イミュータブルなパスワードでスレッドセーフティを実証
        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread(new ThreadSafeTask(SECURE_PASSWORD), "Thread-" + (i + 1));
            thread.start();
        }

        // スレッドの終了を待機 (オプション、出力をよりきれいに表示するため)
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

注意: final キーワードは、SECURE_PASSWORD 変数が一度だけ代入されることを保証します。String 自体がイミュータブルであるため、これにより SECURE_PASSWORD の内容は変更不可能になります。ThreadSafeTask クラスは、このイミュータブルな String に複数のスレッドから安全にアクセスします。

ファイルを保存します。次に、プログラムを再度コンパイルして実行します。

javac ImmutableStringDemo.java
java ImmutableStringDemo

以下のような出力が表示されるはずです。スレッドアクセスに関するメッセージは、決定論的でない順序で表示される場合があります。

Initial name: LabexUser
str1: Hello
str2: Hello
str1 == str2: true
str3: Hello
str1 == str3: false

--- Demonstrating Security and Thread Safety ---
Secure Password: MySecurePassword123
Thread Thread-1 accessing password: MySecurePassword123
Thread Thread-2 accessing password: MySecurePassword123
Thread Thread-3 accessing password: MySecurePassword123

コレクションにおけるハッシュコードキャッシュのためのイミュータビリティの活用

String のイミュータビリティは、HashMapHashSet のようなハッシュベースのコレクションのキーとして使用するのに理想的です。String の値は決して変更されないため、その hashCode() は一度計算してキャッシュすることができます。後続の hashCode() の呼び出しではキャッシュされた値が返され、特に String がキーとして頻繁に使用される場合に、大幅なパフォーマンス向上が期待できます。

もし String がミュータブル(可変)であった場合、HashMap に挿入された後にハッシュコードが変更される可能性があり、元のハッシュコードを使用してオブジェクトを取得することが不可能になります。

HashMap での String の使用を実証するために、ImmutableStringDemo.java にコードを追加しましょう。

スレッドセーフティの実証の後、ImmutableStringDemo.java ファイルの main メソッドに以下のコードを追加してください。

// ~/project/ImmutableStringDemo.java
import java.util.HashMap; // このインポートがファイルの先頭にあることを確認してください

public class ImmutableStringDemo {

    private static final String SECURE_PASSWORD = "MySecurePassword123";

    static class ThreadSafeTask implements Runnable {
        private final String password;

        public ThreadSafeTask(String password) {
            this.password = password;
        }

        @Override
        public void run() {
            System.out.println("Thread " + Thread.currentThread().getName() + " accessing password: " + password);
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    public static void main(String[] args) {
        // ... (name, str1, str2, str3、およびスレッドセーフティに関する前のコードはここにそのまま残ります) ...
        String name = "LabexUser";
        System.out.println("Initial name: " + name);

        String str1 = "Hello";
        String str2 = "Hello";
        System.out.println("str1: " + str1);
        System.out.println("str2: " + str2);
        System.out.println("str1 == str2: " + (str1 == str2));

        String str3 = new String("Hello");
        System.out.println("str3: " + str3);
        System.out.println("str1 == str3: " + (str1 == str3));

        System.out.println("\n--- Demonstrating Security and Thread Safety ---");
        System.out.println("Secure Password: " + SECURE_PASSWORD);

        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread(new ThreadSafeTask(SECURE_PASSWORD), "Thread-" + (i + 1));
            thread.start();
        }
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("\n--- Demonstrating Hashcode Caching ---");

        HashMap<String, Integer> studentScores = new HashMap<>();

        String studentName1 = "Alice";
        String studentName2 = "Bob";
        String studentName3 = "Alice"; // これは String プール内の同じ "Alice" を参照します

        studentScores.put(studentName1, 95);
        studentScores.put(studentName2, 88);
        studentScores.put(studentName3, 92); // これは "Alice" の値を更新します

        System.out.println("Student Scores: " + studentScores);
        System.out.println("Alice's score: " + studentScores.get("Alice"));
        System.out.println("Bob's score: " + studentScores.get("Bob"));

        // String を「変更」しようとしても、新しい String が作成されることを実証します
        String originalString = "Java";
        System.out.println("Original String: " + originalString + ", HashCode: " + originalString.hashCode());

        String modifiedString = originalString.concat(" Programming"); // 新しい String を作成します
        System.out.println("Modified String: " + modifiedString + ", HashCode: " + modifiedString.hashCode());
        System.out.println("Original String (after concat): " + originalString + ", HashCode: " + originalString.hashCode());
    }
}

ファイルを保存します。次に、プログラムを再度コンパイルして実行します。

javac ImmutableStringDemo.java
java ImmutableStringDemo

以下のような出力が表示されるはずです。HashMap とハッシュコードの実証も含まれます。

Initial name: LabexUser
str1: Hello
str2: Hello
str1 == str2: true
str3: Hello
str1 == str3: false

--- Demonstrating Security and Thread Safety ---
Secure Password: MySecurePassword123
Thread Thread-1 accessing password: MySecurePassword123
Thread Thread-2 accessing password: MySecurePassword123
Thread Thread-3 accessing password: MySecurePassword123

--- Demonstrating Hashcode Caching ---
Student Scores: {Bob=88, Alice=92}
Alice's score: 92
Bob's score: 88
Original String: Java, HashCode: 2301506
Modified String: Java Programming, HashCode: -1479700901
Original String (after concat): Java, HashCode: 2301506

concat が呼び出された後でも originalString のハッシュコードが変わらないことに注目してください。これは、concat新しい String オブジェクトを返すため、元の String は変更されないまま残るからです。

まとめ

この実験では、Java プログラミングにおけるイミュータブルな String の基本的な概念について学びました。String がデフォルトでイミュータブルである理由と、この設計上の選択が String プールを通じたメモリ最適化にどのように貢献するかを探りました。また、機密データのセキュリティ強化、並行処理における本質的なスレッドセーフティ、ハッシュコードキャッシュによるハッシュベースのコレクションでのパフォーマンス向上など、イミュータビリティの重要な利点についても理解しました。実践的な演習を完了することで、Java アプリケーションでイミュータブルな String を効果的に作成および利用する実践的な経験を得ることができました。