资讯专栏INFORMATION COLUMN

理解Java中HashMap的工作原理

xiangchaobin / 3374人阅读

摘要:中的使用散列来高效的查找和存储值。理解中的方法是顶层对象中的方法因此中所有的对象都会带有方法。中给出了覆盖方法的最佳实践把某个非零的常数值比如保存在一个名为的类型中。

Java中的HashMap使用散列来高效的查找和存储值。HashMap内部使用Map.Entry的形式来保存key和value,使用put(key,value)方法存储值,使用get(key)方法查找值。

理解hashCode()

Java中的hashCode()方法,是顶层对象Object中的方法,因此Java中所有的对象都会带有hashCode()方法。
在各种最佳实践中,都会建议在编写自己的类的时候要同时覆盖hashCode()equals()方法,但是在使用散列的数据结构时(HashMap,HashSet,LinkedHashSet,LinkedHashMap),
如果不为键覆盖hashCode()equals()方法,将无法正确的处理该键。

hashCode()方法返回一个int值,这个int值就是用这个对象的hashCode()方法产生的hash值。

HashMap的工作原理

在散列表中查找一个值的过程为,先通过键的hashCode()方法计算hash值,然后使用hash值产生下标并使用下标查找数组,这里为什么要用数组呢,因为数组是存储一组元素最快的数据结构,因此使用数组来表示键的信息。

由于数组的容量(也就是表中的桶位数)是固定的,所以不同的键可以产生相同的下标,也就是说,可能会有冲突,因此数组多大就不重要了,任何键总能在数组中找到它的位置。

数组并不直接保存值,因为不同的键可能产生相同的数组下标,数组保存的是LinkedList,因此,散列表的存储结构外层是一个数组,容量固定,数组的每一项都是保存着Entry Object(同时保存key和value)的LinkedList。

由于下标的冲突,不同的键可能会产生相同的bucket location,在使用put(key,value)时,如果两个键产生了相同的bucket location,由于LinkedList的长度是可变的,所以会在该LinkedList中再增加一项Entry Object,其中保存着key和value。

键使用hashCode()方法产生hash值后,利用hash值产生数组的下标,找到值在散列表中的桶位(bucket),也就是在哪一个LinkedList中,如果该桶位只有一个的Object,则返回该Value,如果该桶位有多个Object,那么再对该LinkedList中的Entry Object的键使用equals()方法进行线性的查询,最后找到该键的值并返回。

最后对LinkedList进行线性查询的部分会比较慢,但是,如果散列函数好的话,数组的每个位置就只有较少的值,因此不是查询整个LinkedList,而是快速地跳到数组的某个位置,只对很少的元素进行比较,这就是HashMap会如此快的原因。

在知道了散列的原理后我们可以自己实现一个简单的HashMap(例子来源于《Java编程思想(第四版)》)

public class SimpleHashMap extends AbstractMap {
    //内部数组的容量
    static final int SIZE = 997;

    //buckets数组,内部是一个链表,链表的每一项是Map.Entry形式,保存着HashMap的值
    @SuppressWarnings("unchecked")
    LinkedList>[] buckets = new LinkedList[SIZE];

    public V put(K key, V value) {
        V oldValue = null;
        //使用hashCode()方法产生hash值,使用hash值与数组容量取余获得数组的下标
        int index = Math.abs(key.hashCode()) % SIZE;
        //如果该桶位为null,则插入一个链表
        if (buckets[index] == null) {
            buckets[index] = new LinkedList<>();
        }
        //获得bucket
        LinkedList> bucket = buckets[index];
               
        MapEntry pair = new MapEntry<>(key, value);
        boolean found = false;
        
        ListIterator> it = bucket.listIterator();
        while (it.hasNext()) {
            MapEntry iPair = it.next();
            //对键使用equals()方法线性查询value
            if (iPair.getKey().equals(key)) {
                oldValue = iPair.getValue();
                //找到了键以后更改键原来的value
                it.set(pair);
                found = true;
                break;
            }
        }
        //如果没找到键,在bucket中增加一个Entry
        if (!found) {
            buckets[index].add(pair);
        }
        return oldValue;
    }
    
    //get()与put()的工作方式类似
    @Override
    public V get(Object key) {
        //使用hashCode()方法产生hash值,使用hash值与数组容量取余获得数组的下标
        int index = Math.abs(key.hashCode()) % SIZE;
        if (buckets[index] == null) {
            return null;
        }
        //使用equals()方法线性查找键
        for (MapEntry iPair : buckets[index]) {
            if (iPair.getKey().equals(key)) {
                return iPair.getValue();
            }
        }
        return null;
    }

