类的学习2——【Object类】【方法的重写】【抽象类】【接口】
@
从这里开始,我就开始陆续上传Java程序源代码了,欢迎大家
Gitee https://gitee.com/drip123456/java-se
GIthub https://github.com/Drip123456/JavaSE
顶层Object类
实际上所有类都默认继承自Object类,除非手动指定继承的类型,但是依然改变不了最顶层的父类是Object类。所有类都包含Object类中的方法,比如:

我们发现,除了我们自己在类中编写的方法之外,还可以调用一些其他的方法,那么这些方法不可能无缘无故地出现,肯定同样是因为继承得到的,那么这些方法是继承谁得到的呢?
public class Person extends Object{
//除非我们手动指定要继承的类是什么,实际上默认情况下所有的类都是继承自Object的,只是可以省略
}
所以说我们的继承结构差不多就是:

-J3DcfJk2-1706282704263)
既然所有的类都默认继承自Object,我们来看看这个类里面有哪些内容:
public class Object {
private static native void registerNatives(); //标记为native的方法是本地方法,底层是由C++实现的
static {
registerNatives(); //这个类在初始化时会对类中其他本地方法进行注册,本地方法不是我们SE中需要学习的内容,我们会在JVM篇视频教程中进行介绍
}
//获取当前的类型Class对象,这个我们会在最后一章的反射中进行讲解,目前暂时不会用到
public final native Class<?> getClass();
//获取对象的哈希值,我们会在第五章集合类中使用到,目前各位小伙伴就暂时理解为会返回对象存放的内存地址
public native int hashCode();
//判断当前对象和给定对象是否相等,默认实现是直接用等号判断,也就是直接判断是否为同一个对象
public boolean equals(Object obj) {
return (this == obj);
}
//克隆当前对象,可以将复制一个完全一样的对象出来,包括对象的各个属性
protected native Object clone() throws CloneNotSupportedException;
//将当前对象转换为String的形式,默认情况下格式为 完整类名@十六进制哈希值
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
//唤醒一个等待当前对象锁的线程,有关锁的内容,我们会在第六章多线程部分中讲解,目前暂时不会用到
public final native void notify();
//唤醒所有等待当前对象锁的线程,同上
public final native void notifyAll();
//使得持有当前对象锁的线程进入等待状态,同上
public final native void wait(long timeout) throws InterruptedException;
//同上
public final void wait(long timeout, int nanos) throws InterruptedException {
...
}
//同上
public final void wait() throws InterruptedException {
...
}
//当对象被判定为已经不再使用的“垃圾”时,在回收之前,会由JVM来调用一次此方法进行资源释放之类的操作,这同样不是SE中需要学习的内容,这个方法我们会在JVM篇视频教程中详细介绍,目前暂时不会用到
protected void finalize() throws Throwable { }
}
这里我们可以尝试调用一下Object为我们提供的toString()方法:
public static void main(String[] args) {
Person person = new Student("小明", 18, "男");
String str = person.toString();
System.out.println(str);
}
这里就是按照上面说的格式进行打印:

