详解 Java 的对象与类(三)

简介: 详解 Java 的对象与类(三)

对象与类(三)



1. 理解多态



在说多态之前,我们一起来看一下什么是向上转型和动态绑定。


(1)向上转型


向上转型的三种情况:


① 直接赋值

② 方法传参

③ 方法返回


① 直接赋值


在程序清单1中,父类的引用 引用了 子类对象 这一操作被称为向上转型。


程序清单1:


class Animal {
    public String name;
    public Animal(String name) {
        this.name = name;
    }
}
class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
}
public class Test {
    public static void main(String[] args) {
        Animal animal1 = new Dog("哈士奇"); //父类的引用 引用了 子类对象
        System.out.println(animal1.name);
        Dog dog = new Dog("阿拉斯加");
        Animal animal2 = dog; //父类的引用 引用了 子类对象
        System.out.println(animal2.name);
    }
}


输出结果:


44ca9f82210d4ceba77ebd5372d8f5d3.png


② 方法传参


程序清单2:


class Animal {
    public String name;
    public Animal(String name) {
        this.name = name;
    }
    public void eat(){
        System.out.println(name + " 正在吃烤肉");
    }
}
class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
}
public class Test {
    public static void func(Animal animal){
        animal.eat();
    }
    public static void main(String[] args) {
        Dog dog = new Dog("阿拉斯加");
        func(dog); //方法传参
    }
}


输出结果:


8560013d778d41f885ca3a39737473b8.png


在程序清单2中,我们创建一个 func 方法,接收的参数类型是 Animal,接着我们在主函数调用 func(dog),奇怪的是,我们传参传的却是 Dog 类型,然而却不会报错。


也就是说传参传的是子类类型的变量,接收的是父类类型变量,那么这个行为也称为向上转型。


这个时候我们思考一下,如果我们传参传的是父类,接收的是子类,可以通过编译吗?答案是否,因为不符合继承的原则,编译器会报错。


③ 方法返回


程序清单3:


class Animal {class Animal {
    public String name;
    public Animal(String name) {
        this.name = name;
    }
}
class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
}
public class Test {
    public static Animal func(){
        Dog dog = new Dog("阿拉斯加");
        return dog;
    }
    public static void main(String[] args) {
        System.out.println(func().name);
    }
}


输出结果:


964fe876f7144b14961ff0eedbc22c7f.png


在程序清单3中,我们创建一个 func 方法,在创建方法的时候,定义的返回类型是 Animal 类型,然而,可以看见我们实际返回的却是 Dog 类型,接着,我们在主函数调用 func( ).name,这就等价于 dog.name,最终也可以得出想要的结果,那么这个操作也称为向上转型。


(2)动态绑定


在说动态绑定之前,我们先来看程序清单4和程序清单5.


程序清单4:


class Animal {
    public String name;
    public Animal(String name) {
        this.name = name;
    }
    public void eat(){
        System.out.println("某个动物正在吃东西");
    }
}
class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
}
public class Test {
    public static void main(String[] args) {
        Animal animal = new Dog("小柴犬");
        animal.eat();
    }
}


输出结果:


image.png


程序清单5:


class Animal {
    public String name;
    public Animal(String name) {
        this.name = name;
    }
    public void eat(){
        System.out.println("某个动物正在吃东西");
    }
}
class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
    public void eat(){
        System.out.println(name + " 正在吃东西");
    }
}
public class Test {
    public static void main(String[] args) {
        Animal animal = new Dog("小柴犬");
        animal.eat();
    }
}


输出结果:


fc2030ea745b49a8bc5513d39d526064.png


我们发现程序清单5中,在父类和子类中,出现了同名的方法,而当通过父类的引用调用 eat( ) 方法时,输出结果竟然是子类的方法。那么我们就来总结几个概念。


动态绑定:


① 父类的引用 引用子类的对象

② 通过这个父类的引用 调用父类和子类同名的覆盖方法


重写:



重写表示,在继承的关系下,子类创建了父类已有的某个方法。


重写的规则:


① 方法名相同

② 参数的个数和类型都必须相同

③ 返回值相同

