类的学习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);
}

4ndhCoi-1706282704264)

注意,静态方法不支持重写,因为它是属于类本身的,但是它可以被继承。

基于这种方法可以重写的特性,对于一个类定义的行为,不同的子类可以出现不同的行为,比如考试,学生考试可以得到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

282704266)

因为抽象方法一定要由子类实现,如果子类都访问不了,那么还有什么意义呢?所以说不能为私有。

接口

接口甚至比抽象类还抽象,他只代表某个确切的功能!也就是只包含方法的定义,甚至都不是一个类!接口一般只代表某些功能的抽象,接口包含了一些列方法的定义,类可以实现这个接口,表示类支持接口代表的功能(类似于一个插件,只能作为一个附属功能加在主体上,同时具体实现还需要由主体来实现)

咋一看,这啥意思啊,什么叫支持接口代表的功能?实际上接口的目标就是将类所具有某些的行为抽象出来。

比如说,对于人类的不同子类,学生和老师来说,他们都具有学习这个能力,既然都有,那么我们就可以将学习这个能力,抽象成接口来进行使用,只要是实现这个接口的类,都有学习的能力:

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);
}

可以发现,原对象和克隆对象,是两个不同的对象,但是他们的各种属性都是完全一样的:

.net%2F2022%2F09%2F22%2FE3dNFYT5sWaS8Rx.png&pos_id=img-LXUWxbsO-1706282704267)

通过实现接口,我们就可以很轻松地完成对象的克隆了,在我们之后的学习中,还会经常遇到接口的使用。

注意:以下内容为选学内容,在设计模式篇视频教程中有详细介绍。

克隆操作可以完全复制一个对象的所有属性,但是像这样的拷贝操作其实也分为浅拷贝和深拷贝。

  • 浅拷贝:对于类中基本数据类型,会直接复制值给拷贝对象;对于引用类型,只会复制对象的地址,而实际上指向的还是原来的那个对象,拷贝个基莫。
  • 深拷贝:无论是基本类型还是引用类型,深拷贝会将引用类型的所有内容,全部拷贝为一个新的对象,包括对象内部的所有成员变量,也会进行拷贝。

那么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);
}```

![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/8519e513495b492abf8c988da8d711c0.png)

>
> 可以看到,虽然Student对象成功拷贝,但是其内层对象并没有进行拷贝,依然只是对象引用的复制,所以Java为我们提供的`clone`方法只会进行浅拷贝。
posted @ 2024-02-28 22:24  笠大  阅读(113)  评论(0)    收藏  举报