软件构造——等价性

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对象,这正是我们想要的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值