Java中的"final"关键字是如何工作的?(我仍然可以修改一个对象。)

561
在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不可见,它怎么可能编译通过呢? - Björn Hallström
12
@therealprashant 这并不正确。私有静态变量是有效的,它们可以从定义它们的类中的静态方法中访问。静态变量意味着该变量存在一次,并且不绑定到类的实例。 - mbdavis
3
@mbdavis 哦,是的!谢谢你。但我还是不会删除这条评论,来帮助那些和我有同样想法的人,而你的评论将帮助他们朝着正确的方向思考。 - therealprashant
@therealprashant 好的,不用担心! - mbdavis
20个回答

645

这是一个最喜欢的面试问题。通过这个问题,面试官试图了解你对于构造函数、方法、类变量(静态变量)和实例变量的行为理解程度。
现在,面试官们还在问另一个最喜欢的问题——什么是Java 1.8中的“有效final”
最后我会解释一下Java 1.8中的effectively final

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变量不允许这样。因此编译器决定使用构造函数!您只能为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方法可以覆盖。 (以语法方式阅读。此方法位于子类中)

现在让我们看看在Java 1.8中什么是有效的final

public class EffectivelyFinalDemo { //compile code with java 1.8
    public void process() {
        int thisValueIsFinalWithoutFinalKeyword = 10; //variable is effectively final
        
        //to work without final keyword you should not reassign value to above variable like given below 
        thisValueIsFinalWithoutFinalKeyword = getNewValue(); // delete this line when I tell you.
        
        class MethodLocalClass {
            public void innerMethod() {
                //below line is now showing compiler error like give below
                //Local variable thisValueIsFinalWithoutFinalKeyword defined in an enclosing scope must be final or effectively final
                System.out.println(thisValueIsFinalWithoutFinalKeyword); //on this line only final variables are allowed because this is method local class
                // if you want to test effectively final is working without final keyword then delete line which I told you to delete in above program.  
            }
        }
    }

    private int getNewValue() {
        return 0;
    }
}

如果您没有使用final关键字,那么在Java 1.7或<1.8中上述程序将会抛出错误。实际上,"effectively final"是方法局部内部类的一部分。我知道您很少会在方法本地类中使用这种"effectively final",但是为了面试,我们必须要做好准备。


2
只是为了明确。在方案2中,如果在Test类中设置了foo并创建了多个Test实例,您是否意味着foo将被设置多次,尽管最终指定了它? - Rawr
不理解第二种情况的最后一行:但是foo可以是多个对象。) 这是否意味着,如果我同时创建多个对象,那么初始化最终变量的对象取决于执行顺序? - S S
2
我认为思考第三种情况的一个有用方法是,你正在将 final 赋值给由 foo 引用的内存地址,该地址是一个 ArrayList。你没有将 final 赋值给 foo 的第一个元素(或任何元素)。因此,你不能改变 foo,但可以改变 foo[0] - Lv99Zubat
1
@Rawr 目前情况下,方案2 会因为 foo = new ArrayList(); 而导致编译时错误 - 因为我们在同一个类中,所以 foo 引用了静态变量。 - flow2k
我是一名C++开发者,正在学习Java。把变量声明为final,可以认为它与C++中的const关键字相同吗? - Doug Barbieri
关于场景2。我猜想编译错误是因为在类范围内未初始化静态final变量,这与使用构造函数进行多重实例化无关。如您所知,即使没有实例化Test类,也可以使用静态变量。如果您对此进行初始化,则会发生错误,因为您不能使用构造函数将最终变量重新赋值为另一个foo对象。 - Sergey Afinogenov

604

您总是被允许初始化一个final变量。编译器确保您只能这样做一次。

请注意,在final变量中存储的对象上调用方法与final的语义无关。换句话说:final仅涉及引用本身,而不涉及所引用的对象内容。

Java没有对象不可变性的概念;这需要仔细设计对象,而且这是一个非常棘手的工作。


15
在主方法中尝试执行"t.foo = new ArrayList();",你将会得到编译错误......引用"foo"只绑定到一个ArrayList对象上,而且是final的......它不能指向其他任何ArrayList对象。 - Code2Interface
64
嗯,这一切都与参考值有关,而非数值本身。谢谢! - G.S
2
我有一个问题。我认识的某个人声称“final”也会使变量存储在堆栈上。这是正确的吗?我已经到处搜索,找不到任何可以证实或否定这种说法的参考资料。我在Java和Android文档中进行了搜索。还搜索了“Java内存模型”。也许在C/C++上它是这样工作的,但我不认为在Java上它是这样工作的。我正确吗? - android developer
7
Java中没有任何东西可以明确地控制堆栈/堆的位置。更具体地说,由HotSpot JIT编译器决定的堆栈位置取决于逃逸分析,这比检查变量是否为 final 要复杂得多。可变对象也可以分配到堆栈上。 final 字段可能会帮助逃逸分析,但这是一条相当间接的路线。还要注意,有效 final 变量在源代码中被标记为 final 的变量具有相同的处理方式。 - Marko Topolnik
7
final 出现在类文件中,对于优化运行时具有重要的语义后果。由于 JLS 对一个对象的 final 字段具有强烈的一致性保证,它可能会带来 成本。例如,ARM 处理器必须在具有 final 字段的类的每个构造函数末尾使用显式内存屏障指令。但在其他处理器上则不需要。 - Marko Topolnik
显示剩余10条评论

246

final 关键词有多种用法:

  • 一个 final 类无法被子类化。
  • 一个 final 方法无法被子类覆盖
  • 一个 final 变量只能初始化一次

