[java篇]多态,抽象类,和接口

🥇多态

博主在上一篇写了关于包,继承和组合相关的博客,介绍了面向对象编程的三大特性之一,详细请看上篇文章【java篇】包,继承和组合

向上转型

在我们了解多态之前,让我们先搞明白向上转型,因为它是学懂多态的前提

那么肯定有童鞋问,到底什么是向上转型呢?

向上转型:其实就是父类引用 去引用子类的对象。

详细请看如下代码:

class Animal{
    
}
class Dog extends Animal{
    
}
public class TestDemo {
    public static void main(String[] args) {
    //在这里Animal该类为父类,Dog类为子类
     Animal animal = new Animal();
     Dog dog = new Dog();
     animal = dog;//父类引用 引用子类的对象
    }
}

其实以上的代码还可以进行简化.

public class TestDemo {
    public static void main(String[] args) {
    //在这里Aniamal该类为父类,Dog类为子类
     Animal animal = new Dog;
    }
}

实现向上转型的方法有三种:

  1. 直接赋值法

  2. 方法传参

  3. 方法返回

    在我们介绍向上转型的开始,就是用到了直接传参法。

    方法传参:

    func方法中形参类型为Animal,当子类对象传到func方法中dog对象的类型就变成了Animal,间接的使父类引用去引用了子类对象。

    public class TestDemo {
        public static void func(Animal animal){
            
        }
        public static void main(String[] args) {
        //向上转型:指的是父类引用去引用子类的对象
        //在这里Animal类为父类,Dog类为子类
         Animal animal = new Animal();   
         Dog dog = new Dog();
         func(dog);//func方法调用dog对象
        }
    }
    

方法返回:

public class TestDemo {
    public static Animal func(){
        Dog dog = new Dog();
        return new Dog();
    }
    public static void main(String[] args) {
    //向上转型:指的是父类引用去引用子类的对象
        Animal animal = func();
    }
}

此时func方法实际返回的是具有Animal引用类型的dog对象。同时也间接的是父类的引用去引用到了子类对象。

那我们大致已经了解了向上转型具体是什么之后,来看看在引用子类对象之后的父类能否调用子类中的方法呢?

class Animal{
    public String name;
    public Animal(String name){
        this.name=name;
    }
    public void eat(){
        System.out.println(this.name+"正在吃"+"(Animal)");
    }
}
class Dog extends Animal{
    public Dog(String name){
        super(name);
    }
    public void eatDog(){
        System.out.println(this.name+"正在吃"+"(Dog)");
    }
}
public class TestDemo{
    public static void main(String[] args){
        Animal animal1=new Animal("动物");
        Animal animal2=new Dog("旺财");
        animal1.eat();
        animal2.eatdog(); //报错
    }
}

在这里插入图片描述
这里的 animal1.eat()只能调用Animal中的方法,而在Animal类中没有eatDog方法,所以调用失败。

那么怎样才能调用子类的方法呢?请看下文!

动态绑定

动态绑定:实际上先通过向上转型(即父类的引用去引用子类的对象),通过父类引用,调用子类覆盖父类的重名方法

这里的覆盖也可以称之为覆写,重写

class Animal{
    public String name;
    public Animal(String name){
        this.name=name;
    }
    //父类和子类中有相同的重名方法,子类中的方法覆盖父类中的方法
    public void eat(){
        System.out.println(this.name+"正在吃"+"(Animal)");
    }
}
class Dog extends Animal{
    public Dog(String name){
        super(name);
    }
    public void eat(){
        System.out.println(this.name+"正在吃"+"(Dog)");
    }
}
public class TestDemo{
    public static void main(String[] args){
        Animal animal1=new Animal("动物");
        Animal animal2=new Dog("旺财");
        animal2.eat();
    }
}
//运行结果:旺财正在吃(Dog)

