详解java关键字final(爆肝整理,建议收藏)

作用域

修饰变量

  • 如果是基本数据类型(int,double,char)变量那么表示这个变量是不可变的,即表示一个常量,通常是大写,例如,从jvm角度解释就是java编译器在编译期间就会对其fianl变量进行检查,
public final static int NO_OPTIONS = 0;
public final static int ENCODE = 1;
  • 如果是引用类型(String,Date,Animal,Person)的变量那么表示这个引用只能指向初始化时指向的那个对象,不能再指向别的对象,但被指向的这个对象的属性值并非不能修改,例如
@Data
@AllArgsConstructor
class User {
    private int id;
    private String name;
    private int age;

    public User() {
    }
}


public class Test {

    @Test
    public void test(){
        final User user = new User(1001, "abc", 21);
        //User(id=1001, name=abc, age=21)
        System.out.println(user);
        user.setAge(22);
        //User(id=1001, name=abc, age=22)
        System.out.println(user);
    }
}

以上案例很容易看出,User类型的引用user固定指向的堆中的存储的User对象,引用后续不能发生改变,但堆中存储的user对象是可以改变的,比如age被改为了22

注意点: final修饰的变量一定要有初始化的值且只能初始化一次,否则编译会报错
在这里插入图片描述

修饰类

表示这个类是最终类,是不可被继承的。abstract与final是不能修饰同一个类的,因为当一个类被声明为abstract后就是要被用来继承,与final搭配是互相矛盾的。常见的final类有System,String…

String之所以被设计成final,或者说一个类之所以被设计为final,本意都是从安全性层面考虑的,这些类都是工程师精心设计的艺术品!艺术品易碎!用final就是拒绝继承,防止世界被熊孩子破坏,维护世界和平

修饰方法

那么这个方法就不能被重写,但不影响子类调用父类中的fianl方法,也不影响重载。fianl与abstract不能同时修饰一个方法,因为abstract修饰的方法目的就是为了被子类去重写实现的,而fianl的作用是不允许该方法被子类重写,这是互相矛盾的

String的不可变性和final间的关系

首先String类是用final关键字修饰,这说明String不可继承。再看下面,String类的主力成员字段value是个char[ ]数组,而且是用final修饰的。final修饰的字段创建以后就不可改变。有的人以为故事就这样完了,其实没有。因为虽然value是不可变,也只是value这个引用地址不可变。挡不住Array数组是可变的事实。所以String是不可变的关键都在底层的实现,而不是一个final
。

案例

public class Main {
   public static void main(String[] args) {
       String a = "wzh2";
       final String b = "wzh";
       String d = "wzh";
       String c = b + 2;
       String e = d + 2;
       System.out.println((a == c));
       System.out.println((a == e));
   }
}

这段代码的输出结果是什么呢?答案是: true 和 false

  • 变量a指的是字符串常量池中的 wzh2;
  • 变量 b 是 final 修饰的,变量 b 的值在编译时候就已经确定了它的确定值,相当于b指向了常量池的这个常量;
  • 变量 c 是 b + 2得到的,由于 b 是一个常量,所以在使用 b 的时候直接相当于使用 b 的原始值来进行计算,最后c 生成的也是一个常量,c也指向常量池的中的字符wzh2 ,而 Java 中常量池的规则是同一个字符常量只会存储一份,也就是a和c所指向的常量池空间是一样的,所以 a 和 c 是相等的!
  • d 是指向常量池中 wzh,但由于 d 不是 final 修饰,也就是说在编译及类加载的连接初始化过程使用 d 的时候不会提前知道 d 的值是什么,所以在计算 e 的时候只能使用的是 d 的引用计算,变量d的访问却需要在运行时通过链接来进行,所以这种计算会在堆上生成wzh2 ,所以最终 e 指向的是堆上的 wzh2 , 所以 a 和 e 不相等。

总结:a、c是常量池的wzh2,e是堆上的wzh2

深入分析

在这里插入图片描述

对应的字节码文件

 0 ldc #2 <wzh2>   
 <!--从常量池把字符串wzh2加载到操作数栈顶,#2是一个索引值,代表常量池中的第2个常量-->
 2 astore_1  <!--把字符串类型的变量wzh2存储到第一个局部变量表-->

<!--以上两行等价于String a = "wzh2";-->

 3 ldc #3 <wzh>
 5 astore_2

<!--以上两行等价于final String b = "wzh";-->

 6 ldc #3 <wzh>
 8 astore_3

<!--以上两行等价于String d = "wzh";-->

9 ldc #2 <wzh2>
11 astore 4

<!--以上两行等价于String c = b + 2;因为这里String类型的引用c是被final修饰的,
所以在编译期间即可确定下来引用c所指向的是常量池中的字符串常量wzh2-->

13 new #4 <java/lang/StringBuilder> 
<!--堆中分配一块区域用于创建StringBuider对象,相当于StringBuilder sb = new StringBuilder();
这里StringBuilder是用于做String字符串的+(加号)拼接操作-->

16 dup
17 invokespecial #5 <java/lang/StringBuilder.<init> : ()V> 
<!--执行StringBuilder的构造方法-->
20 aload_3 
<!--加载字符串索引为#3的常量(即String d = "wzh"这个常量)进操作数栈-->

21 invokevirtual #6 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>  
24 iconst_2
25 invokevirtual #7 <java/lang/StringBuilder.append : (I)Ljava/lang/StringBuilder;>

<!--第21到25行,是指执行StringBuilder的append方法进行拼接-->
<!--第21行,相当于首次执行拼接,sb.append("wzh"),也就是拼接即String d = "wzh"这个常量-->
<!--第24行,相当从常量池中加载2这个int类型的常量-->
<!--第25行,相当于执行拼接,sb.append("2")-->

28 invokevirtual #8 <java/lang/StringBuilder.toString : ()Ljava/lang/String;> 
<!--这一步是调用sb的toString这是将其转化为String类型,看源码会发现该方法StringBuilder.toString最终返回的结果的是一个new String("xxx"),很明显,
这个字符串是存储在堆中的字符串-->
31 astore 5  <!--将上一步返回的结果再储局部变量表-->

扩展练习

public class Main {
   public static void main(String[] args) {
       final String b = "wzh";
       String c = b + 2;
       String e = new String("wzh");
       String f = new String("wzh3");
       String g = e + 4;
   }
}

上述代码共生成了多少对象呢,总共7个

  • 第一行代码在字符串常量池生成wzh
  • 第二行代码只会会直接在字符串常量池生成wzh2,因为final常量在编译器即可确认值
  • 第三行代码只会在堆上生成一个字符串对象
  • 第四行代码会在堆上生成一个字符串对象,同时在字符串常量池生成字符对象wzh3
  • 第五行代码底层会生成一个StringBuilder来通过append方法做拼接,拼接完后会通过toString方法成会在堆上生成一个字符串对象,注意,这里虽然StringBuilder的toString底层是调用new String方法创建的拼接对象,但只有涉及到字面量的情况下才会在常量池生成,案例如下
String str = new String("wzh4");

因为这里涉及到字面量"wzh4",所以才会在字符串常量池生成常量

使用final的好处

  • final方法比非final快一些
  • final关键字提高了性能。JVM和Java应用都会缓存final变量。
  • final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
  • 使用final关键字,JVM会对方法、变量及类进行优化
  • 11
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值