记一次由 final 引发的小意外 | Java Debug 笔记

简介: 记一次由 final 引发的小意外 | Java Debug 笔记

final 基本用法



先来大致回顾一下 final 的作用:


  • 当使用 final 修饰类的时候,该类不能被继承
  • 当使用 final 修饰方法的时候,该方法不能够被子类覆盖
  • 当使用 final 修饰变量的时候,该变量不能被二次赋值


由 final 引发的意外



日常开发中经常会使用到常量类,类中的常量通常会使用 final 进行修饰。


在一次常规版本更新时,部署完成之后才想起来要修改一个不太重要的常量。于是直接通过修改常量类的 .class 文件的方式修改常量。


但是后来通过日志发现,程序中仍然使用的是修改前的常量值。


静下来思考和查阅资料发现,问题在于修改的常量是使用 final 修饰。 Java 编译器会认为使用 final 修饰的基本类型和 String 类型是稳定态(Immutable Status),于是将这些 final 修饰的基本类型和 String 类型的值直接编译到使用它们的类的字节码中。以避免运行时引用(Runtime Reference)。


// 常量类
public class Constants {
    public final static String TOKEN_PREFIX = "token:";
}
// 使用到常量的类
public class Main {
    public static void main(String[] args) {
        System.out.println(Constants.TOKEN_PREFIX);
    }
}
复制代码


也就是在编译 Main 类的时候,由于使用到的 Constants.TOKEN_PREFIX 是经过 final 修饰的。所以会直接将该变量的值("token:")直接编译进 Main.class 文件,而不会在运行时再通过 Constants 类引用获取值。这里有点像 C 语言中的宏替换,是 Java 针对 final 做的合理优化。


但是由于我的 “偷懒” ,想直接通过修改常量类的 .class 文件类更新常量。这时候的效果相当于我只是修改了 Constants 类的源代码并重新编译 Constants 类,而没有重新编译使用到 Constants.TOKEN_PREFIX 的 Main 类。所以这时候 Main.class 没有任何变化,使用的仍然是旧的常量值。


结论,final 变量和普通变量的区别为:当 final 变量是基本数据类型以及 String 类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。也就是说在用到该 final 变量的地方,相当于直接访问的这个常量,不需要在运行时确定。以及当源码发生修改还是重新打包发布更加稳妥。


final 的其他注意事项



  • 使用 final 修饰不能保证所指向的对象中的内容不变


当使用 fianl 修饰对象的时候,只能保证变量不能再指向其他对象,但不能保证所指向的对象中的内容不变:


public static void main(String[] args) {
    final List<String> list = new ArrayList<String>();
//        list = new ArrayList<>(); // 不允许指向新的对象
    list.add("1"); // 允许修改对象中的内容
}
复制代码


  • final & static 区别


static 主要强调变量的存储位置。当类被加载的时候,JVM 会为所有被 static 修饰的内容分配空间(存储),内存中只有一个拷贝,不会被分配多次。变量的地址不会发生改变,但是变量指向的内容可变。


final 主要强调的是可变性。当变量被 final 修饰之后,该变量不能指向其他内容。


  • final 不是导致参数不变的原因


不少人有这么一个习惯,就是出于防止在方法内修改了外部变量的目的。喜欢使用 final 对方法的形参进行修饰。


但其实变量 final 不是导致参数不变的原因,我们从基本类型、String类型和引用类型几个方面进行测试。


先看看基本类型 & String 类型:


public class Main {
    public static void main(String[] args) {
        TestFinalClass testFinalClass = new TestFinalClass();
        // 测试基本数据类型 int
        int i = 30;
        System.out.println(i); // 打印 30
        testFinalClass.testIntMethod(i);
        System.out.println(i); // 打印 30
        // 使用包装类型
        Integer integer = new Integer(30);
        System.out.println(integer); // 打印 30
        testFinalClass.testIntegerMethod(integer);
        System.out.println(integer); // 打印 30
        // 测试字面量 string
        String string = "origin string";
//        String string = new String("origin string"); // 测试 String 对象,打印结果一致
        System.out.println(string); // 打印 origin string
        testFinalClass.testStringMethod(string);
        System.out.println(string); // 打印 origin string
    }
}
class TestFinalClass {
    public void testIntMethod(int i) {
        i++;
    }
    public void testIntegerMethod(Integer integer) {
        integer = new Integer(60);
    }
    public void testStringMethod(String string) {
        string = "change string";
    }
}
复制代码


由此可知,无论是使用基本数据类型或是其包装类型,字面量的字符串还是 String 对象。外部变量转换为实参传递给方法都是通过“值复制传递”的方式,无论方法形参是否使用 final 修饰都不影响外部变量。


只是使用 fianl 修饰时,会对方法内修改的代码报语法错误。


再看看引用类型:


public class Main {
    public static void main(String[] args) {
        TestFinalClass testFinalClass = new TestFinalClass();
        List<String> list = new ArrayList<String>();
        list.add("1");
        System.out.println(list); // 打印 [1]
        testFinalClass.testMethod(list);
//        testFinalClass.testFinalMehod(list); // 打印结果一致
        System.out.println(list); // 打印 [1, 2]
    }
}
class TestFinalClass {
    public void testMethod(List list) {
        list.add("2");
    }
    public void testFinalMehod(final List list) {
        list.add("2");
    }
}
复制代码


无论形参是否被 final 修饰,方法内都可以通过调用 List 的方法来实现对外部变量进行修改,说明引用类型的参数传递是通过“地址传递”。


