多态、抽象类和接口(深拷贝和浅拷贝)

目录

前言:

多态:

多态的定义:

向上转型: 

方法重写:

再看toString方法: 

动态绑定:

向下转型: 

小练习:

抽象类:

什么是抽象类?

抽象方法:

抽象类:

抽象类的使用: 

小总结:

接口:

接口是什么?

接口中的方法修饰符: 

接口中的成员修饰符:

接口的使用:

接口的定义格式: 

接口中的代码块使用: 

类使用多个接口: 

接口的继承: 

Comparable接口: 

小练习一: 

小练习二: 

小总结: 

克隆:

浅拷贝:

深拷贝:


前言:

       经过之前的学习,我们都已经了解了什么是继承和封装,那么今天我们就来学习面向对象的最后一个特性,多态。

       当然,了解了多态也就需要知道和它相关的知识,抽象类和接口。

多态:

多态的定义:

       多态:多种形态,去完成某个行为,不同对象去完成时产生不同形态。有一种看人说话的感觉,都是说同一种事物,但是根据不同的对象说话的方式不同。

要想实现多态,需要满足几个条件:

    1.继承关系上:向上转型
    2.子类和父类有同名 覆盖/重写 方法
    3.通过父类对象的引用,去调用这个重写的方法。

我们我们来逐一介绍。

       我们先来看一段代码:

class Animal {
    public String name;
    public int age;

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

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

class Dog extends Animal {
    public Dog(String name, int age) {
        super(name, age);
    }
    public void bark() {
        System.out.println(this.name + "汪汪叫");
    }
}

public class New {
    public static void main(String[] args) {
        Dog a = new Dog("hehe",12);
        a.eat();
        a.bark();
        System.out.println("=========");
        Animal lala = new Animal("lala", 19);
        lala.eat();
        lala.bark();//这是子类特有方法,只能调用父类自己特有的成员方法或者成员变量
    }
}

       这里我们用Dog类继承了Animal类,我们定义了一个Animal类的lala对象,去调用了Dog类中的特定的bark方法,因为bark是Dog的方法,毫无疑问,会报错。

向上转型: 

       之前说过,多态涉及3个定义,此时我们就来讲解向上转型和向下转型。 

Dog gege = new Dog("gege", 10);
Animal animal = gege;
//animal这个引用对象 指向了 dog 这个引用对象

       我们把gege这个Dog类的对象转换为其父类的类型,此时就发生了向上转型。我们将这两句代码合并:

Animal animal = new Dog("gege",11);

       这就是向上转型。此时我们调试程序,来观察如何执行:

       可以看到,先去调用了Dog的构造方法。 向上转型有三个情况:

       此时就完成了向上转型。接下来就要讲解方法重写和动态绑定。

方法重写:

       还是否记得我们之前学到的方法重载?对没错,它和方法重写是两个概念。我们先来讲解方法重写,之后再给出区别。

       此时我们在Dog的类里面写入eat方法(Animal中也有eat方法),并让发生向上转型对象调用。 

class Animal {
    public String name;
    public int age;

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

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

class Dog extends Animal {
    public Dog(String name, int age) {
        super(name, age);
    }
    public void bark() {
        System.out.println(this.name + "汪汪叫");
    }
    public void eat() {
        System.out.println(this.name + "正在吃狗粮");
    }
}

public class New {
    public static void main(String[] args) {
        Animal animal = new Dog("lele", 10);
        animal.eat();
    }
}

       此时你会发现竟然调用了子类的方法。 

       重写也称为覆盖。是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写,返回值和形参都不能改变。

       tips:我们进行方法重写,IDE编译器会出现一个图标来表明发生了方法重写: 

       为了更好的的区分方法进行重写,我们一般重写方法时,都会在上面加上@Override.

@Override//这个注解即发生了方法重写
public void eat() {
    System.out.println(this.name + "正在吃狗粮");
}

       在Java中,有一种术语叫做注解,比如@Override就是其中的一种,起到提示的作用(有点像C的assert函数)。

       这里也需要注意他们的权限,被重写的方法权限修饰符必须大于等于继承的方法。

       为了大家更好的复习:

        实现重写:

