本节目标:能判断一个局部变量该不该用
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 没有统一的「最佳实践」,但有几条可操作的判断:
| 场景 | 建议 | 原因 |
|---|---|---|
| 右侧构造器已经写了类型 | 用 var | var 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 运算符、溢出与数值边界 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。