HashMap面试合集

HashMap简单介绍

HashMap的成员属性

相关代码解释:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量 2的30次方

static final float DEFAULT_LOAD_FACTOR = 0.75f;   //默认的加载因子

static final Entry<?,?>[] EMPTY_TABLE = {};

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; //哈希桶,存放链表。 长度是2的N次方,或者初始化时为0.

transient int size;// 键值对的个数/

int threshold;  //16*0.75 =12

final float loadFactor; // 加载因子   0.75  传进来的值

transient int modCount;// map结构修改次数,累加/

static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;/** 默认阈值*/

transient int hashSeed = 0;

问题一:HashMap的特点

  • 1.继承了AbstractMap类,实现了Cloneable,Serializable,Map接口;
  • 2.允许key/value为null,但key只能有一个null;
  • 3.非线程安全,多个线程同时操作同一个HashMap实例所做的修改不同步;
  • 4.进行遍历时如果执行HashMap的remove(Object key)或者put(Object value)方法时会快速失败,抛出ConcurrentModeificationException。遍历时删除元素只能通过Iterator本身的remove()方法实现。

问题二:什么是哈希冲突,解决哈希冲突的方法有哪些,HashMap使用哪种方法?

哈希冲突就是根据key即经过一个函数f(key)得到的结果作为地址去存放当前的key,value键值对,但是却发现算出来的地址上已经被占用了,这就产生了哈希冲突。

解决哈希冲突的四种方法:

开放定址法

  1. 线性探测法
  2. 线性补偿探测法
  3. 伪随机探测 

链地址法

再哈希法

当发生冲突时,使用第二个、第三个哈希函数计算地址,直到无冲突时。

建立一个公共溢出区

假设哈希函数的值域为[0,m-1],则设向量HashTable[0,m-1]为基础表,另外设立存储空间向量OverTable[0,v]用来存储发生冲突的纪录。

问题三:如果自己指定HashMap的初始容量和加载因子,那么容量的大小,和加载因子的大小对HashMap有什么影响?

几种构造函数:

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
		//初始容量最大不能超过2的30次方
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
		//显然加载因子不能为负数  || 判断是不是一个数字
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();
    }
public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
 public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

 

 根据构造方法可以看出,无论是使用默认的初始容量还是使用指定的初始容量,当你调用HashMap的构造方法时,HashMap是没有进行初始化容量,也就是现在是一个空的HashMap(容量为0),在put(K key,V value)加入第一个元素时调用inflateTable(int toSize)进行扩容,如果初始容量设置为2的幂次方时,就会按照你设置的容量设置;当你设置的初始容量不是2的幂次方时,就会按照你设置的值取大于这个值的最近的2的幂次方。

负载因子的大小决定了HashMap的数据密度。负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或者插入时的比较次数增多,性能下降;负载因子越小,就越容易出发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也越短,查询和插入是的比较次数也越小,性能会越高,但是会造成一定的空间浪费,而且经常扩容也会影响性能,建议初始化预设大一点的空间。

put()加入一个对象

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);//threshold  阈值
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key); //扰动处理后的key的hashcode
        int i = indexFor(hash, table.length);//值应该存放在哪个下标下面
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;  //key 小明   value 13  key 小明   value 12
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  //当有重复的key插入的时候就会替换掉之前的
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
 private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);//将传入的容量大小转化为:>传入容量大小的最小的2的次幂

        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//阈值
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }
 private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {  //本身已经存放一个key为null
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
		//本身没有存放一个key为null
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }
 void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);   //2倍扩容
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }
 void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

问题四:HashMap如何计算key-value结构的存储位置

  • 首先根据要查找的key计算出对应的扰动处理后的key的哈希码 int hash = hash(key); 

  • 然后通过哈希码找到对应哈希表中桶中的地址(值应该存放在哪个下标下面)    int i = indexFor(hash, table.length);

  • 然后判断桶是链表结构还是红黑树结构

    • 普通链表直接遍历链表查找即可

    • 红黑树TODO 

问题五:为什么HashMap的容量要保持2的幂次方

HashMap的容量为什么是2的幂次方,和indexFor(int h, int length)方法中的h & (length-1)有关,符号&是按位与的计算,这是位运算,计算机能直接运算,特别高效,按位与的计算方法是只有当对应位置的数据都是1时,运算结果也为1,当HashMap的容量是2的幂次方时,(n-1)的二进制也就是1111...111,这样与添加的元素的hash进行位运算时,能够得到充分的散列,使得添加的元素均匀地分布在HashMap的每一个位置,减少哈希碰撞。

hash()和indexFor()方法

 final int hash(Object k) {
        int h = hashSeed;  //0
        if (0 != h && k instanceof String) { //在我们的系统里hashSeed = 0
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode(); // key hashcode  散列码

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);  
		//扰动处理 加大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性 & 均匀性,最终减少Hash冲突
    		//让哈希码分布的更加均匀 从而避免出现哈希冲突
    }
 static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);//hash(key) &  16 -1    = hash(key) % 16
    }

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值