Hashtable源码解析

Map--HashTable

上一篇文章中,我们分析了HashMap的源码,这一篇文章我们学习Map接口的另一个实现类---HashTable,在学习之前,不熟悉hashMap的可以先看我的上一篇文章Map--HashMap,我们需要先了解下它和HashMap有哪些异同点。

不同点HashMapHashTable
继承的父类Dictionary类AbstractMap
线程安全性线程不安全线程安全
key和value允许为null不允许为null
遍历方式IteratorIterator和Enumeration
hash值计算重新计算key的hash值直接使用key的hashCode()
初始化默认容量为16默认容量为11
扩容方式原容量*2原容量*2+1
数据结构数组+链表+红黑树数组+链表
Iterator遍历数组的顺序索引从小到大索引从大到小
确认key在数组中的索引i=(n-1)&hashindex=(hash&0x7FFFFFFF)%tab.length
底层数组容量为2的整数幂一定要为2的整数幂不要求

NOTE:HashMap和HashTable最大的不同体现在线程安全、key和value是否为null,HashTable是个过时的集合类,如果使用场景不需要线程安全,可以直接使用hashMap来代替;如果需要在线程安全的场景中使用,可以使用ConcurrentHashMap替换,看起来HashTable好像没什么用,但是面试经常问啊,所以我们还是需要了解下。

因为HashMap和HashTable在存储结构实现方式上很相似,所以这篇文章主要讲解HashTable与HashMap不同的知识点。

我们先看一下HashTable的底层数据结构:

HashTable的数据结构

 

1、成员属性

transient Entry[] table:Entry[ ]数组类型,每个Entry代表一个键值对

transient int count:HashTable内键值对的数量,不是容器的大小

int threshold:调整hashTable容量的阈值

float loadFactor:加载因子

transient int modCount:标记HashTable修改的次数

//Entry[ ]数组类型,每个Entry代表一个键值对
private transient Entry<?,?>[] table;
//HashTable内键值对的数量,不是容器的大小
private transient int count;
//调整hashTable容量的阈值
private int threshold;
//加载因子
private float loadFactor;
//标记HashTable修改的次数
private transient int modCount = 0;

2、构造函数

HashTable有四个构造函数,我们来逐一分析

  • HashTable(int initialCapacity, float loadFactor)

public Hashtable(int initialCapacity, float loadFactor) {
    //初始容量<0,抛出异常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
    //负载因子为非负整数,否则抛出异常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);
//初始化hashTable中的参数、loadFactor、table和threshold
        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry<?,?>[initialCapacity];
    //选用initialCapacity*loadFactor和Max_ARRAY_Size+1最小的作为阈值
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }

MAX_ARRAY_SIZE表示为给数组(Table)分配的最大容量,

 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

有同学可能会对MAX_ARRAY_SIZE取值有疑问,为什么是Integer.MAX_VALUE-8,这是因为数组作为一个对象,需要一块内存来存储对象头信息,对象头信息的最大占用内存不能超过8个字节,所以需要减去这个头信息才是分配给Table的最大容量。

  • HashTable (int initialCapacity)

以给定的初始容量和么默认加载因子(0.75f)构造hashtable

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

以默认的初始容量(11)和加载因子(0.75f)构造HashTable

 public Hashtable() {
        this(11, 0.75f);
    }
  • HashTable(Map<? extends K, ? extends V> t)

使用给定的键值对集合t来构造HashTable

   public Hashtable(Map<? extends K, ? extends V> t) {
       //初始化HashTable
        this(Math.max(2*t.size(), 11), 0.75f);
       //将t中的键值对插入到HashTable中
        putAll(t);
    }

我们看一下putAll方法,它内部使用了增强for循环来遍历,内部调用了put方法,我们在核心方法讲解put方法

public synchronized void putAll(Map<? extends K, ? extends V> t) {
    //使用增强for循环来进行遍历
        for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
            //调用put方法
            put(e.getKey(), e.getValue());
    }

3、核心方法

HashTable内部提供了很多方法,我们在这篇文章主要讲解HashTable中比较重要的方法,put、get和remove