④ 方法名不能被 static 关键字修饰,方法名同时不能被 final 修饰

⑤ 子类方法的修饰限定符的范围必须大于父类方法的修饰限定符范围

如:子类方法使用 protected,父类方法使用 public,这就会使编译器报错。当然,如果使用 private,也是无法重写的,因为 private 修饰的方法只能在同一个类下才能使用。


对比重载的规则:


① 方法名相同

② 方法的参数个数不同 / 方法的参数类型不同

③ 方法的返回值类型不作要求


在程序清单6中,有一行代码被编译器报错,我已通过注释标明出来了。


程序清单6:


class Animal {
    public String name;
    public Animal(String name) {
        this.name = name;
    }
}
class Bird extends Animal {
  public int a = 10;
    public Bird(String name) {
        super(name);
    }
    public void fly(){
        System.out.println(name + " 正在飞翔");
    }
}
public class Test {
    public static void main(String[] args) {
        Animal animal = new Bird("杜鹃");
        animal.fly(); //error
        System.out.println(animal.a);; //error
    }
}


结论:通过父类的引用,只能访问父类自己的成员变量和成员方法,而不能访问任何子类特有的成员变量和成员方法。


举个例子,我们可以看到,在上面的程序中,我们可以看到不是所有的动物都会飞,那么也就是说,父类存在的意义,就是对共性的提取,而发生动态绑定时,多数是有 [ 子类重写父类的方法 ] 的情况存在的。


程序清单7:


class A {
    public A() {
        func();
    }
    public void func() {
        System.out.println("abc");
    }
}
class B extends A {
    @Override
    public void func() {
        System.out.println("xyz");
    }
}
public class Test {
    public static void main(String[] args) {
        B d = new B();
    }
}


输出结果:


8031c0935a8a4ad39a45350a276746bd.png


在程序清单7中,程序也发生了动态绑定,因为子类重写了父类的成员方法,这就会导致我们在父类中遇到 func( ) 方法的时候,会直接选择子类的 func( )方法,详见下面的代码运行顺序。


代码运行顺序:


d61d4de6538b4796a10f763c20c76b19.png


当然,我们在程序清单7 中,如果在类 B 中没有进行对 func( ) 方法的重写,那么输出的结果为 [ abc ].


请看程序清单8,比较复杂但并不难,我们一起看一下:


程序清单8:


class X{
    Y y=new Y();
    public X(){
        System.out.print("X");
    }
}
class Y{
    public Y(){
        System.out.print("Y");
    }
}
public class Z extends X{
    Y y=new Y();
    public Z(){
        System.out.print("Z");
    }
    public static void main(String[] args) {
        new Z();
    }
}


输出结果:


ea6c0b54d50942deb7df065730979d97.png


代码运行顺序:


9da3fe56199e48268716083a9d422fb9.png


(3)多态


多态含义: 一个引用,能表现出多种不同的形态


引入一个例子,比方说:我现在要画画,分别画圆形,矩形,三角形。不管是什么几何图案,它们都是某个形状,那么我们现在归一个类,这三个几何图案都归为形状 Shape 这个类。接下来,我来阐明代码思路:


在程序清单9中,我们创建一个父类 Shape 表示形状,另外创建三个子类 圆形 Cycle,矩形 Rect,花形 Flower。我们的目的是:通过三个子类继承父类的 draw( ) 方法来画出圆形,矩形,花形这三个形状,而我把 draw( ) 方法这个功能又通过 drawShape( ) 方法来实现,如果说:draw( ) 方法的意义就是某事物拥有的一个画画特性,那么drawShape( ) 方法的意义就是如何实现画画的这个特性,并把这个特性无限扩展给别的事物!


在程序中,我们要注意两个地方,这也是多态的核心要点:


  • 我们在三个子类中分别对 draw( ) 方法进行了重写
  • 我们在主函数中通过父类的引用 “引用了” 子类的对象(在这里,如果我们传参的传是子类对象也可以达到目的,我已通过注释表明出来了。)


程序清单9:


