Всім привіт, пані та панове Software Engineers! Давайте поговоримо про запитання, які можуть трапитися на співбесіді. Про те, до чого варто готуватися і що потрібно знати. Це чудова нагода повторити вже знайомий матеріал або ж вивчити його з нуля.Топ-50 Java Core питань та відповідей на співбесіді.  Частина 1 - 1У мене вийшла досить об'ємна добірка поширених запитань про ООП, Java Syntax, винятки в Java, колекції та багатопотоковість, яку для зручності я розіб'ю на кілька частин. Важливо: ми розглядатимемо лише версії Java до 8. Усі нововведення, починаючи з Java 9, тут не розглядатимуться. Будь-які ідеї чи зауваження щодо того, як можна покращити відповіді, вітаються. Приємного читання!

Java-співбесіда: запитання з ООП

1. Які особливості має Java?

Відповідь:
  1. Концепції ООП:

    1. об'єктно-орієнтоване програмування;
    2. успадкування;
    3. інкапсуляція;
    4. поліморфізм;
    5. абстракція.
  2. Кросплатформеність: програму, написану на Java, можна запускати на будь-якій платформі без змін. Єдина вимога — наявність встановленої JVM (Java Virtual Machine).

  3. Висока продуктивність: JIT-компілятор (Just-In-Time Compiler) забезпечує високу продуктивність. Він перетворює байт-код на машинний код, після чого JVM виконує програму.

  4. Багатопотоковість: потік виконання представлений класом Thread. JVM автоматично створює потік main. Програміст може створювати додаткові потоки, успадковуючи клас Thread або реалізуючи інтерфейс Runnable.

2. Що таке успадкування?

Під успадкуванням мається на увазі, що один клас може успадковувати (extends) інший клас. Таким чином можна повторно використовувати код класу, від якого успадковуються. Існуючий клас відомий як superclass, а створюваний — subclass. Також ще кажуть parent і child.

public class Animal {
   private int age;
}

public class Dog extends Animal {

}
де Animal — це parent, а Dogchild.

3. Що таке інкапсуляція?

Таке запитання часто трапляється на співбесіді Java-розробника. Інкапсуляція — це приховування реалізації за допомогою модифікаторів доступу, а також гетерів і сетерів. Це робиться для того, щоб закрити доступ для зовнішнього використання в тих місцях, де розробники вважають це за потрібне. Доступний приклад із життя — автомобіль. Ми не маємо прямого доступу до роботи двигуна. Для нас робота полягає в тому, щоб вставити ключ у замок запалювання та запустити двигун. А які процеси відбуватимуться під капотом — не наша справа. Ба більше, наше втручання в цю діяльність може призвести до непередбачуваної ситуації, через яку можна і машину зламати, і нашкодити собі. Так само відбувається і в програмуванні. Добре описано у Вікіпедії. Стаття про інкапсуляцію є і на JavaRush.

4. Що таке поліморфізм?

Поліморфізм — це здатність програми однаково використовувати об'єкти з однаковим інтерфейсом без інформації про конкретний тип цього об'єкта. Як кажуть, один інтерфейс — безліч реалізацій. За допомогою поліморфізму можна об'єднувати та використовувати різні типи об'єктів відповідно до їхньої спільної поведінки. Наприклад, є клас Animal, який має двох нащадків — Dog і Cat. Спільний клас Animal має загальну поведінку для всіх — видавати звук. У випадку, коли потрібно зібрати разом усіх нащадків класу Animal і викликати метод «видавати звук», використовують можливості поліморфізму. Ось як це виглядатиме:

List<Animal> animals = Arrays.asList(new Cat(), new Dog(), new Cat());
animals.forEach(animal -> animal.makeSound());
Отже, поліморфізм допомагає нам. Причому це стосується і поліморфних (перевантажених) методів.

Запитання на співбесіді — Java Syntax

5. Що таке конструктор у Java?

