深入理解Java中HashCode方法

关于hashCode,维基百科中:

?

1

2

3

4

In the Java programming language, every class implicitly or explicitly

provides a hashCode() method, which digests the data stored in an

instance of the class into a single hash value (a 32-bit signed

integer).

hashCode就是根据存储在一个对象实例中的所有数据,提取出一个32位的整数,该整数的目的是用来标示该实例的唯一性。有点类似于MD5码,每个文件都能通过MD5算法生成一个唯一的MD5码。不过,Java中的hashCode并没有真正的实现为每个对象生成一个唯一的hashCode,还是会有一定的重复几率。

先来看看Object类,我们知道,Object类是java程序中所有类的直接或间接父类,处于类层次的最高点。在Object类里定义了很多我们常见的方法,包括我们要讲的hashCode方法,如下

?

1

2

3

4

5

6

7

8

public final native Class<?> getClass();

public native int hashCode();

public boolean equals(Object obj) {

 return (this == obj);

public String toString() {

 return getClass().getName() + "@" + Integer.toHexString(hashCode());

}

注意到hashCode方法前面有个native的修饰符,这表示hashCode方法是由非java语言实现的,具体的方法实现在外部,返回内存对象的地址。

在java的很多类中都会重写equals和hashCode方法,这是为什么呢?最常见的String类,比如我定义两个字符相同的字符串,那么对它们进行比较时,我想要的结果应该是相等的,如果你不重写equals和hashCode方法,他们肯定是不会相等的,因为两个对象的内存地址不一样。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

public int hashCode() {

  int h = hash;

  if (h == 0) {

    int off = offset;

    char val[] = value;

    int len = count;

      for (int i = 0; i < len; i++) {

        h = 31*h + val[off++];

      }

      hash = h;

    }

    return h;

  }

其实这段代码是这个数学表达式的实现

?

1

s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]

s[i]是string的第i个字符,n是String的长度。那为什么这里用31,而不是其它数呢?《Effective Java》是这样说的:之所以选择31,是因为它是个奇素数,如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算。使用素数的好处并不是很明显,但是习惯上都使用素数来计算散列结果。31有个很好的特性,就是用移位和减法来代替乘法,可以得到更好的性能:31*i==(i<<5)-i。现在的VM可以自动完成这种优化。 

可以看到,String类是用它的value值作为参数来计算hashCode的,也就是说,相同的value就一定会有相同的hashCode值。这点也很容易理解,因为value值相同,那么用equals比较也是相等的,equals方法比较相等,则hashCode一定相等。反过来不一定成立。它不保证相同的hashCode一定有相同的对象。

一个好的hash函数应该是这样的:为不相同的对象产生不相等的hashCode。 

在理想情况下,hash函数应该把集合中不相等的实例均匀分布到所有可能的hashCode上,要想达到这种理想情形是非常困难的,至少java没有达到。因为我们可以看到,hashCode是非随机生成的,它有一定的规律,就是上面的数学等式,我们可以构造一些具有相同hashCode但value值不一样的,比如说:Aa和BB的hashCode是一样的。

如下代码:

?

1

2

3

4

5

6

7

8

9

10

11

public class Main {

  public static void main(String[] args) {

    Main m = new Main();

    System.out.println(m);

    System.out.println(Integer.toHexString(m.hashCode()));

    String a = "Aa";

    String b = "BB";

    System.out.println(a.hashCode());

    System.out.println(b.hashCode());

  }

}

输出结果:

?

1

2

3

4

Main@2a139a55

2a139a55

2112

2112

一般在重写equal函数时,也要重写hashCode函数,这是为什么呢? 

来看看这个例子,让我们创建一个简单的类Employee

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

public class Employee

{

  private Integer id;

  private String firstname;

  private String lastName;

  private String department;

  public Integer getId() {

    return id;

  }

  public void setId(Integer id) {

    this.id = id;

  }

  public String getFirstname() {

    return firstname;

  }

  public void setFirstname(String firstname) {

    this.firstname = firstname;

  }

  public String getLastName() {

    return lastName;

  }

  public void setLastName(String lastName) {

    this.lastName = lastName;

  }

  public String getDepartment() {

    return department;

  }

  public void setDepartment(String department) {

    this.department = department;

  }

}

上面的Employee类只是有一些非常基础的属性和getter、setter.现在来考虑一个你需要比较两个employee的情形。

?

1

2

3

4

5

6

7

8

9

10

11

public class EqualsTest {

  public static void main(String[] args) {

    Employee e1 = new Employee();

    Employee e2 = new Employee();

    e1.setId(100);

    e2.setId(100);

    //Prints false in console

    System.out.println(e1.equals(e2));

  }

}

毫无疑问,上面的程序将输出false,但是,事实上上面两个对象代表的是通过一个employee。真正的商业逻辑希望我们返回true。

为了达到这个目的,我们需要重写equals方法。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public boolean equals(Object o) {

    if(o == null)

    {

      return false;

    }

    if (o == this)

    {

      return true;

    }

    if (getClass() != o.getClass())

    {

      return false;

    }

    Employee e = (Employee) o;

    return (this.getId() == e.getId());

}

在上面的类中添加这个方法,EauqlsTest将会输出true。 

So are we done?没有,让我们换一种测试方法来看看。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

import java.util.HashSet;

import java.util.Set;

public class EqualsTest

{

    public static void main(String[] args)

      {

        Employee e1 = new Employee();

        Employee e2 = new Employee();

        e1.setId(100);

        e2.setId(100);

        //Prints 'true'

        System.out.println(e1.equals(e2));

        Set<Employee> employees = new HashSet<Employee>();

        employees.add(e1);

        employees.add(e2);

        //Prints two objects

        System.out.println(employees);

    }

上面的程序输出的结果是两个。如果两个employee对象equals返回true,Set中应该只存储一个对象才对,问题在哪里呢?

我们忘掉了第二个重要的方法hashCode()。就像JDK的Javadoc中所说的一样,如果重写equals()方法必须要重写hashCode()方法。我们加上下面这个方法,程序将执行正确。

?

1

2

3

4

5

6

7

8

@Override

 public int hashCode()

 {

  final int PRIME = 31;

  int result = 1;

  result = PRIME * result + getId();

  return result;

 }

需要注意记住的事情

尽量保证使用对象的同一个属性来生成hashCode()和equals()两个方法。在我们的案例中,我们使用员工id。
eqauls方法必须保证一致(如果对象没有被修改,equals应该返回相同的值)
任何时候只要a.equals(b),那么a.hashCode()必须和b.hashCode()相等。
两者必须同时重写。

总结

以上就是本文关于深入理解Java中HashCode方法的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站其他相关专题,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!

您可能感兴趣的文章:

猜你喜欢

转载自blog.csdn.net/u012501054/article/details/88863045