首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java HashMap 反向查找:通過 Value 獲取所有 Key 的高效實現方案

     編輯:Java 程式設計入門

問題背景

HashMap 設計為 Key → Value 單向映射,原生不支持反向查找。業務場景常需「根據值找鍵」:如權限碼反查角色、配置值反查鍵名、數據去重反查原始鍵等。

方案對比

方案時間複雜度空間複雜度適用場景優點缺點
遍歷 entrySetO(n)O(1)臨時查詢、數據量小無額外內存、代碼簡單頻繁查詢性能差
維護雙向 MapO(1)O(n)高頻反向查詢、數據穩定查詢極快插入/刪除需同步維護、雙倍內存
Guava BiMapO(1)O(n)值唯一、需強一致性框架級實現、自動同步值必須唯一、不支持一值多鍵
值→鍵集合 MapO(1)O(n)一值多鍵、高頻查詢支持重複值、查詢快維護成本高

方案一:遍歷 entrySet (通用、零依賴)


import java.util.*;

public class ReverseLookup {
    
    /**
     * 獲取指定值對應的所有鍵
     * @return 空列表表示無匹配,不返回 null
     */
    public static  List getKeysByValue(Map map, V targetValue) {
        if (map == null || map.isEmpty()) return Collections.emptyList();
        
        List keys = new ArrayList<>();
        for (Map.Entry entry : map.entrySet()) {
            // null 安全比較
            if (Objects.equals(entry.getValue(), targetValue)) {
                keys.add(entry.getKey());
            }
        }
        return keys;
    }
    
    /**
     * 獲取第一個匹配的鍵 (效能略優)
     */
    public static  K getFirstKeyByValue(Map map, V targetValue) {
        for (Map.Entry entry : map.entrySet()) {
            if (Objects.equals(entry.getValue(), targetValue)) {
                return entry.getKey();
            }
        }
        return null;
    }
    
    // 測試
    public static void main(String[] args) {
        Map map = new HashMap<>();
        map.put(1, "A");
        map.put(2, "A");
        map.put(3, "B");
        map.put(4, "A");
        map.put(5, "C");
        
        System.out.println("值 'A' 對應的鍵: " + getKeysByValue(map, "A"));  // [1, 2, 4]
        System.out.println("值 'B' 對應的鍵: " + getKeysByValue(map, "B"));  // [3]
        System.out.println("值 'D' 對應的鍵: " + getKeysByValue(map, "D"));  // []
        System.out.println("首個 'A' 的鍵: " + getFirstKeyByValue(map, "A")); // 1
    }
}

方案二:流式 API (Java 8+ 簡潔寫法)


public static  List getKeysByValueStream(Map map, V value) {
    return map.entrySet().stream()
        .filter(entry -> Objects.equals(entry.getValue(), value))
        .map(Map.Entry::getKey)
        .collect(Collectors.toList());
}

// 並行流 (大數據量)
public static  List getKeysByValueParallel(Map map, V value) {
    return map.entrySet().parallelStream()
        .filter(entry -> Objects.equals(entry.getValue(), value))
        .map(Map.Entry::getKey)
        .collect(Collectors.toList());
}

方案三:預建反向索引 Map (高頻查詢最優)


import java.util.*;

public class BidirectionalMap {
    private final Map forward = new HashMap<>();
    private final Map> reverse = new HashMap<>();
    
    // 線程安全版本可用 ConcurrentHashMap + ConcurrentSkipListSet
    
    public V put(K key, V value) {
        // 1. 移除舊映射的反向索引
        V oldValue = forward.get(key);
        if (oldValue != null) {
            Set oldKeys = reverse.get(oldValue);
            if (oldKeys != null) {
                oldKeys.remove(key);
                if (oldKeys.isEmpty()) reverse.remove(oldValue);
            }
        }
        
        // 2. 建立新映射
        forward.put(key, value);
        reverse.computeIfAbsent(value, k -> new HashSet<>()).add(key);
        
        return oldValue;
    }
    
    public V get(K key) {
        return forward.get(key);
    }
    
    public Set getKeys(V value) {
        return reverse.getOrDefault(value, Collections.emptySet());
    }
    
    public V remove(K key) {
        V value = forward.remove(key);
        if (value != null) {
            Set keys = reverse.get(value);
            if (keys != null) {
                keys.remove(key);
                if (keys.isEmpty()) reverse.remove(value);
            }
        }
        return value;
    }
    
    public boolean containsKey(K key) { return forward.containsKey(key); }
    public boolean containsValue(V value) { return reverse.containsKey(value); }
    public int size() { return forward.size(); }
    public void clear() { forward.clear(); reverse.clear(); }
    
