HashMap简介以及hashCode写法的建议

映射表Map

Map也叫映射表或者字典,Map中存储的元素是一个键值对key-value,他们共同包装在Entry<K,V>对象中。我们能通过key直接获取value,就像查字典一样。

JDK中,Map有两种实现方式,一是散列技术,二是红黑树。


常见的Map实现

HashMap通过散列技术实现。Map中的key对象必须定义equals以及hashCode方法。该容器的访问效率很高,几乎等同于数组。一般情况下,没有特殊需求,这应该是默认选项。

LinkedHashMapHashMap的子类,性能略差于HashMap。相比HashMap,该容器能以插入的顺序遍历元素。实现原理是使用链表将各个Node串起来,因此,其迭代效率比HashMap更加高效。他的key要求同HashMap一样。

TreeMap通过红黑树结构实现,因此,其内元素是有序的。元素的key对象必须实现Comparable接口,这样红黑树才能对元素进行排序。由于TreeMap是有序的,也就多出了一些与有序性相关的方法,比如firstKeylastKeysubMap(fromKey,toKey)headMap(toKey)等等。他的查询效率比散列低很多,为\(log(n)\)。

ConcurrentHashMap是一种线程安全的HashMap,主要通过synchronized同步块和CAS的方式实现,不涉及同步锁

WeakHashMap中,如果某个键没有被引用,那么该键可以被GC回收。用于特殊用途。

IdentityHashMap,在进行对比key的时候,使用==而不是equals。用于特殊用途。

小结一下,散列的效率比红黑树要高,但是红黑树的元素是有序的。使用散列Map的key必须覆盖equalshashCode,使用红黑树Map必须实现Comparable接口


散列技术与hashCode

前面说过,Map的实现方式之一是利用散列技术,HashMapLinkedHashMap都使用的散列技术。

数组的索引访问时间为$O(1)$,因为数组的内存是连续的且类型固定。因此,只要获取数组首地址和偏移量(索引),即可直接计算出元素的地址。

散列就是利用数组这一特点,如图1,他将数组作为存储元素的基础。通过使用给定函数$f(n)$计算出key的hashcode,之后再对hashcode二次处理,一般是对数组长度取模,便可得到数组的索引。

另外,不同的key可以计算出相同的hashcode,但是散列函数最好能做到散列值均匀分布在数组中。最差的情况是所有散列值都一样,这将严重拖垮Map的存取速度。

综上,我们可以这样描述HashMap的存取过程。

在调用put(key,value)时,key-value首先会包装在Node<K,V>中。对key调用hash(key)计算散列值,之后散列值对数组长度取模,算得一个数组的索引位置,称为插槽(slot)。插槽位不装实际的数据,而是哨兵节点head。之后每一个相同hashCode的Node都会插入该head的链表中。同时,为了优化查询速度,当链表节点过多的时候,会将链表转化为红黑树。

注意,当元素数超过\(arr-length*load-factor\)的时候,会触发Map的resize。这时候,数组拓展一倍,所有的元素重新散列。因此,这也是HashMap中最损害性能的一部分。为了减少这种情况的发生,最好在最初就对Map的容量需求有个大致的估计。

查询即get(key)的过程和put是相似的。首先通过hashCode拿到元素的插槽位,然后遍历链表或者红黑树,通过equals方法逐个对比。这也是,为什么散列一定要覆盖hashCodeequals的原因。hashCodeequals必须唯一确定一个元素。


图1 HashMap的数据结构


hashCode的写法

前面说过,散列的核心步骤就是计算key的hashCode,因此设计好的hash方法也就非常的必要了。

  • hashCode的结果具有一致性,即无论何时计算得到的结果都一样。这就要求,计算hashCode依赖的值是不可变的
  • hashCode不该依赖具有唯一性的对象信息,如果hashCode依赖的信息每个对象只此一份,我们就永远无法在外部创造一个相同的key来获取value了
  • hashCode必须基于对象的内容生成散列码
  • hashCode产生的散列码最好能均匀分布

以上hashCode设计的几点原则,一下给出Josh Bloch给出的重写hashCode的建议

域类型 计算
boolean c = (f? 0 : 1)
byte/char/short/int c = (int)f
long c = (int)(f ^ f>>>32)
float c = Float.floatToIntBits(f)
double long l = Double.doubleToLongBits(f); c = (int)(l ^ l>>>32)
Object c = f.hashCode()
数组 每个元素使用以上规则

以上是基本类型和对象转换hashCode的方法,当我们计算一个对象hashCode的时候因该将他的每一个域(不可变)都考虑进去。将result初始化为17,result乘以37再加上当前域的hash值再赋予result,即:

result = 37 * result +c

比如我们自定义对象CountingString作为key,hashCode便如下定义

public class CountedString {
    private static List<String> created = new ArrayList<>();

    private String s;
    private int id = 0;

