Как работает ключевое слово «final» в Java? (Я все еще могу изменить объект.)

480

В 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"); // Modification-1
  }
  public static void main(String[] args) 
  {
      Test t = new Test();
      t.foo.add("bar"); // Modification-2
      System.out.println("print - " + t.foo);
  }
}

Выше код работает нормально и без ошибок.

Теперь измените переменную как static:

private static final List foo;

Теперь это ошибка компиляции. Как это finalдействительно работает?

GS
источник
поскольку foo не виден - как он может скомпилироваться?
Бьорн Халльстрем
5
@therealprashant это не правда. Закрытые статические переменные допустимы, они доступны из статических методов внутри класса, в котором они определены. Статическая переменная означает, что переменная существует один раз и не привязана к экземпляру класса.
Мбдэвис
3
@mbdavis О, да! благодарю вас. Но все же я не буду удалять комментарий, чтобы помочь людям, которые думают как я, и тогда ваш комментарий заставит их думать в правильном направлении.
есть
@therealprashant хорошо, не беспокойтесь!
Мбдэвис

Ответы:

518

Вы всегда может инициализировать в finalпеременном. Компилятор гарантирует, что вы можете сделать это только один раз.

Обратите внимание, что вызов методов для объекта, хранящегося в finalпеременной, не имеет ничего общего с семантикой final. Другими словами: finalречь идет только о самой ссылке, а не о содержании ссылочного объекта.

У Java нет понятия неизменности объекта; это достигается путем тщательного проектирования объекта, и это далеко не тривиальная задача.

Марко Топольник
источник
12
попробуйте сделать t.foo = new ArrayList (); в основном методе, и вы получите ошибку компиляции ... ссылка foo привязана только к одному конечному объекту ArrayList ... она не может указывать на любой другой ArrayList
Code2Interface
50
хммм. Это все о ссылке, а не о стоимости. Спасибо!
GS
2
У меня вопрос. Кто-то, кого я знаю, заявил, что «final» также заставляет переменную храниться в стеке. Это правильно? Я искал везде и не смог найти ни одной ссылки, которая могла бы одобрить или отклонить это утверждение. Я искал документацию по Java и Android. Также искали "модель памяти Java". Может быть, это работает так на C / C ++, но я не думаю, что это работает на Java. Я прав?
Android-разработчик
4
@androiddeveloper Ничто в Java не может явно управлять размещением стека / кучи. В частности, размещение стека в соответствии с решением JIT-компилятора HotSpot подлежит экранирующему анализу , который гораздо более сложен, чем проверка наличия переменной final. Изменяемые объекты также могут быть размещены в стеке. finalполя могут помочь в анализе побега, но это довольно косвенный путь. Также обратите внимание, что фактически конечные переменные имеют такую ​​же обработку, что и отмеченные finalв исходном коде.
Марко Топольник
5
finalприсутствует в файле класса и имеет значительные семантические последствия для оптимизации времени выполнения. Это также может повлечь за собой затраты, поскольку JLS имеет надежную гарантию согласованности finalполей объекта. Например, процессор ARM должен использовать явную инструкцию барьера памяти в конце каждого конструктора класса, который имеет finalполя. Однако на других процессорах это не нужно.
Марко Топольник
574

Это любимый вопрос интервью . С помощью этих вопросов интервьюер пытается выяснить, насколько хорошо вы понимаете поведение объектов в отношении конструкторов, методов, переменных класса (статических переменных) и переменных экземпляра.

import java.util.ArrayList;
import java.util.List;

class Test {
    private final List foo;

    public Test() {
        foo = new ArrayList();
        foo.add("foo"); // Modification-1
    }

    public void setFoo(List foo) {
       //this.foo = foo; Results in compile time error.
    }
}

В приведенном выше случае мы определили конструктор для Test и дали ему метод setFoo.

О конструкторе: конструктор можно вызывать только один раз за создание объекта с помощью newключевого слова. Вы не можете вызывать конструктор несколько раз, потому что конструктор не предназначен для этого.

