Professor Hans Noodles
41 уровень

Неизменное в Java: final, константы и Immutable

Статья из группы Java Developer
Привет! Слово “модификатор” тебе уже знакомо. Неизменное в Java: final, константы и Immutable - 1Как минимум, ты сталкивался с модификаторами доступа (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 {

   //ошибка! Cannot inherit from final 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 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 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 = "I love 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 = "I love Python";
объект со строкой "I love Java" не изменился и никуда не делся. Он благополучно существует и имеет внутри себя ровно тот же текст, что и раньше. Код:

str1 = "I love Python";
просто создал еще один объект, и теперь переменная str1 указывает на него. Но на объект "I love Java" мы никак не можем повлиять. Так, ладно, давай попробуем по-другому! В классе String полно методов, и некоторые из них, похоже с виду меняют состояние строки! Вот, например, есть метод replace(). Давай поменяем слово “Java” на слово “Python” в нашей строке!

public static void main(String[] args) {

   String str1 = "I love 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 = "I love Java";

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

   str1.substring(10);//обрезаем исходную строку
   System.out.println(str2);
}
Вывод: I love Java I love Java Неизменное в Java: final, константы и Immutable - 3 Ничего не поменялось. И не должно было. Как мы и сказали — объекты String неизменяемые. А что же тогда все эти методы класса String? Они же могут обрезать строку, изменить в ней символы и прочее. Зачем они тогда нужны, если ничего не происходит? Могут! Но они при этом каждый раз возвращают новый объект строки. Бесполезно писать:

str1.replace("Java", "Python");
— ты не изменишь исходный объект. Но если ты запишешь результат работы метода в новую переменную-ссылку, сразу увидишь разницу!

public static void main(String[] args) {

   String str1 = "I love 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 — вернемся попозже.
Комментарии (131)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Sergey Zelenkovsky Уровень 16
15 мая 2021

 
public static void main(String[] args) { 
    String str1 = "I love Java";
    String str2 = str1;
    System.out.println(str2);

    str1 = "I love Python"; 
    System.out.println(str2);
}

 
Вывод: I love Java I love Java Честно говоря, не понимаю, что удивительного в этом коде? Код же выполняется сверху вниз. А тут четверть статьи этому посвятили) Я так понимаю, что если я в конце в коде напишу: System.out.println(str1);, то вывод будет: I love Java I love Python Или я что-то не так понял?
BotGabe Уровень 22, Москва, Россия
4 марта 2021
Ведьмаку заплатите – чеканной монетой, чеканной монетой, во-о-оу Ведьмаку заплатите, зачтется все это вам
Александр Огарков Уровень 22, Москва, Россия
24 января 2021
Всё что я должен понять из этой статьи: final для класса - класс нельзя наследовать, final для метода - метод нельзя переопределять, final для переменной - нельзя изменять первое присвоенное значение (сразу присваивать не обязательно), имя пишется капсом, слова через нижний пробел. Объекты всех классов обёрток, StackTrace, а также классы, используемые для создания больших чисел BigInteger и BigDecimal неизменяемые. Таким образом, при создании или изменении строки, каждый раз создаётся новый объект. Кратко о String Pool: Строки, указанные в коде литералом, попадают в String Pool (другими словами "Кэш строк"). String Pool создан, чтобы не создавать каждый раз однотипные объекты. Рассмотрим создание двух строковых переменных, которые указаны в коде литералом (без new String).

String test = "literal"; 
String test2 = "literal";
При создании первой переменной, будет создан объект строка и занесён в String Pool. При создании второй переменной, будет произведён поиск в String Pool. Если такая же строка будет найдена, ссылка на неё будет занесена во вторую переменную. В итоге будет две различных переменных, ссылающихся на один объект.
Игорь Уровень 13, Минск, Белоруссия
23 декабря 2020
По-моему у них ошибка в выводе должно выводить: Голос! Мяу! Гаф! а во втором случае три раза Голос!
🦔 Виктор Уровень 20, Москва, Россия Expert
10 ноября 2020
Мало примеров и в целом, недосказано. Под конец вскользь упомянут String Pool, а что это не объясняется. Статья озаглавлена какFinal & Co, а по факту пару примеров по строкам, ну, такое... Это называется собирались пироги печь, а по факту лепёшки лепим. В любом случае, конечно, спасибо за труд. Но, гораздо лучше про строки написано здесь: Строки в Java (class java.lang.String). Обработка строк в Java. Часть I: String, StringBuffer, StringBuilder (более детальная статья на Хабре).
Степан Уровень 30
30 октября 2020
Ну конечно же Йенифер )
Фаррух Лутфуллаев Уровень 18, Прага, Чехия
22 октября 2020
Получается мы не можем создать поле какого нибудь класса не константой public static final String name = "Амиго"; обязательно только так? => public static final String CHARACTER_NAME = "Амиго"; или можно написать и так и так?
Sergii-K Уровень 24, Ljubljana, Slovenia
19 октября 2020
"В прошлых лекциях мы видели простой пример наследования: у нас был родительский класс Animal, и два класса-потомка — Cat и Dog" ?! А была лекция о наследовании?! Может быть я где-то пропустил, поделитесь ссылкой, пожалуйста :)
Daria Уровень 19
9 октября 2020
Обожечки-кошечки, пример с Ведьмаком 🥰
Aleksandr Уровень 41
26 сентября 2020
Объясните тогда что происходит в этом случае, если String неизменяемый тип.

        String str = "bla bla bla";
        str += " bla";
        System.out.println(str);
вывод:

bla bla bla bla
Я правильно понимаю, что создается новый объект, переменная str теперь ссылается на новый объект, а старый удаляется?