Java---抽象类与接口

发布于:2025-04-13 ⋅ 阅读:(13) ⋅ 点赞:(0)

前言

上期我们介绍了类与对象的知识点,那么这期我先为大家带来关于抽象类与接口的具体的讲解,希望大家能更进一步理解Java中的特点,下节再为大家介绍两大特性的详解。

一、抽象类

1.抽象类的概念

在面向对象的概念中,所有的对象都是通过类来进行描绘的,但是并不是所有的类来进行描绘对象的。
如果一个类中没有足够的信息内容来回一个具体的对象,那么这种类就是抽象类

2.抽象类的语法

1.抽象类都是被abstract类修饰的叫做抽象类

public abstract class A{
	
}

2.抽象类中的成员方法用abstract修饰的方法,叫做抽象方法

public abstract class A{
	public abstract void test();//抽象方法
}

3.抽象类的特点

  1. 通过上述方法我们可以得知:一个类中有抽象方法,那么这个类必是抽象类
  2. 抽象类中也可以有普通成员变量和普通成员方法
    比如:
abstract class Shape{
    public int a = 10;//普通成员变量
    public void draw(){//普通成员方法
        System.out.println("画图形");
    }
    public abstract void draw2();//抽象方法
}
  1. 抽象类不能直接实例化

	public static void main(String[] args) {
	        Shape shape = new Shape();
    }    

在这里插入图片描述
抽象类不能实例化,那么是用来干什么的呢?
当然是用来被继承的了,所以抽象类也可以实现了向上转型

  1. 抽象类中的方法是不能被private修饰的
    private abstract void draw2();

在这里插入图片描述

  1. final 和 abstract 两者不能同时存在,被abstract修饰的方法就是要被继承的与重写的,而final修饰的不能被重写和继承,属于是密封类和密封方法
public abstract final void draw2();

在这里插入图片描述

  1. 抽象方法不能被static修饰,因为抽象方法要被子类重写,而static是属于类本身的方法
public static final void draw2();

在这里插入图片描述

  1. 当一个普通类继承了这个抽象类之后,这个普通类一定要重写这个抽象类的当中的所有的抽象方法
abstract class Shape{
    public int a = 10;
    public void draw(){
        System.out.println("画图形");
    }
    public abstract void draw2();//抽象方法
    public void test(){
    
    }
}
class React extends Shape{
    @Override
    public void draw2() {
        System.out.println("矩形");
    }
}
class Flower extends Shape{
    @Override
    public void draw2() {
        System.out.println("花" );
    }
}
  1. 子类如果不想重写抽象方法,那么就把子类也设为抽象类
    比如这有一个子类A
abstract class A extends Shape{
    public abstract void testDemo();
}

但是我们会想到,如果一直设计为抽象子类,那么它们的抽象方法怎么办?

class  B extends A{
    @Override
    public void draw2() {

    }
    @Override
    public void testDemo() {

    }
}

所以B继承了A,A继承了Shape,两个类都是抽象类,所以这个普通类B类就要重写两个方法

  1. 当一个抽象类A不想被一个普通类B继承,此时可以把这个B类变成抽象类,那么在当一个普通类C继承这个抽象类B之后,C要重写B和A的里面的所有抽象方法

4.抽象类的操作

给出完整的代码,让我们分析一下:

abstract class Shape{
    public int a = 10;

    public void draw(){
        System.out.println("画图形");
    }
    public abstract void draw2();
    public void test(){

    }

    public Shape() {
        //既然它不能实例化,但是可以让子类调用帮助这个抽象类初始化它自己的成员
    }
}
class React extends Shape{
    @Override
    public void draw2() {
        System.out.println("矩形");
    }
}
class Flower extends Shape{
    @Override
    public void draw2() {
        System.out.println("花" );
    }
}
public class Test {
    public static void drawMap(Shape shape) {
        shape.draw2();
    }
    public static void main(String[] args) {
        Shape shape = new React();
        drawMap(new React());
        drawMap(new Flower());
       
    }
Shape shape1 = new React();
Shape shape2 = new Flower();

这个操作是向上转型,在我们调用了一些抽象方法的时候,由于我们将其抽象方法进行了重写,进而发生了动态绑定,从而发生多态。

