HashMap与hashCode以及equals

来源:互联网 发布:剑三成男脸型数据网盘 编辑:程序博客网 时间:2024/04/30 11:29

http://blog.csdn.net/kobejayandy/article/details/16370027

热心的读者贡献了更多的关于HashMap的问题:

  1. 为什么String, Interger这样的wrapper类适合作为键? String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。
  2. 我们可以使用自定义的对象作为键吗? 这是前一个问题的延伸。当然你可能使用任何对象作为键,只要它遵守了equals()和hashCode()方法的定义规则,并且当对象插入到Map中之后将不会再改变了。如果这个自定义对象时不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了。
  3. 我们可以使用CocurrentHashMap来代替HashTable吗?这是另外一个很热门的面试题,因为ConcurrentHashMap越来越多人用了。我们知道HashTable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。看看这篇博客查看Hashtable和ConcurrentHashMap的区别。

我个人很喜欢这个问题,因为这个问题的深度和广度,也不直接的涉及到不同的概念。让我们再来看看这些问题设计哪些知识点:

  • hashing的概念
  • HashMap中解决碰撞的方法
  • equals()和hashCode()的应用,以及它们在HashMap中的重要性
  • 不可变对象的好处
  • HashMap多线程的条件竞争
  • 重新调整HashMap的大小

总结

HashMap的工作原理

HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用LinkedList来解决碰撞问题,当发生碰撞了,对象将会储存在LinkedList的下一个节点中。 HashMap在每个LinkedList节点中储存键值对对象。

当两个不同的键对象的hashcode相同时会发生什么? 它们会储存在同一个bucket位置的LinkedList中。键对象的equals()方法用来找到键值对。

因为HashMap的好处非常多,我曾经在电子商务的应用中使用HashMap作为缓存。因为金融领域非常多的运用Java,也出于性能的考虑,我们会经常用到HashMap和ConcurrentHashMap。你可以查看更多的关于HashMap和HashTable的文章。


HashSet和HashMap一直都是JDK中最常用的两个类,HashSet要求不能存储相同的对象,HashMap要求不能存储相同的键。 
那么Java运行时环境是如何判断HashSet中相同对象、HashMap中相同键的呢?当存储了“相同的东西”之后Java运行时环境又将如何来维护呢? 

在研究这个问题之前,首先说明一下JDK对equals(Object obj)和hashcode()这两个方法的定义和规范: 
在Java中任何一个对象都具备equals(Object obj)和hashcode()这两个方法,因为他们是在Object类中定义的。 
equals(Object obj)方法用来判断两个对象是否“相同”,如果“相同”则返回true,否则返回false。 
hashcode()方法返回一个int数,在Object类中的默认实现是“将该对象的内部地址转换成一个整数返回”。 
接下来有两个个关于这两个方法的重要规范(我只是抽取了最重要的两个,其实不止两个): 
规范1:若重写equals(Object obj)方法,有必要重写hashcode()方法,确保通过equals(Object obj)方法判断结果为true的两个对象具备相等的hashcode()返回值。说得简单点就是:“如果两个对象相同,那么他们的hashcode应该 相等”。不过请注意:这个只是规范,如果你非要写一个类让equals(Object obj)返回true而hashcode()返回两个不相等的值,编译和运行都是不会报错的。不过这样违反了Java规范,程序也就埋下了BUG。 
规范2:如果equals(Object obj)返回false,即两个对象“不相同”,并不要求对这两个对象调用hashcode()方法得到两个不相同的数。说的简单点就是:“如果两个对象不相同,他们的hashcode可能相同”。 
根据这两个规范,可以得到如下推论: 
1、如果两个对象equals,Java运行时环境会认为他们的hashcode一定相等。 
2、如果两个对象不equals,他们的hashcode有可能相等。 
3、如果两个对象hashcode相等,他们不一定equals。 
4、如果两个对象hashcode不相等,他们一定不equals。 

这样我们就可以推断Java运行时环境是怎样判断HashSet和HastMap中的两个对象相同或不同了。我的推断是:先判断hashcode是否相等,再判断是否equals。 


