HashMap

数组

数组存储区间是连续的,进行插入和删除操作时,平均要移动数组中近一半的元素,时间复杂度为O(N)。但数组的查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;

链表

链表存储区间离散,插入和删除操作无需移动元素,只需修改指针,但查找时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。

哈希表

那么我们能不能综合两者的特性,做出一种寻址容易,插入和删除也容易的数据结构呢?答案是肯定的,这就是我们要提起的哈希表。事实上,哈希表有多种不同的实现方法,我们接下来解释的是最经典的一种方法 —— 拉链法,我们可以将其理解为 链表的数组,如下图所示:

1.1 HashMap 概述

  Map 是 Key-Value 对映射的抽象接口,该映射不包括重复的键,即一个键对应一个值。 简单地说,HashMap 是基于哈希表的 Map 接口的实现,以 Key-Value 的形式存在,即存储的对象是 Entry (同时包含了 Key 和 Value) 。在HashMap中,根据hash算法来计算key-value的存储位置并进行快速存取。特别地,HashMap最多只允许一条Entry的键为Null(多条会覆盖),但允许多条Entry的值为Null。此外,HashMap 是 Map 的一个非同步的实现。

1.2 解决hash冲突

HashMap使用链地址法来解决hash冲突,即数组+链表的组合,JDK1.8之后才引入了红黑树进行存储优化。
每个数组元素上都是一个链表结构,当数据被hash后得到数组的下标,把数据存储到对应下标的链表上。
数组的每个元素都是指向各个链表的头节点

HashMap的数据结构

首先HashMap里面实现一个静态内部类Entry,其重要的属性有 key , value, next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。
  /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    transient Entry[] table;


static class Entry<K,V> implements Map.Entry<K,V> {

    final K key;     // 键值对的键
    V value;        // 键值对的值
    Entry<K,V> next;    // 下一个节点
    final int hash;     // 用于定位数组索引位置

    /**
     * Creates new entry.
     */
    Entry(int h, K k, V v, Entry<K,V> n) {     // Entry 的构造函数
        value = v;
        next = n;
        key = k;
        hash = h;
    }

    ......
}

HashMap 的构造函数

该构造函数意在构造一个具有> 默认初始容量 (16) 和 默认负载因子(0.75) 的空 HashMap,是 Java Collection Framework 规范推荐提供的,其源码如下:

 /**
     * Constructs an empty HashMap with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {

        //负载因子:用于衡量的是一个散列表的空间的使用程度
        this.loadFactor = DEFAULT_LOAD_FACTOR; 

        //HashMap进行扩容的阈值,它的值等于 HashMap 的容量乘以负载因子
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);

        // HashMap的底层实现仍是数组,只是数组的每一项都是一条链表
        table = new Entry[DEFAULT_INITIAL_CAPACITY];

        init();
    }

HashMap 的存储实现

在 HashMap 中,键值对的存储是通过 put(key,vlaue) 方法来实现的,其源码如下:

  /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with key, or null if there was no mapping for key.
     *  Note that a null return can also indicate that the map previously associated null with key.
     */
    public V put(K key, V value) {

        //当key为null时,调用putForNullKey方法,并将该键值对保存到table的第一个位置 
        if (key == null)
            return putForNullKey(value); 

        //根据key的hashCode计算hash值
        int hash = hash(key.hashCode());             //  ------- (1)

        //计算该键值对在数组中的存储位置bucketIndex(哪个桶)
        int i = indexFor(hash, table.length);              // ------- (2)

        //对table的第i个桶上的链表进行遍历,寻找 key 保存的位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {      // ------- (3)
            Object k;
            //判断该条链上是否存在hash值相同且key为同一个对象的映射;
              //若存在,则直接覆盖 value,并返回旧value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;    // 返回旧值
            }
        }

        modCount++; //修改次数增加1,快速失败机制

        //原HashMap中无该映射,将该节点添加至该链表的链头
        addEntry(hash, key, value, i);            
        return null;
    }

equals()和hashCode()的应用

上述关键代码 通过比较hash值和equals方法判断是否存在相同的key
Object类的两个方法hashCode和equals,我们先来看一下这两个方法的默认实现:

    /** JNI,调用底层其它语言实现 */
    public native int hashCode();
     
    /** 默认使用==,直接比较是否同一个对象的引用 */
    public boolean equals(Object obj) {
    	return (this == obj);
    }

对象通过调用 Object.hashCode ( ) 生成晗希值,由于不可避免地会存在晗希值冲突的情况
因此hashCode 相同时 还需要再调用 equals 进行次值的比较,
但是 hashCode将直接判定 Objects 不同 跳过 equals 这加快了冲突处理效率。
Object 类定义中对 hashCode( ) 和equals( ) 要求如下

( 1 )如果两个对象的 equals 的结果是相等的,则两个对象的 hashCode 的返回值也必须是相同的。
( 2)覆写 equals方法必须同时覆写 hashCode

通过上述源码我们可以清楚了解到HashMap保存数据的过程。

1.首先,判断key是否为null,若为null,则直接调用putForNullKey方法;
2.若不为空,则先计算key的hash值,然后根据hash值计算在table数组中的索引位置;
3.1 如果table数组在该位置处有元素,则遍历链表判断是否存在相同的key,若存在则覆盖原来key的value,否则将该元素节 点添加至该链表的头节点,(最先保存的元素放在链尾)。
3.2 若table在该处没有元素,则直接保存

参考文章:
http://www.codeceo.com/article/java-hashmap-learn.html

https://www.hollischuang.com/archives/3542

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值