HashTable的源码和实践

373 阅读6分钟

继承关系

Hashtable继承于Dictionary类,实现了Map接口。Map是"key-value键值对"接口,Dictionary是声明了操作"键值对"函数接口的抽象类。

同时也可以被克隆和序列化。

底层结构

HashTable类中,保存实际数据的,依然是Entry对象。其数据结构与HashMap是相同的。

也是数组加链表

构造方法和初始化

一些基本属性也与hashmap类似

基本属性

	private transient Entry<?,?>[] table;

    /**
     * The total number of entries in the hash table.
     */
    private transient int count;

    /**
     * The table is rehashed when its size exceeds this threshold.  (The
     * value of this field is (int)(capacity * loadFactor).)
     *
     * @serial
     */
    private int threshold;

    /**
     * The load factor for the hashtable.
     *
     * @serial
     * 默认0.75
     */
    private float loadFactor;

    /**
     * 修改的次数
     */
    private transient int modCount = 0;

构造方法

1.传入初始化大小和负载因子的构造方法

初始大小和负载因子都有相应的限制

同时计算出阈值

public Hashtable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);

        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry<?,?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }

2.传入初始化大小的构造方法

默认加载因子为0.75

public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

3.默认构造方法

初始大小为11,加载因子为0.75

 public Hashtable() {
        this(11, 0.75f);
    }

4.传入一个map映射的构造方法

会进行容器初始化大小的计算

public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }

常用api

put

synchronized保证线程安全

public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        //定义并赋值entry数组
        Entry<?,?> tab[] = table;
        
        int hash = key.hashCode();
        // 通过hash值与table长度取余,确定元素的位置
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        // 取出当前位置上的元素  
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        //如果存在,则获取对应下标的数组进行循环比较
        for(; entry != null ; entry = entry.next) {
        	// 查找是否具有相同hash值和key的元素,有则替换,并返回
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        addEntry(hash, key, value, index);
        return null;
    }

put 操作

1.确保value值不为空值

2.确保 key不存在hashtable中,如果存在就替换后返回该对象。

3.不存在直接调用addEntry方法,进行值的放入。

源码可以看出Hashtable是不允许key,value为null

流程图

addEntry方法

源码解读:

private void addEntry(int hash, K key, V value, int index) {
		//记录变化次数
        modCount++;
		//定义并赋值entry数组
        Entry<?,?> tab[] = table;
        //如果hashtable的entry数量大于了阈值,则进行扩容
        //threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            //扩容
            rehash();
			//扩容后重新对tab赋值
            tab = table;
            //重新计算元素的位置
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        @SuppressWarnings("unchecked")
        // 取出当前位置上的元素
        Entry<K,V> e = (Entry<K,V>) tab[index];
        // 进行插入操作,从这里可以看出新的元素总是在链表头的位置
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

总结

在该函数中涉及扩容,但是由于put操作为线程安全,所以扩容时也是线程安全的。

扩容要求:当前元素个数大于等于容量与扩容因子的乘积。

还有一点需注意插入的新节点总是在链表头。

流程图

rehash

源码解读:

@SuppressWarnings("unchecked")
    protected void rehash() {
    	//获取旧的容量大小
        int oldCapacity = table.length;
        //定义赋值旧的map
        Entry<?,?>[] oldMap = table;

        // overflow-conscious code
        //计算新的容量大小,为旧容量的两倍再+1
        int newCapacity = (oldCapacity << 1) + 1;
        //对容量大小进行限制
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
        	//如果旧的容量已经达到最大容量,不再进行扩容
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            //如果超过了最大容量值,则设置为最大容量值    
            newCapacity = MAX_ARRAY_SIZE;
        }
        //初始化新的map,传入容量大小
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
		//记录集合的变化次数
        modCount++;
        //计算阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        //赋值table
        table = newMap;
		//遍历旧的集合,数组转移 这里是从数组尾往前搬移
        for (int i = oldCapacity ; i-- > 0 ;) {
        	//取出每一个位置上的链表,循环链表获取每个元素
            for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
                Entry<K,V> e = old;
                old = old.next;
				// 计算元素在新数组中的位置
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                // 进行元素插入,注意这里是头插法,元素会倒序
                e.next = (Entry<K,V>)newMap[index];
                newMap[index] = e;
            }
        }
    }

流程图

总结

1.新的数组大小是原来的2倍加1,

2.扩容时插入新元素采用的是头插法,元素会进行倒序

3.关键的两重循环

它先去从后向前的遍历数组,然后第二层循环去遍历数组中每个对象链表,重新计算链表中每一个节点的位置,这样两层循环下来,各个节点的位置就调整好了,容器容量也扩展

get

源码解读: synchronized修饰线程安全

  @SuppressWarnings("unchecked")
    public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        // 计算元素的位置
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        //在对应的下标位置,循环链表
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        	// 遍历 寻找hash和key相同的元素
            if ((e.hash == hash) && e.key.equals(key)) {
            	//有,返回
                return (V)e.value;
            }
        }
        //不存在,就返回null
        return null;
    }

流程图

总结

通过hash值与key进行查找,找到立即返回,未找到则返回null。

remove

源码分析: synchronized修饰线程安全

public synchronized V remove(Object key) {
        Entry<?,?> tab[] = table;
        //通过key计算节点存储下标
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>)tab[index];
        //循环遍历链表,通过hash和key判断键是否存在
        //如果存在,直接将该节点设置为空,并从链表上移除
        for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                modCount++;
               	//在链表中删除该元素
                if (prev != null) {
                    prev.next = e.next;
                } else {
                    tab[index] = e.next;
                }
                //减少元素数量
                count--;
                V oldValue = e.value;
                //设置为空
                e.value = null;
                //返回删除的值
                return oldValue;
            }
        }
        //没有删除就返回null
        return null;
    }

流程图

总结

通过key计算阶段存储的下标,根据下标获取对应的链表,循环,寻找hash和key都相等的键,如果存在,就将该节点设置为空,并在链表中删除,返回删除的值。

不存在,就返回空值

特点

线程安全的原因

所有方法都有synchronized修饰,保证多线程下的线程安全。

在Java中,可以使用synchronized关键字来标记一个方法或者代码块,

当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,

这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,

只有等待这个方法执行完毕或者代码块执行完毕,

这个线程才会释放该对象的锁,

其他线程才能执行这个方法或者代码块。

如何实现的线程安全

使用synchronized修饰

hashTable 和hashMap的区别

1、虽然 HashMap 和 Hashtable 都实现了 Map 接口,但 Hashtable 继承于 Dictionary 类,而 HashMap 是继承于 AbstractMap;

2、HashMap 可以允许存在一个为 null 的 key 和任意个为 null 的 value,但是 HashTable 中的 key 和 value 都不允许为 null;

3、Hashtable的方法是同步的,因为在方法上加了 synchronized 同步锁,而 HashMap 是非线程安全的;

4.HashMap的初始化大小为16,扩容是原来的2倍,而Hashtable的初始化大小是11,扩容是原来的2倍+1。

5.Hashtable扩容转移元素时采用的是头插法。HashMap在1.7的时候也是头插法,但是1.8改为尾插法了。

总结

如果想要快速用hashmap,如果要线程安全使用ConcurrentHashMap。

在实际运用中HashTable已经被淘汰了。

写在后面的话

每当你做出选择的时候,同时也以为着失去了另外一种可能性。

那么你每天都在失去,所以不要去纠结对错得失,既然选择了当下这条路。

那么剩下需要考虑的就是如何把这条路走好走宽。