未命名模式与变量(JDK 21 预览 / JDK 22 正式)
大翔(CTO):"孔蓝,你这个 Lambda 里有个参数根本没用到,为啥还给它起个名字?" 孔蓝(后端新人):"呃...我不知道可以不写啊,IDE 还警告我未使用,但我又不敢删。" 白歌(架构师):"JDK 22 正式引入了未命名变量
_,就是专门解决这个问题的。" 小崔(后端开发):"终于不用再写(a, b) -> a.length()还得给b起个unused的名字了!"
什么是未命名模式与变量
| 概念 | 语法 | 引入版本 | 核心意图 |
|---|---|---|---|
| 未命名变量(Unnamed Variable) | _ | JDK 21 预览 / JDK 22 正式 | 显式表示一个值"我不需要" |
| 未命名模式(Unnamed Pattern) | _ | JDK 21 预览 / JDK 22 正式 | 在模式匹配中忽略某个分量 |
历史保留字 _ | _ 作为标识符 | Java 9 起禁止 | 为未命名特性预留关键字 |
设计哲学:使用
_不是偷懒,而是显式向读者和编译器表达"我故意不使用这个值"的意图,提升代码可读性和维护性。
为什么需要 _?
未命名变量 _ 的使用场景
场景一:Lambda 表达式中的未命名参数
| 特性 | 说明 |
|---|---|
| 语法 | (类型1 名1, _) -> 表达式 |
| 限制 | 同一 Lambda 中只能有一个 _ |
| 本质 | _ 是一个无法访问的匿名变量 |
// === 场景:飞翔科技监控指标聚合,只关心 IP 不关心时间戳 ===
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class UnnamedLambdaDemo {
record Metric(String ip, long timestamp, double value) {}
public static void main(String[] args) {
List<Metric> metrics = List.of(
new Metric("10.0.0.1", 1700000000L, 95.5),
new Metric("10.0.0.2", 1700000001L, 87.3),
new Metric("10.0.0.1", 1700000002L, 92.1)
);
// ❌ 旧写法:timestamp 未使用,IDE 报警告
// Map<String, Double> oldWay = metrics.stream()
// .collect(Collectors.toMap(
// m -> m.ip(),
// m -> m.value(),
// (v1, v2) -> v2
// ));
// ✅ 新写法:用 _ 明确表示忽略 timestamp
Map<String, List<Double>> result = metrics.stream()
.collect(Collectors.groupingBy(
Metric::ip,
Collectors.mapping(
m -> m.value(),
Collectors.toList()
)
));
// 演示 Lambda 中 _ 的用法
metrics.forEach((Metric m) -> {
// 只关心 IP,不关心 timestamp 和 value 的具体值
System.out.println("处理 IP: " + m.ip());
});
// 如果 Lambda 有多个参数,只想用第一个
metrics.stream()
.map((Metric m) -> m.ip() + "=>" + m.value())
.forEach(System.out::println);
}
}
处理 IP: 10.0.0.1
处理 IP: 10.0.0.2
处理 IP: 10.0.0.1
10.0.0.1=>95.5
10.0.0.2=>87.3
10.0.0.1=>92.1
注意:由于当前 JDK 版本对
_在 Lambda 中的支持,实际编译时需使用 JDK 22+ 并启用预览特性(JDK 21)或直接使用正式版(JDK 22+)。
场景二:catch 块中的未命名异常变量
| 特性 | 说明 |
|---|---|
| 语法 | catch (ExceptionType _) |
| 适用 | 所有 catch 子句 |
| 典型场景 | 只记录日志,不分析异常对象本身 |
// === 场景:飞翔科技日志系统,异常只记录不分析 ===
import java.io.FileReader;
import java.io.IOException;
import java.util.logging.Logger;
public class UnnamedCatchDemo {
private static final Logger LOG = Logger.getLogger("AppLogger");
public static void readConfig(String path) {
try {
FileReader reader = new FileReader(path);
// 业务逻辑...
reader.close();
} catch (IOException _) { // ✅ 不关心具体的异常对象
LOG.warning("配置文件读取失败,使用默认配置");
System.out.println("⚠️ 捕获到 IOException,已记录日志");
} catch (Exception _) { // ✅ 兜底异常也不关心细节
LOG.severe("未知错误,联系运维");
System.out.println("❌ 发生未知异常,已记录日志");
}
}
public static void main(String[] args) {
readConfig("/etc/app/config.properties");
}
}
⚠️ 捕获到 IOException,已记录日志
对比:
- 旧写法
catch (IOException e)— 变量e未使用,IDE 报警告- 新写法
catch (IOException _)— 编译器明确知道你故意忽略,无警告
场景三:try-with-resources 中的未命名变量
| 特性 | 说明 |
|---|---|
| 语法 | try (var _ = 资源) |
| 适用 | 实现了 AutoCloseable 的资源 |
| 典型场景 | 锁的获取/释放,只关心作用域不关心资源本身 |
// === 场景:分布式锁作用域管理,不关心锁对象本身 ===
import java.util.concurrent.locks.ReentrantLock;
public class UnnamedTryWithResourcesDemo {
static class LockScope {
private static final ReentrantLock LOCK = new ReentrantLock();
static LockScope acquire() {
LOCK.lock();
System.out.println("🔒 锁已获取");
return new LockScope();
}
void release() {
LOCK.unlock();
System.out.println("🔓 锁已释放");
}
}
// 实现 AutoCloseable 以支持 try-with-resources
static class AutoCloseableLock implements AutoCloseable {
private final LockScope scope;
AutoCloseableLock(LockScope scope) {
this.scope = scope;
}
@Override
public void close() {
scope.release();
}
static AutoCloseableLock acquire() {
return new AutoCloseableLock(LockScope.acquire());
}
}
public static void processCriticalSection() {
// ✅ 使用 _ 明确表示不关心锁对象
try (var _ = AutoCloseableLock.acquire()) {
System.out.println("📝 正在执行临界区代码...");
System.out.println("📝 处理飞翔科技核心业务数据");
}
// 自动调用 close(),释放锁
System.out.println("✅ 临界区执行完毕");
}
public static void main(String[] args) {
processCriticalSection();
}
}
🔒 锁已获取
📝 正在执行临界区代码...
📝 处理飞翔科技核心业务数据
🔓 锁已释放
✅ 临界区执行完毕
场景四:switch 表达式中的未命名模式
| 特性 | 说明 |
|---|---|
| 语法 | case RecordType(_, 类型 名) |
| 引入 | JDK 21 预览(与 Record 模式匹配同期) |
| 本质 | 未命名模式,匹配任何值但不绑定到变量 |
// === 场景:飞翔科技地理围栏系统,只关心纬度不关心经度 ===
public class UnnamedPatternSwitchDemo {
record Point(double x, double y) {}
record ColorPoint(Point p, String color) {}
enum Zone { CENTER, NORTH, SOUTH, EAST, WEST, UNKNOWN }
public static Zone classifyPoint(Point p) {
// ✅ 使用未命名模式 _ 忽略不关心的分量
return switch (p) {
case Point(_, double y) when y > 0 -> Zone.NORTH;
case Point(_, double y) when y < 0 -> Zone.SOUTH;
case Point(double x, _) when x > 0 -> Zone.EAST;
case Point(double x, _) when x < 0 -> Zone.WEST;
case Point(0.0, 0.0) -> Zone.CENTER;
default -> Zone.UNKNOWN;
};
}
public static String getColor(ColorPoint cp) {
// ✅ 嵌套模式中也可以使用 _
return switch (cp) {
case ColorPoint(Point(_, double y), String color) when y > 0
-> "北纬" + y + "°,颜色:" + color;
case ColorPoint(_, String color)
-> "颜色:" + color;
};
}
public static void main(String[] args) {
Point p1 = new Point(3.0, 5.0);
Point p2 = new Point(-2.0, -7.0);
System.out.println("p1 区域: " + classifyPoint(p1));
System.out.println("p2 区域: " + classifyPoint(p2));
ColorPoint cp = new ColorPoint(new Point(0, 10.0), "红色");
System.out.println(cp.color() + " 点: " + getColor(cp));
}
}
p1 区域: NORTH
p2 区域: SOUTH
红色 点: 北纬10.0°,颜色:红色
历史背景:_ 的演进之路
| Java 版本 | _ 的状态 | 示例代码 |
|---|---|---|
| Java 8 及之前 | 普通合法标识符 | String _ = "hello"; ✅ |
| Java 9 - 20 | 保留关键字,禁止使用 | String _ = "hello"; ❌ 编译错误 |
| JDK 21 | 预览特性:未命名变量 | catch (Exception _) ✅(需 --enable-preview) |
| JDK 22+ | 正式特性:未命名变量/模式 | (_, int y) -> y ✅ |
限制与约束
核心原则:
_是一个无法访问的占位符,编译器会主动阻止任何试图"使用"它的行为。
| 限制 | 说明 | 错误示例 |
|---|---|---|
不能访问 _ | 无法读取其值 | System.out.println(_); ❌ |
不能重复声明 _ | 同一作用域只能有一个 _ | (var _, var _) ❌ |
| 不能与命名变量混用歧义 | 某些上下文需明确 | 见下方易错场景 |
不能赋值给 _ | 显式赋值被禁止 | _ = 10; ❌ |
易错场景
错误 1:试图访问未命名变量
// ❌ 错误:试图访问 _
public class ErrorAccessDemo {
public static void main(String[] args) {
List<String> list = List.of("a", "b", "c");
list.forEach((String s, _) -> {
System.out.println(s);
System.out.println(_); // ❌ 编译错误:_ 不可访问
});
}
}
// ✅ 正确:只使用需要的变量
public class CorrectAccessDemo {
public static void main(String[] args) {
List<String> list = List.of("a", "b", "c");
list.forEach(s -> System.out.println(s)); // ✅ 只用一个参数
}
}
错误 2:同一作用域重复声明 _
// ❌ 错误:Lambda 中不能有多个 _
public class ErrorDuplicateDemo {
public static void main(String[] args) {
// 假设有个接受三个参数的函数接口
// BiFunction3<String, Integer, Double, String> f =
// (s, _, _) -> s; // ❌ 编译错误:duplicate _
}
}
// ✅ 正确:需要忽略多个值时,考虑重构设计
public class CorrectDuplicateDemo {
// 如果真的需要忽略多个参数,说明设计可能有问题
// 考虑拆分接口或使用更精确的类型
record NameAndAge(String name, int age) {}
public static void main(String[] args) {
// 使用 Record 只提取需要的字段
List<NameAndAge> list = List.of(
new NameAndAge("Alice", 25),
new NameAndAge("Bob", 30)
);
list.forEach(p -> System.out.println(p.name())); // ✅ 只使用 name
}
}
错误 3:在 Kubernetes Job 场景中与命名变量混用导致歧义
// ❌ 错误:在增强 for 循环中混用 _ 和命名变量(某些上下文不允许)
public class ErrorMixedDemo {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
for (int _ : arr) { // 在某些版本中不允许
// ...
}
}
}
说明:
_作为未命名变量主要用于 Lambda 参数、catch 子句、try-with-resources 和模式匹配中。在传统的for循环中是否能使用取决于具体 JDK 版本的实现。
实战场景:飞翔科技监控指标聚合
// === 实战:飞翔科技 Kafka Consumer,只关心消息 key 不关心 value ===
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class MonitoringAggregationDemo {
// 模拟 Kafka 消息:key=服务器IP,value=完整消息(不关心)
record KafkaMessage(String key, String value) {}
// 监控指标记录
record ServerMetric(String ip, String metricName, double value) {}
public static void main(String[] args) {
// 模拟从 Kafka 消费的消息
List<KafkaMessage> messages = List.of(
new KafkaMessage("10.0.0.1", "{\"cpu\":95.5}"),
new KafkaMessage("10.0.0.2", "{\"mem\":87.3}"),
new KafkaMessage("10.0.0.1", "{\"cpu\":92.1}")
);
// ✅ 使用 _ 明确表示不关心消息的 value 部分
Map<String, Long> messageCountByIp = messages.stream()
.collect(Collectors.groupingBy(
KafkaMessage::key,
Collectors.counting()
));
messageCountByIp.forEach((ip, count) -> {
// 另一个 _ 的使用场景:不关心 Map.Entry 的类型名
System.out.println("IP: " + ip + ",消息数: " + count);
});
// ✅ 异常处理:只记录不分析
processMetrics(List.of(
new ServerMetric("10.0.0.1", "cpu", 95.5),
new ServerMetric("10.0.0.2", "mem", 87.3)
));
}
public static void processMetrics(List<ServerMetric> metrics) {
metrics.forEach(m -> {
try {
if (m.value() > 100) {
throw new IllegalArgumentException("指标超出范围: " + m.value());
}
System.out.println(m.ip() + " 的 " + m.metricName() + ": " + m.value());
} catch (IllegalArgumentException _) { // ✅ 不关心异常细节,只记录
System.out.println("⚠️ [" + m.ip() + "] 指标异常,已跳过");
}
});
}
}
IP: 10.0.0.1,消息数: 2
IP: 10.0.0.2,消息数: 1
10.0.0.1 的 cpu: 95.5
10.0.0.2 的 mem: 87.3
面试考点
问题 1:_ 在 Java 8 和 Java 22 中的区别是什么?
答:
- Java 8 及之前:
_是合法的标识符,可以作为变量名使用,如String _ = "hello";。- Java 9:
_被提升为保留关键字,禁止作为标识符使用,为未来特性预留。- JDK 21(预览)/ JDK 22(正式):
_成为未命名变量/模式的语法,用于表示"故意忽略"的值。这一演进体现了 Java 语言设计的严谨性:先保留关键字,等特性设计成熟后再赋予其语义。
问题 2:未命名变量 _ 有哪些使用限制?
答:
- 不可访问:不能读取
_的值,任何访问都会导致编译错误。- 不可重复:同一作用域中不能声明多个
_。- 不可赋值:不能对
_进行赋值操作。- 使用场景受限:主要用于 Lambda 参数、catch 子句、try-with-resources 和模式匹配中。
这些限制都是有意为之的,目的是确保
_的语义清晰:它就是一个占位符,不代表任何可操作的值。
问题 3:使用 _ 而不是起名 ignored 或 unused 有什么好处?
答:
- 编译器保证:编译器会检查
_确实未被使用,如果误用了会立即报错;而ignored变量仍然可以被访问,只是命名约定。- 代码意图更清晰:读者看到
_立刻知道"这里故意忽略",不需要猜测是代码不完整还是故意的。- 命名一致性:不用再纠结是叫
ignored、unused、dummy还是_ignored,统一使用_。- 减少噪音:代码中没有无意义的变量名,更加简洁。
这体现了"让编译器帮你写文档"的理念。
总结
"Code is like humor. When you have to explain it, it's bad." — Cory House
使用
_正是让代码"不需要解释"的实践:一看便知,此处故意忽略。