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

news2024/12/29 10:16:04

目录​​​​​​​

前言:

多态:

多态的定义:

向上转型: 

方法重写:

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1375510.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

自旋锁和互斥锁的区别

自旋锁和互斥锁的区别_自旋锁和互斥锁有什么区别?-CSDN博客

Fluids —— MicroSolvers DOP

目录 Gas SubStep —— 重复执行对应的子步 Switch Solver —— 切换解算器 Gas Attribute Swap —— 交换、复制或移动几何体属性 Gas Intermittent Solve —— 固定时间间隔计算子解算器 Gas External Forces —— 计算外部力并更新速度或速度场 Gas Particle Separate…

在Vivado下利用Tcl实现IP的高效管理

在Vivado下利用Tcl实现IP的高效管理https://cloud.tencent.com/developer/article/1736645 在Vivado下&#xff0c;有两种方式管理IP。一种是创建FPGA工程之后&#xff0c;在当前工程中选中IP Catalog&#xff0c;生成所需IP&#xff0c;这时相应的IP会被自动添加到当前工程中&…

FineBI实战项目一(18):每小时上架商品个数分析开发

点击新建组件&#xff0c;创建每小时上架商品个数组件。 选择线图&#xff0c;拖拽cnt&#xff08;总数&#xff09;到纵轴&#xff0c;拖拽hourStr到横轴。 修改横轴和纵轴的文字。 调节连线样式。 添加组件到仪表板。

vue知识-05

聊天室案例(django接口) # chat.hetm<<script src"/static/axios.js"></script><script src"/static/vue.js"></script><body> <div id"app"><h1>聊天室</h1><button click"handleS…

【无标题】用好Java 8的日期时间类

在 Java 8 之前&#xff0c;我们处理日期时间需求时&#xff0c;使用 Date、Calender 和 SimpleDateFormat&#xff0c;来声明时间戳、使用日历处理日期和格式化解析日期时间。但是&#xff0c;这些类的 API 的缺点比较明显&#xff0c;比如可读性差、易用性差、使用起来冗余繁…

竞赛保研 基于深度学习的行人重识别(person reid)

文章目录 0 前言1 技术背景2 技术介绍3 重识别技术实现3.1 数据集3.2 Person REID3.2.1 算法原理3.2.2 算法流程图 4 实现效果5 部分代码6 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 基于深度学习的行人重识别 该项目较为新颖&#xff0c;适合…

SpringBoot中使用LocalDateTime踩坑记录

文章目录 前言一、为什么推荐使用java.time包的LocalDateTime而不是java.util的Date&#xff1f;二、使用LocalDateTime和LocalDate时遇到了哪些坑&#xff1f;2.1 Redis序列化报错2.1.1 问题现象2.1.2 问题分析2.1.3 解决方案 2.2 LocalDateTime和LocalDate类型的属性返回给前…

Redis主从复制、哨兵及集群

主从复制 主从复制是高可用Redis的基础&#xff0c;哨兵和集群都是在主从复制基础上实现高可用的。主从复制主要实现了数据的多机备份&#xff0c;以及对于读操作的负载均衡和简单的故障恢复。缺陷&#xff1a;故障恢复无法自动化&#xff1b;写操作无法负载均衡&#xff1b;存…

关于httpClient 使用的注意事项

关于httpClient 使用的注意事项 用例 PoolingHttpClientConnectionManager connectionManager new PoolingHttpClientConnectionManager();// 最大连接数-不设置默认20connectionManager.setMaxTotal(200);// 每个路由最大连接数-不设置默认2connectionManager.setDefaultMax…

nginx本地打开vue打包项目

1&#xff0c;首先下载nginx包&#xff0c;然后修改下载的包中的nginx.conf配置文件 其中location后面接的是要增加的域名字段(perbank8)&#xff0c;配置项 index index.html 代表入口文件是index.html 这个在vue项目中也要在nginx配置文件中也要配置同样的路径 在vue项目里…