  1. 最基本的返回值,参数列表,方法名必须是一样的
  2. 被重写的方法的访问修饰限定符,在子类中要大于等于父类的方法修饰限定访问符。
  3. 被private修饰的方法是不可以被重写的。
  4. 被static修饰的方法是不可以重写的。
  5. 被重写的方法返回类型可以不同,但必须具有父子关系。
  6. 被final修饰的方法是不可以被重写的。
  7. 构造方法也是不能被重写的。

       此时我们就来看方法重写和方法重载的区别:

再看toString方法: 

       此时我们再次打印对象,并再看toString方法。

       toString方法是Object类中的方法,因为Java中所有的类都默认继承于Object类,所以当Dog类中没有写toString方法,就会调用Object的toString方法。所以我们每次调用时toString时都会发生向上转型。这就是方法重写。

       对于已经投入使用的类,尽量不要进行修改。最好的方式是:当重新定义了一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。

动态绑定:

       此时在通过父类的引用进行调用的时候,是调用子类的方法,把这个过程就叫做 动态绑定。 

       方法的重载就是静态绑定;方法的重写就是动态绑定。

       我们执行进行方法重写的文件(我上方的代码,最长的那个),之后我们看编译时是如何发生动态绑定的。

       我们直接在IDEA右击,之后点击explorer就会打开当前文件的目录,之后回退到out目录,之后点进去(用手机看的可以不用操作,不用纠结)。找到编辑的类生成的字节码文件(后缀为.class),之后在当前目录中输入cmd打开控制台。之后再控制台中输入javap -c 文件名:

       我们可以看到在编译时,调用的方法确实是调用的Animal的eat方法,但是运行期间调用的是Dog的eat方法。这就叫做动态绑定,是在运行时绑定;而静态绑定在编译的时候就确定调用谁。

静态绑定:也称前期绑定(早绑定)。
动态绑定:也称后期绑定(晚绑定)。需要在程序运行时,才能确定具体调用哪个类的方法。

       当父类引用,引用的子类对象不一样的时候,调用这个重写的方法,所表现出来的行为不一样时,我们把这种思想就叫做多态。

向下转型: 

       向上转型的优点是让代码更加灵活;缺点是不能调用子类特有的方法。

       我们目前了解了向上转型,接下来我们再来看看什么是向下转型。

       此时只能够强制转换才可以成功。

       此时再多定义一个Cat类,并写入方法。

class Cat extends Animal {
    public Cat(String name, int age) {
        super(name, age);
    }
    public void miaomiao() {
        System.out.println(this.name + "喵喵叫");
    }
}

        此时是类型转换异常,所以向下转换是非常不安全的,此时只能这样运行:

Animal animal = new Dog("yuanyuan", 10);

if (animal instanceof Cat) {
    Cat cat = (Cat)animal;
}else {
    System.out.println("理解了!");
}

        instanceof是判断是否是其子类(包括该类),所以我们判断一下即可。

小练习:

        我们来看一个代码:

class B {
    public B() {
        func();
    }
    public void func() {
        System.out.println("B.func()");
    }
}

class D extends B {
    //private int num = 1;
    @Override
    public void func() {
        System.out.println("D.func()");
    }
}

public class Again {
    public static void main(String[] args) {
        D d = new D();
    }
}

运行结果:

        这里先去调用父类的构造方法,父类中调用了它的方法,但是子类中有相同的方法,因为动态绑定的关系,所以会去调用子类重写方法。

       此时我们再打印num的值(放开那条注释):

public void func() {
    System.out.println("D.func() " + num);
}

         会发现并不是1,而是0。此时子类中有该变量,但是还没来得及赋值为1。

抽象类:

什么是抽象类?

        顾名思义,就是抽象的类。哈哈,确实,但是也确实抽象。再讲抽象类之前,我们还是先来看代码。此时我们来打印一些图形,在不使用多态的情况下:

class Shape {
    public void draw() {
        System.out.println("画一个图形:");
    }
}

class Rect extends Shape {
    public void draw() {
        System.out.println("□!");
    }
}

class Triangle extends Shape {
    public void draw() {
        System.out.println("△!");
    }
}

class Cycle extends Shape {
    public void draw() {
        System.out.println("○!");
    }
}

public class Test {
    public static void main(String[] args) {
        Cycle cycle = new Cycle();
        Rect rect = new Rect();
        Triangle triangle = new Triangle();
        String[] strings = {"cycle","rect","cycle","rect","triangle"};

        for (String x : strings) {
            if (x.equals("cycle")) {
                cycle.draw();
            }else if (x.equals("rect")) {
                rect.draw();
            }else if (x.equals("triangle")) {
                triangle.draw();
            }
        }
    }
}