当然,我们直接可以给println传入一个Object类型的对象:
public void println(Object x) {
String s = String.valueOf(x); //这里同样会调用对象的toString方法,所以说跟上面效果是一样的
synchronized (this) {
print(s);
newLine();
}
}
有小伙伴肯定会好奇,这里不是接受的一个Object类型的值的,为什么任意类型都可以传入呢?因为所有类型都是继承自Object,如果方法接受的参数是一个引用类型的值,那只要是这个类的对象或是这个类的子类的对象,都可以作为参数传入。
我们也可以试试看默认提供的equals方法:
public static void main(String[] args) {
Person p1 = new Student("小明", 18, "男");
Person p2 = new Student("小明", 18, "男");
System.out.println(p1.equals(p2));
}
因为默认比较的是两个对象是否为同一个对象,所以说这里得到的肯定是false,但是有些情况下,实际上我们所希望的情况是如果名字、年龄、性别都完全相同,那么这肯定是同一个人,但是这里却做不到这样的判断,我们需要修改一下equals方法的默认实现来完成,这就要用到方法的重写了。
方法的重写
注意,方法的重写不同于之前的方法重载,不要搞混了,方法的重载是为某个方法提供更多种类,而方法的重写是覆盖原有的方法实现,比如我们现在不希望使用Object类中提供的equals方法,那么我们就可以将其重写了:
public class Person{
...
@Override //重写方法可以添加 @Override 注解,有关注解我们会在最后一章进行介绍,这个注解默认情况下可以省略
public boolean equals(Object obj) { //重写方法要求与父类的定义完全一致
if(obj == null) return false; //如果传入的对象为null,那肯定不相等
if(obj instanceof Person) { //只有是当前类型的对象,才能进行比较,要是都不是这个类型还比什么
Person person = (Person) obj; //先转换为当前类型,接着我们对三个属性挨个进行比较
return this.name.equals(person.name) && //字符串内容的比较,不能使用==,必须使用equals方法
this.age == person.age && //基本类型的比较跟之前一样,直接==
this.sex.equals(person.sex);
}
return false;
}
}
在重写Object提供的equals方法之后,就会按照我们的方式进行判断了:
public static void main(String[] args) {
Person p1 = new Student("小明", 18, "男");
Person p2 = new Student("小明", 18, "男");
System.out.println(p1.equals(p2)); //此时由于三个属性完全一致,所以说判断结果为真,即使是两个不同的对象
}
有时候为了方便查看对象的各个属性,我们可以将Object类提供的toString方法重写了:
@Override
public String toString() { //使用IDEA可以快速生成
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
", profession='" + profession + '\'' +
'}';
}
这样,我们直接打印对象时,就会打印出对象的各个属性值了:
public static void main(String[] args) {
Person person = new Student("小明", 18, "男");
System.out.println(person);
}

注意,静态方法不支持重写,因为它是属于类本身的,但是它可以被继承。
基于这种方法可以重写的特性,对于一个类定义的行为,不同的子类可以出现不同的行为,比如考试,学生考试可以得到A,而工人去考试只能得到D:
public class Person {
...
public void exam(){
System.out.println("我是考试方法");
}
...
}
public class Student extends Person{
...
@Override
public void exam() {
System.out.println("我是学生,我就是小镇做题家,拿个 A 轻轻松松");
}
}
public class Worker extends Person{
...
@Override
public void exam() {
System.out.println("我是工人,做题我并不擅长,只能得到 D");
}
}
这样,不同的子类,对于同一个方法会产生不同的结果:
public static void main(String[] args) {
Person person = new Student("小明", 18, "男");
person.exam();
person = new Worker("小强", 18, "男");
person.exam();
}

这其实就是面向对象编程中多态特性的一种体现。
注意,我们如果不希望子类重写某个方法,我们可以在方法前添加final关键字,表示这个方法已经是最终形态:
public final void exam(){
System.out.println("我是考试方法");
}

或者,如果父类中方法的可见性为private,那么子类同样无法访问,也就不能重写,但是可以定义同名方法:
li.net%2F2022%2F09%2F21%2Fd9k21hyGL6WExZ3.png&pos_id=img-s3XgtiBq-1706282704265)
虽然这里可以编译通过,但是并不是对父类方法的重写,仅仅是子类自己创建的一个新方法。
还有,我们在重写父类方法时,如果希望调用父类原本的方法实现,那么同样可以使用super关键字:

@Override
public void exam() {
super.exam(); //调用父类的实现
System.out.println("我是工人,做题我并不擅长,只能得到 D");
}
然后就是访问权限的问题,子类在重写父类方法时,不能降低父类方法中的可见性:
public void exam(){
System.out.println("我是考试方法");
}

因为子类实际上可以当做父类使用,如果子类的访问权限比父类还低,那么在被当做父类使用时,就可能出现无视访问权限调用的情况,这样肯定是不行的,但是相反的,我们可以在子类中提升权限:
protected void exam(){
System.out.println("我是考试方法");
}
@Override
public void exam() { //将可见性提升为public
System.out.println("我是工人,做题我并不擅长,只能得到 D");
}

