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 关键字可以用两种不同的方式解释,具体取决于它的用途:

值类型:对于ints、doubles等,会保证值不会改变,

引用类型:对于对象的引用,final 确保 引用 永远不会改变,这意味着它将始终引用同一个对象。它不保证被引用的对象内部的值保持不变。

因此,final List&lt;Whatever&gt; 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(不是initializestatic 字段(只要它们不是final)多次你要。 assignmentinitialization 的区别参见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 修饰符强制在对构造函数的调用完成时对引用进行初始化(即,您必须在构造函数中对其进行初始化)。

当你在线初始化一个属性时,它会在你为构造函数定义的代码运行之前被初始化,所以你会得到以下结果:

如果foostatic,则foo = new ArrayList() 将在您为类定义的static 构造函数执行之前执行 如果foo 不是staticfoo = 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”关键字是如何工作的? (我仍然可以修改对象。)的主要内容,如果未能解决你的问题,请参考以下文章

深入理解Java中的final关键字(转)

Java中final关键字如何使用?

Java中final关键字如何使用?

深入理解 Java 中的 final 关键字

浅析Java中的final关键字

浅析Java中的final关键字