探究Java中的final关键字

简介: final 关键字的字面意思是最终的,不可修改的。这似乎是一个看见名字就大概知道怎么用的语法,但你是否有深究过final在各个场景中的具体用法,注意事项,以及背后涉及的Java设计思想呢?

一. final 修饰变量

  • 基础: final 修饰基本数据类型变量和引用数据类型变量

相信大家都具备基本的常识: 被 final 修饰的变量是不能够被改变的。但是这里的"不能够被改变"对于不同的数据类型是有不同的含义的。

当 final 修饰的是一个基本数据类型数据时,这个数据的值在初始化后将不能被改变;

当 final 修饰的是一个引用类型数据时,也就是修饰一个对象时,引用在初始化后将永远指向一个内存地址,不可修改。但是该内存地址中保存的对象信息,是可以进行修改的。

上一段话可能比较抽象,希望下面的图能有助于你理解,你会发现虽说有不同的含义,但本质还是一样的。

首先是 final 修饰基本数据类型时的内存示意图:

37.png

如上图, 变量 a 在初始化后将永远指向 003 这块内存,而这块内存在初始化后将永远保存数值 100。

下面是 final 修饰引用数据类型的示意图:

46.jpg

在上图中,变量 p 指向了 0003 这块内存,0003 内存中保存的是对象 p 的句柄(存放对象p数据的内存地址),这个句柄值是不能被修改的,也就是变量 p 永远指向 p 对象. 但是 p 对象的数据是可以修改的。

  1. // 代码示例
  2. publicstaticvoid main(String[] args){
  3.    finalPerson p =newPerson(20,"炭烧生蚝");
  4.    p.setAge(18);   //可以修改p对象的数据
  5.    System.out.println(p.getAge());//输出18

  6.    Person pp =newPerson(30,"蚝生烧炭");
  7.    p = pp;//这行代码会报错, 不能通过编译, 因为p经final修饰永远指向上面定义的p对象, 不能指向pp对象.
  8. }

不难看出 final 修饰变量的本质: final 修饰的变量会指向一块固定的内存,这块内存中的值不能改变。

引用类型变量所指向的对象之所以可以修改,是因为引用变量不是直接指向对象的数据,而是指向对象的引用。

所以被 final 修饰的引用类型变量将永远指向一个固定的对象,不能被修改;对象的数据值可以被修改。

  • 进阶:被 final 修饰的常量在编译阶段会被放入常量池中

final 是用于定义常量的,定义常量的好处是:不需要重复地创建相同的变量。

而常量池是 Java 的一项重要技术,由 final 修饰的变量会在编译阶段放入到调用类的常量池中。

请看下面这段演示代码,这个示例是专门为了演示而设计的,希望能方便大家理解这个知识点。

  1. publicstaticvoid main(String[] args){
  2.    int n1 =2019;          //普通变量
  3.    finalint n2 =2019;    //final修饰的变量

  4.    String s ="20190522";  
  5.    String s1 = n1 +"0522";    //拼接字符串"20190512"
  6.    String s2 = n2 +"0522";    

  7.    System.out.println(s == s1);    //false
  8.    System.out.println(s == s2);    //true
  9. }

温馨提示:整数 -127 - 128 是默认加载到常量池里的,也就是说如果涉及到 -127 - 128 的整数操作,默认在编译期就能确定整数的。所以这里我故意选用数字2019 (大于128),避免数字默认就存在常量池中。

上面的代码运作过程是这样的:

首先根据 final 修饰的常量会在编译期放到常量池的原则,n2会在编译期间放到常量池中。

然后 s 变量所对应的"20190522"字符串会放入到字符串常量池中,并对外提供一个引用返回给 s 变量。(下一篇文章会介绍字符串常量池)

这时候拼接字符串 s1,由于 n1 对应的数据没有放入常量池中,所以 s1 暂时无法拼接,需要等程序加载运行时才能确定 s1 对应的值。

但在拼接 s2 的时候,由于 n2 已经存在于常量池,所以可以直接与"0522"拼接,拼接出的结果是"20190522"

这时系统会查看字符串常量池,发现已经存在字符串20190522,所以直接返回20190522的引用。

所以 s2 和 s 指向的是同一个引用,这个引用指向的是字符串常量池中的20190522。

而 n1 会在程序执行时,才有具体的指向。