其实动态绑定也可以更形象称之为运行时绑定,因为通过反汇编,我们可以看到在编译的时候调用父类的方法,但是在运行的时候会调用子类的方法

方法重写

什么是方法重写呢?

方法名相同

方法的返回值相同

方法中有相同的参数列表

提到方法重写,我们是不是不禁想起了方法的重载呢?

那他们有什么不同之处呢?

方法重写方法重载
概念方法名相同,方法的返回值相同,方法的参数列表相同方法名相同,方法的参数列表不同,方法的返回值不做要求
使用范围继承类中,类外都可以使用
权限没有权限被重写的方法不能拥有比父类更加严格的访问权限

方法重写的注意事项:

  1. .在重写方法中,父类不能被final修饰,因为父类一旦被final修饰就会变成密封方法,就代表此类不能被修改,在以后的重写中不能被使用

  2. 在重写中,修饰子类的访问修饰限定符的访问权限要大于父类的访问修饰限定符权限,并且父类的访问权限不能为private。

  3. 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外)

  4. 访问修饰限定符的限权大小 **private < default(即包的访问权限) < protected < public**

  5. 在重写的过程中,父类和子类都不能被静态static 修饰。

  6. 在我们编写重写代码时,为了给以后修改代码带来便利通常都会使用注解,即解释说明的意思,例如重写:@override

向下转型

我们到现在已经学习了向上转型,那么让我们再来回顾一下他的概念吧,向上转型表示的是父类引用去引用子类对象,那么向下转型岂不就是子类引用去引用父类的对象嘛。哈哈哈哈,博主真是个大聪明。

在此说明一点,向上转型我们不常用,因为它带有安全隐患。

class Animal{
    public String name;
    public Animal(String name){
        this.name=name;
    }
    public void eat(){
        System.out.println(this.name+"正在吃"+"(Animal)");
    }
}
class Dog extends Animal{
    public Dog(String name){
        super(name);
    }
    public void eat(){
        System.out.println(this.name+"正在吃"+"(Dog)");
    }
}
public class TestDemo{
    public static void main(String[] args){

        //向下转型,子类引用去引用父类的对象
        Animal animal = new Dog("旺财");
        //在向下转型之前,一定要实现向上上转向
        Dog dog = (Dog)animal;
        dog.eat();
    }
}

注:在我们要进行向下转型之前,一定要进行向上转型。并且向下转型之后,只能调用子类中的方法。

那么向下转型为什么会存在安全隐患呢?

Animal animal = new Cat("小猫"); 

Bird bird = (Bird)animal; 

bird.fly(); 