class Shape{
    public void draw(){
    }
}
class Cycle extends Shape{
    @Override
    public void draw() {
        System.out.println("○");
    }
}
class Rect extends Shape {
    @Override
    public void draw() {
        System.out.println("□");
    }
}
class Flower extends Shape {
    @Override
    public void draw() {
        System.out.println("♣");
    }
}
//分割线//
public class Test {
    public static void drawShape(Shape shape){
        shape.draw();
    }
    public static void main(String[] args) {
        Shape shape1 = new Cycle();
        Shape shape2 = new Rect();
        Shape shape3 = new Flower();
        drawShape(shape1); //drawShap(new Cycle())
        drawShape(shape2); //drawShap(new Rect())
        drawShape(shape3); //drawShap(new Flower())
    }
}


输出结果:


254d03caa1644f5ebe83664163060be8.png


解析:


在程序清单9中,分割线上方的代码是 [ 类的实现者 ] 编写的,分割线下方的代码是 [ 类的调用者 ] 编写的。

当类的调用者在编写 drawShape 这个方法的时候, 参数类型为父类 Shape,此时在该方法内部并不知道,也不关注当前的 shape 这个引用指向的是 [ 哪个子类 ] 的实例。此时 shape 这个引用调用 draw( ) 方法可能会有多种不同的表现形式,而这种表象形式就称为多态。


但是我必须得额外声明一个点:子类引用 没必要 引用父类的对象,因为不是所有的形状都是圆形,不是所有的形状都是花形…

而圆形,花形,矩形,三角形,长方形等等都是归为了形状,希望读者能明白这个逻辑!


(4)多态的意义


在我第一次接触多态的时候,我认为这个语法是多余的、无用的。比方说:在程序清单9 中,我们大可不必用父类的引用 “引用” 子类的对象,直接通过子类创建一个子类的对象引用,其实也可以做到访问子类的成员。这样一想,连继承这个语法也是无用的了。但后来慢慢接触到更多的代码时,我发现了一个问题:就拿程序清单9 这个例子来说,比方说我现在要画画,但在实际生活中可不止是我上面的三种形状(圆形、矩形、花形),假设我现在需要在我的程序中添加更多的形状(三角形,菱形,长方形…),那么此时的多态的意义就体现出来了。


比方说:现在我可以将 Shap 类的引用全部放在一个数组中,而这些引用都是通过父类 Shap 引用到了不同的对象,而我每添加一个新的形状时,不用再为新的形状创建一个不同的画画方法,直接通过这个数组中的引用变量来执行画画这一操作即可。这样一来,我们可以发现这个多态所支持的拓展能力是很强的,因为当我们需要某个行为和状态,都可以通过父类来表象出来。可以继续往下看,程序清单10 就体现出来了多态的方便之处。


程序清单10:


class Shape{
    public void draw(){
    }
}
class Cycle extends Shape {
    @Override
    public void draw() {
        System.out.println("○");
    }
}
class Rect extends Shape {
    @Override
    public void draw() {
        System.out.println("□");
    }
}
class Flower extends Shape {
    @Override
    public void draw() {
        System.out.println("♣");
    }
}
public class Test {
    public static void drawShape(Shape shape){
        shape.draw();
    }
    public static void main(String[] args) {
        Shape shape1 = new Cycle();
        Shape shape2 = new Rect();
        Shape shape3 = new Flower();
        Shape[] shapes ={shape1,shape3,shape1,shape2,shape3};
        for (Shape shape:shapes) {
            drawShape(shape);
        }
    }
}


输出结果:


5c511af1d0644b92babd99a437577e71.png


在程序清单10中,我们创建了一个 Shape 类型的数组,数组中放的是 Shape 类型(当然,也可以直接放子类的对象)这样一来,我们不但可以打印所有形状,也可以多次打印每个形状。所以说多态的行为拓展能力很强,给我们带来了效率的提升。


2. 抽象类和抽象方法



程序清单11:


abstract class Shape{
    public int a;
    public void func(){
    }
    abstract public void draw();
}
class Flower extends Shape {
    @Override
    public void draw() {
        System.out.println("♣");
    }
}
public class Test {
    public static void drawShape(Shape shape){
        shape.draw();
    }
    public static void main(String[] args) {
        Shape shape3 = new Flower();
        drawShape(shape3);
    }
}


