Java入门系列之集合Hashtable源码分析(十一)

前言

上一节我们实现了散列算法并对冲突解决我们使用了开放地址法和链地址法两种方式,本节我们来详细分析源码,看看源码中对于冲突是使用的哪一种方式以及对比我们所实现的,有哪些可以进行改造的地方。

Hashtable源码分析

我们通过在控制台中实例化Hashtable并添加键值对实例代码来分析背后究竟做了哪些操作,如下:

 public static void main(String[] args) {

        Hashtable hashtable = new Hashtable();
        hashtable.put(-100, "first");
 }

接下来我们来看看在我们初始化Hashtable时,背后做了哪些准备工作呢?

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {

    //存储键值对数据
    private transient Entry<?,?>[] table;

    //存储数据大小
    private transient int count;

    //阈值:(int)(capacity * loadFactor).)
    private int threshold;

    //负载因子: 从时间和空间成本折衷考虑默认为0.75。因为较高的值虽然会减少空间开销,但是增加查找元素的时间成本
    private float loadFactor;

    //指定容量和负载因子构造函数
    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];

        //默认阈值为8
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }

   //指定容量构造函数
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

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

    private static class Entry<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        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;
        }
    }
}

Hashtable内部通过Entry数组存储数据,通过Entry结构可看出采用链地址法解决哈希冲突,当初始化Hashtable未指定容量和负载因子时,默认初始化容量为11,负载因子为0.75,阈值为8,若容量小于0则抛出异常,若容量等于0则容量为1且阈值为0,否则阈值以指定容量*0.75计算或者以指定容量*指定负载因子计算为准。

通过如上源代码和变量定义我们很快能够得出如上结论,这点就不必我们再进行过多讨论,接下来我们再来看看当我们如上添加如上键值对数据时,内部是如何做的呢?

public synchronized V put(K key, V value) {
        if (value == null) {
            throw new NullPointerException();
        }

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

        int hash = key.hashCode();

        int index = (hash & 0x7FFFFFFF) % tab.length;

        Entry<K,V> entry = (Entry<K,V>)tab[index];

        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        addEntry(hash, key, value, index);

        return null;
    }    

我们一步步来分析,首先若添加的值为空则抛出异常,紧接着获取添加键的哈希值,重点来了,如下代码片段的作用是什么呢?

 int index = (hash & 0x7FFFFFFF) % tab.length;

因为数组索引不可能为负值,所以这里通过逻辑与操作将键的哈希值转换为正值,也就是本质上是为了保证索引为正值,那么 int index = (hash & 0x7FFFFFFF) % tab.length; 是如何计算的呢?0x7FFFFFFF的二进制就是1111111111111111111111111111111,由于是正数所以符号为0即01111111111111111111111111111111,而对于我们添加的值为-100,则二进制为11111111111111111111111110011100,将二者转换为二进制进行逻辑加操作,最终结果为01111111111111111111111110011100,转换为十进制结果为2147483548,这是我们讲解的原理计算方式,实际上我们通过十进制相减即可,上述0x7FFFFFFF的十进制为2147483647,此时我们直接在此基础上减去(100-1)即99,最终得到的也是2147483548。最后取初始容量11的模结果则索引为为1。如果是键的哈希值为正值那就不存在这个问题,也就是说通过逻辑与操作得到的哈希值就是原值。接下来获取对应索引在数组中的位置,然后进行循环,问题来了为何要循环数组呢?也就是如下代码片段:

       for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

上述是为了解决相同键值将对应的值进行覆盖,还是不能理解?我们在控制台再加上一行如下代码:

public static void main(String[] args) {

        Hashtable hashtable = new Hashtable();

        hashtable.put(-100, "first");

        hashtable.put(-100, "second");
}

如上我们添加的键都为-100,通过我们对上述循环源码的分析,此时将如上第一行的值first替换为second,换言之当我们添加相同键时,此时会发生后者的值覆盖前者值的情况,同时我们也可以通过返回值得知,若返回值为空说明没有出现覆盖的情况,否则有返回值,说明存在相同的键且返回被覆盖的值。我们通过如下打印出来Hashtable中数据可得出,这点和C#操作Hashtable不同,若存在相同的键则直接抛出异常。

原文地址:https://www.cnblogs.com/dtydd377455/p/11701558.html

时间: 2024-12-11 22:33:48

Java入门系列之集合Hashtable源码分析(十一)的相关文章

Java入门系列之集合HashMap源码分析(十四)