// 执行结果, 抛出异常

Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Bird 

 at Test.main(Test.java:35

因为向上转型的时候,父类引用的是Cat类的对象,不是Bird类的对象。

所以为了向下转型的安全,我们预先判定一下Animal本质是不是Bird的一个实例,之后进行调换,这里不得不用到一个关键字instanceof

Animal animal = new Cat("小猫"); 

if (animal instanceof Bird) { 

     Bird bird = (Bird)animal; 

     bird.fly(); 

} 

这里animal引用的是Cat类的一个对象,而非Bird类,所以返回false

instanceof运算符的前一个操作数通常是一个引用类型变量,后一个操作数通常是一个类,也可以是一个接口(接口请看下文),可以把接口理解成一个特殊的类,用他判断前面对象是否是后面的类,或者是子类,实现类的实例,如果是就返回true,不是就返回false.

在使用instanceof运算符时,instanceof运算符前面的操作数的编译时的类型要与后面的类相同,或者要与后面的类有着父子继承关系,否则会引起编译错误。也就是相当于instanceof运算符后面的类是否是运算符前面的类的父类。

在构造方法中调用重写方法(这是一个坑)

class Animal{
    public String name;
    public Animal(String name){
        eat();
        this.name = name;
    }
    public void eat(){
    
    }
}
class Cat extends Animal{
    public Cat(String name){
        super(name);
    }
    public void eat(){
        System.out.println(this.name + "正在吃");
    }
}
public class TestDemo1 {
    public static void main(String[] args) {
        Animal animal = new Cat("mimi");
    }
}

运行结果: 在这里插入图片描述

有些友友们肯定会问,咦,不是没有调用eat()方法吗?怎么会打印出这个,并且为什么会是null?

为什么会调用eat()方法呢?

回答:因为我们在子类中调用父类的构造方法时,而在父类的构造方法中存在eat()方法,所以调用

为什么调用eat()方法之后打印为什么会是null?

回答:因为我们先调用eat()方法,没用对name进行初始化,所以会打印null,如果先初始化name,在调用eat()方法就不会打印null

理解多态

有了上面向上转型,动态绑定,和方法重写的基础户之后,我们就能很容易的理解多态,其实多态就是一种思想。

例如实现一个打印形状的类

class Shape{
    //实现一个图形方法
    public void draw(){

    }
}
class Cycle extends Shape{
    @Override //重写draw方法,实现打印
    public void draw() {
        System.out.println("打印圆形○");
    }
}
class Rect extends Shape{
    @Override
    public void draw() {
        System.out.println("打印方片♦");
    }
}
public class TestDemo{
    public static void DrawMap(Shape shape){  //向上转型
        shape.draw(); //动态绑定,调用父类和子类中相同的方法
    }
    public static void main(String[] args) {
        DrawMap(new Cycle()); //
        DrawMap(new Rect());
    }
}

当类的调用者在编写DrawMap方法时,参数类型为Shape,此时方法内部不知道,也不关注shape引用的哪个类型(哪个子类)的实例,此时shape调用draw方法,可能会有不同的表现形式。这就被称为多态。

使用多态的好处:

  1. 类的调用者对类的使用成本进一步降低

使用封装的好处:让类的调用者无需知道实现类中的具体实现步骤

使用多态:让类的调用者不需要知道类的具体类型是什么,它只需要知道类的功能(类中的成员方法的功能)即可,让类的调用者对类的使用成本进一步降低。

2.能够有效减少圈复杂度,减少if-else的使用

什么叫 “圈复杂度” ?

圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复度

我们先看一段代码

public static void drawShapes() { 
       Rect rect = new Rect(); 
       Cycle cycle = new Cycle(); 
       Flower flower = new Flower(); 
       String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"}; 
    for (String shape : shapes) { 
        if (shape.equals("cycle")) { 
           cycle.draw(); 
         } else if (shape.equals("rec")) { 
           rect.draw(); 
         } else if (shape.equals("flower")) { 
           flower.draw(); 
        } 
   } 
}

这样使用了多个if-else语句,使代码的圈复杂度提升,同时也不利于代码以后的修改。

我们可以创建一个Shape数组,数组类型为Shape,数组元素都是具体的形状对象,这样实现了向上转型。

在这里插入图片描述

看如下代码:

public static void drawShapes() { 
 // 我们创建了一个 Shape 对象的数组. 
     Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), new Rect(), new Flower()}; 
     for (Shape shape : shapes) { //实现动态绑定
     shape.draw(); 
     } 
}

3.可扩展能力强,改动代码成本较低

如果想打印其他的形状,只需要在写出这个形状所实现的类,然后重写方法即可。

class Triangle extends Shape { 
 @Override  //重写draw方法实现打印三角形
 public void draw() { 
 System.out.println("△"); 
 } 
}

而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高.

super关键字

博主其实在上一篇文章中已经提到了super关键字,在这里就简单回顾一下。

NO区别thissuper
1概念调用当前类的成员变量和成员方法调用父类的成员变量和成员方法
2查找范围先查找本类,如果本类中没有在查找其他类不查找本类直接查找父类
3特殊指向当前对象的引用

🥇抽象类

终于到抽象类了,抽象类,等等抽象是不是很难理解呀。不不它只是字面意思而已,实际上也挺简单的。