Наступні характеристики є правильними:
  1. Коли створюється новий об'єкт, програма використовує відповідний конструктор.
  2. Конструктор нагадує метод. Його особливість полягає в тому, що він не має типу, що повертається (зокрема й void), а його ім'я збігається з ім'ям класу.
  3. Якщо не визначено жодного конструктора, автоматично буде створено конструктор без параметрів.
  4. Конструктор може бути перевантажений.
  5. Якщо було створено конструктор із параметрами, а потрібен ще й конструктор без параметрів, його необхідно створити окремо, оскільки автоматично він уже не генеруватиметься.

6. Які два класи не успадковуються від Object?

Не ведіться на провокацію — таких класів немає. Усі класи прямо або через своїх предків успадковуються від класу Object!

7. Що таке Local Variable?

Ще одне з популярних запитань на співбесіді Java-розробника. Local Variable (локальна змінна) — це змінна, яка оголошена всередині методу й існує доти, доки виконується цей метод. Щойно виконання методу завершується, локальна змінна перестає існувати. Ось програма, яка використовує локальну змінну helloMessage у методі main():

public static void main(String[] args) {
   String helloMessage;
   helloMessage = "Hello, World!";
   System.out.println(helloMessage);
}

8. Що таке Instance Variable?

Instance Variable — це змінна, яка оголошена всередині класу, і вона існує доти, доки існує об'єкт. Приклад — клас Bee, у якому є дві змінні: nectarCapacity та maxNectarCapacity:

public class Bee {

   /**
    * Current nectar capacity
    */
   private double nectarCapacity;

   /**
    * Maximal nectar that can take bee.
    */
   private double maxNectarCapacity = 20.0;
 
  ...
}

9. Що таке модифікатори доступу?

Модифікатори доступу — це інструмент, за допомогою якого можна налаштувати доступ до класів, методів і змінних. Існують такі модифікатори, упорядковані за зростанням рівня доступу:
  1. private — використовується для методів, полів і конструкторів. Рівень доступу — лише клас, у якому його оголошено.
  2. package-private (default) — може використовуватися для класів. Доступ є лише в межах пакета (package), у якому оголошено клас, метод, змінну або конструктор.
  3. protected — такий самий рівень доступу, як і package-private, але також доступ надається класам, які успадковуються від класу з модифікатором protected.
  4. public — використовується також для класів. Надає повний доступ у всьому застосунку.
Топ-50 Java Core питань та відповідей на співбесіді. Частина 1 - 2

10. Що таке перевизначення методів?

Перевизначення методів відбувається тоді, коли дочірній клас хоче змінити поведінку методу батьківського класу. Якщо потрібно, щоб усе ж виконалася реалізація методу батьківського класу, у дочірньому класі можна використати конструкцію виду super.methodName(), яка виконає метод батьківського класу, після чого можна додати власну логіку. Вимоги, яких потрібно дотримуватися:
  • сигнатура методу має бути однаковою;
  • тип значення, що повертається, має бути таким самим.

11. Що таке сигнатура методу?

Топ-50 Java Core питань та відповідей на співбесіді. Частина 1 - 3Сигнатура методу — це сукупність назви методу та аргументів, які він приймає. Сигнатура методу є унікальним ідентифікатором методу під час перевантаження методів.

12. Що таке перевантаження методів?

Перевантаження методів — це властивість поліморфізму, за якої за допомогою зміни сигнатури методу можна створити різні методи для виконання одних і тих самих дій:
  • одна й та сама назва методу;
  • різні аргументи;
  • може бути різний тип значення, що повертається.
Наприклад, один і той самий метод add() у ArrayList може бути перевантажений таким чином і виконуватиме додавання різними способами залежно від вхідних аргументів:
  • add(Object o) — просто додає об'єкт;
  • add(int index, Object o) — додає об'єкт за вказаним індексом;
  • add(Collection<Object> c) — додає колекцію об'єктів;
  • add(int index, Collection<Object> c) — додає колекцію об'єктів, починаючи із зазначеного індексу.

13. Що таке Interface?

