JavaSE第四十五讲:hashCode与equals深度剖析与源码详解

集合Collection框架中的两个接口List与Set,List的实现类主要是ArrayList和LinkedList,前面已经做了详细的剖析,接下来主要讲Set内容。

JDK Doc中对Set的定义:A collection that contains no duplicate[重复] elements.

public interface Set<E>
  
  
   
   extends Collection<E>
  
  
查看Set中的构造方法和List有很多类似之处。都是从Collection中继承过来的,List接口的实现类主要有ArrayList和LinkedList,而Set的实现类有:

All Known Implementing Classes: 
AbstractSet, ConcurrentSkipListSet, CopyOnWriteArraySet, EnumSet, HashSet, JobStateReasons,LinkedHashSet, TreeSet 

下面写例子来验证类HashSet的特性:

package com.ahuier2;

import java.util.HashSet;

public class SetTest1 {
	public static void main(String[] args) {
		HashSet set = new HashSet();
		System.out.println(set.add("a"));
		set.add("b");
		set.add("c");
		set.add("d");
		System.out.println(set.add("a"));
		System.out.println(set);
	}
}
编译执行结果:

true
false
[d, b, c, a]

【说明】:从这边可以看出Set的两种特性,一是Set不支持重复元素,所有第一个放进去a之后,第二次放进去a后会返回false,第二点是Set是无序的,没有顺序的概念。所以打印出来的元素不是按顺序取。


接下来对比以下几个程序,根据他们的不同来总结Set中的equals()和hashCode()方法。

package com.ahuier2;

import java.util.HashSet;

public class SetTest2 {
	public static void main(String[] args) {
		HashSet set = new HashSet();
		set.add(new People("zhangsan"));
		set.add(new People("lisi"));		
		System.out.println(set);
	}
}

class People{
	String name;
	public People(String name){
		this.name = name;
	}
}
编译执行结果:

[com.ahuier2.People@1fb8ee3, com.ahuier2.People@c17164]

【注意】:不一定前面一个是“zhangsan”对象,后面一个是“lisi”对象


修改main方法中的代码段:

	public static void main(String[] args) {
		HashSet set = new HashSet();
		set.add(new People("zhangsan"));
		set.add(new People("lisi"));
		set.add(new People("zhangsan"));
		System.out.println(set);
	}
编译执行结果:

[com.ahuier2.People@1fb8ee3, com.ahuier2.People@c17164, com.ahuier2.People@61de33]

【注意】:往里面再添加 new People("zhangsan"); 后,此时打印出Set中的三个对象,而且他们的 @符号后十六进制都不一样。


继续修改main方法中的代码短:

	public static void main(String[] args) {
		HashSet set = new HashSet();
/*		set.add(new People("zhangsan"));
		set.add(new People("lisi"));
		set.add(new People("zhangsan"));*/
		People p1 = new People("zhangsan");
		set.add(p1);
		set.add(p1);
		System.out.println(set);
	}
编译执行结果:

[com.ahuier2.People@c17164]

【注意】:这边大家可能会这样理解,生成的一个对象zhangsan,地址赋给p1的引用,将同一个对象的引用分两次往Set里面放,所以打印出来的是一个对象,这样的理解对吗?请继续关注下面的剖析......


继续修改main方法中的代码:

	public static void main(String[] args) {
		HashSet set = new HashSet();
/*		set.add(new People("zhangsan"));
		set.add(new People("lisi"));
		set.add(new People("zhangsan"));*/
/*		People p1 = new People("zhangsan");
		set.add(p1);
		set.add(p1);*/		
		String s1 = new String("a");
		String s2 = new String("a");		
		set.add(s1);
		set.add(s2);
		System.out.println(set);		
	}
编译执行结果:

[a]

【注意】:之前我们所写的 set.add("a"); 或者 String s = "a"; 这种方式字符串的字面值形式,而现在所写 String s1 = new String("a");这种形式是再堆中生成字符串的对象,这里对象地址是不同的,只不过里面的内容是相同的而已。详细内容可以参考之前所讲的String学习笔记。这里面为什么和上一个程序出现不同呢,两个对象增加到Set中后,出来只有一个也就是说真正放进去的时候对象只放进去一个?

