- 一、先搞懂final到底是啥
- 二、final修饰类:不能被继承的“铁疙瘩”
- 三、final修饰方法:不能被重写的“固定逻辑”
- 四、final修饰变量:最常用也最容易踩坑
- 4.1 基本类型变量:值真的不能改
- 4.2 引用类型变量:引用不变,内容可变!
- 4.3 成员变量和局部变量的区别
- 4.4 final参数:方法里不能改引用
- 五、并发场景下的final:线程安全的小帮手
- 六、使用final的一些小经验
- 最后想说
接触Java有一阵子了,final这个关键字一直没彻底搞明白,总觉得它就是“不能改”的意思,但实际用起来总踩坑。这几天花时间仔细研究了下,发现里面门道还真不少,整理成笔记方便自己回顾,也希望能帮到和我一样困惑的朋友。
一、先搞懂final到底是啥
我认为final最核心的作用就是“声明不可变”,就像给代码加了把锁,告诉自己也告诉别人,被它修饰的东西不能随便动了。但这里的“不能动”得分情况说,不是一刀切的不能改,具体要看修饰的是类、方法还是变量,这点特别关键,之前我就因为没分清踩过坑。
二、final修饰类:不能被继承的“铁疙瘩”
被final修饰的类,就相当于一个成品,不能再被扩展了,没有子类能继承它。我觉得这种设计特别适合那些功能完整、不需要再修改的类,比如Java自带的String类,要是能被继承改写,字符串的不可变性就没保障了。
给大家整个简单的例子,比如写个工具类,不想别人随便继承篡改:
// final修饰的工具类,不能被继承publicfinalclassCalculateUtil{// 私有构造器,不让外部实例化privateCalculateUtil(){}// 加法工具方法publicstaticintsum(inta,intb){returna+b;}}// 下面这行代码会编译报错,因为不能继承final类// class SubCalculateUtil extends CalculateUtil {}在我看来,这种用法很适合工具类或者安全敏感的类,能守住核心逻辑不被破坏,避免有人瞎继承搞出问题。
三、final修饰方法:不能被重写的“固定逻辑”
final修饰方法的话,子类能继承这个方法直接用,但不能重写它的实现。这就像父类定好的规矩,子类必须遵守,不能擅自修改。
举个实际的例子感受下:
classAnimal{// 普通方法,子类可以重写publicvoideat(){System.out.println("动物吃东西");}// final方法,子类不能重写publicfinalvoidsleep(){System.out.println("动物睡觉(固定逻辑)");}}classDogextendsAnimal{// 重写普通方法,没问题@Overridepublicvoideat(){System.out.println("狗吃骨头");}// 下面这行代码会报错,不能重写final方法// @Override// public void sleep() {// System.out.println("狗趴着睡");// }}还有个小知识点要提一下,private方法默认就是隐式final的,因为子类根本访问不到,自然没法重写。如果子类写了个和父类private方法同名的方法,那只是子类自己的新方法,不算重写。
四、final修饰变量:最常用也最容易踩坑
这是final最常用的场景,但也是最容易出错的地方。核心规则就一条:final变量一旦赋值,就不能再重新赋值了。但这里要区分基本类型和引用类型,差别很大。
4.1 基本类型变量:值真的不能改
如果final修饰的是int、double这些基本类型,那它的值就彻底固定了,改不了一点。
publicclassFinalBasicDemo{publicstaticvoidmain(String[]args){// 声明时直接赋值finalintnum=10;// 下面这行报错,不能重新赋值// num = 20;// 先声明后赋值,也只能赋一次finalStringname;name="小明";// 下面这行也报错// name = "小红";}}这种用法很适合定义常量,比如项目里的配置参数,用final修饰能防止不小心被篡改。
4.2 引用类型变量:引用不变,内容可变!
这是我之前踩过的大坑!final修饰对象、集合这种引用类型时,只是说这个引用不能指向新的对象,但对象里面的内容该怎么改还能怎么改。
给大家整个直观的例子:
importjava.util.ArrayList;importjava.util.List;classStudent{privateStringname;publicStudent(Stringname){this.name=name;}publicvoidsetName(Stringname){this.name=name;}publicStringgetName(){returnname;}}publicclassFinalReferenceDemo{publicstaticvoidmain(String[]args){// final修饰Student对象finalStudentstudent=newStudent("张三");System.out.println("初始名字:"+student.getName());// 可以修改对象里面的内容,没问题student.setName("李四");System.out.println("修改后名字:"+student.getName());// 下面这行报错,不能让引用指向新对象// student = new Student("王五");// 集合的例子更明显finalList<String>list=newArrayList<>();// 可以往集合里加元素list.add("苹果");list.add("香蕉");System.out.println("集合内容:"+list);// 下面这行报错,不能指向新集合// list = new ArrayList<>();}}所以如果想让对象彻底不可变,光用final修饰引用可不够,还得把对象里的字段也用final修饰,并且不提供修改方法,就像String类那样。
4.3 成员变量和局部变量的区别
成员变量(也就是类里定义的变量)用final修饰的话,必须显式初始化,要么声明时直接赋值,要么在构造器里赋值,而且所有构造器都得赋。
classPerson{// 声明时直接赋值privatefinalStringgender="男";// 构造器里赋值privatefinalintage;privatefinalStringaddress;// 第一个构造器publicPerson(intage,Stringaddress){this.age=age;this.address=address;}// 第二个构造器也得赋值publicPerson(intage){this.age=age;this.address="默认地址";}}局部变量(方法里定义的变量)就灵活点,可以先声明后赋值,但必须在第一次使用前赋好值,而且只能赋一次。
4.4 final参数:方法里不能改引用
方法参数用final修饰的话,在方法里面就不能给这个参数重新赋值了,能防止不小心改了传入的引用。
publicclassFinalParamDemo{publicstaticvoidhandleData(finalintid,finalList<String>data){// 下面两行都报错,不能重新赋值// id = 100;// data = new ArrayList<>();// 可以修改集合内容,没问题data.add("处理后的"+id);}publicstaticvoidmain(String[]args){List<String>data=newArrayList<>();data.add("原始数据");handleData(1,data);System.out.println(data);}}我觉得这种用法在处理复杂逻辑时很有用,能避免不小心改了参数引用导致的bug。
五、并发场景下的final:线程安全的小帮手
这部分有点深,但很实用。在我看来,final在多线程里最大的价值就是能保证可见性,而且不用额外加同步,零成本线程安全。
简单说就是,只要对象是正确构造的(没有发生this逸出),那么这个对象里的final字段,在其他线程里看到的一定是初始化后的最终值,不会是默认值。
给大家整个例子:
classSafeData{privatefinalintcode;privatefinalStringmessage;publicSafeData(intcode,Stringmessage){this.code=code;this.message=message;}publicintgetCode(){returncode;}publicStringgetMessage(){returnmessage;}}publicclassFinalThreadDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{SafeDatadata=newSafeData(200,"成功");Threadthread=newThread(()->{// 子线程能正确读到final字段的值System.out.println("状态码:"+data.getCode());System.out.println("信息:"+data.getMessage());});thread.start();thread.join();}}这里关键是不能让this逸出,也就是不能在构造器还没执行完的时候,就把this引用传给其他线程。比如在构造器里启动线程并传入this,那其他线程可能会读到还没初始化好的final字段,就出问题了。
还有个小对比,final和volatile都能保证可见性,但不一样:final只保证初始化后的一次可见性,适合不会修改的字段;volatile保证多次读写的可见性,适合经常修改的共享变量。而且两者都不保证原子性,比如i++这种操作,还是得用锁或者原子类。
六、使用final的一些小经验
我们的经验是,合理用final能让代码更健壮,但别过度使用,不然会降低灵活性。
总结几个常用场景:
- 修饰类:工具类、安全敏感类,不想被继承的类;
- 修饰方法:核心算法、不想被重写的逻辑;
- 修饰变量:常量、不需要修改的配置、多线程共享的只读数据;
- 修饰参数:防止方法内误改参数引用。
还有几个常见误区要避开:
- 以为final修饰引用类型就是对象不可变,其实不是;
- 过度使用final,比如每个变量都加,反而不方便;
- 构造器里让this逸出,导致final的可见性失效。
最后想说
final关键字看着简单,其实里面的细节还挺多的。核心就是分清“不可变的是什么”——是类不能继承,方法不能重写,还是变量不能重新赋值。
我觉得掌握好final,不仅能减少bug,还能让代码的意图更清晰,别人一看就知道哪些东西是不能动的。现在我写代码的时候,遇到该固定的东西就会下意识用final修饰,感觉代码确实稳定了不少。
如果有理解不到位的地方,欢迎大家指正呀!