不知道大家注意了没,在我们编写多态代码的时候,父类中要被子类覆盖的重名方法没用使用到,当然如果父类中的重名方法即使其中有内容,我们也不会用到。那么干脆在这个方法中我们什么都不用实现,这就也出了即将为大家介绍的抽象类

语法规则

1.包含抽象方法的类叫做抽象类,抽象类,其中类和方法都要被abstract所修饰。

2.抽象类不能被实例化,否则会报错

3.抽象类中可以包含数据成员和成员方法

4.如果一个 类继承了抽象类,那么这个类将要重写抽象类中所有的抽象方法。其中抽象类不能被final修饰,final和abstract共存

abstract class Animal{
    public String name;
    public Animal(String name) {
        this.name = name;
    }
    public abstract void eat();//抽象方法
}
class Dog extends Animal{
    public Dog(String name){
        super(name);
    }
    public void eat(){
        System.out.println(this.name + "正在吃(Dog)");
    }
}

一个抽象类A可以被另一个抽象类B继承,如果有一个普通的类C继承了抽象类B,那么这个普通类C就要重写抽象类A和抽象类B中全部的抽象方法。

abstract class A{
    public abstract void eat();    
}
abstract class B extends A{
    public abstract void sleep();
}
class C extends B{
    //普通类C中要重写抽象类A和B中的所有的抽象方法
    //如果漏掉了一个抽象方法,编译就会报错
    public void eat(){
        System.out.println("正在吃");
    }
    public void sleep(){
        System.out.println("正在睡");
    }
}

抽象类中的抽象方法不能被private修饰

抽象类既可以发生向上转型,也可以发生动态绑定即(多态)

抽象类的作用

  • 抽象类最大的作用就是为了继承

    有些童鞋就会问,我不需要抽象类我也可以实现继承呀?为什么要使用抽象类呢?

    回答:如果在我们实现多态的过程中,不小心调用了父类中的被重写的方法,当然调用父类中被重写的方法是我们不需要的,此时编译器也不会报错,如果有抽象类进行编写多态代码,即使不小心调用了父类中被重写的方法,在父类进行实例化时,编译器就会报错。

🥇接口

接口是抽象类的进一步实例,在抽象类中可以包含非抽象方法和字段,而在接口中只能创建抽象方法,字段只能是静态常量。

语法规则

  1. 使用interfase定义一个接口

  2. 在接口中一定都是抽象类方法,所以可以省略abstract

  3. 从JDK1.8开始,接口中可以使用成员方法,当时这个成员方法必须被default所修饰

  4. 接口中的成员变量默认都是public static final修饰

  5. 接口同样不能被实例化

  6. 接口和类的关系是implements,implements表示的是实现某个接口,同样该类需要实现接口中的抽象方法

  7. 接口和接口之间的继承关系:当一个接口A继承了另一个接口B,那么接口A中就拥有可接口B中的属性,一个普通类C要实现接口A和接口B中的抽象方法。(类似于抽象类和抽象类之间的继承关系)

  8. 接口和接口之间的关系,使用extends来维护,意为扩展,一个接口具有了另一个接口的功能

    class Animal{
      public String name;
        public Animal(String name) {
            this.name = name;
        }
    }
    interface A{
        void eat();
    } 
    interface B extends A{
        void sleep();
    }
    class C extends Animal implements B{ //普通类必须先继承父类,然后才能实现接口中的抽象方法
        public C(String name) {
            super(name);
        }
        public void eat(){
            System.out.println("正在吃");   
        }
        public void sleep(){
            System.out.println("正在睡");
        }
    }
    

一个类可以实现多个接口,使用impements关键字,接口在关键字之后,用,隔开

因为在java中没有多继承,所以引出了一个类可以实现多个接口,这样也就间接的实现了"多继承"。

在我们创建接口时,接口的命名一般用大写的字母I开头。

实现多个接口

