HashMap源码分析笔记

本文详细介绍了HashMap的基本概念、数据结构、源码解析,包括默认容量、负载因子、Entry类以及put、get方法的工作原理,强调了在自定义类作为键时需重写hashCode()和equals()方法的重要性。

以前经常用HashMap,但是对它具体怎么实现一点也不了解,这几天看了看它的源码,整理笔记如下做一个记录,其中有些内容是参考一些大神的博客,鉴于本人功力尚浅,可能有写的不正确的地方,欢迎批评指正。

一 HashMap概述

HashMap是基于散列表的Map接口的实现。它允许插入null键和null值,如果要用自己实现的类作为HashMap的键,则必须同时重写hashCode()和equals()这两个方法。HashMap插入“键值对”的开销是固定的。

此外,HashMap不是线程安全的,多线程环境下可以采用concurrent并发包下的concurrentHashMap可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap

二 HashMap数据结构

学过数据结构的都知道哈希表这种数据结构,它其中一个重要的概念就是构造哈希函数,把关键字转换成数组下标。如果两个不同的关键字哈希化后得到的值相同,这种情况称为冲突。解决冲突的办法通常有两种:开放地址法和链地址法。其中开放地址法通过探测方法找到数组的一个空位来插入数据。链地址法是创建一个存放链表的数组,将发生冲突的数据直接接到这个下标所指的链表中。

HashMap就是基于哈希表这种数据结构,底层通过一个Entry类型(放入HashMap的“键值对”都会封装成Entry对象)的数组来实现,它通过key的hashCode()方法计算hash值,从而确定对象的存储位置,这也是HashMap会特别快的原因。HashMap通过链地址法来解决冲突问题(通过Entry的next来添加,下面的源码分析会进一步分析)。



三 HashMap源码分析

首先看它的一些属性:

    static final int DEFAULT_INITIAL_CAPACITY = 16; //默认初始容量
    static final int MAXIMUM_CAPACITY = 1 << 30;   //最大容量
    static final float DEFAULT_LOAD_FACTOR = 0.75f;  //默认负载因子
    transient Entry<K,V>[] table;   // 存放“键值对”数组
    transient int size;  //实际存放元素的个数
    int threshold;   //阈值 threshold=capacity*loadFactor,当实际存放元素个数size超过阈值需要扩充容量
    final float loadFactor;   //负载因子

      负载因子表示数组可以填满的程度,负载因子很大时,数组的利用率很高,但是也很容易产生冲突,查找的性能也会下降。.因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷。一般情况下就使用默认的0.75。

HashMap的构造函数:

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +  loadFactor);
        int capacity = 1;
         //确保容量为仅大于参数initialCapacity的2的指数次方。这样取是为了可用掩码代替除法和取余数等很慢的操作。这个在后面indexFor(int h, int length)这个方 法中有所体现。
        while (capacity < initialCapacity)
            capacity <<= 1;
        this.loadFactor = loadFactor;
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
    }

其他构造函数如果没有指定 initialCapacity或者 loadFactor,则使用默认的容量16和负载因子0.75 :

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    } 
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

对于Entry类,HashMap将存入的“键值对”都封装成Entry类型的对象

   static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;    //key键
        V value;      //value键
        Entry<K,V> next;      //指向下一个Entry对象,这个属性将有冲突的对象连接起来构成一个链表,解决冲突问题
        int hash;      //散列码
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
    .....
}


对于put方法:

   public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);  //计算key的散列值
        int i = indexFor(hash, table.length);//计算该对象存放的数组位置下标
        //在key散列值对应的数组下标位置的链表中找是否已经有相同的key键
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //如果已经有相同的Key,则value的值用新传入的value值替代
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(hash, key, value, i); //新增的Entry对象的hash值传入的是key的散列值。
        return null;
}

可以发现,将“键值对”封装为Entry对象时,传入的hash值取的是Key的hash值,而Key的hash值是通过hashCode()方法计算的;并且在判断是够已经存在相同的key键时,是通过比较hash的值和equals()方法执行的。所以如果要将自己写的类作为HashMap的key键时,一定要重写hashCode()和equals()方法,如果不重写,则默认使用父类Object的hashCode()和equals(),它默认是使用对象的地址来计算散列码,是否equals也是通过地址来判断。

   private V putForNullKey(V value) {
      //将key键为null值的Entry对象放在数组下标为0的链表中
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
           //如果已经有null值的Key键,则value的值用新传入的value值替代
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return 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);
        size++;
    }

对于下面这个方法,起初只是认为它就是将hash值压缩到数组大小的范围内,后来看了书和一些大神写的博客才发现远不止这么简单。

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

我们一般对哈希表的散列很自然地会想到用hash值对length取模(即除法散列法),这种方法基本能保证元素在哈希表中散列的比较均匀,但取模会用到除法运算,效率很低。之前说HashMap容量取2的整数次幂,这样length为2的整数次幂,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率。其次,length为2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性,而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间,因此,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。


