手写一个HashMap

前言

HashMap是我们常见的集合类型,实现了Map接口,JDK1.8之前的底层结构为数据+链表实现,JDK1.8对其进行了优化,在原有的数组+链表的基础上增加了红黑树的结构,提升了数据的访问性能。接下来我们通过手写一个简单的HashMap来了解其实现原理。

自定义接口AndyMap,包含get(),put(),size()三个方法,对应与JDK中的Map的相应方法。同时定义内部接口Entry,包含用于获取Key和Value的方法。

public interface AndyMap<K, V> {
   
    V get(K k);

    V put(K k, V v);

    int size();

    interface Entry<K, V> {
        K getKey();

        V getValue();
    }
}

定义AndyHashMap,实现AndyMap类,定义默认容量为16,(这里用位运算进行了操作,提升性能)。定义默认负载因子为0.75,当数组中的元素达到容量的75%时,需要进行扩容处理。如下所示:

public class AndyHashMap<K, V> implements AndyMap<K, V> {

    //默认容量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //默认负载因子,阈值比例
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //容量
    private int initialCapacity;

    //负载因子
    private float loadFactor;

    //元素表
    private Node<K, V>[] table;
    //元素数量
    private int size;

    public AndyHashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    public AndyHashMap(int defaultInitialCapacity, float defaultLoadFactor) {
        if (defaultInitialCapacity < 0) {
            throw new IllegalArgumentException("Illegal initial capacity: " +
                    defaultInitialCapacity);
        }
        if (defaultLoadFactor <= 0 || Float.isNaN(defaultLoadFactor)) {
            throw new IllegalArgumentException("Illegal initial load factor: " +
                    defaultLoadFactor);
        }
        this.initialCapacity = defaultInitialCapacity;
        this.loadFactor = defaultLoadFactor;
        table = new Node[initialCapacity];

    }
//剩余代码未贴出
.....

}

上面的代码中我们可以看到构造函数这里采用了外观模式,对外暴露了2个构造参数,实则只有一个。

接下来我们看一下Entry的结构定义,Entry中包含4个主要元素,哈希值hash,key,value以及下一个节点的元素,链表的结构就是通过这里来实现的。

   /**
     * Entry结构
     *
     * @param <K>
     * @param <V>
     */
    class Node<K, V> implements AndyMap.Entry<K, V> {
        //key的哈希值
        private int hash;
        //key
        private K key;
        //value
        private V value;
        //下一个节点
        private Node<K, V> next;

        public Node(int hash, K key, V value, Node<K, V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        @Override
        public K getKey() {
            return key;
        }

        @Override
        public V getValue() {
            return value;
        }
    }

Hash算法,哈希算法需要尽可能的减少hash碰撞,使元素散列均匀,防止链表过长造成查询性能下降。此处的hash算法是在获取到key的hashCode后对其的高16位和低16位进行了异或操作之后再返回。

    /**
     * 获取hash值
     *
     * @param key
     * @return
     */
    private int hash(K key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

接下来我们看一下map中最常用的两个方法,put和get的实现。put方法传入key和value,返回旧的value,如果是新的key,则返回null。如下所示

/**
     * put
     *
     * @param k
     * @param v
     * @return
     */
    @Override
    public V put(K k, V v) {
        V oldValue = null;
        if (size >= initialCapacity * loadFactor) {
            //扩容
            resize();
        }
        int hashCode = hash(k);
        int index = hashCode & (initialCapacity - 1);
        if (table[index] == null) {
            table[index] = new Node<>(hashCode, k, v, null);
            size++;
        } else {
            Node<K, V> node = table[index];
            Node<K, V> nextNode = node;
            while (nextNode != null) {
                if (nextNode.hash == hashCode && (k == nextNode.getKey() || k.equals(nextNode.getKey()))) {
                    oldValue = nextNode.getValue();
                    nextNode.value = v;
                    return oldValue;
                }
                nextNode = nextNode.next;
            }
            table[index].next = new Node<>(hashCode, k, v, nextNode);
            size++;
        }
        return oldValue;
    }

在上述代码中我们可以看到,put时首先要进行扩容判断,当元素值达到阈值的时候要对数组进行扩容操作,之所以在插入数据前判断扩容是因为扩容操作比较耗时,如果放在插入后再判断扩容,当最后一次put时候正好达到了扩容条件,由于后续没有put操作,造成本次扩容浪费。

扩容完成后获取key在数组中的下标,这里通过hash值和容量进了取模运算,用来得到0到initialCapacity -1的一个数。

当数组对应位置为null时,对数组进行赋值后元素数加1。

当数组对应位置不为空时,需要对table[index]对应的链表进行遍历操作,分两种情况:

  • 当key存在的时,新值覆盖旧值之后然后返回旧值。
  • 当key不存在的时候,将新的Node对象插入到链表尾部,元素数加1。

扩容和reHash:

每次扩容都是在原来的容量基础上进行double扩容。扩容后要在新的数组上对原来的值重新hash,重新分配存储位置。本例中的重新hash仅是做了重新put操作。

     /**
     * 扩容
     *
     */
    private void resize() {
        int newSize = initialCapacity << 1;
        Node<K, V>[] newTable = new Node[newSize];
        initialCapacity = newSize;
        reHash(newTable);
    }

    /**
     * 重新hash分配存储位置
     *
     * @param newTable
     */
    private void reHash(Node<K, V>[] newTable) {
        List<Node<K, V>> entryList = new ArrayList<>();
        for (Node<K, V> node : table) {
            if (node != null) {
                entryList.add(node);
                while (node.next != null) {
                    entryList.add(node.next);
                    node = node.next;
                }
            }
        }
        table = newTable;
        size = 0;
        for (Node<K, V> node : entryList) {
            put(node.getKey(), node.getValue());
        }
    }

get操作:

get的操作相对简单,根据hash值获取数组下标,然后遍历链表获取对应的值即可。

    /**
     * get
     *
     * @param k
     * @return
     */
    @Override
    public V get(K k) {
        int hashCode = hash(k);
        int index = hash(k) & (initialCapacity - 1);
        if (table[index] == null) {
            return null;
        } else {
            Node<K, V> node = table[index];
            while (node != null) {
                if (node.hash == hashCode && (k == node.getKey() || k.equals(node.getKey()))) {
                    return node.value;
                } else {
                    node = node.next;
                }
            }
        }
        return null;
    }

最后还有一个size方法,返回map中的元素数量即可。size在put时候会进行计算。

    /**
     * 元素数量
     *
     * @return
     */
    @Override
    public int size() {
        return size;
    }

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值