【小家java】Java中IdentityHashMap使用详解---允许key重复(阐述和HashMap的区别)

简介: 【小家java】Java中IdentityHashMap使用详解---允许key重复(阐述和HashMap的区别)

应该有很多人不知道IdentityHashMap的存在,其中不乏工作很多年的Java开发者,会有很多人以为这是第三方jar包,实际上它是Jdk源码自带的集合类。


本文主要讲解IdentityHashMap的使用和他的一些特性。很多场景上使用它,会让你事半功倍。


对Map的认识


其实我们对Map都有一个通用认知:只要key相同,就不能重复往里面put,但是你真的了解**“相同”**这两个字吗?看下面这个例子吧:


 public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        map.put("a", "1");
        map.put("a", "2");
        map.put("a", "3");
        System.out.println(map.size()); //1
        Map<String, String> hashMap = new HashMap<>();
        hashMap.put(new String("a"), "1");
        hashMap.put(new String("a"), "2");
        hashMap.put(new String("a"), "3");
        System.out.println(hashMap.size()); //1
        Map<Integer, String> hashMap2 = new HashMap<>();
        hashMap2.put(new Integer(200), "1");
        hashMap2.put(new Integer(200), "2");
        hashMap2.put(new Integer(200), "3");
        System.out.println(hashMap2.size()); //1
        Map<Demo, String> hashMap3 = new HashMap<>();
        hashMap3.put(new Demo(1), "1");
        hashMap3.put(new Demo(1), "2");
        hashMap3.put(new Demo(1), "3");
        System.out.println(hashMap3.size()); //3
    }


从结果中,你是否感觉到了惊讶?

如果是:那证明你还不是真的了解HashMap

如果不是:那你对底层的了解还是比较透彻的


不管怎么样,我给出下面两段源码,给与解释:

containsKey和get的源码:

public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }
public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }


我们发现,它底层其实都调用了一个getNode方法,关键在于key上面的hash方法,因此我们主要看看这个hash方法:


static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

发现,最终的返回值取决于key.hashCode()方法。好了,问题的答案已经若影若现了。我们发现key是否相同,取决于HashCode是否相等。


可能有人对上面的还有输出结构还有疑问:我的key明明是new出来的,为什么size还是成为了1呢????


这里我贴出两段源码,大家应该就能明白了:

String和Interger的HashCode方法源码:


public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            hash = h = isLatin1() ? StringLatin1.hashCode(value)
                                  : StringUTF16.hashCode(value);
        }
        return h;
    }


@Override
    public int hashCode() {
        return Integer.hashCode(value);
    }


我们发现他俩都重写了此方法,并且只和value值有关。因此只要内容相同,他们的hashCode就是相等的,这就是为什么平时我们都乐意用它们来作键而不会出问题的最根本原因。


而普通对象使用的父类Object的HashCode方法,是个native方法,与地址值有关,因此new出来的对象肯定不是同一个key了。


Map中put方法到底是和hahCode()和equals()什么关系呢?


针对于上面的例子,我们重写Demo类的equals方法如下:


@Override
        public boolean equals(Object obj) {
            return true;
        }


再执行:

 Map<Demo, String> hashMap3 = new HashMap<>();
        hashMap3.put(new Demo(1), "1");
        hashMap3.put(new Demo(1), "2");
        hashMap3.put(new Demo(1), "3");
        System.out.println(hashMap3.size()); //3


我们发现输出的结果还是3。

我们再重写HashCode方法:

@Override
        public boolean equals(Object obj) {
            return true;
        }
        @Override
        public int hashCode() {
            return id;
        }


继续运行上面的方法。结果这次肯定在我们意料之中-----输出的结果为1。

那么我们控制变量法,去掉equals的重写,只保留hashCode试试:

 @Override
        public int hashCode() {
            return id;
        }


输出结果:3


通过精读put方法的源码,我们得出put方法的步骤:


1.通过新key的hashCode()方法,计算出哈希码,然后从Node数组中找到对应的位置,若为null就放进去。若已经有值了,请看第二步


2.调用新key的equals()方法去和已经存在的key比较,如果返回ture 。则视新键与已经存在的键相同,用新值去更新旧值,然后put方法返回旧值

对应源码:

if (p.hash == hash &&
   ((k = p.key) == key || (key != null && key.equals(k)))
){
  // ...
}


若调用equals()返回false,则认为新键和已存在的键不一样,那就会新建一个Node节点,放在此链表里

HashMap的put()方法返回null的特殊情况:

一:要是已经存在键的映射,但是值是null,那么调用put()方法再更新键的值时, put()方法会把旧值null返回(因为旧值为null,所以很特殊)

