JavaRush/Java блог/Java Developer/Final, Константи та Immutable в Java
Автор
Aditi Nawghare
Инженер-программист в Siemens

Final, Константи та Immutable в Java

Стаття з групи Java Developer
учасників
Привіт! Слово "модифікатор" тобі вже знайоме. Як мінімум, ти стикався з модифікаторами доступу (public, private) і з модифікатором static. Сьогодні поговоримо про спеціальний модифікатор final. Він, можна сказати, "цементує" ті ділянки нашої програми, де нам потрібна постійна, однозначна поведінка, що не змінюється. Його можна застосовувати на трьох ділянках нашої програми: у класах, методах і змінних. Незмінне в Java: final, константи та Immutable - 2 Пройдемося ними по черзі. Якщо в оголошенні класу стоїть модифікатор final, це означає, що від цього класу не можна успадковувати. У минулих лекціях ми бачили простий приклад успадкування: у нас був батьківський клас Animal, і два класи-нащадки – Cat і Dog
public class Animal {
}

public class Cat extends Animal {
   //..поля і методи класу Cat
}

public class Dog extends Animal {

   //..поля і методи класу Dog
}
Однак якщо ми вкажемо для класу Animal модифікатор final, успадкувати класи Cat і Dog від нього не вийде.
public final class Animal {

}

public class Cat extends Animal {

   //помилка! Не може успадковувати від кінцевої тварини
}
Компілятор одразу ж видає помилку. У Java вже реалізовано багато final-класів. Найвідоміший із тих, якими ти постійно користуєшся, – String. Крім того, якщо клас оголошено як final, усі його методи теж стають final. Що це означає? Якщо для методу вказано модифікатор final – цей метод не можна перевизначити. Наприклад, у нас є клас Animal, у якому визначено метод voice(). Однак собаки і кішки явно "розмовляють" по-різному. Тому в кожному з класів – Cat і Dog – ми створимо метод voice(), але реалізуємо його по-різному.
public class Animal {

   public void voice() {
       System.out.println("Голос!");
   }
}

public class Cat extends Animal {

   @Override
   public void voice() {
       System.out.println("Мяу!");
   }
}

public class Dog extends Animal {

   @Override
   public void voice() {
       System.out.println("Гав!");
   }
}
У класах Cat і Dog ми перевизначили метод батьківського класу. Тепер тварина подаватиме голос залежно від того, об'єктом якого класу вона є:
public class Main {

   public static void main(String[] args) {

       Cat cat = new Cat();
       Собака dog = new Dog();

       cat.voice();
       dog.voice();
   }
}
Виведення:
Мяу! Гав!
Однак якщо в класі Animal ми оголосимо метод voice() як final, перевизначити його в інших класах буде не можна:
public class Animal {

   public final void voice() {
       System.out.println("Голос!");
   }
}


public class Cat extends Animal {

   @Override
   public void voice() {//помилка! final-метод не може бути перевизначений!
       System.out.println("Мяу!");
   }
}
Тоді наші об'єкти будуть змушені користуватися методом voice(), оскільки він визначений у батьківському класі:
public static void main(String[] args) {

   Cat cat = new Cat();
   Собака dog = new Dog();

   cat.voice();
   dog.voice();
}
Виведення:
Голос! Голос!
Тепер щодо final-змінних. Інакше вони називаються константами. По-перше (і що головне), перше значення, присвоєне константі, не можна змінити. Воно присвоюється один раз і назавжди.
public class Main {

   private static final int CONSTANT_EXAMPLE = 333;