О методе: метод может вызываться столько раз, сколько вы хотите (даже никогда), и компилятор знает это.

Сценарий 1

private final List foo;  // 1

fooэто экземпляр переменной. Когда мы создаем Testобъект класса, тогда переменная экземпляра fooбудет скопирована внутри объекта Testкласса. Если мы присваиваем fooвнутри конструктора, то компилятор знает, что конструктор будет вызываться только один раз, поэтому нет проблем с его назначением внутри конструктора.

Если мы присваиваем fooвнутри метода, компилятор знает, что метод может быть вызван несколько раз, что означает, что значение придется менять несколько раз, что недопустимо для finalпеременной. Таким образом, компилятор решает, что конструктор - хороший выбор! Вы можете присвоить значение конечной переменной только один раз.

Сценарий 2

private static final List foo = new ArrayList();

fooтеперь статическая переменная. Когда мы создаем экземпляр Testкласса, fooне будет скопирован в объект, потому что fooявляется статическим. Теперь fooне является независимым свойством каждого объекта. Это свойство Testкласса. Но fooможет быть видно по нескольким объектам, и если каждый объект, который создается с использованием newключевого слова, в конечном итоге вызовет Testконструктор, который изменяет значение во время создания нескольких объектов (Помните, static fooчто не копируется в каждый объект, но совместно используется несколькими объектами .)

Сценарий 3

t.foo.add("bar"); // Modification-2

Выше Modification-2от вашего вопроса. В приведенном выше случае вы не изменяете первый объект, на который указывает ссылка, но вы добавляете содержимое, fooкоторое разрешено. Компилятор жалуется , если вы пытаетесь присвоить new ArrayList()к fooссылочной переменной.
Правило Если вы инициализировали finalпеременную, то вы не можете изменить ее для ссылки на другой объект. (В этом случае ArrayList)

Финальные классы не могут быть разделены на подклассы.
Финальные методы не могут быть переопределены. (Этот метод в суперклассе),
финальные методы могут переопределять. (Прочтите это грамматически. Этот метод находится в подклассе)

AmitG
источник
1
Просто быть чистым. В Сценарии 2 вы говорите, что fooэто будет установлено несколько раз, несмотря на окончательное обозначение, если fooзадано в классе Test и созданы несколько экземпляров Test?
Rawr
Не поняла последнюю строку для сценария 2: Но fooможет быть .. несколько объектов.) Значит ли это, что если я создаю несколько объектов одновременно, то какой объект инициализирует конечную переменную, зависит от выполнения?
Saumya Suhagiya
1
Я думаю, что полезный способ думать о сценарии 3 - вы назначаете finalадрес памяти, на fooкоторый ссылается ArrayList. Вы не присваиваете finalадрес памяти, на который ссылается первый элемент foo(или любой другой элемент). Поэтому вы не можете измениться, fooно вы можете изменить foo[0].
Пинкертон
@Rawr Сценарий 2 может вызвать ошибку во время компиляции из-за foo = new ArrayList();- fooссылается на статическую переменную, потому что мы находимся внутри одного класса.
flow2k
Я разработчик C ++, изучающий Java. Безопасно ли рассматривать finalпеременную так же, как constключевое слово в C ++?
Даг Барбьери
213

Финальное ключевое слово имеет множество способов использования:

  • Последний класс не может быть разделен на подклассы.
  • Последний метод не может быть переопределен подклассами
  • Последняя переменная может быть инициализирована только один раз

Другое использование:

  • Когда в теле метода определен анонимный внутренний класс, все переменные, объявленные как final в области действия этого метода, доступны из внутреннего класса

Статическая переменная класса будет существовать с самого начала JVM и должна быть инициализирована в классе. Сообщение об ошибке не появится, если вы сделаете это.

czupe
источник
24
Это, безусловно, мой любимый ответ. Просто и понятно, это то, что я ожидаю прочитать в онлайн-документах о Java.
RAnders00
Так что в статических переменных мы можем инициализировать столько раз, сколько захотим?
хорх Saraiva
1
@jorgesaraiva да, статические переменные не являются константами.
Чепе
1
@jorgesaraiva Вы можете назначать (не инициализировать ) staticполя (если их нет final) столько раз, сколько хотите. Посмотрите эту вики для разницы между назначением и инициализацией .
56

