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 {

   //ошибка! 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 — вернемся попозже.
Комментарии (132)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
ForJavaRush
Уровень 15
18 июня 2023, 17:25
Вопрос глуппый но задам... Сборщик мусора не сходит сума при работе с immutable? Наример нам приходится в программе таскать 'с собой' масивы строк и паралельно в них менять значения... Это жесть какая нагрузка на железо...
Sergey Zelenkovsky
Уровень 16
15 мая 2021, 05:57
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 Или я что-то не так понял?
Вадим
Уровень 23
19 мая 2021, 20:36
Да, согласен, обьяснение странное, и ты прав при печати str1 конечно будет "I love Python"
Aleksandr
Уровень 41
21 мая 2021, 12:35
когда ты пишешь str1 = "i love java" , выделяется область памяти с этим значением. далее ты пишешь str2 = str1 , тут уже str2 ссылается на ту же область памяти. Далее str1 = "I love Python" , соответственно str2 всё еще ссылается на область памяти где хранится "i love java" , а для "I love Python" уже выделена новая область, на которую теперь ссылается str1. То есть получается , что теперь обе переменных содержат ссылки на разные области памяти. Поскольку String - immutable, то любые операции со строкой будут просто создавать в памяти новые области , куда и будет записываться результат, даже если ты напишешь так: String str1 = "i love Java"; str1 = str1.substring(0,6); Получается str1 будет содержать ссылку на "i love" ,а старое значение останется в памяти , но не будет содержать никакой ссылки на эту область и в итоге удалится сборщиком мусора
SWK
Уровень 26
23 декабря 2021, 04:03
Вот это, чего ты расписал, не расписано в тексте статьи. И это неправильно.
Евгений N
Уровень 23
6 мая 2022, 19:41
вдруг кто-то тоже "Или я что-то не так понял?" 😀 если вместо String взять встречавшийся ранее класс Cat
Cat str1 =  new Cat("I love Java");
Cat str2 = str1;
System.out.println(str2);
str1.name = "I love Python";
System.out.println(str2);
то результат будет I love Java I love Python т.к. у нас всего один кот, на которого 2 ссылки и которому поменяли имя через одну ссылку, а прочитали через вторую.
BotGabe
Уровень 6
Expert
4 марта 2021, 07:06
Ведьмаку заплатите – чеканной монетой, чеканной монетой, во-о-оу Ведьмаку заплатите, зачтется все это вам
24 января 2021, 08:21
Всё что я должен понять из этой статьи: final для класса - класс нельзя наследовать, final для метода - метод нельзя переопределять, final для переменной - нельзя изменять первое присвоенное значение (сразу присваивать не обязательно), имя пишется капсом, слова через нижний пробел. Объекты всех классов обёрток, StackTrace, а также классы, используемые для создания больших чисел BigInteger и BigDecimal неизменяемые. Таким образом, при создании или изменении строки, каждый раз создаётся новый объект. Кратко о String Pool: Строки, указанные в коде литералом, попадают в String Pool (другими словами "Кэш строк"). String Pool создан, чтобы не создавать каждый раз однотипные объекты. Рассмотрим создание двух строковых переменных, которые указаны в коде литералом (без new String).
String test = "literal";
String test2 = "literal";
При создании первой переменной, будет создан объект строка и занесён в String Pool. При создании второй переменной, будет произведён поиск в String Pool. Если такая же строка будет найдена, ссылка на неё будет занесена во вторую переменную. В итоге будет две различных переменных, ссылающихся на один объект.
Игорь
Уровень 14
23 декабря 2020, 11:36
По-моему у них ошибка в выводе должно выводить: Голос! Мяу! Гаф! а во втором случае три раза Голос!
Александр Backend Developer в Газпром Expert
25 декабря 2020, 11:45
Там всё верно. Это же не конструктор чтобы также вызываться неявно. Вызываются только переопределенные методы классов. Исходный метод класса не вызывается. Справедливости ради, то по твоей логике там должно было бы быть: Голос! Мяу! Голос! Гаф!
🦔 Виктор веду учебный тг-канал в t.me/Javangelion Expert
10 ноября 2020, 12:35
Мало примеров и в целом, недосказано. Под конец вскользь упомянут String Pool, а что это не объясняется. Статья озаглавлена какFinal & Co, а по факту пару примеров по строкам, ну, такое... Это называется собирались пироги печь, а по факту лепёшки лепим. В любом случае, конечно, спасибо за труд. Но, гораздо лучше про строки написано здесь: Строки в Java (class java.lang.String). Обработка строк в Java. Часть I: String, StringBuffer, StringBuilder (более детальная статья на Хабре).
bighugеmistеr Android Developer
1 декабря 2020, 02:08
String Pool мы вообще-то проходили
Мирослав
Уровень 29
Expert
8 апреля 2021, 14:31
Степан
Уровень 30
30 октября 2020, 08:02
Ну конечно же Йенифер )
22 октября 2020, 16:01
Получается мы не можем создать поле какого нибудь класса не константой public static final String name = "Амиго"; обязательно только так? => public static final String CHARACTER_NAME = "Амиго"; или можно написать и так и так?
Benzoleum Бит в Байте
29 октября 2020, 12:10
Можно и так, и так, но лучше всегда следовать устоявшемуся стилю. Ведь твой код часто будут читать другие люди, которые так же привыкли к одному формату. К слову, в разных компаниях бывает разный стиль кода. На сколько я знаю, большинство использует Google Style Fromatting, но не все, конечно.
Sergii-K Java Developer в HYCU
19 октября 2020, 21:25
"В прошлых лекциях мы видели простой пример наследования: у нас был родительский класс Animal, и два класса-потомка — Cat и Dog" ?! А была лекция о наследовании?! Может быть я где-то пропустил, поделитесь ссылкой, пожалуйста :)
Igoreek
Уровень 23
20 октября 2020, 14:33
А это уже в следующей ссылке от профессора... https://javarush.com/groups/posts/1947-instanceof-i-osnovih-nasledovanija
Sergii-K Java Developer в HYCU
20 октября 2020, 18:39
Спасибо, эту видел, но там нет "у нас был родительский класс Animal, и два класса-потомка — Cat и Dog", может ещё какая-то была?
Sergii-K Java Developer в HYCU
23 октября 2020, 12:32
вот нашёл лекцию, о которой идёт речь - https://javarush.com/groups/posts/1927-konstruktorih-bazovihkh-klassov--
Daria Software Developer
9 октября 2020, 12:46
Обожечки-кошечки, пример с Ведьмаком 🥰