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方法.

 

 

探究final在java中的作用的更多相关文章

  1. Java中...的作用

    Java中...的作用,代表接收若干个相同类型的参数 public void testFunction(int...arr){    //接收若干个int类型的参数     for (int i:ar ...

  2. [转载]java中import作用详解

    [转载]java中import作用详解 来源: https://blog.csdn.net/qq_25665807/article/details/74747868 这篇博客讲的真的很清楚,这个作者很 ...

  3. final 在 java 中有什么作用?(未完成)

    final 在 java 中有什么作用?(未完成)

  4. 黑马程序员 Java正则表达式,详解反斜线在Java中的作用

    ---------------------- ASP.Net+Android+IO开发S. .Net培训.期待与您交流! ---------------------- 在程序设计过程中,经常需要对获取 ...

  5. String的不变性到final在java中用法

    final在Java语言里面啥意思 final修饰一个类,那么这个类就是不可继承.string就是一个非常有名的被final修饰的类,不过他的更加有名的是“不可被修改”. 究竟什么是不可改变?stri ...

  6. java中static作用详解

    static表示“全局”或者“静态”的意思,用来修饰成员变量和成员方法,也可以形成静态static代码块,但是Java语言中没有全局变量的概念. 被static修饰的成员变量和成员方法独立于该类的任何 ...

  7. Java中static作用及用法详解(转)

    1.1概述: static是静态修饰符,什么叫静态修饰符呢?大家都知道,在程序中任何变量或者代码都是在编译时由系统自动分配内存来存储的,而所谓静态就是指在编译后所分配的内存会一直存在,直到程序退出内存 ...

  8. 浅谈Java中static作用--转

    static表示“全局”或者“静态”的意思,用来修饰成员变量和成员方法,也可以形成静态static代码块,但是Java语言中没有全局变量的概念. 被static修饰的成员变量和成员方法独立于该类的任何 ...

  9. Java中static作用及使用方法具体解释

    1.1概述: static是静态修饰符,什么叫静态修饰符呢?大家都知道,在程序中不论什么变量或者代码都是在编译时由系统自己主动分配内存来存储的.而所谓静态就是指在编译后所分配的内存会一直存在.直到程序 ...

随机推荐

  1. 新生入学V3.0颗粒归仓

    新生入学系统V3.0接近尾声.每次做项目都有不一样的收获.V1.0,V2.0主要是熟悉了整个项目流程是怎样进行的,可行性分析--需求分析(原型图Axure)--实体设计(PD)--类图时序图(EA)- ...

  2. 【转载】C# sleep 和wait的区别

    eep和wait都是使线程暂时停止执行的方法,但它们有很大的不同. 1. sleep是线程类Thread 的方法,它是使当前线程暂时睡眠,可以放在任何位置. 而wait,它是使当前线程暂时放弃对象的使 ...

  3. mvc5新特性RouteAttribute特征路由

    mvc5新特性RouteAttribute特征路由,本文讲述如何开启mvc5的RouteAttribute路由功能并附上一个实例说明RouteAttribute是怎么工作的 mvc5新特性RouteA ...

  4. net spy memcached 使用demo

    package memcached; import java.io.IOException; import java.net.InetSocketAddress; import net.spy.mem ...

  5. java类加载机制的代码实例

    package typeinfo; import java.util.Random; class Initable { static final int staticFinal = 47 ; stat ...

  6. F08标准中Open命令的newunit选项

    从gfortran 4.5开始Open命令开始支持newunit选项,示例如下: integer :: u open(newunit=u, file="log.txt", posi ...

  7. Ajax_HTTP请求以及响应

    什么是HTTP请求? 就是从用户的浏览器端向服务器端发送请求 一个HTTP请求一般由四个部分组成 1.HTTP请求的方法或者动作,比如GET或者POST请求 2.请求的URL,也就是请求的地址 3.请 ...

  8. object-c iOS 教程 git for mac

    本文转载至 http://blog.csdn.net/u011728347/article/details/10035191   http://rypress.com/tutorials/object ...

  9. hadoop yarn namenode datanoe 启动异常问题解决 分析日志

    cat logs/hadoop-root-datanode-hadoop1.log ********************************************************** ...

  10. Aspose 直接插入SQL Server DataTalbe

    原文链接:http://www.cnblogs.com/hellohongfu/p/7362830.html 下面的代码可以根据excel文件,生成创建表的SQL,以及测试InsertSQL .方法将 ...