前言 我们知道在Java 8中对于HashMap引入了红黑树从而提高操作性能,由于在上一节我们已经通过图解方式分析了红黑树原理,所以在接下来我们将更多精力投入到解析原理而不是算法本身,HashMap在Java中是使用比较频繁的键值对数据类型,所以我们非常有必要详细去分析背后的具体实现原理,无论是C#还是Java原理解析,从不打算一行行代码解释,我认为最重要的是设计思路,重要的地方可能会多啰嗦两句. HashMap原理分析 我们由浅入深,循序渐进,首先了解下在HashMap中定义的几个属性,稍后会

RMI 系列(02)源码分析

目录 RMI 系列(02)源码分析 1. 架构 2. 服务注册 2.1 服务发布整体流程 2.2 服务暴露入口 exportObject 2.3 生成本地存根 2.4 服务监听 2.5 ObjectTable 注册与查找 2.6 服务绑定 2.7 总结 3. 服务发现 3.1 注册中心 Stub 3.2 普通服务 Stub RMI 系列(02)源码分析 1. 架构 RMI 中有三个重要的角色:注册中心(Registry).客户端(Client).服务端(Server). 图1 RMI 架构图 在

计划在CSDN学院推出系列视频课程《源码分析教程5部曲》

?? 计划在CSDN学院推出系列视频课程<源码分析教程5部曲> 源码分析教程5部曲之1--漫游C语言 源码分析教程5部曲之2--C标准库概览 源码分析教程5部曲之3--libevent源码分析 源码分析教程5部曲之4--memcached源码分析 源码分析教程5部曲之5--redis源码分析

Java 集合Hashtable源码深入解析

概要 前面,我们已经系统的对List进行了学习.接下来,我们先学习Map,然后再学习Set:因为Set的实现类都是基于Map来实现的(如,HashSet是通过HashMap实现的,TreeSet是通过TreeMap实现的). 首先,我们看看Map架构.如上图:(01) Map 是映射接口,Map中存储的内容是键值对(key-value).(02) AbstractMap 是继承于Map的抽象类,它实现了Map中的大部分API.其它Map的实现类可以通过继承AbstractMap来减少重复编码.(

JAVA的HashTable源码分析

Hashtable简介 Hashtable同样是基于哈希表实现的,同样 每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长. Hashtable也是JDK1.0引入的类,是线程安全的,能用于多线程环境中. Hashtable同样 实现了Serializable接口,它支持序列化,实现了Cloneable接口,能被克隆 . HashTable源码剖析 Hashtable的源码的很多实现都与HashMap差不多,源码如下(加入了比较详细的注

java并发锁ReentrantReadWriteLock读写锁源码分析

1.ReentrantReadWriterLock基础 所谓读写锁,是对访问资源共享锁和排斥锁,一般的重入性语义为 如果对资源加了写锁,其他线程无法再获得写锁与读锁,但是持有写锁的线程,可以对资源加读锁(锁降级):如果一个线程对资源加了读锁,其他线程可以继续加读锁. java.util.concurrent.locks中关于多写锁的接口:ReadWriteLock public interface ReadWriteLock { /** * Returns the lock used for r

JAVA随笔篇一(Timer源码分析和scheduleAtFixedRate的使用)

写完了基础篇,想了很久要不要去写进阶篇,去写JSP等等的使用方法,最后决定先不去写,因为自己并不是JAVA方面的大牛,目前也在边做边学,所以决定先将自己不懂的拿出来学并记下来. Timer是Java自带的java.util.Timer类,通过调度一个java.util.TimerTask任务.这种方式可以让程序按照某一个频度执行. 1.Timer类的源码分析: public class Timer { /** * The timer task queue. This data structure

MyBatis框架的使用及源码分析(十一) StatementHandler

我们回忆一下<MyBatis框架的使用及源码分析(十) CacheExecutor,SimpleExecutor,BatchExecutor ,ReuseExecutor> , 这4个Excecutor执行sql操作的最终都调用了StatementHandler 来执行,我们拿SimpleExecutor来看: public int doUpdate(MappedStatement ms, Object parameter) throws SQLException { Statement st

jQuery 源码分析(十一) 队列模块 Queue详解

队列是常用的数据结构之一,只允许在表的前端(队头)进行删除操作(出队),在表的后端(队尾)进行插入操作(入队).特点是先进先出,最先插入的元素最先被删除. 在jQuery内部,队列模块为动画模块提供基础功能,负责存储动画函数.自动出队并执行动画函数,同时还要确保动画函数的顺序执行. jQuery的静态方法含有如下API: $.queue(elem,type,data) ;返回或修改匹配元素关联的队列,返回最新的队列,参数如下:   elem ;DOM元素或JavaScript对象 type  ;