Java中final关键字的深入探究

前言

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

一. final 修饰变量

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

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

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

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

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

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

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

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

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

// 代码示例
public static void main(String[] args) {
 final Person p = new Person(20, "炭烧生蚝");
 p.setAge(18); //可以修改p对象的数据
 System.out.println(p.getAge()); //输出18

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

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

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

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

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

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

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

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

public static void main(String[] args) {
 int n1 = 2019;   //普通变量
 final int n2 = 2019; //final修饰的变量

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

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

温馨提示:整数 -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 修饰的常量会在编译期进入常量池,如果有涉及到该常量的操作,很有可能在编译期就已经完成。

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

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

public class Outter {
 public static void main(String[] args) {
  final int a = 10;
  new Thread(){
   @Override
   public void run() {
    System.out.println(a);
   }
  }.start();
 }
}

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

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

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

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

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

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

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

Compiled from "Outter.java"
final class forTest.Outter$1 extends java.lang.Thread {
 forTest.Outter$1();
 Code:
  0: aload_0
  1: invokespecial #1     // Method java/lang/Thread."<init>":()V
  4: return

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

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

3: bipush 10

我们看到 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 的代码块中。

//原代码
public static void test(){
  String s1 = "包夹方法a";
  a();
  String s2 = "包夹方法a";
}

public static final void a(){
  System.out.println("我是方法a中的代码");
  System.out.println("我是方法a中的代码");
}

//经过编译后
public static void test(){
  String s1 = "包夹方法a";
  System.out.println("我是方法a中的代码");
  System.out.println("我是方法a中的代码");
  String s2 = "包夹方法a";
}

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

三. final 修饰类

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

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

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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

时间: 2019-06-14

学习Java的static与final关键字

static:(静态修饰符)面向对象中static修饰的内容是隶属于类,而不是直接隶属于对象的,所以static修饰的成员变量一般称作类成员变量,而static修饰的方法一般称作类方法. 分类: 1.static变量,也叫作静态变量或者类变量.另一种是没有被static修饰的变量,叫实例变量. 2.static方法,也叫作静态方法或者类方法,静态方法中不能定义静态变量,实例方法也不能. 3.static代码块,静态块中,可以访问静态变量,调用静态方法. 注意事项: 1.static不依附于任何对

Java基础教程之final关键字浅析

前言 前面在讲解String时提到了final关键字,本文将对final关键字进行解析. static和final是两个我们必须掌握的关键字.不同于其他关键字,他们都有多种用法,而且在一定环境下使用,可以提高程序的运行性能,优化程序的结构.下面我们来了解一下final关键字及其用法. final从总体上来说是"不可变的",可用于修改类.方法.变量. 一. final类 final修饰的类,该类不能被继承.当你确认一个类永远不会被继承或不想被继承,那么就可以用final修饰. 同样,对于

Java中final关键字详解及实例

final在Java中可以声明成员变量.方法.类以及本地变量.一旦你将引用声明作final,你将不能改变这个引用了,如果你试图将变量再次初始化的话,编译器会报编译错误.  final的含义在不同的场景下有细微的差别,但总体来说,它指的是"不可变". 1. final变量 凡是对成员变量或者本地变量(在方法中的或者代码块中的变量称为本地变量)声明为final的都叫作final变量.final变量经常和static关键字一起使用,作为常量.用final关键字修饰的变量,只能进行一次赋值操作

Java关键字final、static使用总结

一.final 根据程序上下文环境,Java关键字final有"这是无法改变的"或者"终态的"含义,它可以修饰非抽象类.非抽象类成员方法和变量.你可能出于两种理解而需要阻止改变:设计或效率. final类不能被继承,没有子类,final类中的方法默认是final的. final方法不能被子类的方法覆盖,但可以被继承. final成员变量表示常量,只能被赋值一次,赋值后值不再改变. final不能用于修饰构造方法. 注意:父类的private成员方法是不能被子类方法覆

java关键字final用法知识点

inal:最终的,确保使用前是被赋值得,一旦赋值后不可修改. 1 数据 ①局部变量 基本数据类型: 可以先定义后赋值,但要保证在使用前是已被赋值的,一旦赋值后不可修改: 引用数据类型: 可以先定义后赋值,但要保证在使用前是已被赋值的,一旦赋值后不可修改: 引用内容不可修改,但备用用的对象内容可以被修改: ②成员变量 必须保证成员变量在使用前被赋值: 成员变量赋值的方式有两种,1声明变量时赋值,2构造函数中赋值: public static void main(String[] args) { /

浅析对Java关键字final和static的理解

一.final 根据程序上下文环境,Java关键字final有"这是无法改变的"或者"终态的"含义,它可以修饰非抽象类.非抽象类成员方法和变量.你可能出于两种理解而需要阻止改变:设计或效率. final类不能被继承,没有子类,final类中的方法默认是final的. final方法不能被子类的方法覆盖,但可以被继承. final成员变量表示常量,只能被赋值一次,赋值后值不再改变. final不能用于修饰构造方法. 注意:父类的private成员方法是不能被子类方法覆

Java关键字instanceof用法及实现策略

instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为: boolean result = obj instanceof Class 其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false. 注意:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,

说一说java关键字final和transient

首先,说说final. final关键字可以修饰变量,方法,类. final变量: 需求: 1 需要一个永不改变的编译时常量 2 一个运行时被初始化的值,不希望被更改 好处:编译时就执行的计算,减轻运行时的负担 扩展: 可以修饰基本类型和引用对象.修饰基本类型的时候,表示数值很定不变.修饰对象引用的时候,一旦引用被初始化指向一个对象,就无法再将它更改指向另一个对象(该对象本身是可以修改的) 空白final final修饰但又没有给出初始值的域 必须在域的的定义或构造器内用表达式给final赋值(

java关键字final使用方法详解

它所表示的是"这部分是无法修改的".不想被改变的原因有两个:效率.设计.使用到final的有三种情况:数据.方法.类. 一. final数据 有时候数据的恒定不变是很有用的,它能够减轻系统运行时的负担.对于这些恒定不变的数据我可以叫做"常量"."常量"主要应用与以下两个地方:1.编译期常量,永远不可改变.2.运行期初始化时,我们希望它不会被改变.对于编译期常量,它在类加载的过程就已经完成了初始化,所以当类加载完成后是不可更改的,编译期可以将它代入

java关键字static学习心得

static是java中又一重要的关键字,在一定环境下使用,可以提高程序的运行性能,优化程序的结构.其主要应用有以下几方面: 1.static修饰的成员变量,称为类变量/静态变量,以实现所有对象对该成员的共享. 2.static修饰的成员方法,称为类方法/静态方法,可直接通过类名调用,无需创建对象. 3.形成静态代码块,优化程序性能. 4.静态导包,一般将类的方法直接导入到当前类中,从而直接使用方法名即可调用类方法,更加方便. 5.修饰内部类,无需实例化外部类就可以调用内部类. 下面一附代码说明

Java中final关键字详解

谈到final关键字,想必很多人都不陌生,在使用匿名内部类的时候可能会经常用到final关键字.另外,Java中的String类就是一个final类,那么今天我们就来了解final这个关键字的用法. 主要介绍:一.final关键字的基本用法.二.深入理解final关键字 一.final关键字的基本用法 在Java中,final关键字可以用来修饰类.方法和变量(包括成员变量和局部变量).下面就从这三个方面来了解一下final关键字的基本用法. 1.修饰类 当用final修饰一个类时,表明这个类不能

java synchronized关键字的用法

0.先导的问题代码 下面的代码演示了一个计数器,两个线程同时对i进行累加的操作,各执行1000000次.我们期望的结果肯定是i=2000000.但是我们多次执行以后,会发现i的值永远小于2000000.这是因为,两个线程同时对i进行写入的时候,其中一个线程的结果会覆盖另外一个. public class AccountingSync implements Runnable { static int i = 0; public void increase() { i++; } @Override