 	public static void drawMap(Shape shape) {
      	shape.draw2();
   }

这个方法我们来接收Shape类型,但是由于子类继承父类,构成了协变类型,也会发生向上转型,往里面传入了参数时,再调用抽象方法,这个时候发生了动态绑定,就会发生了多态

new React() new Flower() 这种没有名字的对象 —> 匿名对象
匿名对象的缺点:每次使用,都得去重新实例化
所以我们可以使用向上转型的直接赋值,来去传进参数

	public static void main(String[] args) {
	        Shape shape1 = new React();
	        Shape shape2 = new Flower();
	        drawMap(shape1);
	        drawMap(shape2);
       
    }

在这里插入图片描述

5.抽象类的作用

我们都知道普通的类也能被继承,那还需要抽象类来做什么?
但是我们正常都需要跟业务和实际情况来进行选择,
使用抽象类相当于多了一重编译器的校验:
使用抽象类的场景就如上面的代码,实际工作不应该由父类完成,而应由子类完成.。
那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的;
但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题。
意义
很多语法存在的意义都是为了 “预防出错”, 例如我们曾经用过的 final 也是类似. 创建的变量用户不去修改, 不
就相当于常量嘛? 但是加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们.
充分利用编译器的校验, 在实际开发中是非常有意义的.

二、接口

1.接口的概念

比如:我们都知道电脑上面有USB接口,可以插些:U盘,鼠标等符合USB协议的设备;

接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。
在Java中,接口可以看成是多个类的公共规范,是一种引用数据类型

2.接口语法

public interface 接口名称{
	//抽象方法
	//成员变量
}

一般的接口名称前加一个I字母。

interface IShape{//一般情况下我们以I开头来表示接口的
    int a = 10;
    void draw();
    default void test(){
        System.out.println("test()");
    }
    public static void test2(){
        System.out.println(" static test2()");
    }
}

在这个接口中我没有看到修饰符,难道是默认的吗?
并不是默认的!
原因这是在接口中,成员变量默认是被public static final修饰的,成员方法是抽象方法,但是默认被public abstract修饰的但是这种我们一般不写
接口中的方法一般不能实现,都是抽象方法,但是从JDK8之后,可以支持default修饰的成员方法和static修饰的成员方法中可以有具体的实现(即主体)

3.接口的使用与特性

  1. 接口的成员方法必须是默认为public abstract来修饰的。
    其他修饰符不可以;private 和 protected还有default均不可以,都会出现报错
    接口类型也是一种引用类型
  2. 接口不能有普通成员方法,也不能有成员变量
interface IShape{//一般情况下我们以I开头来表示接口的
    private int a = 10;
    void draw();
    default void test(){
        System.out.println("test()");
    }
    public void test2(){
        System.out.println(" static test2()");
    }
}

在这里插入图片描述
在这里插入图片描述
就算你写成了public的成员变量,这时候也是默认成public static final修饰的,所以不会出现错误

  1. 接口不能被new关键字来进行实例化,但是也可以用向上转型
IShape iShape = new IShape();

在这里插入图片描述

  1. 类实现了接口,那么该子类就要重写接口中的所有抽象的方法,用implements关键字来实现
class Rect implements IShape{
    @Override
    public void draw() {
        System.out.println("矩形");
    }
}
class Flower implements IShape{
    @Override
    public void draw() {
        System.out.println("花");
    }
}
  1. 如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类
abstract class A implements IShape{
    
}

当然了,还是要还的,因为他只能被用来继承,所以这种类被继承后还是需要重写接口中的方法

  1. 当一个类实现了接口当中的方法之后,当前类中的方法不能不加public,因为原来接口中的方法是public修饰的,所以重写的方法要大于等于public的范围,所以必须是public修饰
class Rect implements IShape{
    @Override
    void draw() {
        System.out.println("矩形");
    }
}

在这里插入图片描述

