《Java 编程入门》4.2 var、final 与类型推断

把 var 还原成编译期类型推断而不是动态类型:哪些位置能用、哪些位置编译失败、什么时候用 var 反而降低可读性;再讲 final 局部变量、static final 常量与「final 引用不等于不可变对象」的区别,并在 library 的 BookLabels 与 LoanPolicy 里落地。

本节目标:能判断一个局部变量该不该用 var,能解释 final 到底冻结了什么,并知道 static final 常量的命名与初始化边界。
适用版本:JDK 25 LTS(Temurin 25.0.5);var 从 JDK 10 起可用。

var 是编译期推断,不是动态类型

看到 var,很多人的第一反应是「Java 也有动态类型了」。不是。var 只做一件事:让你省略局部变量的类型声明,编译器根据初始化表达式推断出静态类型。推断发生在编译期,字节码里的类型是确定的。

var title = "Java 编程入门";

编译器把 title 推断为 String。如果之后写下面这行,会编译失败:

title = 42;
错误: 不兼容的类型: int无法转换为String

如果 var 是动态类型,这行应该能通过,运行时再决定。它没有。var 变量和显式声明 String title 的变量在字节码层面没有区别,只是源码更短。

这也意味着 var 不能让「类型不明显」的代码变清晰。它只是把类型从左边移到右边,读者仍然要从初始化表达式推断。用不用 var,本质上是可读性取舍,不是功能取舍。

哪些位置可以用 var

var 只能用于带初始化器的局部变量。常见场景:

var title = "Java 编程入门";
var days = 14;
var priceCents = 8900L;
var book = new Book("978-7-115-54608-9", title, "Leeting Yan", 2026, 8900);
var builder = new StringBuilder();

也可以用在 for 循环和增强 for 里:

for (var i = 1; i <= 3; i++) {
    builder.append(i).append(' ');
}

var titles = new String[]{"Java 编程入门", "Effective Java"};
for (var each : titles) {
    total += each.length();
}

还可以用在 try-with-resources 里:

try (var reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
    System.out.println(reader.readLine());
}

这三个位置的共同点是:初始化表达式就在同一行,编译器有足够信息推断类型。for (var i = 1; ...) 推断为 int,for (var each : titles) 推断为 String,try (var reader = ...) 推断为 BufferedReader。

var 推断的是初始化表达式的静态类型,不是运行时类型。下面两行不一样:

var builder = new StringBuilder("library");   // 推断为 StringBuilder
var sequence = (CharSequence) builder;        // 推断为 CharSequence

第二行用了强制转换,var 就推断为 CharSequence,之后不能调用 StringBuilder 特有的方法。var 不是「什么都行」,它精确地跟随表达式的声明类型。

哪些位置不能用 var

var 的限制比想象中多。下面这些都会编译失败:

// 1. 字段不能用
var field = 1;

// 2. 方法参数不能用
public void take(var value) {
}

// 3. 局部变量必须初始化
var x;

// 4. 不能用 null 初始化
var y = null;

// 5. 数组初始化器不能省略类型
var arr = {1, 2, 3};

前两个的错误原文:

错误: 此处不允许使用 'var'
    var field = 1;
    ^