二:要是找到的位置上没有键的映射,put()方法也是返回null


IdentityHashMap


顾名思义,它允许"自己"相同的key保存进来,因此又一个相同二字。直接看例子


   public static void main(String[] args) {
        //IdentityHashMap使用===================================
        Map<String, String> identityHashMap = new IdentityHashMap<>();
        identityHashMap.put(new String("a"), "1");
        identityHashMap.put(new String("a"), "2");
        identityHashMap.put(new String("a"), "3");
        System.out.println(identityHashMap.size()); //3
        Map<Demo, String> identityHashMap2 = new IdentityHashMap<>();
        identityHashMap2.put(new Demo(1), "1");
        identityHashMap2.put(new Demo(1), "2");
        identityHashMap2.put(new Demo(1), "3");
        System.out.println(identityHashMap2.size()); //3
    }


备注,此时的Demo类没有复写任何方法。从结果我们可以看出,它好像违背了Map的规则,把相同的key保存进去了。


是的,这就是它最大的特性之一。因此对应的,我们看看get方法结果


System.out.println(identityHashMap.get("a")); //null
System.out.println(identityHashMap2.get(new Demo(1))); //null


得到的是两个大大的null。那么我们继续针对于Demo类,重写eq和hashCode方法如下:


 private static class Demo {
        private Integer id;
        public Demo(Integer id) {
            this.id = id;
        }
        @Override
        public boolean equals(Object obj) {
            return true;
        }
        @Override
        public int hashCode() {
            return id;
        }
    }



重新执行get方法,我们发现,得到的还是null。所以它竟然与eq和HashCode方法都木有关系哟。为了解释这个问题,我插播一个小例子:


Java中==,到底比较的什么?


 public static void main(String[] args) {
        Demo demo1 = new Demo(1);
        Demo demo2 = new Demo(1);
        System.out.println(demo1 == demo2); //false
        System.out.println(demo1.hashCode()); //1
        System.out.println(demo2.hashCode()); //1
        System.out.println(System.identityHashCode(demo1)); //998351292
        System.out.println(System.identityHashCode(demo2)); //1684106402
    }


从这个例子中,我们能够得出结论:


==比较的是地址值,而不是HashCode,所以这里以后千万不要掉进误区了。


而我们的IdentityHashMap,比较key值,直接使用的是==,因此上面例子出现的结果,我们自然而然的就能够理解了。so,下面这句输出:


   public static void main(String[] args) {
        Demo demo1 = new Demo(1);
        Demo demo2 = new Demo(1);
        Map<Demo, String> identityHashMap = new IdentityHashMap<>();
        identityHashMap.put(demo1,"demo1");
        identityHashMap.put(demo2,"demo2");
        System.out.println(identityHashMap.get(demo1)); //demo1
    }


不再输出null,而是能够get到值了。


最后


1.比如对于要保存的key,k1和k2,当且仅当k1== k2的时候,IdentityHashMap才会相等,而对于HashMap来说,相等的条件则是:对比两个key的hashCode等


2.IdentityHashMap不是Map的通用实现,它有意违反了Map的常规协定。并且IdentityHashMap允许key和value都为null。


3.同HashMap,IdentityHashMap也是无序的,并且该类不是线程安全的,如果要使之线程安全,可以调用Collections.synchronizedMap(new IdentityHashMap(…))方法来实现。


注意:


    IdentityHashMap重写了equals和hashcode方法,不过需要注意的是hashCode方法并不是借助Object的hashCode来实现的,而是通过System.identityHashCode方法来实现的。


    hashCode的生成是与key和value都有关系的,这就间接保证了key和value这对数据具备了唯一的hash值。同时通过重写equals方法,判定只有key值全等情况下才会判断key值相等。这就是IdentityHashMap与普通HashMap不同的关键所在。