只是使用 fianl 修饰时,无法对变量进行二次赋值(让 list 指向一个新的对象)。


再来看看如果是在非 final 修饰形参的方法里,重新赋值 list 会发生什么:


public class Main {
    public static void main(String[] args) {
        TestFinalClass testFinalClass = new TestFinalClass();
        List<String> outside = new ArrayList<String>();
        outside.add("1");
        System.out.println(outside);
        testFinalClass.testMethod(outside);
        System.out.println(outside);
    }
}
class TestFinalClass {
    public void testMethod(List inside) {
//        inside = new ArrayList<String>(); // 先重新赋值再调用 add 方法,打印结果是 [1] 和 [1]
        inside.add("2");
        inside = new ArrayList<String>(); // 先调用 add 方法再重新赋值,打印结果是 [1] 和 [1, 2]
    }
}
复制代码


为什么会出现这样的结果?在调用 add 方法的前后重新赋值会导致结果不一致?


其实这还是因为对于参数是引用类型的时候,是使用“地址传递”的方式所致的:


  • 对于先调用 add 方法再重新赋值的情况:
  1. 首先 outside 变量先指向一个 ArrayList 对象的堆内存地址,举例为 0x00ff4233
  2. outside 传递到方法内,由于是引用类型,所以进行的是“地址传递”。这时候 inside 和 outside 变量都指向 0x00ff4233 堆内存地址
  3. 使用 inside 调用 add 方法,实际上是操作 0x00ff4233 内存里的 ArrayList 对象,和外部变量 outside 是同一对象,所以对外部变量产生了影响
  4. 重新 new 一个 ArrayList 对象(在堆内存重新申请了 ArrayList 对象的新地址),并将 inside 指向它
  • 对于先重新赋值再调用 add 方法的情况:
  1. 首先 outside 变量先指向一个 ArrayList 对象的堆内存地址,举例为 0x00ff4233
  2. outside 传递到方法内,由于是引用类型,所以进行的是“地址传递”。这时候 inside 和 outside 变量都指向 0x00ff4233 堆内存地址
  3. 直接重新 new 一个 ArrayList 对象(在堆内存重新申请了 ArrayList 对象的新地址),并将 inside 指向它,这时候相当于丢失了对原地址的引用
  4. 使用 inside 调用 add 方法,实际操作的是新创建的 ArrayList 对象,不是刚开始传入的外部变量


总结:方法的形参是否用 final 修饰,不是决定外部变量能否被修改的原因。外部变量能否被修改是由 Java 的参数传递机制决定的。该机制限定了当传递的是基本数据类型(包括其包装类型)或是字符串(字面量或是 String 对象)类型时是通过“值复制传递”方式进行传递,而引用类型是通过“地址传递”方式进行传递。

相关文章
|
3月前
|
存储 缓存 安全
除了变量,final还能修饰哪些Java元素
在Java中,final关键字不仅可以修饰变量,还可以用于修饰类、方法和参数。修饰类时,该类不能被继承;修饰方法时,方法不能被重写;修饰参数时,参数在方法体内不能被修改。
42 2
|
28天前
|
前端开发 JavaScript Java
Java构建工具-maven的复习笔记【适用于复习】
这篇文档由「潜意识Java」创作,主要介绍Maven的相关知识。内容涵盖Maven的基本概念、作用、项目导入步骤、依赖管理(包括依赖配置、代码示例、总结)、依赖传递、依赖范围以及依赖的生命周期等七个方面。作者擅长前端开发,秉持“得之坦然,失之淡然”的座右铭。期待您的点赞、关注和收藏,这将是作者持续创作的动力! [个人主页](https://blog.csdn.net/weixin_73355603?spm=1000.2115.3001.5343)
35 3
|
4月前
|
Java 开发工具 Android开发
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
本系列教程笔记详细讲解了Kotlin语法,适合需要深入了解Kotlin的开发者。若需快速学习Kotlin,建议查看“简洁”系列教程。本期重点介绍了Kotlin与Java的共存方式,包括属性、单例对象、默认参数方法、包方法、扩展方法以及内部类和成员的互操作性。通过这些内容,帮助你在项目中更好地结合使用这两种语言。
69 1
|
28天前
|
存储 Java 开发者
【潜意识Java】深入详细理解分析Java中的toString()方法重写完整笔记总结,超级详细。
本文详细介绍了 Java 中 `toString()` 方法的重写技巧及其重要
49 10
【潜意识Java】深入详细理解分析Java中的toString()方法重写完整笔记总结,超级详细。
|
2月前
|
安全 Java 编译器
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
|
2月前
|
Java 开发工具 Android开发
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
|
3月前
|
Java 编译器 Android开发
Kotlin教程笔记(28) -Kotlin 与 Java 混编
Kotlin教程笔记(28) -Kotlin 与 Java 混编
58 2
|
3月前
|
Java
final 在 java 中有什么作用
在 Java 中,`final` 关键字用于限制变量、方法和类的修改或继承。对变量使用 `final` 可使其成为常量;对方法使用 `final` 禁止其被重写;对类使用 `final` 禁止其被继承。
57 1
|
2月前
|
Java 数据库连接 编译器
Kotlin教程笔记(29) -Kotlin 兼容 Java 遇到的最大的“坑”
Kotlin教程笔记(29) -Kotlin 兼容 Java 遇到的最大的“坑”
82 0

热门文章

最新文章