1. 概述
在Java中,LinkedHashMap
是维护键值对插入顺序的利器。实际开发中,我们经常需要获取LinkedHashMap
的第一个或最后一个条目。本文将探讨几种实现方式,并分析各自优劣。
✅ 核心要点:
LinkedHashMap
继承自HashMap
,但额外维护了插入顺序- 支持两种顺序模式:插入顺序和访问顺序
- 本文以插入顺序为例,但方法同样适用于访问顺序
2. 准备LinkedHashMap示例
先快速回顾LinkedHashMap
的特性:它属于Java集合框架,与普通HashMap
的最大区别在于维护元素的插入顺序。根据构造参数不同,可以是插入顺序或访问顺序(访问顺序下,最近访问的元素会移至末尾)。
我们以插入顺序为例,创建测试用例:
static final LinkedHashMap<String, String> THE_MAP = new LinkedHashMap<>();
static {
THE_MAP.put("key one", "a1 b1 c1");
THE_MAP.put("key two", "a2 b2 c2");
THE_MAP.put("key three", "a3 b3 c3");
THE_MAP.put("key four", "a4 b4 c4");
}
⚠️ 注意:本文假设LinkedHashMap
非空,将通过单元测试验证各方案效果。
3. 遍历Map条目法
Map
的entrySet()
方法返回所有条目的Set
视图。对于LinkedHashMap
,这个Set
中的元素顺序与Map内部顺序一致。因此,我们可以通过迭代器轻松获取首尾元素:
获取第一个条目
Entry<String, String> firstEntry = THE_MAP.entrySet().iterator().next();
assertEquals("key one", firstEntry.getKey());
assertEquals("a1 b1 c1", firstEntry.getValue());
获取最后一个条目
需要遍历到最后一个元素:
Entry<String, String> lastEntry = null;
Iterator<Entry<String,String>> it = THE_MAP.entrySet().iterator();
while (it.hasNext()) {
lastEntry = it.next();
}
assertNotNull(lastEntry);
assertEquals("key four", lastEntry.getKey());
assertEquals("a4 b4 c4", lastEntry.getValue());
🔍 关键点:迭代操作不会改变访问顺序LinkedHashMap
的元素顺序,只有显式的get(key)
等操作才会影响顺序。
4. 转换数组法
数组支持高效的随机访问。我们可以将LinkedHashMap
条目转为数组,直接获取首尾元素:
Entry<String, String>[] theArray = new Entry[THE_MAP.size()];
THE_MAP.entrySet().toArray(theArray);
获取首尾元素
// 获取第一个元素
Entry<String, String> firstEntry = theArray[0];
assertEquals("key one", firstEntry.getKey());
assertEquals("a1 b1 c1", firstEntry.getValue());
// 获取最后一个元素
Entry<String, String> lastEntry = theArray[THE_MAP.size() - 1];
assertEquals("key four", lastEntry.getKey());
assertEquals("a4 b4 c4", lastEntry.getValue());
✅ 优势:代码简洁,性能稳定(时间复杂度O(1))
5. Stream API法
Java 8引入的Stream API提供了更函数式的处理方式:
获取第一个条目
Entry<String, String> firstEntry = THE_MAP.entrySet().stream().findFirst().get();
assertEquals("key one", firstEntry.getKey());
assertEquals("a1 b1 c1", firstEntry.getValue());
获取最后一个条目
使用skip()
方法跳过前n-1个元素:
Entry<String, String> lastEntry = THE_MAP.entrySet().stream()
.skip(THE_MAP.size() - 1)
.findFirst()
.get();
assertNotNull(lastEntry);
assertEquals("key four", lastEntry.getKey());
assertEquals("a4 b4 c4", lastEntry.getValue());
⚠️ 注意:skip(n)
会丢弃前n个元素,当n=map.size()-1时,流中只剩最后一个元素。
6. 反射API法
深入LinkedHashMap
源码(截至Java 21),它通过双向链表维护顺序,并用head
和tail
引用首尾节点:
/**
* 双向链表的头节点(最旧)
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* 双向链表的尾节点(最新)
*/
transient LinkedHashMap.Entry<K,V> tail;
通过反射可以直接读取这两个字段:
// 获取第一个条目
Field head = THE_MAP.getClass().getDeclaredField("head");
head.setAccessible(true);
Entry<String, String> firstEntry = (Entry<String, String>) head.get(THE_MAP);
assertEquals("key one", firstEntry.getKey());
assertEquals("a1 b1 c1", firstEntry.getValue());
// 获取最后一个条目
Field tail = THE_MAP.getClass().getDeclaredField("tail");
tail.setAccessible(true);
Entry<String, String> lastEntry = (Entry<String, String>) tail.get(THE_MAP);
assertEquals("key four", lastEntry.getKey());
assertEquals("a4 b4 c4", lastEntry.getValue());
❌ 踩坑警告:Java 9+的模块系统会限制反射访问,运行时会报错:
java.lang.reflect.InaccessibleObjectException: Unable to make field transient java.util.LinkedHashMap$Entry
java.util.LinkedHashMap.head accessible: module java.base does not "opens java.util" to unnamed module ...
解决方案:在Maven的surefire插件中添加JVM参数:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>
--add-opens java.base/java.util=ALL-UNNAMED
</argLine>
</configuration>
</plugin>
</plugins>
</build>
⚠️ 风险提示:此方法依赖LinkedHashMap
内部实现,未来Java版本可能修改字段名或移除这些字段。
7. 总结
我们探讨了四种获取LinkedHashMap
首尾条目的方法:
方法 | 优点 | 缺点 |
---|---|---|
遍历法 | 直观易懂 | 获取最后一个元素需遍历整个Map |
数组转换法 | 代码简洁,性能稳定 | 需要额外数组空间 |
Stream API法 | 函数式风格,可读性强 | 获取最后一个元素效率较低 |
反射法 | 性能最高(直接访问字段) | 依赖内部实现,Java 9+需额外配置 |
💡 推荐选择:
- 优先使用数组转换法(平衡性能与可读性)
- Java 8环境可考虑Stream API法
- 反射法仅适用于性能敏感且能接受风险的场景
完整示例代码可在GitHub获取。