Java 中的“final”关键字是如何工作的? (我仍然可以修改对象。)
Posted
技术标签:
【中文标题】Java 中的“final”关键字是如何工作的? (我仍然可以修改对象。)【英文标题】:How does the "final" keyword in Java work? (I can still modify an object.) 【发布时间】:2013-03-17 07:22:49 【问题描述】:在 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
到底是如何工作的?
【问题讨论】:
因为 foo 不可见 - 它怎么编译? @therealprashant 这不是真的。私有静态变量是有效的,它们可以从定义它们的类中的静态方法访问。静态变量意味着该变量只存在一次,并且不绑定到类的实例。 @mbdavis 哦,是的!谢谢。但是我仍然不会删除评论来帮助那些像我一样思考的人,然后你的评论会让他们朝着正确的方向思考。 @therealprashant 好的,不用担心! 相关帖子 - What is the equivalent of Java's final in C#? 【参考方案1】:"A final variable can only be assigned once"
*Reflection*
- “哇哇等等,拿着我的啤酒”。
冻结 final
字段在两种情况下发生:
让我们违法
public class HoldMyBeer
final int notSoFinal;
public HoldMyBeer()
notSoFinal = 1;
static void holdIt(HoldMyBeer beer, int yetAnotherFinalValue) throws Exception
Class<HoldMyBeer> cl = HoldMyBeer.class;
Field field = cl.getDeclaredField("notSoFinal");
field.setAccessible(true);
field.set(beer, yetAnotherFinalValue);
public static void main(String[] args) throws Exception
HoldMyBeer beer = new HoldMyBeer();
System.out.println(beer.notSoFinal);
holdIt(beer, 50);
System.out.println(beer.notSoFinal);
holdIt(beer, 100);
System.out.println(beer.notSoFinal);
holdIt(beer, 666);
System.out.println(beer.notSoFinal);
holdIt(beer, 8888);
System.out.println(beer.notSoFinal);
输出:
1
50
100
666
8888
“final” 字段已分配 5 个不同的 “final” 值(注意引号) .而且它可能会不断被分配不同的值......
为什么?因为反射就像 Chuck Norris,如果它想改变一个已初始化的 final 字段的值,它就会这样做。有人说他自己就是将新值推入堆栈的人:
Code:
7: astore_1
11: aload_1
12: getfield
18: aload_1
19: bipush 50 //wait what
27: aload_1
28: getfield
34: aload_1
35: bipush 100 //come on...
43: aload_1
44: getfield
50: aload_1
51: sipush 666 //...you were supposed to be final...
60: aload_1
61: getfield
67: aload_1
68: sipush 8888 //ok i'm out whatever dude
77: aload_1
78: getfield
【讨论】:
还可以查看相关有用的answer。【参考方案2】:值得一提的是一些直截了当的定义:
类/方法
您可以将类的部分或全部方法声明为
final
,以表明该方法不能被子类覆盖。
变量
一旦
final
变量被初始化,它总是包含相同的值。
final
基本上避免被任何东西(子类,变量“重新分配”)覆盖/覆盖,视情况而定。
【讨论】:
我认为关于变量的最终定义有点短; “在 Java 中,当 final 关键字与原始数据类型(int、float、.. 等)的变量一起使用时,变量的值不能更改,但是当 final 与非原始变量一起使用时(请注意,非原始变量在 Java 中总是对对象的引用),被引用对象的成员可以更改。对于非原始变量,final 只是意味着它们不能更改为引用任何其他对象“。 geeksforgeeks.org/g-fact-48 同样有效,特别是作为原始和非原始情况提及。谢了。【参考方案3】:java中的final
关键字用于限制用户。 java final
关键字可以在许多上下文中使用。最终可以是:
-
变量
方法
类
final
关键字可以与变量一起应用,没有值的final
变量称为空白final
变量或未初始化的final
变量。它只能在构造函数中初始化。空白的final
变量可以是static
,也只能在static
块中初始化。
Java 最终变量:
如果您将任何变量设为final
,则您无法更改final
变量的值(它将是常量)。
final
变量示例
有一个final变量speedlimit,我们要改变这个变量的值,但是它不能改变,因为final变量一旦赋值就永远不能改变。
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类的例子
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
方法示例
(Honda 中的 run() 无法覆盖 Bike 中的 run())
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
【讨论】:
【参考方案4】:我想在这里写一个更新和深入的答案。
final
关键字可以在多个地方使用。
-
类
final class
表示没有其他类可以扩展该最终类。当 Java 运行时 (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.
因此,当它执行该对象的任何方法时,该方法不需要在运行时使用 virtual table 解析。即不能应用运行时多态性。所以运行时间并不关心这一点。这意味着它可以节省处理时间,从而提高性能。
-
方法
任何类的final method
意味着扩展该类的任何子类不能覆盖最终方法。所以这个场景中的运行时行为也和我之前提到的类的行为完全一样。
-
字段、局部变量、方法参数
如果上面的任何一种指定为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缓存。因为它不需要一次又一次地从主内存加载回 /em>。否则它会加载到 L2 缓存并不时从主内存加载。所以这也是性能的提升。
所以在以上3个场景中,当我们没有在我们可以使用的地方指定final
关键字时,我们不用担心,compiler optimizations会为我们做这些。编译器优化还为我们做了很多其他的事情。 :)
【讨论】:
【参考方案5】:final
关键字可以用两种不同的方式解释,具体取决于它的用途:
值类型:对于int
s、double
s等,会保证值不会改变,
引用类型:对于对象的引用,final
确保 引用 永远不会改变,这意味着它将始终引用同一个对象。它不保证被引用的对象内部的值保持不变。
因此,final List<Whatever> foo;
确保foo
始终引用同一个列表,但该列表的内容可能会随着时间而改变。
【讨论】:
最后,我正在寻找的答案。【参考方案6】:这是一个最喜欢的面试问题。通过这些问题,面试官试图了解您对构造函数、方法、类变量(静态变量)和实例变量的对象行为的理解程度。
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
)
final 类不能被子类化final 方法不能被覆盖。 (此方法在超类中)final 方法可以覆盖。 (以语法方式阅读。此方法在子类中)
【讨论】:
只是为了清楚。在场景 2 中,如果在 Test 类中设置了foo
并创建了多个 Test 实例,那么尽管最终指定了 foo
,但您是否会多次设置 foo
?
不理解方案 2 的最后一行:但foo
可以是.. 多个对象。) 这是否意味着,如果我一次创建多个对象,那么哪个对象正在初始化最终变量取决于执行?
我认为考虑方案 3 的一种有用方法是将 final
分配给 foo
引用的内存地址,这是一个 ArrayList。您没有将final
分配给foo
的第一个元素(或与此相关的任何元素)引用的内存地址。因此您无法更改foo
,但您可以更改foo[0]
。
@Rawr 就目前而言,场景 2 会导致编译时错误,因为 foo = new ArrayList();
- foo
指的是静态变量,因为我们在同一个类中。
我是一名学习 Java 的 C++ 开发人员。将变量上的 final
视为与 C++ 中的 const
关键字相同是否安全?【参考方案7】:
阅读所有答案。
还有另一个用例可以使用final
关键字,即在方法参数中:
public void showCaseFinalArgumentVariable(final int someFinalInt)
someFinalInt = 9; // won't compile as the argument is final
可用于不应更改的变量。
【讨论】:
【参考方案8】:以下是使用 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 变量可以稍后赋值(声明时不强制赋值),但只能赋值一次。
最终类不能扩展(继承)最终类
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();
【讨论】:
【参考方案9】:假设您有两个钱盒,红色和白色。您只为这些钱盒分配了两个孩子,并且不允许他们互换它们的钱盒。所以你有红色或白色的钱盒(最终)你不能修改盒子但你可以把钱放在你的盒子上。没人在乎(修改-2)。
【讨论】:
【参考方案10】:-
由于最终变量是非静态的,它可以在构造函数中初始化。但是如果你把它设为静态,它就不能被构造函数初始化(因为构造函数不是静态的)。
列表的添加预计不会因列表最终而停止。
final
只是将引用绑定到特定对象。您可以随意更改该对象的“状态”,但不能更改对象本身。
【讨论】:
【参考方案11】:这是一道很好的面试题。有时他们甚至会问你最终对象和不可变对象有什么区别。
1) 当有人提到一个最终对象时,意味着引用不能改变,但它的状态(实例变量)可以改变。
2) 不可变对象是其状态不可更改,但其引用可以更改的对象。 例如:
String x = new String("abc");
x = "BCG";
ref 变量 x 可以更改为指向不同的字符串,但“abc”的值不能更改。
3) 实例变量(非静态字段)在构造函数被调用时被初始化。所以你可以在构造函数中为你的变量初始化值。
4) “但我看到你可以更改类的构造函数/方法中的值”。 -- 你不能在方法中改变它。
5) 在类加载期间初始化一个静态变量。所以你不能在构造函数中初始化,它必须在它之前完成。因此,您需要在声明本身期间为静态变量赋值。
【讨论】:
【参考方案12】:首先,你在代码中初始化(即第一次分配) foo 的位置在这里:
foo = new ArrayList();
foo 是一个对象(类型为 List),因此它是 reference 类型,而不是 value 类型(如 int)。因此,它包含对存储 List 元素的内存位置(例如 0xA7D2A834)的引用。像这样的线条
foo.add("foo"); // Modification-1
不要更改 foo 的值(同样,它只是对内存位置的引用)。相反,他们只是将元素添加到引用的内存位置。要违反 final 关键字,您必须再次尝试重新分配 foo,如下所示:
foo = new ArrayList();
那会给你一个编译错误。
现在,考虑一下添加 static 关键字时会发生什么。
当您没有 static 关键字时,实例化该类的每个对象都有自己的 foo 副本。因此,构造函数将值分配给 foo 变量的一个空白的新副本,这非常好。
但是,当您使用 static 关键字时,与该类关联的内存中只存在一个 foo。如果您要创建两个或更多对象,构造函数每次都会尝试重新分配那个 foo,这违反了 final 关键字。
【讨论】:
【参考方案13】:您始终可以初始化 final
变量。编译器确保您只能执行一次。
请注意,对存储在final
变量中的对象调用方法与final
的语义无关。换句话说:final
仅与引用本身有关,与被引用对象的内容无关。
Java 没有对象不变性的概念;这是通过仔细设计对象来实现的,而且是一项非常艰巨的工作。
【讨论】:
尝试做 t.foo = new ArrayList();在 main 方法中,您将得到编译错误...引用 foo 仅绑定到 ArrayList 的一个最终对象...它不能指向任何其他 ArrayList 嗯。都是关于参考而不是价值。谢谢! 我有一个问题。我认识的人声称“最终”也使变量存储在堆栈中。它是否正确?我到处搜索,找不到任何可以批准或不批准此声明的参考。我搜索了 Java 和 android 文档。还搜索了“Java 内存模型”。也许它在 C/C++ 上以这种方式工作,但我不认为它在 Java 上以这种方式工作。我说的对吗? @androiddeveloper Java 中没有任何东西可以显式控制堆栈/堆的位置。更具体地说,HotSpot JIT 编译器决定的堆栈放置受转义分析的影响,这比检查变量是否为final
要复杂得多。可变对象也可以堆栈分配。 final
字段 可能 有助于逃逸分析,但这是一条相当间接的途径。另请注意,有效最终变量与源代码中标记为final
的变量具有相同的处理方式。
final
存在于类文件中,对优化运行时具有重要的语义影响。它还可能产生成本,因为JLS 对对象的final
字段的一致性有很强的保证。例如,ARM 处理器必须在具有final
字段的类的每个构造函数的末尾使用显式内存屏障指令。但是,在其他处理器上,这不是必需的。【参考方案14】:
final
是Java中用来限制用户的保留关键字,可以应用于成员变量、方法、类和局部变量。在 Java 中,final 变量通常使用 static
关键字声明,并被视为常量。例如:
public static final String hello = "Hello";
当我们在变量声明中使用 final
关键字时,存储在该变量中的值以后不能更改。
例如:
public class ClassDemo
private final int var1 = 3;
public ClassDemo()
...
注意:声明为 final 的类不能扩展或继承(即,不能有超类的子类)。还需要注意的是,声明为 final 的方法不能被子类覆盖。
使用 final 关键字的好处在 this thread 中进行了说明。
【讨论】:
the value stored inside that variable cannot be changed latter
部分正确。只有原始数据类型才适用。如果任何对象被制作为final
,比如arraylist,它的值可以改变,但引用不能改变。谢谢!【参考方案15】:
final关键字有多种使用方式:
最终的类不能被子类化。 最终的方法不能被子类覆盖 最终的变量只能初始化一次其他用法:
在方法体内定义匿名内部类时, 在该方法范围内声明为 final 的所有变量都是 可从内部类中访问一个静态类变量从 JVM 开始就存在,并且应该在类中初始化。如果您这样做,错误消息将不会出现。
【讨论】:
这是迄今为止我最喜欢的答案。简单直接,这是我希望在有关 java 的在线文档中阅读的内容。 那么在静态变量中我们可以初始化多少次? @jorgesaraiva 是的,静态变量不是常量。 @jorgesaraiva 您可以assign(不是initialize)static
字段(只要它们不是final
)多次你要。 assignment 和 initialization 的区别参见this wiki。【参考方案16】:
以上都是正确的。此外,如果您不希望其他人从您的类创建子类,则将您的类声明为 final。然后它成为你的类树层次结构的叶级,没有人可以进一步扩展它。避免巨大的类层次结构是一种很好的做法。
【讨论】:
【参考方案17】:如果您将foo
设为静态,则必须在类构造函数(或定义它的内联)中对其进行初始化,如下例所示。
类构造函数(不是实例):
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
修饰符确保 const 仅适用于原始类型和引用。
当您声明final
对象时,您得到的是对该对象的final
引用,但该对象本身不是常量。
在声明 final
属性时,您真正实现的是,一旦您为特定目的声明了一个对象(如您声明的 final List
),那么只有该对象将用于该目的:您将无法将List foo
更改为另一个List
,但您仍然可以通过添加/删除项目来更改您的List
(您使用的List
将是相同的,只是其内容有所改变) .
【讨论】:
【参考方案18】:final
关键字表示一个变量只能被初始化一次。在您的代码中,您只执行最终的一次初始化,因此满足条款。该语句执行foo
的单独初始化。注意final
!= 不可变,只表示引用不能改变。
foo = new ArrayList();
当您将foo
声明为static final
时,必须在加载类时初始化变量,并且不能依赖实例化(也就是对构造函数的调用)来初始化foo
,因为静态字段必须在没有 a 实例的情况下可用班级。无法保证在使用静态字段之前已经调用了构造函数。
当您在static final
场景下执行您的方法时,Test
类在实例化t
之前加载,此时没有foo
的实例化,这意味着它尚未初始化,因此设置了foo
所有对象的默认值是null
。在这一点上,我假设当您尝试将项目添加到列表时,您的代码会抛出 NullPointerException
。
【讨论】:
【参考方案19】:当你将它设为静态最终时,它应该在静态初始化块中进行初始化
private static final List foo;
static
foo = new ArrayList();
public Test()
// foo = new ArrayList();
foo.add("foo"); // Modification-1
【讨论】:
以上是关于Java 中的“final”关键字是如何工作的? (我仍然可以修改对象。)的主要内容,如果未能解决你的问题,请参考以下文章