如何在 Java 中從 LinkedHashMap 取得第一個或最後一個條目
1. 概述
在Java中, LinkedHashMap
是一個強大的工具,用於維護鍵值對,同時保留插入順序。一個常見的要求是存取LinkedHashMap.
在本教程中,我們將探索實現這一目標的各種方法。
2. 準備LinkedHashMap
範例
在深入研究訪問LinkedHashMap,
我們先簡單回顧一下LinkedHashMap.
首先, LinkedHashMap
是 Java Collections Framework 的一部分,擴充了HashMap
類別。此外,與常規HashMap,
LinkedHashMap
依照元素插入的順序維護元素的順序。
根據我們使用的建構函數,此順序可以是插入順序或存取順序。簡單來說,插入順序是指元素依照新增至LinkedHashMap,
而存取順序是指元素依照存取頻率排序,最近造訪的元素出現在最後。
在本教程中,我們將以插入順序的LinkedHashMap
為例來示範如何取得第一個和最後一個條目。但是,這些解決方案也適用於存取順序LinkedHashMap
。
那麼,接下來,讓我們準備一個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");
}
如上面的範例所示,我們使用靜態區塊來初始化THE_MAP
物件。
為簡單起見,在本教程中,我們假設LinkedHashMap
物件不為 null 或為空。此外,我們將利用單元測試斷言來驗證每種方法是否產生預期結果。
3. 迭代映射條目
我們知道Map
的entrySet()
方法傳回由映射支援的Set
中的所有條目。此外,對於LinkedHashMap,
傳回的Set
中的條目遵循映射物件中的條目順序。
因此,我們可以透過迭代器迭代entrySet()
的結果來輕鬆存取LinkedHashMap
中的任何條目Iterator.
例如, linkedHashMap.entrySet().iterator().next()
傳回map中的第一個元素:
Entry<String, String> firstEntry = THE_MAP.entrySet().iterator().next();
assertEquals("key one", firstEntry.getKey());
assertEquals("a1 b1 c1", firstEntry.getValue());
然而,取得最後一個條目並不像我們檢索第一個條目那麼簡單。我們必須迭代到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());
在此範例中,我們使用Iterator
物件迭代LinkedHashMap
並不斷更新lastEntry
變量,直到到達最後一個條目。
我們的範例顯示這種方法可以完成插入順序LinkedHashMaps.
有人可能會問這種方法是否也適用於存取順序LinkedHashMap,
,因為存取順序LinkedHashMap
中的存取條目可能會改變其順序。
因此,值得注意的是,迭代地圖視圖不會影響支援地圖的迭代順序。這是因為**只有對地圖的明確存取操作才會影響順序,例如map.get(key).
**因此,這種方法也適用於存取順序LinkedHashMap
。
4. 將Map
條目轉換為陣列
我們知道數組對於隨機存取來說性能非常好。因此,如果我們可以將LinkedHashMap
條目轉換為數組,我們就可以有效地存取數組的第一個和最後一個元素。
我們了解到,我們可以透過呼叫entrySet() 來取得Set
中的所有映射項目entrySet().
因此,Java Collection
的toArray()
方法幫助我們從Set
中取得陣列:
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());
5. 使用串流API
Stream API從Java 8開始就被引入了。它提供了一系列方便的方法,讓我們可以輕鬆處理集合。
接下來,讓我們看看如何使用 Java Stream
從LinkedHashMap
取得第一個條目:
Entry<String, String> firstEntry = THE_MAP.entrySet().stream().findFirst().get();
assertEquals("key one", firstEntry.getKey());
assertEquals("a1 b1 c1", firstEntry.getValue());
如我們所看到的,我們在entrySet()
結果上呼叫了stream()
方法來取得映射條目的流物件。然後, findFirst()
為我們提供流中的第一個元素。值得注意的是, findFirst()
方法傳回一個Optional
物件。由於我們知道映射不會為空,因此我們直接呼叫get()
方法來從Optional
物件中檢索映射條目。
有多種方法可以從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());
skip()
方法接受一個int
參數。顧名思義, skip(n)
傳回一個流,丟棄原始流中的前n
個元素。因此,我們將THE_MAP.size() – 1
傳遞給skip()
方法來取得僅包含最後一個元素的流,即THE_MAP
的最後一個條目。
6.使用反射API
到目前為止(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;
因此,我們可以簡單地讀取這兩個變數來獲取所需的條目。但是, head
和tail
不是公共變數。因此,我們無法在java.util
套件之外直接存取它們。
幸運的是,我們有一個強大的武器: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());
需要注意的是,當我們使用 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 ....
這是因為,從版本9開始,Java引入了模組化系統,這對Reflection API進行了合理的限制。
為了解決反射非法存取問題,我們可以在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>
此外,必須記住,此方法取決於LinkedHashMap
實作中「 head
」和「 tail
」欄位的存在。如果 Java 的未來版本可能發生更改,例如刪除或重命名這些字段,則此解決方案可能不再有效。
七、結論
在本文中,我們透過簡要了解LinkedHashMap.
然後,我們深入研究實際範例來說明從LinkedHashMap.
與往常一樣,範例的完整原始程式碼可在 GitHub 上取得。