List和Set都是集合Collection的两个类,但是它们又有不同,最主要的一点就是List允许加入两个相同的元素,而Set不允许加入两个相同的元素。那么它们内部究竟是怎么判断元素不同的呢?说到这,就有必要先了解java中Object的equals和hashcode方法了。
由于Object是所有类的父类,所以java类默认都有这两个方法。
一般我们在定义类的时候都不会覆盖它们,当然这没什么问题,但有些情况下需要重写它们。先来看个例子吧。
package com.Howard.test01;
/**
* 书
* @author Howard
* 2017年2月24日
*/
public class Book {
/**
* 书名
*/
private String name;
/**
* 页数
*/
private int pageNum;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPageNum() {
return pageNum;
}
public void setPageNum(int pageNum) {
this.pageNum = pageNum;
}
}
测试类:
package com.Howard.test01;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
public class EqualsTest {
public static void main(String[] args) {
Book b1 = new Book();
Book b2 = new Book();
b1.setName("java编程思想");
b1.setPageNum(880);
b2.setName("java编程思想");
b2.setPageNum(880);
List<Book> list = new LinkedList<>();
Set<Book> set = new HashSet<>();
list.add(b1);
list.add(b2);
set.add(b1);
set.add(b2);
System.out.println(b1.equals(b2));
System.out.println(b1==b2);
System.out.println(b1.hashCode());
System.out.println(b2.hashCode());
System.out.println("list:"+list.size());
System.out.println("set:"+set.size());
}
}
运行结果:
false
false
366712642
1829164700
list:2
set:2
可以看到:此时equals比较出来的结果是false,即它们并不是同一个元素。默认的equals方法是根据对象的内存地址是否一样来判断的,很明显,这里是分别new出的实例对象,故返回false。
这时我们为Book重写equals(eclipse可以右键source—>Generate Hashcode and equals自动生成,这里先重写equals)
package com.Howard.test01;
/**
* 书
* @author Howard
* 2017年2月24日
*/
public class Book {
/**
* 书名
*/
private String name;
/**
* 页数
*/
private int pageNum;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPageNum() {
return pageNum;
}
public void setPageNum(int pageNum) {
this.pageNum = pageNum;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Book other = (Book) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (pageNum != other.pageNum)
return false;
return true;
}
}
再次运行上面的测试类:
true
false
366712642
1829164700
list:2
set:2
可以看出:此时equals返回的是true,当我们重新覆盖了equals后,具体的返回值就要看具体实现equals的逻辑是怎么写的了。按上面写的,就是根据对象属性的值是否一样来判断(一般eclipse为我们自动重构的equals方法也是这么判断的)
现在我们把重写的equals注释掉,重写hashcode方法(同样可以用eclipse帮我们生成)
package com.Howard.test01;
/**
* 书
* @author Howard
* 2017年2月24日
*/
public class Book {
/**
* 书名
*/
private String name;
/**
* 页数
*/
private int pageNum;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPageNum() {
return pageNum;
}
public void setPageNum(int pageNum) {
this.pageNum = pageNum;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + pageNum;
return result;
}
}
重写运行测试类:
false
false
1783492
1783492
list:2
set:2
这时看到,原本b1和b2的hashcode一直不一样的,现在一样了。但此时set和list里面的元素数量都仍然是2,即set还是把它们当作不同元素。
那么接下来重写加上equals的方法:
package com.Howard.test01;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
public class EqualsTest {
public static void main(String[] args) {
Book b1 = new Book();
Book b2 = new Book();
b1.setName("java编程思想");
b1.setPageNum(880);
b2.setName("java编程思想");
b2.setPageNum(880);
List<Book> list = new LinkedList<>();
Set<Book> set = new HashSet<>();
list.add(b1);
list.add(b2);
set.add(b1);
set.add(b2);
System.out.println(b1.equals(b2));
System.out.println(b1==b2);
System.out.println(b1.hashCode());
System.out.println(b2.hashCode());
System.out.println("list:"+list.size());
System.out.println("set:"+set.size());
}
}
再一次运行测试类:
true
false
1783492
1783492
list:2
set:1
可以看到:b1和b2hash值一样,但是set却不把它们当作不同元素了。原来set在判断相同元素时,先调用的equals进行比较,由于我们重写了equals方法,所以这时候是根据属性值比较,而b1和b2属性值是相等的,所以此时set把它们当作相同的元素,并没用添加成功。
但是我们刚才在未重写hashcode,重写了equals是list和set的元素数量却仍然是2,这是为什么呢?其实set在比较元素时,如果发现equals相等,那么就会当作相同元素处理,如果发现不相等,那么还会去调用hashcode进行比较,只有equals和hashcode都返回false,set才把它当作不同的元素。这一点,我们可以看看hashset的add方法的源码便一目了然了(这里我使用的jdk版本是7)。
所以我们一般在重写equals方式时,都会一并重写了hashcode方法。
正因为此,在我们把元素add进集合后,最好不要再改变元素属性的值,如果需要,先取出来修改再放进去。为什么这么说呢,看看下面的例子就知道了
(我也是在看到张孝祥老师的视频后才知道有这么回事)
package com.Howard.test01;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
public class EqualsTest {
public static void main(String[] args) {
Book b1 = new Book();
b1.setName("java编程思想");
b1.setPageNum(880);
Set<Book> set = new HashSet<>();
System.out.println("改变前:"+b1.hashCode());
set.add(b1);
System.out.println("set:"+set.size());
b1.setPageNum(770);
System.out.println("改变后:"+b1.hashCode());
set.remove(b1);
System.out.println("set:"+set.size());
}
}
结果:
改变前:417483024
set:1
改变后:1248881984
set:1
结果看到:当我们尝试着取出set里面的元素b1,发现并没有用,b1的hashcode改变,set根据hashcode根本不能将其移除。这就导致了内存泄露。
最后总结下:默认的equals方法是根据内存地址判断,hashset的add方法在比较元素时,如果发现equals相等,那么就会当作相同元素处理,如果发现不相等,那么还会去调用hashcode进行比较,只有equals和hashcode都返回false,才把它们当作不同的元素。当我们在重写equals时一般也把hashcode重写(因为一般我们重写也是为了根据对象属性判断相等而不是内存地址)。所以一般有这么个规则:如果x.equals(y)返回“true”,那么x和y的hashCode()必须相等。
如果x.equals(y)返回“false”,那么x和y的hashCode()有可能相等,也有可能不等。反之, 如果两个对象hashcode相等,他们不一定equals, 如果两个对象hashcode不相等,他们一定不equals。
在元素加进集合后,不要试图去改变元素属性的值。