哈希表逆天神技!手写 CustomHashMap + JDK 源码解析,面试官当场傻眼!

引言

Redis 的超高性能离不开其底层哈希表(HashMap),它是键值存储的核心,提供 O(1) 平均时间复杂度的存取操作。在面试中,“手写一个 HashMap” 是大厂必考题,考察你对哈希函数、冲突处理、动态扩容和源码设计的理解。继前五篇实现基础缓存、键过期、AOF 持久化、LRU 和多种淘汰策略后,本篇带你手写 CustomHashMap,替换 Java 的 HashMap,并深入解析 JDK HashMap 源码,揭开 Redis 哈希表的性能秘密,让你在面试中直接让面试官目瞪口呆!

目标:

  • 实现 CustomHashMap,支持 O(1) 存取、冲突处理和动态扩容。

  • 解析 JDK HashMap 源码,揭示 Redis 哈希表的优化思路。

  • 兼容 TTL、AOF 和淘汰策略,提供可运行代码和测试用例。

  • 提供详细步骤、图表和面试要点,助你深入学习。

适合人群:

  • 想深入理解 Redis 哈希表和 JDK HashMap 源码的开发者。

  • 准备大厂面试、需手写 HashMap 的程序员。

  • 对数据结构、源码分析和性能优化感兴趣的初学者和进阶者。

前置工作:

  • 环境:Java 8+,Maven/Gradle。

  • 依赖:前五篇的 SimpleCacheExpiryCacheAofCacheLruCacheMultiEvictionCache 类(本篇继承)。

  • 代码仓库:https://github.com/codeInbpm/redis-handwritten

  • 建议:运行前五篇代码后,跟着本篇敲代码,体验哈希表的性能魔法!

Redis 哈希表应用场景

在这里插入图片描述

为什么需要自定义 HashMap?

Redis 的键值存储依赖高效的哈希表实现,类似 Java 的 HashMap,提供以下核心特性:

  • O(1) 存取:哈希函数将键映射到数组索引,平均 O(1) 复杂度。

  • 冲突处理:通过链表(或红黑树)解决哈希冲突。

  • 动态扩容:当负载因子超限时,自动扩容保持性能。

  • 内存优化:Redis 定制哈希表,结合渐进式 rehash(第7篇)提升并发性能。

面试常见问题:

  • “如何实现一个高效的 HashMap?哈希函数如何设计?”

  • “HashMap 的冲突处理和扩容机制是什么?”

  • “Redis 的哈希表与 Java HashMap 有何不同?”

  • “JDK HashMap 的源码优化了哪些细节?”

在这里插入图片描述

本篇将实现 CustomHashMap,替换 Java 的 HashMap 用于 HashMapCache,并深入解析 JDK HashMap 源码(Java 8),揭示 Redis 哈希表的底层原理,为第7篇的渐进式 rehash铺路。

CustomHashMap的实现步骤(详细展开)

步骤 1:设计哈希表数据结构

目标:
为 CustomHashMap 设计高效的数据结构,支持键值存取和冲突处理。

详细说明:

  • 使用数组存储桶(buckets),每个桶是一个单向链表,存储键值对。
  • Entry 类:定义节点,包含:
  • key, value:键值对。
  • hash:预计算的哈希值,优化查找。
  • next:指向链表下一节点,处理冲突。

容量和负载因子:

  • 初始容量为 16,负载因子为 0.75。
  • 当元素数量超过 capacity * loadFactor,触发扩容。

兼容性:

  • 设计与 Java HashMap 一致的接口,支持前篇的 TTL、AOF 和淘汰策略。

初始设置:

  • 数组 Entry[] table 初始化为 16。
  • 容量为 2 的幂,优化哈希计算。

要求:

  • 定义 Entry 类,包含 key, value, hash, next。
  • 使用 Entry[] table 存储桶。
  • 支持 put, get, remove, size 方法。
  • 确保容量为 2 的幞,哈希计算使用位运算。