输出结果:


648ec850208c4e788f72a2f29fd2fc46.png


在程序清单11 中,我对程序清单9 进行了简化,我添加了 abstract 关键字,可以发现我将父类变成了抽象类,父类中的一个成员方法我也变成了抽象方法。其写法变成了下面的形式:


abstract public void draw();


(1)抽象类存在的意义


为什么会有抽象类存在呢?抽象类,顾名思义,它就是一个很抽象,不具象的类,换句话说:抽象类的本身没有具体实现。它除了被子类继承外,没有用途、没有值、没有目的。我们也可以通俗地将抽象类理解为子类的空模板。


(2)总结 abstract 关键字


那么我们来总结一下被 abstract 修饰的类和方法:


  • 被 abstract 修饰的类叫做 [ 抽象类 ]
  • 被 abstract 修饰的方法叫做 [ 抽象方法 ]


① 抽象类不可以通过 new 实例化对象,抽象类的存在只是为了被子类继承

② 抽象类中依然可以包含普通的成员变量和非抽象的成员方法

③ 如果一个普通类继承了一个抽象类,那么普通类必须重写抽象类的所有抽象方法

④ 抽象方法没有具体实现,只是为了被子类重写方法再调用

⑤ 一个抽象类可以继承另一个抽象类,此时的情况与上述概述有些不同,由于用的少,读者可以自己通过编译器实现一下细节部分

⑥ abstract 修饰父类的意义只有一个,那就是父类被继承,其实际工作不应该由父类完成,而应由子类完成


3. 接口



接口从某种意义上来说,就是完完全全的抽象类。


接口:由 interface 实现,如:


interface IA {
}


(1)接口的语法规则

① 接口中的普通方法,不能有具体实现,如果非要实现,需要被 default 修饰这个方法


② 接口中的所有方法的限定符都是 public,那么所有的方法都可以省略 public,而接口中的所有抽象方法的限定符都是 abstract public,那么所有的抽象方法同样可以省略 abstract public


③ 接口中的成员变量默认是被 public static final 修饰的,也就是说,接口中的成员变量必须被初始化


接口不可以通过 new 实例化对象


如果一个类实现了一个接口,那么在这个类中,必须重写接口中所有的抽象方法,而且重写的抽象方法必须被 public 修饰(因为接口中的抽象方法默认是 public abstract)


⑥ 如果一个类 A 实现了接口 B,那么对应的代码格式为:


interface IB{
}
class A implements IB{
}


⑦ 一个类可以实现多个接口,然而一个类只能继承一个类


interface IX{
}
interface IY{
}
class Z{
}
class A extends Z implements IX,IY{
}


⑧ 两个接口之间使用 extends,表示一个接口拓展另一个接口。

注意下面代码,当类 A 实现了接口 IY ,类 A 必须重写两个方法。


interface IX{
    void func1();
}
interface IY extends IX{
    void func2();
}
class A implements IY{
    @Override
    public void func2() {
    }
    @Override
    public void func1() {
    }
}


在程序清单12中,我通过注释标明了接口使用时的一些语法规则,这十分重要!


程序清单12:


interface IShape{
    public int a; //error
    public static final int b = 10; //right
    int c = 20; //right
    public void func1(){ //error
    }
    default public void func2(){ //right
    }
    public static void write(){ //right
    }
    abstract public void draw1(){ //error
    }
    abstract public void draw2(); //right
    void draw3(); //right
}
public class Test {
    public static void main(String[] args) {
        IShape iShape = new IShape(); //error
    }
}


在程序清单13中,我利用类实现了接口,和前面的动态绑定思想一样,但实现方式不同,我模拟了形状打印


程序清单13:


interface IShape{
    abstract public void draw();   
}
class Cycle implements IShape {
    @Override
    public void draw() {
        System.out.println("○");
    }
}
class Rect implements IShape {
    @Override
    public void draw() {
        System.out.println("□");
    }
}
class Flower implements IShape {
    @Override
    public void draw() {
        System.out.println("♣");
    }
}
public class Test {
    public static void drawShape(IShape ishape){
        ishape.draw();
    }
    public static void main(String[] args) {
        IShape iShape1 = new Cycle();
        IShape iShape2 = new Rect();
        IShape iShape3 = new Flower();
        drawShape(iShape1); //drawShape(new Cycle());
        drawShape(iShape2); //drawShape(new Rect());
        drawShape(iShape3); //drawShape(new Flower());
    }
}

输出结果:


3029103bfb804b3a94d56dc0f79b3456.png


程序清单14:


class Animal{
    String name;
    public Animal(String name) {
        this.name = name;
    }
}
interface IFly{
    void fly();
}
class Bird extends Animal implements IFly{
    public Bird(String name) {
        super(name);
    }
    @Override
    public void fly() {
        System.out.println(name + " 正在飞");
    }
}
public class Test {
    public static void main(String[] args) {
        Bird bird = new Bird("蜜蜂");
        bird.fly();
    }
}


输出结果:


181ca38d20304242abbd21551cfbe909.png


在程序清单14中,我们可以看到类 Bird 继承了类 Animal,并实现了接口 IFly,因为我们知道,不是所有的动物都会飞,而在 Java 中,一个类最多只能继承一个类,所以我们可以让鸟类飞的功能放在接口 IFly 中。这样看来,从某种意义上来说,接口语法存在的意义,或许就是弥补类不能多个继承的短板。


(2)继承与接口的综合


程序清单15:


class Animal{
    String name;
    public Animal(String name) {
        this.name = name;
    }
    public void eat(){
        System.out.println(name + " 正在吃东西");
    }
}
interface IFlying{
    void fly();
}
class Bird extends Animal implements IFlying{
    public Bird(String name) {
        super(name);
    }
  //重写了接口中的方法
    @Override
    public void fly() {
        System.out.println(name + " 正在飞");
    }
  //重写了父类中的方法
    @Override
    public void eat() {
        System.out.println(name + " 正在喝蜂蜜");
    }
}
public class Test {
    public static void fly(IFlying iFlying){
        iFlying.fly();
    }
    public static void main(String[] args) {
        fly(new Bird("老鹰"));
        new Bird("蜜蜂").eat();
    }
}


输出结果:


2bbf3eb01da44655b1bbef3560e419ab.png


对程序清单15进行分析:


在程序清单15中,我们演示了继承和接口,有几个语法点很关键:


① 在父类构造带参数的方法的同时,子类也需要构造同样的带参数方法。


public Bird(String name) {
}


② 在父类已有的普通成员方法情况下,子类却重写了父类的此方法,那么在编译时,系统直接使用子类重写的方法。


public void eat(){
}


③ 接口中的抽象方法是被 abstract public 修饰的,那么在一个类实现接口的时候,我们就必须对此抽象方法进行重写。


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


4. 三个常用接口



(1)Comparable 接口


[ Comparable 接口、Comparator 接口 ] 这两个接口和我之前写的C语言博客qsort函数十分相似,思想是一样的,感兴趣的读者可以去翻一翻我的博客


引入:假设我们现在手中有一张学生名单,上面有他们的各种信息,那么我们想什么办法给这些学生进行排序呢?我们可以按名字首字母排序,也可以按年龄排序,当然在初中高中,每次月考完,都是按成绩排序。所以在程序清单16中,我们创建一个类 Student,表示一个学生,类中有其名字、年龄、成绩。此时我们生成一个带三个参数的构造方法。我们先让类实现接口 Comparable,在类 Student 中,我们重写方法 compareTo.


在主函数中,我们创建两个对象,然后通过对象变量 student1 调用这个 compareTo方法,进行比较 student2,我们先来将学生按名字进行排序吧!那么在 compareTo 中,this.age 就等价于 student1.age,o.age 就等价于 student2.age,也就是说:我们拿学生1和学生2的年龄进行排序,而实现这一功能,就需要使用 Comparable 接口。如果学生1的年龄大于学生2,那么就返回正数,反之,返回负数。