  1. 接口当中,不能使用构造方法和静态代码块
interface IShape{//一般情况下我们以I开头来表示接口的
  	public IShape(){
        
    }
    static{
        
    }
}

在这里插入图片描述

4.实现多个接口

在Java中,类和类之间是单继承的,一个类只能有一个父类,即Java中不支持多继承,但是一个类可以实现多个接口。
在父类中是放入一些共性的特点,而不是特有的,是共有的
比如我们创建了一个动物类,再创建几个具体的动物的类,再分别创建几个接口:

//游泳接口
public interface ISwimming {
    void swim();
}
//跑步接口
public interface IRunning {
    void run();
}
//飞行接口
public interface IFlying {
    void fly();
}
//动物类:
public abstract class Animals {
    public String name;
    public int age;

    public Animals(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public abstract void eat();
}

鸟可以进行飞行和跑步,所以实现这两接口,并且继承了动物类

//鸟类
public class Bird extends Animals implements IFlying, IRunning {
    public Bird(String name, int age) {
        super(name, age);//帮助父类进行构造,用super来进行访问父类
    }

    @Override
    public void eat() {
        System.out.println(this.name + "正在吃饭!");
    }

    @Override
    public void fly() {
        System.out.println(this.name + "正在用翅膀飞!");
    }

    @Override
    public void run() {
        System.out.println(this.name + "正在用鸟腿跑!");
    }
}

狗类是可以进行跑步和游泳,所以实现这两个接口,并且继承了父类

//狗类
public class Dog extends Animals implements IRunning, ISwimming {
    public Dog(String name, int age) {
        super(name, age);//帮助父类进行构造
    }
    @Override
    public void eat() {
        System.out.println(this.name + "正在吃狗粮!");
    }

    @Override
    public void run() {
        System.out.println(this.name + "正在用狗腿跑!");
    }

    @Override
    public void swim() {
        System.out.println(this.name + "正在狗腿游泳!");
    }
}

主类

public class Test {
    public static void test1(Animals animals){
        animals.eat();
    }
    public static void test4(IFlying iFlying){
        iFlying.fly();
    }
    public static void test2(IRunning iRunning){
        iRunning.run();
    }
    public static void test3(ISwimming iSwimming){
        iSwimming.swim();
    }
    public static void main(String[] args) {
        Bird bird = new Bird("小鸟",1);
        Dog dog = new Dog("小狗",10);
        test1(dog);
        test1(bird);
        
        test2(bird);
        test2(dog);
        System.out.println("============");
        test3(dog);
        //test3(bird);bird没有实现swimming的接口
        System.out.println("============");
        test4(bird);
        //test4(dog);dog没有实现了flying的接口
       
    }
}

在这里插入图片描述
上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口

语法上先继承后实现,否则会报错
继承表示是其中的一种,接口表示的是它具有什么特性,能做什么。

这时候我们在创建一个机器人类:

public class Robot implements IRunning {
    @Override
    public void run() {
        System.out.println("机器人在跑!");
    }
}

比如我们在主类中测试

test2(new Robot());

在这里插入图片描述

那么这种操作的好处是什么?
时刻牢记多态的好处, 让我们忘记类型. 有了接口之后, 类的使用者就不必关注具体类型,
而只关注某个类是否具备某种能力

我们创建了这个机器人类,实现了跑步的接口,所以直接去实现调用test2的方法,这个时候我们就可以发现了这个接口,回避了向上转型会使其更加灵活。

5.接口之间的继承

在Java中,类和类之间是单继承的,但是一个类可以实现多个接口,接口与接口之间可以多继承。
用接口达到多继承的目的。
接口的继承用extends关键字

interface A{
    void testA();
}
interface B{
    void testB();
}
interface C{
    void testC();
}
interface D extends B,C{
    //D 这个接口具备了B和C接口的功能
    //并且D还可以有自己的方法
    void testD();
}

在接口中是进行抽象方法的声明,所以接口之间继承不用重写抽象方法
当类来实现这个D接口的时候,我们要重写D接口中的方法和D所继承的接口中的方法

public class Test implements D{
    @Override
    public void testB() {

    }