    public CountedString(String s) {
        this.s = s;
        created.add(s);
        for (String ss : created) {
            if (ss.equals(s))//如果s,则他们的id编号必不相同
                id++;
        }
    }

    @Override
    public String toString() {
        return "[s:"+s+"],[id:"+id+"],[hashCode:"+hashCode()+"]";
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = result * 37 + s.hashCode();
        result = result * 37 + id;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        return obj instanceof CountedString &&
                (s.equals(((CountedString) obj).s)) &&
                (id == ((CountedString) obj).id);
    }
}

借助框架

最后,实现好hashCode和equals是很需要技巧的,我们可以借助Apache Commons3等一类的框架,他们都自带了不错的工具。

参考

  • Effective Java 3
  • Java编程思想

原文地址:https://www.cnblogs.com/Franken-Fran/p/hashmap_into.html

时间: 2024-08-07 22:50:36

HashMap简介以及hashCode写法的建议的相关文章

HashMap 简介

HashMap 简介 HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一. JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间. 底层数据结构分析 JDK1.8之前 JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也

关于hashMap中 计算hashCode的逻辑推理(二)

hashMap中,为了使元素在数组中尽量均匀的分布,所以使用取模的算法来决定元素的位置.如下: 1 //方法一: 2 static final int hash(Object key){//jdk1.8 3 int h; 4 return (key == null) ? 0 : h = key.hashCode() ^ (h >>> 16); 5 } 6 //方法二: 7 static int indexFor(int h,int length){//低版本的源码 8 return h

10条影响CSS渲染速度的写法与建议(摘抄HTML5中国)

1.尽量避免 *{} 由于不同浏览器对HTML标签的解释有差异,所以最终的网页效果在不同的浏览器中可能是不一样的,为了消除这方面的风险,设计者通常会在CSS的一开始就把所有的默认属性全部去除,以达到所有标签属性值都统一的效果.所以就有了*通配符.* 会遍历所有的标签: *{ margin:0;padding:0} 建议的解决方法: (1)不要去使用生僻的标签,因为这些标签在不同浏览器中解释出来的效果不一样:所以要尽可能的去使用那些常用的标签: (2)不要使用*:而是把常用到的这些标签进行处理:例

10条影响CSS渲染速度的写法与建议

1.*{} #zishu *{} 尽量避开 由于不同浏览器对HTML标签的解释有差异,所以最终的网页效果在不同的浏览器中可能是不一样的,为了消除这方面的风险,设计者通常会在CSS的一个始就把所有标签的默认属性全部去除,以达到所有签标属性值都统一的效果.所以就有了*通配符.*会遍历所有的标签: *{margin:0; padding:0} 建议的的解决办法: 1)不要去使用生僻的标签,因为这些标签往往在不同浏览器中解释出来的效果不一样:所以你要尽可能的去使用那些常用的标签: 2)不要使用*:而是把

HashMap和HashTable简介和区别

一.HashMap简介 HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长. HashMap是非线程安全的,只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的concurrentHashMap. HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆. HashMap存数据的过程是: HashMap内部维护了一个存储数据的Entr

HashMap源码及原理解析

1.HashMap简介 HashMap提供所有可选的Map操作,并允许使用 null 值和 null 键,是线程不安全的.(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同.)此类不保证映射的顺序,特别是它不保证该顺序恒久不变. HashMap的实例有两个参数影响其性能:初始容量 和加载因子.容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量.加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度.当哈希表中的条目(或者说元素)数超出了加载因

HashMap源码分析(JDK1.8)

一.HashMap简介 HashMap是一种基于数组+链表+红黑树的数据结构,其中红黑树部分在JDK1.8后引入,当链表长度大于8的时候转换为红黑树. HashMap继承于AbstractMap(Map的骨架实现类),实现Map接口. HashMap因为采用hashCode的值存储,所以性能一般情况下为O(1).   HashMap最多只允许一条记录的键为null,允许多条记录的值为null. HashMap线程不安全,如在多线程环境下可以使用Collections工具类将其转换为线程安全,也可

java 中的 equals、==与hashcode

一.== 详解 1.简单的背景 Java中一切都是对象,在程序运行时,每个对象的存储位置有以下几个选择: 1)寄存器:速度最快,容量最小,在Java中存储器是完全透明的--无法控制也无法建议编译器将某个对象存入存储器中: 2)堆栈:位于RAM中,通过堆栈指针可以获得这个区域在内存中的地址,可以通过控制堆栈指针的加减实现存储的分配.在创建程序时,Java系统必须知道所有存储在堆栈的项目的确切生命周期以便控制堆栈指针的移动.基本类型的对象和其他对象的对象引用存储在这一区域. 3)堆:堆是一种通用的内

java集合之hashmap

第1部分 HashMap介绍 HashMap简介 HashMap 的实现不是同步的,这意味着它不是线程安全的.它的key.value都可以为null.此外,HashMap中的映射不是有序的. HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”.容量是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量.加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度.当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构