在程序清单1中,我们发现,类 Student 实现了 Comparable 接口,那么在类中必须重写 Comparable 接口的方法 compareTo( ). 当然,Comparable 接口就是为了比较而设计的,而其对应的 compareTo( ) 方法也是如此。


程序清单16:


class Student implements Comparable<Student>{
    public String name;
    public int age;
    public double score;
    public Student(String name, int age, double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }
    @Override
    public int compareTo(Student o) {
        return this.age - o.age;
    }
}
public class Test {
    public static void main(String[] args) {
        Student student1 = new Student("A",21,90);
        Student student2 = new Student("C",28,75);
        System.out.println(student1.compareTo(student2));
    }
}


输出结果:


d12e1c63763e4ba6bd9fa385d11b9ee9.png


在程序清单17中,我们选择让学生以年龄升序:


使用Arrays.sort() 方法的时候,编译器不知道我们是基于什么方式进行排序的,
因为 students 数组中,有三个不同的类型。
那么我们就实现比较接口,告诉编译器,以学生名字进行怕排序。


程序清单17:


class Student implements Comparable<Student>{
    public String name;
    public int age;
    public double score;
    public Student(String name, int age, double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }
    @Override
    public int compareTo(Student o) {
        return this.age - o.age;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", score=" + score +
                '}';
    }
}
public class Test {
    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student("A",21,90);
        students[1] = new Student("C",28,75);
        students[2] = new Student("B",24,70);
        System.out.println(Arrays.toString(students));
        Arrays.sort(students);
        System.out.println(Arrays.toString(students));
    }
}


输出结果:


de21eeb7fe75415a8c9f808f5971e942.png


如果我们让学生以名字降序,那么我们就改变 compareTo 方法中的代码,代码如下:


public int compareTo(Student o) {
  return o.age - this.age;
}


同样地,如果我们让学生以成绩升序,那么代码如下:


public int compareTo(Student o) {
  return (int)(this.score - o.score);
}


如果我们让学生以名字升序,那么代码如下:


public int compareTo(Student o) {
  return this.name.compareTo(o.name);
}


读者可以自己去试一下这些操作,了解不同类型的比较方式。


(2)Comparator 接口


我们来看一下 Comparator 接口来排序两个学生年龄的大小,与 Comparable 接口的思想相同,接下来我主要说明一下 Comparator 接口的语法。


在程序清单18中,我们需要额外创建一个类 AgeComparator 来实现 Comparator 接口,并在这个类中,我们创建一个重写方法 compare( ),那么在主函数中,我们传参只需要传两个对象变量 student1 和 student2 就可以了,那么回过头来看重写方法 compare( ),student1.age 就等价于 o1.age;student2.age 就等价于 o2.age.


与上面的 Comparable 接口思想相同,不管哪个类实现了接口,那么一定要重写对应接口中的抽象方法。接下来,让我们看一下 Comparator 接口是怎么进行实现的吧。


程序清单18:


class Student {
    public String name;
    public int age;
    public double score;
    public Student(String name, int age, double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }
}
class AgeComparator 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("Z",11,85);
        Student student2 = new Student("Y",18,73);
        AgeComparator ageComparator = new AgeComparator();
        System.out.println(ageComparator.compare(student1, student2));
    }
}


输出结果:


4d4b62e477074bbbb8fbdd4f7db48552.png


在程序清单19中,我将一个学生的三个参数分别进行了排列。


程序清单19:


class Student {
    public String name;
    public int age;
    public double score;
    public Student(String name, int age, double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", score=" + score +
                '}';
    }
}
class AgeComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.age - o2.age;
    }
}
class ScoreComparator implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return (int)(o1.score - o2.score);
    }
}
class NameComparator implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o1.name.compareTo(o2.name);
    }
}
public class Test {
    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student("Z",11,85);
        students[1] = new Student("Y",18,73);
        students[2] = new Student("X",14,80);
        System.out.println(Arrays.toString(students));
        System.out.println();
        AgeComparator ageComparator = new AgeComparator();
        Arrays.sort(students, ageComparator);
        System.out.println(Arrays.toString(students));
        System.out.println();
        ScoreComparator scoreComparator = new ScoreComparator();
        Arrays.sort(students,scoreComparator);
        System.out.println(Arrays.toString(students));
        System.out.println();
        NameComparator nameComparator= new NameComparator();
        Arrays.sort(students,nameComparator);
        System.out.println(Arrays.toString(students));
        System.out.println();
    }
}