    @Override
    public void testC() {

    }

    @Override
    public void testD() {

    }
    //就如同子孙类一样

总结:
接口间的继承相当于把多个接口合并在一起。
在接口中是进行抽象方法的声明,所以接口之间继承不用重写抽象方法。

6.接口的实例

(1).对象大小的比较

当我们先给出了几个对象,从而想要按照一定顺序比较其中内容的大小
我们先创建了一个Student类

class Student {
    public String name ;
    public int age;
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

这个时候我们再创建一个主类

public class Test {
	public static void main(String[] args) {
        Student student1 = new Student("zhangsan", 10);
        Student student2 = new Student("lisi", 15);
        System.out.println(student1 > student2);
    }
}    

这么运行会出现错误;(因为两者都是引用类型,无法直接进行比较)
在这里插入图片描述
所以这个时候我们用接口来实现这个自定义类型的比较,所以分别是 Comparable和Comparator接口

(1).Comparable接口

这个是接口Comparable中的源码
在这里插入图片描述
这个接口中涉及到后续的泛型,关于这个泛型到后期的数据结构我们在详细的讲解
如果我们要实现比较的方法,就要用compareTo这个方法,
当然如果直接使用:

System.out.println(student1.compareTo(student2));

在这里插入图片描述
因为compareTo是Comparable中的方法,所以要想使用这个方法,就要实现这个接口,并指定比较的类型用<>

class Student implements Comparable<Student>{
    public String name ;
    public int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(Student o) {//重写的CompareTo方法
        return this.age - o.age;
        //大于0,则前者大于后者
        //小于0,前者小于后者
        //this.age表示的是student1,o.age表示的是student2
	}
}

public class Test {
	public static void main(String[] args) {
        Student student1 = new Student("zhangsan", 10);
        Student student2 = new Student("lisi", 15);
        System.out.println(student1.compareTo(student2));
    }
}

在这里插入图片描述
如果我们给出的是student的对象数组,让其大小进行排序,不作对student类进行修改,只在主类中进行实例化

import java.util.Arrays;
public class Test {
    public static void main(String[] args) {
        //利用对象数组
        Student[] students = new Student[3];
        students[0] = new Student("zhangsan",10);
        students[1] = new Student("lisi",2);
        students[2] = new Student("liwu",18);

       	Arrays.sort(students);//这个时候要用comparable来进行一定的顺序排序.
        System.out.println(Arrays.toString(students));
    }
}

在这里插入图片描述
因为通过comparable这个接口来进行查找一定的内容,从而按一定的内容来进行比较大小
当然,我们想要按其他内容的时候进行排序,需要改动某些代码,这时候就容易会导致某些错误。

(2).Comparator接口

Comparator接口中有compare的方法为默认权限,所以我们调用这个方法
这种方式叫做比较器,在类的外部进行实现

class Student2{
    public String name;
    public int age;

    @Override
    public int compareTo(Student2 o) {
        return this.age - o.age;
    }