为何如此设计:

  • 数组 + 链表是 HashMap 的经典结构,简单高效,平均 O(1)。
  • 预计算哈希值减少重复计算,提升性能。
  • 负载因子和 2 的幂容量是 JDK HashMap 的优化实践,Redis 也采用类似设计。

CustomHashMap 数据结构
在这里插入图片描述

(画完这个图的我)

步骤 2:实现哈希函数和冲突处理

目标:

  • 设计高效哈希函数,处理键冲突,确保 O(1) 平均存取。

详细说明:
哈希函数:

  • 使用 key.hashCode() 生成初始哈希值。

  • 优化分散性:hash = (h ^ (h >>> 16)) & (capacity - 1),高位参与计算,减少冲突。

  • 容量为 2 的幞,& (capacity - 1) 等价于取模,性能更高。

冲突处理:

  • 同一桶内冲突时,追加节点到链表头部(O(1) 插入)。

  • 查找时遍历链表,比较 hash 和 key.equals()。

操作流程:

  • get(key):计算哈希,定位桶,遍历链表,匹配键后返回 value。

  • put(key, value):计算哈希,定位桶,若键存在则更新 value,否则插入新节点。

  • remove(key):计算哈希,定位桶,移除匹配节点。

  • TTL 兼容:节点存储 expiry,get 时检查过期时间。
    要求:

  • 哈希函数确保均匀分布,减少冲突概率。

  • 链表操作保持 O(1) 插入和删除。

  • 兼容 TTL 检查,过期键返回 null。

  • 异常处理空键或值。
    为何如此设计:

  • 高位异或优化哈希分布,参考 JDK HashMap 的 hash 方法。

  • 链表头部插入减少操作复杂度。

  • TTL 集成保持与前篇一致性。

哈希表操作流程

在这里插入图片描述

步骤 3:实现动态扩容

目标:
实现哈希表动态扩容,保持性能稳定。
详细说明:

扩容触发:

  • 当 size > capacity * loadFactor(如 0.75),触发扩容。

  • 新容量为原容量两倍(如 16 -> 32)。

扩容流程:

  • 创建新数组 newTable,容量翻倍。

  • 遍历旧数组的每个节点,重新计算哈希,分配到新桶。

  • 更新 table 引用,释放旧数组。

优化:

  • 容量为 2 的幞,哈希计算使用 & (capacity - 1)。

  • 链表节点批量转移,减少拷贝开销。

  • TTL 和 AOF:扩容不影响 TTL 检查和 AOF 记录。

  • 异常处理:捕获内存不足异常,确保健壮性。
    要求:

  • 扩容后键值对完整性无损。

  • 尽量减少 rehash 时间开销。

  • 保持容量为 2 的幞。
    为何如此设计:

  • 动态扩容防止链表过长,维持 O(1) 平均复杂度。

  • 2 的幞容量简化哈希计算,参考 JDK 和 Redis 设计。

  • 异常处理提升生产级可靠性。

在这里插入图片描述

步骤 4:整合到缓存系统并解析 JDK 源码

目标:
将 CustomHashMap 整合到 HashMapCache,替换 Java HashMap,并解析 JDK HashMap 源码。
详细说明:
整合到 HashMapCache:

  • HashMapCache 继承 MultiEvictionCache。

  • 替换 Map<String, Node> 为 CustomHashMap<String, Node>。

  • 调整 get, put, removeEvictedNode 方法,使用 CustomHashMap 接口。

  • 兼容 LFU/CLOCK/FIFO、TTL 和 AOF 逻辑。

JDK HashMap 源码解析:

  • 哈希函数:hash = (h ^ (h >>> 16)) 优化分散性。
  • 冲突处理:Java 8 中,链表长度超过 8 时转为红黑树,减少 O(n) 退化。
  • 扩容:容量翻倍,节点重新分配,树化/解树化处理。
  • Redis 对比:Redis 使用自定义哈希表,支持渐进式 rehash(第7篇),内存更紧凑。