相关文章
|
2月前
|
存储 缓存 人工智能
Java int和Integer的区别
本文介绍了Java中int与Integer的区别及==与equals的比较机制。Integer是int的包装类,支持null值。使用==比较时,int直接比较数值,而Integer比较对象地址;在-128至127范围内的Integer值可缓存,超出该范围或使用new创建时则返回不同对象。equals方法则始终比较实际数值。
|
28天前
|
Java 测试技术
Java浮点类型详解:使用与区别
Java中的浮点类型主要包括float和double,它们在内存占用、精度范围和使用场景上有显著差异。float占用4字节,提供约6-7位有效数字;double占用8字节,提供约15-16位有效数字。float适合内存敏感或精度要求不高的场景,而double精度更高,是Java默认的浮点类型,推荐在大多数情况下使用。两者都存在精度限制,不能用于需要精确计算的金融领域。比较浮点数时应使用误差范围或BigDecimal类。科学计算和工程计算通常使用double,而金融计算应使用BigDecimal。
433 28
|
3月前
|
存储 Java C语言
Java List 复制:浅拷贝与深拷贝方法及区别
我是小假 期待与你的下一次相遇 ~
293 1
|
2月前
|
安全 算法 Java
Java 中 synchronized 与 AtomicInteger 的区别
在Java多线程编程中,`synchronized`和`AtomicInteger`均用于实现线程安全,但原理与适用场景不同。`synchronized`是基于对象锁的同步机制,适用于复杂逻辑和多变量同步,如银行转账;而`AtomicInteger`采用CAS算法,适合单一变量的原子操作,例如计数器更新。二者各有优劣,应根据具体需求选择使用。
72 0
|
3月前
|
存储 安全 Java
Java 集合面试题从数据结构到 HashMap 源码剖析详解及长尾考点梳理
本文深入解析Java集合框架,涵盖基础概念、常见集合类型及HashMap的底层数据结构与源码实现。从Collection、Map到Iterator接口,逐一剖析其特性与应用场景。重点解读HashMap在JDK1.7与1.8中的数据结构演变,包括数组+链表+红黑树优化,以及put方法和扩容机制的实现细节。结合订单管理与用户权限管理等实际案例,展示集合框架的应用价值,助你全面掌握相关知识,轻松应对面试与开发需求。
187 3
|
3月前
|
算法 Java 数据库连接
Java 与 C++ 区别深入剖析及应用实例详解
本文深入剖析了Java和C++两种编程语言的区别,从编译与执行机制、面向对象特性、数据类型与变量、内存管理、异常处理等方面进行对比,并结合游戏开发、企业级应用开发、操作系统与嵌入式开发等实际场景分析其特点。Java以跨平台性强、自动内存管理著称,适合企业级应用;C++则因高性能和对硬件的直接访问能力,在游戏引擎和嵌入式系统中占据优势。开发者可根据项目需求选择合适语言,提升开发效率与软件质量。附面试资料链接:[点此获取](https://pan.quark.cn/s/4459235fee85)。
251 0
|
4月前
|
Java
Java 中 Exception 和 Error 的区别
在 Java 中,`Exception` 和 `Error` 都是 `Throwable` 的子类,用于表示程序运行时的异常情况。`Exception` 表示可被捕获和处理的异常,分为受检异常(Checked)和非受检异常(Unchecked),通常用于程序级别的错误处理。而 `Error` 表示严重的系统级问题,如内存不足或 JVM 错误,一般不建议捕获和处理。编写程序时应重点关注 `Exception` 的处理,确保程序稳定性。
109 0
|
7月前
|
存储 缓存 安全
Java HashMap详解及实现原理
Java HashMap是Java集合框架中常用的Map接口实现,基于哈希表结构,允许null键和值,提供高效的存取操作。它通过哈希函数将键映射到数组索引,并使用链表或红黑树解决哈希冲突。HashMap非线程安全,多线程环境下需注意并发问题,常用解决方案包括ConcurrentHashMap和Collections.synchronizedMap()。此外,合理设置初始化容量和加载因子、重写hashCode()和equals()方法有助于提高性能和避免哈希冲突。
390 17
Java HashMap详解及实现原理
|
5月前
|
Java 编译器 程序员
java中重载和多态的区别
本文详细解析了面向对象编程中多态与重载的概念及其关系。多态是OOP的核心,分为编译时多态(静态多态)和运行时多态(动态多态)。编译时多态主要通过方法重载和运算符重载实现,如Java中的同名方法因参数不同而区分;运行时多态则依赖继承和方法重写,通过父类引用调用子类方法实现。重载是多态的一种形式,专注于方法签名的多样性,提升代码可读性。两者结合增强了程序灵活性与扩展性,帮助开发者更好地实现代码复用。
201 0
|
8月前
|
Java 程序员 调度
Java 高级面试技巧:yield() 与 sleep() 方法的使用场景和区别
本文详细解析了 Java 中 `Thread` 类的 `yield()` 和 `sleep()` 方法,解释了它们的作用、区别及为什么是静态方法。`yield()` 让当前线程释放 CPU 时间片,给其他同等优先级线程运行机会,但不保证暂停;`sleep()` 则让线程进入休眠状态,指定时间后继续执行。两者都是静态方法,因为它们影响线程调度机制而非单一线程行为。这些知识点在面试中常被提及,掌握它们有助于更好地应对多线程编程问题。
329 9