通过这里的例子可以导出上面程序所描述的推测是错误的!下面我们就开始剖析这里面的端倪!

查看JDK Doc中HashSet的add()方法:

add
public boolean add(E e)
Adds the specified element to this set if it is not already present. More formally, adds the specified element e to this set if this set contains no element e2 such that (e==null ? e2==null : e.equals(e2)). If this set already contains the element, the call leaves the set unchanged and returns false. 

[添加一个指定的元素增加到Set中,如果这个元素没有存在的话,更加正确的说法是,指定的元素e增加Set中如果这个元素e不包含满足(e==null ? e2==null : e.equals(e2))[e2为空,则e也为空,如果e2不为空,则e.equals(e2)]这个条件的e2的话,如果这个元素已经存在,就不会修改这个Set并返回false,即不往Set中添加这个元素]

所以如何判断Set中是否存在我们要放进去的这个元素呢?这个是重点也是Set中的关键点。


查看JDK Doc中关于Object类的equals()方法的特点:

public boolean equals(Object obj)

Indicates whether some other object is "equal to" this one. 

The equals method implements an equivalence relation on non-null object references: 

1) It is reflexive: for any non-null reference value x, x.equals(x) should return true. 

                 [自反性:x.equals(x)应该返回true]


2) It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true. 

                 [对称性:x.equals(y)为true,那么y.equals(x)也为true。]


3) It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true. 

 [传递性:x.equals(y)为 true并且y.equals(z)为true,那么x.equals(z)也应该为true.] 

4) It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified. 

                 [一致性:x.equals(y)的第一次调用为true,那么x.equals(y)的第二次、第三次、第n次调用也应该为true,前提条件是在比较之间没有修改x也没有修改y。]


5) For any non-null reference value x, x.equals(null) should return false. 

                 [

对于非空引用x,x.equals(null)返回false。]


      
The equals method for class Object implements the most discriminating possible equivalence relation on objects; that is, for any non-null reference values x and y, this method returns true if and only if x and y refer to the same object (x == y has the value true). 

[对于Object类中equals()方法来说在对象上实现了最有差别的可能的相等关系,也就是说对于非空引用x和y,如果x和y只指向同一个对象(x he  y 有相同的值),则这个方法返回为真] 

这边所述即为Object类中equals()方法的说明,Object中equals()方法就是用等号来比较的,这个地方可以参考Java中的Object类的equals()方法的源代码。


Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes. 

[注意,通常我们有必要在重写equasl()方法的时候,有必要去重写hashCode这个方法,来维护对hashCode()方法的一种一般性的约束,以此来声明相等的对象也必须要有相等的hash codes]


现在我们继续来看Object类中的hashCode()方法,这个方法与equals()方法存在紧密联系

hashCode
public int hashCode()
[返回一个整数]
Returns a hash code value for the object. This method is supported for the benefit of hashtables such as those provided by java.util.Hashtable. 

[返回这个对象的hash值,为了能更好的使用类似java.util.Hashtable包下的hashtables,提供了这个方法的支持(也就是要去重写hashCode()这个方法)]


The general contract of hashCode is: [hashCode的一般性契约]
1) Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.

[在一个Java应用的执行过程中,当我们在相同的对象上调用hashCode()这个方法,必须返回相同的整数值,这个整数从第一次启动到另外一次启动这个相同的程序,它是不一定需要保持一致的。](这边可以理解为如果再一次Java应用去调用这个函数返回123,再调用的过程中,有可能接下来还会继续调用这个方法,则它的返回值也必须是123,但是如果退出了这个应用,下次再启动这个应用时,调用这个方法之后,有可能返回的是456了。) 


2) If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result. 

[如果两个对象通过Object中的equals()方法比较是相等的话,那么在这两个对象上调用hashCode方法,它们返回的hashCode值也必须是一样的]


