作为Java中所有类的根类,Object提供了很多基础的方法,我们经常会覆写它的方法,但很多时候因为不了解这些方法内在的含义以及与其他方法之间的关系而错误的覆写。下面介绍一下各个方法,已经如何合理地覆写它们。
hashcode方法为对象定义了计算哈希值的方式。在Java中有这么几条硬性的规定:
覆写hashCode方法最大的目的在于使对象能够在用哈希值辅助存储的数据结构中能够正常工作,如HashMap、HashSet和HashTable。
在Java原生的hashCode实现方式下会给对象的每个实例一个不同的哈希值,这样上面的第二点要求是没法满足的,举个具体的例子来说,我们定义一个Man类来表示一个人,有一个唯一的id(类似身份证号)和姓名两个参数。用一个HashMap来表示他的所有的朋友。
public class Man {
private String name;
private int id;
public Man(String name,int id) {
this.name=name;
this.id=id;
}
public static void main(String args[]){
Map<Man, Man> friends = new HashMap<Man, Man>();
friends.put(new Man("Mike", 1211), new Man("Andy", 1119));
System.out.println(friends.get(new Man("Mike", 1211)));
}
}
执行上面的程序我们可以看到输出的是null。因为Mike虽然是同一个人(有着相同的id),但在上面的语句中却通过new来生成了两个不同的对象,根据Java原生的hashCode方法,会导致存储和查找的位置不同,最终查找失败。
那么我们该如何覆写hashCode呢,最简单的方式是直接返回一个常数,这样上面的三条规则就都能满足了。但这样的话所有人包括Mike和Andy的哈希值都会相同,这样他们都会存储到一个散列桶中,散列表直接退化为一个链表或数组,效率大幅下降。典型的做法是把在equals中要用到的域都通过计算集成到哈希值中。一些专家做了一些优化,并形成了一套可行性很高的方案:
result = 31 * result + h
集成到result中下面给出例子中的对象的散列实现方式:
@Override
public int hashCode() {
int result = 1;
result = 31 * result + id;
result = 31 * result + (name == null ? 0 : name.hashCode());
return result;
}
equals方法用来判断两个对象在逻辑上是不是等同的,例如,如果一个人是通过id唯一确定的,那么不管new几个id相同的对象,逻辑上他们都是同一个人。Java中对equals方法的要求是:
原Object对equals的实现方式是通过引用是否相同,虽然符合上面的所有的性质。但此时new Man("Mike", 21).equals(new Man("Mike", 21))
返回为false,显然不符合我们的要求。
为了实现以上的约束,并尽量提高程序的效率,我们可以对equals进行如下的构造:
下面是例子中的equals方法的实现,把只要id相等的对象都认为是同一个人:
@Override
public boolean equals(Object obj) {
if(obj==this){
return true;
}
if(!(obj instanceof Man)){
return false;
}
Man other = (Man)obj;
if(this.id==other.id){
return true;
}
else{
return false;
}
}
clone方法用来给出对象本身的一个副本。
很多时候我们需要复制一个对象,如当作副本进行缓存,当然可以获取相应的参数通过构造函数来实现对象的复制,但这会使得调用方的方法变得繁杂。Object提供了clone方法来帮助对象创建副本。
想要你的类能够调用clone方法,该类必须要实现Cloneable接口,这是一个空的接口,仅仅来声明实现了这个接口的类是可以clone的。如果不实现该接口,会抛出CloneNotSupportedException异常。需要注意的是,Object中的clone方法是protected的,且返回的值是Object类型。如果我们要覆写clone方法,应把该方法改为public,且返回类自身的对象。
@Override
public Man clone() throws CloneNotSupportedException {
return (Man) super.clone();
}
在例子中只是调用了super方法并进行了一下转型,这是因为Man中只有基本类型的域。但如果类中有可变的引用域时,简单的调用super.clone()就不行了。下面为Man添加一个妻子的属性,妻子是一个独立的对象。具体代码如下:
public class Woman implements Cloneable {
private String name;
public Woman(String name) {
this.name = name;
}
@Override
public Woman clone() throws CloneNotSupportedException {
return (Woman) super.clone();
}
@Override
public String toString() {
return String.format("[name:%s]", this.name);
}
public void rename(String newName) {
this.name = newName;
}
}
public class Man implements Cloneable{
private String name;
private int id;
private Woman wife;
public Man(String name, int id, Woman wife) {
this.name = name;
this.id = id;
this.wife = wife;
}
@Override
public Man clone() throws CloneNotSupportedException {
return (Man) super.clone();
}
@Override
public String toString() {
return String.format("[name:%s, id:%d]", this.name,this.id);
}
public static void main(String args[]) {
Man mike = new Man("Mike", 1211,new Woman("Anny"));
try {
Man mike2 = (Man) mike.clone();
mike.wife.rename("Lily");
System.out.println(mike2.wife);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
这时的输出是[name:Lily]
,可以看出Mike的妻子改了个名字,结果保存的副本也改变了。那要怎么做才能使副本的值不变呢?我们需要对可变的引用也进行复制。
@Override
public Man clone() throws CloneNotSupportedException {
Man man = (Man) super.clone();
man.wife = (Woman) this.wife.clone();
return man;
}
toString方法用来将对象的信息转化成一个字符串。
Object中对toString实现的方式是类名+@+十六进制的对象编号
,例如drfish.Man@24da72,显然这种形式的输出阅读起来不方便,让人无法了解对象的真正情况。Java建议所有的类都覆写toString方法,返回对象的关键信息,方便查看和诊断。
toString的实现比较简单,只要自己构造一个包含想要展示的信息的字符串并返回。
@Override
public String toString() {
return String.format("[name:%s, id:%d]", this.name,this.id);
}
输出结果如下:[name:Mike, id:1211]
这篇文章主要介绍了Java中的基础类Object中的一些可覆写的方法,以及在覆写它们时需要注意的事项。