       这样写就是不知道多态才会这样写,如果利用多态来写,就可以省去很多代码:

public class Test {
    public static void main(String[] args) {
        /*Shape shape1 = new Cycle();
        Shape shape2 = new Triangle();
        Shape shape3 = new Rect();*/

        Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), new Rect(), new Triangle()};
        for (Shape shape : shapes) {
            shape.draw();
        }
    }
}

       我们发现父类中的draw方法有些累赘,但是又不能不写,因为是其他形状继承的,否则完成不了多态。所以就有了抽象方法。

抽象方法:

       因为父类中的方法是被重写的,不会被调用,所以我们我们可以什么都不写:

class Shape {
    public void draw() {
        
    }
}

       我们可以这样写,但是还是不够完美。此时我们还想再简洁一些。

       所以这不是一个方法,所以只能是抽象方法,使用抽象关键字abstract修饰。

        但是依旧报错,如果此方法为抽象方法,那么这个类也必须是抽象类。

抽象类:

        抽象方法必须在抽象类中使用,此时我们使用了抽象方法,所以要把类描述为抽象类:

abstract class Shape {
    public abstract void draw();
}

         因为所有对象都是通过类来描述的,但是反过来,并不是所有类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个对象,这样的类就是抽象类。

         因为抽象类中缺少一些关键信息,所以抽象类是不可以实例化的。 

         抽象类当中,可以和普通类一样,定义成员变量和成员方法。

abstract class Shape {
    
    public String name;
    public abstract void draw();
}

抽象类的使用: 

       既然把方法抽象就要声明这个类是抽象类,那么抽象类肯定是继承时才会使用。此时直接继承会报错:

       必须实现抽象类中的抽象方法才可以被继承。

abstract class Shape {

    public String name;
    public abstract void draw();
}

class Flower extends Shape {
    @Override
    public void draw() {
        System.out.println("❀!");
    }
}

       所以抽象类的出现本身就是为了被继承。而且抽象方法和成员不能被final修饰(书写顺序无所谓)。

public class Test2 {
    public static void drawMap(Shape shape) {
        shape.draw();
    }
    public static void main(String[] args) {
        Shape shape = new Cycle();
        //因为抽象类不能实例化,所以只能向上转型
        drawMap(shape);
    }
}

       抽象类也可以被继承,可以被抽象类继承,也可以被正常的类继承,但是不是 抽象类 继承的抽象类必须实现抽象类中的实例方法(就是正常的类继承了抽象类,就必须实现之前所有抽象类中的方法)。

       所以出来混,迟早还是要还的。

tips:抽象类的图标我们可以观察一下,和其他正常类的图标不一样: 

小总结:

        抽象类也是类,内部可以包含普通的方法和属性,甚至构造方法。

        抽象类不能被private修饰;抽象方法不能被final和static修饰,因为抽象方法要被子类重写。因为可以通过子类来调用父类的构造方法。

        一个类只能继承一个抽象类。

接口:

接口是什么?

        实际生活中,接口就是设备上的(en……编不出来了)。接口属于一种标准,使用时只要符合规范,就可以使用。

        Java中接口可以看成多个类的公共规范,是一种引用数据类型。

        接口是使用interface方法来修饰的,接口当中不能有被实现的方法,意味着只能有抽象方法:

        但是两个方法除外(JDK8引入的):一个是被static修饰的方法,一个是被default修饰的方法。否则只是普通的抽象方法。

接口中的方法修饰符: 

       之后我们来观察接口中的方法修饰符:

       所以接口中的方法默认都是public abstract修饰的,即使前面没写,也是默认加上的。

接口中的成员修饰符:

       再看接口成员修饰符:

        接口中的成员都是 public static final 修饰的,即使前面没写,也是默认加上的,也意味着它是常量。

        也就是说,接口是对抽象类的抽象,抽象的抽象更不能实例化。

接口的使用:

        说了这么多,那么接口到底是如何使用的?比抽象还抽象。

        类和接口之间的关系,可以使用implements来进行关联。

        但是此时为什么报错,是因为接口中的方法没有被重写,和抽象类类似。 

        所以此时我们再来实现之前的打印形状,就可以不使用抽象类了,可以直接使用接口了。

