☰
Java中只有两个属性的“键值对”怎么实现?盘点SimpleEntry、Pair与record
2026/9/26 7:49:36 网站建设 项目流程

刚看到这个标题的时候,我第一反应是:这大概率是一个准备面试的朋友,或者刚写完几个工具类的同事搜出来的问题。因为在真实业务里,“键值对”三个字往往会把人带偏到 HashMap 上去,可你再仔细读一遍——“就只有2个属性的”——其实人家要的只是一个能同时装下两个值、且能按“键”取“值”的小对象,而不是一个大而全的 Map 容器。

Java 里这类的选择还真不少,但质量参差不齐,有些是 JDK 自带的,有些藏在第三方库里,有些只存在于特定的 Android 环境下。而且,这个问题的背后还藏着一个很容易被忽略的设计判断题:到底是直接用现成的 Pair 类,还是自己定义一个只有两个字段的业务类?这篇我把能想到的都给捋一遍,包括每个类的适用场景、坑点、示例代码,以及面试时怎么答才能让你跟只会背八股文的人区分开。

1. 先把需求看清:“键值对”不等于“Map集合”

很多人一看到“键值对”就开始背 HashMap 的知识点,这是最大的误区。HashMap 的语义是“一个容器里装了很多对”,而你现在的问题是“就只有2个属性”,说白了就是两个值之间的一对一关系。这两者的应用场景、内存开销、代码可读性完全不同。

1.1 需要“两个属性绑在一起”的常见场景

我梳理了一下平时开发中最常遇到的情况,基本可以归成三类:

  • 方法要返回两个值。Java 的方法签名只允许返回一个对象,但现实里你经常要同时拿到“状态码”和“提示信息”,或者“结果数据”和“分页信息”。用 Map 塞两个 key 是一种解法,但调用方每次都得用魔法字符串去取,很容易写错。
  • 循环里临时配对。比如遍历一个列表,把每个元素的 id 和 name 组成一对传给下一个方法处理。这时候你没有理由专门定义一个全局类,也不想为了这种临时配对搞一张 Map 出来。
  • 作为缓存或事件消息的单条数据。这种场景往往只需要一对数据,却被塞进了 Map,导致代码里充满map.get("userId")这样含义模糊的调用。

在这些前提下,你要的不是 Map 这种“集合结构”,而是一个二元组。真正合适的东西应该是承载两个属性、能让你直接通过键拿到值的对象,而不是一个完整的数据结构。

1.2 用单个元素的 Map 到底有多别扭

先承认,Java 新手用 HashMap 解决这个问题太正常了,我自己刚工作时也干过这种事。代码写出来大概是这样:

Map<String, String> result = new HashMap<>(); result.put("status", "success"); result.put("message", "操作完成"); // 取的时候还得用字符串 String status = result.get("status"); String message = result.get("message");

用多了问题就来了:

  1. 魔法字符串无处不在。取值的 key 一旦拼错,运行时才发现为 null,编译期完全帮不上忙。
  2. 语义不清晰。看代码的人第一眼根本不知道这个 Map 里到底存了哪几个键,你需要翻遍所有 put 的地方才能拼出完整画面。
  3. 性能开销不划算。HashMap 内部有桶数组、Node 节点、扩容机制,为了存一对数据,你无形中创建了一堆多余的对象。在循环里这么写,GC 压力直接翻倍。
  4. 迭代别人写的这种 Map 更要命。你永远不知道里面会不会多出一个你没见过的键,因为 Map 本身的约束力约等于零。

所以如果你真的只是想存一对数据,用 Map 不是“能不能”的问题,而是“值不值得”的问题。在代码评审里看到单条数据 Map,我一般都会建议对方换掉,理由不是报错,而是它让整个代码的意图变得模糊了。

那不用 Map 用什么?下面从 JDK 自带的方案开始。

2. JDK 原生选手:AbstractMap.SimpleEntry 与 SimpleImmutableEntry

如果你不想引入任何第三方依赖,那么最标准的“只有两个属性”的键值对类,就是java.util.AbstractMap里的两个静态内部类:SimpleEntry和SimpleImmutableEntry。