    @Override
    public Set> entrySet() {
        Set> set = new HashSet<>();
        for (LinkedList> bucket : buckets) {
            if (bucket == null) {
                continue;
            }
            for (MapEntry mpair : bucket) {
                set.add(mpair);
            }
        }
        return set;
    }

    public static void main(String[] args) {
        SimpleHashMap m = new SimpleHashMap<>();
        m.putAll(Countries.capitals(25));
        System.out.println(m);
        System.out.println(m.get("ERITREA"));
        System.out.println(m.entrySet());
    }
}
编写良好的hashCode()方法

如果hashCode()产生的hash值能够让HashMap中的元素均匀分布在数组中,可以提高HashMap的运行效率。
一个良好的hashCode()方法首先是能快速地生成hash值,然后生成的hash值能使HashMap中的元素在数组中尽量均匀的分布,
hash值不一定是唯一的,因为容量是固定的,总会有下标冲突的情况产生。

《Effective Java》中给出了覆盖hashCode()方法的最佳实践:

把某个非零的常数值,比如17,保存在一个名为result的int类型中。

对于对象中的每个关键域f(指equals()方法中涉及的域),完成以下步骤:

为该域计算int类型的散列码c,根据域的类型的不同,又可以分为以下几种情况:

如果该域是boolean类型,则计算(f?1:0)

如果该域是String类型,则使用该域的hashCode()方法

如果该域是byte、char、short或int类型,则计算(int)f

如果该域是long类型,则计算(int)(f^>>>32)

如果该域是float类型,则计算Float.floatToIntBits(f)

如果该域是double类型,则计算Double.doubleToLongBits(f)返回一个long类型的值,再根据long类型的域,生成int类型的散列码

如果该域是一个对象引用,并且该类的equals()方法通过递归调用equals方式来比较这个域,则同样为这个域递归地调用hashCode()

如果该域是一个数组,则要把每一个元素当作多带带的域来处理,也就是说递归地应用上述原则

按照公式:result = 31 * result + c,返回result。

写一个简单的类并用上述的规则来覆盖hashCode()方法

public class SimpleHashCode {
    private static long counter = 0;
    private final long id = counter++;
    private String name;
    
    @Override
    public int hashCode(){
        int result = 17;
        if (name != null){
            result = 31 * result + name.hashCode(); 
        }
        result = result * 31 + (int) id;
        return result;
    }
    
    @Override
    public boolean equals(Object o){
        return o instanceof SimpleHashCode && id == ((SimpleHashCode)o).id;
    }
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/65980.html

相关文章

  • HashMap 工作原理

    摘要:的工作原理是近年来常见的面试题。让我们再来看看这些问题设计哪些知识点的概念中解决碰撞的方法和的应用,以及它们在中的重要性不可变对象的好处多线程的条件竞争重新调整的大小总结的工作原理基于原理,我们通过和方法储存和获取对象。 HashMap 的工作原理是近年来常见的 Java 面试题。几乎每个 Java 程序员都知道 HashMap,都知道哪里要用 HashMap,知道Hashtable和...

    zhisheng 评论0 收藏0
  • java 基础 - 收藏集 - 掘金

    摘要:基础知识复习后端掘金的作用表示静态修饰符,使用修饰的变量,在中分配内存后一直存在,直到程序退出才释放空间。将对象编码为字节流称之为序列化,反之将字节流重建成对象称之为反序列化。 Java 学习过程|完整思维导图 - 后端 - 掘金JVM 1. 内存模型( 内存分为几部分? 堆溢出、栈溢出原因及实例?线上如何排查?) 2. 类加载机制 3. 垃圾回收 Java基础 什么是接口?什么是抽象...

    makeFoxPlay 评论0 收藏0
  • 金三银四,2019大厂Android高级工程师面试题整理

    摘要:原文地址游客前言金三银四,很多同学心里大概都准备着年后找工作或者跳槽。最近有很多同学都在交流群里求大厂面试题。 最近整理了一波面试题,包括安卓JAVA方面的,目前大厂还是以安卓源码,算法,以及数据结构为主,有一些中小型公司也会问到混合开发的知识,至于我为什么倾向于混合开发,我的一句话就是走上编程之路,将来你要学不仅仅是这些,丰富自己方能与世接轨,做好全栈的装备。 原文地址:游客kutd...

    tracymac7 评论0 收藏0

发表评论

0条评论

xiangchaobin

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<