当拼接 s1 的时候,会创建一个新的 String 类型对象,也就是说字符串常量池中的 20190522 会对外提供一个新的引用。

所以当 s1 与 s 用 "==" 判断时, 由于对应的引用不同, 会返回 false。而 s2 和 s 指向同一个引用,返回true。

这个例子额外说明的是:由于被 final 修饰的常量会在编译期进入常量池,如果有涉及到该常量的操作,很有可能在编译期就已经完成。

  • 探索: 为什么局部/匿名内部类在使用外部局部变量时,只能使用被 final 修饰的变量?

提示: 在JDK1.8以后,通过内部类访问外部局部变量时,无需显式把外部局部变量声明为final。不是说不需要声明为final了,而是这件事情系统在编译期间帮我们做了。 但是我们还是有必要了解为什么要用 final 修饰外部局部变量。

  1. publicclassOutter{
  2.    publicstaticvoid main(String[] args){
  3.        finalint a =10;
  4.        newThread(){
  5.            @Override
  6.            publicvoid run(){
  7.                System.out.println(a);
  8.            }
  9.        }.start();
  10.    }
  11. }

在上面这段代码, 如果没有给外部局部变量 a 加上 final 关键字,是无法通过编译的。可以试着想想:当 main 方法已经执行完后,main 方法的栈帧将会弹出,如果此时 Thread 对象的生命周期还没有结束,还没有执行打印语句的话,将无法访问到外部的 a 变量。

那么为什么加上 final 关键字就能正常编译呢?

我们通过查看反编译代码看看内部类是怎样调用外部成员变量的。

我们可以先通过 javac 编译得到 .class文件(用IDE编译也可以),然后在命令行输入 javap-c.class文件的绝对路径,就能查看 .class 文件的反编译代码。

以上的 Outter 类经过编译产生两个 .class 文件,分别是 Outter.classOutter$1.class

也就是说内部类会单独编译成一个.class文件。

下面给出 Outter$1.class的反编译代码。

  1. Compiled from "Outter.java"
  2. finalclass forTest.Outter$1extends java.lang.Thread{
  3.  forTest.Outter$1();
  4.    Code:
  5.       0: aload_0
  6.       1: invokespecial #1                  // Method java/lang/Thread."<init>":()V
  7.       4:return

  8.  publicvoid run();
  9.    Code:
  10.       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
  11.       3: bipush        10
  12.       5: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
  13.       8:return
  14. }

定位到 run()方法反编译代码中的第3行:

3:bipush10

我们看到 a 的值在内部类的 run()方法执行过程中是以压栈的形式存储到本地变量表中的,

也就是说在内部类打印变量 a 的值时,这个变量 a 不是外部的局部变量 a,因为如果是外部局部变量的话,应该会使用 load指令加载变量的值。

也就是说系统以拷贝的形式把外部局部变量 a 复制了一个副本到内部类中,内部类有一个变量指向外部变量a所指向的值。

但研究到这里好像和 final 的关系还不是很大,不加 final 似乎也可以拷贝一份变量副本,只不过不能在编译期知道变量的值罢了。这时该思考一个新问题了:

现在我们知道内部类的变量 a 和外部局部变量 a 是两个完全不同的变量,

那么如果在执行 run() 方法的过程中, 内部类中修改了 a 变量所指向的值,就会产生数据不一致问题。

正因为我们的原意是内部类和外部类访问的是同一个a变量,所以当在内部类中使用外部局部变量的时候应该用 final 修饰局部变量,这样局部变量a的值就永远不会改变,也避免了数据不一致问题的发生。

二. final修饰方法

使用 final 修饰方法有两个作用,首要作用是锁定方法,不让任何继承类对其进行修改。

另外一个作用是在编译器对方法进行内联,提升效率。 但是现在已经很少这么使用了,近代的Java版本已经把这部分的优化处理得很好了。

但是为了满足求知欲还是了解一下什么是方法内敛:

方法内敛: 当调用一个方法时,系统需要进行保存现场信息,建立栈帧,恢复线程等操作,这些操作都是相对比较耗时的。

