HashMap,HashSet

HashMap源码解析

// 默认初始容量为16,必须为2的n次幂
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 最大容量为2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子为0.75f
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// Entry数组,长度必须为2的n次幂
transient Entry[] table;
// 已存储元素的数量
transient int size ;
// 下次扩容的临界值,size>=threshold就会扩容,threshold等于capacity*load factor
int threshold;
// 加载因子
final float loadFactor ;

HashMap底层是用Entry数组存储数据,同时定义了初始容量,最大容量,加载因子
Entry是HashMap的内部类,它继承了Map中的Entry接口,它定义了键(key),值(value),和下一个节点的引用(next),以及hash值。
Entry是单线链表的一个节点。也就是说HashMap的底层结构是一个数组,而数组的元素是一个单向链表。

这里写图片描述

自己实现的map存在一个问题就是查询时需要遍历所有的key,为了解决这个问题HashMap采用hash算法将key散列为一个int值,这个int 值对应到数组的下标,再做查询操作的时候,拿到key的散列值,根据数组下标就能直接找到存储在数组的元素。但是由于hash可能会出现相同的散列值,为 了解决冲突,HashMap采用将相同的散列值存储到一个链表中,也就
是说在一个链表中的元素他们的散列值绝对是相同的。(hash值相同)

public V put(K key, V value) {
    // 如果key为null,调用putForNullKey方法进行存储
    if (key == null)
        return putForNullKey(value);
    // 使用key的hashCode计算key对应的hash值
    int hash = hash(key.hashCode());
    // 通过key的hash值查找在数组中的index位置
    int i = indexFor(hash, table.length );
    // 取出数组index位置的链表,遍历链表找查看是有已经存在相同的key
    for (Entry<K,V> e = table [i]; e != null; e = e. next) {
        Object k;
    // 通过对比hash值、key判断是否已经存在相同的key
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            // 如果存在,取出当前key对应的value,供返回
            V oldValue = e. value;
            // 用新value替换之旧的value
            e. value = value;
            e.recordAccess( this);
            // 返回旧value,退出方法
            return oldValue;
        }
    }
    // 如果不存在相同的key
    // 修改版本+1
    modCount++;
    // 在数组i位置处添加一个新的链表节点
    addEntry(hash, key, value, i);
    // 没有相同key的情况,返回null
    return null;
}

通过将key做hash取得一个散列值,将散列值对应到数组下标,然后将k-v组成链表节点存进数组中。

static int hash(int h) {
    // 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);
}

Map中的元素越多,hash冲突的几率也就越大,数组长度是固定的,所以导致链表越来越长,那么查询的效率当然也 就越低下了。还记不记得同时数组容器的ArrayList怎么做的,扩容!而HashMap的扩容resize,需要将所有的元素重新计算后,一个个重新 排列到新的数组中去(copy到新的数组),这是非常低效的,和ArrayList一样,在可以预知容量大小的情况下,提前预设容量会减少HashMap的扩容,提高性能。

再来看看加载因子的作用,如果加载因子越大,数组填充的越满,这样可以有效的利用空间,但是有一个弊端就是可能会导致冲突的加大,链表过长,反过来却又会造成内存空间的浪费。所以只能需要在空间和时间中找一个平衡点,那就是设置有效的加载因子

查找:

public V get(Object key) {
    // 如果key等于null,则调通getForNullKey方法
    if (key == null)
        return getForNullKey();
    // 计算key对应的hash值
    int hash = hash(key.hashCode());
    // 通过hash值找到key对应数组的索引位置,遍历该数组位置的链表
    for (Entry<K,V> e = table [indexFor (hash, table .length)];e != null;e = e. next) {
        Object k;
        // 如果hash值和key都相等,则认为相等
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
        // 返回value
        return e.value ;
    }
    return null;
}

在hash值相同的时候,继续判断(k = e.key) == key || key.equals(k),equals方法判断,如果hash方法得出的hash值相同,并且equals方法判断也相同,则返回value。===>引申出问题hashcode()与equals()方法重写的问题。

private V getForNullKey() {
    // 遍历数组第一个位置处的链表
    for (Entry<K,V> e = table [0]; e != null; e = e. next) {
        if (e.key == null)
        return e.value ;
    }
    return null;
}

从删除和查找可以看出,在根据key查找元素的时候,还是需要通过遍历,但是由于已经通过hash对key散列,要遍历的只是发生冲突后生成的链表,这样遍历的结果就已经少很多了,比我们自己写的完全遍历效率提升了n倍。

HashSet解析

// 底层使用HashMap来保存HashSet的元素
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
// 由于Set只使用到了HashMap的key,所以此处定义一个静态的常量Object类,来充当HashMap的value
private static final Object PRESENT = new Object();

HashSet是用HashMap来保存数据,而主要使用到的就是HashMap的key。
一个静态的常量Object类PRESENT来充当HashMap的value,将new出来的Object分配到堆空间(一个空的Object对象占用8byte)从根源上避免NullPointerException的出现,在代码中再也不会写这样的代码if (xxx == null) { … } else {….}

遍历HashSet是基于迭代器,基于HashMap内部类HashIterator

final Entry<K,V> nextEntry() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    // 取得HashMap底层数组中链表的一个节点
    Entry<K,V> e = next;
    if (e == null)
        throw new NoSuchElementException();
    // 将next指向下一个节点,并判断是否为null
    if ((next = e.next) == null) {
        Entry[] t = table;
        // 如果为null,则遍历真个数组,知道取得一个不为null的节点
        while (index < t.length && ( next = t[index ++]) == null)
        ;
    }
    current = e;
    // 返回当前节点
    return e;
}

该方法主要思路是,首选拿去HashMap低层数组中第一个不为null的节点,每次调用迭代器的next()方法,就用该节点next一下,当当前节点 next到最后为null,就拿数组中下一个不为null的节点继续遍历。什么意思呢,就是循环从数组第一个索引开始,遍历整个Hash表。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值