测试程序如下:首先我们定义一个类,重写hashCode()和equals(Object obj)方法 

Java代码
[java] view plaincopy
  1. class A {    
  2.     
  3.     @Override    
  4.     public boolean equals(Object obj) {    
  5.         System.out.println("判断equals");    
  6.         return false;    
  7.     }    
  8.     
  9.     @Override    
  10.     public int hashCode() {    
  11.         System.out.println("判断hashcode");    
  12.         return 1;    
  13.     }    
  14. }    
 

然后写一个测试类,代码如下: 

Java代码
[java] view plaincopy
  1. public class Test {    
  2.     
  3.     public static void main(String[] args) {    
  4.         Map<A,Object> map = new HashMap<A, Object>();    
  5.         map.put(new A(), new Object());    
  6.         map.put(new A(), new Object());    
  7.             
  8.         System.out.println(map.size());    
  9.     }    
  10.         
  11. }    
 

运行之后打印结果是: 

判断hashcode 
判断hashcode 
判断equals 
2
 

可以看出,Java运行时环境会调用new A()这个对象的hashcode()方法。其中: 
打印出的第一行“判断hashcode”是第一次map.put(new A(), new Object())所打印出的。 
接下来的“判断hashcode”和“判断equals”是第二次map.put(new A(), new Object())所打印出来的。 

那么为什么会是这样一个打印结果呢?我是这样分析的: 
1、当第一次map.put(new A(), new Object())的时候,Java运行时环境就会判断这个map里面有没有和现在添加的 new A()对象相同的键,判断方法:调用new A()对象的hashcode()方法,判断map中当前是不是存在和new A()对象相同的HashCode。显然,这时候没有相同的,因为这个map中都还没有东西。所以这时候hashcode不相等,则没有

[java] view plaincopy
  1. import java.util.HashMap;    
  2. import java.util.Map;    
  3.     
  4.     
  5. class A {    
  6.     
  7.     @Override    
  8.     public boolean equals(Object obj) {    
  9.         System.out.println("判断equals");    
  10.         return true;    
  11.     }    
  12.     
  13.     @Override    
  14.     public int hashCode() {    
  15.         System.out.println("判断hashcode");    
  16.         return 1;    
  17.     }    
  18. }    
  19.     
  20.     
  21. public class Test {    
  22.     
  23.     public static void main(String[] args) {    
  24.         Map<A,Object> map = new HashMap<A, Object>();    
  25.         map.put(new A(), new Object());    
  26.         map.put(new A(), new Object());    
  27.             
  28.         System.out.println(map.size());    
  29.     }    
  30.         
  31. }    
必要再调用 equals(Object obj)方法了。参见推论4(如果两个对象hashcode不相等,他们一定不equals) 
2、当第二次map.put(new A(), new Object())的时候,Java运行时环境再次判断,这时候发现了map中有两个相同的hashcode(因为我重写了A类的hashcode()方 法永远都返回1),所以有必要调用equals(Object obj)方法进行判断了。参见推论3(如果两个对象hashcode相等,他们不一定equals),然后发现两个对象不equals(因为我重写了equals(Object obj)方法,永远都返回false)。 
3、这时候判断结束,判断结果:两次存入的对象不是相同的对象。所以最后打印map的长度的时候显示结果是:2。 



改写程序如下: 
Java代码


运行之后打印结果是: 

判断hashcode 
判断hashcode 
判断equals 



显然这时候map的长度已经变成1了,因为Java运行时环境认为存入了两个相同的对象。原因可根据上述分析方式进行分析。 

以上分析的是HashMap,其实HashSet的底层本身就是通过HashMap来实现的,所以他的判断原理和HashMap是一样的,也是先判断hashcode再判断equals。 

所以:写程序的时候应尽可能的按规范来,不然在不知不觉中就埋下了bug! 


java中的==、equals()、hashCode()源码分析

在java编程或者面试中经常会遇到 == 、equals()的比较。自己看了看源码,结合实际的编程总结一下。