在类中要实现对各接口时,该类必须重写每个接口中的抽象方法。

class Animal{
//实现一个动物类
    public String name;
    public Animal(String name){
        this.name = name;
    }
}
//跑接口
interface IRunning{
     void Running();
}
//游泳接口
interface  ISwimming{
    void Swimming();
}
//飞行接口
interface IFlying{
    void Flying();
}
class Cat extends Animal implements IRunning{
    public Cat(String name){
        super(name);//调用父类中的构造方法
    }
    @Override
    public void Running(){
        System.out.println(this.name + "正在跑");
    }
}
class Dark extends Animal implements IRunning,IFlying,ISwimming {
    public Dark(String name) {
        super(name);
    }
    @Override
    public void Running() {
        System.out.println(this.name + "正在跑");
    }
    @Override
    public void Swimming() {
        System.out.println(this.name + "正在游");
    }
    @Override
    public void Flying() {
        System.out.println(this.name + "正在飞");
    }
}
public class TestDemo3 {
    public static void Swimming(ISwimming iSwimming){
        iSwimming.Swimming();
    }
    public static void Running(IRunning iRunning){
        iRunning.Running();
    }
    public static void Flying(IFlying iFlying){
        iFlying.Flying();
    }
    public static void main(String[] args) {
    Swimming(new Dark("鸭子"));
    Running(new Dark("鸭子"));
    Flying(new Dark("鸭子"));

    }
}

我们甚至可以不知道父类的具体类型是什么,也可以实现多个接口

class Robot implements IRunning { 
 private String name; 
 public Robot(String name) { 
 this.name = name; 
 } 
 @Override 
 public void run() { 
 System.out.println(this.name + "正在用轮子跑"); 
 } 
} 
Robot robot = new Robot("机器人"); 
walk(robot); 
//机器人正在用轮子跑

接口与接口之间的继承关系

接口与接口之间的继承,通过extends关键字进行连接,**普通类要实现其中的接口,必须全部实现接口中的抽象方法,**博主在接口中的语法规则中已经详细阐述。

接口使用实例

comparable接口

我们在学习java初识的时候,我们了解了Arrays类中的sort方法,它可以对数组中的数字进行排序。

 public class TestDemo3{
    public static void main(String[] args) {
        //对一组数进行排序
        int []array = {5,4,8,9,8,3};
        System.out.println(Arrays.toString(array));
        Arrays.sort(array);
        System.out.println(Arrays.toString(array));
    }
}

运行结果:

在这里插入图片描述

现在我们要实现一个学生类,并且对这些学生的某些属性进行排序包括(成绩,姓名等),看看我们是否还能用sort进行排序

class Student{
    private String name;
    private int age;
    private int score;

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

设置类型为学生的数组,向数组中传入学生的姓名,年龄,成绩

 public class TestDemo3{
    public static void main(String[] args) {
        Student[] student = new Student[3];
        student[0] = new Student("zhangsan",15,89);
        student[1] =  new Student("lisi",19,90);
        student[2] =  new Student("wangwu",20,80);
        //对学生进行排序
       Arrays.sort(student);
        System.out.println(student);
    }
}

输出结果: 在这里插入图片描述
这里说编译错误,显示ClassCaseException 表示的是类转换异常,该类不能转换成为java.lang.Comparable.

为什么呢?

因为编译器不知道,你要比较的是什么,是姓名,年龄,还是成绩,我们没有明确的指出。

我们可以在报错中找到Comparable,可以看看它是如何实现的
在这里插入图片描述
我们可以看到Comparable是一个接口,之后的<“T”>代表的是一个泛型,我们可以在Student类中去实现这个接口(Comparable),这里的要改为<“Student”>,至于详细的泛型概念,博主现在还没有写到。

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

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

但是现在还没有完成排序,因为我们知道当实现接口以后,还要重写接口中的抽象方法
在这里插入图片描述

重写接口中的抽象方法

@Override
public int compareTo(Student o) {  //重写接口中的抽象方法
    return this.age - o.age;
}

看看sort方法具体是怎样实现的。

当数组中的元素,如果左边的元素大于右边的元素时,就会发生交换元素。
在这里插入图片描述
此时运行代码:
在这里插入图片描述
但是comparable关键字,也具有一个缺点

当我们要比较学生的姓名时,重写接口中的抽象方法