3) It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hashtables. 

[如果两个对象根据Java中的Object类的equals()方法比较的话,它们是不相等的,那么再这两个对象上调用hashCode()方法必须生成不同的整型值,这种情况不一样要强制的,然而,程序员应该注意如果在不同的对象上产生了不同的的返回值,那么我们应该再hashtables上改进它的性能]


As much as is reasonably practical, the hashCode method defined by class Object does return distinct integers for distinct objects. (This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the JavaTM programming language.) 

[根据实际的情况来来说,针对Objcet类的hashCode()方法来说,不同的对象会产生不同的整数值,(通常他们实现方式是在对象的内部地址转换成一个整数,但这种技术并非Java规范语言所强制的。)]

为什么为出现这种情况呢?查看JDK Doc中Object类的toString()方法的实现:

getClass().getName() + '@' + Integer.toHexString(hashCode())

类的名字 + @ + hashCode的十六进制表示

从这边就可以知道:不同的对象hash code是不同的,他们返回出不同的地址,hash code可以看做是他们地址的的表现形式,对于Object类来说。注意后面跟的那一串只是内部地址的表现形式,通过某一种机制转化来的。


关于Object类的hashCode()方法的特点: 

a) 在Java应用的一次执行过程当中,对于同一个对象的hashCode方法的多次调用,他们应该返回同样的值(前提是该对象的信息没有发生变化)。 

b) 对于两个对象来说,如果使用equals方法比较返回true,那么这两个对象的hashCode值一定是相同的。 

c) 对于两个对象来说,如果使用equals方法比较返回false,那么这两个对象的hashCode值不要求一定不同(可以相同,可以不同),但是如果不同则可以提高应用的性能。 

d) 对于Object类来说,不同的Object对象的hashCode值是不同的(Object类的hashCode值表示的是对象的地址)。


查看HashSet的源代码验证之前程序出现不同的原因,这里暂且先不研究,等下回分解,先把理论思路理清:

   当使用 HashSet 时,hashCode()方法就会得到调用,判断已经存储在集合中的对象的 hash code 值是否与增加的对象的 hash code 值一致;如果不一致,直接加进去;如 果一致,再进行 equals 方法的比较,equals 方法如果返回 true,表示对象已经加进去了,就不会再增加新的对象,否则加进去。 【这里只是概况,其实整个实现非常复杂

所以,返回来看上面的程序中:

//		set.add(new People("zhangsan"));
//		set.add(new People("lisi"));
//		set.add(new People("zhangsan"));
【说明】:这边是两个不同的对象,他们的地址是不同的,所以hashCode值一定是不同的,所以一定可以放进这个Set里面


//		People p1 = new People("zhangsan");		
//		set.add(p1);
//		set.add(p1);
说明】:接下来看这一个程序,它们不能放进Set两次,因为p1与p2指向相同的对象,所以他们的hashCode值是一样的,进而判断equals()方法,p1.equals(p1)返回true,也就是自己跟自己所以返回true,此时根据上面定义返回true,表明对象已经加进去了,所以无法继续添加这个元素。

		String s1 = new String("a");
		String s2 = new String("a");
		set.add(s1);
		set.add(s2);
【说明】:这个程序是否可以往Set中添加呢,这边判断的时候就要注意了,由于String这个类有可能重写了Object中的hashCode()方法和equals()方法[equals()方法在前几笔记中提到已经重写了Object的中的equals()方法]

查看String类中hashCode()的源代码

    /**
     * Returns a hash code for this string. The hash code for a
     * <code>String</code> object is computed as
     * <blockquote><pre>
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * </pre></blockquote>
     * using <code>int</code> arithmetic, where <code>s[i]</code> is the
     * <i>i</i>th character of the string, <code>n</code> is the length of
     * the string, and <code>^</code> indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    public int hashCode() {
        int h = hash;
        if (h == 0 && count > 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;
    }

查看JDK Doc中的String类的hashCode()方法

hashCode
public int hashCode()

    Returns a hash code for this string. The hash code for a String object is computed as
          s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

[hashCode()方法调用后,产生的hashCode值,这个值是由上面的这个公式来算的,s[0]表示字符串的第一个字符]从这边可以看出,如果两个字符串内容相同的话,则他们返回的hashCode值也是一样的。

注意的是这里面产生的这个公式很大程度上在于这个经验值,也就是说这个公式是根据多次,规范的测试得出比较好的公式。

		String s1 = new String("a");
		String s2 = new String("a");		
		System.out.println("hash code: " + (s1.hashCode() == s2.hashCode()));	
		set.add(s1);
		set.add(s2);	
		System.out.println(set);

编译执行结果:

hash code: true
[a]

【说明】:可见如果两个字符串的值相同的话,则他们的hashCode值也是一样的。


如果我们重写 equals 方法,那么也要重写 hashCode 方法,反之亦然。这两个方法总是相辅相成的,因为我们都不知道将来是否要用到Set,会往Set里面添加元素,所以再重写了equals()方法后,也要重写hashCode()方法。


以上是String类自己定义的两个重写后的hashCode()方法和equals()方法,如果我们自己定义一个类,想让其内容一样,就不需要再往Set中添加了,比如两个学生的名字一样就不需要往Set中添加信息了,这在实际开发中很常见,因为实际开发更多的判断是否能往Set中放置是根据内容决定的,而非地址来决定的。

实现方式:这种情况下必须重写hashCode()方法和equals()方法。

实现代码:

package com.ahuier2;

import java.util.HashSet;

public class SetTest3 {
	public static void main(String[] args) {
		HashSet set = new HashSet();
		Student s1 = new Student("zhangsan");
		Student s2 = new Student("zhangsan");
		set.add(s1);
		set.add(s2);
		System.out.println(set);
	}
}

class Student{
	String name;
	public Student(String name){
		this.name = name;
	}
	/*
	 * 这边重写hashCode(),不需要像String那样去写一个很复杂的函数,这边的hashCode的产生主要是name属性
	 * 来决定的,所以这边直接调用name的hashCode,返回name的hashCode值作为当前的hashCode值就可以了。
	 */
		
	public int hashCode(){
		return this.name.hashCode(); 
	}
    public boolean equals(Object obj){
    	if(this == obj){
    		return true;
    	}
    	if(null != obj && obj instanceof Student){
    		Student s = (Student)obj;
    		if(name.equals(s.name)){
    			return true;
    		}
    	}
    	return false;
    }
}
编译执行结果:

[com.ahuier2.Student@aa9c3074]

【说明】:产生了一个对象,所以程序设计成功。重写的hashCode()中this.name.hashCode()其实就是调用String类中的hashCode()方法的。重写的equals()方法可以参看之前所讲内容笔记中的equals()方法的重写例子


法二:可以根据Eclipse中的帮助自动重写类的hashCode()方法和equals()方法,对于每个类来说他们实现起来都可以是不一样的,只要符合逻辑要求就可以了。

步骤:Source --> Generate hashCode() and equals() --> ok

选择要包含的hashCode()和equals()方法的成员变量,因为重写这两个方法后实现内容是否一致是根据这个类的成员变量来体现的,所以选择name属性,点击OK

package com.ahuier2;

import java.util.HashSet;

public class SetTest3 {
	public static void main(String[] args) {
		HashSet set = new HashSet();
		Student s1 = new Student("zhangsan");
		Student s2 = new Student("zhangsan");
		set.add(s1);
		set.add(s2);
		System.out.println(set);
	}
}

class Student{
	String name;
	public Student(String name){
		this.name = name;
	}
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Student other = (Student) obj;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		return true;
	}
}
编译执行结果:

[com.ahuier2.Student@aa9c3093]

【注意】:根据Eclipse生成重写的这两个方法,一定要将光标放在需要重写这两个方法的类中,再操作生成的步骤。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值