HashSet中对象的equal和hashCode方法重要性
大家都知道,平时我们要用到的集合大部分是List和Set。集合,在数学严格定义上讲,具有以下三个特征:
-
确定性:集合中的元素应该是确定的,不能模棱两可。
-
互异性:集合中的元素应该是互不相同的,相同的元素在集合中只能算作一个。
-
无序性:集合中的元素是无次序关系的。
根据上面三个特征,所以我觉得Set才是真正意义上的集合概念。而List(可以有重复的元素)最多只能算是一个容器,把具有一致性的东西都可以装在里面。
至于“确定性”和“无序性”,这里不讨论。那都是数学上的抽象概念。我们这里只讨论集合中元素的唯一性问题。
其
实集合的存在,就是对集合一系列的操作,比如数学上我们有相交,相并,补集的概念。而具体到程序里集合的操作,包括增加一个元素,删除一个元素,等等。前
面我们说到过,元素在集合里必需是唯一的,而唯一的标准是什么?我们说两个实体是同一的,一般都会先列出某些个标准,然后对应着一一验证
,如果都能成立,则同一性成立。比如说在学校里,用学号做为标准来标识学生的唯一性,在商场里,用条形码(其实也是数字编码)标识商品的唯一性。而在程序
里,我们怎么来定义标准呢?
你说的对,就是对象的equal方法,我们在进行集合的操作时,就是用equal方法来判断对象是否唯一。
这里要强调的是,在java里千万要警惕 == 操作符,==是判断两个对象的物理地址的,没有任何业务意义。
class Student{
private String code;
private String name;
public Student(String code,String name){
this.code = code;
this.name = name;
}
public boolean equals(Object obj){
if(obj == null)
return false;
if(!(obj instanceof Student))
return false;
Student p = (Student)obj;
return code.equals(p.code);
}
}
代码中重写了equals方法,如果两个Student实例中code相等,则认为是相同的对象。
写个测试代码:
public static void main(String[] args) throws Exception{
Set<Student> ss = new HashSet<Student>();
Student s = new Student("001","lily");
Student s1 = new Student("001","lily");
ss.add(s);
ss.add(s1);
System.out.println(ss.size());
}
输出:2
你可能会问,为什么重写了equals方法,s和s1实例的code都为"001",为什么集合中仍可以放两个对象呢?
原因就在于HashSet的实现是通过HashMap的KeySet做为存储结构的,大家都知道哈希算法为了避免键值冲突,一般有两个方法:二次哈希和链地址法。则HashMap就是用的链地址法。
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;
}
看上面代码中下划线的黑体字,就是用hashCode()方法得到键值的桶(桶的概念是一个具有相同哈希值的容器)地址,然后才在一个桶中调用equals方法比较对象是否相等。
其实到这里我们已经知道了hashCode为什么一定要重写了。如果为两个实例即使equals方法成立,可是如果不在一个桶中,则都本不会调用equals方法比较了。
所以,上面的Student类为了保证唯一性,必需重写hashCode方法:
public int hashCode(){
return Integer.valueOf(code);
}
这里要提下HashMap源码中要注意的两个方法:
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
return h & (length-1);
}
indexFor方法是根据算出来的哈希值和哈希表长度进行and操作,所以为了保证哈希表分布的均匀性,所以要用hash(key.hashCode())再次进行哈希运算,为了保证最终计算出来的hash值的低位更随机均匀。