操作调整:

  • get:检查 TTL,更新淘汰策略状态(如 LFU 的 accessCount)。
  • put:插入或更新节点,触发扩容时同步更新链表和频率映射。
  • AOF:调用父类的 appendToAof 和 loadAof。

要求:

  • CustomHashMap 接口与 Java HashMap 一致。

  • 保留 LFU/CLOCK/FIFO、TTL 和 AOF 功能。

  • 解析 JDK 源码,提取 Redis 相关优化点。

  • 保持 O(1) 或近似 O(1) 性能。

为何如此设计:

  • 替换 Java HashMap 验证自定义实现的正确性。
  • 源码解析帮助理解 Redis 哈希表的优化思路。
  • 兼容前篇功能,为渐进式 rehash(第7篇)铺路。

HashMapCache 整合与源码解析
在这里插入图片描述
在这里插入图片描述

步骤 5:添加测试用例

目标:

  • 通过 JUnit 测试验证 CustomHashMap 和 HashMapCache 的功能,覆盖存取、冲突、扩容、淘汰、TTL 和 AOF。

详细说明:
测试场景:

  • CustomHashMap:验证 put, get, remove,冲突处理,动态扩容。
  • HashMapCache:验证 LFU/CLOCK/FIFO 淘汰、TTL 过期、AOF 恢复。
  • 异常处理:测试空键/值、无效 TTL。
  • 性能:插入大量键,验证扩容正确性。

实现细节:

  • 使用 @Before 和 @After 清理 AOF 文件和资源。
  • 测试扩容(插入超过容量触发 resize)。
  • 通过 Thread.sleep 测试 TTL 过期。
  • 模拟重启验证 AOF 恢复。

用户学习点:

  • 提供详细测试代码,读者可直接运行。
  • 附带输出解释,展示哈希表和缓存行为。

要求:

  • 测试覆盖所有功能和边界条件。
  • 验证扩容后键值对完整性。
  • 提供清晰断言和错误信息。

为何如此设计:

  • 全面测试确保代码可靠性,增强用户信心。
  • 模拟真实场景(扩容、过期、重启)帮助理解哈希表应用。

在这里插入图片描述

核心代码实现

以下是 CustomHashMapHashMapCache 的完整实现,存放在 CustomHashMap.java 和 HashMapCache.java。

代码 1:CustomHashMap

public class CustomHashMap<K, V> {
    private static class Entry<K, V> {
        K key;
        V value;
        int hash;
        Entry<K, V> next;
        Entry(K key, V value, int hash) {
            this.key = key;
            this.value = value;
            this.hash = hash;
        }
    }

    private Entry<K, V>[] table;
    private int size;
    private int capacity;
    private final float loadFactor;
    private static final int INITIAL_CAPACITY = 16;
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;

    @SuppressWarnings("unchecked")
    public CustomHashMap() {
        this.capacity = INITIAL_CAPACITY;
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        this.table = new Entry[INITIAL_CAPACITY];
    }

    public V get(K key) {
        if (key == null) throw new IllegalArgumentException("Key cannot be null");
        int hash = hash(key);
        int index = hash & (capacity - 1);
        for (Entry<K, V> entry = table[index]; entry != null; entry = entry.next) {
            if (entry.hash == hash && (key.equals(entry.key))) {
                return entry.value;
            }
        }
        return null;
    }

    public void put(K key, V value) {
        if (key == null || value == null) throw new IllegalArgumentException("Key or value cannot be null");
        int hash = hash(key);
        int index = hash & (capacity - 1);

        for (Entry<K, V> entry = table[index]; entry != null; entry = entry.next) {
            if (entry.hash == hash && key.equals(entry.key)) {
                entry.value = value;
                return;
            }
        }

        if (size >= capacity * loadFactor) {
            resize();
            index = hash & (capacity - 1);
        }

        Entry<K, V> newEntry = new Entry<>(key, value, hash);
        newEntry.next = table[index];
        table[index] = newEntry;
        size++;
    }