Множинне успадкування не реалізоване в Java, тому для вирішення цієї проблеми були додані інтерфейси в тому вигляді, у якому ми їх знаємо ;) Довгий час інтерфейси містили лише методи без їхньої реалізації. У межах цієї відповіді говоритимемо саме про них. Наприклад:


public interface Animal {
   void makeSound();
   void eat();
   void sleep();
}
Із цього випливають деякі особливості:
  • усі методи в інтерфейсі є public і abstract;
  • усі змінні — public static final;
  • класи не успадковують інтерфейси (extends), а реалізують їх (implements). При цьому клас може реалізовувати будь-яку кількість інтерфейсів;
  • класи, що реалізують інтерфейс, повинні надати реалізацію всіх методів, оголошених в інтерфейсі.
Ось як це виглядає:

public class Cat implements Animal {
   public void makeSound() {
       // реалізація методу
   }

   public void eat() {
       // реалізація
   }

   public void sleep() {
       // реалізація
   }
}

14. Що таке default method в Interface?

Тепер поговоримо про дефолтні методи. Навіщо вони потрібні? Ці методи додали, щоб, як то кажуть, «і вовки ситі, і вівці цілі». Про що йдеться? З одного боку, потрібно було додати нову функціональність: лямбда-вирази та Stream API. З іншого боку, необхідно було зберегти те, чим славиться Java, — зворотну сумісність. Для цього довелося додати вже готову реалізацію методів до інтерфейсів. Так і з'явилися дефолтні методи. Отже, дефолтний метод — це реалізований метод в інтерфейсі, який має ключове слово default. Наприклад, усім відомий метод stream() в інтерфейсі Collection. Перевірте — цей інтерфейс зовсім не такий простий, як здається :). Або ще один не менш відомий метод forEach() з інтерфейсу Iterable. Його також не існувало, доки не були додані дефолтні методи. До речі, про це також можна почитати на JavaRush.

15. А як тоді успадкувати два однакові дефолтні методи?

Виходячи з попередньої відповіді на те, що таке дефолтний метод, можна поставити інше питання. Якщо можна реалізувати методи в інтерфейсах, теоретично можна реалізувати два інтерфейси з однаковим методом, і як таке робити? Є два різні інтерфейси з однаковим методом:

interface A {
   default void foo() {
       System.out.println("Foo A");
   }
}

interface B {
   default void foo() {
       System.out.println("Foo B");
   }
}
І є клас, який реалізує ці два інтерфейси. Щоб не було невизначеності й код скомпілювався, нам потрібно перевизначити метод foo() у класі C, причому можна просто викликати в ньому метод foo() будь-якого з інтерфейсів — A або B. Але як вибрати конкретний метод інтерфейсу A чи B? Для цього є конструкція A.super.foo().

public class C implements A, B {
   @Override
   public void foo() {
       A.super.foo();
   }
}
или:

public class C implements A, B {
   @Override
   public void foo() {
       B.super.foo();
   }
}
Таким чином, метод foo() класу C використовуватиме або дефолтний метод foo() з інтерфейсу A, або метод foo() з інтерфейсу B.

16. Що таке абстрактні методи та класи?

У Java є зарезервоване слово abstract, яке використовується для позначення абстрактних класів і методів. Для початку — визначення. Абстрактним методом називається метод, оголошений без реалізації за допомогою ключового слова abstract в абстрактному класі. Тобто це метод, аналогічний методу в інтерфейсі, але з додаванням ключового слова abstract, наприклад:

public abstract void foo();
Абстрактним класом називається клас, який має також abstractслово:

public abstract class A {

}
Абстрактний клас має кілька особливостей:
  • на його основі не можна створити об'єкт;
  • може містити абстрактні методи;
  • може не містити абстрактних методів.
Абстрактні класи потрібні для узагальнення певної абстракції (даруйте за тавтологію), якої в реальному житті не існує, але яка містить багато спільної поведінки й стану (тобто методів і змінних). Прикладів із життя безліч. Це може бути «тварина», «автомобіль», «геометрична фігура» тощо.