interface S {
    /*public int a = 1;
    public static int b = 2;
    public static final int c =3;*/

    void draw();

}

class R implements S {
    @Override
    public void draw() {
        System.out.println("矩形!");
    }
}

class F implements S {
    @Override
    public void draw() {
        System.out.println("❀!");
    }
}

public class Test3 {
    public static void func(S shape)
    {
        shape.draw();
    }
    public static void main(String[] args) {
        S shape1 = new R();
        S shape2 = new F();
        S[] shapes = {shape1, shape2};
        for (S sh : shapes) {
            func(sh);
        }
    }
}

       既然接口无法进行实例化,那么我们就可以利用向上转型和动态绑定,通过多态的方法对其进行使用。

接口的定义格式: 

       接口的定义格式与定义类的格式基本相同,将class关键字换成了interface关键字,就定义了一个接口。

       命名规则即规范:创建接口时,接口命名一般使用大写字母I开头。接口命名一般使用“形容词”词性单词。接口中的方法和属性不要添加任何修饰符号(因为默认也会有),保持代码简洁性。

       还是和之前一样,接口中的方法,都是要重写的,因为出来混都是要还的。

接口中的代码块使用: 

       接口中不能有静态代码块、构造代码块和实例代码块。

类使用多个接口: 

       我们可以在一个类中使用多个接口。当我们在一个类中要使用多个接口时,可以使用“,”隔开。

abstract class Animal {
    public String name;
    public int age;

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

interface IFly {
    void fly();
}

interface IRun {
    void Run();
}

interface ISwim {
    void Swim();
}

class Dog extends Animal implements IRun {
    public Dog(String name, int age){
        super(name, age);
    }
    @Override
    public void Run() {
        System.out.println(this.name + "正在跑!");
    }
}

class Frog extends Animal implements ISwim,IRun {
    public Frog(String name, int age) {
        super(name, age);
    }

    @Override
    public void Swim() {
        System.out.println(this.name + "蛙泳");
    }

    @Override
    public void Run() {
        System.out.println(this.name + "跳跳");
    }
}

class Duck extends Animal implements ISwim,IRun,IFly {
    public Duck(String name, int age) {
        super(name, age);
    }

    @Override
    public void fly() {
        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 running(IRun iRun) {
        iRun.Run();
    }
    
    public static void flying(IFly iFly) {
        iFly.fly();
    }
    
    public static void main(String[] args) {
        running(new Dog("二狗",11));
        flying(new Duck("唐老鸭",10));
    }
}

       所以Java使用接口也就实现了多继承问题。 

接口的继承: 

         接口之间,也可以实现继承。如果一个类使用的是继承的接口,则需要重写父类接口和子类接口的所有方法。

interface A {
    void testA();
}

interface  B extends A {
    void testB();
}

class TestDemo1 implements B {
    @Override
    public void testA() {

    }

    @Override
    public void testB() {

    }
}

       所以还是那句话,出来混还是要还的。

Comparable接口: 

       我们如果对一个类进行比较时,需要明确类型。否则就会报错。

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", 11);

        System.out.println(student1 > student2);
        //报错,因为没有指定类型
    }
}

       为了实现比较,我们需要使用接口。我们导入一个包,之后利用这个接口(Comparable)进行比较。

       我们点进去这个接口并看里面的内容:

       这里面有一个<>,代表泛型的意思,泛型我们以后再了解。当前代表我们要比较的类型,此时我们传入Student.

class Student implements Comparable<Student>

        我们可以看到这个接口中有一个compareTo的抽象方法。

        因为接口中有一个抽象方法,所以我们使用这个接口要去重写这个compareTo方法。

public int compareTo(Student o) {
    if (this.age > o.age) {
        return 1;
    }else if (this.age == this.age) {
        return 0;
    }else {
        return -1;
    }
}

//主方法调用
System.out.println(student1.compareTo(student2));

       我们目前重写了该方法,但是有弊端,此时再次调用该方法只能比较年龄了。所以我们可以改进。但是我们要根据姓名进行比较时就没办法了。

       此时我们可以进行改进,我们可以使用比较器Comparator这个接口:

       其实就是实现一个类,之后重写compare方法,之后创建这个类的对象之后调用方法。

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

