equals和hashCode学习

《effective java》(第2版)第九条“覆盖equals时总要覆盖hashCode”。

以下为整理对该两个方法的学习。通过实例说明,在使用hashMap、hashSet时必须覆写equals和hashcode原因。以及可能造成的错误。

各种情况:(1)equals和hashCode都没有覆写;(2)只覆写equals方法;(3)只覆写hashCode方法

Object中hashCode方法:由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。) 

Object中equals方法:即,对于任何非空引用值 xy当且仅当xy 引用同一个对象时,此方法才返回truex == y 具有值true

public class Person {
    private int age;
    private String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return this.age;
    }

    public String getName() {
        return this.name;
    }
    // 未实现equals和hashCode方法
}

(1)当equals和hashCode都没有覆写
public class HashTest extends TestCase {
	Map<Person, String> map;
	
	@Override
	protected void setUp() throws Exception {
		map = new HashMap<Person, String>();
	}
	
	/**
	 * Person没有覆写equals和hashcode方法
	 */
	public void testNoMethod() {
		map.put(new Person(21, "a"), "a");
		/* 由于没有覆写hashCode方法(使用Object的hashCode方法),两次new的Person虽然参数一致,但hashcode返回值不一样,所以无法拿到之前put的key */
		assertFalse(map.get(new Person(21, "a")) != null);
	}
}
(2)只覆写了equals方法
public boolean equals(Object obj) {
		if (obj == this)
			return true;
		if (!(obj instanceof Person))
			return false;
		Person person = ((Person) obj);
		return (this.age == person.getAge())
				&& (this.name.equals(person.getName()));
	}
public class HashTest extends TestCase {

	Map<Person, String> map;

	@Override
	protected void setUp() throws Exception {
		map = new HashMap<Person, String>();
	}
	
	/**
	 * Person只覆写了equals方法
	 */
	public void testOnlyEqualsMethod() {
		map.put(new Person(21, "a"), "a");
		/*
		 * 由于没有覆写hashCode方法(使用Object的hashCode方法),两次new的Person虽然参数一致,但hashcode返回值不一样,所以无法拿到之前put的key
		 */
		assertFalse(map.get(new Person(21, "a")) != null);
	}
}
(3)只覆写hashCode方法
public int hashCode() {
	final int hashMultiplier = 41;
	int result = 7;
	result = result * hashMultiplier + this.age;
	result = result * hashMultiplier + this.name.hashCode();
	return result;
}
 /**
  * Person只覆写了hashCode方法
  */
public void testOnlyHashCodeMethod() {
	map.put(new Person(0, "a"), "a");
	assertTrue(map.get(new Person(0, "a")) == null);
}

map.get(new Person(0, "a"))得到的内容会为null。当调用get方法时,会先调用该对象hashCode方法取哈希值,如果该哈希值在map中不存在返回null;如果存在会调用Person的equals方法,判断map中的hashCode相等对象与new Person(0, "a")的equals方法(本例子的情况),由于没有覆写equals方法,会调用Objece的equals方法,返回false,所以拿到null。

总结:

综上,当往map中put值时,首先调用该对象的hashCode方法计算哈希值,如果map中没有,就将该对象put到map中,如果该对象的哈希值在map中已经有,会调用该对象的equals方法,如果返回true,不会put到map中,返回false认为map中的对象和该对象不一样,会放到map中。

当调用map的get方法时,会先调用该对象hashCode方法取哈希值,如果该哈希值在map中不存在返回null;如果存在会调用equals方法,如果返回true拿到该对象,如果返回false返回null。

HashSet和HashMap类似。

对于哈希数据结构的集合,有两个概念,一个是“桶(bucket)”,一个是“装载因子”。

要想查找表中对象的位置,就要先计算它的哈希值,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引。例如:如果某个对象的哈希值为76268,并且有128个桶,对象应该保存在第108号桶中(76268除以128余108)。

