Java final关键字修饰类、方法、变量的不同作用及实战指南

 更新时间:2026年01月08日 10:47:36   作者:普通网友  
在Java中final关键字是一个修饰符,它用于表示某个类、方法或变量是不可变的或不可继承的,这篇文章主要介绍了Java final关键字修饰类、方法、变量的不同作用及实战指南,需要的朋友可以参考下

一、引言

你真的懂 final 吗?我见过不止两个项目因为误用 final 栽了大跟头 —— 前年电商大促,核心库存类没加 final 被新人继承重写,库存扣减逻辑被篡改,直接造成超卖损失几十万;还有次分布式系统中,接口返回的 final 集合被研发强行修改内容,引发上下游数据不一致。很多人以为 final 就只是 “不能改”,却忽略了它在线程安全、JVM 优化、代码健壮性上的关键作用。你大概率也踩过 “final 变量能改内容”“final 方法能变相重写” 的坑,读完这篇,我会把踩过的血泪教训和生产级用法全告诉你,让你彻底用对 final。

二、核心知识点解析

1. 从 final 变量开始:为什么新手总搞混 “不可变”?

我带过的实习生里,80% 都犯过这个错:写了final List<String> list = new ArrayList<>();,然后往里面 add 元素,发现居然能加,就吐槽 “final 没用”。这是最典型的误解 —— 新手把 “引用不可变” 和 “对象不可变” 混为一谈,甚至有人觉得 final 就是 “常量” 的代名词。

说白了,final 变量的核心是 **“赋值后不能改指向”**:修饰基本类型(int/long)时,是值不能改;修饰引用类型(对象 / 集合)时,是指向的内存地址不能改,但地址里的对象内容该咋改还咋改。就像你有把 final 的快递柜钥匙,钥匙本身不能换,但你能往柜子里放、拿东西。

final 修饰类型不可变的对象可变的对象
基本类型(int)变量的值
引用类型(List)变量的内存地址集合里的元素
// Java 17+
public class FinalVariableDemo {
    public static void main(String[] args) {
        // ❌ 新手错误认知:final集合不能加元素
        final List<String> goodsList = new ArrayList<>();
        goodsList.add("手机"); // 能执行!因为只是改集合内容,没改地址
        System.out.println(goodsList); // 输出:[手机]

        // ✅ 真正的不可变:地址和内容都不能改
        final List<String> immutableList = List.of("电脑", "平板");
        // immutableList.add("耳机"); // 运行时报错:UnsupportedOperationException
    }
}

2. final 方法:重写的坑比你想的深

我曾经在支付项目里见过这个 bug:核心的签名验证方法verifySign()没加 final,被子类重写后,验证逻辑被改得稀碎,导致支付回调验签失败。很多人觉得 “方法加 final 只是不能重写”,却不知道 JVM 对 final 方法有优化 —— 因为不会被重写,JIT 编译时能直接内联,性能提升 10% 左右(我在压测中实测过)。

⚠️ 关键提醒:子类可以定义和 final 方法同名的方法,但这不是 “重写”(术语:方法隐藏),只是新方法,多态场景下会出逻辑混乱。就像你爸把家门锁死(final),你没法撬锁,但能在隔壁自己装个新锁,看似一样,实则不是一个门。

// Java 17+
public class FinalMethodDemo {
    public static void main(String[] args) {
        Parent parent = new Child();
        parent.pay(); // 输出:父类支付逻辑(不是重写,是隐藏)
    }
}

class Parent {
    final void pay() {
        System.out.println("父类支付逻辑"); // final方法不能被重写
    }
}

class Child extends Parent {
    // 不是重写,只是子类新方法
    void pay() {
        System.out.println("子类支付逻辑");
    }
}

3. final 类:什么时候该把类封死?

我见过有人把所有工具类都加 final,也见过有人给核心业务类漏加 final—— 前者导致扩展难,后者被恶意继承篡改逻辑。final 类的核心作用是 “封死继承”,适合两类场景:一是工具类(如 java.lang.Math),不需要扩展且要保证逻辑稳定;二是核心业务类(如订单类),防止继承篡改核心逻辑。

💡 小技巧:Java 17 + 中,final 类和 sealed 类(密封类)是互补的 ——final 是 “完全不让继承”,sealed 是 “只让指定类继承”,根据场景选就行。

