Как создать неизменяемый класс в Java

Введение

Эта статья предоставляет обзор того, как создать неизменяемый класс в программировании на языке Java.

Объект является неизменяемым, когда его состояние не изменяется после инициализации. Например, String – неизменяемый класс, и, после создания экземпляра, значение объекта String никогда не изменится. Узнайте больше о почему класс String неизменяем в Java.

Поскольку неизменяемый объект не может быть обновлен, программы должны создавать новый объект для каждого изменения состояния. Однако у неизменяемых объектов также есть следующие преимущества:

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

Узнайте больше о многопоточности в Java и просмотрите вопросы с собеседований по многопоточности в Java.

Создание неизменяемого класса в Java

Чтобы создать неизменяемый класс в Java, вам нужно следовать этим общим принципам:

  1. Объявите класс как final, чтобы его нельзя было расширить.
  2. Сделайте все поля private, чтобы не разрешать прямой доступ.
  3. Не предоставляйте методы-установщики для переменных.
  4. Сделайте все изменяемые поля final, чтобы значение поля можно было присвоить только один раз.
  5. Инициализируйте все поля с помощью метода конструктора, выполняющего глубокое копирование.
  6. Выполняйте клонирование объектов в методах получения, чтобы возвращать копию вместо фактической ссылки на объект.

Ниже приведен пример класса, иллюстрирующий основы неизменяемости. Класс FinalClassExample определяет поля и предоставляет конструктор, который использует глубокое копирование для инициализации объекта. Код в методе main файла FinalClassExample.java тестирует неизменяемость объекта.

Создайте новый файл с именем FinalClassExample.java и скопируйте в него следующий код:

FinalClassExample.java
import java.util.HashMap;
import java.util.Iterator;

public final class FinalClassExample {

	// поля класса FinalClassExample
	private final int id;
	
	private final String name;
	
	private final HashMap<String,String> testMap;

	
	public int getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	// Функция-геттер для изменяемых объектов

	public HashMap<String, String> getTestMap() {
		return (HashMap<String, String>) testMap.clone();
	}

	// Метод-конструктор, выполняющий глубокое копирование
	
	public FinalClassExample(int i, String n, HashMap<String,String> hm){
		System.out.println("Performing Deep Copy for Object initialization");

		// Ключевое слово "this" относится к текущему объекту
		this.id=i;
		this.name=n;

		HashMap<String,String> tempMap=new HashMap<String,String>();
		String key;
		Iterator<String> it = hm.keySet().iterator();
		while(it.hasNext()){
			key=it.next();
			tempMap.put(key, hm.get(key));
		}
		this.testMap=tempMap;
	}

	// Тестирование неизменяемого класса

	public static void main(String[] args) {
		HashMap<String, String> h1 = new HashMap<String,String>();
		h1.put("1", "first");
		h1.put("2", "second");
		
		String s = "original";
		
		int i=10;
		
		FinalClassExample ce = new FinalClassExample(i,s,h1);
		
		// вывод значений ce
		System.out.println("ce id: "+ce.getId());
		System.out.println("ce name: "+ce.getName());
		System.out.println("ce testMap: "+ce.getTestMap());
		// изменение значений локальных переменных
		i=20;
		s="modified";
		h1.put("3", "third");
		// снова выводим значения
		System.out.println("ce id after local variable change: "+ce.getId());
		System.out.println("ce name after local variable change: "+ce.getName());
		System.out.println("ce testMap after local variable change: "+ce.getTestMap());
		
		HashMap<String, String> hmTest = ce.getTestMap();
		hmTest.put("4", "new");
		
		System.out.println("ce testMap after changing variable from getter methods: "+ce.getTestMap());

	}

}

Скомпилируйте и запустите программу:

  1. javac FinalClassExample.java
  2. java FinalClassExample

Примечание: При компиляции файла вы можете получить следующее сообщение: Примечание: FinalClassExample.java использует непроверенные или небезопасные операции, потому что метод-геттер использует непроверенное приведение типа от HashMap<String,String> к Object. Вы можете игнорировать предупреждение компилятора для целей этого примера.

Вы получите следующий вывод:

Output
Performing Deep Copy for Object initialization ce id: 10 ce name: original ce testMap: {1=first, 2=second} ce id after local variable change: 10 ce name after local variable change: original ce testMap after local variable change: {1=first, 2=second} ce testMap after changing variable from getter methods: {1=first, 2=second}

Вывод показывает, что значения HashMap не изменились, потому что конструктор использует глубокое копирование, а функция-геттер возвращает клон исходного объекта.

Что происходит, когда вы не используете глубокое копирование и клонирование

Вы можете внести изменения в файл FinalClassExample.java, чтобы показать, что происходит, когда вы используете поверхностное копирование вместо глубокого копирования и возвращаете объект вместо копии. Объект больше не является неизменяемым и может быть изменен. Внесите следующие изменения в примерный файл (или скопируйте и вставьте из примера кода):

  • Удалите метод конструктора, предоставляющий глубокое копирование, и добавьте метод конструктора, предоставляющий поверхностное копирование, который выделен в следующем примере.
  • В функции getter удалите return (HashMap<String, String>) testMap.clone(); и добавьте return testMap;.

Теперь примерный файл должен выглядеть следующим образом:

FinalClassExample.java
import java.util.HashMap;
import java.util.Iterator;

public final class FinalClassExample {

	// поля класса FinalClassExample
	private final int id;
	
	private final String name;
	
	private final HashMap<String,String> testMap;

	
	public int getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	// Функция Getter для изменяемых объектов

	public HashMap<String, String> getTestMap() {
		return testMap;
	}

	// Метод конструктора, выполняющий поверхностное копирование

	public FinalClassExample(int i, String n, HashMap<String,String> hm){
		System.out.println("Performing Shallow Copy for Object initialization");
		this.id=i;
		this.name=n;
		this.testMap=hm;
	}

	// Тестируем неизменяемый класс

	public static void main(String[] args) {
		HashMap<String, String> h1 = new HashMap<String,String>();
		h1.put("1", "first");
		h1.put("2", "second");
		
		String s = "original";
		
		int i=10;
		
		FinalClassExample ce = new FinalClassExample(i,s,h1);
		
		// выводим значения ce
		System.out.println("ce id: "+ce.getId());
		System.out.println("ce name: "+ce.getName());
		System.out.println("ce testMap: "+ce.getTestMap());
		// изменяем значения локальных переменных
		i=20;
		s="modified";
		h1.put("3", "third");
		// выводим значения снова
		System.out.println("ce id after local variable change: "+ce.getId());
		System.out.println("ce name after local variable change: "+ce.getName());
		System.out.println("ce testMap after local variable change: "+ce.getTestMap());
		
		HashMap<String, String> hmTest = ce.getTestMap();
		hmTest.put("4", "new");
		
		System.out.println("ce testMap after changing variable from getter methods: "+ce.getTestMap());

	}

}

Скомпилируйте и запустите программу:

  1. javac FinalClassExample.java
  2. java FinalClassExample

Output
Performing Shallow Copy for Object initialization ce id: 10 ce name: original ce testMap: {1=first, 2=second} ce id after local variable change: 10 ce name after local variable change: original ce testMap after local variable change: {1=first, 2=second, 3=third} ce testMap after changing variable from getter methods: {1=first, 2=second, 3=third, 4=new}

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

Заключение

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

Source:
https://www.digitalocean.com/community/tutorials/how-to-create-immutable-class-in-java