    // 測試
    public static void main(String[] args) {
        BidirectionalMap bimap = new BidirectionalMap<>();
        bimap.put(1, "A");
        bimap.put(2, "A");
        bimap.put(3, "B");
        
        System.out.println("反向查找 'A': " + bimap.getKeys("A"));  // [1, 2]
        bimap.put(2, "C");  // 更新 key=2 的值
        System.out.println("更新後 'A': " + bimap.getKeys("A"));   // [1]
        System.out.println("新值 'C': " + bimap.getKeys("C"));     // [2]
    }
}

方案四:Guava BiMap (值唯一場景)


// 依賴: com.google.guava:guava:32.1.3-jre
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;

BiMap biMap = HashBiMap.create();
biMap.put(1, "A");
biMap.put(2, "B");
// biMap.put(3, "A"); // 拋出 IllegalArgumentException: value already present

// 反向視圖 (O(1) 查詢)
BiMap inverse = biMap.inverse();
System.out.println(inverse.get("A"));  // 1

// 強制覆蓋值
biMap.forcePut(3, "A");  // 會移除 key=1 的映射

方案五:Apache Commons Collections BidiMap


// 依賴: org.apache.commons:commons-collections4:4.4
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;

BidiMap bidiMap = new DualHashBidiMap<>();
bidiMap.put(1, "A");
bidiMap.put(2, "B");
System.out.println(bidiMap.getKey("A"));  // 1 (單值唯一)

性能基準測試 (JMH 參考)


@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class ReverseLookupBenchmark {
    private Map map;
    private BidirectionalMap biMap;
    
    @Setup
    public void setup() {
        map = new HashMap<>();
        biMap = new BidirectionalMap<>();
        for (int i = 0; i < 10000; i++) {
            String val = "VAL_" + (i % 100);  // 100 個不同值,每值 100 個鍵
            map.put(i, val);
            biMap.put(i, val);
        }
    }
    
    @Benchmark
    public List traversalLookup() {
        return ReverseLookup.getKeysByValue(map, "VAL_50");
    }
    
    @Benchmark
    public Set indexLookup() {
        return biMap.getKeys("VAL_50");
    }
}

/*
典型結果 (10000 條,查找 100 個鍵):
traversalLookup    ~1,200,000 ns/op  (1.2 ms)
indexLookup        ~   50,000 ns/op  (0.05 ms)  → 快 24 倍
*/

避坑指南

❌ 直接用 values() 判斷包含


// 低效:values() 返回 Collection,contains 也是 O(n) 遍歷
if (map.values().contains("A")) { ... }

// 正確:entrySet 遍歷或預建索引

❌ 並發環境下直接遍歷


// 危險:ConcurrentModificationException 或讀到中間狀態
// 正確:ConcurrentHashMap + 段鎖,或複製快照遍歷
Map snapshot = new HashMap<>(concurrentMap);
List keys = getKeysByValue(snapshot, value);

❌ 值為可變對象導致查找失效


// 危險:List 作為 Value,修改內容後 hashCode/equals 變化
Map> map = new HashMap<>();
List list = new ArrayList<>(Arrays.asList("a"));
map.put("key1", list);
list.add("b");  // 值對象內容變了,但 Map 結構沒變

// 查找時需注意:Objects.equals 比較的是當前內容
// 若需按引用查找,用 == 或 IdentityHashMap

❌ 內存洩漏:反向索引未及時清理


// 長期運行服務中,定期清理空集合
if (keys.isEmpty()) reverse.remove(value);
// 或使用 WeakHashMap (僅 Key 弱引用,Value 強引用不適用)
// 自定義 ReferenceQueue 清理機制

最佳實踐總結

  1. 臨時/低頻查詢:直接遍歷 entrySet(),配合 Objects.equals 空安全
  2. 高頻查詢、值可重複:封裝 BidirectionalMap 維護反向索引 Map>
  3. 值唯一、需強一致性:使用 Guava BiMap,框架級可靠
  4. 並發環境:ConcurrentHashMap + ConcurrentSkipListSet 或 CopyOnWriteArrayList
  5. 超大數據:考慮數據庫反向索引、Elasticsearch、Redis 反向映射

一行代碼速查手冊


// Java 8+ 單行獲取所有鍵
List keys = map.entrySet().stream()
    .filter(e -> Objects.equals(e.getValue(), target))
    .map(Map.Entry::getKey)
    .toList();  // Java 16+ / Collectors.toList()

// 僅判斷是否存在
boolean exists = map.containsValue(target);  // O(n) 遍歷

// 獲取任意一個鍵
K anyKey = map.entrySet().stream()
    .filter(e -> Objects.equals(e.getValue(), target))
    .map(Map.Entry::getKey)
    .findAny().orElse(null);
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved