当前位置: 首页 > news >正文

【Java基础】Java 中 的`final` 关键字

前言

在 Java 编程的世界里,final 关键字是一个强大且常用的工具。它可以应用于类、方法和变量,赋予它们不同的 “不可变” 特性。

一、final 修饰类

1. 定义与特性

当使用 final 修饰一个类时,这个类就如同被上了一把坚固的锁,不能被其他类继承,即不会有子类。这是 Java 语言精心设计的一种机制,目的是确保类的设计和实现不会被意外修改,从而保证类的行为和功能始终保持一致。

2. 示例代码

// 定义一个 final 类
final class FinalClass {
    public void printMessage() {
        System.out.println("This is a final class.");
    }
}

// 以下代码会编译错误,因为 FinalClass 是 final 类,不能被继承
// class SubClass extends FinalClass {
// }

public class Main {
    public static void main(String[] args) {
        FinalClass fc = new FinalClass();
        fc.printMessage();
    }
}

3. 代码解释

在上述代码中,FinalClass 被明确声明为 final 类。当我们试图创建它的子类 SubClass 时,编译器会立刻抛出错误,这是因为 Java 严格禁止继承 final 类。这种限制就像是给类加上了一层保护罩,确保其内部的实现不会被外部通过继承的方式随意改变。

4. 应用场景

  • 安全性:在 Java 的核心类库中,有许多重要的类被设计成 final 类,例如 java.lang.String。这是因为字符串在 Java 里是广泛使用的基础数据类型,为了防止用户通过继承来修改其行为,保证字符串操作的安全性和一致性,将其设计为 final 类是非常必要的。
  • 性能优化:由于 final 类不能被继承,编译器在处理这类类时可以进行更多的优化。例如,在调用 final 类的方法时,编译器能够直接确定方法的具体实现,避免了动态绑定所带来的开销,从而显著提高代码的执行效率。

二、final 修饰方法

1. 定义与特性

final 修饰一个方法时,这个方法就像是被贴上了“禁止修改”的标签,不能被其子类重写(覆盖)。这一特性确保了方法的实现逻辑在整个继承体系中始终保持不变,保证了方法行为的一致性和稳定性。

2. 示例代码

class ParentClass {
    // 定义一个 final 方法
    public final void finalMethod() {
        System.out.println("This is a final method.");
    }

    public void nonFinalMethod() {
        System.out.println("This is a non-final method.");
    }
}

class ChildClass extends ParentClass {
    // 以下代码会编译错误,因为 finalMethod 是 final 方法,不能被重写
    // @Override
    // public void finalMethod() {
    //     System.out.println("Trying to override final method.");
    // }

    @Override
    public void nonFinalMethod() {
        System.out.println("Overriding non-final method.");
    }
}

public class Main {
    public static void main(String[] args) {
        ChildClass cc = new ChildClass();
        cc.finalMethod();
        cc.nonFinalMethod();
    }
}

3. 代码解释

在上述代码中,ParentClass 中的 finalMethod 被声明为 final 方法。当 ChildClass 尝试重写该方法时,编译器会报错,这清晰地体现了 final 方法的不可重写特性。而 nonFinalMethod 没有被 final 修饰,所以可以在 ChildClass 中被重写,以满足不同的业务需求。

4. 应用场景

  • 保证方法实现的一致性:当一个方法的实现逻辑是核心且不希望被子类改变时,我们可以将其声明为 final 方法。例如,Object 类中的 getClass() 方法就是 final 方法,它确保了在任何情况下获取对象类信息的方式都是一致的,不会因为子类的重写而产生混乱。
  • 性能优化final 方法在调用时可以避免动态绑定的过程,因为编译器在编译时就能够确定方法的具体实现。这样一来,方法调用的效率得到了显著提高,尤其是在频繁调用的场景下,性能提升更为明显。

三、final 修饰变量

1. 基本数据类型变量

对于基本数据类型的变量,一旦使用 final 修饰,它就摇身一变成为了常量。这个常量的值一旦被赋值,就如同被封印一般,不能再被改变。并且,final 修饰的变量必须要有初始值,这是 Java 语言的硬性规定。因为一旦变量被声明为 final,它就失去了重新赋值的资格,如果没有初始值,就会导致变量永远处于未赋值的状态,这在 Java 中是不被允许的。

示例代码
public class Main {
    public static void main(String[] args) {
        // 正确,给 final 变量赋初始值
        final int num = 10;
        // 以下代码会编译错误,因为 num 是 final 变量,不能被重新赋值
        // num = 20;
        System.out.println("The value of num is: " + num);

        // 以下代码会编译错误,因为 final 变量必须有初始值
        // final int uninitialized;
        // uninitialized = 30;
    }
}
代码解释

在上述代码中,num 被声明为 final 变量并赋予初始值 10。此后,任何试图重新赋值的操作都会引发编译错误。同时,如果声明一个 final 变量但没有立即赋值,后续再进行赋值同样会报错,这再次强调了 final 变量必须在声明时就确定初始值。

2. 引用数据类型变量

