zoukankan      html  css  js  c++  java
  • 理解Java中HashMap的工作原理

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

    理解hashCode()

    Java中的hashCode()方法,是顶层对象Object中的方法,因此Java中所有的对象都会带有hashCode()方法。 在各种最佳实践中,都会建议在编写自己的类的时候要同时覆盖hashCode()equals()方法, 但是在使用散列的数据结构时(HashMapHashSetLinkedHashSetLinkedHashMap), 如果不为键覆盖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<K, V> extends AbstractMap<K, V> {
        //内部数组的容量
        static final int SIZE = 997;
    
        //buckets数组,内部是一个链表,链表的每一项是Map.Entry形式,保存着HashMap的值
        @SuppressWarnings("unchecked")
        LinkedList<MapEntry<K, V>>[] 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<MapEntry<K, V>> bucket = buckets[index];
                   
            MapEntry<K, V> pair = new MapEntry<>(key, value);
            boolean found = false;
            
            ListIterator<MapEntry<K, V>> it = bucket.listIterator();
            while (it.hasNext()) {
                MapEntry<K, V> 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<K, V> iPair : buckets[index]) {
                if (iPair.getKey().equals(key)) {
                    return iPair.getValue();
                }
            }
            return null;
        }
    
        @Override
        public Set<Map.Entry<K, V>> entrySet() {
            Set<Map.Entry<K, V>> set = new HashSet<>();
            for (LinkedList<MapEntry<K, V>> bucket : buckets) {
                if (bucket == null) {
                    continue;
                }
                for (MapEntry<K, V> mpair : bucket) {
                    set.add(mpair);
                }
            }
            return set;
        }
    
        public static void main(String[] args) {
            SimpleHashMap<String, String> 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()方法的最佳实践:

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

    2. 对于对象中的每个关键域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()
      • 如果该域是一个数组,则要把每一个元素当作单独的域来处理,也就是说递归地应用上述原则
    1. 按照公式: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;
        }
    }

    参考:

  • 相关阅读:
    各种数字证书区别
    微信支付前端对接流程
    ts笔记 流动类型
    支付宝支付前端对接流程
    ts笔记索引签名
    [翻译]Selenium API 命令和操作(JAVA)
    玩一玩yolo目标检测
    快速上手MyBatis
    Swift快速入门
    Windows远程桌面后不能粘贴问题处理
  • 原文地址:https://www.cnblogs.com/chenny3/p/10226171.html
Copyright © 2011-2022 走看看