对于get方法:

  public V get(Object key) {
        //如果为Null值的key键,则在table[0]位置的链表中查找
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
   }
    private V getForNullKey() {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }
    final Entry<K,V> getEntry(Object key) {
        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];e != null;e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }
    public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }

可以发现,HashMap查找时,不需要遍历整个数组,它根据散列值快速的调到数组的某个位置,并对很少的元素进行比较就可以完成。



参考文章:http://blog.youkuaiyun.com/ns_code/article/details/36034955

                    http://www.cnblogs.com/liuling/p/2013-8-22-01.html








标题SpringBoot智能在线预约挂号系统研究AI更换标题第1章引言介绍智能在线预约挂号系统的研究背景、意义、国内外研究现状及论文创新点。1.1研究背景与意义阐述智能在线预约挂号系统对提升医疗服务效率的重要性。1.2国内外研究现状分析国内外智能在线预约挂号系统的研究与应用情况。1.3研究方法及创新点概述本文采用的技术路线、研究方法及主要创新点。第2章相关理论总结智能在线预约挂号系统相关理论,包括系统架构、开发技术等。2.1系统架构设计理论介绍系统架构设计的基本原则和常用方法。2.2SpringBoot开发框架理论阐述SpringBoot框架的特点、优势及其在系统开发中的应用。2.3数据库设计与管理理论介绍数据库设计原则、数据模型及数据库管理系统。2.4网络安全与数据保护理论讨论网络安全威胁、数据保护技术及其在系统中的应用。第3章SpringBoot智能在线预约挂号系统设计详细介绍系统的设计方案,包括功能模块划分、数据库设计等。3.1系统功能模块设计划分系统功能模块,如用户管理、挂号管理、医生排班等。3.2数据库设计与实现设计数据库表结构,确定字段类型、主键及外键关系。3.3用户界面设计设计用户友好的界面,提升用户体验。3.4系统安全设计阐述系统安全策略,包括用户认证、数据加密等。第4章系统实现与测试介绍系统的实现过程,包括编码、测试及优化等。4.1系统编码实现采用SpringBoot框架进行系统编码实现。4.2系统测试方法介绍系统测试的方法、步骤及测试用例设计。4.3系统性能测试与分析对系统进行性能测试,分析测试结果并提出优化建议。4.4系统优化与改进根据测试结果对系统进行优化和改进,提升系统性能。第5章研究结果呈现系统实现后的效果,包括功能实现、性能提升等。5.1系统功能实现效果展示系统各功能模块的实现效果,如挂号成功界面等。5.2系统性能提升效果对比优化前后的系统性能
在金融行业中,对信用风险的判断是核心环节之一,其结果对机构的信贷政策和风险控制策略有直接影响。本文将围绕如何借助机器学习方法,尤其是Sklearn工具包,建立用于判断信用状况的预测系统。文中将涵盖逻辑回归、支持向量机等常见方法,并通过实际操作流程进行说明。 一、机器学习基本概念 机器学习属于人工智能的子领域,其基本理念是通过数据自动学习规律,而非依赖人工设定规则。在信贷分析中,该技术可用于挖掘历史数据中的潜在规律,进而对未来的信用表现进行预测。 二、Sklearn工具包概述 Sklearn(Scikit-learn)是Python语言中广泛使用的机器学习模块,提供多种数据处理和建模功能。它简化了数据清洗、特征提取、模型构建、验证与优化等流程,是数据科学项目中的常用工具。 三、逻辑回归模型 逻辑回归是一种常用于分类任务的线性模型,特别适用于二类问题。在信用评估中,该模型可用于判断借款人是否可能违约。其通过逻辑函数将输出映射为0到1之间的概率值,从而表示违约的可能性。 四、支持向量机模型 支持向量机是一种用于监督学习的算法,适用于数据维度高、样本量小的情况。在信用分析中,该方法能够通过寻找最佳分割面,区分违约与非违约客户。通过选用不同核函数,可应对复杂的非线性关系,提升预测精度。 五、数据预处理步骤 在建模前,需对原始数据进行清理与转换,包括处理缺失值、识别异常点、标准化数值、筛选有效特征等。对于信用评分,常见的输入变量包括收入水平、负债比例、信用历史记录、职业稳定性等。预处理有助于减少噪声干扰,增强模型的适应性。 六、模型构建与验证 借助Sklearn,可以将数据集划分为训练集和测试集,并通过交叉验证调整参数以提升模型性能。常用评估指标包括准确率、召回率、F1值以及AUC-ROC曲线。在处理不平衡数据时,更应关注模型的召回率与特异性。 七、集成学习方法 为提升模型预测能力,可采用集成策略,如结合多个模型的预测结果。这有助于降低单一模型的偏差与方差,增强整体预测的稳定性与准确性。 综上,基于机器学习的信用评估系统可通过Sklearn中的多种算法,结合合理的数据处理与模型优化,实现对借款人信用状况的精准判断。在实际应用中,需持续调整模型以适应市场变化,保障预测结果的长期有效性。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值