Java中的“final”关键字如何工作?

How does the “final” keyword in Java work? (I can still modify an object.)

在Java中,我们使用EDCOX1的0个关键字变量来指定其值不被改变。但我看到您可以更改类的构造函数/方法中的值。同样,如果变量是static,那么它是一个编译错误。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

1
private static final List foo;

现在它是一个编译错误。这个final是如何工作的?


这是一个最喜欢的面试问题。通过这个问题,面试官试图了解您对对象在构造器、方法、类变量(静态变量)和实例变量方面的行为有多了解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

1
private final List foo;  // 1

foo是一个实例变量。当我们创建Test类对象时,实例变量foo将被复制到Test类的对象内。如果我们在构造函数内分配foo,那么编译器知道构造函数只被调用一次,因此在构造函数内分配它没有问题。如果我们在一个方法内分配foo,编译器知道一个方法可以被多次调用,这意味着该值必须多次更改,这对于final变量是不允许的。所以编译器决定构造函数是好的选择!您只能给最后一个变量赋值一次。

场景2

1
private static final List foo = new ArrayList();

foo现在是一个静态变量。当我们创建Test类的实例时,由于foo是静态的,因此不会将foo复制到对象。现在,foo不是每个对象的独立属性。这是Test类的属性。但是多个对象可以看到foo,如果使用new关键字创建的每个对象最终调用Test构造函数,该构造函数在多个对象创建时更改值(记住,static foo不是在每个对象中复制,而是在多个对象之间共享)。

场景3

1
t.foo.add("bar"); // Modification-2

以上是你的问题。在上述情况下,您不会更改第一个引用对象,而是在foo中添加内容,这是允许的。如果您试图将new ArrayList()赋给foo引用变量,编译器会抱怨。规则如果已初始化final变量,则不能将其更改为引用其他对象。(在这种情况下,ArrayList)

最后一个类不能子类不能重写最终方法。(此方法在超类中)最终方法可以重写。(用语法的方式读这个。此方法在子类中)


始终允许您初始化final变量。编译器确保只能执行一次。

注意,对存储在final变量中的对象调用方法与final的语义无关。换句话说:final只是关于引用本身,而不是关于被引用对象的内容。

Java没有对象不可变的概念,这是通过仔细设计对象来实现的,并且远不是微不足道的努力。


final关键字有多种使用方法:

  • 最后一个类不能子类。
  • 最后一个方法不能被子类重写
  • 最后一个变量只能初始化一次

其他用法:

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

静态类变量将从JVM的开始就存在,并且应该在类中初始化。如果执行此操作,将不会显示错误消息。


final关键字可以用两种不同的方式解释,具体取决于它的用途:

价值类型:对于ints、doubles等,保证价值不变。

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

因此,final List foo;确保foo始终引用同一列表,但该列表的内容可能会随着时间的推移而变化。


如果使foo成为静态的,则必须在类构造函数(或在定义它的内联构造函数)中初始化它,如以下示例所示。

类构造函数(不是实例):

1
2
3
4
5
6
private static final List foo;

static
{
   foo = new ArrayList();
}

内联:

1
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将是相同的,只是其内容发生了更改)。


值得一提的是一些直截了当的定义:

类/方法

You can declare some or all of a class methods as final, in order to indicate that the method cannot be overridden by subclasses.

变量

Once a final variable has been initialized, it always contains the same value.

final基本上避免被任何东西(子类,变量"重新分配")覆盖/重写,具体视情况而定。


这是一个很好的面试问题。有时他们甚至会问你,最终对象和不变对象之间的区别是什么。

1)当有人提到最后一个对象时,这意味着引用不能更改,但是它的状态(实例变量)可以更改。

2)不可变对象是指其状态不能更改,但其引用可以更改的对象。前任:

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

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

3)实例变量(非静态字段)在调用构造函数时初始化。因此,可以在构造函数中初始化变量的值。

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

5)在类加载期间初始化静态变量。因此,不能在构造函数内部初始化,必须在它之前完成。因此,您需要在声明过程中为静态变量赋值。


EDCOX1×7是Java中的一个保留关键字,用来限制用户,它可以应用于成员变量、方法、类和局部变量。最终变量通常用Java中的EDCOX1×25个关键字来声明,并被视为常量。例如:

1
public static final String hello ="Hello";

当我们在变量声明中使用final关键字时,存储在该变量中的值不能在后面更改。

例如:

1
2
3
4
5
6
public class ClassDemo {
  private final int var1 = 3;
  public ClassDemo() {
    ...
  }
}

注意:声明为final的类不能扩展或继承(即,不能有超级类的子类)。值得注意的是,声明为final的方法不能被子类重写。

使用final关键字的好处在这个线程中得到了解决。