输出结果:


a897007ccc45402d9a18a30b37be5a86.png


(3)Cloneable 接口


注意几个要点:


① 一个对象要被克隆,需要使用一个类实现 Cloneable 接口。

② 虽然通过一个对象调用了 Clone( ) 方法,但是必须在类中重写 Clone( ) 方法,这一点是 Cloneable 接口 特有的性质。

③ 这里 Clone( ) 方法 返回的是 Object 类型,所以我们要强制转换成 Person 类型。

④ Cloneable 是一个空接口,空接口又被称为标志接口,代表当前这个类是可以被克隆的。


程序清单20:( 代码实现的方式为深拷贝 )


class Person implements Cloneable{
    public int age = 10;
    @Override
    public String toString() {
        return "Person{" +
                "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();
        Person person2 = (Person) person1.clone();
        System.out.println(person1);
        System.out.println(person2);
        System.out.println("------------------");
        person2.age = 99;
        System.out.println(person1);
        System.out.println(person2);
    }
}


输出结果:


b1f23def93994bd48e426ca92768e8e7.png


分析内部结构图:


01259d6445424598a1521630eec731a6.png


程序清单21:( 代码实现为浅拷贝 )


class Money{
    public int dollar = 17;
    @Override
    public String toString() {
        return "Money{" +
                "dollar=" + dollar +
                '}';
    }
}
class Person implements Cloneable{
    public int age = 10;
    public Money money = new Money();
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person();
        Person person2 = (Person) person1.clone();
        System.out.println(person1.money);
        System.out.println(person2.money);
        System.out.println("------------------");
        person2.money.dollar = 99;
        System.out.println(person1.money);
        System.out.println(person2.money);
    }
}


输出结果:


e3b04bc2f70842ca85104856cb89bb4a.png


注意:


这里是通过对象变量 person1,而只拷贝了 Person 类中的成员变量,而 Person 类之外的变量,没有进行拷贝。


分析内部结构图:


c465a3ad246c4274825abe773d6584b2.png


程序清单22:( 代码实现为深拷贝 )


class Money implements  Cloneable{
    public int dollar = 17;
    @Override
    public String toString() {
        return "Money{" +
                "dollar=" + dollar +
                '}';
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
class Person implements Cloneable{
    public int age = 10;
    public Money money = new Money();
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person temp = (Person) super.clone();
        this.money = (Money) this.money.clone();
        return temp;
    }
}
public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person();
        Person person2 = (Person) person1.clone();
        System.out.println(person1.money);
        System.out.println(person2.money);
        System.out.println("------------------");
        person2.money.dollar = 99;
        System.out.println(person1.money);
        System.out.println(person2.money);
    }
}


输出结果:


39b7ee2065d040a7be62ecaaccca20a8.png


注意:


虽然这里通过对象变量 person1,只拷贝了 Person 类中的成员变量,然而在 Clone( ) 方法中,我们也进行了对 Money 类中成员进行了拷贝。


分析内部结构图:


1e410e8d19024e0b9ed3a4488446cfc0.pngimage.jpeg


Over. 谢谢观看哟~