  @Override
    public int compareTo(Student o) {  //重写接口中的抽象方法
        return this.name - o.name;
   }

当我们要比较学生的成绩时,重写接口中的抽象方法

@Override
public int compareTo(Student o) {  //重写接口中的抽象方法
    return this.score - o.score;
}
  • 可以看出,当要实现其他的比较其他学生属性的时候,要修改原来重写的方法,对代码要重新修改,有较大的的局限性

为了弥补这个缺陷,聪明的先辈们研究出了comparator接口

comparator接口

还是根据上面的Student类,去实现student数组中的各种属性排序。因为我们要在Student类外用到student中的有些属性,所以这里还要实现student的getter和setter方法。

class Student { //学生类实现Comparable接口
        private String name;
        private int age;
        private int score;

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

        public int getAge() {
            return age;
        }

         public void setAge(int age) {
            this.age = age;
        }

        public int getScore() {
             return score;
        }

        public void setScore(int score) {
            this.score = score;
       }
}

然后我们创建一个比较学生年龄的类,实现Comparator接口,重写Comparator接口中的抽象类

那让我们先看看Comparator接口的具体实现
在这里插入图片描述
通过接口,实现学生年龄的比较

class AgeComparator implements Comparator<Student> {
    //重写接口中的抽象方法

    @Override
    public int compare(Student o1, Student o2) {
        return o1.getAge() - o2.getAge();
    }
}
AgeComparator ageComparator = new AgeComparator();
Arrays.sort(student, ageComparator);
System.out.println(Arrays.toString(student));

运行结果:
在这里插入图片描述
如果要实现学生类中其他属性的比较,那么我们可以新建一个比较其他属性的类实现Comparator接口,重写它的抽象方法

例如比较学生的成绩

class ScoreComparator implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getScore() - o2.getScore();
    }
}

学生的姓名比较

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

我们可以将NameComparator 和 ScoreComparator 类实现接口,想成比较器,其中使用Comparator接口要比使用Comparable接口的局限性要小很多。所以我们在以后比较某些类中的某些属性的大小时,优先考虑Comparator接口。如果我们要实现其他比较器的时候,及直接可以新建一个类,重写接口中的抽象类即可

cloneable接口和深拷贝

让我们先看看这一段代码

class Person{
public String name = "zhangsan";

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
}
public class TestDemo4{
    public static void main(String[] args) {
        Person person = new Person();
        System.out.println(person.name);
    }

Person 在栈和堆上的图像
在这里插入图片描述
那么说到这了,克隆是什么呢?

其实克隆就相当于制造出一个和原对象一样的副本。
在这里插入图片描述
那我们该如何实现代码呢?

克隆,就要用到choneable接口,通过实现它的抽象方法,去实现克隆

class Person implements Cloneable{
    public String name = "xiaozhou";
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
}

我们可以按住Ctrl点击Cloneable,查看Clonable接口是怎样实现的
在这里插入图片描述
奇怪了,为什么Choneable接口没有实现呢?这个接口是一个空接口,他也被称为标记接口,这个空接口的作用是:如果有一个类要实现这个接口,那么这个类就可以被克隆。

我们要实现克隆,就必须要重写Object方法(在java中,Object类是所有类的父类)

class Person implements Cloneable{
    public String name = "xiaozhou";
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
    @Override//重写克隆方法
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    //Object类时所有类的父类
}

即使这样我们这时候还不能实现克隆。
在这里插入图片描述
发现了没有,clone(),现在还标红,这是为什么呢?

因为我们在重写Object方法后,它的返回值为Object的,然后就会变成了向下转型,所以我们要对person进行强制类型转换

因为重写的克隆方法抛出异常,解决方法是,我们鼠标点击clone,然后Ait + enter
在这里插入图片描述
然后选中红框里的选项,就会抛出异常

class Person implements Cloneable{
    public String name = "xiaozhou";
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
    @Override//重写克隆方法
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    //Object类时所有类的父类
}
public class TestDemo4 {