public class Test {
    public static void main(String[] args) {
        Student student1 = new Student("zhangsan", 10);
        Student student2 = new Student("lisi", 11);

        AgeCompare ageCompare = new AgeCompare();
        System.out.println(ageCompare.compare(student1, student2));
    }

}

        此时我们再比较姓名。创建一个类(比较器),之后实现compare方法,因为是字符串比较,所以用调用compareTo方法。

class NameCompare implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.name.compareTo(o2.name);
    }
}
NameCompare nameCompare = new NameCompare();
System.out.println(nameCompare.compare(student1, student2));

       此时是String类型调用的compareTo方法,所以我们先进入该方法。

       此时我们我们看String这个类型,进入源码观察。

       我们可以发现,String类实现了Comparable接口,所以是String类重写了compareTo方法。

       之后我们来比较 比较器 和 方法重写 各自的优缺点:

       此时两种方法都可以写,但是可以根据实际情况来修改,利用比较器灵活性更强。

小练习一: 

       我们定义一个接口,这个接口时USB,它里面有打开设备方法和关闭方法,之后定义三个类,分别是Computer类和Mouse类和KeyBoard类,

package demo2;

public class Test {
    public static void main(String[] args) {
        Computer computer1 = new Computer();
        Mouse mouse = new Mouse();
        computer1.useService(mouse);

        System.out.println("==========");

        KeyBoard keyBoard = new KeyBoard();
        computer1.useService(keyBoard);
    }
}
package demo2;

public class Computer {
    public void powerOn() {
        System.out.println("打开电脑");
    }

    public void powerOff() {
        System.out.println("关闭电脑");
    }

    public void useService(USB usb) {
        usb.openDevice();//先打开

        if (usb instanceof Mouse) {
            Mouse mouse = (Mouse)usb;
            mouse.click();
        }else if (usb instanceof KeyBoard) {
            KeyBoard keyBoard = (KeyBoard)usb;
            keyBoard.flap();
        }

        usb.closeDevice();//后关闭
    }
}
public class Mouse implements USB{

    @Override
    public void openDevice() {
        System.out.println("鼠标开始工作");
    }

    public void click() {
        System.out.println("疯狂点击鼠标……");
    }

    @Override
    public void closeDevice() {
        System.out.println("鼠标结束工作");
    }
}
public class KeyBoard implements USB{

    @Override
    public void openDevice() {
        System.out.println("插上键盘设备");
    }

    public void flap() {
        System.out.println("疯狂敲击键盘……");
    }

    @Override
    public void closeDevice() {
        System.out.println("关闭键盘设备");
    }
}
package demo2;

public interface USB {
    void openDevice();
    void closeDevice();
}

小练习二: 

       了解了Comparable接口后,我们再来对一组数据进行排序,也是一个类。

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[] students = new Student[3];
        students[0] = new Student("zhangsan", 11);
        students[1] = new Student("lisi", 5);
        students[2] = new Student("wangwu", 7);
        System.out.println("排序前:" + Arrays.toString(students));

        Arrays.sort(students);

        System.out.println("排序后:" + Arrays.toString(students));

    }
}

       执行此代码会发生错误,那么此时我们就点击这个报错,看是哪里出现了错误:

       和之前举的例子一样,是因为Arrays.sort排序必须指定类型,而且发现了向上转型。Comparable是一个接口,但是此时我们的类和这个接口没有任何关系,所以我们要去使用这个接口。

       使用接口以后,我们发现里面调用了compareTo方法,这个方法是接口的抽象方法,所以我们要重写一遍,并得知是排序的是哪个类型。

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) {
        return this.age - o.age;
    }
}

小总结: 

       接口也属于一种类型,可以去引用实现了该接口的具体类型。

       两个特例,静态的方法直接通过接口名调用即可;default方法需要实例化一个对象,通过向上转型才能调用成功。

       和重写抽象类方法一样,重写权限修饰符必须大于等于父类,但是由于接口的方法默认都是 public abstract static 修饰的,所以重写方法只能用public修饰。

       接口相较于抽象类,可以更好的让其他类使用更加灵活。书写顺序不能错。

       2个关系:

  1. 类和接口之间的关系 ->implements 实现
  2. 接口和接口之间的关系 -> extends 拓展

       接口的修饰符可以为abstract,并且默认为其修饰。

克隆:

       克隆也是拷贝,一般编程中分为两种,深拷贝和浅拷贝。