可以看到作为子类时就可以正常调用,但是如果将其作为父类使用,因为访问权限不足所有就无法使用,总之,子类重写的方法权限不能比父类还低。
抽象类
在我们学习了类的继承之后,实际上我们会发现,越是处于顶层定义的类,实际上可以进一步地进行抽象,比如我们前面编写的考试方法:
protected void exam(){
System.out.println("我是考试方法");
}
这个方法再子类中一定会被重写,所以说除非子类中调用父类的实现,否则一般情况下永远都不会被调用,就像我们说一个人会不会考试一样,实际上人怎么考试是一个抽象的概念,而学生怎么考试和工人怎么考试,才是具体的一个实现,所以说,我们可以将人类进行进一步的抽象,让某些方法完全由子类来实现,父类中不需要提供实现。
要实现这样的操作,我们可以将人类变成抽象类,抽象类比类还要抽象:
public abstract class Person { //通过添加abstract关键字,表示这个类是一个抽象类
protected String name; //大体内容其实普通类差不多
protected int age;
protected String sex;
protected String profession;
protected Person(String name, int age, String sex, String profession) {
this.name = name;
this.age = age;
this.sex = sex;
this.profession = profession;
}
public abstract void exam(); //抽象类中可以具有抽象方法,也就是说这个方法只有定义,没有方法体
}
而具体的实现,需要由子类来完成,而且如果是子类,必须要实现抽象类中所有抽象方法:
public class Worker extends Person{
public Worker(String name, int age, String sex) {
super(name, age, sex, "工人");
}
@Override
public void exam() { //子类必须要实现抽象类所有的抽象方法,这是强制要求的,否则会无法通过编译
System.out.println("我是工人,做题我并不擅长,只能得到 D");
}
}
抽象类由于不是具体的类定义(它是类的抽象)可能会存在某些方法没有实现,因此无法直接通过new关键字来直接创建对象:

要使用抽象类,我们只能去创建它的子类对象。
抽象类一般只用作继承使用,当然,抽象类的子类也可以是一个抽象类:
public abstract class Student extends Person{ //如果抽象类的子类也是抽象类,那么可以不用实现父类中的抽象方法
public Student(String name, int age, String sex) {
super(name, age, sex, "学生");
}
@Override //抽象类中并不是只能有抽象方法,抽象类中也可以有正常方法的实现
public void exam() {
System.out.println("我是学生,我就是小镇做题家,拿个 A 轻轻松松");
}
}
注意,抽象方法的访问权限不能为private:

因为抽象方法一定要由子类实现,如果子类都访问不了,那么还有什么意义呢?所以说不能为私有。
接口
接口甚至比抽象类还抽象,他只代表某个确切的功能!也就是只包含方法的定义,甚至都不是一个类!接口一般只代表某些功能的抽象,接口包含了一些列方法的定义,类可以实现这个接口,表示类支持接口代表的功能(类似于一个插件,只能作为一个附属功能加在主体上,同时具体实现还需要由主体来实现)
咋一看,这啥意思啊,什么叫支持接口代表的功能?实际上接口的目标就是将类所具有某些的行为抽象出来。
比如说,对于人类的不同子类,学生和老师来说,他们都具有学习这个能力,既然都有,那么我们就可以将学习这个能力,抽象成接口来进行使用,只要是实现这个接口的类,都有学习的能力:
public interface Study { //使用interface表示这是一个接口
void study(); //接口中只能定义访问权限为public抽象方法,其中public和abstract关键字可以省略
}
我们可以让类实现这个接口:
public class Student extends Person implements Study { //使用implements关键字来实现接口
public Student(String name, int age, String sex) {
super(name, age, sex, "学生");
}
@Override
public void study() { //实现接口时,同样需要将接口中所有的抽象方法全部实现
System.out.println("我会学习!");
}
}
public class Teacher extends Person implements Study {
protected Teacher(String name, int age, String sex) {
super(name, age, sex, "教师");
}
@Override
public void study() {
System.out.println("我会加倍学习!");
}
}
接口不同于继承,接口可以同时实现多个:
public class Student extends Person implements Study, A, B, C { //多个接口的实现使用逗号隔开
}
所以说有些人说接口其实就是Java中的多继承,但是我个人认为这种说法是错的,实际上实现接口更像是一个类的功能列表,作为附加功能存在,一个类可以附加很多个功能,接口的使用和继承的概念有一定的出入,顶多说是多继承的一种替代方案。
接口跟抽象类一样,不能直接创建对象,但是我们也可以将接口实现类的对象以接口的形式去使用:

当做接口使用时,只有接口中定义的方法和Object类的方法,无法使用类本身的方法和父类的方法。
接口同样支持向下转型:
public static void main(String[] args) {
Study study = new Teacher("小王", 27, "男");
if(study instanceof Teacher) { //直接判断引用的对象是不是Teacher类型
Teacher teacher = (Teacher) study; //强制类型转换
teacher.study();
}
}
这里的使用其实跟之前的父类是差不多的。
从Java8开始,接口中可以存在方法的默认实现:
public interface Study {
void study();
default void test() { //使用default关键字为接口中的方法添加默认实现
System.out.println("我是默认实现");
}
}
如果方法在接口中存在默认实现,那么实现类中不强制要求进行实现。
接口不同于类,接口中不允许存在成员变量和成员方法,但是可以存在静态变量和静态方法,在接口中定义的变量只能是:
public interface Study {
public static final int a = 10; //接口中定义的静态变量只能是public static final的
public static void test(){ //接口中定义的静态方法也只能是public的
System.out.println("我是静态方法");
}
void study();
}
跟普通的类一样,我们可以直接通过接口名.的方式使用静态内容:
public static void main(String[] args) {
System.out.println(Study.a);
Study.test();
}
接口是可以继承自其他接口的:
public interface A exetnds B {
}
并且接口没有继承数量限制,接口支持多继承:
public interface A exetnds B, C, D {
}
接口的继承相当于是对接口功能的融合罢了。
最后我们来介绍一下Object类中提供的克隆方法,为啥要留到这里才来讲呢?因为它需要实现接口才可以使用:
package java.lang;
public interface Cloneable { //这个接口中什么都没定义
}
实现接口后,我们还需要将克隆方法的可见性提升一下,不然还用不了:
public class Student extends Person implements Study, Cloneable { //首先实现Cloneable接口,表示这个类具有克隆的功能
public Student(String name, int age, String sex) {
super(name, age, sex, "学生");
}
@Override
public Object clone() throws CloneNotSupportedException { //提升clone方法的访问权限
return super.clone(); //因为底层是C++实现,我们直接调用父类的实现就可以了
}
@Override
public void study() {
System.out.println("我会学习!");
}
}
接着我们来尝试一下,看看是不是会得到一个一模一样的对象:
public static void main(String[] args) throws CloneNotSupportedException { //这里向上抛出一下异常,还没学异常,所以说照着写就行了
Student student = new Student("小明", 18, "男");
Student clone = (Student) student.clone(); //调用clone方法,得到一个克隆的对象
System.out.println(student);
System.out.println(clone);
System.out.println(student == clone);
}
可以发现,原对象和克隆对象,是两个不同的对象,但是他们的各种属性都是完全一样的:

通过实现接口,我们就可以很轻松地完成对象的克隆了,在我们之后的学习中,还会经常遇到接口的使用。
注意:以下内容为选学内容,在设计模式篇视频教程中有详细介绍。
克隆操作可以完全复制一个对象的所有属性,但是像这样的拷贝操作其实也分为浅拷贝和深拷贝。
- 浅拷贝:对于类中基本数据类型,会直接复制值给拷贝对象;对于引用类型,只会复制对象的地址,而实际上指向的还是原来的那个对象,拷贝个基莫。
- 深拷贝:无论是基本类型还是引用类型,深拷贝会将引用类型的所有内容,全部拷贝为一个新的对象,包括对象内部的所有成员变量,也会进行拷贝。
那么clone方法出来的克隆对象,是深拷贝的结果还是浅拷贝的结果呢?
public static void main(String[] args) throws CloneNotSupportedException {
Student student = new Student("小明", 18, "男");
Student clone = (Student) student.clone();
System.out.println(student.name == clone.name);
}```

>
> 可以看到,虽然Student对象成功拷贝,但是其内层对象并没有进行拷贝,依然只是对象引用的复制,所以Java为我们提供的`clone`方法只会进行浅拷贝。

浙公网安备 33010602011771号