1. == 

  java中的==是比较两个对象在JVM中的地址。比较好理解。看下面的代码:

复制代码
1 public class ComAddr{2     public static void main(String[] args) throws Exception {3         String s1 = "nihao";4         String s2 = "nihao";5         String s3 = new String("nihao");6         System.out.println(s1 == s2);    //    true7         System.out.println(s1 == s3);    //    false8     }9 }
复制代码

  上述代码中:

  (1)s1 == s2为true,是因为s1和s2都是字符串字面值"nihao"的引用,指向同一块地址,所以相等。

  (2)s1 == s3为false,是因为通过new产生的对象在堆中,s3是堆中变量的引用,而是s1是指向字符串字面值"nihao"的引用,地址不同所以不相等。

2.equals()

   equals是根类Obeject中的方法。源代码如下:

public boolean equals(Object obj) {    return (this == obj);}

  可见默认的equals方法,直接调用==,比较对象地址。

   不同的子类,可以重写此方法,进行两个对象的equals的判断。

  String类源码中重写的equals方法如下,

复制代码
 1     public boolean equals(Object anObject) { 2         if (this == anObject) { 3             return true; 4         } 5         if (anObject instanceof String) { 6             String anotherString = (String) anObject; 7             int n = value.length; 8             if (n == anotherString.value.length) { 9                 char v1[] = value;10                 char v2[] = anotherString.value;11                 int i = 0;12                 while (n-- != 0) {13                     if (v1[i] != v2[i])14                             return false;15                     i++;16                 }17                 return true;18             }19         }20         return false;21     }
复制代码

  从上面的代码中可以看到,

  (1)String类中的equals首先比较地址,如果是同一个对象的引用,可知对象相等,返回true。

  (2)若果不是同一个对象,equals方法挨个比较两个字符串对象内的字符,只有完全相等才返回true,否则返回false。

3.hashcode()

  hashCode是根类Obeject中的方法。

  默认情况下,Object中的hashCode() 返回对象的32位jvm内存地址。也就是说如果对象不重写该方法,则返回相应对象的32为JVM内存地址。

  String类源码中重写的hashCode方法如下,

复制代码
 1 public int hashCode() { 2     int h = hash;    //Default to 0 ### String类中的私有变量, 3     if (h == 0 && value.length > 0) {    //private final char value[]; ### Sting类中保存的字符串内容的的数组 4         char val[] = value; 5  6         for (int i = 0; i < value.length; i++) { 7             h = 31 * h + val[i]; 8         } 9         hash = h;10     }11     return h;12 }
复制代码

  String源码中使用private final char value[];保存字符串内容,因此String是不可变的。

  看下面的例子,没有重写hashCode方法的类,直接返回32位对象在JVM中的地址;Long类重写了hashCode方法,返回计算出的hashCode数值:

复制代码
 1 public class ComHashcode{ 2     public static void main(String[] args) throws Exception { 3         ComHashcode a = new ComHashcode(); 4         ComHashcode b = new ComHashcode(); 5         System.out.println(a.hashCode());    //870919696 6         System.out.println(b.hashCode());    //298792720 7          8         Long num1 = new Long(8); 9         Long num2 = new Long(8);10         System.out.println(num1.hashCode());    //811         System.out.println(num2.hashCode());    //812     }13 }
复制代码

总结:

(1)绑定。当equals方法被重写时,通常有必要重写hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。

(2)绑定原因。Hashtable实现一个哈希表,为了成功地在哈希表中存储和检索对象,用作键的对象必须实现hashCode 方法和 equals 方法。同(1),必须保证equals相等的对象,hashCode 也相等。因为哈希表通过hashCode检索对象。

(3)默认。

  ==默认比较对象在JVM中的地址。

  hashCode 默认返回对象在JVM中的存储地址。

  equal比较对象,默认也是比较对象在JVM中的地址,同==

 

参考:

http://docs.oracle.com/javase/7/docs/api/



0 0
原创粉丝点击