17. Яка різниця між String, StringBuilder і StringBuffer?

Значення String зберігаються в пулі рядків (constant string pool). Щойно буде створено рядок, він з'явиться в цьому пулі. Видалити його звідти не можна. Наприклад:

String name = "book";
...змінна буде посилатися на рядок у пулі рядків (Constant String Pool) Топ-50 Java Core питань та відповідей на співбесіді. Частина 1 - 4Якщо задати змінній name інше значення, вийде наступне:

name = "pen";
Constant string pool Топ-50 Java Core питань та відповідей на співбесіді. Частина 1 - 5Таким чином, ці два значення так і залишаться там.StringBuffer:
  • значення StringBuffer зберігається в купі (Heap). Якщо значення змінюється, нове значення замінює попереднє без створення нового об'єкта;
  • StringBuffer є синхронізованим, тому він потокобезпечний;
  • через синхронізацію працює повільніше, ніж StringBuilder.
Приклад:

StringBuffer name = "book";
Топ-50 Java Core питань та відповідей на співбесіді. Частина 1 - 6Як тільки значення name зміниться, в стеку зміниться значення:Топ-50 Java Core питань та відповідей на співбесіді. Частина 1 - 7StringBuilder майже такий самий, як і StringBuffer, тільки він не є потокобезпечним. Саме тому його швидкодія вища, ніж у StringBuffer.

18. Яка різниця між абстрактним класом та інтерфейсом?

Абстрактний клас:
  • абстрактні класи мають конструктор за замовчуванням; він викликається щоразу, коли створюється нащадок цього абстрактного класу;
  • може містити як абстрактні, так і неабстрактні методи. Ба більше, він може взагалі не містити абстрактних методів, але при цьому залишатися абстрактним класом;
  • клас, що успадковує абстрактний клас, повинен реалізувати лише його абстрактні методи;
  • абстрактний клас може містити instance-змінні (див. питання №8).
