Как работает ключевое слово "final" в Java? (Я все еще могу изменять объект.)
В Java мы используем ключевое слово final
с переменными, чтобы указать, что их значения не должны изменяться. Однако я заметил, что можно изменять значение в конструкторе и методах класса. Если же переменная объявлена как static
, то это приводит к ошибке компиляции.
Вот код:
import java.util.ArrayList;
import java.util.List;
class Test {
private final List foo;
public Test() {
foo = new ArrayList();
foo.add("foo"); // Модификация-1
}
public static void main(String[] args) {
Test t = new Test();
t.foo.add("bar"); // Модификация-2
System.out.println("print - " + t.foo);
}
}
Код выше работает нормально и не вызывает ошибок.
Теперь давайте изменим переменную на static
:
private static final List foo;
Теперь возникает ошибка компиляции. Как же на самом деле работает final
?
5 ответ(ов)
Это любимый вопрос на собеседовании. С помощью этого вопроса интервьюер пытается выяснить, насколько хорошо вы понимаете поведение объектов в отношении конструкторов, методов, переменных класса (статических переменных) и переменных экземпляра.
В наши дни интервьюеры также часто задают другой популярный вопрос: что такое effectively final в Java 1.8. Я расскажу о effectively final
в конце.
import java.util.ArrayList;
import java.util.List;
class Test {
private final List foo; // комментарий-1
public Test() {
foo = new ArrayList(); // комментарий-2
foo.add("foo"); // Изменение-1 комментарий-3
}
public void setFoo(List foo) {
//this.foo = foo; Приведет к ошибке на этапе компиляции.
}
}
В приведенном примере мы определили конструктор для класса Test
и предоставили метод setFoo
.
О конструкторе: Конструктор может быть вызван только один раз при создании объекта с использованием ключевого слова new
. Вы не можете вызывать конструктор несколько раз, потому что они не предназначены для этого.
О методе: Метод может вызываться сколько угодно раз (даже ни разу), и компилятор это знает.
Сценарий 1
private final List foo; // 1
foo
является переменной экземпляра. Когда мы создаем объект класса Test
, переменная экземпляра foo
будет скопирована внутрь объекта класса Test
. Если мы назначаем значение final foo
внутри конструктора, компилятор знает, что конструктор будет вызван только один раз, поэтому нет никаких проблем с присваиванием этого значения внутри конструктора.
Если мы назначим значение final foo
внутри метода, компилятор понимает, что метод может быть вызван несколько раз, что означает, что значение придется изменять несколько раз, что для переменной final
не допускается. Поэтому компилятор решает, что конструктор – хороший выбор! Вы можете присвоить значение переменной final только один раз.
Сценарий 2
private static final List foo = new ArrayList();
Теперь foo
является статической переменной. Когда мы создаем экземпляр класса Test
, foo
не будет скопирована в объект, так как foo
статическая. Теперь foo
не является независимым свойством каждого объекта. Это свойство класса Test
. Но foo
может быть видна разными объектами, и если каждый объект Test
, созданный с использованием ключевого слова new
, будет вызывать конструктор Test
, который изменяет значение переменной final static во время создания нескольких объектов (помните, что static foo
не копируется в каждый объект, а делится между несколькими объектами). Чтобы предотвратить это, компилятор знает, что для final static нельзя инициализировать значение внутри конструктора, а также нельзя предоставить метод для его изменения. Поэтому мы должны объявить и инициализировать финальный объект List в том же месте, что и комментарий-1 в приведенной выше программе.
Сценарий 3
t.foo.add("bar"); // Изменение-2
Приведенное выше Изменение-2
относится к вашему вопросу. В данном случае вы не изменяете первый объект, на который ссылается foo
, но добавляете содержимое внутри foo
, что допустимо. Компилятор выдаст ошибку, если вы попытаетесь присвоить new ArrayList()
переменной-ссылке foo
.
Правило: Если вы инициализировали переменную final
, вы не можете изменить ее на ссылку на другой объект. (В данном случае – ArrayList
)
final классы не могут быть подклассированы.
final методы не могут быть переопределены. (Этот метод находится в суперклассе).
final методы могут переопределять. (Читайте это грамматически – этот метод находится в подклассе).
Теперь давайте рассмотрим что такое effectively final в Java 1.8?
public class EffectivelyFinalDemo { // компилируйте код с помощью Java 1.8
public void process() {
int thisValueIsFinalWithoutFinalKeyword = 10; // переменная effectively final
// чтобы работать без ключевого слова final, вы не должны переназначать значение вышеуказанной переменной, как показано ниже
thisValueIsFinalWithoutFinalKeyword = getNewValue(); // удалите эту строку, когда я вам скажу.
class MethodLocalClass {
public void innerMethod() {
// нижеуказанная строка теперь вызывает ошибку компиляции, как показано ниже
// Локальная переменная thisValueIsFinalWithoutFinalKeyword, определенная в внешней области, должна быть final или effectively final
System.out.println(thisValueIsFinalWithoutFinalKeyword); // в этой строке разрешены только финальные переменные, так как это локальный класс метода
// если вы хотите проверить, работает ли effectively final без ключевого слова final, удалите строку, которую я велел удалить в программе выше.
}
}
}
private int getNewValue() {
return 0;
}
}
Приведенная программа вызовет ошибку в Java 1.7 или <1.8, если вы не используете ключевое слово final. Effectively final является частью локальных классов методов. Я понимаю, что вы редко будете использовать такие effectively final в локальных классах методов, но для собеседования мы должны быть готовы.
Вы всегда можете инициализировать переменную final
. Компилятор гарантирует, что вы сможете сделать это только один раз.
Обратите внимание, что вызов методов у объекта, хранящегося в переменной final
, не имеет отношения к семантике final
. Другими словами, final
касается только самой ссылки, а не содержимого ссылаемого объекта.
В Java нет концепции неизменяемости объектов; это достигается путем тщательного проектирования самого объекта, что является далеко не тривиальной задачей.
Ключевое слово final в Java имеет несколько способов использования:
- Финальный класс не может быть наследован.
- Финальный метод не может быть переопределен в подклассах.
- Финальная переменная может быть инициализирована только один раз.
Другие применения:
- Когда анонимный внутренний класс определяется внутри метода, все переменные, объявленные как final в области видимости этого метода, доступны из внутреннего класса.
Статическая переменная класса будет существовать с момента запуска JVM и должна быть инициализирована в самом классе. Если вы сделаете это, сообщение об ошибке не появится.
Ключевое слово final
может интерпретироваться по-разному в зависимости от того, к чему оно применяется:
Типы значений: Для таких типов, как int
, double
и т.д., оно гарантирует, что значение не может измениться.
Ссылочные типы: Для ссылок на объекты final
гарантирует, что ссылка не изменится, то есть она всегда будет указывать на один и тот же объект. Оно не дает никаких гарантий относительно неизменности значений внутри самого объекта.
Таким образом, final List<Whatever> foo;
гарантирует, что foo
всегда будет ссылаться на один и тот же список, но содержимое этого списка может меняться со временем.
Если вы делаете foo
статическим, вы должны инициализировать его в конструкторе класса (или сразу при определении), как в следующих примерах.
Конструктор класса (не экземпляра):
private static final List foo;
static
{
foo = new ArrayList();
}
Сразу при определении:
private static final List foo = new ArrayList();
Проблема заключается не в том, как работает модификатор final
, а в том, как работает модификатор static
.
Модификатор final
требует, чтобы ссылка была инициализирована к моменту завершения вызова вашего конструктора (т.е. вы должны инициализировать её в конструкторе).
Когда вы инициализируете атрибут сразу, он инициализируется до выполнения кода, определенного в конструкторе, так что вы получите следующие результаты:
- если
foo
статический,foo = new ArrayList()
будет выполнен до того, как будет выполнен ваш статический блок инициализацииstatic{}
для класса; - если
foo
не статический,foo = new ArrayList()
будет выполнен до вызова вашего конструктора.
Когда вы не инициализируете атрибут сразу, модификатор final
требует, чтобы вы его инициализировали и чтобы вы делали это в конструкторе. Если у вас также есть модификатор static
, то конструктором, в котором вам нужно инициализировать атрибут, является блок инициализации класса: static{}
.
Ошибка, которую вы получаете в своем коде, возникает из-за того, что static{}
выполняется при загрузке класса, до момента создания экземпляра этого класса. Следовательно, вы не инициализируете foo
на момент создания класса.
Можно рассматривать блок static{}
как конструктор для объекта типа Class
. В этом месте вы должны выполнять инициализацию ваших статических финальных атрибутов класса (если это не сделано сразу).
Кстати:
Модификатор final
гарантирует неизменяемость только для примитивных типов и ссылок.
Когда вы объявляете final
объект, вы получаете final
ссылку на этот объект, но сам объект не является неизменным.
На самом деле, когда вы объявляете финальный атрибут, вы достигаете того, что, как только вы объявите объект для вашей конкретной цели (как финальный List
, который вы объявили), именно этот объект будет использоваться для данной цели: вы не сможете изменить List foo
на другой List
, но вы всё ещё сможете изменять содержимое вашего List
, добавляя или удаляя элементы (используемый вами List
останется тем же, только с изменённым содержимым).
Какова цель использования "final class" в Java?
Изменение приватного статического финального поля с помощью рефлексии в Java
Как объявить массив в одну строку?
Загрузка JDK Java на Linux через wget приводит к отображению страницы лицензии вместо установки
Создание репозитория Spring без сущности