    public static void main(String[] args) throws CloneNotSupportedException {
        Person person = new Person();
        Person person1 =  (Person)person.clone();//因为重写的方法的返回值时Object,编译器认为不安全,所以我们要把他强制类型转换成Person
        System.out.println(person1.name);
    }
}

在这里插入图片描述

克隆成功!

简单的克隆接口我们到现在介绍完了,现在就到了复杂阶段

我们在Person类中添加一个成员变量a

class Person implements Cloneable{
     public int a = 1;
    @Override
    public String toString() {
        return "Person{" +
                "name='" + a + '\'' +
                '}';
    }
    @Override//重写克隆方法
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    //Object类时所有类的父类
}
public class TestDemo4 {

    public static void main(String[] args) throws CloneNotSupportedException {
        Person person = new Person();
        Person person1 =  (Person)person.clone();//因为重写的方法的返回值时Object,编译器认为不安全,所以我们要把他强制类型转换成Person
        System.out.println(person.a);
        System.out.println(person1.a);
        System.out.println("========================");
        person1.a = 5;
        System.out.println(person.a);
        System.out.println(person1.a);
    }
}

在这里插入图片描述

他们运行之后a值所对应的地址也不相同

由此我们可以看到当在克隆之后,改变克隆后的值,另外一个被克隆的a的值不会改变,也就是说在克隆后对象person和person1已经没有联系,这就被称为浅拷贝

那我们看看下面的代码,是不是觉得瞬间就被打脸了呢

class Money{
    public int money = 10;
}
class Person implements Cloneable{
public Money money = new Money();
    public int a = 1;
    @Override//重写克隆方法
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    //Object类时所有类的父类
}
public class TestDemo4 {

    public static void main(String[] args) throws CloneNotSupportedException {
        Person person = new Person();
        Person person1 =  (Person)person.clone();//因为重写的方法的返回值时Object,编译器认为不安全,所以我们要把他强制类型转换成Person
        System.out.println(person.money.money);
        System.out.println(person1. money.money);
        System.out.println("========================");
        person1. money.money = 5;
        System.out.println(person. money.money);
        System.out.println(person1. money.money);
    }
}

运行结果:
在这里插入图片描述
并且在运行之后,person和person1所对应的money值的地址相同。

这是为什么呢?

请看图!!

在这里插入图片描述
我们现在注意!

当我们克隆的时候,由于克隆的是Person的对象,我们只把这个对象(0x456)进行了克隆,但是,Person的对象的引用所指向的对象(0x123)我们没有进行克隆,所以在进行对Person的对象进行克隆后,(0x123)没有进行克隆,所以克隆出的Person2对象和Person1对象

,他们的引用都指向了(0x123).

那么该怎样解决此事呢?

那我们就要克隆person对象的对象即可,这也被称为深拷贝

这时候Money也要实现克隆接口

class Money implements Cloneable{
    public int money = 10;
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}


修改Person类中实现克隆的接口


class Person implements Cloneable{
public Money money = new Money();
    public int a = 1;
    @Override//重写克隆方法
    protected Object clone() throws CloneNotSupportedException {
         Person personClone = (Person)super.clone();  //克隆person所有用的对象
         personClone.money = (Money)money.clone(); //克隆person所引用的对象的引用的对象
         return personClone;
    }
    //Object类时所有类的父类
}

运行结果:

在这里插入图片描述

  • 19
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小周学编程~~~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值