1. 不可变数据类型的等价性
利用抽象函数AF:若AF映射到的结果相同,则等价。
利用观察者方法Observers:通过Obervers得到的结果在client端相同,则等价。
2. 可变数据类型的等价性
观察等价性:在不改变状态的前提下,调用Creators、Producers、Observers方法,对象看起来一致则等价。
行为等价性:调用对象的任何方法都展示出一样的结果。
3. Java对等价性的处理
对于不可变数据类型,由于没有mutator方法,观察等价性等价于行为等价性。
对于可变数据类型,Java往往倾向于实现严格的观察等价性,例如Collections;但是也有其他的类实现行为等价性,例如StringBuilder。
4. equals()与==
equals是Object类中定义的方法,具体如下:
public boolean equals(Object obj)
{
return this==obj;
}
可以看出来,在初始的Object类中,equals与==是同样的效果。而对于==,判定依据是两个对象的地址是否相等,即在快照图中两个箭头是否指向同一个对象气泡。
一般对于基本数据类型采用==,而对于对象数据类型采用equals()。对于大量Java已经定义好的对象类,例如String、Integer等,内部实现已经重写Override了Object类中的equals()方法。
因此对于我们自定义的ADT,需要重写equals()方法,来让它的行为是比较具体内容而非地址。
现在提供一个Num类,是一个自定义的类,具体内部实现隐藏。其次提供Fal类的具体实现,可以看到,Fal.java中并没有重写equals()方法。Fal类调用Num类进行per的声明。
(这些类都没有实际应用意义,只是作为理解的例子)
public class Fal
{
private Num per;
private int gra;
public Fal(Num per, int gra)
{
this.per=per;
this.gra=gra;
}
public int getGra()
{
return gra;
}
public Num getPer()
{
return per;
}
}
对其进行应用的客户端如下:
public class Beh
{
public static void main(String[] args)
{
Num tn1=new Num("Ben", 1);
Fal tf1=new Fal(tn1, 90);
Fal tf5=new Fal(tn1, 90);
return tf1.equals(tf5);
}
}
最终返回的结果是:false。然而我们设计的目的,并非要区别引用的地址,而是聚焦于对象的具体内容。这样的默认继承自Object类的equals()方法明显不是我们想要的。
因此对于自定义的ADT,重写equals()方法是极为必要的。我们修改Fal.java:
public class Fal
{
private Num per;
private int gra;
public Fal(Num per, int gra)
{
this.per=per;
this.gra=gra;
}
public int getGra()
{
return gra;
}
public Num getPer()
{
return per;
}
@Override
public int hashCode()
{
return 31*per.hashCode()+gra;
}
@Override
public boolean equals(Object obj)
{
if(this==obj)
return true;
if(obj==null)
return false;
if(!(obj instanceof Fal))
return false;
Fal other=(Fal) obj;
if(per!=other.per)
return false;
if(gra!=other.gra)
return false;
return true;
}
}
最终返回结果是true。
5. 重写equals()
重写equals()方法,要求实现:自反性、传递性、对称性、非空、一致
自反性:x.equals(x)。
传递性:若有x.equals(y),且y.equals(z),则有x.equals(z)。
对称性:若x.equals(y),则y.equals(x)。
非空:.equals(null)=false。
一致:x.equals(y)结果唯一,除非对象发生突变。
@Override
public boolean equals(Object obj)
{
if(this==obj)
return true;
if(obj==null)
return false;
if(!(obj instanceof Fal))
return false;
Fal other=(Fal) obj;
if(per!=other.per)
return false;
if(gra!=other.gra)
return false;
return true;
}
再看上述重写的equals()方法的代码,在第二个if判断中检验非空性。在第三个if中,判断传入的参数obj是否是Fal类型,这个任务交由instanceof来实现。instanceof判断对象是否属于某个类型,但是它是一个动态类型检查的方法,因此除了在equals中,建议不要在其他地方使用。
但是比较上述代码,在重写equals()方法的同时,我们还重写了hashCode()方法:
@Override
public int hashCode()
{
return 31*per.hashCode()+gra;
}
hashCode() 方法同样是在Object类中定义的方法,作用是获取散列码,返回一个int整数,以确定该对象在哈希表中的索引位置。
散列表存储方式是键值对,它能根据键通过散列码快速检索出对应的值。
若两个对象相等,则散列码一定相等;但散列码相等的对象不一定相等。
6. 重写hashCode()方法对重写equals()方法的必要性
这里给出强制要求:在重写equals()方法的同时,重写hashCode()方法。
public class Fal
{
……
@Override
public boolean equals(Object obj)
{
if(this==obj)
return true;
if(obj==null)
return false;
if(!(obj instanceof Fal))
return false;
Fal other=(Fal) obj;
if(per!=other.per)
return false;
if(gra!=other.gra)
return false;
return true;
}
}
现在给出重写equals()方法但是未重写hashCode()方法的情况,定义Beh类:
public class Beh
{
public static void main(String[] args)
{
Num tn1=new Num("Ben", 1);
Fal tf1=new Fal(tn1, 90);
Fal tf5=new Fal(tn1, 90);
Set<Fal> beh=new HashSet<>();
beh.add(tf1);
beh.add(tf5);
System.out.println(beh);
}
}
Set类型的beh变量要求内部不能有相同的元素,因此我们的目的应该是运行结束后,beh中只有一个元素,但是结果:
[forEquals.Fal@404cdd1c, forEquals.Fal@28611029]
beh在其中加入了两个相同的元素,这明显是错误的。
在重写hashCode()之后:
public class Fal
{
……
@Override
public int hashCode()
{
return 31*per.hashCode()+gra;
}
@Override
public boolean equals(Object obj)
{
if(this==obj)
return true;
if(obj==null)
return false;
if(!(obj instanceof Fal))
return false;
Fal other=(Fal) obj;
if(per!=other.per)
return false;
if(gra!=other.gra)
return false;
return true;
}
}
同样的Beh.java,运行结果是:
[forEquals.Fal@404cdd1c]
很明显,这才是我们要的结果。
更新Beh.java:
public class Beh
{
public static void main(String[] args)
{
Num tn1=new Num("Ben", 1);
Num tn2=new Num("Bob", 2);
Num tn3=new Num("Alice", 3);
Num tn4=new Num("Lucy", 4);
Fal tf1=new Fal(tn1, 90);
Fal tf2=new Fal(tn2, 92);
Fal tf3=new Fal(tn3, 94);
Fal tf4=new Fal(tn4, 96);
Fal tf5=new Fal(tn1, 90);
Set<Fal> beh=new HashSet<>();
beh.add(tf1);
beh.add(tf2);
beh.add(tf3);
beh.add(tf4);
beh.add(tf5);
System.out.println(beh);
}
}
输出结果:
[forEquals.Fal@589d2c4e, forEquals.Fal@17f9eeee,
forEquals.Fal@404cdd1c, forEquals.Fal@28611029]
只加入了四个Fal对象,这正是我们想要的。