    public Student2(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

比如我们要按对象的年龄大小进行比较,这时候再创建一个age类,实现了Comparator这个接口

class AgeComparator implements Comparator<Student2>{
    @Override
    public int compare(Student2 o1, Student2 o2) {
        return o1.age - o2.age;
    }
}

这个时候我们先把在外部的类实例化,进而要比较需要两个对象的变量传递到compare方法中去,从去重写这个方法,
并且发生向上转型(参数类型)

import java.util.Comparator;
public class Test2 {
    public static void main(String[] args) {
        Student2 student1 = new Student2("zhangsan", 10);
        Student2 student2 = new Student2("lisi", 15);
        AgeComparator ageComparator = new AgeComparator();
        System.out.println(ageComparator.compare(student1, student2));
	}
}

当我们按照name进行比较的时候,这个时候在创建了Name类,再去实现了Comparator这个接口

class NameComparator implements Comparator<Student2>{
    @Override
    public int compare(Student2 o1, Student2 o2) {
        return o1.name.compareTo(o2.name);
    }
}

name是引用类型,不能进行相减来进行比较,但是我们发现了String类中也有去实现Comparable的方法
所以我们只需要重写compareTo方法即可
在这里插入图片描述
当然我们还是需要进行将Name类进行类的实例化

 NameComparator nameComparator = new NameComparator();
 System.out.println(nameComparator.compare(student1, student2));

在这里插入图片描述

关于对于自定义类型比较的总结:
Comparator这种方式的优势就是对于类的侵入性不强,会比较灵活
Comparable这种方式对于类的侵入性比较强,所以做好不要去改动原有的逻辑,比如我按名字排序这个时候就会容易发生变化
并且两者可以共存,互不干扰;因为comparable是对原有的类进行实现,comparator是对要比较内容的类而实现,所以互不干扰

(2).实现类的克隆

(1).类的克隆

Java中有许多的内置接口,而Cloneable就是一个非常有用的接口
Object类中有一个clone方法,调用这个对象可实现创建一个对象的“拷贝”,但是想要成功调用这个方法,就要实现这个其中的接口

class Person {
    public int age;
    public Person(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person{" +
                " age=" + age +
                " }";
    }
}
public class Test3 {
    public static void main(String[] args){
        Person person1 = new Person(10);
        System.out.println(person1);
        Person person2 = person1.clone();
        System.out.println(person1);
    }
}

例如:我们创建一个person类,存入了一个年龄的成员变量,并重写了toString方法;
然后我们对Person类实例化,并存入了一个age为10的值让其构造方法为它自己进行初始化,这个时候我们直接打印了person1,这个时候我们再创建一个变量person2来接收person1的拷贝后的值。
但是如果我们直接这么做,直接去调用:

Person person1 = person.clone();

在这里插入图片描述
这个时候就会出现这种错误;
因为clone方法是在object类中的方法,所以我们点击查看clone的源码:

protected native Object clone() throws CloneNotSupportedException;

这个时候我们就会发现了这个clone方法是本地方法,是用C/C++来写的,所以我们也无法知道这个具体的过程实现,并且我们也看到了这个方法是被protected修饰的,它的访问权限是在不同包的子类可以访问不同包的父类被protected修饰的成员变量和方法, 但是也需要用super来访问,所以需要重写这个clone方法;

class Person {
    public int age;
    public Person(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person{" +
                " age=" + age +
                " }";
    }
	@Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();//调用object类的方法来进行访问
    }
}

在这里插入图片描述
这个时候我们会发现还是有错误啊,但是这时候提示我们类型不兼容,因为Object类是所有的类的父类,但是其中clone方法是比较特殊的,它返回的是object类型,但是它并没有与Person构成显示的父与子的协变类型,所以这也反映了不构成重写的条件之一,所以会出现这种错误,但是我们把这个类型强制转换为Person类,所以就可以了

public class Test3 {
    public static void main(String[] args){
        Person person1 = new Person(10);
        System.out.println(person1);
        Person person2 = (Person) person1.clone();
        System.out.println(person1);
    }
}

这个时候在运行结果:
在这里插入图片描述
这个时候还是报了错误,告诉我们还有异常这错误,但是异常处理我们后期会详细讲解,因为clone方法的异常是受查异常/编译时异常,所以必须在编译时处理:

public class TestDemo {
    public static void main(String[] args) throws CloneNotSupportedException{//必须在编译时处理
        Person person1 = new Person(10);
        System.out.println(person1);
        Person person2 = (Person) person1.clone();
        System.out.println(person2);
}

但是我们运行完,还是会出现错误,这可是真愁人
在这里插入图片描述
CloneNotSupportedException,不支持克隆,告诉我们Person类不支持克隆
所以我们这时候就要提到前面所说的Cloneable接口了,这个时候我们就用Person实现这个接口,
那么运行结果:
在这里插入图片描述
这时候我们就完成了person2对person1的所指的对象的克隆
这个时候我们来看一下Cloneable这个接口:
在这里插入图片描述
Cloneable是一个空接口,我们可以在IDEA中去查看源码:
它是一个空接口,也作为一种标记接口,因为它用来证明当前类是可以被克隆的

在堆上,创建了一个对象,假设地址是0X98,那么在虚拟机栈上得到是person1所指这个对象的地址,通过clone的方法来进行克隆出堆中一个相同的一份的对象,但是两者(person1和person2)在堆上不是一个位置,所以person2这个克隆后的获得了一个新的地址0X88(假设的),所以我们通过 return super.clone() 来克隆。

那么就来总结一下Person person1 = person.clone();这个其中的所有的错误:

  1. 修饰符的错误,protected,这一步用super.clone()访问object类,来重写这个clone方法
  2. 异常,clone方法的异常是受查异常/编译时异常,所以必须在编译时处理,(在main后面加入throws)
  3. 向下转型,进行强制转换,因为我们用的是object类中的clone方法,所以返回类型是固定的object类,而不是构成了协变类型。所以我们要进行向下转型,将其转换为子类;进行了上述的操作,还是会报错:CloneNotSupportedException,不支持克隆。
  4. 所以我们要实现Cloneable这个接口,从而才能使用

我的理解:clone方法实现的具体操作,用super.clone来访问,Cloneable判断这个类是否支持克隆操作

(2).浅拷贝

这时候我们在Person类的基础上再重新创建了一个Money类,让它与其Person类构成组合

class Money{
    public double money = 19.9;
}
class Person1 implements Cloneable{
    public int age;
    public Money m;

    public Person1(int age) {
        this.age = age;
        this.m = new Money();//在构造方法中实例化一个money
    }
    @Override
    public String toString() {
        return "Person{" +
                " age=" + age +
                " }";
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class TestDemo {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person1 person1 = new Person1(10);
        Person1 person2 = (Person1) person1.clone();
        System.out.println(person1);
        System.out.println(person2);
        System.out.println("===============");
        System.out.println(person1.m.money);
        System.out.println(person2.m.money);//先调用的是person1的成员变量m,
        //由于在构造方法中进行了m的实例化,所以我还可以调用Money类中的成员变量
        System.out.println("===============");
        //将person2进行修改,正常我们所期望的是只有person2进行修改,person1没有变化
        person2.m.money = 99.99;
        System.out.println(person1.m.money);
        System.out.println(person2.m.money);
    }
}

this.m = new Money();也就是在堆上的空余的空间再创建了一个实例化Money的对象,并且这个m在堆上那个原本的空间占有的一定内存空间。

person1.m.money为什么会这么调用money?
这个m是Peron1类的成员变量,但是我们也在Person1类中的构造方法去实例化Money这个类,所以这个m又是Money实例化的变量名,再用这个m来调用了Money类中的成员变量money

将person2进行修改,正常我们所期望的是只有person2进行修改,person1没有变化,
在这里插入图片描述
但是可惜的是两者都发生了变化。
所以我们可以通过图例来演示一下这个过程:
在这里插入图片描述

在虚拟机栈上,会创建两个空间分别是person1和person2,
person1所实例化的对象会在堆上创建一个对象的空间(地址为0x98),存入两个成员变量,但是其中m是Money类型即是引用类型,并且也是实例化了,所以那么也会是创建money类型的对象的空间(地址为0x65),这时候在对第一个对象中的成员变量m也会得到它实例化的地址
person1.clone()是把person1指向的对象进行克隆,但是没把这个对象的空间中成员变量m所指的对象进行克隆
所以那也意味着成员变量m还是指的是0x65的它自己的对象空间Money,这个对象的空间的成员变量是money
person2还是指的是第一次的对象中的对象的成员变量money;
所以这个时候堆money的修改还是一次就改变person1和person2所指的money的值,两者还是一样的

此时这个现象被称之为浅拷贝:并没有将对象中的对象去进行克隆

(3).深拷贝
class Money1 implements Cloneable{
    public double money = 19.9;
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
class Person2 implements Cloneable{
    public int age;
    public Money1 m;
    public Person2(int age) {
        this.age = age;
        this.m = new Money1();//在构造方法中实例化一个money
    }
    @Override
    public String toString() {
        return "Person{" +
                " age=" + age +
                " }";
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person2 tmp = (Person2) super.clone();
        //tmp.m类的调用成员变量,this.m.clone()中的this是指这个对象的成员变量m的引用
        tmp.m = (Money1) this.m.clone();//谁调用某个方法谁就是this,现在是person1来调用,那么person1就是this
        return tmp;
    }
}
public class Test5 {
    public static void main(String[] args) throws CloneNotSupportedException{
        Person2 person1 = new Person2(10);
        Person2 person2 = (Person2) person1.clone();
        System.out.println(person1.m.money);
        System.out.println(person2.m.money);
        System.out.println("===============");
        person2.m.money = 99.9;
        System.out.println(person1.m.money);
        System.out.println(person2.m.money);   
    }
}

这时候我们把Money1类也进行实现了Cloneable的接口,这时候我们再对clone方法的重写,
但是我们会发现两者的重写的clone方法不同,在Person2类中的重写方法,创建一个临时变量tmp,通过将其向下转型变成了Person2类型,tmp.m是Person2类的调用成员变量m,this.m.clone()就是person1正在调用clone方法,这个时候先访问了Pesron2类中成员变量m,再去通过Money1类的实例化之后再调用clone方法,这个时候最终是Money1这个类型去调用这个方法;
但是我们会发现tmp是局部变量,除了生命作用域就会销毁,但是最终返回的是给person2,又因为person2是引用类型,会得到了tmp的地址。

所以我们可以通过图例来演示一下这个过程:
在这里插入图片描述

在虚拟机栈上,会创建两个空间分别是person1和person2,还有一个在person类中创建一个临时变量tmp,
因为是把对象的对象进行克隆,所以要再类中的另一个类(该类作为成员变量)中重写clone方法,但是在原有的类中的clone方法也需要进行修改
person1所实例化的对象会在堆上创建一个对象的空间(地址为0x98),存入两个成员变量,但是其中m是Money类型即是引用类型,
并且也是实例化了,所以那么也会是创建money类型的对象的空间(地址为0x65),这时候在对中的成员变量m也会得到它实例化的地址
因为我们还在Person类中修改了重写方法;通过创建了一个临时变量tmp也是为Person类型,先把外面的对象先进行克隆完成,
这时候再通过这个临时变量去调用Person类中的m(其实也就是类的引用),
由于我将m这个变量也进行了Money类的实例化的操作,所以这时候再用m去调用正常的clone方法,从而通过去调用Money类中的clone方法
这个就完成了克隆那么Money类的克隆的地址(0x888),但是tmp是临时的局部的变量,出了作用域就销毁,最终返回的是给的是person2
[person2 = (Person2) person1.clone()]这个就是返回了person2么
因为它是类类型(引用类型)所以这个时候的person2就会得到它的地址(0x91)
最终person2和person1所指的两个值是不一样

此时这个现象被称之为深拷贝:完全将对象中的对象去进行克隆。

注意:深拷贝与浅拷贝就是看的是代码实现的过程,跟克隆方法没有关系

三、抽象类和接口的区别(面试常考)

(1).抽象类中可以有普通成员方法和成员变量,其中还可以有抽象方法,如果子类继承抽象类,必须要重写抽象类中的抽象方法,如果不想重写抽象方法,那么就要将子类设计为抽象类
一个类可以实现多个接口(模拟多继承),但是类与类之间的继承必须是单继承的关系,继承用extends关键字
(2).接口中不可以有普通的成员方法和成员变量,它的成员变量默认是public static final修饰,成员方法默认是public abstract修饰,如果类要实现接口,就要重写接口中的抽象方法
实现用implements关键字,接口之间也可以继承多个接口(这种操作也是多继承),这种操作不用父接口的抽象方法;但是用类实现时,要注意重写接口与被继承接口的所有的抽象方法。

这期内容就分享到这里了,希望大家可以获得新的知识,当然,如果有哪些细节和内容不足,欢迎大家在评论区中指出!