算法和数据结构

关于Java中的final关键字小结

一 final可以修饰的范围

💡

  • 字段
  • 方法

二 final修饰的字段

1 final修饰字段的限制和注意事项

当字段被指定为final时,如果该类的对象被实例化之后,则该字段值不可再次被修改。也就是说如果某个类里有个字段是final的,new一个该类的对象之后,则该对象的该字段值不允许再次被修改了。

同时,在IDEA里进行代码测试验证时,发现某个类里定义了一个final字段时,IDEA会主动提示,我们是否通过构造函数来初始化该字段,或者提示在定义该字段时就对其进行初始化,或者通过无参构造方法来初始化该字段,再或者把该字段定义为非final修饰。

此外,当字段被定义为final时,通过IDEA帮我们生成getter()和setter()时,会发现,IDEA自动识别了,无需或者说不可以为该字段创建setter()方法,只能生成getter()方法。

2 测试验证代码和执行结果

package com.knockatdatabase;

import java.time.LocalDate;
import java.util.Calendar;

/**
 * Hello world!
 *
 */
public class App 
{
    private final String name;

    public App(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public static void main(String[] args )
    {
        App app = new App("KnockatDatabase");
        System.out.println(app.getName());
        App app2 = new App("KnockatDatabase~~~~~~~~~~~~~~~~~~~~");
        System.out.println(app2.getName());

        System.out.println( "Hello World!" );
        Calendar.getInstance().getTime();
        LocalDate today = LocalDate.now();
    }
}

运行结果:

/Users/huangwei/Software/jdk-24.0.2.jdk/Contents/Home/bin/java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:60649,suspend=y,server=n -agentpath:/private/var/folders/8t/v80nxtp52454n5b1zs20w0y80000gn/T/idea_libasyncProfiler_dylib_temp_folder/libasyncProfiler.dylib=version,jfr,event=wall,interval=10ms,cstack=no,file=/Users/huangwei/IdeaSnapshots/App_2026_01_06_112556.jfr,log=/private/var/folders/8t/v80nxtp52454n5b1zs20w0y80000gn/T/App_2026_01_06_112556.jfr.log.txt,logLevel=DEBUG -javaagent:/Users/huangwei/Library/Caches/JetBrains/IntelliJIdea2024.3/captureAgent/debugger-agent.jar -Dkotlinx.coroutines.debug.enable.creation.stack.trace=false -Ddebugger.agent.enable.coroutines=true -Dkotlinx.coroutines.debug.enable.flows.stack.trace=true -Dkotlinx.coroutines.debug.enable.mutable.state.flows.stack.trace=true -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath /Users/huangwei/IdeaProjects/java_core_11th_edition/chap05/target/classes:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar com.knockatdatabase.App
Connected to the target VM, address: '127.0.0.1:60649', transport: 'socket'
KnockatDatabase
KnockatDatabase~~~~~~~~~~~~~~~~~~~~
Hello World!
Disconnected from the target VM, address: '127.0.0.1:60649', transport: 'socket'

Process finished with exit code 0

三 final修饰方法

1 为啥需要通过final来修饰方法

如果类中的某些特定方法,不希望被子类重写override,则可以在父类上把该方法指定为final。

比如Calendar类中的getTime()和setTime()方法,就被定义为final:

    /**
     * Returns a {@code Date} object representing this
     * {@code Calendar}'s time value (millisecond offset from the <a
     * href="#Epoch">Epoch</a>").
     *
     * @return a {@code Date} representing the time value.
     * @see #setTime(Date)
     * @see #getTimeInMillis()
     */
    public final Date getTime() {
        return new Date(getTimeInMillis());
    }

    /**
     * Sets this Calendar's time with the given {@code Date}.
     * <p>
     * Note: Calling {@code setTime()} with
     * {@code Date(Long.MAX_VALUE)} or {@code Date(Long.MIN_VALUE)}
     * may yield incorrect field values from {@code get()}.
     *
     * @param date the given Date.
     * @see #getTime()
     * @see #setTimeInMillis(long)
     * @throws NullPointerException if {@code date} is {@code null}
     */
    public final void setTime(Date date) {
        Objects.requireNonNull(date, "date must not be null");
        setTimeInMillis(date.getTime());
    }

如果某个类被定义为final,则该类中的所有方法自动变为final,而字段却不会自动变成final。

四 final修饰类

1 为啥需要final修饰类

如果不希望某个类被继承,则可以把该类设计为final。

2 final修饰的类有哪些特征

如果类被定义为final,则该类的所有方法自动变成final,但是字段却不是默认变为final。

3 常见的哪些类被定义为final

LocalData,LocalTime,MinguoDate,String

比较有意思的是,可以看到Java里还单独为台湾定义了一个民国日期,专门用于处理台湾习惯把1912年当做民国元年来处理的习惯。但是JDK文档里说明的好像有点儿政治味儿了。

4 为什么String被定义为final

String被设计为 final,主要是出于安全性、性能线程安全这三个核心维度的考量

4.1. 核心安全性 (Security)

String 在 Java 中被广泛用于参数传递,例如网络连接的 URL、文件路径、数据库连接地址等。

  • 防止恶意劫持: 如果 String 不是 final 的,开发者可以继承它并重写方法。假设一个系统方法预期接收一个路径,但你传入了一个虚假的子类,在校验通过后,子类通过重写逻辑返回了另一个危险路径,这会导致严重的系统安全漏洞。
  • 类加载机制: String 常用于类加载器的参数。如果 String 可变,加载过程中的类名可能被修改,从而导致加载错误的类。

4.2. 线程安全性 (Thread Safety)

由于 final 类的对象通常设计为不可变 (Immutable)

  • 无锁并发: 不可变对象可以在多个线程之间自由共享,而不需要任何同步机制(Synchronization)。这极大地提升了并发性能。
  • 消除副作用: 你不用担心一个方法在处理字符串或日期时,另一个线程悄悄修改了它的值。

4.3. 性能优化与常量池 (Optimization)

这是 String 设计为 final 最直观的原因:

  • 字符串常量池 (String Pool): 只有当 String 不可变时,常量池才有意义。如果多个变量指向内存中同一个 “abc”,而其中一个变量修改了它,其他变量都会受到影响。通过设计成 final,Java 可以实现字符串的复用,节省大量堆内存。
  • Hash 缓存: 由于 String 是不可变的,它的 hashCode 只需要在创建时计算一次并缓存起来。这使得 String 作为 HashMap 的 key 时效率极高。

5 补充说明解决 Calendar 的“历史遗留”痛点

早期的 java.util.DateCalendar可变的 (Mutable),这被认为是 Java 设计史上最大的失误之一:

  • 易错性: 在旧代码中,如果你把一个 Calendar 对象传给两个方法,方法 A 修改了月份,方法 B 的逻辑就会莫名其妙地出错。
  • Java 8 的改进: 正是因为 Calendar 的设计缺陷(非 final 且可变),Java 8 引入了全新的 java.time 包。
    • LocalDateZonedDateTimeLocalTime、MinguoDate等全都是 final 的。
    • 它们是不可变对象,任何修改操作都会返回一个新的实例,而不是修改原对象。

留言