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


480

在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不可见-如何编译?
比约恩Hallstrom的

5
@therealprashant不是真的。私有静态变量是有效的,可以从定义它们的类内部的静态方法访问它们。静态变量表示该变量存在一次且未绑定到类的实例。
mbdavis,2015年

3
@mbdavis哦,是的!谢谢。但我仍然不会删除此评论以帮助像我这样思考的人,然后您的评论将使他们朝正确的方向思考。
therealprashant 2015年

@therealprashant好的,不用担心!
mbdavis,2015年

Answers:


518

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

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

Java没有对象不变性的概念。这是通过精心设计对象来实现的,这是一项艰巨的工作。


12
尝试做t.foo = new ArrayList(); 在main方法中,您将得到编译错误...参考foo仅绑定到ArrayList的一个最终对象...它不能指向任何其他ArrayList
Code2Interface 2013年

50
嗯。其全部与参考无关。谢谢!
GS

2
我有个问题。我认识的人声称“最终”也使变量存储在堆栈中。它是否正确?我到处搜索,找不到任何可以批准或不同意此声明的参考。我已经搜索了Java和Android文档。还搜索了“ Java内存模型”。也许它可以在C / C ++上以这种方式工作,但是我不认为它可以在Java上以这种方式工作。我对么?
Android开发人员

4
@androiddeveloper Java中没有任何东西可以显式控制堆栈/堆的放置。更具体地说,由HotSpot JIT编译器决定的堆栈放置需要进行转义分析,这比检查变量是否为涉及得多final。可变对象也可以堆栈分配。final字段可能有助于进行转义分析,但这是一条非常间接的路线。还要注意,最终变量实际上具有与final源代码中标记的变量相同的处理方式。
Marko Topolnik 2014年

5
final出现在类文件中,并且对优化运行时具有重大的语义影响。由于JLS 对对象字段的一致性有很强的保证,因此也可能会产生成本final。例如,ARM处理器必须在具有final字段的类的每个构造函数的末尾使用显式的内存屏障指令。但是,在其他处理器上则不需要。
Marko Topolnik 2014年

574

这是一个最喜欢的面试问题。带着这个问题,访问者试图找出您对对象的构造,方法,类变量(静态变量)和实例变量的理解程度。

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其中添加了允许的内容。如果您尝试将a分配new ArrayList()foo引用变量,则编译器会抱怨。
规则如果已初始化final变量,则无法将其更改为引用其他对象。(在这种情况下ArrayList

final类不能被子类化
final方法不能被覆盖。(此方法在超类中)
最终方法可以重写。(以语法方式阅读。此方法在子类中)


1
只是要清楚。在场景2中,您是说foo如果foo在Test类中设置了Test并且创建了Test的多个实例,那么即使最终指定,它也会被多次设置?
拉尔'16

不了解方案2的最后一行,foo可以是..多个对象。)这是否意味着,如果我一次创建多个对象,那么哪个对象初始化的最终变量取决于执行?
Saumya Suhagiya'9

1
我认为考虑方案3的一种有用方法是将您分配finalfooArrayList 引用的内存地址。您没有分配final给的第一个元素foo(或与此有关的任何元素)所引用的内存地址。因此,你不能改变,foo但你可以改变foo[0]
Pinkerton

@Rawr就目前而言,方案2将导致编译时错误,因为foo = new ArrayList();- foo引用了静态变量,因为我们在同一个类中。
flow2k

我是学习Java的C ++开发人员。final在变量上考虑与constC ++中的关键字相同是否安全?
道格·巴比耶里

213

Final关键字有多种使用方式:

  • 期末不能被子类化。
  • 最终方法不能被子类覆盖
  • 最终变量只能初始化一次

其他用法:

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

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


24
到目前为止,这是我最喜欢的答案。简单明了,这就是我希望在有关Java的在线文档中阅读的内容。
RAnders00 2015年

因此,在静态变量中,我们可以根据需要进行多次初始化?
jorge saraiva

1
@jorgesaraiva是的,静态变量不是常量。
czupe

