Неизменяемые строки в программировании на Java

JavaBeginner
Практиковаться сейчас

Введение

В этой лабораторной работе вы узнаете о преимуществах использования неизменяемых строк (immutable Strings) в программировании на Java. Вы поймете, почему строки в Java по умолчанию являются неизменяемыми, и как эта характеристика может повысить общую производительность и безопасность ваших приложений. К концу этой лабораторной работы вы сможете создавать и эффективно использовать неизменяемые строки в своих программах на Java.

Создание файла проекта Java

На этом шаге вы создадите новый файл Java с именем ImmutableStringDemo.java в вашем каталоге ~/project. Этот файл будет содержать весь Java-код для данной лабораторной работы.

Сначала убедитесь, что вы находитесь в каталоге ~/project. Вы можете проверить текущий каталог с помощью команды pwd:

pwd

Вы должны увидеть вывод, похожий на этот:

/home/labex/project

Теперь создайте файл ImmutableStringDemo.java с помощью команды touch:

touch ImmutableStringDemo.java

После создания файла вы можете открыть его в редакторе WebIDE, чтобы начать писать ваш Java-код.

Понимание неизменяемости строк и пула строк (String Pool)

В Java объекты String являются неизменяемыми, что означает, что их значение не может быть изменено после создания. Этот выбор дизайна предлагает несколько преимуществ, включая безопасность, потокобезопасность и оптимизацию производительности за счет пула строк (String pool).

Пул строк — это специальная область памяти в куче (heap), которая хранит строковые литералы. Когда вы создаете строковый литерал, Java сначала проверяет, существует ли уже идентичная строка в пуле. Если да, возвращается ссылка на существующую строку, что экономит память. Если нет, в пуле создается новая строка, и возвращается ссылка на нее.

Давайте добавим базовую структуру класса Java и продемонстрируем создание строк. Откройте ImmutableStringDemo.java в WebIDE и добавьте следующий код:

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

    public static void main(String[] args) {
        // Создание строкового литерала
        String name = "LabexUser";
        System.out.println("Initial name: " + name);

        // Создание строк, которые могут использовать пул строк
        String str1 = "Hello";
        String str2 = "Hello"; // Скорее всего, будет ссылаться на тот же объект, что и str1, в пуле строк

        System.out.println("str1: " + str1);
        System.out.println("str2: " + str2);
        System.out.println("str1 == str2: " + (str1 == str2)); // Проверяет, ссылаются ли они на один и тот же объект

        // Создание строки с использованием ключевого слова 'new'
        // Это всегда создает новый объект в куче, даже если литерал существует в пуле
        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

Этот вывод демонстрирует, что str1 и str2 ссылаются на один и тот же объект в пуле строк, в то время как str3 (созданный с помощью new String()) является отдельным объектом в куче, несмотря на то, что его содержимое идентично.

Обеспечение неизменяемости для безопасности и потокобезопасности

Неизменяемость имеет решающее значение для безопасности и потокобезопасности в Java. Когда строка неизменяема, ее значение не может быть изменено какой-либо частью программы после создания. Это предотвращает изменение конфиденциальных данных, хранящихся в строках, таких как пароли или URL-адреса, вредоносным кодом.

Более того, неизменяемые объекты по своей природе потокобезопасны. Несколько потоков могут одновременно получать доступ к неизменяемой строке без необходимости внешней синхронизации, поскольку нет риска того, что один поток изменит строку, пока другой ее читает.

Продемонстрируем это, добавив ключевое слово final к строке и создав простой потокобезопасный пример.

Измените ваш файл ImmutableStringDemo.java следующим образом. Мы объявим final строку на уровне класса, чтобы сделать ее доступной для нашего класса ThreadSafeTask.

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

public class ImmutableStringDemo {

    // Объявление final строки для безопасности и потокобезопасности
    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 ---");

        // Использование final строки
        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 безопасно обращается к этой неизменяемой строке из нескольких потоков.

Сохраните файл. Затем снова скомпилируйте и запустите программу:

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

Использование неизменяемости для кэширования хэш-кодов в коллекциях

Неизменяемость строк делает их идеальными для использования в качестве ключей в хэш-основанных коллекциях, таких как HashMap и HashSet. Поскольку значение строки никогда не изменяется, ее hashCode() может быть вычислен один раз и закэширован. Последующие вызовы hashCode() будут возвращать закэшированное значение, что приведет к значительному повышению производительности, особенно когда строки часто используются в качестве ключей.

Если бы строки были изменяемыми, их хэш-код мог бы измениться после вставки в HashMap, что сделало бы невозможным получение объекта по исходному хэш-коду.

Давайте добавим код в ImmutableStringDemo.java, чтобы продемонстрировать использование строк в HashMap.

Добавьте следующий код в метод main вашего файла ImmutableStringDemo.java после демонстрации потокобезопасности:

// ~/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"; // Будет ссылаться на ту же "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 originalString = "Java";
        System.out.println("Original String: " + originalString + ", HashCode: " + originalString.hashCode());

        String modifiedString = originalString.concat(" Programming"); // Создает новую строку
        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

Обратите внимание, что хеш-код originalString остается прежним даже после вызова concat, поскольку concat возвращает новый объект String, оставляя оригинал нетронутым.

Резюме

В этой лабораторной работе вы изучили фундаментальную концепцию неизменяемых строк в программировании на Java. Вы рассмотрели, почему строки по умолчанию неизменяемы, и как этот выбор дизайна способствует оптимизации памяти через пул строк (String pool). Вы также поняли критические преимущества неизменяемости, включая повышенную безопасность конфиденциальных данных, присущую потокобезопасность для параллельных операций и улучшенную производительность в хэш-основанных коллекциях благодаря кэшированию хеш-кодов. Выполнив практические упражнения, вы получили практический опыт в эффективном создании и использовании неизменяемых строк в ваших Java-приложениях.