    public V remove(K key) {
        if (key == null) throw new IllegalArgumentException("Key cannot be null");
        int hash = hash(key);
        int index = hash & (capacity - 1);
        Entry<K, V> prev = null;
        for (Entry<K, V> entry = table[index]; entry != null; entry = entry.next) {
            if (entry.hash == hash && key.equals(entry.key)) {
                if (prev == null) {
                    table[index] = entry.next;
                } else {
                    prev.next = entry.next;
                }
                size--;
                return entry.value;
            }
            prev = entry;
        }
        return null;
    }

    public int size() {
        return size;
    }

    private int hash(K key) {
        int h = key.hashCode();
        return (h ^ (h >>> 16)) & 0x7fffffff; // 优化分散性,确保正数
    }

    @SuppressWarnings("unchecked")
    private void resize() {
        try {
            Entry<K, V>[] oldTable = table;
            capacity *= 2;
            table = new Entry[capacity];
            size = 0;
            for (Entry<K, V> entry : oldTable) {
                while (entry != null) {
                    put(entry.key, entry.value);
                    entry = entry.next;
                }
            }
        } catch (OutOfMemoryError e) {
            throw new RuntimeException("Failed to resize hashmap due to insufficient memory", e);
        }
    }
}

代码 2:HashMapCache

package com.redis.cache;

import java.io.*;
import java.util.*;

public class HashMapCache extends MultiEvictionCache {
    private static class Node {
        String key, value;
        long expiry;
        int accessCount; // LFU
        int referenceBit; // CLOCK
        long insertionOrder; // FIFO
        Node prev, next;
        Node(String key, String value, long expiry) {
            this.key = key;
            this.value = value;
            this.expiry = expiry;
            this.accessCount = 1;
            this.referenceBit = 1;
            this.insertionOrder = insertionCounter++;
        }
    }

    private CustomHashMap<String, Node> cache;
    private Node head, tail;
    private int capacity;
    private EvictionStrategy strategy;
    private TreeMap<Integer, List<Node>> freqMap;
    private Node clockHand;
    private static long insertionCounter;

    public HashMapCache(int capacity, EvictionStrategy strategy) {
        super(capacity, strategy);
        this.capacity = capacity;
        this.strategy = strategy;
        this.cache = new CustomHashMap<>();
        this.freqMap = new TreeMap<>();
        this.head = new Node(null, null, 0);
        this.tail = new Node(null, null, 0);
        head.next = tail;
        tail.prev = head;
        this.clockHand = head;
        loadAof();
    }

    @Override
    public String get(String key) {
        if (key == null) throw new IllegalArgumentException("Key cannot be null");
        Node node = cache.get(key);
        if (node == null || System.currentTimeMillis() > node.expiry) {
            if (node != null) {
                removeNode(node);
                cache.remove(key);
                if (strategy == EvictionStrategy.LFU) updateFreqMap(node, -node.accessCount);
            }
            return null;
        }
        updateNodeAccess(node);
        return node.value;
    }

    @Override
    public void put(String key, String value, long ttlMillis) {
        if (key == null || value == null) throw new IllegalArgumentException("Key or value cannot be null");
        if (ttlMillis <= 0) throw new IllegalArgumentException("TTL must be positive");

        Node node = cache.get(key);
        if (node != null) {
            node.value = value;
            node.expiry = System.currentTimeMillis() + ttlMillis;
            updateNodeAccess(node);
        } else {
            if (cache.size() >= capacity) {
                removeEvictedNode();
            }
            node = new Node(key, value, System.currentTimeMillis() + ttlMillis);
            cache.put(key, node);
            addToHead(node);
            if (strategy == EvictionStrategy.LFU) {
                freqMap.computeIfAbsent(1, k -> new ArrayList<>()).add(node);
            }
        }
        appendToAof("PUT", key, value, ttlMillis);
    }

