【JAVA 基础】IdentityHashMap和HashMap的区别---允许key重复

对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)))
){
	// ...
}
  1. 若调用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不同的关键所在。
posted @ 2021-04-10 13:07  satire  阅读(336)  评论(0)    收藏  举报