// Java 17+
final class OrderService { // 订单核心类,封死继承
    public void createOrder() {
        System.out.println("创建订单(核心逻辑,禁止篡改)");
    }
}

// class OrderServiceExt extends OrderService {} // 编译报错:无法继承final类

三、实战代码

示例 1:基础用法(Java 17+)

// Java 17+
public class FinalBasicDemo {
    // final静态常量(全大写,命名规范)
    public static final double TAX_RATE = 0.06;
    // final实例变量(必须初始化)
    private final String orderId;

    // 构造器初始化final实例变量
    public FinalBasicDemo(String orderId) {
        this.orderId = orderId; // 只能赋值一次
    }

    // final方法(不能被重写)
    final String getOrderId() {
        return this.orderId;
    }

    public static void main(String[] args) {
        FinalBasicDemo demo = new FinalBasicDemo("ORD2025001");
        System.out.println(demo.getOrderId()); // 输出:ORD2025001
        // demo.orderId = "ORD2025002"; // 编译报错:final变量不能重新赋值
    }
}

执行结果:ORD2025001关键说明:final 实例变量必须在声明时或构造器中初始化,赋值后不能改,这是新手最容易忘的点。

示例 2:进阶场景 - final 在并发中的应用(Java 17+)

// Java 17+
public class FinalConcurrentDemo {
    // final变量保证多线程下可见性(JVM保证初始化完成后才发布)
    private final String config;

    public FinalConcurrentDemo() {
        this.config = loadConfig(); // 初始化配置
    }

    private String loadConfig() {
        return "db.url=localhost:3306";
    }

    // 多线程读取final变量,无线程安全问题
    public String getConfig() {
        return this.config;
    }

    public static void main(String[] args) {
        FinalConcurrentDemo demo = new FinalConcurrentDemo();
        // 10个线程读取配置
        for (int i = 0; i < 10; i++) {
            new Thread(() -> System.out.println(demo.getConfig())).start();
        }
    }
}

优势说明:final 变量在 JVM 中会被特殊处理,初始化完成后对所有线程可见,无需加 volatile 也能保证可见性,这是并发编程中常用的优化手段。

示例 3:踩坑示范 - final 引用类型修改内容(Java 17+)

// Java 17+
public class FinalPitfallDemo {
    public static void main(String[] args) {
        // ❌ 坑点:以为final集合不能改内容
        final HashMap<String, Integer> stockMap = new HashMap<>();
        stockMap.put("手机", 100); // 能执行,只是地址不能改
        stockMap.put("手机", 99); // 甚至能修改已有元素的值
        System.out.println(stockMap.get("手机")); // 输出:99

        // ✅ 正确做法:用不可变集合
        final Map<String, Integer> immutableStockMap = Map.of("手机", 100);
        // immutableStockMap.put("手机", 99); // 运行时报错,彻底不可变
    }
}

后果说明:生产环境中,这种坑会导致 “以为数据没改,实际被偷偷修改”,比如库存数据被篡改,引发超卖。

示例 4:最佳实践 - 生产级 final 用法(Java 17+)

// Java 17+
import java.util.Collections;
import java.util.List;

// final核心业务类,防止继承篡改
public final class ProductService {
    // final常量(编译期确定,JVM优化)
    public static final int MAX_STOCK = 1000;
    // final引用 + 不可变集合,双重保障
    private final List<String> hotProducts;

    // 构造器初始化,返回不可变视图
    public ProductService(List<String> hotProducts) {
        // 防御性拷贝 + 不可变视图,彻底防止修改
        this.hotProducts = Collections.unmodifiableList(new ArrayList<>(hotProducts));
    }

    // final方法,保证逻辑稳定 + JVM内联优化
    public final List<String> getHotProducts() {
        return this.hotProducts; // 返回的集合不能修改
    }

    public static void main(String[] args) {
        List<String> tempList = List.of("手机", "电脑");
        ProductService service = new ProductService(tempList);
        // service.getHotProducts().add("平板"); // 运行时报错,彻底安全
    }
}

优势说明:结合 final 引用和不可变集合,既保证地址不变,又保证内容不变;final 类 + final 方法,彻底杜绝继承篡改,适合核心业务场景。

四、易错点与避坑指南

