我们创建一个ArrayList类charAray ,向其中添加元素,之后将charAray添加到HashSet setArray中,用contains进行测试,之后改变charAray中的元素再用contains进行测试,最后用equals方法进行测试。
public static void main(String[] args) {
ArrayList<Character> charAray = new ArrayList<>();
charAray.add('a');
Set<ArrayList> setArray = new HashSet<>();
setArray.add(charAray);
System.out.println(setArray.contains(charAray));
charAray.add('b');
System.out.println(setArray.contains(charAray));
for (ArrayList temp : setArray) {
System.out.println(temp.equals(charAray));
}
}
得到的结果如下
true
false
true
可以看到第二个输出为false但是第三个为true,这设计到了HashSet和ArrayList的内部实现。正如其名,HashSet内部是一个哈希表的结构,在添加元素时,用C语言对比,可以将HashSet看做一个数组,其内部存放的是指向ArrayList的指针,ArrayList类的内部方法hashCode会根据自身的元素返回一个int类型变量,HashSet根据hashCode的返回值决定将元素插入到哪个位置。
ArrayList中的hashCode定义如下
public int hashCode() {
int hashCode = 1;
for (E e : this)
hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
return hashCode;
}
很明显,ArrayList中添加元素的时候将导致其hashCode的返回值改变,但是HashSet在ArrayList中的hashCode发生变改变时,不会修改元素的位置。当调用contains函数测试charAray时,HashSet会根据对象的hashCode值定位到其内部元素obj,再使用equals方法比较charAray和obj是否相同,并返回true或false。由于charAray的hashCode已经发生变化,HashSet无法定位到我们想要的对象,所以调用contains函数将会返回false。我们无法修改ArrayList的实现,所以这个问题无法解决,但是对于我们的自定义类我们的自由度很高,可以探索一下解决办法,能不能在修改对象属性或者对象等价的情况下均返回true。
定义类Apple,不对hashCode和equals重写
public class Apple {
private String color;
public Apple(String color) {
this.color = color;
}
public void changeColor(String color) {
this.color = color;
}
public static void main(String[] args) {
Set<Apple> S = new HashSet<>();
Apple apple1 = new Apple("green");
S.add(apple1);
System.out.println(S.contains(apple1));
System.out.println(apple1.hashCode());
apple1.changeColor("123");
System.out.println(S.contains(apple1));
System.out.println(apple1.hashCode());
}
运行如下代码
public static void main(String[] args) {
Set<Apple> S = new HashSet<>();
Apple apple1 = new Apple("green");
S.add(apple1);
System.out.println(S.contains(apple1));
System.out.println(apple1.hashCode());
System.out.println();
Apple apple2 = new Apple("green");
System.out.println(S.contains(apple2));
System.out.println(apple2.hashCode());
System.out.println();
apple1.changeColor("red");
System.out.println(S.contains(apple1));
System.out.println(apple1.hashCode());
}
得到如下结果
true
488970385
false
1209271652
true
488970385
在不重写hashCode方法时,我们看到修改类中元素不会改变hashCode值,此外Object中的equals是直接对地址进行比较,所以第三个测试返回true。但是这种情况下即使两个对象是等价的,也无法用contains进行判断。我们修改hashCode和equals方法,如下所示
@Override
public int hashCode() {
return color.length();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Apple))
return false;
if (obj == this)
return true;
return this.color.equals(((Apple) obj).color);
}
此时重新运行代码,得到如下结果
true
5
true
5
false
3
这时第二种测试得到了true但是第三种得到false,为了让这两种测试都得到true,我们可以修改类内部定义:添加final int变量HC(代表hashCode),并修改构造方法和hashCode方法
final private int HC;
public Apple(String color) {
this.color = color;
HC = color.length();
}
@Override
public int hashCode() {
return HC;
}
这样在实现一个类的时候其hashCode便被定下且不会再发生改变,运行代码我们可以得到如下结果
true
5
true
5
true
5
这样看似很好但是运行如下代码会遇到新的问题
public static void main(String[] args) {
Set<Apple> S = new HashSet<>();
Apple apple1 = new Apple("green");
S.add(apple1);
apple1.changeColor("123");
Apple apple3 = new Apple("123");
System.out.println(S.contains(apple3));
}
输出结果为false
,和我们的想法不符合,由于HashSet使用了哈希表的情况,这两种情况不能同时满足。所以在定义自己的可变数据类型时,最好不要修改hashCode方法,可能会导致各种想不到的错误,此外在HashSet中加入可变数据类型的时候一定要小心使用。而使用不可变数据类型时,因为内部数据不会发生改变,所以我们可以大胆操作。