在Java中使用EDCOX1 0个关键字来限制用户。Java EDCOX1的0个关键字可以在许多上下文中使用。决赛可以是:

  • 变量
  • 方法
  • final关键字可用于变量,没有值的final变量称为空的final变量或未初始化的final变量。它只能在构造函数中初始化。空白EDOCX1×0变量可以是EDOCX1,7,也可以在EDCOX1×7块中初始化。

    Java最终变量:

    如果将任何变量设为final,则不能更改final变量的值(它将是常量)。

    final变量示例

    有一个最终变量speedlimit,我们要改变这个变量的值,但是它不能改变,因为一旦给最终变量赋值,就永远不能改变。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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,则不能扩展它。

    最后一堂课的例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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方法示例(本田的run()不能覆盖自行车的run())

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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


    假设你有两个钱箱,红的和白的。你只给这些钱箱分配了两个孩子,不允许他们交换钱箱。所以你有红色或白色的钱箱(最终版),你不能修改这个盒子,但你可以把钱放在你的盒子上。没人在乎(修改-2)。


    阅读所有答案。

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

    1
    2
    3
    4
    5
    public void showCaseFinalArgumentVariable(final int someFinalInt){

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

    }

    可用于不应更改的变量。


    我想在这里写一个更新和深入的答案。

    final关键字可以在多个地方使用。

  • 一个final class意味着没有其他类可以扩展最后一个类。当Java运行时(JRE)知道一个对象引用是一个最终类的类型(如F)时,它知道该引用的值只能是F的类型。

    前任:

    1
    2
    3
    4
    F myF;
    myF = new F();    //ok
    myF = someOther;  //someOther cannot be in type of a child class of F.
                      //because F cannot be extended.

    因此,当它执行该对象的任何方法时,不需要使用虚拟表在运行时解析该方法。也就是说,不能应用运行时多态性。所以运行时间并不麻烦。这意味着它可以节省处理时间,从而提高性能。

  • 方法
  • 任何类的final method表示扩展该类的任何子类都不能重写该最终方法。因此,这个场景中的运行时行为也与我为类提到的前一个行为非常相同。

  • 字段、局部变量、方法参数
  • 如果上述任何一种指定为final,则表示该值已最终确定,因此该值不能更改。

    前任:

    对于字段,本地参数

    1
    2
    3
    4
    5
    6
    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

    用于方法参数

    1
    2
    3
    void someMethod(final String s){
        s = someOtherString; //compile error
    }

    这仅仅意味着不能更改final参考值的值。即只允许一次初始化。在这个场景中,在运行时,由于JRE知道值不能更改,所以它将所有这些最终确定的值(最终引用的)加载到一级缓存中。因为它不需要从主内存中反复加载。否则,它将加载到二级缓存,并从主内存进行定时加载。所以这也是一个性能改进。

    因此,在上述3个场景中,当我们没有在可以使用的地方指定final关键字时,我们不需要担心,编译器优化将为我们做到这一点。编译器优化还可以为我们做很多其他的事情。:)


    final关键字表示变量只能初始化一次。在代码中,您只执行final的一次初始化,这样就满足了条件。此语句执行foo的单独初始化。注意final!=不可变,仅表示引用不能更改。

    1
    foo = new ArrayList();

    当您将foo声明为static final时,必须在加载类时初始化变量,并且不能依赖实例化(即对构造函数的调用)来初始化foo,因为静态字段必须在没有类实例的情况下可用。不能保证在使用静态字段之前已经调用了构造函数。

    当您在static final方案下执行方法时,Test类在实例化t之前加载,此时没有foo的实例化,这意味着它尚未初始化,因此foo被设置为所有null对象的默认值。在这一点上,我假设当您试图向列表中添加一个项目时,您的代码抛出了一个NullPointerException


    当您使它成为静态的final时,它应该在静态初始化块中初始化。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
        private static final List foo;

        static {
            foo = new ArrayList();
        }

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

  • 由于最后一个变量是非静态的,所以可以在构造函数中初始化它。但是如果您使它成为静态的,它就不能由构造函数初始化(因为构造函数不是静态的)。
  • 添加到列表中不会因使列表成为最终列表而停止。final只是将引用绑定到特定对象。您可以自由更改该对象的"状态",但不能更改对象本身。

  • 首先,代码中初始化foo的位置(即第一次分配)如下:

    1
    foo = new ArrayList();

    foo是一个对象(带有类型列表),所以它是一个引用类型,而不是值类型(如int)。因此,它保存对存储列表元素的内存位置(例如,0xA7D2A834)的引用。像这样的线

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

    不要更改foo的值(同样,它只是对内存位置的引用)。相反,它们只是将元素添加到引用的内存位置。要违反最后一个关键字,您必须再次尝试重新分配foo,如下所示:

    1
    foo = new ArrayList();

    这会给你一个编译错误。

    现在,排除了这一点,考虑一下添加静态关键字时会发生什么。

    如果没有静态关键字,则实例化类的每个对象都有自己的foo副本。因此,构造函数为foo变量的空白新副本赋值,这是完全正确的。

    但是,当您拥有static关键字时,在与类关联的内存中只存在一个foo。如果要创建两个或多个对象,构造函数将试图每次重新分配一个foo,这违反了最后一个关键字。


    下面是使用final的不同上下文。

    最终变量最终变量只能分配一次。如果变量是引用,这意味着变量不能重新绑定到引用另一个对象。

    1
    2
    3
    4
    5
    6
    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类

    1
    2
    3
    4
    5
    6
    7
    final class Base { }
    class Derived extends Base { } //Compiler Error:cannot inherit from final Base

    public class Main {
       public static void main(String args[]) {
       }
    }

    final方法final方法不能被子类重写。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //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();
        }
    }

    最重要的是正确的。此外,如果您不希望其他人从您的类中创建子类,那么将您的类声明为final。然后它成为类树层次结构的叶级,没有人可以进一步扩展它。避免类的巨大层次结构是一个很好的实践。