一、Object类的结构
上图为Object类的结构树,由此可以清晰的看到整个Object的架构。其中个人经过搜索、日常开发的总结,认为Object、clone、equals(Object)、hashCode、getClass、toString这几个方法相对重要(仅属个人意见,如有不同之见,欢迎讨论)。可能有人认为notify、wait等线程有关的方法也很重要,但是从个人角度出发,我认为这些方法更应该放在线程里去研究和讨论。
二、native方法介绍
我们都知道,Java的底层是通过C、C++等语言实现的,那么Java是如何区分这些方法并能准确地去调用的呢?
2.1 native关键字
在Java中,如果一个方法使用native关键字来修饰,即表明该方法并不是由Java实现的,它是由non-java即C、C++负责实现。其具体的实现方法被编译在了dll文件中,由Java调用。由native修饰的方法有:
2.2 registerNatives()
registerNatives(),顾名思义,注册native修饰的方法,其作用是将C、C++等方法映射到Java中由native修饰的方法,这也是Java为何能做到准确地去调用non-java方法。其源码如下:
private static native void registerNatives();
可以看到,这里并没有执行此方法。Java使用的是静态代码块去执行registerNatives(),源码如下:
static {
registerNatives();
}
2.3 clone()
protected native Object clone() throws CloneNotSupportedException;
通过上述源码,我们知道,clone不是Java原生的方法,且Object提供的复制是浅复制,不是深度复制。
浅复制是指只复制对象的引用,而深度复制则是将原来复制的对象完完全全的复制出来,此时被复制的对象与复制出来的对象已经没有任何关系,概括起来就是,浅复制复制引用与值,引用与值都不变;深度复制复制值,引用变值不变。
可以看到,clone方法显式抛出不支持复制的异常,这说明,实现对象的复制是有条件的。
1) 方法由protected修饰,说明若要实现复制,需要继承Object(默认都是继承Object的...)
2)返回类型为Object,表明若要得到我们想要复制的结果需要进行类型转换
3)实现Cloneable接口,否则会抛出不支持复制的异常
以下为一段测试clone的代码:
class Son {
private Integer sonAge;
private String sonName;
public Son(Integer sonAge, String sonName) {
super();
this.sonAge = sonAge;
this.sonName = sonName;
}
public Son() {
super();
}
public Integer getSonAge() {
return sonAge;
}
public void setSonAge(Integer sonAge) {
this.sonAge = sonAge;
}
public String getSonName() {
return sonName;
}
public void setSonName(String sonName) {
this.sonName = sonName;
}
}
public class Parent {
public static void main(String[] args) throws CloneNotSupportedException {
Son son = new Son(10,"清然");
Parent parent = new Parent(20,"安然",son);
Parent temp1 = parent.clone();
}
private Integer age;
private String name;
private Son son;
public Parent(Integer age, String name, Son son) {
super();
this.age = age;
this.name = name;
this.son = son;
}
public Parent() {
super();
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Son getSon() {
return son;
}
public void setSon(Son son) {
this.son = son;
}
}
此时编译器提示需要进行类型转换:
完成类型转换后,提示需要对异常进行处理,即CloneNotSupportedException:
将异常抛出后,没有发现任何编译错误,我们运行main方法,控制台出现如下错误:
Exception in thread "main" java.lang.CloneNotSupportedException: jdkreader.java.lang.object.Parent
at java.lang.Object.clone(Native Method)
at jdkreader.java.lang.object.Parent.main(Parent.java:16)
异常显示Parent并不支持clone,此时我们需要实现Cloneable接口:
public class Parent implements Cloneable
我们可以看一下复制后的两个对象的关系:
Son son = new Son(10,"清然");
Parent parent = new Parent(20,"安然",son);
Parent temp1 = (Parent) parent.clone();
Parent temp2 = (Parent) parent.clone();
System.out.println("temp1 == temp2 : " + (temp1 == temp2));
运行结果为:
temp1 == temp2 : false
很明显,结果为false,因为复制出来是一个新的对象,引用不同。我们再对他们的各个变量进行一一比较:
System.out.println("temp1Age == temp2Age : " + (temp1.getAge() == temp2.getAge()));
System.out.println("temp1Name == temp2Name : " + (temp1.getName() == temp2.getName()));
System.out.println("temp1Son == temp2Son : " + (temp1.getSon() == temp2.getSon()));
运行结果为:
temp1Age == temp2Age : true
temp1Name == temp2Name : true
temp1Son == temp2Son : true
很奇怪,他们的内部属性却全部是相同的,这是为什么呢?
因为Object提供的clone是浅复制,如果是基本类型,则复制其值,如果是引用内容,则复制其引用。所以两者指向的地址是相同的,故相等。
2.4 getClass
public final native Class<?> getClass();
此方法返回的是运行时类对象,这点从注释可明显看出:
Returns the runtime class of this {@code Object}.
什么是类对象?我们知道,在Java中,一切皆对象。在Java中,类是是对具有一组相同特征或行为的实例的抽象并进行描述,对象则是此类所描述的特征或行为的具体实例。作为概念层次的类,其本身也具有某些共同的特性,如都具有类名称、由类加载器去加载,都具有包,具有父类,属性和方法等。于是,Java中有专门定义了一个类,Class,去描述其他类所具有的这些特性。
2.5 hashCode
public native int hashCode();
这也是由non-java实现的方法,返回的是一个对象的散列值,类型为int。一般情况下,在当前程序的运行期间,一个对象多次调用hashCode方法,其返回的散列值是相同的。这里需要注意的是:
若两个对象相等,则它们的散列值一定相等,反之,散列值相等,两个对象不一定相等;
若两个对象不相等,则它们的散列值不一定相等,反之,散列值不同,两个对象一定不相等。
在Java中,有许多地方都应用到了hash,如集合set、map,equals等,这里不做过多研究。
2.6 equals
public boolean equals(Object obj) {
return (this == obj);
}
关于“==”与equals的区别,面试的时候多数人都被问到过。我们知道“==”比较基本数据类型时,比较的是值,当比较对象时,比较的是其引用;而equals比较的是两个对象是否相等。
在Object类中,equals与“==”其实是等价的,但是我们在很多情况下是需要重写equals方法的,例如比较两个学生是否为同一个人,我们直接使用equals方法肯定是不可行的。通常情况下,我们认为如果学号相同,那么这两个学生就是同一个人。重写的示例如下:
Student类
/**
* 用于测试重写Object类equals方法
*
* @author xuyong
*
*/
public class Student {
private String no; //学号
private String name; //姓名
public Student(String no, String name) {
super();
this.no = no;
this.name = name;
}
public Student() {
super();
}
public String getNo() {
return no;
}
public void setNo(String no) {
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
测试类
Student stu1 = new Student("1", "路人甲");
Student stu2 = new Student("1", "路人乙");
System.out.println(stu1.equals(stu2));
运行,控制台打印false。此时,我们重写一下Student类的equals方法:
@Override
public boolean equals(Object obj) {
if (obj instanceof Student) {
Student stu = (Student)obj;
return no.equals(stu.getNo());
}
return super.equals(obj);
}
再次运行刚刚的代码,控制台打印true。于是,我们便实现了通过学号判断是否为同一个人的业务。
不过,骚年们,以为这就结束了吗?NO!
由于Java需要维护hash的一般规律,没错就是刚刚标红的内容:两个对象相等,那么他们的散列值必须相等。
但是此时,我们测试一下他们的hash是否相等,可以明显发现,他们是不相等的。
System.out.println(stu1.hashCode() == stu2.hashCode());
所以,我们在重写equals方法时,必须要重写hashCode方法,由于我们是通过学号判断的,所以最好也是使用学号的散列值替代原有的散列值:
@Override
public int hashCode() {
return no.hashCode();
}
此时再运行方法,就会发现返回的是true了。
2.7 finalize
protected void finalize() throws Throwable { }
该方法用于垃圾回收,一般由 JVM 自动调用,一般不需要程序员去手动调用该方法。
————————————————
版权声明:本文为CSDN博主「t1heluosh1」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/PostersXu/article/details/81947715