1. Overview
1.概述
In Java, LinkedHashMap is a powerful tool for maintaining key-value pairs while preserving the order of insertion. One common requirement is to access the first or last entry in a LinkedHashMap.
在 Java 中,LinkedHashMap是一种功能强大的工具,可在保持插入顺序的同时维护键值对。一个常见的需求是访问 LinkedHashMap 中的第一个或最后一个条目。
In this tutorial, we’ll explore various approaches to achieving this.
在本教程中,我们将探讨实现这一目标的各种方法。
2. Preparing a LinkedHashMap Example
2.准备 LinkedHashMap 示例
Before diving into the implementation for accessing the first and last entries in a LinkedHashMap, let’s briefly review the characteristics of LinkedHashMap.
在深入了解访问 LinkedHashMap 中第一个和最后一个条目的实现之前,让我们简要回顾一下 LinkedHashMap 的特性。
First, LinkedHashMap is a part of the Java Collections Framework and extends the HashMap class. Furthermore, unlike a regular HashMap, a LinkedHashMap maintains the order of elements in the order in which they were inserted.
首先,LinkedHashMap 是 Java 集合框架的一部分,并扩展了 HashMap 类。此外,与常规的 HashMap 不同,LinkedHashMap 保持元素的插入顺序。
Depending on the constructor we use, this order can be either the insertion order or the access order. Simply put, the insertion order means that the elements are ordered according to when they’re added to the LinkedHashMap, while the access order implies that the elements are ordered by the frequency of access, with the most recently accessed elements appearing last.
根据我们使用的构造函数,该顺序可以是插入顺序或访问顺序。简单地说,插入顺序是指根据元素被添加到 LinkedHashMap 的时间排序,而访问顺序是指根据元素被访问的频率排序,最近被访问的元素出现在最后。
In this tutorial, we’ll take an insertion-order LinkedHashMap as an example to demonstrate how to obtain the first and the last entries. But, the solutions work for access-order LinkedHashMaps too.
在本教程中,我们将以插入序列的 LinkedHashMap 为例,演示如何获取第一个和最后一个条目。但是,这些解决方案也适用于访问阶 LinkedHashMaps。
So, next, let’s prepare a LinkedHashMap object example:
接下来,让我们准备一个 LinkedHashMap 对象示例:
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");
}
As the example above shows, we used a static block to initialize the THE_MAP object.
如上面的示例所示,我们使用 静态块来初始化THE_MAP对象。
For simplicity, in this tutorial, we assume the LinkedHashMap object isn’t null or empty. Also, we’ll leverage unit test assertions to verify whether each approach produces the expected result.
为简单起见,在本教程中,我们假设 LinkedHashMap 对象不是空的或空的。此外,我们还将利用单元测试断言来验证每种方法是否产生预期结果。
3. Iterating Through Map Entries
3.遍历地图条目
We know that Map‘s entrySet() method returns all entries in a Set backed by the map. Furthermore, for a LinkedHashMap, the entries in the returned Set follow the entries order in the map object.
我们知道,Map 的entrySet()方法会返回由该 map 支持的Set中的所有条目。此外,对于 LinkedHashMap 而言,返回的 Set 中的条目将遵循 map 对象中的条目顺序。
Therefore, we can easily access any entry in a LinkedHashMap by iterating entrySet()‘s result through an Iterator. For example, linkedHashMap.entrySet().iterator().next() returns the first element in the map:
因此,我们可以通过 迭代器迭代 entrySet() 的结果,轻松访问 LinkedHashMap 中的任何条目。例如,linkedHashMap.entrySet().iterator().next() 返回映射中的第一个元素:
Entry<String, String> firstEntry = THE_MAP.entrySet().iterator().next();
assertEquals("key one", firstEntry.getKey());
assertEquals("a1 b1 c1", firstEntry.getValue());
However, obtaining the last entry isn’t as simple as how we retrieve the first entry. We must iterate to the last element in the Set to get the last map entry:
但是,获取最后一个条目并不像获取第一个条目那么简单。我们必须遍历 Set 中的最后一个元素,才能获得最后一个地图条目:
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());
In this example, we iterate through the LinkedHashMap using an Iterator object and keep updating the lastEntry variable until we reach the last entry.
在此示例中,我们使用 Iterator 对象遍历 LinkedHashMap 并不断更新 lastEntry 变量,直至到达最后一个条目。
Our examples show this approach does the job for an insertion-order LinkedHashMaps. Some may ask if this approach works for access-order LinkedHashMap, too, since access entries in access-order LinkedHashMap may change their order.
我们的示例表明,这种方法适用于插入阶 LinkedHashMap 。有人可能会问,这种方法是否也适用于访问阶 LinkedHashMap ,因为访问阶 LinkedHashMap 中的访问条目可能会改变顺序。
Therefore, it’s worth noting that iterating over a view of the map won’t affect the order of iteration of the backing map. This is because only explicit access operations on the map will affect the order, such as map.get(key). So, this approach works for access-order LinkedHashMap as well.
因此,值得注意的是,对地图视图的迭代不会影响后备地图的迭代顺序。这是因为 只有对地图的显式访问操作才会影响顺序,例如 map.get(key)。因此,这种方法也适用于访问顺序LinkedHashMap。
4. Converting Map Entries to an Array
4.将 Map 条目转换为数组
We know that arrays are pretty performant for random access. So, if we can convert the LinkedHashMap entries to an array, we can efficiently access the array’s first and last elements of the array.
我们知道,数组对于随机存取具有相当高的性能。因此,如果我们能将 LinkedHashMap 条目转换为数组,就能高效地访问数组的第一个和最后一个元素。
We’ve learned we can get all map entries in a Set by calling entrySet(). Thus, Java Collection‘s toArray() method helps us to obtain an array from the Set:
我们已经了解到,通过调用 entrySet() 可以获取 Set 中的所有地图条目。因此,Java Collection 的 toArray() 方法可以帮助我们从 Set 中获取数组:
Entry<String, String>[] theArray = new Entry[THE_MAP.size()];
THE_MAP.entrySet().toArray(theArray);
Then, accessing the first and the last elements from the array isn’t a challenge for us:
这样,访问数组中的第一个和最后一个元素对我们来说就不是难题了:
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());
5. Using Stream API
5.使用流应用程序接口
Stream API has been introduced since Java 8. It provides a series of convenient methods that allow us to handle collections easily.
Stream API 自 Java 8 开始引入。它提供了一系列方便的方法,让我们可以轻松处理集合。
Next, let’s see how to get the first entry from a LinkedHashMap using Java Streams:
接下来,让我们看看如何使用 Java Streams 从 LinkedHashMap 中获取第一个条目:
Entry<String, String> firstEntry = THE_MAP.entrySet().stream().findFirst().get();
assertEquals("key one", firstEntry.getKey());
assertEquals("a1 b1 c1", firstEntry.getValue());
As we can see, we called the stream() method on the entrySet() result to get the map entries’ stream object. Then, findFirst() gives us the first element from the stream. It’s worth noting that the findFirst() method returns an Optional object. Since we knew the map wouldn’t be empty, we called the get() method directly to retrieve the map entry from the Optional object.
我们可以看到,我们在 entrySet() 结果上调用了 stream() 方法,以获取地图条目的流对象。然后,findFirst() 将为我们提供流中的第一个元素。值得注意的是,findFirst() 方法返回一个 Optional 对象。由于我们知道 map 不会是空的,因此我们直接调用 get() 方法,从 Optional 对象中检索 map 条目。
There are various ways to get the last element from a Stream instance. For example, we can use the skip() function to solve the problem:
有多种方法可以从 Stream 实例中获取最后一个元素。例如,我们可以使用 skip() 函数来解决这个问题:
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());
The skip() method accepts an int parameter. As its name implies, skip(n) returns a stream discarding the first n elements in the original stream. Therefore, we passed THE_MAP.size() – 1 to the skip() method to obtain a stream only containing the last element, which is THE_MAP‘s last entry.
skip() 方法接受一个 int 参数。顾名思义, skip(n) 返回的流将丢弃原始流中的前n个元素。 因此,我们向 skip() 方法传递了THE_MAP.size() – 1 以 获得一个只包含最后一个元素的流,也就是 THE_MAP 的最后一个条目。
6. Using Reflection API
6.使用反射 API
Until now (Java 21), LinkedHashMap‘s implementation maintains a doubly linked list to hold key-value entries. Also, the head and the tail variables reference the map’s first and last entries:
直到现在(Java 21),LinkedHashMap 的实现都会维护一个 双重链表 来保存键值条目。此外, head 和 tail 变量引用了映射的第一个和最后一个条目:
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
Therefore, we can simply read these two variables to get the required entries. However, head and tail aren’t public variables. So, we cannot directly access them outside the java.util package.
因此,我们只需读取这两个变量,就能获得所需的条目。但是,head 和 tail 并不是公有变量。因此,我们无法在 java.util 包之外直接访问它们。
Fortunately, we have a powerful weapon: Java Reflection API, which allows us to read or write to runtime attributes of classes. For example, we can read the head and the tail fields using reflection to get the map’s first and last entries:
幸运的是,我们有一个强大的武器:Java Reflection API,它允许我们读取或写入类的运行时属性。例如,我们可以使用反射读取 head 和 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());
We should note that when we run the test above with Java version 9 or later, the test fails with the following error message:
我们应该注意到,当我们使用 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 ....
This is because, since version 9, Java has introduced the modular system, which limits the Reflection API reasonably.
这是因为,从版本 9 开始,Java 引入了模块化系统,它合理地限制了 Reflection API。
To resolve the reflection illegal access problem, we can add “–add-opens java.base/java.util=ALL-UNNAMED” to the Java command line. As we use Apache Maven as the build tool, we can add this option to the surefire-plugin configuration to ensure the test using reflection runs smoothly with “mvn test“:
要解决反射非法访问问题,我们可以在 Java 命令行中添加”-add-opens java.base/java.util=ALL-UNNAMED“。由于我们使用 Apache Maven 作为构建工具,因此可以在surefire-plugin配置中添加此选项,以确保使用反射的测试能通过”mvn test“顺利进行:
<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>
Additionally, it’s essential to remember that this method hinges on the presence of the “head” and “tail” fields within the LinkedHashMap implementation. In case of potential alterations in Java’s future releases, such as removing or renaming these fields, this solution may no longer be effective.
此外,请务必记住,此方法取决于 LinkedHashMap 实现中是否存在”head“和”tail“字段。如果 Java 的未来版本可能发生变化,例如删除或重命名这些字段,那么此解决方案可能不再有效。
7. Conclusion
7.结论
In this article, we initiated our exploration by briefly understanding the key features of a LinkedHashMap. Then, we delved into practical examples to illustrate various methods for retrieving the first and last key-value pairs from a LinkedHashMap.
在本文中,我们首先简要了解了 LinkedHashMap 的主要特征,然后深入研究了实际示例,以说明从 LinkedHashMap 中检索第一个和最后一个键值对的各种方法。
As always, the complete source code for the examples is available over on GitHub.
与往常一样,这些示例的完整源代码可在 GitHub 上获取。