Java 数据结构——Hashbable底层源码分析

目录

 

1、概述

2、原理(图解)

3、源码分析

4、知识延伸


一、概述

  • Hashtable也称为散列表,它存储的内容是键值对(key-value)映射,是根据关键字值(key value)直接进行访问的数据结构。也就是说,它通过把关键字值映射到一个位置来访问记录,以加快查找的速度。这个映射函数称为哈希函数(也称为散列函数),映射过程称为哈希化,存放记录的数组叫做散列表。
  • Hashtable继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
  • Hashtable的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null,Hashtable中的映射不是有序的。
  • Hashtable的实例有两个参数影响其性能:   初始容量(initialCapacity) :哈希表中桶 的数量,初始容量 就是哈希表创建时的容量;    负载因子(load Factor):负载因子哈希表是0.1 到 1.0 范围内的数字,当容量自动增加(扩容rehash)之前允许哈希表得到满足的度量。初始容量和负载因子这两个参数只是对该实现的提示。通常,默认负载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。负载因子过高虽然减少了空间开销,但同时也增加了查找某个条目的时间。

 

二、原理(图解)

 

三、源码分析

1、相关参数定义:

 /**
  * 为一个Entry[]数组类型,Entry代表了“拉链”的节点,每一个Entry代表了一个键值对,哈希表的"key- 
    value键值对"都是存储在Entry数组中的。
  */
private transient Entry<?,?>[] table;

 /**
  * 哈希表中条目的总数
  */
private transient int count;

 /**
  * table数组扩容的节点数阈值,以此来判断是否达到扩容标准
  */
private int threshold;

 /**
  * 负载因子默认0.75f
  */
private float loadFactor;

 /**
  * Hashtable被修改次数,用来实现“fail-fast”机制的(也就是快速失败)。
  */
private transient int modCount = 0;

2、构造函数:

// 初始化默认构造函数。
public Hashtable() {
  //默认容量为11 负载因子为0.75
  this(11, 0.75f);
} 

// 初始化指定“容量大小”的构造函数
public Hashtable(int initialCapacity) {
   this(initialCapacity, 0.75f);
}

// 初始化指定“容量大小”和“加载因子”的构造函数
public Hashtable(int initialCapacity, float loadFactor) {
   //传入容量不能<0,否则会报异常 
   if (initialCapacity < 0)
      throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);

   //传入的加载因子范围在0——1之间,默认0.75 建议设置在0.7——0.75之间,否则也会报异常
   if (loadFactor <= 0 || Float.isNaN(loadFactor))
      throw new IllegalArgumentException("Illegal Load: "+loadFactor);

   //如果传入的容量为0,那么会默认把容量初始为1,可看出容量默认为11,最小为1
   if (initialCapacity==0)
      initialCapacity = 1;
      this.loadFactor = loadFactor;
      //创建容量为initialCapacity的Entry数组table
      table = new Entry<?,?>[initialCapacity];

      //计算出数组的阀值,也算一个临界值吧(阀值表示当table的长度达到这个阀值(临界值)之后就会触 
      //发扩容机制(rehash))
      //计算阀值的公式:阀值=容量*负载因子 与 当前系统数组最大长度+1 的最小值
      //MAX_ARRAY_SIZE =Integer.MAX_VALUE - 8

      threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}

// 初始化包含“子Map”的构造函数
public Hashtable(Map<? extends K, ? extends V> t) {

   // 初始容器取map最大尺寸*2和11取大的,也就是最小也是11,负载因子0.75
   this(Math.max(2*t.size(), 11), 0.75f);
   //将传入的map放入数组
   putAll(t);
}

//将传入的map放入数组
public synchronized void putAll(Map<? extends K, ? extends V> t) {
   //遍历循环传入的map,调用put放入数组
   for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
      //放入
      put(e.getKey(), e.getValue());
}

接下来主要分析常用的几个方法(get / put / remove / rehash) ,在这个之前我们先看下Entry这个类

3、Entry类分析

//Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。 
private static class Entry<K,V> implements Map.Entry<K,V> {
        //定义一个int类型的hash字段
        final int hash;
        //定义key字段
        final K key;
        //定义value字段
        V value;
        //存储下一个Entry数组对象
        Entry<K,V> next;

        //构造函数
        protected Entry(int hash, K key, V value, Entry<K,V> next) {
            this.hash = hash;
            this.key =  key;
            this.value = value;
            this.next = next;
        }
        