【实际情况要比该规则复杂,如下方代码(HashMap中的put方法),会将该对象K key的hashCode再次进行哈希:int hash = hash(key.hashCode());】

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
上方代码中的table就是前面描述的“桶(bucket)概念”,int i = indexFor(hash, table.length);部分代码计算该对象应放到桶的索引位置,indexFor方法如下:
static int indexFor(int h, int length) {
        return h & (length-1);
    }
由于两个对象的哈希值可能一致,也就出现了两个对象放在一个桶中的情况,此时就会调用equals方法,如果返回true就认为该对象已存在,不进行存储了;如果返回false这种现象被称为散列冲突,这时,需要用新对象与桶中的所有对象进行比较,查看这个对象是否已经存在。

综上,对于HashSet当调用add时,先取hashcode,进行计算要保存到的桶索引,如果桶中没有其他元素,就直接放进去(不调用equals方法),如果桶中已经有元素,就会调用equals方法,判断新对象是否在桶中已经存在了,如果不存在,就放进去。

HashMap调用get方法时,先取hashcode,计算桶索引位置,然后到该桶中查找,查找时hashCode和equals两个方法都会调用(即使桶中只有一个元素),代码如下:

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        // 计算哈希值  
        int hash = hash(key.hashCode());
        // 根据索引indexFor取得对应的桶
        for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 
            return e.value; 
        } 
        return null;
}

当满足”if (e.hash == hash && ((k = e.key) == key || key.equals(k)))“条件时,才认为找到,返回结果。以下测试说明该规则,代码:

// 计算后的桶的索引值不同
public void test04() {
	Set<Person> set = new HashSet<Person>();
	set.add(new Person(0, "a"));
	set.add(new Person(1, "b"));
	// 11864和11906是事先计算好的两个对象的hashCode值
	System.out.println(hash(11864) & (16 - 1));	// 3
	System.out.println(hash(11906) & (16 - 1));	// 5
	System.out.println(set.size());
}
	
/**
 * HashMap中的方法
 */
static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
}


在执行完set的add方法后,打断点查看set,如下图

存储在了桶的索引位置3和5。以下为非同一个对象,但计算桶索引位置相同的情况:

/**
 * 计算后的桶的索引值相同
 */
public void test05() {
	Set<Person> set = new HashSet<Person>();
	set.add(new Person(22, "w"));
	set.add(new Person(24, "y"));
	// 11864和11906是事先计算好的两个对象的hashCode值
	System.out.println(hash(12788) & (16 - 1));	// 11
	System.out.println(hash(12872) & (16 - 1));	// 11
	System.out.println(set.size());
}
	
/**
 * HashMap中的方法
 */
static int hash(int h) {
	h ^= (h >>> 20) ^ (h >>> 12);
	return h ^ (h >>> 7) ^ (h >>> 4);
}

上图中,两个对象都存储在了索引为11的桶中。

关于”装载因子“,测试理解如下:

/**
 * 装载因子测试
 */
public void testLoadFactor() {
	Set<Person> set = new HashSet<Person>();
	/* 装载因子:0.75;table桶:16 */
	set.add(new Person(0, "a"));
	set.add(new Person(1, "b"));
	set.add(new Person(2, "c"));
	set.add(new Person(3, "d"));
	set.add(new Person(4, "e"));
	set.add(new Person(5, "f"));
	set.add(new Person(6, "g"));
	set.add(new Person(7, "h"));
	set.add(new Person(8, "i"));
	set.add(new Person(9, "j"));
	set.add(new Person(10, "k"));
	set.add(new Person(11, "l"));
	/* 装载因子:0.75;table桶:32 【16*0.75=12,当放第13个元素时,会用双倍的桶数自动进行再散列】 */
	set.add(new Person(12, "m"));
}

set.add(new Person(0, "a"));执行前截图:


set.add(new Person(12, "m"));执行后截图:


测试现象:装载因子默认值:0.75;


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值