finalКлючевое слово может быть истолковано двумя различными способами в зависимости от того, что он используется на:

Типы значений: для ints, doubles и т. Д. Это гарантирует, что значение не может измениться,

Типы ссылок: для ссылок на объекты finalгарантирует, что ссылка никогда не изменится, а это означает, что она всегда будет ссылаться на один и тот же объект. Это не дает никаких гарантий относительно того, что значения в объекте, на который делается ссылка, остаются неизменными.

Таким образом, final List<Whatever> foo;гарантирует, что fooвсегда ссылается на один и тот же список, но содержимое этого списка может меняться со временем.

Smallhacker
источник
23

Если вы делаете foostatic, вы должны инициализировать его в конструкторе класса (или в строке, где вы его определяете), как в следующих примерах.

Конструктор класса (не экземпляр):

private static final List foo;

static
{
   foo = new ArrayList();
}

В линию:

private static final List foo = new ArrayList();

Проблема здесь не в том, как finalработает модификатор, а в том, как staticработает модификатор.

finalМодификатор навязывает инициализацию справки по времени вызова конструктор Завершает (т.е. вы должны инициализировать его в конструкторе).

Когда вы инициализируете атрибут в строке, он инициализируется до запуска кода, который вы определили для конструктора, так что вы получите следующие результаты:

  • если fooесть static, foo = new ArrayList()будет выполнен до того , как будет выполнен static{}конструктор, определенный вами для вашего класса
  • если fooнет static, foo = new ArrayList()будет выполнен до запуска вашего конструктора

Когда вы не инициализируете атрибут в строке, finalмодификатор заставляет его инициализировать и делать это в конструкторе. Если у вас также есть staticмодификатор, конструктор вам придется инициализировать атрибут является блок инициализации класса: static{}.

Ошибка, которую вы получаете в своем коде, связана с тем, что static{}он запускается при загрузке класса, до того, как вы создадите экземпляр объекта этого класса. Таким образом, вы не будете инициализированы fooпри создании класса.

Думайте о static{}блоке как о конструкторе для объекта типа Class. Здесь вы должны выполнить инициализацию static finalатрибутов вашего класса (если это не сделано в строке).

Примечание:

В finalМодификатор уверяет константные-Несс только для примитивных типов и ссылок.

Когда вы объявляете finalобъект, вы получаете final ссылку на этот объект, но сам объект не является константой.

При объявлении finalатрибута вы действительно достигнете того, что, как только вы объявите объект для вашей конкретной цели (например final List, объявленный вами), этот и только этот объект будет использоваться для этой цели: вы не сможете изменить его List fooна другой List, но вы все равно можете изменить свой List, добавив / удалив элементы (то, что Listвы используете, будет таким же, только с измененным содержимым).

lucian.pantelimon
источник
8

Это очень хороший вопрос для интервью. Иногда они могут даже спросить вас, в чем разница между конечным объектом и неизменным объектом.

1) Когда кто-то упоминает конечный объект, это означает, что ссылка не может быть изменена, но ее состояние (переменные экземпляра) может быть изменено.

2) Неизменяемый объект - это объект, состояние которого не может быть изменено, но его ссылка может быть изменена. Пример:

    String x = new String("abc"); 
    x = "BCG";

Переменная x может быть изменена для указания другой строки, но значение «abc» не может быть изменено.

3) Переменные экземпляра (нестатические поля) инициализируются при вызове конструктора. Таким образом, вы можете инициализировать значения переменных внутри конструктора.

4) «Но я вижу, что вы можете изменить значение в конструкторе / методах класса». - Вы не можете изменить это внутри метода.

5) Статическая переменная инициализируется во время загрузки класса. Таким образом, вы не можете инициализировать внутри конструктора, это должно быть сделано еще до этого. Поэтому вам необходимо присвоить значения статической переменной во время самого объявления.

