Java 10 的 var 为什么不能随便用?从订单汇总看懂类型推断与泛型陷阱

本文主题: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>();

在这两个例子中,编译器分别推断出 BigDecimalArrayList<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,以后可以赋另一个字符串,也可以赋 nullvar 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;
}

沿着调用顺序,可以读出三个事实:

  1. totalsByCustomer 的初始化表达式是 new LinkedHashMap<String, BigDecimal>(),推断类型为 LinkedHashMap<String, BigDecimal>;它保留客户首次出现的顺序。
  2. orders 的参数类型已经写明为 List<Order>,因此增强 for 中的 order 推断为 Order
  3. 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 无法告诉编译器你想要的是 OrderString,还是其他引用类型。可以写 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 被标红,先核对三项:

  1. Project SDK:使用支持目标语言级别的 JDK。
  2. Language level :局部变量 var 至少为 10;Lambda 参数 var 至少为 11。
  3. 构建配置 :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/longfloat/double 混淆
空泛型容器 必须检查推断结果 不要意外得到 Object 元素类型
大方法中跨很多行使用的变量 先缩小作用域 类型来源距离太远,省略后更难维护

尤其不要因为"这是新特性",就把整个项目的 StringListMap 批量改成 var。对于一行清楚的代码,省几个字符没有必要成为一次大范围改动的理由。

9.1 再确认几个容易混淆的边界

1. var 是 Java 的动态类型吗?

不是。它是编译期的局部变量类型推断,变量仍受静态类型检查。

2. var 是普通关键字吗?

JEP 286 将其描述为保留类型名,而不是像 intclass 那样的普通关键字。它不能拿来声明类名,但并不是从此所有标识符位置都禁止出现 var。学习时应重点记住允许使用的位置。

3. var 会降低运行效率吗?

类型推断发生在编译期,没有增加运行时"猜类型"的步骤。在推断类型与原显式类型一致、表达式相同的前提下,不应期待 var 本身带来性能提升或下降。如果替换导致静态类型或重载选择变化,那是代码语义变了,要单独分析。

4. var 只能推断基本类型和普通对象吗?

不是。数组、泛型类型也可以推断;JEP 286 还允许某些无法直接用普通类型名写出的匿名类类型、交叉类型。本篇不展开这些边界,不能据此把 var 简化成"永远等价于手写某个类名"。

参考资料

  1. JEP 286:Local-Variable Type Inference(Java 10)
  2. OpenJDK:Local Variable Type Inference Style Guidelines
  3. JEP 323:Local-Variable Syntax for Lambda Parameters(Java 11)

文中语法规则以 JEP 为依据,使用建议结合 OpenJDK 风格指南和本文教学示例整理。业务示例不冒充生产项目实践结果。

总结

本文围绕"在订单汇总代码中,哪些类型信息可以交给编译器推断",主要分析了:

  1. 初始化表达式怎样决定局部变量类型,以及它与 Object、final 的区别;
  2. 金额、集合和方法返回值在真实执行链路中怎样传递;
  3. 空泛型容器、数字字面量和接口声明替换可能带来的语义变化;
  4. 如何区分 Java 10 / Java 11,并用编译结果和运行输出验证判断。

整个学习过程可以概括为:

text 复制代码
先确认订单汇总规则
→ 找到每个变量的初始化表达式
→ 判断编译期推断类型
→ 检查业务约束是否仍然清楚
→ 用编译和运行结果验证

我更想记住的是:少写一个类型名,只是语法上的变化;仍能让别人看懂金额、订单和客户之间的数据关系,才是值得保留的写法。这次只验证了独立教学程序,没有改造业务仓库,也没有据此得出生产环境性能或财务正确性的结论。

小鱼点睛

好的 var 写法应当减少重复信息,同时保留业务含义。初始化表达式越明确、变量作用域越小,越适合使用;如果省略后必须来回跳转才能看懂,显式类型往往更有价值。


这篇文章记录了我对 Java 局部变量类型推断的学习与验证。如果你在业务代码中遇到不同的使用场景,欢迎结合具体例子一起交流。

相关推荐
信誓旦旦的程序猿41 分钟前
【量化系统从零构建 #04】存储设计:选型·建库·交易日历
java·人工智能·python·股票数据api·股票数据·股票数据api接口·股票api数据接口
shmily麻瓜小菜鸡1 小时前
JS/TS 易踩坑知识点 — 模块化与工程化类
开发语言·javascript·ecmascript
geovindu1 小时前
CSharp: Observer Pattern
开发语言·后端·观察者模式·设计模式·c#·.netcore·行为模式
Flynt2 小时前
把公司项目迁到 Spring Boot 4.0:编译通过只是开始
java·spring boot·后端
郝学胜-神的一滴2 小时前
C++11 工程级应用 09:告别无谓拷贝,解锁高性能移动语义
开发语言·数据结构·c++·vscode·软件工程·visual studio
Tangyuewei2 小时前
给老 Spring 项目装个 AI Agent
java·人工智能·spring
她的男孩2 小时前
多租户和数据权限怎么共存?扒完拦截器注册链路,我找到 4 个隐蔽的坑
java·后端·架构
MC皮蛋侠客3 小时前
OPC UA 系列(一):标准全景与 Python 最小闭环——让第一条设备数据流动起来
开发语言·python·opcua
小溪学编程3 小时前
Java BufferedReader 详解:从基础用法到性能优化
java·python·性能优化