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个回答

2

阅读所有答案。

还有另一种用户案例可以使用final关键字,即在方法参数中:

public void showCaseFinalArgumentVariable(final int someFinalInt){

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

}

可用于不应更改的变量。


2

首先,你在代码中初始化(即第一次赋值)foo的地方是这里:

foo = new ArrayList();

foo 是一个对象(类型为 List),因此它是一种引用类型,而不是值类型(如 int)。因此,它持有对存储您的 List 元素的内存位置(例如 0xA7D2A834)的引用。类似于这样的行:

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

不要改变foo的值(它只是一个指向内存位置的引用)。相反,它们只是将元素添加到该引用的内存位置中。要违反final关键字,您需要尝试再次按以下方式重新分配foo:

foo = new ArrayList();

这会导致编译错误。


现在,想一想当你添加了static关键字时会发生什么。

如果没有static关键字,每个实例化类的对象都拥有自己的foo副本。因此,构造函数将为空白的、新的foo变量分配一个值,这是完全可以的。

然而,如果有static关键字,则只存在一个与该类相关联的foo。如果创建两个或多个对象,则构造函数会尝试每次重新分配那一个foo,从而违反了final关键字的规定。


2
假设你有两个存钱罐,一个红色的,一个白色的。你只给这两个孩子分配了这两个罐子,并且不允许他们交换罐子。所以你有一个红色或白色的存钱罐(最终状态),你不能修改罐子,但是你可以往罐子里放钱。无论如何都没有人会在意(修改-2)。

1
我想在这里写一篇更新且深入的答案。 final 关键字可以用在几个地方。
final class 表示没有其他类可以继承该 final 类。当 Java 运行时 (JRE) 知道一个对象引用是 final 类型 (比如 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.

当执行该对象的任何方法时,该方法不需要使用虚拟表在运行时解析。即无法应用运行时多态性。因此,运行时不会关心这个。这意味着它节省了处理时间,从而提高了性能。
2. 方法
任何类的final方法意味着扩展该类的任何子类都不能覆盖该final方法。因此,在这种情况下的运行时行为与我之前提到的类的行为也是相同的。
3. 字段、局部变量、方法参数
如果将上述任何一种指定为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缓存并从主内存中不时地加载。因此,这也是性能的提高。

因此,在上述三种情况中,当我们没有在可以使用的位置指定final关键字时,我们不需要担心,编译器优化会为我们完成。编译器优化还为我们做了很多其他事情。 :)


1

以下是使用final的不同情境。

Final变量 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[]) {
   }
}

Final methods指的是无法被子类重写的方法。

//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();
    }
}

1
  1. 由于最终变量是非静态的,因此可以在构造函数中初始化。但如果将其设置为静态,则无法通过构造函数进行初始化(因为构造函数不是静态的)。
  2. 将列表设置为final并不意味着添加操作会停止。 final只是将引用绑定到特定对象。您可以自由更改该对象的“状态”,但不能更改对象本身。

1
“final”关键字表示变量只能被初始化一次。在您的代码中,您只执行了一次final的初始化,因此条件得到满足。该语句执行了foo的唯一初始化。请注意,“final”不等于不可变,它只是意味着引用不能改变。
foo = new ArrayList();

当你将foo声明为static final时,变量必须在类加载时初始化,并且不能依赖于实例化(也就是调用构造函数)来初始化foo,因为静态字段必须在没有类的实例的情况下可用。不能保证在使用静态字段之前会调用构造函数。
static final场景下执行方法时,在实例化t之前加载Test类,此时没有实例化foo,这意味着它尚未初始化,因此foo被设置为所有对象的默认值null。在这一点上,我假设当您尝试向列表添加项目时,代码会抛出NullPointerException

1
当您将其声明为静态常量时,应在静态初始化块中进行初始化。
    private static final List foo;

    static {
        foo = new ArrayList();
    }

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

0
以上都是正确的。如果您不希望其他人从您的类创建子类,则将您的类声明为final。然后它就成为了您的类树层次结构的叶级别,没有人可以进一步扩展它。避免使用庞大的类层次结构是一个好习惯。

0

针对你的问题,我只能说在这种情况下,你无法更改 foo引用值。你只需将放入同一引用中,这就是为什么你可以将添加到foo引用中的原因。这个问题出现的原因是你可能不太理解引用值基本值之间的区别。引用值也是一个值,它存储在堆内存中的对象地址(这是一个值)。

public static void main(String[] args) 
  {
      Test t = new Test();
      t.foo.add("bar"); // Modification-2
      System.out.println("print - " + t.foo);
  }

但是在这种情况下,如果您尝试编写以下代码,您将会看到编译时错误发生。

public static void main(String[] args)
{
    Main main = new Main();
    main.foo=new ArrayList<>();//Cannot assign a value to final variable 'foo'
    System.out.println("print - " + main.foo);
}

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