user892871
источник
7

finalКлючевое слово в Java используется для ограничения пользователя. finalКлючевое слово Java может использоваться во многих контекстах. Финал может быть:

  1. переменная
  2. метод
  3. учебный класс

finalКлючевое слово может быть применен с переменными, в finalпеременную , которая не имеет никакого значения, называется пустым finalпеременной или неинициализированным finalпеременной. Его можно инициализировать только в конструкторе. Также finalможет быть пустая переменная, staticкоторая будет инициализирована только в staticблоке.

Конечная переменная Java:

Если вы сделаете любую переменную final, вы не можете изменить значение из finalпеременной (это будет постоянным).

Пример finalпеременной

Есть конечная переменная speedlimit, мы собираемся изменить значение этой переменной, но она не может быть изменена, потому что окончательная переменная после присвоения значения никогда не может быть изменена.

class Bike9{  
    final int speedlimit=90;//final variable  
    void run(){  
        speedlimit=400;  // this will make error
    }  

    public static void main(String args[]){  
    Bike9 obj=new  Bike9();  
    obj.run();  
    }  
}//end of class  

Финальный класс Java:

Если вы сделаете любой класс как final, вы не сможете его расширить .

Пример финального класса

final class Bike{}  

class Honda1 extends Bike{    //cannot inherit from final Bike,this will make error
  void run(){
      System.out.println("running safely with 100kmph");
   }  

  public static void main(String args[]){  
      Honda1 honda= new Honda();  
      honda.run();  
      }  
  }  

Финальный метод Java:

Если вы сделаете какой-либо метод финальным, вы не сможете его переопределить .

Пример finalметода (run () в Honda не может переопределить run () в Bike)

class Bike{  
  final void run(){System.out.println("running");}  
}  

class Honda extends Bike{  
   void run(){System.out.println("running safely with 100kmph");}  

   public static void main(String args[]){  
   Honda honda= new Honda();  
   honda.run();  
   }  
}  

Поделиться с: http://www.javatpoint.com/final-keyword

Али Зиаэ
источник
7

Стоит упомянуть некоторые простые определения:

Классы / Методы

Вы можете объявить некоторые или все методы класса как final, чтобы указать, что метод не может быть переопределен подклассами.

переменные

Как только finalпеременная была инициализирована, она всегда содержит одно и то же значение.

final в основном избегайте перезаписи / надписывания чем-либо (подклассы, переменная «переназначение»), в зависимости от случая.

ivanleoncz
источник
1
Я думаю, что окончательное определение переменных немного короткое; «В Java, когда ключевое слово final используется с переменной примитивных типов данных (int, float и т. Д.), Значение переменной не может быть изменено, но когда final используется с не примитивными переменными (обратите внимание, что не примитивные переменные всегда являются ссылками на объекты в Java), члены упомянутого объекта могут быть изменены. final для не примитивных переменных просто означает, что они не могут быть изменены для ссылки на любой другой объект ". geeksforgeeks.org/g-fact-48
ceyun
Также справедливо, особенно для упоминания как примитивных и не примитивных случаев. Tks.
ivanleoncz
4

finalявляется зарезервированным ключевым словом в Java для ограничения пользователя и может применяться к переменным-членам, методам, классам и локальным переменным. Конечные переменные часто объявляются с staticключевым словом в Java и рассматриваются как константы. Например:

public static final String hello = "Hello";

Когда мы используем final ключевое слово с объявлением переменной, значение, хранящееся внутри этой переменной, не может быть изменено последним.

Например:

public class ClassDemo {
  private final int var1 = 3;
  public ClassDemo() {
    ...
  }
}

Примечание . Класс, объявленный как final, не может быть расширен или унаследован (т. Е. Не может быть подкласса суперкласса). Также хорошо отметить, что методы, объявленные как final, не могут быть переопределены подклассами.

Преимущества использования окончательного ключевого слова рассматриваются в этой теме .