如果使用 final 修饰一个了一个方法 a,在其他调用方法 a 的类进行编译时,方法 a 的代码会直接嵌入到调用 a 的代码块中。

  1. //原代码
  2. publicstaticvoid test(){
  3.    String s1 ="包夹方法a";
  4.    a();
  5.    String s2 ="包夹方法a";
  6. }

  7. publicstaticfinalvoid a(){
  8.    System.out.println("我是方法a中的代码");
  9.    System.out.println("我是方法a中的代码");
  10. }

  11. //经过编译后
  12. publicstaticvoid test(){
  13.    String s1 ="包夹方法a";
  14.    System.out.println("我是方法a中的代码");
  15.    System.out.println("我是方法a中的代码");
  16.    String s2 ="包夹方法a";
  17. }

在方法非常庞大的时候,这样的内嵌手段是几乎看不到任何性能上的提升的,在最近的 Java 版本中,不需要使用 final 方法进行这些优化了。---《Java编程思想》

三. final 修饰类

使用 final 修饰类的目的简单明确:表明这个类不能被继承。

当程序中有永远不会被继承的类时,可以使用 final 关键字修饰。

被 final 修饰的类所有成员方法都将被隐式修饰为 final 方法。

相关文章
|
2月前
|
存储 缓存 安全
除了变量,final还能修饰哪些Java元素
在Java中,final关键字不仅可以修饰变量,还可以用于修饰类、方法和参数。修饰类时,该类不能被继承;修饰方法时,方法不能被重写;修饰参数时,参数在方法体内不能被修改。
33 2
|
13天前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
14天前
|
缓存 安全 Java
Java volatile关键字:你真的懂了吗?
`volatile` 是 Java 中的轻量级同步机制,主要用于保证多线程环境下共享变量的可见性和防止指令重排。它确保一个线程对 `volatile` 变量的修改能立即被其他线程看到,但不能保证原子性。典型应用场景包括状态标记、双重检查锁定和安全发布对象等。`volatile` 适用于布尔型、字节型等简单类型及引用类型,不适用于 `long` 和 `double` 类型。与 `synchronized` 不同,`volatile` 不提供互斥性,因此在需要互斥的场景下不能替代 `synchronized`。
2106 3
|
2月前
|
JavaScript 前端开发 Java
java中的this关键字
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。自学前端2年半,正向全栈进发。若我的文章对你有帮助,欢迎关注,持续更新中!🎉🎉🎉
56 9
|
2月前
|
设计模式 JavaScript 前端开发
java中的static关键字
欢迎来到瑞雨溪的博客,博主是一名热爱JavaScript和Vue的大一学生,致力于全栈开发。如果你从我的文章中受益,欢迎关注我,将持续分享更多优质内容。你的支持是我前进的动力!🎉🎉🎉
56 8
|
2月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
50 4
|
2月前
|
Java
final 在 java 中有什么作用
在 Java 中,`final` 关键字用于限制变量、方法和类的修改或继承。对变量使用 `final` 可使其成为常量;对方法使用 `final` 禁止其被重写;对类使用 `final` 禁止其被继承。
41 0
|
3月前
|
Java 程序员
在Java编程中,关键字不仅是简单的词汇,更是赋予代码强大功能的“魔法咒语”。
【10月更文挑战第13天】在Java编程中,关键字不仅是简单的词汇,更是赋予代码强大功能的“魔法咒语”。本文介绍了Java关键字的基本概念及其重要性,并通过定义类和对象、控制流程、访问修饰符等示例,展示了关键字的实际应用。掌握这些关键字,是成为优秀Java程序员的基础。
39 3
|
3月前
|
算法 Java
在Java编程中,关键字和保留字是基础且重要的组成部分,正确理解和使用它们
【10月更文挑战第13天】在Java编程中,关键字和保留字是基础且重要的组成部分。正确理解和使用它们,如class、int、for、while等,不仅能够避免语法错误,还能提升代码的可读性和执行效率。本指南将通过解答常见问题,帮助你掌握Java关键字的正确使用方法,以及如何避免误用保留字,使你的代码更加高效流畅。
46 3
|
3月前
|
Java 程序员
Java 面试高频考点:static 和 final 深度剖析
本文介绍了 Java 中的 `static` 和 `final` 关键字。`static` 修饰的属性和方法属于类而非对象,所有实例共享;`final` 用于变量、方法和类,确保其不可修改或继承。两者结合可用于定义常量。文章通过具体示例详细解析了它们的用法和应用场景。
45 3