❌ 常见错误 1:混淆 final 引用和对象不可变

  • 错误代码:
    final List<Integer> stockList = new ArrayList<>();
    stockList.add(100); // 以为会报错,实际能执行
    System.out.println(stockList.size()); // 输出:1
    
  • 实际场景:我在库存系统中见过这个问题,研发以为加了 final 就安全,结果库存被偷偷修改,导致超卖。
  • 根本原因:final 只锁 “引用地址”,不锁 “对象内容”,新手把 “引用不可变” 和 “对象不可变” 混为一谈。
  • ✅ 正确做法:
    final List<Integer> stockList = Collections.unmodifiableList(new ArrayList<>());
    // stockList.add(100); // 运行时报错,彻底不可变
    
  • 防守方案:核心数据集合,一律用Collections.unmodifiableXXX或 Java 9 + 的List.of()创建不可变集合。

❌ 常见错误 2:final 方法以为能 “变相重写”

  • 错误代码:
    class PayService {
        final void pay() { System.out.println("正常支付"); }
    }
    class HackPayService extends PayService {
        void pay() { System.out.println("篡改支付逻辑"); } // 以为是重写
    }
    
  • 实际场景:我在支付项目中见过,研发以为子类的同名方法能覆盖父类 final 方法,上线后发现支付逻辑混乱,排查了 3 小时才定位到。
  • 根本原因:final 方法不能被重写,子类同名方法只是 “方法隐藏”,多态场景下会执行父类方法,导致逻辑不符合预期。
  • ✅ 正确做法:
    class PayService {
        final void pay() { System.out.println("正常支付"); }
        // 如需扩展,提供钩子方法
        protected void beforePay() {}
    }
    class HackPayService extends PayService {
        @Override
        protected void beforePay() {
            System.out.println("扩展支付前置逻辑"); // 用钩子方法扩展,而非重写
        }
    }
    
  • 防守方案:final 方法如需扩展,提供 protected 的钩子方法,而非让子类重写核心方法。

❌ 常见错误 3:final 变量未初始化导致编译错误

  • 错误代码:
    public class FinalInitDemo {
        private final String userId; // 未初始化
        public FinalInitDemo() {
            // 没给userId赋值
        }
    }
    
  • 实际场景:新人写代码时经常忘初始化 final 实例变量,编译报错后还不知道原因。
  • 根本原因:final 实例变量必须在 “声明时”“构造器中” 或 “初始化块中” 赋值,且只能赋值一次,JVM 不允许 final 变量处于未初始化状态。
  • ✅ 正确做法:
    public class FinalInitDemo {
        private final String userId;
        // 构造器中初始化
        public FinalInitDemo(String userId) {
            this.userId = userId;
        }
        // 或声明时初始化
        // private final String userId = "default";
    }
    
  • 防守方案:开发工具(IDEA)开启 “final 变量未初始化” 的提示,编码时优先在构造器中初始化。

❌ 常见错误 4:过度使用 final 类导致扩展难

  • 错误代码:
    final class CommonUtils { // 普通工具类加final,后续无法扩展
        public static String formatStr(String str) {
            return str.trim();
        }
    }
    
  • 实际场景:我在中台项目中见过,前期给所有工具类加 final,后期需要扩展时只能复制粘贴代码,导致代码冗余。
  • 根本原因:把 “核心类需要 final” 的原则滥用在所有类上,忽略了工具类的扩展需求。
  • ✅ 正确做法:
    // 普通工具类不加final,提供静态方法即可
    class CommonUtils {
        // 私有构造器,防止实例化
        private CommonUtils() {}
        public static String formatStr(String str) {
            return str.trim();
        }
    }
    // 如需扩展,直接新增方法,无需继承
    
  • 防守方案:仅对 “核心业务类”“不可变工具类(如 Math)” 加 final,普通工具类用 “私有构造器 + 静态方法” 即可。

❌ 常见错误 5:多线程下 final 变量的发布问题

  • 错误代码:
    public class FinalPublishDemo {
        private final List<String> data;
        // 错误:final变量在构造器中逸出
        public FinalPublishDemo() {
            data = new ArrayList<>();
            new Thread(() -> System.out.println(data)).start(); // 构造器中发布
        }
    }
    
  • 实际场景:我在分布式系统中见过,final 变量在构造器中发布,导致其他线程读取到未初始化完成的对象。
  • 根本原因:JVM 保证 final 变量初始化完成后才对其他线程可见,但构造器中发布对象会打破这个保证,线程可能读取到未完全初始化的 final 变量。
  • ✅ 正确做法:
    public class FinalPublishDemo {
        private final List<String> data;
        public FinalPublishDemo() {
            data = new ArrayList<>();
        }
        // 构造器完成后再发布
        public void publish() {
            new Thread(() -> System.out.println(data)).start();
        }
    }
    
  • 防守方案:禁止在构造器中发布 final 变量,必须等构造器执行完成后再让其他线程访问。

