Object——所有类的超类
在Java中,有一个类的始祖,它就是Object类,在java中每个类都扩展了Object。所有类包括自定义的类吗?比如自己写的一个员工类:
class Employee
{
...
}
这里并没有写成
class Employee extends Object
{
...
}
Java规定,如果一个类没有明确指出超类(即没有使用extends),那么它的超类就是Object。
Object与其他派生类转换
因为所以类都派生自Object,故所有的类都可以转换为Object类。
Object obj = null;
String str = "java";
int[] arr = {3,23,4};
obj = str;
obj = arr;
那么Object可以转换为其他类么,可以但必须使用强制类型转换,而且不一定能成功。下面看成功转换的案例:
Object obj1 = "java";
Object obj2 = new int[]{3,23,1};
String str = (String) obj1;
int[] arr = (int[]) obj2;
上述是没有问题的,可以正常编译及执行。但是看看下面的代码,它就不能编译成功了
Object obj1 = "java";
Object obj2 = new int[]{3,23,1};
String str = (String) obj2; // error
int[] arr = (int[]) obj1; // error
结合上述两个例子,可以推断出,当Object变量存的数据内容符合强制转换后的变量类型,才可以转换成功。
下面介绍Object的几个常见方法:
equals方法
Object中有一个equals方法,用来测试两个对象是否相等。该方法判断两个对象相等的条件是,两个对象的引用是否相等。如果两个对象的引用相等的话,那么毋庸置疑这两个对象一定相等。
但是,我们经常需要对对象的字段进行比较,如果两个对象具有相同的字段值,就认为这两个对象相等。比如,有两个员工姓名、年龄以及薪水相等的话,就认为他们相等。
下面我们来重写父类的equals方法,不再比较引用,而是比较几个字段值。
package com.studyjava.demo;
import java.util.Objects;
class Employee
{
private String name;
private int age;
private double salary;
public Employee (String name, int age, double salary)
{
this.name = name;
this.age = age;
this.salary = salary;
}
public String getName ()
{
return this.name;
}
public int getAge ()
{
return this.age;
}
public double getSalary ()
{
return this.salary;
}
public boolean equals (Object other)
{
if (this == other) {
return true;
}
if (other == null) {
return false;
}
if (this.getClass() != other.getClass()) {
return false;
}
Employee otherE = (Employee) other;
if (Objects.equals(this.name, otherE.name)
&& this.age == otherE.age
&& this.salary == otherE.salary) {
return true;
} else {
return false;
}
}
}
这里我们刚开始使用了“==”来比较两个对象。补充“==”知识点:
- java中的基本数据类型判断是否相等,直接使用"=="就行了,相等返回true,否则,返回false。
- 但是java中的引用类型的对象比较变态,假设有两个对象obj1,obj2, obj1==obj2 判断是obj1,obj2这两个变量的引用是否相等,即它们所指向的是否为同一个对象(和Object.equals判断方法一致)。
- 在Java API中,有些类重写了equals()方法,它们的比较规则是:当且仅当该equals方法参数不是 null,两个变量的类型、内容都相同,则比较结果为true。这些类包括:String、Double、Float、Long、Integer、Short、Byte、、Boolean、BigDecimal、BigInteger等等,太多太多了,但是常见的就这些了,具体可以查看API中类的equals()方法,就知道了。
好了,现在我们来分析这个重写的equals方法。
- 注意参数的类型必须是Object。如果写成equals(Employee other),则不会覆盖Object类的equals方法,而是定义了一个完全无关的方法。
- 第一个if用了“==”来判断这两个变量是否引用同一对象。如果引用同一变量,那么肯定相等。
- 第二个if用来判断other是否为null,如果为null,那么返回false。这是因为,this肯定不为null,如果为null的话就会抛出异常。
- 第三个条件判断是否同属一个类,如果不是同属一个类,那么就需返回false。
- 接着就需对字段值进行比较了。这里用了Objects.equals()来比较name属性,为什么不直接用this.name.equals(otherE.name)?这是防止this.name如果为null的话会抛出异常。
Objects类中的equals方法用于检测一个对象是否等于另一个对象。
if (a.equals(b)) {......}
这里,为了防止a可能为null的情况,可以换成Objects.equals方法。如果a和b都是null,则Objects(a,b)将返回true,若其中一个为null,则返回false,若都不是null,则返回a.equals(b)。
下面,我们把Employee的子类Manager的equals方法也重写下:
package com.studyjava.demo;
class Manager extends Employee
{
private double bonus;
public Manager (String name, int age, double salary)
{
super(name, age, salary);
this.bonus = 0;
}
public double getSalary ()
{
return super.getSalary() + this.bonus;
}
public void setBonus (double bonus)
{
this.bonus = bonus;
}
public boolean equals (Object manager)
{
if (!super.equals(manager)) {
return false;
}
Manager mng = (Manager) manager;
return this.bonus == mng.bonus;
}
}
相等测试与继承
如果a.equals(b),a和b不属于同一个类,但他们之间是派生关系,equals该如何处理呢?这是一个非常值得讨论的话题。争论的核心在于是用getClass()检测是否为同一个类,还是用intanceof检测是否为继承关系。
java官方对equals方法有一个对称性的要求,即a.equals(b)为true,那么b.equals(a)也应该为true。这是非常合理的,因为我们不想在使用equals时还需要考虑用a.equals(b)还是b.equals(a)。
假如在之前的例子中,将Employee中equals方法中的
this.getClass() != other.getClass()
修改为
other instanceof this
那么,如果e.equals(m)返回true,(e为Employee实例,m为Manager实例)。那么为满足对称性,要使m.equals(e)也返回true。这样就导致一个问题,Manager中的equals就收到了限制。该equals方法必须愿意将自己与任何一个Employee对象进行比较,而不考虑经理特有的那部分信息。
关于是使用intanceof还是使用getClass,下面给出最佳实践:
- 如果子类可以有自己的相等性概念,则对称性需求将强制使用getClass检测。
- 如果由超类决定相等性概念,那么就可以使用instanceof检测,这样就可以在不同子类的对象之间进行相等性比较。
在员工和经理的例子中,只要相应的字段相等,就认为两个对象相等。如果两个Manager对象的姓名、年龄以及薪水均相等,而奖金不相等,就认为它们是不同的,因此我们要使用getClass检测。
但如果假设使用员工的ID作为相等性检测标准,并且这个相等性概念适用于所有子类,就可以使用instanceof检测,而且应该将Employee.equals声明为final。
hashCode方法
散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的。如果重新定义了equals方法,就必须为用户可能插入散列表的对象重新定义hashCode方法。
equals与hashCode的定义必须相容,如果x.equals(y)返回true,那么x.hashCode就必须与y.hashCode返回相同的值。例如,如果定义Employee.equals比较员工的ID,那么hashCode方法就需要散列ID,而不是员工的姓名或存储地址。
下面,我们来完成Employee的hashCode方法。
public int hashCode ()
{
return 3 * Objects.hashCode(this.name)
+ 5 * Objects.hashCode(this.age)
+ 7 * Objects.hashCode(this.salary);
}
这里使用Objects.hashCode而不是使用name.hashCode等是为了防止name为null情况。Objects.hashCode(a),若a为null则返回0;
还有更好的方法,需要组合多个散列值时,可以调用Objects.hash,并提供若干参数,该方法原型为
static int hash(Object ...objects)
public int hashCode ()
{
return Objects.hash(this.name, this.age, this.salary);
}
最后,再完成下Manager类的hashCode方法。
public int hashCode ()
{
return super.hashCode() + Objects.hashCode(this.bonus);
}
toString方法
toString是一个非常常用的调试方法。它返回一个表示对象值的一个字符串。
默认返回格式如下:对象的class名称
+ @
+ hashCode的十六进制字符串
clone
protected native Object clone() throws CloneNotSupportedException;
。
这个方法用于克隆对象。被克隆的对象必须实现java.lang.Cloneable
接口,否则会抛出CloneNotSupportedException
异常。
默认的clone方法是浅拷贝模式
。所谓浅拷贝,指的是对象内属性引用的对象只会拷贝引用地址,而不会将引用的对象重新分配内存。深拷贝则是会连引用的对象也重新创建。
getClass
public final native Class<?> getClass();
。获取对象的运行时class对象,class对象就是描述对象所属类的对象。这个方法通常是和Java反射机制搭配使用的。