本文主题:Java 10 局部变量类型推断与业务代码中的使用边界
适合读者:已经学习 Java 变量、集合和方法,准备在项目中使用 var 的开发者
案例基础:独立编写的订单汇总教学程序,不是公司项目源码
验证环境:JDK 17.0.19,分别使用 Java 10、Java 11 编译级别
关键词:Java 10、var、泛型、BigDecimal、IntelliJ IDEA

🐟 这里是yurenpai
27届开发者,主要学习 Java 后端与 AI 应用开发。
这里记录真实项目中的代码调用链、Agent/RAG 工程化、问题排查和开发复盘。
个人理念:学习新语法,先弄清它省略了什么,再确认它有没有改变代码的含义。
写在前面
先说结论:Java 10 的 var 允许省略局部变量类型的显式书写,类型仍在编译期确定,后续代码仍要遵守类型检查。
学习这个语法时,我想先弄清一个问题:一行声明少写了类型,为什么有时更清楚,有时却会把订单 ID 列表变成可以放任何对象的集合?
仅记住"var 可以自动推断类型"还不够。写业务代码时,更需要判断:推断出的类型是不是自己想要的,省略之后别人还能不能看懂。
本文使用同一个构造案例展开:**按客户汇总已审核订单的待回款金额。**从金额、订单集合到汇总结果,沿着变量的使用过程分析 var。
需要提前说明两点:本文是一篇语法学习文章,没有对真实业务项目进行改造;Java 11 才增加的 Lambda 参数 var 会单独标注,不归到 Java 10 中。
读完后,应能回答三个问题:var 的类型从哪里来,哪些写法会丢失自己原本想表达的约束,怎样通过编译和运行结果验证判断。
短代码片段默认放在方法体内,并导入所用标准库类;它们是独立语法实验,不能把各段重复变量直接拼在同一方法里。完整可运行程序单独提供,正文按执行顺序分析关键代码。
文章目录
-
- 写在前面
- [一、从订单汇总出发,var 能省掉哪些重复信息?](#一、从订单汇总出发,var 能省掉哪些重复信息?)
- 二、先画清类型推断过程:编译器替我做了什么?
-
- [2.1 var 不等于 Object](#2.1 var 不等于 Object)
- [2.2 var 不等于 final](#2.2 var 不等于 final)
- [三、把 var 放回业务链路:订单如何变成汇总结果?](#三、把 var 放回业务链路:订单如何变成汇总结果?)
-
- [3.1 示例目录和职责](#3.1 示例目录和职责)
- [3.2 先看调用链,再看代码](#3.2 先看调用链,再看代码)
- [3.3 关键方法中,三个 var 分别是什么类型?](#3.3 关键方法中,三个 var 分别是什么类型?)
- [3.4 用四笔订单走一遍](#3.4 用四笔订单走一遍)
- [四、为什么订单 ID 集合的元素会被推断成 Object?](#四、为什么订单 ID 集合的元素会被推断成 Object?)
-
- [4.1 var ids = new ArrayList<>(),元素到底是什么类型?](#4.1 var ids = new ArrayList<>(),元素到底是什么类型?)
- [4.2 空集合工厂也有类似问题](#4.2 空集合工厂也有类似问题)
- [4.3 var 推断接口,还是具体实现类?](#4.3 var 推断接口,还是具体实现类?)
- 五、金额与数量的类型,不能只凭变量名判断
-
- [5.1 0、0L、1.0、1.0F 不是一回事](#5.1 0、0L、1.0、1.0F 不是一回事)
- [5.2 var 不会修复整数除法](#5.2 var 不会修复整数除法)
- [5.3 金额仍要遵守 BigDecimal 的规则](#5.3 金额仍要遵守 BigDecimal 的规则)
- 六、能用和不能用的位置,要看推断条件
-
- [6.1 Java 10 支持的声明位置](#6.1 Java 10 支持的声明位置)
- [6.2 只声明、不初始化](#6.2 只声明、不初始化)
- [6.3 用 null 初始化](#6.3 用 null 初始化)
- [6.4 一条声明定义多个变量](#6.4 一条声明定义多个变量)
- [6.5 直接写数组初始化列表](#6.5 直接写数组初始化列表)
- [6.6 直接把 Lambda 或方法引用赋给 var](#6.6 直接把 Lambda 或方法引用赋给 var)
- [七、版本边界:Lambda 参数中的 var 是 Java 11 新增](#七、版本边界:Lambda 参数中的 var 是 Java 11 新增)
- [八、在 IDEA 和编译器里验证自己的判断](#八、在 IDEA 和编译器里验证自己的判断)
-
- [8.1 先检查项目语言级别和实际编译器](#8.1 先检查项目语言级别和实际编译器)
- [8.2 验证的是"类型规则",不只是最后能打印结果](#8.2 验证的是“类型规则”,不只是最后能打印结果)
- 九、回到业务代码:什么时候保留显式类型?
-
- [9.1 再确认几个容易混淆的边界](#9.1 再确认几个容易混淆的边界)
- 参考资料
- 总结
一、从订单汇总出发,var 能省掉哪些重复信息?
在这个教学场景中,输入是订单列表,处理规则是"只累计已审核且尚未结清的订单",输出是每个客户的待回款金额。
var 不参与判断订单是否审核、金额是否合法。它只改变相关局部变量的声明方式。为了看清这一点,先观察示例方法中金额和订单编号的初始化写法。
Java 中有些声明会把同一个类型写两遍:
java
BigDecimal totalAmount = new BigDecimal("199.90");
ArrayList<String> orderNumbers = new ArrayList<String>();
右侧已经告诉读者:这里分别创建一个金额对象和一个字符串列表。Java 10 允许将这类局部声明简化为:
java
var totalAmount = new BigDecimal("199.90");
var orderNumbers = new ArrayList<String>();
在这两个例子中,编译器分别推断出 BigDecimal 和 ArrayList<String>。
需要注意,Java 7 的菱形语法 <> 早已能省略右侧部分泛型信息:
java
List<String> orderNumbers = new ArrayList<>();
**菱形语法主要减少右侧泛型参数的重复,var 则省略左侧局部变量类型。两者可以配合,但不能不加判断地把两边信息都删掉。**后面会具体解释。
后文会把这些局部声明放回订单处理链路中。讨论的目标是减少重复表达,同时保留数量、金额和集合元素的类型约束,不是给已有项目批量换语法。
二、先画清类型推断过程:编译器替我做了什么?
先看初始化,再看赋值。下面是订单示例中的独立局部变量实验,重点观察第一次赋值怎样决定后续允许写入的数据:
java
var quantity = 10; // int
var orderId = 1001L; // long
var orderNo = "SO2026090901"; // String
quantity = 20; // 可以:仍然是 int
orderNo = "SO2026090902"; // 可以:仍然是 String
但下面这一段不能编译:
java
var quantity = 10;
quantity = "十件"; // 编译错误:不能把 String 赋给 int
这条处理链发生在编译阶段,不是订单系统运行时再猜类型:
#mermaid-svg-Nev4Cv4z8rEhhPgW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Nev4Cv4z8rEhhPgW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Nev4Cv4z8rEhhPgW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Nev4Cv4z8rEhhPgW .error-icon{fill:#552222;}#mermaid-svg-Nev4Cv4z8rEhhPgW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Nev4Cv4z8rEhhPgW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Nev4Cv4z8rEhhPgW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Nev4Cv4z8rEhhPgW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Nev4Cv4z8rEhhPgW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Nev4Cv4z8rEhhPgW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Nev4Cv4z8rEhhPgW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Nev4Cv4z8rEhhPgW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Nev4Cv4z8rEhhPgW .marker.cross{stroke:#333333;}#mermaid-svg-Nev4Cv4z8rEhhPgW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Nev4Cv4z8rEhhPgW p{margin:0;}#mermaid-svg-Nev4Cv4z8rEhhPgW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Nev4Cv4z8rEhhPgW .cluster-label text{fill:#333;}#mermaid-svg-Nev4Cv4z8rEhhPgW .cluster-label span{color:#333;}#mermaid-svg-Nev4Cv4z8rEhhPgW .cluster-label span p{background-color:transparent;}#mermaid-svg-Nev4Cv4z8rEhhPgW .label text,#mermaid-svg-Nev4Cv4z8rEhhPgW span{fill:#333;color:#333;}#mermaid-svg-Nev4Cv4z8rEhhPgW .node rect,#mermaid-svg-Nev4Cv4z8rEhhPgW .node circle,#mermaid-svg-Nev4Cv4z8rEhhPgW .node ellipse,#mermaid-svg-Nev4Cv4z8rEhhPgW .node polygon,#mermaid-svg-Nev4Cv4z8rEhhPgW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Nev4Cv4z8rEhhPgW .rough-node .label text,#mermaid-svg-Nev4Cv4z8rEhhPgW .node .label text,#mermaid-svg-Nev4Cv4z8rEhhPgW .image-shape .label,#mermaid-svg-Nev4Cv4z8rEhhPgW .icon-shape .label{text-anchor:middle;}#mermaid-svg-Nev4Cv4z8rEhhPgW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Nev4Cv4z8rEhhPgW .rough-node .label,#mermaid-svg-Nev4Cv4z8rEhhPgW .node .label,#mermaid-svg-Nev4Cv4z8rEhhPgW .image-shape .label,#mermaid-svg-Nev4Cv4z8rEhhPgW .icon-shape .label{text-align:center;}#mermaid-svg-Nev4Cv4z8rEhhPgW .node.clickable{cursor:pointer;}#mermaid-svg-Nev4Cv4z8rEhhPgW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Nev4Cv4z8rEhhPgW .arrowheadPath{fill:#333333;}#mermaid-svg-Nev4Cv4z8rEhhPgW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Nev4Cv4z8rEhhPgW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Nev4Cv4z8rEhhPgW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Nev4Cv4z8rEhhPgW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Nev4Cv4z8rEhhPgW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Nev4Cv4z8rEhhPgW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Nev4Cv4z8rEhhPgW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Nev4Cv4z8rEhhPgW .cluster text{fill:#333;}#mermaid-svg-Nev4Cv4z8rEhhPgW .cluster span{color:#333;}#mermaid-svg-Nev4Cv4z8rEhhPgW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Nev4Cv4z8rEhhPgW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Nev4Cv4z8rEhhPgW rect.text{fill:none;stroke-width:0;}#mermaid-svg-Nev4Cv4z8rEhhPgW .icon-shape,#mermaid-svg-Nev4Cv4z8rEhhPgW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Nev4Cv4z8rEhhPgW .icon-shape p,#mermaid-svg-Nev4Cv4z8rEhhPgW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Nev4Cv4z8rEhhPgW .icon-shape .label rect,#mermaid-svg-Nev4Cv4z8rEhhPgW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Nev4Cv4z8rEhhPgW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Nev4Cv4z8rEhhPgW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Nev4Cv4z8rEhhPgW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 读取初始化表达式
编译器推断变量类型
检查后续赋值与方法调用
检查通过后生成字节码
纯文本流程:读取初始化表达式 → 编译期推断类型 → 检查后续代码 → 生成字节码。
后面的赋值语句不会反过来参与这次局部变量的类型推断。var quantity = 10 一旦推断为 int,它不会因为后面出现字符串就变成另一种类型。
引用类型仍遵循普通赋值规则。例如 var text = "订单" 推断为 String,以后可以赋另一个字符串,也可以赋 null;var number = 1 推断为基本类型 int,则不能赋 null。
2.1 var 不等于 Object
java
Object explicitObject = "订单";
var inferredText = "订单";
// explicitObject.length(); // 编译错误:Object 没有 length() 方法
int length = inferredText.length(); // 可以:inferredText 的静态类型是 String
Object 是明确指定的类型,var 是让编译器推断类型的声明语法。两者不能互相替代。
2.2 var 不等于 final
java
var quantity = 10;
quantity = 20; // 可以重新赋值
final var approvedQuantity = 10;
// approvedQuantity = 20; // 编译错误:final 局部变量不能再次赋值
final var orderNumbers = new ArrayList<String>();
orderNumbers.add("SO001"); // 可以:final 限制引用重新赋值,不保证对象内容不可变
因此,var 不会自动赋予变量"只读"或"不可变"的性质。
小鱼点睛
var 省略的是类型的书写,final 限制的是变量再次赋值。类型能否改变、变量能否重新赋值、对象内容能否修改,是三个不同的问题。
三、把 var 放回业务链路:订单如何变成汇总结果?
这个示例中,类型推断由编译器完成,筛选和累加由程序执行。理解 var 不需要虚构 Controller、Service 或数据库调用。
3.1 示例目录和职责
本地配套示例的实际结构如下:
text
Java10-var-示例代码/
└─ VarOrderDemo.java
├─ OrderStatus:表示草稿或已审核状态
├─ Order:保存构造的订单数据,计算本单待回款金额
├─ summarizeOutstanding:过滤订单并按客户累计
└─ main:构造输入、调用汇总方法、打印结果
这里仅有一个 Java 文件,树中缩进项表示文件内部的类型与方法,不是额外目录。
| 位置 | 输入 | 输出 | 上下游和边界 |
|---|---|---|---|
Order.outstandingAmount |
本单应收和已收金额 | BigDecimal 差额 |
被汇总方法调用,只做本单减法 |
summarizeOutstanding |
List<Order> |
Map<String, BigDecimal> |
由 main 调用,负责过滤、分组和累加 |
main |
代码内构造的订单样本 | 控制台文本 | 调用汇总方法,不访问真实服务 |
为缩短示例,使用客户名分组;真实系统应使用稳定的客户 ID,防止同名客户被合并。示例假设同一币种、金额合法且已收不大于应收,也不包含发票、退款、账龄等完整财务口径。
3.2 先看调用链,再看代码
#mermaid-svg-PaVZLKpXWpX9TQul{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PaVZLKpXWpX9TQul .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PaVZLKpXWpX9TQul .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PaVZLKpXWpX9TQul .error-icon{fill:#552222;}#mermaid-svg-PaVZLKpXWpX9TQul .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PaVZLKpXWpX9TQul .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PaVZLKpXWpX9TQul .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PaVZLKpXWpX9TQul .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PaVZLKpXWpX9TQul .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PaVZLKpXWpX9TQul .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PaVZLKpXWpX9TQul .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PaVZLKpXWpX9TQul .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PaVZLKpXWpX9TQul .marker.cross{stroke:#333333;}#mermaid-svg-PaVZLKpXWpX9TQul svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PaVZLKpXWpX9TQul p{margin:0;}#mermaid-svg-PaVZLKpXWpX9TQul .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PaVZLKpXWpX9TQul .cluster-label text{fill:#333;}#mermaid-svg-PaVZLKpXWpX9TQul .cluster-label span{color:#333;}#mermaid-svg-PaVZLKpXWpX9TQul .cluster-label span p{background-color:transparent;}#mermaid-svg-PaVZLKpXWpX9TQul .label text,#mermaid-svg-PaVZLKpXWpX9TQul span{fill:#333;color:#333;}#mermaid-svg-PaVZLKpXWpX9TQul .node rect,#mermaid-svg-PaVZLKpXWpX9TQul .node circle,#mermaid-svg-PaVZLKpXWpX9TQul .node ellipse,#mermaid-svg-PaVZLKpXWpX9TQul .node polygon,#mermaid-svg-PaVZLKpXWpX9TQul .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PaVZLKpXWpX9TQul .rough-node .label text,#mermaid-svg-PaVZLKpXWpX9TQul .node .label text,#mermaid-svg-PaVZLKpXWpX9TQul .image-shape .label,#mermaid-svg-PaVZLKpXWpX9TQul .icon-shape .label{text-anchor:middle;}#mermaid-svg-PaVZLKpXWpX9TQul .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PaVZLKpXWpX9TQul .rough-node .label,#mermaid-svg-PaVZLKpXWpX9TQul .node .label,#mermaid-svg-PaVZLKpXWpX9TQul .image-shape .label,#mermaid-svg-PaVZLKpXWpX9TQul .icon-shape .label{text-align:center;}#mermaid-svg-PaVZLKpXWpX9TQul .node.clickable{cursor:pointer;}#mermaid-svg-PaVZLKpXWpX9TQul .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PaVZLKpXWpX9TQul .arrowheadPath{fill:#333333;}#mermaid-svg-PaVZLKpXWpX9TQul .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PaVZLKpXWpX9TQul .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PaVZLKpXWpX9TQul .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PaVZLKpXWpX9TQul .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PaVZLKpXWpX9TQul .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PaVZLKpXWpX9TQul .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PaVZLKpXWpX9TQul .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PaVZLKpXWpX9TQul .cluster text{fill:#333;}#mermaid-svg-PaVZLKpXWpX9TQul .cluster span{color:#333;}#mermaid-svg-PaVZLKpXWpX9TQul div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PaVZLKpXWpX9TQul .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PaVZLKpXWpX9TQul rect.text{fill:none;stroke-width:0;}#mermaid-svg-PaVZLKpXWpX9TQul .icon-shape,#mermaid-svg-PaVZLKpXWpX9TQul .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PaVZLKpXWpX9TQul .icon-shape p,#mermaid-svg-PaVZLKpXWpX9TQul .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PaVZLKpXWpX9TQul .icon-shape .label rect,#mermaid-svg-PaVZLKpXWpX9TQul .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PaVZLKpXWpX9TQul .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PaVZLKpXWpX9TQul .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PaVZLKpXWpX9TQul :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} main 构造订单列表
summarizeOutstanding 遍历订单
筛选已审核订单
outstandingAmount 计算待回款金额
排除已结清订单
merge 按客户累加
main 打印汇总结果
纯文本流程:构造订单 → 筛选已审核订单 → 计算待回款金额 → 排除已结清订单 → 按客户累加 → 打印结果。
3.3 关键方法中,三个 var 分别是什么类型?
下面摘取配套程序的 summarizeOutstanding 方法。它接收订单列表,返回客户与金额的映射;重点看集合、循环变量和金额变量的类型来源。这是教学程序中的完整方法,不是公司源码,也不是一个可以单独运行的完整 Java 文件。
java
public static Map<String, BigDecimal> summarizeOutstanding(List<Order> orders) {
// 推断为 LinkedHashMap<String, BigDecimal>,保留客户首次出现的顺序。
var totalsByCustomer = new LinkedHashMap<String, BigDecimal>();
for (var order : orders) { // order 推断为 Order
if (order.status != OrderStatus.APPROVED) {
continue;
}
var outstanding = order.outstandingAmount(); // BigDecimal
if (outstanding.signum() <= 0) {
continue;
}
// 相同客户累加金额;方法引用的目标类型由 merge 的方法签名提供。
totalsByCustomer.merge(
order.customerName, outstanding, BigDecimal::add);
}
return totalsByCustomer;
}
沿着调用顺序,可以读出三个事实:
totalsByCustomer的初始化表达式是new LinkedHashMap<String, BigDecimal>(),推断类型为LinkedHashMap<String, BigDecimal>;它保留客户首次出现的顺序。orders的参数类型已经写明为List<Order>,因此增强for中的order推断为Order。outstandingAmount的返回类型声明为BigDecimal,因此outstanding也是BigDecimal;接下来才能调用signum,判断是否还有未结清金额。
merge 接收客户、当前金额和累加函数。第一次遇到客户时存入金额,再遇到同一客户时调用 BigDecimal::add。这个方法引用有 merge 方法签名提供的目标函数式接口,与后面介绍的"直接赋给 var"不同。
方法参数和返回值仍然显式声明。这既是 Java 10 的语法边界,也让调用方清楚知道输入和输出契约。
3.4 用四笔订单走一遍
main 只负责构造样本、调用汇总方法和打印结果。下面保留完整入口方法,Order 的字段、构造方法和导入声明见配套文件;不要把这段当成整个程序直接编译。
java
public static void main(String[] args) {
var orders = List.of(
new Order("客户甲", OrderStatus.APPROVED, "1000.00", "300.00"),
new Order("客户甲", OrderStatus.APPROVED, "200.00", "0.00"),
new Order("客户乙", OrderStatus.DRAFT, "500.00", "0.00"),
new Order("客户丙", OrderStatus.APPROVED, "600.00", "600.00")
); // List<Order>
var totals = summarizeOutstanding(orders); // Map<String, BigDecimal>
for (var entry : totals.entrySet()) { // Map.Entry<String, BigDecimal>
System.out.println(entry.getKey() + ":" + entry.getValue().toPlainString());
}
}
输入和处理结果如下:
| 样本 | 状态 | 应收 / 已收 | 本次处理 |
|---|---|---|---|
| 客户甲第一笔 | 已审核 | 1000.00 / 300.00 | 累计 700.00 |
| 客户甲第二笔 | 已审核 | 200.00 / 0.00 | 再累计 200.00 |
| 客户乙 | 草稿 | 500.00 / 0.00 | 排除 |
| 客户丙 | 已审核 | 600.00 / 600.00 | 已结清,排除 |
程序实际输出:
text
客户甲:900.00
注意 main 中的 totals:虽然被调用方法内部创建了 LinkedHashMap,但其返回类型声明为 Map<String, BigDecimal>,所以 totals 推断为 Map<String, BigDecimal>。var 不会在运行时追踪返回对象的实现类来决定变量类型。
四、为什么订单 ID 集合的元素会被推断成 Object?
最需要警惕的情况,是把两边都看起来重复的类型信息删掉,结果让集合失去了原本希望表达的元素约束。
以下仍以订单 ID 集合为例。这些是对示例 main 方法的独立语法实验,不会访问或修改真实订单。
4.1 var ids = new ArrayList<>(),元素到底是什么类型?
下面的代码可以编译,但可能不是你的本意:
java
var ids = new ArrayList<>(); // ArrayList<Object>,不是原始类型 ArrayList
ids.add(1001L);
ids.add("不是数字ID"); // 也能加入
var first = ids.get(0); // 静态类型为 Object
为什么?
原来这样声明时:
java
List<Long> ids = new ArrayList<>();
左侧的 List<Long> 提供了目标类型,帮助右侧推断元素类型。
改成 var ids = new ArrayList<>() 后,左侧没有目标类型,右侧又没有构造参数帮助推断,因此这里的元素类型被推断为 Object。
编译器不会等你执行 ids.add(1001L),再把列表重新推断成 ArrayList<Long>。
以下两种方式都能保留订单 ID 的类型约束:
java
List<Long> explicitIds = new ArrayList<>();
var inferredIds = new ArrayList<Long>();
但也不能得出"var 和 <> 永远不能一起用"的结论。如果构造参数提供了足够信息,仍然可以正确推断:
java
var ids = new ArrayList<>(List.of(1001L, 1002L)); // ArrayList<Long>
4.2 空集合工厂也有类似问题
java
var unknownIds = List.of(); // List<Object>
var orderIds = List.<Long>of(); // List<Long>
var selectedIds = List.of(1001L); // List<Long>
这里还要分清两个维度:泛型参数决定元素类型约束,集合实现决定是否允许修改。 List.of 创建的是不可修改列表,写成 var 不会让它变成可修改列表。
需要后续追加元素时,可以这样写:
java
var editableIds = new ArrayList<Long>();
editableIds.add(1001L);
4.3 var 推断接口,还是具体实现类?
java
List<String> explicitList = new ArrayList<>();
var inferredList = new ArrayList<String>();
explicitList = new LinkedList<>(); // 可以:声明类型是 List<String>
// inferredList = new LinkedList<String>(); // 不可以:推断类型是 ArrayList<String>
对于右侧的 new ArrayList<String>(),var 推断出的是 ArrayList<String>。如果右侧是一个声明返回 List<String> 的方法调用,它通常推断为 List<String>,不会在运行时检查方法实际返回的是哪种实现类。
这也会影响重载方法选择。把原本的接口声明替换成 var 后,如果变量静态类型变得更具体,编译器可能选择另一个重载版本。因此,"能编译"不代表批量替换前后行为一定相同。
局部范围很小、类型来源清楚时,用具体实现类并不必然有问题;当接口约束、集合顺序或后续替换实现很重要时,显式类型更能表达设计意图。
小鱼点睛
泛型推断需要信息来源。var 和空菱形同时出现、构造参数又没有提供类型线索时,集合元素可能被推断为 Object;后续 add 操作不会再替你收紧类型。
五、金额与数量的类型,不能只凭变量名判断
5.1 0、0L、1.0、1.0F 不是一回事
java
var count = 0; // int
var totalCount = 0L; // long
var ratio = 1.0; // double
var weight = 1.0F; // float
把 long count = 0 改成 var count = 0,类型会从 long 变为 int。金额、数量、主键等业务字段不能只凭变量名猜类型。
例如 var orderId = 1 得到的是 int,并不会因为变量叫 orderId 就自动推断为 Long;如果方法参数需要 Long,也不能期待 int 自动完成"先扩宽成 long 再装箱成 Long"的调用转换。
5.2 var 不会修复整数除法
java
var completed = 80;
var planned = 100;
var rate = completed / planned; // int,结果为 0
double stillWrong = completed / planned; // double,结果仍是 0.0
var correctRate = (double) completed / planned; // double,结果为 0.8
stillWrong 的结果仍错误,是因为除法先按两个 int 执行,结果得到 0 后才转换为 double。这个问题不是 var 独有,但省略类型会让它更容易被忽视。
这里的 completed / planned 可理解为订单已完成数量与计划数量之比。真实履约报表还需要处理计划数量为零、舍入位数和百分比显示规则,这些都不是 var 能解决的。
5.3 金额仍要遵守 BigDecimal 的规则
java
var unitPrice = new BigDecimal("19.90");
var quantity = 3;
var totalAmount = unitPrice.multiply(BigDecimal.valueOf(quantity));
System.out.println(totalAmount.toPlainString()); // 59.70
var 在这里推断出的类型很明确,读起来也自然。但它不会消除 new BigDecimal(0.1) 带来的二进制浮点表示问题,不会自动设置舍入规则,也不会给金额带上币种。
六、能用和不能用的位置,要看推断条件
6.1 Java 10 支持的声明位置
| 使用位置 | Java 10 是否允许 | 说明 |
|---|---|---|
| 方法内带初始化表达式的局部变量 | 允许 | var quantity = 10; |
| 构造方法体、初始化代码块内的局部变量 | 允许 | 仍是局部变量,不是字段或构造参数 |
普通 for 循环中的变量声明 |
允许 | for (var i = 0; i < 3; i++) |
增强 for 循环变量 |
允许 | for (var orderNo : orderNumbers) |
| try-with-resources 中的资源声明 | 允许 | 资源仍须满足 AutoCloseable 等规则 |
| 实例字段、静态字段 | 不允许 | 例如类体直接声明 private var count = 0; |
| 普通方法、构造方法的参数 | 不允许 | 不能写 void update(var order) |
| 方法返回类型 | 不允许 | 不能写 public var query() |
catch 参数 |
不允许 | 不能写 catch (var e) |
Lambda 参数显式写 var |
Java 10 不允许 | Java 11 才支持,见第七节 |
把几个合法位置放在一起看:
java
var orderNumbers = List.of("SO001", "SO002");
for (var i = 0; i < orderNumbers.size(); i++) {
System.out.println(orderNumbers.get(i));
}
for (var orderNo : orderNumbers) {
System.out.println(orderNo);
}
// BufferedReader、StringReader 来自 java.io
try (var reader = new BufferedReader(new StringReader("SO001\nSO002"))) {
System.out.println(reader.readLine());
}
这里的 List.of 是 Java 9 已有的集合工厂方法,并不是 Java 10 的 var 新增能力。资源示例需要导入对应类,并在外层方法声明或处理 IOException。
增强 for 没有写 = ... 也可以使用 var,因为它有专门的循环变量推断规则,类型由数组或可迭代对象的元素类型决定。
6.2 只声明、不初始化
java
var orderNo; // 编译错误:没有初始化表达式,无法推断类型
即使你准备下一行再赋值,也不行。需要延后赋值时,应保留显式类型:
java
String orderNo;
orderNo = "SO001";
6.3 用 null 初始化
java
var order = null; // 编译错误
null 无法告诉编译器你想要的是 Order、String,还是其他引用类型。可以写 Order order = null,但更应结合实际控制流判断是否需要先放一个空值。
"var 不能以 null 初始化"和"推断出的引用变量以后不能赋 null"是两回事。
6.4 一条声明定义多个变量
java
var planned = 100, completed = 80; // 编译错误
应拆成两条声明,或者按原有语法使用显式类型:
java
var planned = 100;
var completed = 80;
6.5 直接写数组初始化列表
java
var quantities = {10, 20, 30}; // 编译错误
数组初始化列表需要目标类型,而这里的 var 没有提供目标类型。改成显式数组创建表达式即可:
java
var quantities = new int[]{10, 20, 30}; // 推断为 int[]
所以"var 不能用于数组"是错误说法。不能写的是上面的裸初始化列表,以及 var quantities[] = ... 这种额外带数组维度的声明形式。
6.6 直接把 Lambda 或方法引用赋给 var
java
var printer = (String text) -> System.out.println(text); // 编译错误
即使 Lambda 参数写了 String,仍然需要一个目标函数式接口,才能确定整个 Lambda 属于什么类型。
使用显式接口即可:
java
Consumer<String> printer = text -> System.out.println(text);
Consumer<String> anotherPrinter = System.out::println;
同理,var printer = System.out::println 也不能编译。不要将它与 Java 11 支持的"在 Lambda 参数位置写 var"混为一谈。
七、版本边界:Lambda 参数中的 var 是 Java 11 新增
Java 11 允许在 Lambda 参数上写 var:
java
BiFunction<Integer, Integer, Integer> add = (var left, var right) -> left + right;
这里左右两个参数仍由目标函数式接口 BiFunction<Integer, Integer, Integer> 推断为 Integer。它不是允许普通方法参数随便写 var,也不是首次引入 Lambda 参数推断------(left, right) -> left + right 在 Java 8 就已经支持。
Java 11 这项改进的一个用途,是在保持参数类型推断的同时添加注解或修饰符:
java
BiFunction<Integer, Integer, Integer> add =
(final var left, final var right) -> left + right;
同一 Lambda 参数列表必须统一风格,以下写法均不合法:
| 错误写法 | 原因 |
|---|---|
(var left, right) -> left + right |
不能部分使用 var,部分省略类型 |
(var left, Integer right) -> left + right |
不能混用 var 和显式参数类型 |
var text -> text.trim() |
使用 var 声明单个 Lambda 参数时,也必须带括号 |
var add = (var left, var right) -> left + right |
左侧仍未提供目标函数式接口 |
介绍 Java 10 时把这部分也说成"Java 10 已支持",会让读者直接编译失败。
八、在 IDEA 和编译器里验证自己的判断
8.1 先检查项目语言级别和实际编译器
如果 var 被标红,先核对三项:
- Project SDK:使用支持目标语言级别的 JDK。
- Language level :局部变量
var至少为 10;Lambda 参数var至少为 11。 - 构建配置 :Maven、Gradle 的编译级别应与项目要求一致。安装了 JDK 17,并不代表项目的 Java 8 编译配置就自动支持
var。
在终端执行:
powershell
java -version
javac -version
这两个命令可能来自不同目录,尤其是 Windows 配置过多个 JDK 时。IDEA 使用的 JDK 也可能与终端的 PATH 不同。
在配套文件 VarOrderDemo.java 所在目录,使用 JDK 17 编译器验证 Java 10 兼容性,可以执行:
powershell
javac --release 10 -encoding UTF-8 VarOrderDemo.java
java VarOrderDemo
前提是这两个命令确实指向所选 JDK;也可以用该 JDK 的 bin 目录下的绝对路径执行。不要因此直接调高现有业务项目的语言级别,是否升级应按项目兼容性要求决定。
--release 10 用于检查 Java 10 语言和对应标准 API 范围,并生成相应目标版本的 class 文件;它不是把 JDK 17 运行时变成 JDK 10。
验证推断类型时,IDEA 可以提供类型信息和相关检查提示。但代码审查、Git diff 和博客阅读往往没有这些提示,因此不能靠"鼠标悬停就能看类型"来为含糊的代码命名兜底。
本文示例核验环境:JDK 17.0.19,Java 10 示例使用
javac --release 10;Java 11 Lambda 参数示例另用--release 11检查。合法代码检查编译结果,故意错误的代码检查是否被编译器拒绝;完整示例同时核对实际输出。此项验证不等同于使用原生 JDK 10 编译器或生产环境测试。
8.2 验证的是"类型规则",不只是最后能打印结果
本次对正文短片段补齐必要导入与外围方法后编译,并单独编译运行配套完整程序。故意错误的示例则逐项确认被拒绝,不能把"报错了"直接算成验证失败。
| 测试目标与输入 | 预期结果 | 实际结果 | 状态 |
|---|---|---|---|
| 四笔订单样本 | 只输出客户甲 900.00 | 客户甲:900.00 | OK |
var quantity = 10 后赋字符串 |
编译失败 | 类型不兼容,被拒绝 | OK |
new ArrayList<>() 配合 var |
元素类型为 Object |
可传入 List<Object> 参数,验证成立 |
OK |
构造参数提供 Long 元素 |
推断 ArrayList<Long> |
可传入 List<Long> 参数,验证成立 |
OK |
80 / 100 与显式转 double 后相除 |
分别为 0、0.8 | 与预期一致 | OK |
| Lambda 参数写 var | Java 10 拒绝,Java 11 接受 | 两种编译级别结果符合预期 | OK |
| final var 引用指向可变列表后添加元素 | 添加成功 | 列表含有新增元素 | OK |
验证中最容易推翻直觉的是空泛型集合:它能编译,恰好说明编译器遵循了推断规则,不能说明它保留了开发者心里期待的 Long 约束。对于泛型,运行时 getClass 无法代替静态类型验证,因为泛型类型擦除后通常无法从该对象直接区分 List<Long> 和 List<Object>。
小鱼点睛
验证 var 要同时检查"允许的代码能编译""禁止的代码被拒绝"和"推断结果符合业务意图"。只有一条运行成功的输出,证明不了所有类型边界。
九、回到业务代码:什么时候保留显式类型?
OpenJDK 的 var 风格指南强调:读代码比写代码更频繁,可读性不应依赖 IDE,也不存在"所有局部变量一律使用/禁止使用 var"的万能规则。
可以采用以下判断:
| 场景 | 建议 | 原因 |
|---|---|---|
var amount = new BigDecimal("19.90") |
可以使用 | 右侧类型明确,变量含义清楚 |
| 遍历类型已知的订单集合 | 可以使用 | for (var order : orders) 在短上下文内易读 |
| 长泛型声明、短作用域 | 结合上下文使用 | 能减少重复,同时保证读者理解集合结构 |
var result = service.execute(request) |
谨慎 | 读者可能不知道返回订单、数量、布尔值还是响应包装 |
| ID、计数、比例等容易受字面量影响的位置 | 优先保留类型或明确字面量 | 避免 int/long、float/double 混淆 |
| 空泛型容器 | 必须检查推断结果 | 不要意外得到 Object 元素类型 |
| 大方法中跨很多行使用的变量 | 先缩小作用域 | 类型来源距离太远,省略后更难维护 |
尤其不要因为"这是新特性",就把整个项目的 String、List、Map 批量改成 var。对于一行清楚的代码,省几个字符没有必要成为一次大范围改动的理由。
9.1 再确认几个容易混淆的边界
1. var 是 Java 的动态类型吗?
不是。它是编译期的局部变量类型推断,变量仍受静态类型检查。
2. var 是普通关键字吗?
JEP 286 将其描述为保留类型名,而不是像 int、class 那样的普通关键字。它不能拿来声明类名,但并不是从此所有标识符位置都禁止出现 var。学习时应重点记住允许使用的位置。
3. var 会降低运行效率吗?
类型推断发生在编译期,没有增加运行时"猜类型"的步骤。在推断类型与原显式类型一致、表达式相同的前提下,不应期待 var 本身带来性能提升或下降。如果替换导致静态类型或重载选择变化,那是代码语义变了,要单独分析。
4. var 只能推断基本类型和普通对象吗?
不是。数组、泛型类型也可以推断;JEP 286 还允许某些无法直接用普通类型名写出的匿名类类型、交叉类型。本篇不展开这些边界,不能据此把 var 简化成"永远等价于手写某个类名"。
参考资料
- JEP 286:Local-Variable Type Inference(Java 10)
- OpenJDK:Local Variable Type Inference Style Guidelines
- JEP 323:Local-Variable Syntax for Lambda Parameters(Java 11)
文中语法规则以 JEP 为依据,使用建议结合 OpenJDK 风格指南和本文教学示例整理。业务示例不冒充生产项目实践结果。
总结
本文围绕"在订单汇总代码中,哪些类型信息可以交给编译器推断",主要分析了:
- 初始化表达式怎样决定局部变量类型,以及它与 Object、final 的区别;
- 金额、集合和方法返回值在真实执行链路中怎样传递;
- 空泛型容器、数字字面量和接口声明替换可能带来的语义变化;
- 如何区分 Java 10 / Java 11,并用编译结果和运行输出验证判断。
整个学习过程可以概括为:
text
先确认订单汇总规则
→ 找到每个变量的初始化表达式
→ 判断编译期推断类型
→ 检查业务约束是否仍然清楚
→ 用编译和运行结果验证
我更想记住的是:少写一个类型名,只是语法上的变化;仍能让别人看懂金额、订单和客户之间的数据关系,才是值得保留的写法。这次只验证了独立教学程序,没有改造业务仓库,也没有据此得出生产环境性能或财务正确性的结论。
小鱼点睛
好的 var 写法应当减少重复信息,同时保留业务含义。初始化表达式越明确、变量作用域越小,越适合使用;如果省略后必须来回跳转才能看懂,显式类型往往更有价值。
这篇文章记录了我对 Java 局部变量类型推断的学习与验证。如果你在业务代码中遇到不同的使用场景,欢迎结合具体例子一起交流。