Деста Хайлеселасси Хагос
источник
2
the value stored inside that variable cannot be changed latterчастично верно. Это верно только для примитивных типов данных. В случае, если какой-либо объект сделан так же final, как и arraylist, его значение может измениться, но не ссылка. Спасибо!
GS
3

Предположим, у вас есть две копилки, красная и белая. Вы назначаете эти копилки только двум детям, и им не разрешается менять их ящики. Итак, у вас есть красные или белые копилки (окончательно), вы не можете изменить коробку, но вы можете положить деньги на свою коробку. Никого не волнует (Модификация-2).

Гусейн
источник
2

Прочитайте все ответы.

В другом пользовательском случае finalключевое слово может быть использовано, например, в аргументе метода:

public void showCaseFinalArgumentVariable(final int someFinalInt){

   someFinalInt = 9; // won't compile as the argument is final

}

Может использоваться для переменной, которая не должна быть изменена.

Притам Банерджи
источник
1

Когда вы делаете его статическим финалом, он должен быть инициализирован в статическом блоке инициализации

    private static final List foo;

    static {
        foo = new ArrayList();
    }

    public Test()
    {
//      foo = new ArrayList();
        foo.add("foo"); // Modification-1
    }
Евгений Дорофеев
источник
1

finalКлючевое слово указывает на то, что переменная может быть инициализирован только один раз. В вашем коде вы выполняете только одну инициализацию final, поэтому условия выполняются. Этот оператор выполняет одиночную инициализацию foo. Обратите внимание, что final! = Неизменный, это только означает, что ссылка не может измениться.

foo = new ArrayList();

Когда вы объявляете , fooкак static finalпеременная должна быть инициализирована при загрузке класса и не может полагаться на конкретизации (иначе вызов конструктора) для инициализацииfoo , поскольку статические поля должны быть доступны без экземпляра класса. Нет гарантии, что конструктор был вызван до использования статического поля.

Когда вы выполняете свой метод в соответствии со static finalсценарием, Testкласс загружается до создания экземпляра, tв это время нет экземпляров fooзначения, что он не был инициализирован, поэтому fooдля всех объектов установлено значение по умолчанию null. На данный момент я предполагаю, что ваш код выдает, NullPointerExceptionкогда вы пытаетесь добавить элемент в список.

Кевин Бауэрсокс
источник
1

Прежде всего, место в вашем коде, где вы инициализируете (т.е. присваиваете первый раз) foo, находится здесь:

foo = new ArrayList();

foo - это объект (с типом List), поэтому это ссылочный тип, а не тип значения (например, int). Таким образом, он содержит ссылку на область памяти (например, 0xA7D2A834), где хранятся ваши элементы List. Такие линии

foo.add("foo"); // Modification-1

не меняйте значение foo (опять же, это просто ссылка на ячейку памяти). Вместо этого они просто добавляют элементы в указанное место в памяти. Чтобы нарушить последнее ключевое слово, вам нужно будет снова назначить foo следующим образом:

foo = new ArrayList();

Это будет давать вам ошибку компиляции.


Теперь со всем этим подумайте о том, что происходит, когда вы добавляете ключевое слово static .

Если у вас нет ключевого слова static, каждый объект, который создает экземпляр класса, имеет свою собственную копию foo. Следовательно, конструктор присваивает значение пустой, свежей копии переменной foo, что совершенно нормально.

Однако, когда у вас есть ключевое слово static, в памяти существует только один foo, связанный с классом. Если бы вам нужно было создать два или более объектов, конструктор попытался бы каждый раз переназначать этот foo, нарушая ключевое слово final .

Нико Беллик
источник
1
  1. Поскольку последняя переменная не является статичной, ее можно инициализировать в конструкторе. Но если вы сделаете его статическим, он не может быть инициализирован конструктором (потому что конструкторы не являются статическими).
  2. Добавление в список не должно останавливаться на том, что список будет окончательным. finalпросто привязывает ссылку к определенному объекту. Вы можете изменять «состояние» этого объекта, но не самого объекта.
Анкит
источник
1

