详解Java中的final关键字

本文原文地址:https://jiang-hao.com/articles/2019/coding-java-final-keyword.html[1]

final 简介[2]

final关键字可用于多个场景,且在不同场景具有不同的作用。首先,final是一个非访问修饰符适用于变量,方法或类。下面是使用final的不同场景:

[图片上传失败...(image-bee40-1555334312011)]

上面这张图可以概括成:

  • final修饰变量时,被修饰的变量必须被初始化(赋值),且后续不能修改其值,实质上是常量;
  • final修饰方法时,被修饰的方法无法被所在类的子类重写(覆写);
  • final修饰时,被修饰的类不能被继承,并且final类中的所有成员方法都会被隐式地指定为final方法,但成员变量则不会变。

final 修饰变量

当使用final关键字声明类成员变量或局部变量后,其值不能被再次修改;也经常和static关键字一起,作为类常量使用。很多时候会容易把staticfinal关键字混淆,<u>static作用于成员变量用来表示只保存一份副本,而final的作用是用来保证变量不可变</u>。如果final变量是引用,这意味着该变量不能重新绑定到引用另一个对象,但是可以更改该引用变量指向的对象的内部状态,即可以从final数组final集合中添加或删除元素。最好用全部大写来表示final变量,使用下划线来分隔单词。

例子

//一个final成员常量
final int THRESHOLD = 5;
//一个空的final成员常量
final int THRESHOLD;
//一个静态final类常量
static final double PI = 3.141592653589793;
//一个空的静态final类常量
static final double PI;

初始化final变量

我们必须初始化一个final变量,否则编译器将抛出编译时错误。final变量只能通过初始化器或赋值语句初始化一次。初始化final变量有三种方法:

  1. 可以在声明它时初始化final变量。这种方法是最常见的。如果在声明时初始化,则该变量称为final变量。下面是初始化空final变量的两种方法。
  2. 可以在instance-initializer块 或内部构造函数中初始化空的final变量。如果您的类中有多个构造函数,则必须在所有构造函数中初始化它,否则将抛出编译时错误。
  3. 可以在静态块内初始化空的final静态变量。

这里注意有一个很普遍的误区。<u>很多人会认为static修饰的final常量必须在声明时就进行初始化,否则会报错。但其实则不然,我们可以先使用static final关键字声明一个类常量,然后再在静态块内初始化空的final静态变量。</u>让我们通过一个例子看上面初始化final变量的不同方法。

// Java program to demonstrate different 
// ways of initializing a final variable 
  
class Gfg  
{ 
    // a final variable direct initialize 
    // 直接赋值
    final int THRESHOLD = 5; 
      
    // a blank final variable 
    // 空final变量
    final int CAPACITY; 
      
    // another blank final variable 
    final int  MINIMUM; 
      
    // a final static variable PI direct initialize 
    // 直接赋值的静态final变量
    static final double PI = 3.141592653589793; 
      
    // a  blank final static variable 
    // 空的静态final变量,此处并不会报错,因为在下方的静态代码块内对其进行了初始化
    static final double EULERCONSTANT; 
      
    // instance initializer block for initializing CAPACITY 
    // 用来赋值空final变量的实例初始化块
    { 
        CAPACITY = 25; 
    } 
      
    // static initializer block for initializing EULERCONSTANT
    // 用来赋值空final变量的静态初始化块
    static{ 
        EULERCONSTANT = 2.3; 
    } 
      
    // constructor for initializing MINIMUM 
    // Note that if there are more than one 
    // constructor, you must initialize MINIMUM 
    // in them also 
    // 构造函数内初始化空final变量;注意如果有多个
    // 构造函数时,必须在每个中都初始化该final变量
    public GFG()  
    { 
        MINIMUM = -1; 
    } 
          
} 

何时使用final变量:**

普通变量和final变量之间的唯一区别是我们可以将值重新赋值给普通变量;但是对于final变量,一旦赋值,我们就不能改变final变量的值。因此,final变量必须仅用于我们希望在整个程序执行期间保持不变的值。

final引用变量:
final变量是对象的引用时,则此变量称为final引用变量。例如,finalStringBuffer变量:

final StringBuffer sb;

final变量无法重新赋值。但是对于final的引用变量,可以更改该引用变量指向的对象的内部状态。请注意,这不是重新赋值。final的这个属性称为非传递性。要了解对象内部状态的含义,请参阅下面的示例:

// Java program to demonstrate  
// reference final variable 
  
class Gfg 
{ 
    public static void main(String[] args)  
    { 
        // a final reference variable sb 
        final StringBuilder sb = new StringBuilder("Geeks"); 
          
        System.out.println(sb); 
          
        // changing internal state of object 
        // reference by final reference variable sb 
        // 更改final变量sb引用的对象的内部状态
        sb.append("ForGeeks"); 
          
        System.out.println(sb); 
    }     
} 

输出:

Geeks
GeeksForGeeks

非传递属性也适用于数组,因为在Java中数组也是对象。带有final关键字的数组也称为final数组

注意 :

  1. 如上所述,final变量不能重新赋值,这样做会抛出编译时错误。
   // Java program to demonstrate re-assigning 
   // final variable will throw compile-time error 
   
   class Gfg 
   { 
     static final int CAPACITY = 4; 
   
     public static void main(String args[]) 
     { 
       // re-assigning final variable 
       // will throw compile-time error 
       CAPACITY = 5; 
     } 
   } 

输出:

   Compiler Error: cannot assign a value to final variable CAPACITY
  1. 当在方法/构造函数/块中创建final变量时,它被称为局部final变量,并且必须在创建它的位置初始化一次。参见下面的局部final变量程序:
   // Java program to demonstrate 
   // local final variable 
   
   // The following program compiles and runs fine 
   
   class Gfg 
   { 
    public static void main(String args[]) 
    { 
        // local final variable 
        final int i; 
        i = 20; 
        System.out.println(i); 
    } 
   } 

输出:

   20
  1. 注意C ++ const变量和Java final变量之间的区别。声明时,必须为C ++中的const变量赋值。对于Java中的final变量,正如我们在上面的示例中所看到的那样,可以稍后赋值,但只能赋值一次。
  2. finalforeach循环中:在foreach语句中使用final声明存储循环元素的变量是合法的。
  // Java program to demonstrate final 
  // with for-each statement 

  class Gfg 
  { 
    public static void main(String[] args) 
    { 
      int arr[] = {1, 2, 3}; 

      // final with for-each statement 
      // legal statement 
      for (final int i : arr) 
        System.out.print(i + " "); 
    }    
  } 

输出:

1 2 3

说明:由于i变量在循环的每次迭代时超出范围,因此实际上每次迭代都重新声明,允许使用相同的标记(即i)来表示多个变量。

final 修饰类

当使用final关键字声明一个类时,它被称为final类。被声明为final的类不能被扩展(继承)。final类有两种用途:

  1. 一个是彻底防止被继承,因为final类不能被扩展。例如,所有包装类IntegerFloat等都是final类。我们无法扩展它们。
  2. final类的另一个用途是创建一个类似于String类的不可变类。只有将一个类定义成为final类,才能使其不可变。
  final class A
  {
       // methods and fields
  }
  // 下面的这个类B想要扩展类A是非法的
  class B extends A 
  { 
      // COMPILE-ERROR! Can't subclass A
  }

Java支持把class定义成final,似乎违背了面向对象编程的基本原则,但在另一方面,封闭的类也保证了该类的所有方法都是固定不变的,不会有子类的覆盖方法需要去动态加载。这给编译器做优化时提供了更多的可能,最好的例子是String,它就是final类,Java编译器就可以把字符串常量(那些包含在双引号中的内容)直接变成String对象,同时对运算符"+"的操作直接优化成新的常量,因为final修饰保证了不会有子类对拼接操作返回不同的值。
对于所有不同的类定义一顶层类(全局或包可见)、嵌套类(内部类或静态嵌套类)都可以用final来修饰。但是一般来说final多用来修饰在被定义成全局(public)的类上,因为对于非全局类,访问修饰符已经将他们限制了它们的也可见性,想要继承这些类已经很困难,就不用再加一层final限制。

final与匿名内部类

匿名类(Anonymous Class)虽然说同样不能被继承,但它们并没有被编译器限制成final。另外要提到的是,网上有许多地方都说因为使用内部类,会有两个地方必须需要使用 final 修饰符:

  1. 在内部类的方法使用到方法中定义的局部变量,则该局部变量需要添加 final 修饰符
  2. 在内部类的方法形参使用到外部传过来的变量,则形参需要添加 final 修饰符