目录
相关文章
|
8天前
|
存储 Java
java的对象详解
在Java中,对象是根据类模板实例化的内存实体,具有唯一标识符、属性及行为。通过`new`关键字实例化对象并用构造方法初始化。变量存储的是对象引用而非对象本身,属性描述对象状态,方法定义其行为。Java利用垃圾回收机制自动处理不再使用的对象内存回收,极大地简化了对象生命周期管理,同时对象具备封装、继承和多态性,促进了代码的重用与模块化设计。这使得Java程序更易于理解、维护和扩展。
|
8天前
|
Java
java的类详解
在 Java 中,类是面向对象编程的核心概念,用于定义具有相似特性和行为的对象模板。以下是类的关键特性:唯一且遵循命名规则的类名;描述对象状态的私有属性;描述对象行为的方法,包括实例方法和静态方法;用于初始化对象的构造方法;通过封装保护内部属性;通过继承扩展其他类的功能;以及通过多态增强代码灵活性。下面是一个简单的 `Person` 类示例,展示了属性、构造方法、getter 和 setter 方法及行为方法的使用。
|
4天前
|
Java 编译器
Java——类与对象(继承和多态)
本文介绍了面向对象编程中的继承概念,包括如何避免重复代码、构造方法的调用规则、成员变量的访问以及权限修饰符的使用。文中详细解释了继承与组合的区别,并探讨了多态的概念,包括向上转型、向下转型和方法的重写。此外,还讨论了静态绑定和动态绑定的区别,以及多态带来的优势和弊端。
19 9
Java——类与对象(继承和多态)
|
4天前
|
SQL Java 编译器
Java——类与对象(封装)
封装是面向对象编程中的概念,指将数据(属性)和相关操作(方法)组合成独立单元(类),使外部无法直接访问对象的内部状态,只能通过提供的方法进行交互,从而保护数据安全。例如,手机将各种组件封装起来,只暴露必要的接口供外部使用。实现封装时,使用`private`关键字修饰成员变量,并提供`get`和`set`方法进行访问和修改。此外,介绍了包的概念、导入包的方式及其注意事项,以及`static`关键字的使用,包括静态变量和方法的初始化与代码块的加载顺序。
18 10
Java——类与对象(封装)
|
4天前
|
Java C语言
Java——类与对象
这段内容介绍了Java中的类和对象、`this`关键字及构造方法的基本概念。类是对现实世界事物的抽象描述,包含属性和方法;对象是类的实例,通过`new`关键字创建。`this`关键字用于区分成员变量和局部变量,构造方法用于初始化对象。此外,还介绍了标准JavaBean的要求和生成方法。
18 9
Java——类与对象
|
3天前
|
存储 安全 Java
Java——String类详解
String 是 Java 中的一个类,用于表示字符串,属于引用数据类型。字符串可以通过多种方式定义,如直接赋值、创建对象、传入 char 或 byte 类型数组。直接赋值会将字符串存储在串池中,复用相同的字符串以节省内存。String 类提供了丰富的方法,如比较(equals() 和 compareTo())、查找(charAt() 和 indexOf())、转换(valueOf() 和 format())、拆分(split())和截取(substring())。此外,还介绍了 StringBuilder 和 StringJoiner 类,前者用于高效拼接字符串,后者用于按指定格式拼接字符串
10 1
Java——String类详解
|
5天前
|
存储 Java
Java的对象和类的相同之处和不同之处
在 Java 中,对象和类是面向对象编程的核心。
|
4天前
|
Java
Java Character 类详解
`Character` 类是 Java 中的一个封装类,位于 `java.lang` 包中,主要用于处理单个字符。它是一个最终类,提供了多种静态方法来检查和操作字符属性,如判断字符是否为字母、数字或空格,以及转换字符的大小写等。此外,`Character` 类还支持自动装箱和拆箱,简化了 `char` 和 `Character` 之间的转换。以下是一些示例代码,展示了如何使用 `Character` 类的方法来检查字符属性和执行字符转换。掌握 `Character` 类的用法有助于更高效地处理字符数据。
|
4天前
|
Java
Java Number & Math 类详解
在 Java 中,`Number` 类和 `Math` 类是处理数字和数学运算的重要工具。
|
5天前
|
存储 Java
Java编程中的对象序列化与反序列化
【9月更文挑战第12天】在Java的世界里,对象序列化与反序列化是数据持久化和网络传输的关键技术。本文将带你了解如何通过实现Serializable接口来标记一个类的对象可以被序列化,并探索ObjectOutputStream和ObjectInputStream类的使用,以实现对象的写入和读取。我们还将讨论序列化过程中可能遇到的问题及其解决方案,确保你能够高效、安全地处理对象序列化。