Інтерфейс:
  • не має конструктора й не може бути ініціалізований;
  • до Java 8 міг містити лише абстрактні методи (починаючи з Java 8 з'явилися default та static методи);
  • класи, що реалізують інтерфейс, повинні реалізувати всі його абстрактні методи (крім default methods);
  • інтерфейси можуть містити лише константи.

19. Чому доступ до елемента масиву має складність O(1)?

Це запитання мені буквально поставили на останній співбесіді. Як я дізнався пізніше, його ставлять, щоб зрозуміти, як мислить кандидат. Звісно, практична користь від цього знання невелика — достатньо просто знати цей факт. Спочатку потрібно уточнити, що O(1) — це позначення часової складності алгоритму, коли операція виконується за сталий час. Тобто це позначення найшвидшого можливого виконання. Щоб відповісти на це запитання, потрібно зрозуміти, що ми знаємо про масиви. Щоб створити масив int, потрібно написати таке:

int[] intArray = new int[100];
З цього запису можна зробити кілька висновків:
  1. Під час створення масиву відомий його тип. Якщо тип відомий, то зрозуміло, який розмір матиме кожна комірка масиву.
  2. Відомий розмір масиву.
Звідси випливає: щоб зрозуміти, у яку комірку потрібно записати значення, достатньо обчислити, у яку область пам'яті виконати запис. Для машини це найпростіша операція. Вона знає початкову адресу виділеної пам'яті, кількість елементів і розмір однієї комірки. Тому адреса потрібної комірки обчислюється як початкова адреса масиву плюс індекс елемента, помножений на розмір однієї комірки.

А як виходить O(1) під час доступу до об'єктів ArrayList?

Це запитання логічно випливає з попереднього. Адже справді, коли ми працюємо з масивом, у якому зберігаються примітивні типи, ми заздалегідь знаємо розмір кожного елемента. Але що робити, якщо маємо таку схему, як на зображенні: Топ-50 Java Core питань та відповідей на співбесіді. Частина 1 - 8і ми хочемо створити колекцію з елементами, які мають тип A, і додати різні реалізації — B, C, D:

List<A> list = new ArrayList();
list.add(new B());
list.add(new C());
list.add(new D());
list.add(new B());
Як у цій ситуації зрозуміти, яким буде розмір кожної комірки, адже кожен об’єкт може бути різним і мати різні додаткові поля або взагалі бути повністю іншим? Що робити? Тут питання поставлене так, щоб заплутати й спантеличити. Ми ж знаємо, що насправді в колекціях зберігаються не самі об’єкти, а лише посилання на ці об’єкти. А всі посилання мають однаковий розмір, і він відомий. Тому підрахунок місця тут працює так само, як і в попередньому питанні.

21. Автоупаковка (autoboxing) та авторозпаковка (unboxing)

Історична довідка: автоупаковка та авторозпаковка — одне з головних нововведень JDK 5. Автоупаковка (autoboxing) — це процес автоматичного перетворення примітивного типу у відповідний клас-обгортку. Авторозпаковка (unboxing) робить протилежне до автоупаковки — перетворює клас-обгортку на примітив. А от якщо значення обгортки дорівнює null, то під час розпаковки буде викинуто виняток NullPointerException.

Відповідність: примітив — обгортка

Примітив Клас-обгортка
boolean Boolean
int Integer
byte Byte
char Character
float Float
long Long
short Short
double Double

Автоупаковка відбувається:

  • коли примітивному значенню присвоюють посилання на клас-обгортку:

    До Java 5:

    
    // Ручна упаковка, або як це було до Java 5.
    public void boxingBeforeJava5() {
       Boolean booleanBox = new Boolean(true);
       Integer intBox = new Integer(3);
       // і так далі для інших типів
    }
    

    Після Java 5:

    
    // Автоматична упаковка, або як це почало працювати в Java 5.
    public void boxingJava5() {
       Boolean booleanBox = true;
       Integer intBox = 3;
       // і так далі для інших типів
    }
    
  • коли примітивне значення передають як аргумент методу, який очікує клас-обгортку:

    
    public void exampleOfAutoboxing() {
       long age = 3;
       setAge(age);
    }
    
    public void setAge(Long age) {
       this.age = age;
    }
    

Авторозпаковка відбувається:

  • коли об'єкт класу-обгортки присвоюють примітивній змінній:

    
    // До Java 5:
    int intValue = new Integer(4).intValue();
    double doubleValue = new Double(2.3).doubleValue();
    char c = new Character((char) 3).charValue();
    boolean b = Boolean.TRUE.booleanValue();
    
    // Починаючи з Java 5:
    int intValue = new Integer(4);
    double doubleValue = new Double(2.3);
    char c = new Character((char) 3);
    boolean b = Boolean.TRUE;
    
  • У випадках із арифметичними операціями. Вони застосовуються тільки до примітивних типів, для цього потрібно розпаковувати примітив.

    
    // До Java 5
    Integer integerBox1 = new Integer(1);
    Integer integerBox2 = new Integer(2);
    
    // для порівняння потрібно було писати так:
    integerBox1.intValue() > integerBox2.intValue()
    
    // Починаючи з Java 5
    integerBox1 > integerBox2
    
  • коли передають в обгортку метод, який приймає відповідний примітив:

    
    public void exampleOfAutoboxing() {
       Long age = new Long(3);
       setAge(age);
    }
    
    public void setAge(long age) {
       this.age = age;
    }
    

22. Що таке ключове слово final і де його використовують?

Ключове слово final можна використовувати для змінних, методів і класів.
  1. final змінну не можна перепризначити іншому об'єкту.
  2. final клас не можна успадковувати.
  3. final метод не можна перевизначити в класі-нащадку.
Ми коротко розглянули основні особливості, а тепер обговоримо їх докладніше.

final змінні

Java надає два способи створити змінну та присвоїти їй значення:
  1. Можна оголосити змінну, а ініціалізувати її пізніше.
  2. Можна оголосити змінну й одразу її ініціалізувати.
Приклади використання final змінних у цих випадках:

public class FinalExample {

   //статическая переменная final, которая сразу же инициализируется:
   final static String FINAL_EXAMPLE_NAME = "I'm likely final one";

   //final переменная, которая не инициализирована, но работать будет только если
   //инициализировать это в конструкторе:
   final long creationTime;

   public FinalExample() {
       this.creationTime = System.currentTimeMillis();
   }

   public static void main(String[] args) {
       FinalExample finalExample = new FinalExample();
       System.out.println(finalExample.creationTime);

       // final поле FinalExample.FINAL_EXAMPLE_NAME не может быть заасайнено
//    FinalExample.FINAL_EXAMPLE_NAME = "Not you're not!";

       // final поле Config.creationTime не может быть заасайнено
//    finalExample.creationTime = 1L;
   }
}

Чи можна вважати final змінну константою?

Оскільки final змінній не можна присвоїти нове значення, може здатися, що будь-яка final змінна є константою. Але це не зовсім так. Якщо тип даних, на який посилається змінна, є immutable (незмінним), тоді так — таку змінну можна вважати константою. Якщо ж тип даних є mutable (змінним), то за допомогою методів цього об'єкта можна змінити його внутрішній стан. Тобто, хоча final змінну не можна перепризначити іншому об'єкту, сам об'єкт, на який вона посилається, може змінюватися. У такому разі називати таку змінну константою не можна. Як видно з прикладу, лише частина final змінних є справжніми константами, тоді як інші посилаються на змінні об'єкти, стан яких можна змінювати.

public class FinalExample {

   // final змінні, що посилаються на незмінні об'єкти:
   final static String FINAL_EXAMPLE_NAME = "I'm likely final one";
   final static Integer FINAL_EXAMPLE_COUNT = 10;

   // final змінні, що посилаються на змінні об'єкти:
   final List<String> addresses = new ArrayList();
   final StringBuilder finalStringBuilder = new StringBuilder("constant?");
}

Локальні final-змінні

Коли final змінну створюють усередині методу, її називають локальною final змінною:

public class FinalExample {

   public static void main(String[] args) {
       // Так можна
       final int minAgeForDriveCar = 18;

       // А можна й так — у циклі foreach:
       for (final String arg : args) {
           System.out.println(arg);
       }
   }

}
Ми можемо використовувати ключове слово final у розширеному циклі for, оскільки після завершення кожної ітерації цього циклу щоразу створюється нова змінна. Проте це не стосується звичайного циклу for. Тому наведений нижче код призведе до помилки компіляції.

// Локальній final змінній i не можна присвоїти нове значення
for (final int i = 0; i < args.length; i++) {
   System.out.println(args[i]);
}

final клас

Не можна успадковувати клас, оголошений як final. Простіше кажучи, жоден клас не може бути нащадком такого класу. Чудовим прикладом final класу в JDK є String. Один із перших кроків під час створення незмінного (immutable) класу — оголосити його як final. Тоді жоден інший клас не зможе успадковуватися від нього:

public final class FinalExample {
}

// Тут виникне помилка компіляції
class WantsToInheritFinalClass extends FinalExample {
}

final методи

Коли метод оголошено з модифікатором final, його називають final методом. Такий метод не можна перевизначити в класі-нащадку. До речі, хорошим прикладом final методу в класі Object є getClass(). Оскільки він оголошений як final, його не можна перевизначити.

public class FinalExample {
   public final String generateAddress() {
       return "Some address";
   }
}

class ChildOfFinalExample extends FinalExample {

   // Тут виникне помилка компіляції
   @Override
   public String generateAddress() {
       return "My OWN Address";
   }
}

Як і де використовувати final у Java

  • використовуйте ключове слово final для оголошення констант рівня класу;
  • використовуйте final змінні для об'єктів, якщо не хочете, щоб їх можна було перепризначити. Наприклад, для властивостей об'єкта, які використовуються під час логування;
  • якщо не потрібно, щоб клас можна було успадковувати, оголосіть його як final;
  • якщо потрібно створити immutable клас, одним із перших кроків є оголосити його як final;
  • якщо потрібно, щоб реалізація методу не змінювалася в класах-нащадках, оголосіть цей метод як final. Це гарантує, що його реалізація не буде перевизначена.

23. Що таке mutable та immutable?

Mutable

Mutable називають об'єкти, стан яких можна змінити після створення. Тобто після створення такого об'єкта можна змінювати його поля або внутрішній стан без створення нового об'єкта. Прикладами mutable класів є StringBuilder та StringBuffer. Приклад:

public class MutableExample {

   private String address;

   public MutableExample(String address) {
       this.address = address;
   }

   public String getAddress() {
       return address;
   }

   // Цей сетер може змінити поле address
   public void setAddress(String address) {
       this.address = address;
   }

   public static void main(String[] args) {

       MutableExample obj = new MutableExample("first address");
       System.out.println(obj.getAddress());

       // Оновлюємо поле address, отже це mutable об'єкт
       obj.setAddress("Updated address");
       System.out.println(obj.getAddress());
   }
}

Immutable

Immutable називають об'єкти, стан яких не можна змінити після створення. Будь-яка спроба змінити такий об'єкт призводить до створення нового об'єкта, а не до зміни існуючого. Саме тому immutable об'єкти чудово підходять для використання як ключі в HashMap. Прикладами immutable класів є String, Integer, Double та інші. Приклад:

// Зробимо цей клас final, щоб його не можна було успадкувати
public final class ImmutableExample {

   private String address;

   ImmutableExample(String address) {
       this.address = address;
   }

   public String getAddress() {
       return address;
   }

   // Видаляємо сетер

   public static void main(String[] args) {

       ImmutableExample obj = new ImmutableExample("old address");
       System.out.println(obj.getAddress());

       // Тому це поле неможливо змінити, отже це immutable об'єкт
       // obj.setAddress("new address");
       // System.out.println(obj.getAddress());

   }
}

24. Як написати immutable клас?

Після того як ми з'ясували, що таке mutable та immutable об'єкти, закономірно виникає наступне запитання: як написати immutable клас? Щоб створити immutable клас, потрібно дотримуватися таких правил:
  • оголосити клас як final;
  • зробити всі поля private і надати до них лише гетери. Сетери створювати не потрібно;
  • зробити всі поля final, щоб їх можна було ініціалізувати лише один раз;
  • ініціалізувати всі поля через конструктор. Якщо поле є mutable, слід виконати глибоке копіювання (тобто скопіювати сам об'єкт, його поля, вкладені об'єкти тощо);
  • якщо гетери повертають mutable об'єкти, необхідно повертати їх копії, а не посилання на самі об'єкти.
Приклад:

/**
* Приклад створення immutable об'єкта.
*/
public final class FinalClassExample {

   private final int age;

   private final String name;

   private final HashMap<String, String> addresses;

   public int getAge() {
       return age;
   }

   public String getName() {
       return name;
   }

   /**
    * Створюємо копію об'єкта перед тим, як повернути його.
    */
   public HashMap<String, String> getAddresses() {
       return (HashMap<String, String>) addresses.clone();
   }

   /**
    * У конструкторі виконуємо глибоке копіювання для mutable об'єктів.
    */
   public FinalClassExample(int age, String name, HashMap<String, String> addresses) {
       System.out.println("Виконуємо глибоке копіювання в конструкторі");
       this.age = age;
       this.name = name;
       HashMap<String, String> temporaryMap = new HashMap<>();
       String key;
       Iterator<String> iterator = addresses.keySet().iterator();
       while (iterator.hasNext()) {
           key = iterator.next();
           temporaryMap.put(key, addresses.get(key));
       }
       this.addresses = temporaryMap;
   }
}