Fluids —— Up-ressing simulations

目录 Low-res setup Hi-res setup 想象需要一个波浪特写镜头&#xff0c;撞击岩石并产生飞溅&#xff1b;通常会模拟大部分周围海洋&#xff0c;以获得波浪运动&#xff1b;最后&#xff0c;可能只需要模拟tank的20%左右&#xff1b; 通常工作流是测试低粒子数的模拟&#xf…

【一文详解】知识分享:(C#开发学习快速入门)

面向对象(OOP) c语言是面向过程。 c是面向过程面向对象。 c#是纯粹的面向对象: 核心思想是以人的思维习惯来分析和解决问题。万物皆对象。 面向对象开发步骤: 分析对象 特征行为关系(对象关系/类关系) 写代码: 特征–>成员变量 方法–>成员方法 实例化–具体对象 …

JS中垃圾数据是如何自动回收的

JS中垃圾数据是如何自动回收的 背景垃圾回收机制调用栈中的数据回收堆空间中数据回收垃圾回收器的工作流程副垃圾回收器主垃圾回收器 全停顿 背景 在JS栈和堆&#xff1a;数据是如何存储的一文中提到了 JavaScript 中的数据是如何存储的&#xff0c;并通过示例代码分析了原始数…

基于Java SSM框架实现雁门关风景区宣传网站项目【项目源码】

基于java的SSM框架实现雁门关风景区宣传网站演示 Java技术 Java技术它是一个容易让人学会和使用的一门服务器语言。它在编程的过程当中只需要很少的知识就能建立起一个真正的交互站点。对于这个教程来说它并不需要你完全去了解这种语言&#xff0c;只要能快速融入web站点就可以…

盘点2023年我用过的AI大模型,国内也能免费用

大家好&#xff0c;这里是程序员晚枫&#xff0c;今天给大家分享3个我用过的AI工具。 国外的工具这里就不推荐了&#xff0c;推荐了你也不能用。今天只推荐几个国内也能用的。 这些工具的下载链接&#xff0c;我都整理好了&#xff0c;需要的朋友可以在评论区告诉我哟~ 百度…

tailwindcss真的好用吗?

写在前面 今天写一篇关于tailwindcss 的文章&#xff0c;其实这个css技术已经出现很久了&#xff0c;在一些大型项目很多人也已经在用了&#xff0c;虽然不是说必须要会吧&#xff0c;但是没听说过肯定是不行的&#xff0c;他的操作逻辑应该是和unocss差不多&#xff0c;但是今…

MySQL:DML数据操作语言(添加,删除,修改),DDL数据查询语言(条件查询,分组查询,排序查询,分页查询)

目录 1.DML&#xff08;数据操作语言&#xff09;1.添加数据2.修改数据3.删除数据 2.DQL(数据查询语言)1.DQL-语法2.基本查询3.条件查询(WHERE)1.语法&#xff1a;2.条件&#xff1a;3.案例: 4.聚合函数1.介绍2.常见聚合函数3.语法4.案例 5.分组查询&#xff08;GROUP BY&#…

VS2019 C++安装最基本的组件

我是C初学者&#xff0c; 我只想用最基本的C编写程序&#xff0c;不需要MFC。 所以下载了一个离线安装版本放在这里&#xff0c;我自己用的现成的文件。 使用以下命令行创建&#xff1a; 1.C基本组件 不含MFC, 带ATL库 共(1.96G) vs2019.exe --layout d:\vs2019 --lang zh-CN …

C#用string.Replace方法批量替换某一类字符串

目录 一、关于字符串及其操作常识 二、String.Replace 方法 1.重载 2.Replace(Char, Char) 3.Replace(String, String) &#xff08;1&#xff09;实例&#xff1a; &#xff08;2&#xff09;生成结果&#xff1a; 4.Replace(String, String, StringComparison) 5.…