Введение
В этой лабораторной работе вы узнаете о преимуществах использования неизменяемых строк (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-приложениях.