    private void updateNodeAccess(Node node) {
        if (strategy == EvictionStrategy.LFU) {
            updateFreqMap(node, -node.accessCount);
            node.accessCount++;
            updateFreqMap(node, node.accessCount);
            moveToHead(node);
        } else if (strategy == EvictionStrategy.CLOCK) {
            node.referenceBit = 1;
        }
    }

    private void removeEvictedNode() {
        if (strategy == EvictionStrategy.LFU) {
            Map.Entry<Integer, List<Node>> entry = freqMap.firstEntry();
            if (entry != null) {
                List<Node> nodes = entry.getValue();
                Node node = nodes.remove(nodes.size() - 1);
                if (nodes.isEmpty()) freqMap.remove(entry.getKey());
                removeNode(node);
                cache.remove(node.key);
            }
        } else if (strategy == EvictionStrategy.CLOCK) {
            while (true) {
                clockHand = clockHand.next == tail ? head.next : clockHand.next;
                if (clockHand == head) continue;
                if (System.currentTimeMillis() > clockHand.expiry) {
                    Node toRemove = clockHand;
                    clockHand = clockHand.prev;
                    removeNode(toRemove);
                    cache.remove(toRemove.key);
                    return;
                }
                if (clockHand.referenceBit == 0) {
                    removeNode(clockHand);
                    cache.remove(clockHand.key);
                    return;
                }
                clockHand.referenceBit = 0;
            }
        } else if (strategy == EvictionStrategy.FIFO) {
            Node toRemove = tail.prev;
            if (toRemove != head) {
                removeNode(toRemove);
                cache.remove(toRemove.key);
            }
        }
    }

    private void addToHead(Node node) {
        node.next = head.next;
        node.prev = head;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void moveToHead(Node node) {
        removeNode(node);
        addToHead(node);
    }

    private void updateFreqMap(Node node, int count) {
        if (strategy != EvictionStrategy.LFU) return;
        freqMap.computeIfAbsent(count, k -> new ArrayList<>()).remove(node);
        if (freqMap.get(count) != null && freqMap.get(count).isEmpty()) {
            freqMap.remove(count);
        }
    }
}

测试代码

  • 以下是 HashMapCacheTest.java,验证 CustomHashMap 和 HashMapCache 功能。
package com;
import com.redis.cache.CustomHashMap;
import com.redis.cache.HashMapCache;
import com.redis.cache.MultiEvictionCache;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;

import java.io.File;

public class HashMapCacheTest {
    private HashMapCache cache;
    private File aofFile = new File("appendonly.aof");

    @Before
    public void setUp() {
        if (aofFile.exists()) aofFile.delete();
    }

    @After
    public void tearDown() {
        if (cache != null) cache.shutdown();
        if (aofFile.exists()) aofFile.delete();
    }

    @Test
    public void testCustomHashMapBasic() {
        CustomHashMap<String, String> map = new CustomHashMap<>();
        map.put("key1", "value1");
        map.put("key2", "value2");
        assertEquals("value1", map.get("key1"));
        assertEquals("value2", map.get("key2"));
        assertEquals(2, map.size());
        map.put("key1", "newValue");
        assertEquals("newValue", map.get("key1"));
        map.remove("key2");
        assertNull(map.get("key2"));
        assertEquals(1, map.size());
    }

    @Test
    public void testCustomHashMapResize() {
        CustomHashMap<String, String> map = new CustomHashMap<>();
        for (int i = 0; i < 20; i++) {
            map.put("key" + i, "value" + i);
        }
        assertEquals(20, map.size());
        assertEquals("value10", map.get("key10"));
    }