原因大多是说当我们创建匿名内部类的那个方法调用运行完毕之后,因为局部变量的生命周期和方法的生命周期是一样的,当方法弹栈,这个局部变量就会消亡了,但内部类对象可能还存在。 此时就会出现一种情况,就是我们调用这个内部类对象去访问一个不存在的局部变量,就可能会出现空指针异常。而此时需要使用 final 在类加载的时候进入常量池,即使方法弹栈,常量池的常量还在,也可以继续使用,JVM 会持续维护这个引用在回调方法中的生命周期。

<span style='color:red;'>但是 JDK 1.8 取消了对匿名内部类引用的局部变量 final 修饰的检查</span>

对此,theonlin专门通过实验做出了总结:其实局部内部类并不是直接调用方法传进来的参数,而是内部类将传进来的参数通过自己的构造器备份到了自己的内部,自己内部的方法调用的实际是自己的属性而不是外部类方法的参数。外部类中的方法中的变量或参数只是方法的局部变量,这些变量或参数的作用域只在这个方法内部有效,所以方法中被 final的变量的仅仅作用是表明这个变量将作为内部类构造器参数,其实final不加也可以,加了可能还会占用内存空间,影响 GC**。最后结论就是,需要使用 final 去持续维护这个引用在回调方法中的生命周期这种说法应该是错误的,也没必要。

final 修饰方法

下面这段话摘自《Java编程思想》第四版第143页:

使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。

当使用final关键字声明方法时,它被称为final方法。final方法无法被覆盖(重写)。比如Object类,它的一些方法就被声明成为了final。如果你认为一个方法的功能已经足够完整了,子类中不需要改变的话,你可以声明此方法为final。以下代码片段说明了用final关键字修饰方法:

class A 
{
    // 父类的ml方法被使用了final关键字修饰
    final void m1() 
    {
        System.out.println("This is a final method.");
    }
}

class B extends A 
{
    // 此处会报错,子类B尝试重写父类A的被final修饰的ml方法
    @override
    void m1()
    { 
        // COMPILE-ERROR! Can't override.
        System.out.println("Illegal!");
    }
}

而关于高效,是因为在java早期实现中,如果将一个方法指明为final,就是同意编译器将针对该方法的调用都转化为内嵌调用(内联)。大概就是,如果是内嵌调用,虚拟机不再执行正常的方法调用(参数压栈,跳转到方法处执行,再调回,处理栈参数,处理返回值),而是直接将方法展开,以方法体中的实际代码替代原来的方法调用。这样减少了方法调用的开销。所以有一些程序员认为:除非有足够的理由使用多态性,否则应该将所有的方法都用 final 修饰。这样的认识未免有些偏激,因为在最近的java设计中,虚拟机(特别是hotspot技术)可以自己去根据具体情况自动优化选择是否进行内联,只不过使用了final关键字的话可以显示地影响编译器对被修饰的代码进行内联优化。所以请切记,对于Java虚拟机来说编译器在编译期间会自动进行内联优化,这是由编译器决定的,对于开发人员来说,一定要设计好时空复杂度的平衡,不要滥用final。

注1:类的private方法会隐式地被指定为final方法,也就同样无法被重写。可以对private方法添加final修饰符,但并没有添加任何额外意义。

注2:在java中,你永远不会看到同时使用finalabstract关键字声明的类或方法。对于类,final用于防止继承,而抽象类反而需要依赖于它们的子类来完成实现。在修饰方法时,final用于防止被覆盖,而抽象方法反而需要在子类中被重写。

有关final方法和final类的更多示例和行为**,请参阅使用final继承

final 优化编码的艺术

final关键字在效率上的作用主要可以总结为以下三点:

  • 缓存:final配合static关键字提高了代码性能,JVM和Java应用都会缓存final变量。
  • 同步:final变量或对象是只读的,可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
  • 内联:使用final关键字,JVM会显式地主动对方法、变量及类进行内联优化。

更多关于final关键字对代码的优化总结以及注意点可以参考IBM的《Is that your final answer?》这篇文章。


  1. 本文原文地址:https://jiang-hao.com/articles/2019/coding-java-final-keyword.html

  2. 本文由笔者参考多篇博文汇总作成,因数量众多不一一列出,主体部分从GeeksforGeeks网站翻译,实际由Gaurav Miglani撰写。如果您发现任何不正确的内容,或者您想要分享有关上述主题的更多信息,请撰写评论。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342