错误: 此处不允许使用 'var'
    public void take(var value) {
                     ^

后三个的错误原文:

错误: 无法推断本地变量 x 的类型
        var x;
            ^
  (无法在不带初始化程序的变量上使用 'var')
错误: 无法推断本地变量 y 的类型
        var y = null;
            ^
  (变量初始化程序为 'null')
错误: 无法推断本地变量 arr 的类型
        var arr = {1, 2, 3};
            ^
  (数组初始化程序需要显式目标类型)

null 没有类型,编译器无法推断;{1, 2, 3} 也没有目标类型,必须写成 new int[]{1, 2, 3}。var 不是「省略一切类型」,它要求右侧能独立确定类型。

还有一条容易忽略:一次不能声明多个 var。var a = 1, b = 2; 不合法,每个变量单独写一行。

可读性:什么时候该用 var

var 没有统一的「最佳实践」,但有几条可操作的判断:

场景建议原因
右侧构造器已经写了类型用 varvar book = new Book(...) 类型一目了然
右侧是字面量谨慎var count = 0 看不出是 int 还是 long
右侧是方法调用看方法名var books = repository.findAll() 要能看出元素类型
类型是关键信息写显式类型Map<String, List<Book>> 写出来更有信息量
需要向上转型写显式类型CharSequence text = new StringBuilder() 表达接口意图

var count = 0 的问题是:读者不知道 count 是 int、long 还是 Integer,而这三者在溢出和空值上的行为不同。第 4.3 节会看到 int 溢出的例子,如果类型被 var 藏起来,排查会更难。

var book = new Book(...) 则很清楚:右侧已经写了 Book,左边再写一遍是重复。var builder = new StringBuilder() 同理。

一个实用规则:如果只看这一行,读者能说出变量的类型,就用 var;说不出来,就写显式类型。 这条规则不完美,但能挡住大部分「为了短而短」的用法。

final 冻结的是引用,不是对象

final 用在局部变量上,表示「这个变量只能赋值一次」:

final var loanDays = 14;
loanDays = 21;   // 编译失败
错误: 无法为 final 变量 loanDays 分配值

用在字段上,表示「构造器里赋值一次,之后不能再改」:

private final String isbn;

Book 的 isbn、title、author 都是 final,构造器里赋值后不能再改。available 不是 final,因为借书、还书要改它。

但 final 只冻结引用,不冻结对象内容:

final var names = new StringBuilder("library");
names.append("-cli");          // 合法
names = new StringBuilder();   // 编译失败

names 指向的对象可以变,names 这个变量不能重新指向别的对象。要真正不可变,需要对象本身不提供修改方法,比如 String、record(第 9 章)、List.of() 返回的不可变列表(第 10 章)。

这个区别在传参时很重要。一个方法接收 final List<Book> books,它不能把 books 指向新列表,但可以调用 books.add(...) 修改内容。final 不是「只读」的同义词。

static final 常量

static final 表示「类级别、只赋值一次」的常量:

public static final int LOAN_DAYS = 14;
public static final int LATE_FEE_CENTS_PER_DAY = 50;

命名惯例是全大写加下划线。static final 的初始化有两种时机:

  • 声明时直接赋值,适合字面量或简单表达式。
  • 静态初始化块,适合需要多步计算的常量。
public static final int MAX_LATE_FEE_CENTS;

static {
    MAX_LATE_FEE_CENTS = LOAN_DAYS * LATE_FEE_CENTS_PER_DAY * 10;
}

静态初始化块在类第一次加载时执行,只执行一次。如果块里抛异常,类初始化失败,后续使用会得到 ExceptionInInitializerError。常量的初始化逻辑要尽量简单,不要在静态块里做 I/O 或网络调用。

static final 引用一个可变对象时,同样只冻结引用:

public static final StringBuilder PREFIX = new StringBuilder("library");

别的代码仍然可以 PREFIX.append(...)。要暴露常量集合,用不可变对象,或者返回副本。

effectively final

局部变量即使没写 final,只要赋值后不再改变,就是「effectively final」。这个性质在匿名类和 lambda 里很重要:它们只能捕获 effectively final 的局部变量。

例如,一个局部变量在匿名类或 lambda 里被捕获后,就不能再重新赋值;否则编译失败。第 12 章讲 lambda 时会展开,现在只需要知道:不要在捕获之后重新赋值局部变量。 需要可变状态时,用字段或长度为 1 的数组,而不是绕开编译器。

在 library 里落地

给 library 加一个书架标签工具,用 var 简化局部变量:

package com.plume.library.domain;

public final class BookLabels {
    private BookLabels() {
    }

    public static String shelfLabel(Book book) {
        var isbn = book.isbn();
        var digits = isbn.replace("-", "");
        var suffix = digits.substring(digits.length() - 4);
        var label = new StringBuilder();
        label.append(book.title()).append(" [").append(suffix).append(']');
        return label.toString();
    }
}

四个 var 的类型都能从右侧看出来:isbn 和 digits 是 String,suffix 是 String,label 是 StringBuilder。这里用 var 不会降低可读性,反而减少了三处重复的 String。

再定义借阅规则常量:

package com.plume.library.domain;

public final class LoanPolicy {
    public static final int LOAN_DAYS = 14;
    public static final int RENEWAL_DAYS = 7;
    public static final int LATE_FEE_CENTS_PER_DAY = 50;
    public static final int MAX_LATE_FEE_CENTS = 5_000;

    private LoanPolicy() {
    }

    public static int dueDay(int borrowDay) {
        return Math.addExact(borrowDay, LOAN_DAYS);
    }

    public static int lateFeeCents(int daysLate) {
        if (daysLate <= 0) {
            return 0;
        }
        long raw = (long) daysLate * LATE_FEE_CENTS_PER_DAY;
        return (int) Math.min(raw, MAX_LATE_FEE_CENTS);
    }
}

LOAN_DAYS、RENEWAL_DAYS、LATE_FEE_CENTS_PER_DAY、MAX_LATE_FEE_CENTS 都是 static final,全大写。lateFeeCents 先用 long 计算,避免 int 乘法溢出,再用 Math.min 封顶。第 4.3 节会解释为什么这里必须转 long。

验证标签:

var book = new Book("978-7-115-54608-9", "Java 编程入门",
        "Leeting Yan", 2026, 8900);
System.out.println(BookLabels.shelfLabel(book));
Java 编程入门 [6089]

digits 去掉了 ISBN 里的连字符,suffix 取最后四位。这里用 var 是安全的,因为每一步的输入输出都是 String,不需要额外解释。

常见错误与边界

「以为 var 是动态类型。」 var x = 1; x = "s"; 会编译失败。var 只是省略类型声明,静态类型在编译期就固定了。IDE 里把鼠标悬停在变量上,能看到推断出的类型。

「用 var 接 null。」 var x = null; 编译失败,因为 null 没有类型。需要空值时写显式类型:String x = null; 或 Integer x = null;。

「以为 final 让对象不可变。」 final List<Book> books 仍然可以 books.add(...);final StringBuilder 仍然可以 append。final 只保证变量不再指向别的对象。不可变要靠类型设计,不靠修饰符。

「在 lambda 里改了捕获的局部变量。」 被 lambda 捕获的局部变量必须是 final 或 effectively final。捕获后重新赋值会编译失败,错误信息是「local variables referenced from a lambda expression must be final or effectively final」。需要可变状态时,用字段或容器。

小结

  • var 是编译期类型推断,不是动态类型;推断结果就是初始化表达式的静态类型。
  • var 只能用于带初始化器的局部变量,可用于 for、增强 for、try-with-resources;不能用于字段、方法参数、null、数组初始化器。
  • 右侧类型明显时用 var,类型是关键信息时写显式类型;不要为了短而牺牲可读性。
  • final 冻结的是变量的引用,不是对象内容;final List 仍可修改元素。
  • static final 是全大写的类级常量;初始化要简单,避免在静态块里做 I/O。
  • effectively final 是 lambda 捕获的前提,捕获后不要重新赋值。

类型和声明方式都定了,剩下的是运算。整数除法、取模、溢出、浮点误差和位运算,会在罚金计算和状态标志上同时出现。下一节把这些边界一次讲清。

阅读导航:上一节:4.1 基本类型与包装类型 · 下一节:4.3 运算符、溢出与数值边界 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Netty 高性能网络编程》15.3 linkd-v2 验收与复盘
  2. 《Netty 高性能网络编程》15.2 限流、超时与降级
  3. 《Netty 高性能网络编程》15.1 网关路由与转发