    @Test
    public void testHashMapCache() throws InterruptedException {
        cache = new HashMapCache(3, MultiEvictionCache.EvictionStrategy.LFU);
        cache.put("key1", "value1", 5000);
        cache.put("key2", "value2", 5000);
        cache.put("key3", "value3", 5000);
        for (int i = 0; i < 5; i++) cache.get("key1");
        cache.put("key4", "value4", 5000); // 移除低频 key2
        assertNull(cache.get("key2"));
        assertEquals("value1", cache.get("key1"));

        cache.put("key5", "value5", 1000);
        Thread.sleep(1500);
        assertNull(cache.get("key5")); // 过期

        cache = new HashMapCache(3, MultiEvictionCache.EvictionStrategy.LFU);
        assertEquals("value1", cache.get("key1")); // AOF 恢复
    }

    @Test(expected = IllegalArgumentException.class)
    public void testInvalidInput() {
        cache = new HashMapCache(3, MultiEvictionCache.EvictionStrategy.LFU);
        cache.put(null, "value", 1000);
    }
}

运行方式:

  • 确保 Maven 项目包含 JUnit 依赖(同前五篇)。

  • 将 SimpleCache.java, ExpiryCache.java, AofCache.java, LruCache.java, MultiEvictionCache.java, CustomHashMap.java, HashMapCache.java 和 HashMapCacheTest.java 放入项目。

  • 运行测试:mvn test 或通过 IDE 执行。

测试输出(示例):

Cache full, removed key: key2

JDK HashMap 源码解析

以下是 JDK 8 HashMap 的关键优化,揭示与 Redis 哈希表的关联:

  • 哈希函数 (java.util.HashMap.hash):
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 高位异或低位,减少冲突,Redis 类似优化哈希分布。

冲突处理 (putVal, getNode):

  • 链表存储冲突节点,长度超过 8 时转为红黑树(TREEIFY_THRESHOLD)。
  • Redis 哈希表使用链表,未采用红黑树,优先内存紧凑。

·动态扩容 (resize):·

  • 容量翻倍,节点重新分配。
  • Redis 引入渐进式 rehash(第7篇),分摊扩容开销。

性能优化:

  • 2 的幂容量,使用 & (n - 1) 替代模运算。
  • 延迟初始化,首次 put 时分配数组。

Redis 对比:

  • Redis 哈希表更轻量,支持渐进式 rehash和高并发。
  • JDK HashMap 通用性强,功能丰富(如红黑树)。

JDK HashMap 源码解析
在这里插入图片描述

面试要点

问:如何实现一个高效的 HashMap?

  • 答:使用数组 + 链表,哈希函数映射键到桶,链表处理冲突。容量为 2 的幂,位运算优化哈希。动态扩容保持负载因子(如 0.75)。本实现参考 JDK 哈希函数和扩容机制。

问:HashMap 的冲突处理如何优化?

  • 答:链表适合低冲突场景,高冲突可转为红黑树(如 JDK 8,链表长度 > 8)。本实现使用链表,简单高效,Redis 类似优先内存紧凑。

问:动态扩容的性能影响?

  • 答:一次性 rehash 可能导致延迟,Redis 使用渐进式 rehash(第7篇)分摊开销。本实现一次性扩容,适合学习。

问:Redis 哈希表与 JDK HashMap 的区别?

  • 答:Redis 哈希表轻量,支持渐进式 rehash,优化内存和并发。JDK HashMap 通用性强,引入红黑树。本实现是简化版,接近 JDK 设计。
    在这里插入图片描述

下一步预告

恭喜你掌握 Redis 的哈希表核心!下一篇文章,我们将实现 渐进式 rehash,让哈希表扩容如丝般顺滑!” 敬请期待!

  • 完整代码:https://github.com/codeInbpm/redis-handwritten
    互动:

运行代码了吗?有问题?欢迎在评论区留言!

觉得本篇炸裂吗?一键三连,关注系列,下一期更精彩!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wáng bēn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值