   public static void main(String[] args) {

       CONSTANT_EXAMPLE = 999;//помилка! Не можна присвоїти нове значення final-змінній!
   }
}
Константу необов'язково ініціалізувати відразу ж. Це можна зробити і пізніше. Але значення, присвоєне першим, так і залишиться назавжди.
public static void main(String[] args) {

   final int CONSTANT_EXAMPLE;

   CONSTANT_EXAMPLE = 999;//так робити можна
}
По-друге, зверни увагу на назву нашої змінної. Для констант у Java прийнята інша угода про іменування. Це не звичний нам camelCase. У випадку зі звичайною змінною ми б назвали її constantExample, але назви констант пишуться капсом, а між словами (якщо їх кілька) ставиться нижнє підкреслення – "CONSTANT_EXAMPLE". Навіщо потрібні константи? Наприклад, вони стануть у пригоді, якщо ти постійно використовуєш якесь незмінне значення в програмі. Скажімо, ти вирішив увійти в історію і самотужки написати гру "Відьмак 4". У грі вочевидь буде постійно використовуватися ім'я головного героя – "Ґеральт з Рівії". Цей рядок та імена інших героїв краще виділити в константу: потрібне тобі значення зберігатиметься в одному місці, і ти точно не помилишся, друкуючи його в мільйонний раз.
public class TheWitcher4 {

   private static final String GERALT_NAME = "Ґеральт з Рівії";
   private static final String YENNEFER_NAME = "Йеннефер з Венґерберґу";
   private static final String TRISS_NAME = "Трісс Мерігольд";

   public static void main(String[] args) {

       System.out.println("Відьмак 4");
       System.out.println("Це вже четверта частина Відьмака, а " + GERALT_NAME + " ніяк не визначиться хто йому" +
               " подобається більше: " + YENNEFER_NAME + " або " + TRISS_NAME);

       System.out.println("Але якщо ви ніколи не грали у Відьмака – почнемо спочатку.");
       System.out.println("Головного героя звуть " + GERALT_NAME);
       System.out.println(GERALT_NAME + " - відьмак, мисливець на чудовиськ");
   }
}
Виведення:
Відьмак 4 Це вже четверта частина Відьмака, а Ґеральт із Рівії ніяк не визначиться, хто йому подобається більше: Йеннефер з Венґерберґу або Трісс Мерігольд. Але якщо ви ніколи не грали у Відьмака – почнемо спочатку. Головного героя звати Ґеральт з Рівії. Ґеральт з Рівії – відьмак, мисливець на чудовиськ.
Ми виділили імена героїв у константи, і тепер абсолютно точно не помилимося, і не буде необхідності щоразу писати їх руками. Ще один плюс: якщо нам зрештою все-таки потрібно буде змінити значення змінної в усій програмі, достатньо зробити це в одному місці, а не переробляти вручну в усьому коді :)

Immutable-типи

