【小家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不同的关键所在。


相关文章
|
14天前
|
存储 安全 Java
Java 集合框架中的老炮与新秀:HashTable 和 HashMap 谁更胜一筹?
嗨,大家好,我是技术伙伴小米。今天通过讲故事的方式,详细介绍 Java 中 HashMap 和 HashTable 的区别。从版本、线程安全、null 值支持、性能及迭代器行为等方面对比,帮助你轻松应对面试中的经典问题。HashMap 更高效灵活,适合单线程或需手动处理线程安全的场景;HashTable 较古老,线程安全但性能不佳。现代项目推荐使用 ConcurrentHashMap。关注我的公众号“软件求生”,获取更多技术干货!
34 3
|
2月前
|
Java
Java之HashMap详解
本文介绍了Java中HashMap的源码实现(基于JDK 1.8)。HashMap是基于哈希表的Map接口实现,允许空值和空键,不同步且线程不安全。文章详细解析了HashMap的数据结构、主要方法(如初始化、put、get、resize等)的实现,以及树化和反树化的机制。此外,还对比了JDK 7和JDK 8中HashMap的主要差异,并提供了使用HashMap时的一些注意事项。
108 2
Java之HashMap详解
|
2月前
|
安全
HashTable与HashMap的区别
(1)HashTable的每个方法都用synchronized修饰,因此是线程安全的,但同时读写效率很低 (2)HashTable的Key不允许为null (3)HashTable只对key进行一次hash,HashMap进行了两次Hash (4)HashTable底层使用的数组加链表HashTable与HashMap的区别
33 2
|
19天前
|
存储 安全 Java
如何优雅地回答HashSet与HashMap的区别?看这里!
哈喽,大家好!我是小米,29岁程序员。本文聚焦Java开发中经典的面试题——HashSet和HashMap的区别。HashSet基于HashMap实现,存储唯一值;HashMap存储键值对。两者在数据结构、使用场景、操作方法等方面有显著差异。HashSet无序且依赖元素的hashCode和equals方法保证唯一性,而HashMap需注意线程安全问题。掌握这些知识点,助你轻松应对面试。更多技术干货,欢迎关注我的微信公众号“软件求生”。
33 4
|
2月前
|
Java 程序员
Java社招面试题:& 和 && 的区别,HR的套路险些让我翻车!
小米,29岁程序员,分享了一次面试经历,详细解析了Java中&和&&的区别及应用场景,展示了扎实的基础知识和良好的应变能力,最终成功获得Offer。
91 14
|
1月前
|
Java
java中面向过程和面向对象区别?
java中面向过程和面向对象区别?
27 1
|
2月前
|
存储 缓存 安全
java 中操作字符串都有哪些类,它们之间有什么区别
Java中操作字符串的类主要有String、StringBuilder和StringBuffer。String是不可变的,每次操作都会生成新对象;StringBuilder和StringBuffer都是可变的,但StringBuilder是非线程安全的,而StringBuffer是线程安全的,因此性能略低。
70 8
|
2月前
|
Java
Java代码解释++i和i++的五个主要区别
本文介绍了前缀递增(++i)和后缀递增(i++)的区别。两者在独立语句中无差异,但在赋值表达式中,i++ 返回原值,++i 返回新值;在复杂表达式中计算顺序不同;在循环中虽结果相同但使用方式有别。最后通过 `Counter` 类模拟了两者的内部实现原理。
Java代码解释++i和i++的五个主要区别
|
2月前
|
存储 Java API
Java交换map的key和value值
通过本文介绍的几种方法,可以在Java中实现Map键值对的交换。每种方法都有其优缺点,具体选择哪种方法应根据实际需求和场景决定。对于简单的键值对交换,可以使用简单遍历法或Java 8的Stream API;对于需要处理值不唯一的情况,可以使用集合存储或Guava的Multimap。希望本文对您理解和实现Java中的Map键值对交换有所帮助。
51 1
|
3月前
|
存储 Java 程序员
Java面试加分点!一文读懂HashMap底层实现与扩容机制
本文详细解析了Java中经典的HashMap数据结构,包括其底层实现、扩容机制、put和查找过程、哈希函数以及JDK 1.7与1.8的差异。通过数组、链表和红黑树的组合,HashMap实现了高效的键值对存储与检索。文章还介绍了HashMap在不同版本中的优化,帮助读者更好地理解和应用这一重要工具。
81 5