读者人群
本文章适合有一定的Java基础的人阅读。
问题引入
如果hashMap的key是一个自定义的类,怎么办?
需要重写自定义类的hashCode()与equals()方法。
下面来分析下具体的原因:
· 下面的例子是用来测试的小demo:
package j2se.equals方法;
/** @version:
* @Description: JavaBean,User类
* @author: wangxi
* @date: 2018年8月12日 上午9:28:57
*/
public class User {
private String username;
private int age;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public User(String username, int age) {
this.username = username;
this.age = age;
}
// 重写Object类中的toString()
@Override
public String toString() {
return "User [username=" + username + ", age=" + age + "]";
}
// 重写Object类的hashCode()
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((username == null) ? 0 : username.hashCode());
return result;
}
// 重写Object类的equals()
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
User other = (User) obj;
if (age != other.age)
return false;
if (username == null) {
if (other.username != null)
return false;
} else if (!username.equals(other.username))
return false;
return true;
}
}
package j2se.equals方法;
import java.util.HashMap;
import java.util.Map;
/** @version:
* @Description: 测试对象的equals()与hashCode()
* @author: wangxi
* @date: 2018年8月12日 上午9:29:09
*/
public class Test {
public static void main(String[] args) {
User user1 = new User("wangx", 1);
User user2 = new User("wangx", 1);
// == 一定是比较两个对象在堆内存的地址
System.out.println(user1 == user2);
/**
hashcode和equals的约定关系如下:
1、如果两个对象相等(调用equals返回true),那么他们一定有相同的哈希值(hash code)。
2、如果两个对象的哈希值相等,那么这两个对象有可能相等也有可能不相等。
(需要再通过equals来判断)
3、等价的(调用equals返回true)对象必须产生相同的散列码。
不等价的对象,不要求产生的散列码不相同。
*
*/
System.out.println(user1.equals(user2));
HashMap<User, Integer> map = new HashMap<>();
// put()会先调用hashCode().进行O(1)时间判断要加入的元素是否在容器中,这样节省了时间
map.put(user1, 1);
map.put(user2, 1);
System.out.println(map);
}
}
// 输出结果如下:
false
true
{User [username=wangx, age=1]=1} // 此时重写了那两个方法的输出。
案例分析:Map的key使用了自定义类,并且User类也重写了上述两个方法,此时在Test类的map输出只有一个元素。这与map的put()有关系:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 初始化tab、n变量
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 判断key的hash值对应的Entry数组头元素是否为空
if ((p = tab[i = (n - 1) & hash]) == null)
// 如果为空,直接添加
tab[i] = newNode(hash, key, value, null);
// 如果不为空,会覆盖掉原来的值(前提是调用equals方法存在这个值)
else {
.........
而key的hash值怎么来的呢,请看下面代码:
static final int hash(Object key) {
int h;
/** 下面代码调用了key的hashCode()方法,这也不难理解为什么
* map的key为自定义类时需要重写上述两个方法。
*/
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
还需要明确一点:putVal()方法会先调用hashCode()方法,进行O(1)时间比较,如果两个HashCode相等,进而会调用equals()方法比较。
假设User类没有重写上述两个方法:
那么putVal()会直接调用Object类的hashCode()与equals()方法。下面看看Object类的源码:
// 调用底层的方法,生成32位机器码
public native int hashCode();
// 直接比较对象在堆内存的地址
public boolean equals(Object obj) {
return (this == obj);
}
如果User类没有重写上述方法,那么最终输出的map对象就会有两个值相同的User对象。(new出来的对象,地址一定不相同,hashCode也不相同。)
HashSet的应用
其实原理和hashMap基本相同
且看hashSet源码:
/** add()方法最终会调用map中的put()方法
* 而此时map的key为add()方法传进来的参数,value是常量PRESENT
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// 下面是JDK对常量的解释
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
Set集合不含有重复元素也利用hashCode与equals(),并且还利用了hashCode()方法O(1)时间判断元素是否在集合中的特性,大大的节省了时间。
public class SetDemo {
// 验证set集合元素的唯一性
public static void main(String[] args) {
Set<Integer> set1 = new HashSet<>();
set1.add(1);
set1.add(1);
System.out.println(set1);
Set<Integer> set2 = new TreeSet<>();
set2.add(1);
set2.add(1);
System.out.println(set2);
}
}
// 输出结果:
[1]
[1]
在String类的应用
且来看下String类的源码:
// 把字符串中每个字符进行累加,就得到字符串的hashcode值
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
// 只要有一个字符不相等,就返回false,两个字符串不等。
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
hashMap的key如果是String类型时,此时会缓存key的hashcode值,这也是一种JDK优化策略。
effective Java一书中说道:hashCode()与equals()同存在,同不存在。
在Integer、Long等的使用
// Integer类重写了equals(),Long,Float等包装类类似。
public boolean equals(Object obj) {
// 首先判断是否为Integer对象
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}