За час роботи на Java ти вже, напевно, звик до того, що програміст практично повністю керує станом усіх об'єктів. Захотів – створив об'єкт Cat. Захотів – перейменував його. Захотів – поміняв вік, або ще що-небудь. Але в Java є кілька типів даних, які відрізняються особливим станом. Вони є незмінними, або Immutable. Це означає, що якщо клас незмінний, стан його об'єктів змінити неможливо. Приклади? Можливо ти здивуєшся, але найвідоміший приклад Immutable-класу – String! Здавалося б, хіба ми не можемо змінити значення рядка? Ну, давай спробуємо:
public static void main(String[] args) {

   String str1 = "Я люблю Java";

   String str2 = str1;// обидві змінні-посилання вказують на один рядок.
   System.out.println(str2);

   str1 = "I love Python";// але поведінка str1 ніяк не впливає на str2
   System.out.println(str2);//str2 продовжує вказувати на рядок "I love Java", хоча str1 уже вказує на інший об'єкт
}
Виведення:
I love Java I love Java
Після того, як ми написали:
str1 = "Я люблю Python";
об'єкт із рядком "I love Java" не змінився і нікуди не подівся. Він благополучно існує і містить всередині себе рівно той самий текст, що й раніше. Код:
str1 = "Я люблю Python";
просто створив ще один об'єкт, і тепер змінна str1 вказує на нього. Але на об'єкт "I love Java" ми ніяк не можемо вплинути. Так, гаразд, давай спробуємо інакше! У класі String повно методів, і деякі з них, схоже, на вигляд змінюють стан рядка! Ось, наприклад, є метод replace(). Давай поміняємо слово "Java" на слово "Python" у нашому рядку!
public static void main(String[] args) {

   String str1 = "Я люблю Java";

   String str2 = str1;// обидві змінні-посилання вказують на один рядок.
   System.out.println(str2);

   str1.replace("Java", "Python");// спробуємо змінити стан str1, замінивши слово "Java" на "Python"
   System.out.println(str2);
}
Виведення:
I love Java I love Java
Знову не вийшло! Може, метод кривий, не працює? Спробуємо інший. Ось, наприклад, substring(). Обрізає рядок за номерами переданих символів. Давай обріжемо наш до перших 10 символів:
public static void main(String[] args) {

   String str1 = "Я люблю Java";

   String str2 = str1;// обидві змінні-посилання вказують на один рядок.
   System.out.println(str2);

   str1.substring(10);//обрізаємо вихідний рядок
   System.out.println(str2);
}
Виведення:
I love Java I love Java
Нічого не змінилося. І не повинно було. Як ми і сказали – об'єкти String незмінні. А що ж тоді всі ці методи класу String? Вони ж можуть обрізати рядок, змінити в ньому символи та інше. Навіщо вони тоді потрібні, якщо нічого не відбувається? Можуть! Але вони водночас щоразу повертають новий об'єкт рядка. Марно писати:
str1.replace("Java", "Python");
– ти не зміниш вихідний об'єкт. Але якщо ти запишеш результат роботи методу в нову змінну-посилання, відразу побачиш різницю!
public static void main(String[] args) {

   String str1 = "Я люблю Java";

   String str2 = str1;// обидві змінні-посилання вказують на один рядок.
   System.out.println(str2);

   String str1AfterReplacement = str1.replace("Java", "Python");
   System.out.println(str2);

   System.out.println(str1AfterReplacement);
}
Тільки так всі ці методи String і працюють. З об'єктом "I love Java" нічого зробити не можна. Тільки створити новий об'єкт і написати: "Новий об'єкт = результат якихось маніпуляцій з об'єктом "I love Java"". Які типи ще належать до Immutable? З того, що тобі залізобетонно потрібно запам'ятати вже зараз, – усі класи-обгортки над примітивними типами – незмінні. Integer, Byte, Character, Short, Boolean, Long, Double, Float – усі ці класи створюють Immutable об'єкти. Сюди ж належать і класи, які використовуються для створення великих чисел, – BigInteger і BigDecimal. Ми нещодавно проходили винятки і торкалися StackTrace. Так ось: об'єкти класу java.lang.StackTraceElement теж незмінні. Це логічно: якби хтось міг змінювати дані нашого стека, це могло б звести нанівець усю роботу з ним. Уяви, що хтось заходить у StackTrace і змінює OutOfMemoryError на FileNotFoundException. А тобі з цим стеком працювати і шукати причину помилки. А програма водночас взагалі не використовує файли :) Тому від гріха подалі ці об'єкти зробили незмінними. Ну, зі StackTraceElement більш-менш зрозуміло. А навіщо комусь знадобилося робити незмінними рядки? У чому проблема, якби можна було змінювати їхні значення. Напевно, навіть зручніше б було :/ Причин тут кілька. По-перше, економія пам'яті. Незмінні рядки можна поміщати в String Pool і використовувати щоразу один і той самий замість створення нових. По-друге, безпека. Наприклад, більшість логінів і паролів у будь-якій програмі – рядки. Можливість їх зміни могла б спричинити проблеми з авторизацією. Є й інші причини, але поки що ми не дійшли до них у вивченні Java – повернемося пізніше.
Коментарі
  • популярні
  • нові
  • старі
Щоб залишити коментар, потрібно ввійти в систему
Для цієї сторінки немає коментарів.