对于引用数据类型的变量,final 修饰的效果稍有不同。当一个引用类型的变量被 final 修饰后,它所引用的对象就像被固定在了一个位置,不能再被改变指向其他对象。然而,对象的内容是可以被修改的。同样,final 修饰的引用类型变量也必须在声明时或构造函数中进行初始化。

示例代码
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // 声明并初始化一个 final 引用类型变量
        final List<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        System.out.println("List elements: " + list);

        // 以下代码会编译错误,因为 list 是 final 变量,不能被重新赋值
        // list = new ArrayList<>();
    }
}
代码解释

在上述代码中,list 被声明为 final 变量,它引用了一个 ArrayList 对象。我们可以自由地向这个 ArrayList 中添加或删除元素,也就是改变对象的内容。但是,一旦 list 引用了某个 ArrayList 对象,就不能再让它引用另一个 ArrayList 对象,否则会引发编译错误。

3. 静态常量(private static final

private static final 组合常常被用于定义类的静态常量。其中,private 关键字的作用是将该常量的访问权限限制在类的内部,确保其不会被外部随意访问和修改;static 表示该常量属于类本身,而不是类的某个实例,这意味着无论创建多少个类的实例,该常量只有一份副本;final 则保证了该常量的值一旦确定,就不能再被改变。这种常量在编译时就已经被确定,并且在整个程序的运行过程中始终保持不变。

示例代码
public class Constants {
    private static final int MAX_VALUE = 100;

    public static void main(String[] args) {
        System.out.println("The maximum value is: " + MAX_VALUE);
    }
}
代码解释

在上述代码中,MAX_VALUE 是一个使用 private static final 修饰的静态常量。它只能在 Constants 类的内部被访问,并且其值不能被修改。这种方式常用于定义一些全局的、不可变的常量,能够大大提高代码的可读性和可维护性。

4. final 变量在不同位置的初始化

  • 成员变量final 成员变量的初始化方式比较灵活,既可以在声明时直接进行初始化,也可以在构造函数中完成初始化。但需要特别注意的是,必须保证在每个构造函数中都对其进行初始化,否则会引发编译错误。
class FinalMemberVariable {
    // 声明时初始化
    final int num1 = 10;
    final int num2;

    // 构造函数中初始化
    public FinalMemberVariable() {
        num2 = 20;
    }
}
  • 局部变量final 局部变量的初始化规则相对简单,它必须在使用前进行初始化,一旦完成初始化,就不能再对其进行重新赋值。
public class FinalLocalVariable {
    public static void main(String[] args) {
        final int num;
        num = 30; // 初始化
        // num = 40; // 编译错误,不能重新赋值
        System.out.println(num);
    }
}

5. final 变量和不可变对象的区别

虽然 final 修饰引用类型变量时,引用不可变,但对象内容可变,而不可变对象是指对象一旦创建,其内部状态就不能被改变。例如,String 类就是不可变对象,即使不使用 final 修饰,其内容也不能被修改。

String str = "Hello";
// 下面这行代码实际上是创建了一个新的 String 对象,而不是修改原对象
str = str + " World"; 

与之对比,使用 final 修饰的 List,虽然引用不能变,但可以修改列表内容。

final List<String> finalList = new ArrayList<>();
finalList.add("Element"); // 可以修改列表内容

四、final 修饰的常量在编译时的替换原理

1. 原理概述

final 修饰的常量是基本数据类型或字符串常量时,在编译阶段,编译器会如同一个智能的替换机器,将代码中对该常量的引用直接替换为常量的值。这是因为编译器在编译时就已经确切地知道了常量的值,为了提高代码的执行性能,它会直接将常量的值嵌入到代码中,从而避免了在运行时对常量的额外访问。

2. 示例代码及编译后分析

编写的代码
public class CompileTimeReplacement {
    private static final int NUM = 10;

    public static void main(String[] args) {
        int result = NUM * 2;
        System.out.println("The result is: " + result);
    }
}
编译后的代码(伪代码表示)
public class CompileTimeReplacement {
    public static void main(String[] args) {
        int result = 10 * 2; // 编译器直接将 NUM 替换为 10
        System.out.println("The result is: " + result);
    }
}
代码解释

在上述代码中,NUM 是一个 final 常量。在编译过程中,编译器会将 result = NUM * 2; 这行代码中的 NUM 直接替换为其值 10,得到 result = 10 * 2;。这样一来,在运行时就不需要再去访问 NUM 这个常量,直接进行计算即可,大大提高了代码的执行效率。

3. 注意事项

需要注意的是,只有当 final 常量的值在编译时就能够确定的情况下,才会进行替换操作。如果 final 常量的值是在运行时才能确定的,那么编译器就无法进行替换。

import java.util.Random;

public class NonCompileTimeReplacement {
    private static final int NUM;

    static {
        Random random = new Random();
        NUM = random.nextInt(100); // 运行时确定值
    }

    public static void main(String[] args) {
        int result = NUM * 2;
        System.out.println("The result is: " + result);
    }
}

在这个例子中,NUM 的值是在运行时通过 Random 类随机生成的,编译器在编译时无法确定其具体值,所以不会对代码中的 NUM 进行替换操作。

4. 对代码维护的影响

编译时替换虽然提高了性能,但也会对代码维护产生一定影响。如果常量的值需要修改,仅仅修改常量的定义是不够的,因为已经编译的代码中常量引用已经被替换。这就需要重新编译所有引用该常量的代码,否则可能会出现不一致的情况。

五、final 与多线程

1. 线程安全性

final 变量在多线程环境下展现出了良好的线程安全性。由于 final 变量一旦完成初始化,其值就不能再被修改,所以多个线程可以安全地访问 final 变量,而无需额外的同步机制来保证数据的一致性。

2. 示例代码

public class FinalInMultiThreading {
    private final int num;

    public FinalInMultiThreading(int num) {
        this.num = num;
    }

    public int getNum() {
        return num;
    }

    public static void main(String[] args) {
        FinalInMultiThreading obj = new FinalInMultiThreading(10);

        // 线程 1 访问 num
        Thread thread1 = new Thread(() -> {
            System.out.println("Thread 1: " + obj.getNum());
        });

        // 线程 2 访问 num
        Thread thread2 = new Thread(() -> {
            System.out.println("Thread 2: " + obj.getNum());
        });

        thread1.start();
        thread2.start();
    }
}

3. 代码解释

在上述代码中,num 是一个 final 成员变量。多个线程可以同时访问 obj.getNum() 方法,而不会出现数据不一致的问题。这是因为 num 的值一旦在构造函数中被初始化,就不会再被改变,各个线程读取到的始终是同一个稳定的值。

4. 与 volatile 关键字的对比

volatile 关键字主要用于保证变量的可见性,即一个线程修改了 volatile 变量的值,其他线程能立即看到最新的值。而 final 关键字主要用于保证变量的不可变性。在多线程场景中,如果一个变量既需要保证不可变,又需要保证可见性,那么可以结合使用 finalvolatile 关键字。

class VolatileFinalExample {
    private final int finalValue;
    private volatile boolean isInitialized;

    public VolatileFinalExample(int value) {
        this.finalValue = value;
        this.isInitialized = true;
    }

    public int getFinalValue() {
        if (isInitialized) {
            return finalValue;
        }
        return -1;
    }
}

总结

final 关键字在 Java 中无疑是一个功能强大且用途广泛的工具,它可以灵活地修饰类、方法和变量,赋予它们不同的“不可变”特性。

  • final 类不能被继承,这为类的安全性和一致性提供了坚实的保障,同时也为编译器进行性能优化创造了有利条件。
  • final 方法不能被重写,确保了方法实现的稳定性,避免了子类意外修改方法逻辑,显著提高了方法调用的效率。
  • final 变量一旦赋值就不能再被重新赋值,对于基本数据类型而言是值不可变,对于引用数据类型则是引用不可变,但对象内容可变。而且,final 变量必须有初始值,并且在不同的位置有不同的初始化要求。
  • private static final 组合常用于定义类的静态常量,这种方式能够极大地提高代码的可读性和可维护性。
  • final 修饰的常量在编译时,如果其值在编译时就能确定,会被直接替换为常量的值,从而有效地提高了代码的执行效率,但也会对代码维护产生一定影响。
  • 在多线程环境下,final 变量具有一定的线程安全性,多个线程可以安全地访问 final 变量,无需担心数据不一致的问题。同时,可结合 volatile 关键字满足更多场景需求

相关文章:

  • 17年哪个网站做h5最好我想学做互联网怎么入手
  • 可以和朋友合资做网站吗seo优化网站优化
  • 安徽安搜做的网站怎么样百度小说网
  • 东莞兼职招聘网最新招聘湖南网站优化
  • 怎么做样网站二级域名网站免费建站
  • 用家里的电脑做网站服务器郑州seo优化顾问
  • Cocos Creator3.8.6拖拽物体的几种方式
  • rust学习笔记8-枚举与模式匹配
  • 使用Pycharm创建第一个Python程序
  • Maven入门教程
  • AI在投资和金融领域有什么用法和提示词
  • Redis持久化方案RDB和AOF
  • 零信任架构
  • C语言408考研先行课第一课:数据类型
  • 一个基于vue3的图片瀑布流组件
  • FFmpeg av_read_frame 和iOS系统提供的 AVAudioRecorder 实现音频录制的区别
  • redis开启过期监听
  • 《CWAP-404》,第一章:802.11 协议(1.1~1.3)
  • 搭建gn环境踩坑存档
  • 网络原理---TCP/IP
  • Windows对比MacOS
  • 头歌实验---C/C++程序设计:实验三:选择结构程序设计进阶
  • <Revit二次开发>详细介绍Autodesk.Revit.DB.HostObject类的FindInserts 方法
  • Java 大视界 —— Java 大数据在智慧能源微电网能量管理中的关键技术(100)
  • TVbox蜂蜜影视:智能电视观影新选择,简洁界面与强大功能兼具
  • 如何在 WPS 中集成 DeepSeek