JavaRush /Java блог /Random UA /Топ-50 Java Core питань та відповідей на співбесіді. Част...
Roman Beekeeper
35 рівень

Топ-50 Java Core питань та відповідей на співбесіді. Частина 1

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

Java співбесіда: питання щодо ОВП

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

Відповідь:
  1. ООП концепти:

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

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

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

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

Під наслідуванням мається на увазі, що один клас може успадковувати (" extends ") інший клас. Таким чином можна перевикористовувати код із класу, від якого успадковуються. Існуючий клас відомий як superclass, а створюваний - subclass. Також ще кажуть parentі child.
public class Animal {
   private int age;
}

public class Dog extends Animal {

}
де Animal- це parent, а Dog- child.

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- використовується і для класів. Повноцінний доступ у всьому додатку.
  5. Топ-50 Java Core питань та відповідей на співбесіді.  Частина 1 - 2

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

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

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?

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

public interface Animal {
   void makeSound();
   void eat();
   void sleep();
}
Із цього випливають деякі нюанси:
  • всі методи в інтерфейсі - публічні та абстрактні;
  • всі змінні - 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, з іншого боку, потрібно було залишити те, чим славиться джава – зворотну сумісність. Для цього потрібно було запровадити вже готові рішення до інтерфейсів. Тож до нас і прийшли дефолтні методи. Тобто дефолтний метод — це реалізований метод в інтерфейсі, який має ключове слово 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.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. Що таке абстрактні методи та класи?

У Джава є зарезервоване слово abstract, яке використовується для позначення абстрактних класів та методів. Для початку – визначення. Абстрактним методом називається метод, створений без реалізації з ключовим словом abstractв абстрактному класі. Тобто це метод як в інтерфейсі, тільки з добавкою ключового слова, наприклад:
public abstract void foo();
Абстрактним класом називається клас, який має також abstractслово:
public abstract class A {

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

17. Яка різниця між String, String Builder та String Buffer?

Значення 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Таким чином, ці два значення так і залишаться там. String Buffer:
  • значення Stringзберігаються у стеку (Stack). Якщо значення змінено, то нове значення буде замінено на старе;
  • String Bufferсинхронізований, і тому він є потокобезпечним;
  • через потокобезпеку швидкість роботи залишає бажати кращого.
Приклад:
StringBuffer name = "book";
Топ-50 Java Core питань та відповідей на співбесіді.  Частина 1 - 6Як тільки значення name зміниться, в стеку зміниться значення: Топ-50 Java Core питань та відповідей на співбесіді.  Частина 1 - 7StringBuilder Точно такий же, як і StringBuffer, тільки він не є безпечним. Тому швидкість його явно вища, ніж у StringBuffer.

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

Абстрактний клас:
  • абстрактні класи мають дефолтний конструктор; він викликається щоразу, коли створюється нащадок цього абстрактного класу;
  • містить як абстрактні методи, так і не абстрактні. За великим рахунком, може і не містити абстрактних методів, але все одно бути абстрактним класом;
  • клас, що успадковується від абстрактного, має реалізувати лише абстрактні методи;
  • абстрактний клас може містити Instance Variable (дивися питання №5).
Інтерфейс:
  • не має жодного конструктора та не може бути ініціалізований;
  • лише абстрактні способи би мало бути додані (крім default methods);
  • класи, що реалізують інтерфейс, повинні продати всі способи (крім default methods);
  • інтерфейси можуть містити лише константи.

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

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

А як виходить О(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();
    
    //і після JDK 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 змінної, здається, що це змінні константи. Але це лише на перший погляд. Якщо тип даних, який посилається змінна — immutable, то, так, це константа. Якщо ж тип даних mutable, тобто змінюється, з допомогою методів і змінних можна буде змінити значення об'єкта, який посилається finalзмінна, й у разі назвати її константою не можна. Так ось, на прикладі видно, що частина фінальних змінних справді константи, а частина ні, і їх можна змінити.
public class FinalExample {

   //Незмінні фінальні змінні:
   final static String FINAL_EXAMPLE_NAME = "I'm likely final one";
   final static Integer FINAL_EXAMPLE_COUNT  = 10;

   //змінні фільна змінні
   final List<String> addresses = new ArrayList();
   final StringBuilder finalStringBuilder = new StringBuilder("constant?");
}

Local final змінні

Коли finalзмінна створюється всередині методу, її називають local 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щоразу створюється нова змінна. Тільки це все не стосується нормального циклу for, тому наведений нижче код видасть помилку часу компіляції.
// final local перейена j не може бути призначена
for (final int i = 0; i < args.length; i ++) {
   System.out.println(args[i]);
}

Final клас

Не можна розширювати клас, оголошений як final. Простіше кажучи, ніякий клас не може успадковуватися від цього. Прекрасним прикладом finalкласу в JDK є String. Перший крок до створення незмінного класу - помітити його як finalі тоді не можна буде його розширити:
public final class FinalExample {
}

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

Final методи

Коли метод маркований як final, його називають final метод (логічно, правда?). Final метод не можна перевизначати у класу спадкоємця. До речі, методи в класі Object — wait() та notify() — це 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 змінні для об'єктів, коли ви не хочете, щоб вони були змінені. Наприклад, специфічні для об'єкта властивості, які ми можемо використовувати з метою логування;
  • якщо не потрібно, щоб клас було розширено, відзначити його як остаточний;
  • якщо потрібно створити immutable< клас, потрібно зробити його фінальним;
  • якщо необхідно, щоб реалізація способу не змінювалася у спадкоємцях, позначити спосіб як final. Це дуже важливо, щоб бути впевненим, що реалізація не зміниться.

23. Що таке mutable immutable?

Mutable

Mutable називаються об'єкти, чиї стани та змінні можна змінити після створення. Наприклад, такі класи, як StringBuilder, StringBuffer. Приклад:
public class MutableExample {

   private String address;

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

   public String getAddress() {
       return address;
   }

   // цей сетер може змінити поле name
   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());

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

Immutable

Іммутатів називаються об'єкти, стани та змінні яких не можна змінити після створення об'єкта. Чим не відмінний ключ для HashMap, так?) Наприклад, String, Integer, Double і таке інше. Приклад:
// зробимо цей клас фінальним, щоб ніхто не міг його змінити
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.setName("new address");
       // System.out.println(obj.getName());

   }
}

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

Після того, як з'ясуйте, що таке mutable та immutable об'єкти, наступне питання буде закономірним – як написати його? Щоб написати immutable незмінний клас, слід слідувати простим пунктам:
  • зробити клас фінальним.
  • зробити всі поля приватними та створити тільки гетери до них. Сетери, зрозуміло, не потрібно.
  • Зробити всі mutable поля final, щоб встановити значення можна було лише один раз.
  • ініціалізувати всі поля через конструктор, виконуючи глибоке копіювання (тобто копіюючи і сам об'єкт, і його змінні, і змінні змінних, і так далі)
  • клонувати об'єкти mutable змінних в гетерах, щоб повертати тільки копії значень, а не посилання на актуальні об'єкти.
Приклад:
/**
* Приклад створення іммутального об'єкта.
*/
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;
   }
}
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