Object类及其方法
Object:所有类的超类
Object类型的变量
equals方法
相等测试与继承
hashcode方法
toString方法
往期文章回顾
Object类及其方法
Object:所有类的超类
Object类是Java
中所有类的超类,虽然我们不必指明某类继承Object类(即public class Employee extends Object
),可以说Object类一切类的祖先。
Object类型的变量
众所周知,面向对象具有三大特性:「封装、继承、多态。」
「封装」即隐藏数据的具体实现,使用时并不需要知道底层。
「继承」即通过扩展父类,使该类既具有父类的基本特性,也有子类的特性(可以增加自己的新特性)。
「多态」即同一个对象有不同的状态(面对不同的情景),简单来说就是「替换原则」
例如可以使用Object的对象变量引用任何类型的对象:
Object object=new Employee(“Harry Hacker”,35000);
当然这个object变量只能作为一个存储任何值的「泛型容器」(即任何类都可以向上转型成为object)
如果需要使用该对象的「具体行为」,需要将其转换成确切的类型。
在Java
中,只有「基本类型」不是对象,例如数值、字符和布尔类型的值都不是对象
「所有的数组类型,不论是对象数组还是基本类型的数组都是对象,都扩展了Object类。」
Employee[] staff=new Employee[10];
Object obj=staff; //OK
Object obj=new int[10];//OK
equals方法
equals
方法是Object类的一个方法,作用是检测一个对象是否等于另一个对象。
并且Object的默认equals
方法是比较两个对象的引用是否相等。(这种默认行为本质上是合理的)那么什么是对象的引用呢?
简单来说就是定义一个变量,使其指向一个对象,那个变量就是对象引用。
如果两个对象的引用都相等,那么两个对象肯定相等。
但是现实是我们总是需要基于「特定的情况」来比较对象的相等性。
比如在一个公司中,比较员工对象是否相等,只需要比较数值是否相等即可,
此时就需要重写Object类的equals
方法,来提供我们需要的实现。
public class Employee{
.........
@Override
public boolean equals(Object otherObject){
//比较当前传入对象的引用是否相等
if(this==otherObject) return true;
//若传入的对象为空,则立即返回false
if(otherObject==null) return false;
//比较传入的对象与本例中的对象是否属于同一个类
if(getClass()!=otherObject.getclass()) return false;
//将other强制类型转换成Employee类
Employee other=(Employee)otherObject;
//最后返回比较的结果(进行数值上的比较)
return name.equals(other.name) && salary==other.salary && hireDay.equals(other.hireDay);
//第二个版本
return Objects.equals(name,other.name)&& salary==other.salary && Objects.equals(hireeDay,other.hireDay)
}
}
需要注意的是getClass()
返回一个对象所属的类,只有两个对象属于同一个类,才有可能相等。
Obejcts的equals
方法用来处理name或者hireDay都为null的情况。
若两者均为null,则返回true 若其中一个为null,则返回false 若两个参数都不为null,则调用a.equals(b)
子类中重写的父类equals方法时,会首先调用超类equals方法,若检测失败,对象则不相等。若超类的字段都相等,那么比较子类的实例字段。
public class Manager extends Employee{
.......
@Override
//重写Employee类的equals方法
public boolean equals(Object otherObject){
//if比较的是当前类对象与传入的对象是否属于同一个类
if(!super.equals(otherObject))return false;
Manager other=(Manager)otherObject;
return bonus==other.bonus;
}
}
相等测试与继承
根据Java
语言规范要求equals方法必须要有以下特性;
自反性:对于任何非空的引用X,X.equals(X)应该返回true。 对称性:对于任何的引用X、Y,需要满足当且仅当Y.equals(X)返回true时,X.equals(Y)也应该返回true。 传递性:对于任何引用X、Y、Z,如果X.equals(Y)返回true,Y.equals(Z)返回true,X.equals(Z)也应该返回true。 一致性:如果X、Y的引用没有变化,那么反复调用X.equals(Y)应该返回相同的结果。 对于任意非空引用X,X.equals(null)应该返回false。
需要注意的是instanceof这个关键词,比如在比较两个对象是否相等时用instanceof关键词会得到奇怪的结果
Employee e =new Employee();
Manager m=new Manager();
e.equals(m)//返回true
m.equals(e)//返回false
以上来看,用instanceof关键词是不合理的,它违背了对称性的原则。
现在来看,有两种完全不同的概念:
若子类有自己的相等性概念,那么对称性需求将强制使用 getClass()
检测若完全由超类来决定相等性概念,那么可以使用instanceof关键词进行检测,可以在不同子类的对象之间进行相等性比较。
下面是编写一个完美的equals
方法的建议
显式参数命名为otherObject,稍后需要将其强制转换为另一个名为other的变量。 检测this和otherObject是否相等: if(this==otherObject)return true;
检查身份比逐个检查字段开销要小。检测otherObject是否为null,若为null,返回false, if(otherObject==null)return false
比较this和otherObject的类,如果equals的语义可以在子类中被改变(子类重写equals)就使用 getClass()
检测,if(!getClass()==otherObject.getClass()) return false
如果所有的子类都有相同的相等性语义,可以使用instanceof检测 if(!(otherObject instanceof ClassName))return false
将otherObject强制转换成相应类类型的变量: ClassName other=(ClassName)otherObject
再根据相等性概念来比较字段,基本类型字段用**==**,使用 Obejcts.equals()
比较对象字段,若所有字段都匹配,返回ttrue,否则,返回false。若子类重写了equals方法,就在其中包含一个super.equals(other)
的调用。
hashcode方法
hashcode即「散列码」,本身其实并没有任何规律,只是对象导出的一个「整型值」。
如果X,Y是两个不同的对象,那么他们的散列值一般不会相同。
由于hashcode方法是定义在Object类中的,因此每个对象都有一个默认的散列值,其值由对象的存储地址得出
来看个例子:
这里可以发现一个很有趣的事,s和t具有相同的散列码,tb和sb虽然载内容上是相同的,但散列码却不同。
原因分析:
字符串s和t的散列码是由「内容」导出的,这是由于String类重写了hashcode方法,此时不再依据对象地址来获得散列码,而是通过对象内容,如果对象内容相同,则散列码相同,反之,则不同。(尽管这两个对象地址是不同的) 字符串构建器StringBuilder类的对象则是依据「对象地址」来确定散列码的。出现内容相同但散列码不同的情况是因为:StringBuilder类没有重写Object类的hashcode方法,是依据对象地址来确定散列值,但由于通过对象构造器的方式会在堆内存中划出不同的空间,于是内容相同的两者会有不同的内存地址,也就会有不同的散列码。
如果重新定义了equals
方法,就必须为用户可能插入散列表的对象重新定义hashcode
方法
hashcode
方法应该返回一个整数(可以为负),**应该要合理地组织实例字段的散列码,以便能够让不同的对象产生的散列码分布更加均匀。**以下是Employee的hashcode
方法。
public class Employee{
......
public int hashcode(){
return 7*Objects.hashcode(name)+11*Double.hashcode(salary)+13*Obejcts.hashcode(hireDay);
//使用Objects.hashcode()方法来保证安全性:
//1.若参数为null,则返回0
//2.若参数不为null,则返回参数调用hashcode的结果。
//3.Double.hashcode()避免创建Double对象。
}
}
其实还有一种更简单的重写方法:使用Objects.hash(),
public int hashcode(){
return Objects.hash(name,salary,hireDay);
//此方法会对各个参数调用Objects.hashcode(),并组合这些散列值。
}
此外还有非常重要的一点:「equals()
方法必须与hashcode()
方法兼容」,意思是若X.equals(Y)
返回true,那么X.hashcode()
的值必须与Y.hashcode()
的值相同。
toString方法
toString
方法顾名思义就是将某个对象转成String类型的对象。它会返回「对象值的一个字符串。」
绝大多数的toString
方法都遵循这样的格式:类的名字,随后是一对方括号括起来的字段值
以下是Employee类的一个例子:
public String toString(){
return getClass().getName()="[name="+name+",salary="+salary+",hireDay="+hireDay+"]";
//通过调用getClass().getName()方法来获得类名的字符串,这样的方法也可以由子类调用。
}
当设计子类时,也必须要重写toString
方法,并加入子类自己的字段。如果超类使用了getClass().getName()
方法,那么子类只需要调用super.toString()
即可。「调用父类的toString()
方法,方法中的getClass()返回子类类型,并以字符串的形式返回子类类名。」
例如子类的重写方法
public class Manager extends Employee{
.........
public String toString(){
return super.toString()+"[bonus="+bonus+"]";
//将其打印输出会产生如下内容
/*
Manager[name=...,salary=....,hireDay=....][bonus=......]
*/
}
}
toString
方法比较常见是因为:只要将对象与一个字符串通过操作符“+”连接起来,Java编译器会自动调用toString方法来获得这个对象的字符串描述。
注意:可以不写X.toString()
而写“ ”+X,虽然功能实现的功能相同,但有一些细节需要知道:「X.toString()中的X必须是一个对象,其他类型则不行,但“ ”+X中的X可以是对象,也可以是基本数据类型。」
如果X是一个对象,那么System.out.println(X)
会简单地调用X.toString()
方法,并打印输出得到的字符串,具体内容是:对象的类名加上散列码。(此时并未覆盖Object类的toString()方法)
toString方法的优点:
便利用户可以获得有关对象状态的有用信息。 是一个重要的调式工具。 使得对象的内容更加简洁明了。
综上所述:本质上这三个Object方法都需要重写,使得内容更加清晰。