В языке программирования Java ключевое final
слово используется в нескольких контекстах для определения объекта, который может быть назначен только один раз.
После final
присвоения переменной она всегда содержит одно и то же значение. Если final
переменная содержит ссылку на объект, то состояние объекта может быть изменено операциями над объектом, но переменная всегда будет ссылаться на один и тот же объект (это свойство final
называется нетранзитивностью [1] ). Это относится и к массивам, поскольку массивы — это объекты; если final
переменная содержит ссылку на массив, то компоненты массива могут быть изменены операциями с массивом, но переменная всегда будет ссылаться на один и тот же массив. [2]
Последний класс не может быть подклассом . Поскольку это может повысить безопасность и эффективность, многие классы стандартной библиотеки Java являются окончательными, например java.lang.System
и java.lang.String
.
Пример:
публичный финальный класс MyFinalClass {...} публичный класс ThisIsWrong расширяет MyFinalClass {...} // запрещено
Последний метод не может быть переопределен или скрыт подклассами. [3] Это используется для предотвращения неожиданного поведения подкласса, изменяющего метод, который может иметь решающее значение для функции или согласованности класса. [4]
Пример:
public class Base { public void m1 () {...} public Final void m2 () {...} public static void m3 () {...} public static Final void m4 () {...} } public class Derived расширяет Base { public void m1 () {...} // ОК, переопределяем Base#m1() public void m2 () {...} // запрещено public static void m3 () {...} // ОК, скрываем Base#m3() public static void m4 () {...} // запрещено }
Распространенным заблуждением является то, что объявление метода как final
повышает эффективность, позволяя компилятору напрямую вставлять метод везде, где он вызывается (см. встроенное расширение ). Поскольку метод загружается во время выполнения , компиляторы не могут этого сделать. Только среда выполнения и JIT- компилятор точно знают, какие классы были загружены, и поэтому только они могут принимать решения о том, когда встраивать, является ли метод окончательным или нет. [5]
Исключением являются компиляторы машинного кода, которые генерируют непосредственно исполняемый машинный код , специфичный для платформы . При использовании статического связывания компилятор может с уверенностью предположить, что методы и переменные, вычисляемые во время компиляции, могут быть встроены.
Конечную переменную можно инициализировать только один раз либо с помощью инициализатора, либо с помощью оператора присваивания . Ее не нужно инициализировать в момент объявления: это называется «пустой финальной переменной». Пустая конечная переменная экземпляра класса должна быть обязательно назначена в каждом конструкторе класса, в котором она объявлена; аналогично, пустая финальная статическая переменная должна быть обязательно назначена в статическом инициализаторе класса, в котором она объявлена; в противном случае в обоих случаях возникает ошибка времени компиляции. [6] (Примечание: если переменная является ссылкой, это означает, что переменная не может быть повторно привязана для ссылки на другой объект. Но объект, на который она ссылается, по-прежнему является изменяемым , если он изначально был изменяемым.)
В отличие от значения константы , значение конечной переменной не обязательно известно во время компиляции. Хорошей практикой считается представлять конечные константы заглавными буквами, используя подчеркивание для разделения слов. [7]
Пример:
общественный класс Сфера { // pi — универсальная константа, настолько постоянная, насколько это возможно. public static Final Double PI = 3.141592653589793 ; публичный финальный двойной радиус ; публичный финал Double xPos ; публичный финальный двойной yPos ; публичный финальный двойной zPos ; Сфера ( двойной x , двойной y , двойной z , двойной r ) { радиус = r ; хПос = х ; уПос = у ; zPos = z ; } [ ... ] }
Любая попытка переназначить radius
, xPos
, yPos
или zPos
приведет к ошибке компиляции. Фактически, даже если конструктор не устанавливает конечную переменную, попытка установить ее вне конструктора приведет к ошибке компиляции.
Чтобы проиллюстрировать, что окончательность не гарантирует неизменности: предположим, что мы заменяем три позиционные переменные одной:
публичная финальная позиция pos ;
где pos
объект с тремя pos.x
свойствами pos.y
и pos.z
. Тогда pos
нельзя назначить, но можно присвоить три свойства, если только они сами не являются окончательными.
Как и полная неизменяемость , использование финальных переменных имеет большие преимущества, особенно при оптимизации. Например, Sphere
вероятно, будет функция, возвращающая его объем; знание того, что его радиус постоянен, позволяет нам запомнить вычисленный объем. Если у нас относительно мало Sphere
файлов и их объемы нужны нам очень часто, прирост производительности может быть существенным. Создание радиуса a Sphere
final
информирует разработчиков и компиляторов о том, что такого рода оптимизация возможна во всем коде, использующем Sphere
s.
Хотя это и кажется нарушением final
принципа, следующее утверждение является юридическим:
for ( final SomeObject obj : someList ) { // делаем что-нибудь с объектом }
Поскольку переменная obj выходит за пределы области видимости на каждой итерации цикла, она фактически переобъявляется на каждой итерации, позволяя obj
использовать один и тот же токен (т. е.) для представления нескольких переменных. [8]
Конечные переменные можно использовать для построения деревьев неизменяемых объектов. После создания эти объекты гарантированно больше не изменятся. Чтобы добиться этого, неизменяемый класс должен иметь только финальные поля, и эти финальные поля сами могут иметь только неизменяемые типы. Примитивные типы Java неизменяемы, как и строки и некоторые другие классы.
Если приведенная выше конструкция нарушается из-за наличия в дереве объекта, который не является неизменяемым, ожидание не означает, что что-либо, достижимое через конечную переменную, является постоянным. Например, следующий код определяет систему координат, начало которой всегда должно находиться в (0, 0). Источник реализован с использованием метода java.awt.Point
, и этот класс определяет свои поля как общедоступные и изменяемые. Это означает, что даже при достижении origin
объекта по пути доступа только с конечными переменными этот объект все равно можно изменить, как демонстрирует приведенный ниже пример кода.
импортировать java.awt.Point ; публичный класс FinalDemo { статический класс Координатная система { частное начало конечной точки = новая точка ( 0 , 0 ); public Point getOrigin () { return origin ; } } public static void main ( String [] args ) { CoordateSystem координаторная система = новая координатная система (); система координат . getOrigin (). х = 15 ; утвердить систему координат . getOrigin (). получитьX () == 0 ; } }
Причина этого в том, что объявление переменной Final означает лишь то, что эта переменная будет указывать на один и тот же объект в любое время. Однако объект, на который указывает переменная, не находится под влиянием этой последней переменной. В приведенном выше примере координаты x и y начала координат можно свободно изменять.
Чтобы предотвратить эту нежелательную ситуацию, общим требованием является то, что все поля неизменяемого объекта должны быть окончательными, а типы этих полей сами по себе должны быть неизменяемыми. Это лишает права java.util.Date
использовать java.awt.Point
некоторые другие классы в таких неизменяемых объектах.
Когда анонимный внутренний класс определен в теле метода, все переменные, объявленные final
в области этого метода, доступны из внутреннего класса. Для скалярных значений после присвоения значение переменной final
не может измениться. Для значений объекта ссылка не может измениться. Это позволяет компилятору Java «захватывать» значение переменной во время выполнения и сохранять копию как поле во внутреннем классе. Как только внешний метод завершается и его кадр стека удаляется, исходная переменная исчезает, но частная копия внутреннего класса сохраняется в собственной памяти класса.
импортировать javax.swing.* ; публичный класс FooGUI { public static void main ( String [] args ) { // инициализируем компоненты графического интерфейса Final JFrame jf = new JFrame ( «Hello world!» ); //разрешает доступ к jf из тела внутреннего класса jf . add ( new JButton ( "Нажмите на меня" )); // упаковываем и делаем видимыми в потоке Event-Dispatch SwingUtilities . вызываетLater ( new Runnable () { @Override public void run () { jf .pack (); // это была бы ошибка времени компиляции, если бы jf не был окончательным jf .setLocationRelativeTo ( null ) ; jf .setVisible ( true ) ; } }); } }
Пустая переменная Final , появившаяся в Java 1.1, представляет собой конечную переменную, в объявлении которой отсутствует инициализатор. [9] [10] До версии Java 1.1 конечная переменная должна была иметь инициализатор. Пустой финал по определению «финал» может быть назначен только один раз. т. е. он должен быть отменен при назначении. Чтобы сделать это, компилятор Java запускает анализ потока, чтобы гарантировать, что для каждого присвоения пустой конечной переменной переменная определенно не была назначена до присвоения; в противном случае возникает ошибка времени компиляции. [11]
окончательное логическое значение hasTwoDigits ; if ( число >= 10 && число < 100 ) { hasTwoDigits = true ; } if ( число > - 100 && число <= - 10 ) { hasTwoDigits = true ; // ошибка компиляции, поскольку последняя переменная уже может быть назначена. }
Кроме того, перед доступом к нему должен быть обязательно назначен пустой финал. [11]
последнее логическое значение isEven ; если ( число % 2 == 0 ) { isEven = true ; } Система . вне . println ( евен ); // ошибка компиляции, поскольку переменная не была назначена в else-кейсе.
Однако обратите внимание, что нефинальная локальная переменная также должна быть точно назначена перед доступом. [11]
логическое значение isEven ; // *не* финал если ( число % 2 == 0 ) { isEven = true ; } Система . вне . println ( евен ); // Та же ошибка компиляции, поскольку нефинальная переменная не была назначена в else-кейсе.
В C и C++ аналогичной конструкцией является const
ключевое слово . Это существенно отличается от final
Java, главным образом тем, что является квалификатором типа : const
является частью типа , а не только частью идентификатора (переменной). Это также означает, что константность значения может быть изменена путем приведения (явного преобразования типов), в данном случае известного как «константное приведение». Тем не менее, отказ от константности и последующее изменение объекта приводит к неопределенному поведению , если объект был изначально объявлен const
. В Java final
действуют строгие правила, согласно которым невозможно скомпилировать код, который напрямую нарушает или обходит окончательные ограничения. Однако с помощью отражения часто можно изменить конечные переменные. Эта функция чаще всего используется при десериализации объектов с конечными членами.
Кроме того, поскольку C и C++ напрямую предоставляют указатели и ссылки, существует различие между тем, является ли сам указатель константой, и являются ли константами данные, на которые указывает указатель. Применение const
к самому указателю, как в случае SomeClass * const ptr
, означает, что содержимое, на которое имеется ссылка, может быть изменено, но сама ссылка не может быть изменена (без приведения). Такое использование приводит к поведению, имитирующему поведение final
ссылки на переменную в Java. Напротив, при применении const только к ссылочным данным, как в const SomeClass * ptr
, содержимое не может быть изменено (без приведения), но сама ссылка может быть изменена. И ссылка, и содержимое, на которое ссылаются, могут быть объявлены как const
.
C# можно считать похожим на Java с точки зрения особенностей языка и базового синтаксиса: в Java есть JVM, в C# — .Net Framework; В Java есть байт-код, в C# — MSIL; В Java нет поддержки указателей (реальная память), в C# то же самое.
Что касается финального ключевого слова, в C# есть два связанных ключевых слова:
sealed
readonly
[12]Обратите внимание, что ключевое различие между производным ключевым словом C/C++ const
и ключевым словом C# readonly
заключается в том, что const
оно оценивается во время компиляции, а readonly
вычисляется во время выполнения, и, следовательно, может иметь выражение, которое вычисляется и фиксируется только позже (во время выполнения).