Java中只有两个属性的“键值对”怎么实现?盘点SimpleEntry、Pair与record
刚看到这个标题的时候我第一反应是这大概率是一个准备面试的朋友或者刚写完几个工具类的同事搜出来的问题。因为在真实业务里“键值对”三个字往往会把人带偏到 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 解决这个问题太正常了我自己刚工作时也干过这种事。代码写出来大概是这样MapString, String result new HashMap(); result.put(status, success); result.put(message, 操作完成); // 取的时候还得用字符串 String status result.get(status); String message result.get(message);用多了问题就来了魔法字符串无处不在。取值的 key 一旦拼错运行时才发现为 null编译期完全帮不上忙。语义不清晰。看代码的人第一眼根本不知道这个 Map 里到底存了哪几个键你需要翻遍所有 put 的地方才能拼出完整画面。性能开销不划算。HashMap 内部有桶数组、Node 节点、扩容机制为了存一对数据你无形中创建了一堆多余的对象。在循环里这么写GC 压力直接翻倍。迭代别人写的这种 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.EntryString, 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.EntryString, 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.EntryString, 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 与 AndroidJDK 之外实际开发里还有几个常见的 Pair 类值得了解。它们有的来自 Apache 老牌工具库有的曾经跟 JDK 绑定过有的则是 Android 开发者的老朋友。3.1 Apache Commons Lang3功能最全的 Pair 工具如果你项目里已经用了 Apache Commons Lang3那它提供的Pair系列应该是使用最顺手的。坐标如下dependency groupIdorg.apache.commons/groupId artifactIdcommons-lang3/artifactId version3.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 PairString, 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.ofMutablePairString, 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; PairString, 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和secondimport android.util.Pair; PairString, 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.EntryLong, 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代码更短。这样带来的收益是非常明确的语义自解释。方法返回类型UserBrief一看就知道是“用户摘要信息”getUserName()也不会让你猜。类型安全。两个字段的类型被固定编译期就能检查。而PairString, String无法约束第一项是 id 还是 name。便于扩展。后续如果还要加一个avatarUrl字段在类里加一个字段和构造参数即可所有调用点都能感知到变化如果一开始用 Map新增字段意味着新增一个魔法字符串 key后果不堪设想。几乎没有额外成本。一个二十行的类在 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 怎么回答才能不落俗套如果是面试题我会建议你按这个顺序来组织答案先把问题定义清楚。面试官问的是“只有2个属性”的键值对不是“键值对集合”。所以先指出这一点说明你清楚 Map 和二元组之间的边界。给出标准答案。JDK 自带的就是AbstractMap.SimpleEntry和AbstractMap.SimpleImmutableEntry可以单独 new 出来当键值对用。再提第三方方案。如果允许引入依赖Apache Commons Lang3 的Pair/ImmutablePair/MutablePair也很常用JavaFX 和 Android 也各有一版Pair但要注意版本和平台限制。最后落点要高级。谈谈“如果这两个属性具有明确业务含义自定义类或者 record 往往比任何通用 Pair 都好”。这一句话就能跟只会背 API 的候选人拉开差距。这样回答既展示了知识面又体现了设计判断力面试官很难不点头。最后再分享一个小技巧。如果你的项目里有人一直在用“只有一个元素的 HashMap”评审的时候不要只说“这样不对”直接把SimpleEntry或record的代码示例甩过去。你会发现很多这种用法并不是同事不会别的而是他们压根不知道 Java 里还有这么轻量的键值对类。把这个认知缺口补上代码能清爽一大截。