五、总结与延伸

快速回顾

  1. final 变量锁 “指向”:基本类型值不变,引用类型地址不变,内容可变;
  2. final 方法防重写:JVM 会内联优化,子类同名方法只是隐藏而非重写;
  3. final 类封继承:仅用于核心业务类 / 不可变工具类,避免过度使用。

延伸学习

  1. 学习 Java 21 的 sealed 类,和 final 类搭配实现灵活的继承控制;
  2. 研究 JVM 对 final 的优化机制(如常量折叠、方法内联);
  3. 了解 final 和 immutable 对象的设计模式。

面试高频提问 + 简洁答案

  1. final 修饰变量的作用?→ 基本类型值不变,引用类型地址不变,保证可见性和不可变;
  2. final 方法能被重写吗?→ 不能,子类同名方法是方法隐藏,非重写;
  3. final 类的使用场景?→ 核心业务类(防篡改)、不可变工具类(如 Math);
  4. final 和 volatile 的区别?→ final 保证不可变 / 可见性,volatile 保证可见性 / 禁止指令重排;
  5. Java 17 中 final 和 sealed 类的区别?→ final 完全禁止继承,sealed 只允许指定类继承。

到此这篇关于Java final关键字修饰类、方法、变量的不同作用及实战指南的文章就介绍到这了,更多相关Java final关键字修饰类、方法、变量内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java高并发ThreadPoolExecutor类解析线程池执行流程

    java高并发ThreadPoolExecutor类解析线程池执行流程

    这篇文章主要为大家介绍了java高并发ThreadPoolExecutor类解析线程池执行流程,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • MyBatis使用annonation定义类型映射的简易用法示例

    MyBatis使用annonation定义类型映射的简易用法示例

    这篇文章主要介绍了MyBatis使用annonation定义类型映射的简易用法示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-09-09
  • Java实现短信发送验证码功能

    Java实现短信发送验证码功能

    这篇文章主要介绍了Java实现短信发送验证码功能,本文通过实例代码给大家介绍的非常详细,需要的朋友可以参考下
    2018-10-10
  • shell脚本运行java程序jar的方法

    shell脚本运行java程序jar的方法

    本篇文章主要介绍了shell脚本运行java程序jar的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • maven 删除下载失败的包的方法

    maven 删除下载失败的包的方法

    本文介绍了当Maven包报红时,使用删除相关文件的方法来解决该问题,具有一定的参考价值,感兴趣的可以了解一下
    2023-09-09
  • SpringSecurity整合springBoot、redis实现登录互踢功能

    SpringSecurity整合springBoot、redis实现登录互踢功能

    这篇文章主要介绍了SpringSecurity整合springBoot、redis实现登录互踢,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-05-05
  • Spring Cloud 系列之注册中心 Eureka详解

    Spring Cloud 系列之注册中心 Eureka详解

    Netflix Eureka 是由 Netflix 开源的一款基于 REST 的服务发现组件,包括 Eureka Server 及 Eureka Client。这篇文章主要介绍了Spring Cloud 系列之注册中心 Eureka,需要的朋友可以参考下
    2020-11-11
  • springboot集成本地缓存Caffeine的三种使用方式(小结)

    springboot集成本地缓存Caffeine的三种使用方式(小结)

    本文主要介绍了springboot集成本地缓存Caffeine的三种使用方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-06-06
  • Java获取文件夹下所有文件名称的方法示例

    Java获取文件夹下所有文件名称的方法示例

    这篇文章主要介绍了Java获取文件夹下所有文件名称的方法,涉及java针对文件与目录相关操作技巧,需要的朋友可以参考下
    2017-06-06
  • 关于Java 并发的 CAS

    关于Java 并发的 CAS

    后端开发锁成为一个不可避免的话题,今天我们讨论的是与之对应的无锁 CAS。本文会从怎么来的、是什么、怎么用、原理分析、遇到的问题等不同的角度带你真正搞懂 CAS。
    2021-09-09

最新评论