1
@jorgesaraiva您可以根据需要分配(不初始化static字段(只要不是final)。有关分配初始化之间的区别,请参见此Wiki

56

根据final关键字的用途,可以用两种不同的方式解释该关键字:

值类型:对于ints,doubles等,它将确保该值不能更改,

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

因此,请final List<Whatever> foo;确保foo始终引用相同的列表,但是列表的内容可能会随时间而变化。


23

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

类构造器(不是实例):

private static final List foo;

static
{
   foo = new ArrayList();
}

排队:

private static final List foo = new ArrayList();

这里的问题不是final修饰符如何工作,而是static修饰符如何工作。

final修改的时候强制参考的初始化调用构造函数结束(即您必须在构造函数初始化)。

内联初始化属性时,它会在运行为构造函数定义的代码之前进行初始化,因此您将获得以下结果:

  • 如果foostaticfoo = new ArrayList()则将在执行static{}您为类定义的构造函数之前执行
  • 如果foo不是staticfoo = new ArrayList()将在运行构造函数之前执行

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

您在代码中获得的错误是由于static{}实例化该类的对象之前加载该类时运行的事实。因此,foo创建类时将不会初始化。

将该static{}块视为类型的对象的构造函数Class。这是您必须对static final类属性进行初始化的地方(如果没有内联完成的话)。

边注:

final修改确保常量性只对基本类型和引用。

声明final对象时,得到的是对该对象的final 引用,但是对象本身不是常量。

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


8

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

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

2)不可变对象是其状态不能更改但其引用可以更改的对象。例如:

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

可以将ref变量x更改为指向其他字符串,但不能更改“ abc”的值。

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

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

5)在类加载期间初始化静态变量。因此,您无法在构造函数内部进行初始化,因此必须在初始化之前完成。因此,您需要声明本身期间将值分配给静态变量。


7

finaljava中的关键字用于限制用户。java final关键字可以在许多上下文中使用。最终可以是:

  1. 变量
  2. 方法

final关键字可与变量被应用,一个final不具有任何值的变量,被称为空白final变量或未初始化final变量。它只能在构造函数中初始化。空白final变量也可以仅staticstatic块中初始化。

Java最终变量:

如果你做任何变量final,你不能改变值final变量(这将是不变)。

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 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()无法覆盖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


7

值得一提的是一些简单的定义:

类/方法

您可以将某些或所有类方法声明为final,以指示该方法不能被子类覆盖。

变数

final变量一旦初始化,就始终包含相同的值。

final 基本上要根据情况避免用任何东西(子类,变量“重新分配”)覆盖/覆盖。


1
我认为关于变量的最终定义有点简短;“在Java中,当final关键字与原始数据类型的变量(int,float等)一起使用时,该变量的值无法更改,但是当final与非原始变量一起使用时(请注意,非原始变量总是对Java中对象的引用),所引用对象的成员可以更改。非基本变量的final表示不能将其更改为引用任何其他对象。” geeksforgeeks.org/g-fact-48
ceyun

也有效,特别是作为原始和非原始情况提及。Tks。
ivanleoncz

4

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

public static final String hello = "Hello";

当我们将final关键字与变量声明一起使用时,该变量内部存储的值以后就无法更改。

例如:

public class ClassDemo {
  private final int var1 = 3;
  public ClassDemo() {
    ...
  }
}

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

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


2
the value stored inside that variable cannot be changed latter是部分正确的。仅原始数据类型适用。如果将任何对象设置为final,例如arraylist,则其值可以更改,但引用不能更改。谢谢!
GS 2014年

3

假设您有两个扑满,红色和白色。您只给这些扑满分配两个孩子,不允许他们互换他们的盒子。因此,您有红色或白色的钱箱(最终),您无法修改钱箱,但可以在盒子上放钱。没人在乎(修改2)。


2

阅读所有答案。

在另一种用户情况下,final可以在方法参数中使用关键字:

public void showCaseFinalArgumentVariable(final int someFinalInt){

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

}

可以用于不应更改的变量。


1

当您将其设为静态final时,应在静态初始化块中对其进行初始化

    private static final List foo;

    static {
        foo = new ArrayList();
    }

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

1

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

foo = new ArrayList();

当声明foostatic final变量时,必须在加载类时进行初始化,并且不能依赖实例化(也称为构造函数调用)进行初始化foo因为静态字段必须在没有类实例的情况下可用。无法保证在使用静态字段之前会调用构造函数。

在这种static final情况下执行此方法时,在Test实例化之前加载该类t时,没有实例化的foo含义,即尚未初始化,因此foo将所有对象的默认值设置为null。在这一点上,我假设您NullPointerException尝试向列表中添加项目时,您的代码抛出。


1

首先,在代码中初始化(即首次分配)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关键字。


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

1

以下是使用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[]) {
   }
}

最终方法最终方法不能被子类覆盖。

//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

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

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.

因此,当它执行该对象的任何方法时,不需要在运行时使用虚拟表来解析该方法。即运行时多态不能应用。因此,运行时不必为此担心。这意味着它可以节省处理时间,从而提高性能。

  1. 方法

final method任何类的A 表示扩展该类的任何子类都不能覆盖该最终方法。因此,这种情况下的运行时行为也与我为类提到的先前行为完全相同。

  1. 字段,局部变量,方法参数

如果将以上任何一种指定为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在可以使用的位置指定关键字时,就不必担心,编译器优化将为我们做到这一点。编译器优化还可以为我们做很多其他事情。:)


0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.