        //克隆
        @SuppressWarnings("unchecked")
        protected Object clone() {
            return new Entry<>(hash, key, value,
                                  (next==null ? null : (Entry<K,V>) next.clone()));
        }

        // 获取Entry中的key
        public K getKey() {
            return key;
        }

        //获取Entry中的value值
        public V getValue() {
            return value;
        }
        
        //设置Entry中的value值  可以看出hashtable的key和value都不能为null,否则会抛异常
        public V setValue(V value) {
            if (value == null)
                throw new NullPointerException();

            V oldValue = this.value;
            this.value = value;
            return oldValue;
        }
        
        //比较equals
        public boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;

            return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
               (value==null ? e.getValue()==null : value.equals(e.getValue()));
        }
        
        //获取hashCode
        public int hashCode() {
            return hash ^ Objects.hashCode(value);
        }
        
        
        public String toString() {
            return key.toString()+"="+value.toString();
        }
    }

4、get方法

//根据key从哈希表中获取对应的value值
//获取方法同步锁synchronized 
public synchronized V get(Object key) {

        //创建Entry[]数组类型
        Entry<?,?> tab[] = table;

        //获取key的hash值
        int hash = key.hashCode();

        //再根据hash值和table的长度计算出在table数组中的索引位置
        //扩展:hash值为int类型 4个字节 32bit.
        //     为了在hash为负值的情况下,去掉起符号位,所以和0x7FFFFFFF进行&操作
        //     0x7FFFFFFF 二进制 0111 1111 1111 1111 1111 1111 1111 1111
        //     负数与其进行&操作将产生一个正整数

        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;
    }

5、put放入哈希表

 

 //添加键值对   
 public synchronized V put(K key, V value) {
        //判断value是否为空
        if (value == null) {
            throw new NullPointerException();
        }

        Entry<?,?> tab[] = table;

        //先获取key的hash值
        int hash = key.hashCode();

        //再根据hash值和table的长度计算出在table数组中的索引位置
        int index = (hash & 0x7FFFFFFF) % tab.length;

        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                //若添加的key在Hashtable已经存在,则用新value覆盖原有的value
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
        //添加链表节点
        addEntry(hash, key, value, index);
        return null;
    }


//添加链表节点
private void addEntry(int hash, K key, V value, int index) {

        //更改次数加1
        modCount++;

        Entry<?,?> tab[] = table;

        //当哈希表实际容量>=哈希表的阀值(临界值),触发扩容操作
        if (count >= threshold) {
            //进行扩容操作
            rehash();
            
            //将扩容后的table赋值给新创建的Entry数组tab[]
            tab = table;

            //先获取key的hash值
            hash = key.hashCode();

            //再根据hash值和table的长度计算出在table数组中的索引位置
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // 创建一个新的Entry数组
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];
        tab[index] = new Entry<>(hash, key, value, e);

        //哈希表哈希表实际容量+1
        count++;
    }

6、rehash 扩容解析 

//扩容操作
protected void rehash() {
        //获得扩容前数组容量
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;

        //计算得出新的数组容量,新数组容量=旧数组容量*2+1(<<1表示右移一位 <<1=2的一次方=2)
        int newCapacity = (oldCapacity << 1) + 1;
        
        //如果新数组容量>数组规定的最大容量限制,则使用最大限制容器值 MAX_ARRAY_SIZE
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            //如果旧的数组容量=MAX_ARRAY_SIZE则
            if (oldCapacity == MAX_ARRAY_SIZE)

                return;
            newCapacity = MAX_ARRAY_SIZE;
        }

        //创建一个容量为newCapacity的新的数组对象
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

        //容器更改次数+1
        modCount++;

        //获取新的数组阀值(临界值)
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);

        //将新的数组赋值给table参数
        table = newMap;
 
        //依次循环将原有元素复制到新的Hashtable中
        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;
            }
        }
    }

四、Hashtable知识延伸

1)Java中==,equals和hashCode有什么区别和联系?https://www.cnblogs.com/aspirant/p/7079538.html

2)为什么加载因子要用0.75? https://www.cnblogs.com/aspirant/p/11470928.html

3)为什么获取数组下标时要 key.hashCode() & 0x7fffffff ?https://blog.csdn.net/weixin_39590058/article/details/88925659

4)为什么扩容是2N+1?

5)负载因子值的大小,对HashMap有什么影响?

6) Hashtable的复杂度为什么是O(1)?

7)Hashtable最大容量为什么是2^31-8?https://blog.csdn.net/ChineseYoung/article/details/80787071

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gogoed

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值