java:简单聊聊hashMap的实现

缘由

偶然看到一位兄台在面试的时候被问到了java中hashmap的实现。突然来了兴趣,就想着自己也看看hashmap的实现的源码。此外,由于被老师要求使用JE22实现音乐网站的事,最近正在奋力利用传智博客的视频学习java web,所以nginx的事恐怕只能暂时放一下放了。

从官方文档来看

综述

  • 网址:http://www.javaweb.cc/JavaAPI1.6/
我挑了几句重要的来分析分析,
  1. 基于哈希表的 Map 接口的实现
  2. 允许使用 null 值和 null 键
  3. 此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
    这话说的很生硬,我觉得就是遍历键或者值的时候,遍历得到的顺序是不是固定的。
  4. 迭代所需时间和HashMap的大小有关,如果需要迭代,不要设置太高的初始容量。
  5. HashMap 的实例有两个参数影响其性能:初始容量 和加载因子。
    初始容量就是整个hash的数组的长度,加载因子就是决定了一个阈值,是默认是0.75,如果在此hash中的条目的数量大于了0.75*初始容量,使用rehash重建hash表
  6. hashmap不是同步的。也就是在多线程访问这个hash表的时候,并不是线程安全的。
构造方法:

方法:


看完了上面的可能对这个hashmap的使用没什么问题了。

重要部分

碰撞的解决

我们都知道hash表有两种常见的实现方法,
  1. 拉链法
  2. 开放寻址法
nginx的实现方法是开放寻址法。而这个HashMap使用的确实拉链法,也就是说,实现的hash表的数组的每一个元素,都形成了一个单链表。当两key算出来的hashkey一样的时候,则会以链表的形成存储在一起。当然查询的时候,算出了在哈希表的下标之后,去看看对应下标的第一个元素是不是 ,如果不是,则遍历这个单链表。貌似C语言中define的设计也是这样的吧。摘一张别人的图(见参考博客),希望大家明白。



从源码上看

相关的这个句:
    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    transient Entry<K,V>[] table;
这个table变量就是hash表的数组,而且表明table是一个Entry类的数组,也就是每一个table的元素是一个Entry类的实例。看看Entry类的关键:
    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
	}
上图摘了一点点Entry类的一些内容,我们可以看到关键就是那个
Entry<K,V> next;
这里表明了每一个Entry的对象都有一个指向下一个对象的引用(之前一直在搞C,C里面是指针,这里不知道用引用合适不合适)。那么通过这个对象就很容易找到同一个hash值的下一个对象。


四个构造函数

我只看看其中的较为简单的三个:
  1. HashMap(int initialCapacity, float loadFactor)
  2. HashMap(int initialCapacity)
  3. HashMap()


从源码的角度来看,HashMap(int initialCapacity) 与 HashMap()只是利用了默认的参数调用了HashMap(int initialCapacity, float loadFactor)而已。 
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
我们来看看关键的HashMap(int initialCapacity, float loadFactor),摘取了部分代码:
public HashMap(int initialCapacity, float loadFactor) {
		//如果我们设定的大小大于了最大的,那么就使用默认的最大大小
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
			
        // Find a power of 2 >= initialCapacity
	// hash表的大小必须是 2 的N次方,所只有用这样的办法来找到
	//合适的大小
	int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;


        this.loadFactor = loadFactor;
		//设置阈值,当大于这个阈值的时候就会自动扩容
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
		//这里只是确定是否使用另一种hash计算的方式
		//备选哈希函数是只适用于容量大于指定的阈值大小的Map。 
		//默认情况下,值是-1 。 此值禁用备选哈希函数。 
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
	//留给子类用的,可以在这里写一些子类需要初始化的内容
        init();
    }
插入一个键值对
public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
	//算出hash值
        int hash = hash(key);
	//取模
        int i = indexFor(hash, table.length);
	for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
			//如果这个key相同,则覆盖
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
               	V oldValue = e.value;
                e.value = value;
				//并没有做什么实现
				//英文解释是用来标记一下,这个e被覆盖过
                e.recordAccess(this);
                return oldValue;
            }
        }
	//每增加一个元素就会自加1
        modCount++;
	//加在链表末尾
	addEntry(hash, key, value, i);
        return null;//成功就返回null
    }
	
void addEntry(int hash, K key, V value, int bucketIndex) {
	//如果容量大于阈值就就扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            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);
	//hash表的键值对多了一个
        size++;
}
	
Entry(int h, K k, V v, Entry<K,V> n) {
		value = v;
		//如果是被插入到了table数组同一个key的链表的表头。
		next = n;
		key = k;
		hash = h;
}

取模的方式

static int indexFor(int h, int length) {
    return h & (length-1);
}

h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。这是一种非常聪明的取模的方式,比如说:

8 & (16-1):0100 &1111=0100
9 & (16-1):0101 &1111=0101

如果这个数大于16的话,那么除了最右边的四个,都会被忽略掉,而且那么就会取到正确的值。
这里也就注定了为什么必须这个hash表的长度必须是2的大小必须是 2 的N次方。如果不取2的N次会有极大的增加冲突,而且还会导致某些hash表的下标永远得到,分析的过程请看:

modCount的作用

官方文档已经说清楚了这个hashmap 不是线程安全的,所以modCount的作用就是为防止线程不是安全的而引发出来的一些错误的事情。modCount当在被增加、减少等等操作的时候都会自加1。如下所示:
public V put(K key, V value) {
	//每修改了hash表的元素多少就会自加1
        modCount++;
	//加在链表末尾
	addEntry(hash, key, value, i);
        return null;//成功就返回null
    }
当我们在迭代hash表的时候,还有有另一个属于迭代器的参数:expectedModCount。
当在初始化迭代器的时候,我们会给这个赋值,而在使用迭代器遍历的时候,都每次很检查这个两个值是否相等,如果不相等就说明,有别的线程修改了这个hash表,那么就会抛出异常。代码如下:
private abstract class HashIterator<E> implements Iterator<E> {
    int expectedModCount;   // For fast-fail
    HashIterator() {
        expectedModCount = modCount;
    }

    final Entry<K,V> nextEntry() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}


这虽然是个抽象类,但是我们具体能够使用的对键的迭代、对值的迭代、以及对键值对的迭代是其的实现类
  
  private final class ValueIterator extends HashIterator<V> {
        public V next() {
            return nextEntry().value;
        }
    }


    private final class KeyIterator extends HashIterator<K> {
        public K next() {
            return nextEntry().getKey();
        }
    }


    private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
        public Map.Entry<K,V> next() {
            return nextEntry();
        }
    }
这就是fast-fail机制,官方文档对其有着较为完整的介绍:

注意,此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作(会导致modCount的值改变);仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:
  • Map m = Collections.synchronizedMap(new HashMap(...));
由所有此类的“collection 视图方法”所返回的迭代器都是快速失败(fast-fail)的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间发生任意不确定行为的风险。
注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。
从上面的论述可以看出,官方并不建议在需要同步的情况下使用HashMap。


参考博客

深入Java集合学习系列:HashMap的实现原理

Hash碰撞& 拒绝服务漏洞 - 备选哈希函数sun.misc.Hashing.stringHash32

总结

发现自己有点关心错了东西,现在主要是完成老师的音乐网址建设的事,这些比较底层的时候,好像还不如研究nginx来的有意思。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值