3.1、put方法

put方法时是将指定的键值添加到hashTable中,其添加步骤可以概括为①判断value不为null,为null时,则抛出异常②计算key的hash值并找到key的索引,获取key所在位置的entry③遍历entry,判断key是否存在④如果key存在,则将用新的值替换旧值⑤如果指定的位置key不存在,直接添加,并返回null

 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<?,?> tab[] = table;
     //直接使用hashCode作为hash值
        int hash = key.hashCode();
     //找到key的索引位置
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
     //获取指定下标的entry
        Entry<K,V> entry = (Entry<K,V>)tab[index];
     //遍历链表
        for(; entry != null ; entry = entry.next) {
            //判断链表中是否有与entry的hash和key相等的对象,如果有,则让新值覆盖旧值,并返回旧值
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
//如果在Table没有对应key值,则新添加一个
        addEntry(hash, key, value, index);
        return null;
    }

在这个程序中,有的同学可能会有疑问,比如key的索引位置是怎么计算的,添加的流程是什么,

  • index的计算

hash&0x7FFFFFFF操作是为了使得hashCode的值为正数,(计算后的值)%tab.length表示对数组长度取模,从概率上讲,采用取模计算可以保证结点在数组上的分配比价均匀,这只是减少哈希冲突一种策略。

如果在Table中对没有对应的key值,则需要新添加一个,我们具体看一下添加流程

  • addEntry(hash, key, value, index)

addEntry方法的作用是将指定的值的放入到指定坐标下,其步骤如下①判断entry中的个数是否大于阈值②大于阈值时需要扩容并重新计算值的位置③不大于阈值时需要将指定的值放入到index位置下,原有的index位置的元素向后移,可以看出数据的插入是前插

  private void addEntry(int hash, K key, V value, int index) {
        modCount++;

        Entry<?,?> tab[] = table;
      //判断entry的个数是否大于阈值(阈值默认是11*0.75f)
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            //如果超过阈值时,扩容并改变原来元素的位置
            rehash();

            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        @SuppressWarnings("unchecked")
      //将原下标为index的元素赋值给e
        Entry<K,V> e = (Entry<K,V>) tab[index];
      //将新的结点放在坐标为index的位置,并将新结点的next设置为e
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

我们对上面程序中扩容方法rehash还没有讲解,我们来讲解下rehash的源码

  • rehash()

rehash方法是对扩容后的数组重新计算下标值并放入的数组中,其步骤如下①将新的数组的容量扩展为(原容量)*2+1,②判断新数组的容量是否超出了最大容量的限制③超出了容量限制,就将最大容量赋值给新的数组容量④遍历原有数组中的元素并重新计算新的索引,将值存入到新的数组中

  protected void rehash() {
        int oldCapacity = table.length;
      //将原来的数组赋值给oldMap
        Entry<?,?>[] oldMap = table;

        // overflow-conscious code
      //新的容量扩容为:(原有容量)*2+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;
        }
      //以新容量的大小构造新的容器
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

        modCount++;
      //根据新容量重新计算阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        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;
            }
        }
    }

3.2、get方法

返回指定key的value,如果不存在,则返回null

 public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
     //计算索引值
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
     //根据索引找到数组的位置,根据key找到指定的值,并返回,如果没有则返回null
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

3.3、remove方法

删除指定key的键值对,其流程如下①根据key找到数组中的索引,获取key所在的entry②遍历entry,判断key是否存在③如果key存在,删除指定的键值对,并返回Value值④如果不存在,返回null

    public synchronized V remove(Object key) {
        Entry<?,?> tab[] = table;
        //计算key在hashtabke中的索引
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        //根据索引得到头结点键值对
        Entry<K,V> e = (Entry<K,V>)tab[index];
        //遍历entry,找到key的键值对并删除,返回value值
        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;
    }

参考文献

[1]https://blog.csdn.net/panweiwei1994/article/details/77427010

[2]https://blog.csdn.net/panweiwei1994/article/details/77428710

[3]jdk开发文档

[4]马克.艾伦.维斯.数据结构与算法分析

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值