很多人对Map.Entry接口很熟,但并不知道可以直接new出它的实现类。其实SimpleEntry从 Java 1.6 开始就存在了,只是日常写业务的时候很少被单独拎出来用。

2.1 SimpleEntry:可改写、可单独使用的标准键值对

先看最简单的用法:

import java.util.AbstractMap; import java.util.Map; public class SimpleEntryDemo { public static void main(String[] args) { Map.Entry<String, Integer> entry = new AbstractMap.SimpleEntry<>("age", 18); System.out.println(entry.getKey()); // age System.out.println(entry.getValue()); // 18 // 可以直接改 value entry.setValue(21); System.out.println(entry.getKey() + " -> " + entry.getValue()); // age -> 21 } }

关键信息有几点:

  • getKey()拿第一个属性,也就是键。
  • getValue()拿第二个属性,也就是值。
  • setValue()可以修改值,但注意这个方法是Map.Entry接口定义的,理论上它主要服务于 Map 内部更新条目,而不是给外部消费者用的。

SimpleEntry本身也是Map.Entry实现,所以它可以被塞回HashMap吗?技术上可以,因为put单个Map.Entry时,HashMap 会读取它的 key 和 value。但在实际使用中,我们更多是把它当作独立的一对数据来传递,相当于“免容器版的 Map 条目”。

我实际项目中见得最多的用法,是把它当方法返回值:

public Map.Entry<String, Integer> getMaxScore() { return new AbstractMap.SimpleEntry<>("math", 98); }

调用方直接getKey()、getValue(),比返回一个 List 或者 Map 都清爽。不过这个方法签名看起来还是有点“通用”,看代码的人未必能一眼看懂 key 到底是什么。

2.2 SimpleImmutableEntry:只读的键值对,不怕被改

SimpleImmutableEntry跟SimpleEntry几乎一模一样,唯一的区别是不能调用 setValue 修改值。示例:

import java.util.AbstractMap; import java.util.Map; public class ImmutableEntryDemo { public static void main(String[] args) { Map.Entry<String, String> entry = new AbstractMap.SimpleImmutableEntry<>("token", "abc123"); System.out.println(entry.getKey()); // token System.out.println(entry.getValue()); // abc123 // 这行会抛 UnsupportedOperationException entry.setValue("def456"); } }

如果你做的是配置下发、事件通知这类数据,一旦创建就不希望被中途篡改,用SimpleImmutableEntry非常合适。它跟SimpleEntry在线程安全上的区别,本质上不在于这个类本身有多线程保护,而是因为“值引用不可替换”,所以在只看不改的语义下,共享起来更安全。

不过这里要给你提个醒,也是很多人踩过的坑:

注意:SimpleImmutableEntry只是不让替换 value 引用,并不是深不可变。如果你的 value 本身是一个可变对象,比如一个ArrayList,那你照样能拿到这个 list 然后往里加元素。不可变这个概念,要区分“引用不可改”和“对象内容不可改”。

2.3 两个 JDK 原生类的共同弱点

SimpleEntry和SimpleImmutableEntry作为 JDK 自带方案,胜在零依赖、代码简单。但它们有一个共同的尴尬:字段名太抽象。

你想一想,如果你要保存的是“姓名+年龄”,那getKey()返回姓名还是年龄?getValue()返回的又是哪个?如果不查 put 的地方,或者不看调用处注释,读代码的人很容易搞混。

而且它们没有任何方法能告诉你“键的类型和值的类型分别是什么业务含义”,一切都靠约定。所以我的建议是:临时传递、纯粹为了省事时用它们;一旦这两个属性在业务里反复出现,就别偷懒了,还是自己定义一个类更稳妥。

3. 第三方 Pair 家族:Apache Commons、JavaFX 与 Android

JDK 之外,实际开发里还有几个常见的 Pair 类值得了解。它们有的来自 Apache 老牌工具库,有的曾经跟 JDK 绑定过,有的则是 Android 开发者的老朋友。

3.1 Apache Commons Lang3:功能最全的 Pair 工具

如果你项目里已经用了 Apache Commons Lang3,那它提供的Pair系列应该是使用最顺手的。坐标如下:

<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.14.0</version> </dependency>

核心类在org.apache.commons.lang3.tuple包下,包括抽象类Pair,以及两个具体实现ImmutablePair和MutablePair。

最常见用法是:

import org.apache.commons.lang3.tuple.Pair; public class PairDemo { public static void main(String[] args) { // of 方法默认返回 ImmutablePair Pair<String, Integer> pair = Pair.of("age", 18); // 两套读取 API 都可以 System.out.println(pair.getLeft()); // age System.out.println(pair.getValue()); // 18 System.out.println(pair.getKey()); // age System.out.println(pair.getRight()); // 18 } }

Pair.of静态工厂方法返回的是一个不可变对,如果需要修改 value,可以显式用MutablePair.of:

MutablePair<String, Integer> mutablePair = MutablePair.of("age", 18); mutablePair.setValue(20); System.out.println(mutablePair);

Apache 这套方案比 JDK 自带强在哪?我觉得主要是三点:

  • 同时提供 Left/Right 和 Key/Value 两套命名,你可以选更能表达当前语义的那一套读法。
  • 实现了 equals、hashCode、toString、compareTo,调试和放进集合的时候很省心。
  • 实现了 Serializable,在某些需要序列化传递的场景下直接可用。

代价是引入一个第三方依赖。如果你的项目本来就有 Commons Lang3,那用它没毛病;如果就是为了一个键值对单独引一个包,我个人觉得不划算。

3.2 JavaFX 与 Android 各自的 Pair

还有一个很多人不知道的点:javafx.util.Pair曾经是 JDK 的一部分。在 JDK 8 到 JDK 10 时代,你什么都不用引,直接就能:

import javafx.util.Pair; Pair<String, String> pair = new Pair<>("name", "Tom"); System.out.println(pair.getKey()); // name System.out.println(pair.getValue()); // Tom

但请注意,从 JDK 11 开始 JavaFX 从 JDK 中剥离了,所以现在你在新版 JDK 里直接用javafx.util.Pair是会编译报错的。这个类更多是历史遗留项目里出现,新项目一般不会遇到。

Android 环境也有一个android.util.Pair,它的读取方式不太一样,用的是公共字段first和second:

import android.util.Pair; Pair<String, Integer> pair = new Pair<>("age", 18); String key = pair.first; Integer value = pair.second;

这种“暴露字段”的写法更直接,但也意味着你没有getKey()、getValue()这种方法约束,外部代码能直接改字段(如果字段不是 final)。好在 Android 的 Pair 构造完之后,两个字段是 final 的,所以基本还是只读语义。

至于键盘事件里的keyCode、KeyEvent.KEYCODE_*那些定义,属于 Android 输入系统的事件键值,跟我们要讨论的“Java 键值对类”是两码事,别混为一谈。

3.3 对比这些 Pair 类,该怎么选

我把主流候选方案放在一起对比一下,方便你直接参考:

类来源可变性读取方式适合场景
AbstractMap.SimpleEntryJDK 1.6+可变getKey/getValue临时传递、单条键值对、Map 实现内部
AbstractMap.SimpleImmutableEntryJDK 1.6+不可变getKey/getValue只读传递、常量、事件数据
javafx.util.PairJDK 8~10 自带,之后移除不可变getKey/getValue老项目兼容
org.apache.commons.lang3.tuple.Pair第三方库抽象类,有不可变/可变实现getLeft/right、getKey/value已引入 Commons Lang3 的项目
android.util.PairAndroid SDK字段 final,基本只读first/second 公共字段Android 开发
自定义类或 record自己写看实现业务字段名绝大多数真实业务代码

这里“可变性”我统一指“能否替换 value 的引用”,不是说里面的对象内容不能改。你再看一眼这个表就会发现,除了自定义类,其他方案的语义多多少少都带着“通用二元组”的味道,缺少业务含义。

4. 面试官真正想听到的回答:自己定义一个只有两个属性的类

这个问题十有八九出现在 Java 基础面试里。我见过太多候选人一上来就背“HashMap 是基于数组加链表加红黑树实现的”,却完全没意识到面试官问的是“只有两个属性”的场景。下面说说我对这道题的理解。

4.1 为什么“自己写一个类”反而更优

先设一个很常见的场景:你需要在方法里返回“用户 id 和用户名称”。用SimpleEntry写是这样的:

Map.Entry<Long, String> result = new AbstractMap.SimpleEntry<>(1001L, "张三"); Long userId = result.getKey(); String userName = result.getValue();

代码能跑,但你调用getKey()的时候,脑子里必须要加一层“key 是 id”的映射。如果这个方法被十几处代码调用,这层隐式映射就会在每一次阅读时增加认知负担。

换成自己定义的类之后:

public class UserBrief { private final Long userId; private final String userName; public UserBrief(Long userId, String userName) { this.userId = userId; this.userName = userName; } public Long getUserId() { return userId; } public String getUserName() { return userName; } }

或者用 Lombok 的@Value,代码更短。这样带来的收益是非常明确的:

  1. 语义自解释。方法返回类型UserBrief一看就知道是“用户摘要信息”,getUserName()也不会让你猜。
  2. 类型安全。两个字段的类型被固定,编译期就能检查。而Pair<String, String>无法约束第一项是 id 还是 name。
  3. 便于扩展。后续如果还要加一个avatarUrl字段,在类里加一个字段和构造参数即可,所有调用点都能感知到变化;如果一开始用 Map,新增字段意味着新增一个魔法字符串 key,后果不堪设想。
  4. 几乎没有额外成本。一个二十行的类在 Java 项目里完全不是负担,反而是 Java 面向对象的基本功。

所以,除非两个属性只是 ** 凑巧要绑在一起、用完就扔**,否则自定义类几乎永远是更优解。这也是经验丰富的开发者不会第一条就推荐HashMap的原因。

4.2 用 record 一句话解决“只有2个属性”

如果你用的是 JDK 14 及以上,那么答案里一定要提record。这是 Java 原生支持“我只想定义两个字段”的极简语法:

public record UserBrief(Long userId, String userName) {}

就这么一行,构造器、equals、hashCode、toString自动生成,字段默认是 private final。调用方式:

UserBrief user = new UserBrief(1001L, "张三"); System.out.println(user.userId()); // 注意 record 的访问方法是 userId() System.out.println(user.userName());

record特别适合这种“数据载体”的场景,因为它本身定位就是不可变数据聚合体,不需要任何繁琐的样板代码。我在新项目里,凡是遇到“只有两个属性、要来回传递”的数据,第一选择基本都是record。

注意:record的 getter 不是getUserId(),而是userId()。如果你从 JavaBean 那一套切过来,刚开始容易写顺手了直接敲getUserId()然后报编译错误。这个需要一点适应时间,但适应之后就会觉得这才是 Java 该有的简洁。

4.3 怎么回答才能不落俗套

如果是面试题,我会建议你按这个顺序来组织答案:

  1. 先把问题定义清楚。面试官问的是“只有2个属性”的键值对,不是“键值对集合”。所以先指出这一点,说明你清楚 Map 和二元组之间的边界。
  2. 给出标准答案。JDK 自带的就是AbstractMap.SimpleEntry和AbstractMap.SimpleImmutableEntry,可以单独 new 出来当键值对用。
  3. 再提第三方方案。如果允许引入依赖,Apache Commons Lang3 的Pair/ImmutablePair/MutablePair也很常用;JavaFX 和 Android 也各有一版Pair,但要注意版本和平台限制。
  4. 最后落点要高级。谈谈“如果这两个属性具有明确业务含义,自定义类或者 record 往往比任何通用 Pair 都好”。这一句话就能跟只会背 API 的候选人拉开差距。

这样回答,既展示了知识面,又体现了设计判断力,面试官很难不点头。

最后再分享一个小技巧。如果你的项目里有人一直在用“只有一个元素的 HashMap”,评审的时候不要只说“这样不对”,直接把SimpleEntry或record的代码示例甩过去。你会发现,很多这种用法并不是同事不会别的,而是他们压根不知道 Java 里还有这么轻量的键值对类。把这个认知缺口补上,代码能清爽一大截。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询