浅拷贝:

       Java中其实也有现成的拷贝方法,我们先来看代码:

class Person {
    public String name;
    public int age;

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

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

       此时我们有一个学生类,此时我们要创建两个对象,其中一个要去拷贝一个已经实例化的对象。 

public class Test {
    public static void main(String[] args) {
        Person person1 = new Person("zhangsan",10);

        //此时新创将一个对象,并将person1内容拷贝给新对象
        Person person2 = person1;//此时需要调用克隆方法
    }
}

        此时就需要调用clone方法。我们要知道所有类都是继承与Object的类,所以我们搜索Object类。

        在Object里面寻找clone方法。

       我们可以看到Object类中有clone方法。但是 .(点) 不出来,这是因为protected的原因。

        我们之前讲过,用protected修饰的成员或者方法,调用时就必须使用super。但是此时主方法是静态方法,不能使用this和super,所以要么再写一个方法,要么直接在子类中调用。

        所以我们重写clone方法(因为默认继承Object类,这个类中有这个方法,所以进行重写),之后通过对象调用。此时我们在子类中重写clone方法。

        但是可能会抛出异常,异常处理我们以后再学,就先按照提示输入即可。 

        还是报错,此时就需要向下转型。

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person("zhangsan",10);

        //此时新创将一个对象,并将person1内容拷贝给新对象
        Person person2 = (Person) person1.clone();//此时需要调用克隆方法
    }
}

        此时就完成了克隆。但是注意这是浅拷贝。

class Person {
    public String name;
    public int age;

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

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person("zhangsan",10);

        //此时新创将一个对象,并将person1内容拷贝给新对象
        Person person2 = (Person) person1.clone();//此时需要调用克隆方法
        System.out.println(person1);
        System.out.println(person2);
    }
}

        当然了,此时你去执行,依旧报错(哈哈哈)。

        这个方法确实重写了,但是当我们要实现克隆时,一定要使用一个Cloneable接口,才能实现克隆。

         我们把它叫做空接口/标记接口,表明当前类是可以被克隆的。之前报错是因为不支持克隆,实现接口以后就可以克隆。

        此时就完成了浅拷贝,那么接下来我们来了解深拷贝。

深拷贝:

        此时就需要用到继承来观察了。观察一下代码:

class Money {
    public double m = 19.9;
}
class Person implements Cloneable {
    public String name;
    public int age;

    public Money money = new Money();

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

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person("zhangsan",10);

        //此时新创将一个对象,并将person1内容拷贝给新对象
        Person person2 = (Person) person1.clone();//此时需要调用克隆方法

        System.out.println(person1.money.m);
        System.out.println(person2.money.m);
        System.out.println("=============");
        person1.money.m = 200;

        System.out.println(person1.money.m);
        System.out.println(person2.money.m);

    }
}

        注意这里我们只改变了了person1的值,如果拷贝的话不会影响person2的值。但是结果并不是想象中的。

        这就是浅拷贝的弊端。对于嵌套的内容他们指向相同的地址。        此时我们就要来了解深拷贝了。 在Money里面也要使用克隆接口,拷贝时也要把父类拷贝进去(注意要先在Money实现Cloneable接口)。

class Money implements Cloneable {
    //这里面也要使用克隆接口 和 重写克隆方法
    public double m = 19.9;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

        之后去Person类中拷贝Money。

@Override
protected Object clone() throws CloneNotSupportedException {
    //为了实现深拷贝
    Person tmp = (Person)super.clone();
    //之后tmp中的money再拷贝一次
    tmp.money = (Money)this.money.clone();
    return tmp;
}

        此时就正式完成了深拷贝。 

class Money implements Cloneable{
    public double m = 19.9;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

class Person implements Cloneable {
    public String name;
    public int age;
    public Money money = new Money();


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

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person tmp = (Person)super.clone();
        tmp.money = (Money)this.money.clone();
        return tmp;
    }
}

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person("zhangsan",10);

        //此时新创将一个对象,并将person1内容拷贝给新对象
        Person person2 = (Person) person1.clone();//此时需要调用克隆方法
        System.out.println(person1.money.m);
        System.out.println(person2.money.m);
        System.out.println("============");
        person1.money.m = 200;

        System.out.println(person1.money.m);
        System.out.println(person2.money.m);
    }
}

        完结了这一篇,希望大佬点点赞。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值