var 局部变量类型推断(JDK 10)
周一晨会上,大翔宣布了一个让全组沸腾的消息。
"从这周开始,我们的新项目全面切换到 JDK 11。孔蓝,你先用 var 把之前的代码重构一下。"
孔蓝愣住了:"var?那不是 JavaScript 的关键字吗?Java 不是强类型语言吗?"
小崔插话:"我昨晚已经试过了——
var list = new ArrayList<String>()真的能编译!而且 IDEA 还能自动推断出类型。"白歌合上笔记本:"别高兴太早。var 不是万能钥匙,用错了反而降低代码可读性。今天我就把 var 的正确使用姿势一次性讲清楚。"
定义表
| 概念 | 描述 |
|---|---|
| 类型推断(Type Inference) | 编译器根据右侧表达式自动推导变量类型,无需手动声明 |
| var | JDK 10 引入的保留类型名,用于声明局部变量,让编译器推断具体类型 |
| 局部变量 | var 只能用于方法体内部、for 循环初始化、try-with-resources 中声明的局部变量 |
| 编译期确定 | Java 的 var 是编译期类型推断,字节码层面与显式声明完全等价,无运行时开销 |
Mermaid 流程图:var 的使用边界
Mermaid 时序图:var 的编译过程
基础用法
4.1 最典型的场景:右侧类型已明确
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class VarBasicDemo {
public static void main(String[] args) {
// === 场景一:构造对象 ===
// 传统写法
ArrayList<String> names1 = new ArrayList<String>();
// 钻石操作符(JDK 7)
ArrayList<String> names2 = new ArrayList<>();
// var 写法(JDK 10)—— 最简洁
var names3 = new ArrayList<String>(); // 推断为 ArrayList<String>
names3.add("大翔");
names3.add("白歌");
System.out.println(names3);
// === 场景二:Map 构造 ===
var employeeMap = new HashMap<Integer, String>(); // 推断为 HashMap<Integer, String>
employeeMap.put(1001, "大翔");
employeeMap.put(1002, "白歌");
System.out.println(employeeMap);
// === 场景三:方法返回值 ===
var upper = "hello".toUpperCase(); // 推断为 String
var length = upper.length(); // 推断为 int
System.out.println("upper = " + upper + ", length = " + length);
}
}
输出:
[大翔, 白歌]
{1001=大翔, 1002=白歌}
upper = HELLO, length = 5
4.2 var 与循环
import java.util.ArrayList;
import java.util.List;
public class VarLoopDemo {
public static void main(String[] args) {
var employees = new ArrayList<String>();
employees.add("大翔");
employees.add("白歌");
employees.add("小崔");
employees.add("孔蓝");
// === 增强 for 循环中使用 var ===
for (var emp : employees) { // 推断为 String
System.out.println("员工: " + emp);
}
// === 传统 for 循环中使用 var ===
for (var i = 0; i < employees.size(); i++) { // 推断为 int
System.out.println("第" + i + "位: " + employees.get(i));
}
}
}
输出:
员工: 大翔
员工: 白歌
员工: 小崔
员工: 孔蓝
第0位: 大翔
第1位: 白歌
第2位: 小崔
第3位: 孔蓝
进阶用法
5.1 var 与 try-with-resources
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class VarTryWithResourcesDemo {
public static void main(String[] args) {
// var 在 try-with-resources 中自动推断资源类型
try (var reader = new BufferedReader(new FileReader("pom.xml"))) {
var line = reader.readLine();
while (line != null) {
System.out.println(line);
line = reader.readLine();
}
} catch (IOException e) {
System.err.println("文件读取失败: " + e.getMessage());
}
}
}
5.2 var 与复杂泛型
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class VarComplexGenericDemo {
public static void main(String[] args) {
// === 场景:飞翔科技部门人员分组 ===
var deptEmployees = new HashMap<String, List<String>>();
deptEmployees.put("技术部", List.of("大翔", "白歌", "小崔", "孔蓝"));
deptEmployees.put("市场部", List.of("Frank", "黄俪"));
deptEmployees.put("运维部", List.of("李眉"));
// 传统写法:Map.Entry<String, List<String>> 类型冗长
for (Map.Entry<String, List<String>> entry : deptEmployees.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// var 写法:编译器推断为 Map.Entry<String, List<String>>
for (var entry : deptEmployees.entrySet()) {
System.out.println("【var版】" + entry.getKey() + ": " + entry.getValue());
}
// === 流操作中的中间变量 ===
var result = deptEmployees.values().stream()
.flatMap(List::stream)
.filter(name -> name.length() == 2)
.collect(Collectors.joining(", "));
// result 推断为 String
System.out.println("两字员工: " + result);
}
}
输出:
技术部: [大翔, 白歌, 小崔, 孔蓝]
市场部: [Frank, 黄俪]
运维部: [李眉]
【var版】技术部: [大翔, 白歌, 小崔, 孔蓝]
【var版】市场部: [Frank, 黄俪]
【var版】运维部: [李眉]
两字员工: 大翔, 白歌, 小崔, 孔蓝, 黄俪, 李眉
易错场景
6.1 var 不能用于 null 初始化
// ❌ 编译错误:无法推断类型
// var name = null; // 错误: 无法推断本地变量 name 的类型
// ✅ 正确:必须显式声明类型
String name = null;
编译器需要从右侧表达式推断类型,
null本身没有类型信息。
6.2 var 不能用于 Lambda 表达式
import java.util.function.Predicate;
public class VarLambdaPitfall {
public static void main(String[] args) {
// ❌ 编译错误:Lambda 表达式需要目标类型
// var predicate = (String s) -> s.length() > 3;
// ✅ 正确:显式声明函数式接口类型
Predicate<String> predicate = (String s) -> s.length() > 3;
System.out.println(predicate.test("飞翔科技")); // true
}
}
6.3 var 不能用于字段和方法签名
public class VarScopePitfall {
// ❌ 编译错误:var 不能用于字段
// var companyName = "飞翔科技";
// ✅ 正确:字段必须显式声明类型
private String companyName = "飞翔科技";
// ❌ 编译错误:var 不能用于方法参数
// public void setName(var name) { }
// ✅ 正确:参数必须显式声明类型
public void setName(String name) {
this.companyName = name;
}
// ❌ 编译错误:var 不能用于返回类型
// public var getName() { return companyName; }
// ✅ 正确:返回类型必须显式声明
public String getName() {
return companyName;
}
}
6.4 钻石操作符与 var 的行为差异
import java.util.ArrayList;
public class VarDiamondPitfall {
public static void main(String[] args) {
// === 钻石操作符省略右侧泛型 ===
// var + 钻石操作符 = 推断为 ArrayList<Object> ← 容易踩坑!
var list1 = new ArrayList<>(); // 推断为 ArrayList<Object>
list1.add("字符串");
list1.add(123); // 可以添加任意类型!
System.out.println("list1: " + list1);
// ✅ 正确:右侧指明泛型
var list2 = new ArrayList<String>(); // 推断为 ArrayList<String>
list2.add("字符串");
// list2.add(123); // 编译错误:类型不匹配
// === 传统写法对比 ===
// 以下两种写法等价,都得到 ArrayList<String>
ArrayList<String> list3 = new ArrayList<>(); // 钻石推断
ArrayList<String> list4 = new ArrayList<String>(); // 完整写法
}
}
输出:
list1: [字符串, 123]
白歌的审查意见:
var list = new ArrayList<>()是新人最容易犯的错误。右侧没有类型信息时编译器只能推断为Object。建议:使用 var 时,右侧必须有足够的类型信息。
6.5 数组初始化器的陷阱
public class VarArrayPitfall {
public static void main(String[] args) {
// ❌ 编译错误:var 不支持数组初始化器
// var numbers = {1, 2, 3, 4, 5};
// ✅ 正确方式一:显式声明类型
int[] numbers1 = {1, 2, 3, 4, 5};
// ✅ 正确方式二:使用 new 表达式
var numbers2 = new int[]{1, 2, 3, 4, 5};
System.out.println("numbers1长度: " + numbers1.length);
System.out.println("numbers2长度: " + numbers2.length);
}
}
var 的可读性权衡
在飞翔科技的代码评审会上,白歌和白歌自己争论了很久:var 到底会不会降低代码可读性?
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class VarReadabilityDemo {
public static void main(String[] args) {
// === 推荐使用 var 的场景 ===
// 场景一:右侧构造表达式已经明确类型
var employees = new ArrayList<String>(); // 一眼看出是 ArrayList<String>
// 场景二:类型名过长,不增加信息量
var inputStream = new ByteArrayInputStream(new byte[1024]);
// === 不建议使用 var 的场景 ===
// 场景一:右侧是方法调用,看不出返回类型
var result = someMethod(); // result 是什么类型?得看方法定义
var data = employeeService.findById(1001); // 返回 Employee?Optional<Employee>?
// 场景二:数值类型容易产生歧义
var count = 10; // 是 int 还是 long?
var price = 99.9; // 是 float 还是 double?
// 字面量规则:整数默认 int,小数默认 double
// 如果需要 long 或 float,用显式声明更清晰
long totalCount = 10L;
float unitPrice = 99.9f;
// 场景三:推断类型与开发者预期不符
var names = List.of("大翔", "白歌"); // 推断为 List<String>(不可变!)
// names.add("小崔"); // ❌ UnsupportedOperationException
// 如果期望的是可变 List,显式声明更安全:
List<String> mutableNames = new ArrayList<>(List.of("大翔", "白歌"));
}
// 辅助方法
static String someMethod() { return "result"; }
static class EmployeeService {
static String findById(int id) { return "员工" + id; }
}
static EmployeeService employeeService = new EmployeeService();
}
面试考点
面试官常问的两个问题:
问题一:"Java 引入 var 后还是强类型语言吗?"
答案:仍然是强类型语言。var 是编译期类型推断,编译器根据右侧表达式确定具体类型后替换 var,生成的字节码与显式声明完全一致。运行时没有任何类型信息丢失。这与 JavaScript 的
var/let(动态类型)有本质区别。
问题二:"var 可以在哪些地方使用?哪些地方不能?"
答案:能用的地方:局部变量声明(必须初始化)、增强 for 循环、传统 for 循环索引、try-with-resources 资源声明。不能用的地方:类字段、方法参数、方法返回类型、catch 块的异常类型、Lambda 表达式(作为变量)、null 初始化、数组初始化器
{}。
问题三:"var list = new ArrayList<>() 推断出什么类型?为什么?"
答案:推断为
ArrayList<Object>。因为钻石操作符<>的泛型推断依赖左侧的目标类型,而 var 没有提供目标类型信息,编译器只能回退到原始类型的上界Object。正确写法是var list = new ArrayList<String>()。
小崔的感悟:"var 简单到让人不放心——一行代码改个关键字就能跑,但背后的规则其实不少。记住白歌说的:var 是'锦上添花',不是'雪中送炭'。能提升可读性就用,拿不准就老老实实写完整类型。"