Ниже приведены различные контексты, в которых используется final.

Конечные переменные Конечную переменную можно назначить только один раз. Если переменная является ссылкой, это означает, что переменная не может быть повторно связана с ссылкой на другой объект.

class Main {
   public static void main(String args[]){
      final int i = 20;
      i = 30; //Compiler Error:cannot assign a value to final variable i twice
   }
}

окончательной переменной может быть присвоено значение позже (не обязательно для присвоения значения при объявлении), но только один раз.

Финальные классы Финальный класс не может быть расширен (унаследован)

final class Base { }
class Derived extends Base { } //Compiler Error:cannot inherit from final Base

public class Main {
   public static void main(String args[]) {
   }
}

Финальные методы Финальный метод не может быть переопределен подклассами.

//Error in following program as we are trying to override a final method.
class Base {
  public final void show() {
       System.out.println("Base::show() called");
    }
}     
class Derived extends Base {
    public void show() {  //Compiler Error: show() in Derived cannot override
       System.out.println("Derived::show() called");
    }
}     
public class Main {
    public static void main(String[] args) {
        Base b = new Derived();;
        b.show();
    }
}
roottraveller
источник
1

Я думал написать обновленный и подробный ответ здесь.

final Ключевое слово может быть использовано в нескольких местах.

  1. классы

Это final classозначает, что ни один другой класс не может расширить этот последний класс. Когда Java Run Time ( JRE ) знает, что ссылка на объект находится в типе конечного класса (скажем, F), она знает, что значение этой ссылки может быть только в типе F.

Пример:

F myF;
myF = new F();    //ok
myF = someOther;  //someOther cannot be in type of a child class of F.
                  //because F cannot be extended.

Поэтому, когда он выполняет какой-либо метод этого объекта, этот метод не требует разрешения во время выполнения с использованием виртуальной таблицы . то есть полиморфизм во время выполнения не может быть применен. Так что время выполнения не беспокоит об этом. Это означает, что это экономит время обработки, что повышает производительность.

  1. методы

A final methodлюбого класса означает, что любой дочерний класс, расширяющий этот класс, не может переопределить этот последний метод (ы). Таким образом, поведение во время выполнения в этом сценарии также совпадает с предыдущим поведением, которое я упомянул для классов.

  1. поля, локальные переменные, параметры метода

Если кто-либо указал какой-либо вид выше как final, это означает, что значение уже завершено, поэтому значение не может быть изменено .

Пример:

Для полей локальные параметры

final FinalClass fc = someFC; //need to assign straight away. otherwise compile error.
final FinalClass fc; //compile error, need assignment (initialization inside a constructor Ok, constructor can be called only once)
final FinalClass fc = new FinalClass(); //ok
fc = someOtherFC; //compile error
fc.someMethod(); //no problem
someOtherFC.someMethod(); //no problem

Для параметров метода

void someMethod(final String s){
    s = someOtherString; //compile error
}

Это просто означает, что значение finalконтрольного значения не может быть изменено. т.е. разрешена только одна инициализация. В этом сценарии во время выполнения, поскольку JRE знает, что значения нельзя изменить, она загружает все эти окончательные значения (окончательных ссылок) в кэш L1 . Потому что это не нужно , чтобы загрузить обратно снова и снова из основной памяти . В противном случае он загружается в кэш L2 и время от времени загружается из основной памяти. Так что это также улучшение производительности.

Таким образом, во всех вышеупомянутых 3 сценариях, когда мы не указали finalключевое слово в местах, которые мы можем использовать, нам не нужно беспокоиться, оптимизация компилятора сделает это за нас. Есть также много других вещей, которые оптимизация компилятора делает для нас. :)

Supun Wijerathne
источник
0

Прежде всего, это правильно. Кроме того, если вы не хотите, чтобы другие создавали подклассы из вашего класса, объявите ваш класс как final. Затем он становится конечным уровнем иерархии дерева классов, и никто не может его расширить. Это хорошая практика, чтобы избежать огромной иерархии классов.

Шехан Симен
источник