其他用途:

  • 当匿名内部类在方法体中定义时,该方法范围内声明为 final 的所有变量都可以从内部类中访问

静态类变量将从 JVM 启动时就存在,并且应该在类中进行初始化。如果这样做,错误消息将不会出现。


33
这是我最喜欢的答案。简单明了,这正是我期望在Java在线文档中阅读到的内容。 - randers
那么在静态变量中,我们可以随意多次初始化吗? - jorge saraiva
1
@jorgesaraiva 是的,静态变量不是常量。 - czupe
3
@jorgesaraiva 只要它们不是 final,你可以随意为 static 字段分配(而不是初始化)多次。请参阅此维基了解“赋值”和“初始化”的区别。 - user9514304

72
< p >关键字 final 在不同类型的情况下有不同的解释:

值类型: 对于像 intdouble 等值类型,它将确保该值不会改变。

引用类型: 对于对象的引用,final 确保该引用永远不会改变,意味着它始终指向相同的对象。但它不能保证被引用对象内部的值保持不变。

因此,final List<Whatever> foo; 确保 foo 总是引用同一个列表,但是该列表的内容可能随时间而改变。


4
终于,我找到了我要寻找的答案。 - Darius Duesentrieb

24

如果你将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不是static,则foo = new ArrayList()将在运行构造函数之前执行

当您没有在线初始化属性时,final修饰符会强制您进行初始化,并且必须在构造函数中进行。如果您还有一个static修饰符,则必须在类的初始化块中初始化该属性:static{}

您在代码中遇到的错误是由于static{}在创建类的对象之前加载类时运行。因此,在创建类时,您将无法初始化foo

static{}块视为类型为Class的对象的构造函数。这是您必须对您的static final类属性进行初始化的位置(如果未在线初始化)。

附注:

final修饰符仅保证原始类型和引用的常量性。

当您声明一个final对象时,您会得到对该对象的final引用,但是对象本身并不是常量。

当您声明一个final属性时,您真正实现的是,一旦为特定目的(如您声明的final List)声明了对象,那么只有该对象将用于该目的:您将无法将List foo更改为另一个List,但仍然可以通过添加/删除项目来更改您的List(使用的List将是相同的,只是其内容已更改)。


13

Java中的final关键字用于限制用户。Java final关键字可以用于许多上下文中,包括:

  1. 变量
  2. 方法

final关键字可应用于变量。一个没有值的final变量称为空白final变量或未初始化的final变量。它只能在构造函数中初始化。空白final变量也可以是static的,在static块中初始化。

Java final变量:

如果将任何变量定义为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类的示例

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方法:

如果您将任何方法声明为final,您无法覆盖它。

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


12

这是一个非常好的面试问题。有时候他们甚至会问你最终对象和不可变对象之间的区别。

1) 当有人提到最终对象时,它意味着引用不能被更改,但其状态(实例变量)可以更改。

2) 不可变对象是指其状态无法更改,但其引用可以更改。例如:

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

引用变量x可以更改为指向不同的字符串,但是"abc"的值无法更改。

3)实例变量(非静态字段)在构造函数被调用时初始化。所以你可以在构造函数内部将值初始化给你的变量。

4)"但是我看到你可以在类的构造方法/方法中更改它的值"。-- 你不能在方法内部更改它。

5)静态变量在类加载时初始化。所以你不能在构造函数内进行初始化,必须在之前完成初始化。因此,你需要在声明时就给静态变量分配值。


11

需要提到一些简单明了的定义:

类/方法

你可以将类的某些或全部方法声明为final,以表示该方法不能被子类重写。

变量

一旦一个final变量被初始化,它总是包含相同的值。

final基本上避免了被任何东西(子类、变量“重新赋值”)覆盖/替换,具体情况取决于使用场景。


3
我认为关于变量的最终定义有点简短。在Java中,当使用final关键字与原始数据类型(int、float等)的变量一起使用时,该变量的值不能改变;但是当final与非原始变量一起使用时(请注意,在Java中,非原始变量始终是对象的引用),所引用对象的成员可以更改。对于非原始变量,final只是意味着它们无法更改以引用任何其他对象。 - ceyun
同样适用于原始和非原始情况,特别是在提及时有效。谢谢。 - ivanleoncz

7

"最终变量只能被赋值一次"

*Reflection* - "哇哇等等,先别急".


final 字段的 Freeze 发生在两种情况下:

  • 构造函数结束时。
  • 当反射设置字段的值时。(可以任意多次

让我们来违法

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                

1
请查看相关有用的答案 - Arvind Kumar Avinash

4

final 是 Java 中的保留关键字,用于限制用户,并可应用于成员变量、方法、类和局部变量。在 Java 中,通常使用 static 关键字声明 final 变量,并将其视为常量。例如:

public static final String hello = "Hello";

当我们在变量声明中使用final关键字时,存储在该变量中的值不能被修改。例如:
public class ClassDemo {
  private final int var1 = 3;
  public ClassDemo() {
    ...
  }
}

注意: 声明为final的类不能被扩展或继承(即,超类不能有子类)。同样需要注意的是,声明为final的方法不能被子类覆盖。

使用final关键字的优点在于 这个帖子中讨论了


3
“the value stored inside that variable cannot be changed latter” 这句话只在部分情况下成立,仅适用于原始数据类型。如果将任何对象声明为 final,比如数组列表(arraylist),其内部的值可以改变,但是引用不会改变。谢谢! - G.S

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接