写点什么

【Java 基础语法】万字解析 Java 的多态、抽象类和接口

作者:Java高工P7
  • 2021 年 11 月 11 日
  • 本文字数:8996 字

    阅读完需:约 30 分钟

    }


}




当我们如出现 eat 被写成了 ate 时候,那么编译器就会发现父类中是没有 ate 方法的,就会编译报错,提示无法构成重写



  • 重写时可以修改返回值,方法名和参数类型及个数都不可以修改。仅当返回值为类类型时,重写的方法才可以修改返回值类型,且必须是父类方法返回值的子类;要么就不修改,与父类返回值类型相同



了解到这,大家对于重写肯定有了一个概念。此时我们再回忆一下之前学过的重载,可以做一个表格来进行对比


| 区别 | 重载(Overload) | 重写(Override) |


| --- | --- | --- |


| 概念 | 方法名称相同、参数列表不同、返回值无要求 | 方法名称相同、参数列表相同、返回类型一般相同 |


| 范围 | 重载不是必须在一个类当中(继承) | 继承关系 |


| 限制 | 没有权限要求 | 被覆写的方法不能拥有比父类更严格的访问控制权限 |


比较结果就是,两者没啥关系呀



讲到这里,我们好像一直没有说明上一小节的标题动态绑定是啥


那么什么叫做动态绑定呢?发生的条件如下


  1. 发生向上转型(父类引用需要引用子类对象)


  1. 通过父类引用,来调用子类和父类的同名覆盖方法


那为啥是叫动态的呢?经过反汇编我们可以发现


  • 编译的时候: 调用的是父类的方法


  • 但是运行的时候: 实际上调用的是子类的方法


因此这其实是一个动态的过程,也可以叫其运行时绑定

4. 向下转型

既然介绍了向上转型,那肯定也缺不了向下转型呀!什么时向下转型呢?想想向上转型就可以猜到它就是


把父类对象赋值给了子类对象的引用


那么换成代码就是


// 假设 Animal 是父类,Dog 是子类


public class TestDemo{


public static void main(String[] args){


Animal animal=new Animal("动物");


Dog dog=animal;


}


}


但是只是上述这样写是不行的,会报错



为什么呢?我们可以这样想一下


狗是动物,但是动物不能说是狗,这相当于是一个包含的关系。



因此可以将狗的对象直接赋值给动物,但是不能将动物的对象赋值给狗


我们就可以使用强制类型转换,这样上述代码就不会报错了


public class TestDemo{


public static void main(String[] args){


Animal animal=new Animal("动物");


Dog dog=(Dog)animal;


}


}


我们接着用 dog 引用去运行一下 eat 方法


public class TestDemo{


public static void main(String[] args){


Animal animal=new Animal("动物");


Dog dog=(Dog)animal;


dog.eat();


}


}


运行后出现了错误



动物不能被转换成狗!



那我们该怎么做呢?我们要记住一点:


使用向下转型的前提是:一定要发生了向上转型


public class TestDemo{


public static void main(String[] args){


Animal animal=new Dog("二哈");


Dog dog=(Dog)animal;


dog.eat();


}


}


这样就没问题啦!


像上述我们提到使用向下转型的前提是要发生向上转型。我们其实可以理解为,我们在使用向上转型的时候,有些功能无法做到,故我们再使用向下转型来完善代码(emmm,纯属个人愚见啦)。就比如


// 假设我的 Dog 类中有一个看家的方法 guard


public class TestDemo{


public static void main(String[] args){


Animal animal=new Dog("二哈");


animal.guard();


}


}


上述代码就会报错,因为 Animal 类中是没有 guard 方法的。因此我们就要借用向下转型


public class TestDemo{


public static void main(String[] args){


Animal animal=new Dog("二哈");


Dog dog =animal;


dog.guard();


}


}


注意:


其实向下转型不常使用,使用它可能会不小心犯一些错误。如果我们上述的代码又要继续使用一些其他动物的特有方法,如果忘了它们没有发生向上转型,就会报错。


为了避免这种错误: 我们可以使用 instanceof


instanceof:可以判定一个引用是否是某个类的实例,如果是则返回 true,不是则返回 false,如



public class TestDemo{


public static void main(String[] args){


    Animal animal=new Dog("二哈");


    if(animal instanceof Bird){


        Bird bird=(Bird)animal;


        bird.fly();


    }


}


}




上述代码就是先判断 Animal 的引用是否是 Bird 的实例,我们知道它应该是 Dog 的实例,故返回 false

5. 关键字 super

其实上章就讲解过了 super 关键字,这里我再用一个表格比较下 this 和 super,方便理解


| 区别 | this | super |


| --- | --- | --- |


| 概念 | 访问本类中的属性和方法 | 由子类访问父类中的属性和方法 |


| 查找范围 | 先查找本类,如果本类没有就调用父类 | 直接调用父类 |


| 表示 | 表示当前对象 | 无 |


| 共性 1 | 不能被放在 static 修饰的方法中 | 不能被放在 static 修饰的方法中 |


| 共性 2 | 要放在第一行(不能和 super 一起使用) | 要放在第一行(不能和 this 一起使用) |

6. 在构造方法中调用重写方法(坑)

接下来我们看一段代码,大家可以猜猜结果是啥哦!


class Animal{


public String name;


public Animal(String name){


eat();


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){


Dog dog=new Dog("二哈");


}


}


结果就是




如果没猜对的,一般有两个疑惑:


  • 没有调用 eat 方法,但为什么结果是这样的?


  • 为啥是 null?


解答:


  • 疑惑一: 因为子类继承父类需要帮父类构造方法,所以子类创建对象时,就构造了父类的构造方法,就执行了父类的 eat 方法


  • 疑惑二: 由于父类构造方法是先执行 eat 方法,而 name 的赋值在后面一步,多以此时的 name 是 null


结论:


构造方法中可以调用重写的方法,并且发生了动态绑定

7. 理解多态

介绍到这里,我们终于要开始正式介绍我们今天的一大重点多态了!那什么是多态呢?其实他和继承一样是一种思想,我们可以先看一段代码


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 TestDemo{


public static void main(String[] args) {


Cycle shape1=new Cycle();


Rect shape2=new Rect();


Flower shape3=new Flower();


drawMap(shape1);


drawMap(shape2);


drawMap(shape3);


}


public static void drawMap(Shape shape){


shape.draw();


}


}


我们发现 drawMap 这个方法被调用者使用时,都是经过父类调用了其中的 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("rect")) {


rect.draw();


} else if (shape.equals("flower")) {


flower.draw();


}


}


}


这段代码的意思就是要分别打印圆、方片、圆、方片、花,如果不使用多态的话,我们一般就会写出上面这种方法。而使用多态的话,代码就会显得很简单,如


public static void drawShapes() {


// 我们创建了一个 Shape 对象的数组.


Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), new Rect(), new Flower()};


for (Shape shape : shapes) {


shape.draw();


}


}


我们可以通过下面这种图理解上面的代码


![在这里插入图片描述](https://img-blog.csdnimg.cn/d3a8020e8c554194a63b02ea795aac43.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5ZCe5ZCe5ZCQ5ZCQ5aSn6a2U546L,size_15,color_FFFFFF,t_70,g_se,x_


【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


16#pic_center)


而整体看起来,使用了多态的代码就简单了很多


3)可扩展能力强


如上述画图的代码,如果我们要新增一种新的形状,使用多态的方式改动成本也比较低,如



// 增加三角形


class Triangle extends Shape {


@Override 


public void draw() { 


  System.out.println("△"); 


} 


}




运用多态的话,我们扩展的代码增加一个新类就可以。而对于不使用多态的情况,就还需要对 if-else 语句进行一定的修改,故改动成本会更高

8. 小结

到此为止,面向对象的三大特点:封装、继承、多态已经全部介绍完了。由于我个人的理解也有限,所以讲的可能不好、不足,希望大家多多理解呀。



接下来将会介绍抽象类和接口,其中也会进一步运用到多态,大家可以多多练习,加深思想的理解。


二、抽象类



1. 概念

我们上面刚写过一个画图型的代码,其中父类的定义是这样的


class Shape{


public void draw(){


}


}


我们发现,父类中的 draw 方法里面没有内容,而绘图都是通过各种子类的 draw 方法完成的。


像上述代码,这种没有实际工作的方法,我们可以通过 abstract 来设计设计成一个抽象方法,而包含抽象方法的类就是抽象类


设计之后的代码就是这样的


abstract class Shape{


public abstract void draw();


}

2. 注意事项

  • 方法和类都要由 abstract 修饰



  • 抽象类中可以定义其他数据成员和成员方法,如



abstract class Shape{


    public int a;


    public void b(){


        // ...


    }


    public abstract void draw();


}




但要使用这些成员和方法,需要靠子类通过 super 才能使用



  • 抽象类不可以被实例化



  • 抽象方法不能是被 private 修饰的



  • 抽象方法不能是被 final 修饰的,它与 abstract 不能被共存



  • 如果子类继承了抽象类,但不需要重写父类的抽象方法,则可以将子类用 abstract 修饰,如



abstract class Shape{


    public abstract void draw();


}


abstract Color extends Shape{



}




此时该子类中既可以定义普通方法也可以定义抽象方法



  • 一个抽象类 A 可以被另外的抽象类 B 继承,但是如果有其他的普通类继承了抽象类 B,则该普通类需要重写 A 和 B 中的所有抽象方法


3. 抽象类的意义

我们要知道抽象类的意义就是为了被继承


从注意事项中就知道抽象类本身是不能被实例化的,要想使用它,只能创建子类去继承,就比如


abstract class Shape{


public int a;


public void b(){


// ...


}


public abstract void draw();


}


class Cycle extends Shape{


@Override


public void draw(){


System.out.println("画一个?");


}


}


public class TestDemo{


public static void main(String[] args){


Shape shape=new Cycle();


}


}


要注意子类需要重写父类的所有抽象方法,不然代码就会报错

3. 抽象类的作用

那么抽象类既然不能被实例化,那为什么要用它呢?


使用了抽象类就相当于多了一重编译器的效验


啥意思呢?就比如按照上述画图的代码,实际工作其实是由子类完成的,如果不小心误用了父类,父类不是抽象类的话是不会报错的,因此将父类设计成抽象类,它会在父类被实例化的时候报错,让我们尽早地发现错误


三、接口




我们上面介绍了抽象类,抽象类中除了抽象方法还可以包含普通的方法和成员。


而接口中也可包含方法和字段,但只能是抽象方法和静态常量。

1. 语法规则

我们可以将上述 Shape 改写成一个 接口,代码如下


interface IShape{


public static void draw();


}


具体的语法规则如下:


  • 接口是使用 interface 定义的



  • 接口的命名一般以大写字母 I 开头



  • 接口中的方法一定是抽象的、被 public 修饰的方法,因此其中抽象方法可以简化代码为



interface IShape{


    void draw();


}




这样写默认是 `public abstract` 的



  • 接口中也可以包含被 public 修饰的静态常量,并且可以省略 public static final,如



interface IShape{


    public static final int a=10;


    public static int b=10;


    public int c=10;


    int d=10;


}




  • 接口不能被单独实例化,和抽象类一样需要被子类继承使用,但是接口中使用 implements 继承,如



interface IShape{


    public static void draw();


}


class Cycle implements IShape{


    @Override


    public void draw(){


        System.out.println("画一个圆预?");


    }


}




和 extends 表达含义是”扩展“不同,implements 表达的是”实现“,即表示当前什么都没有,一切需要从头构造



  • 基础接口的类需要重写接口中的全部抽象方法



  • 一个类可以使用 implements 实现多个接口,每个接口之间使用逗号分隔开就可以,如



interface A{


    void func1();


}


interface B{


    void func2();


}


class C implements A,B{


    @Override


    public void func1(){



    }


    @Override


    public void func2{



    }


}




注意这个类要重写所有继承的接口的所有抽象方法,在 IDEA 中使用 `ctrl + i` ,快速实现接口



  • 接口和接口之间的关系可以使用 extends 来维护,这是意味着”扩展“,即某个接口扩展了其他接口的功能,如



interface A{


    void func1();


}


interface B{


    void func2();


}


interface D implements A,B{


    @Override


    public void func1(){



    }


    @Override


    public void func2{



    }


    void func3();


}




注意:


在 JDK1.8 开始,接口当中的方法可以是普通方法,但前提是:这个方法是由 default 修饰的(即是这个接口的默认方法),如



interface IShape{


void draw();


default public void func(){


    System.out.println("默认方法");


}


}


2. 实现多个接口

我们之前介绍过,Java 中的继承是单继承,即一个类只能继承一个父类


但是可以同时实现多个接口,故我们可以通过多接口去达到多继承类似的效果


接下来通过代码来理解吧!


class Animal{


public String name;


public Animal(String name){


this.name=name;


}


}


class Bird extends Animal{


public Bird(String name){


super(name);


}


}


此时子类 Bird 继承了父类 Animal,但是不能再继承其他类了,但是还可以继续实现其他的接口,如


class Animal{


public String name;


public Animal(String name){


this.name=name;


}


}


interface ISwing{


void swing();


}


interface IFly{


void fly();


}


class Bird extends Animal implements ISwing,IFly{


public Bird(String name){


super(name);


}


@Override


public void swing(){


System.out.println(this.name+"在游");


}


@Override


public void fly(){


System.out.println(this.name+"在飞");


}


}


上述代码就相当于实现了多继承,因此接口的出现很好的解决了 Java 单继承的问题


并且我们可以感受到,接口表达的好像是具有了某种属性,因此有了接口以后,类的使用者就不必关注具体的类型了,而只要关注该类是否具备某个能力,比如


public class TestDemo {


public static void fly(IFly flying){


flying.fly();


}


public static void main(String[] args) {


IFly iFly=new Bird("飞鸟");


fly(iFly);


}


}


因为飞鸟本身具有飞的属性,所以我们不必关注具体的类型,因为只要会飞的都可以实现飞的属性,如超人也会飞,就可以定义一个超人的类


class SuperMan implements IFly{


@Override


public void fly(){


System.out.println("超人在飞");


}


}


public class TestDemo {


public static void fly(IFly flying){


flying.fly();


}


public static void main(String[] args) {


fly(new SuperMan());


}


}


注意:


子类先继承父类再实现接口

3. 接口的继承

语法规则里面就介绍了,接口和接口之间可以使用 extends 来维护,可以使某个接口扩展其他接口的功能


这里就不再重述了



下面我们再学习一些接口,来加深对于接口的理解

4. Comparable 接口

我们之前介绍过 Arrays 类中的 sort 方法,它可以帮我们进行排序,比如


public class TestDemo {


public static void main(String[] args) {


int[] array={2,9,4,1,7};


System.out.println("排序前:"+Arrays.toString(array));


Arrays.sort(array);


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


}


}


而接下来我想要对一个学生的属性进行排序。


首先我实现一个 Student 类,并对 toString 方法进行了重写


class Student{


private String name;


private int age;


private 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 +


'}';


}


}


接下来我写了一个数组,并赋予了学生数组一些属性


public class TestDemo {


public static void main(String[] args) {


Student[] student=new Student[3];


student[0]=new Student("张三",18,96.5);


student[0]=new Student("李四",19,99.5);


student[0]=new Student("王五",17,92.0);


}


}


那么我们可以直接通过 sort 函数进行排序吗?我们先写如下代码


public class TestDemo {


public static void main(String[] args) {


Student[] student=new Student[3];


student[0]=new Student("张三",18,96.5);


student[1]=new Student("李四",19,99.5);


student[2]=new Student("王五",17,92.0);


System.out.println("排序前:"+student);


Arrays.sort(student);


System.out.println("排序后:"+student);


}


}


最终结果却是



我们来分析一下


ClassCastException:类型转换异常,说 Student 不能被转换为 java.lang.Comparable



这是什么意思呢?我们思考由于 Student 是我们自定义的类型,里面包含了多个类型,那么 sort 方法怎么对它进行排序呢?好像没有一个依据。



此时我通过报错找到了 Comparable





可以知道这个应该是一个接口,那我们就可以尝试将我们的 Student 类继承这个接口,其中后面的 < T > 其实是泛型的意思,这里改成 < Student > 就行



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 String toString() {


    return "Student{" +


            "name='" + name + '\'' +


            ", age=" + age +


            ", score=" + score +


            '}';


}


}




但此时还不行,因为继承需要重写接口的抽象方法,所以经过查找,我们找到了



增加的重写方法就是



@Override


public int compareTo(Student o) {


// 新的比较的规则


}




这里应该就是比较规则的设定地方了,我们再看看 sort 方法中的交换



也就是说如果此时的左值大于右值,则进行交换



那么如果我想对学生的年龄进行排序,重写后的方法应该就是



@Override


public int compareTo(Student o) {


// 新的比较的规则


return this.age-o.age;


}




此时再运行代码,结果就是


而到这里我们可以更深刻的感受到,接口其实就是某种属性或者能力,而上述 Student 这个类继承了这个比较的接口,就拥有了比较的能力


缺点:


  • 当我们比较上述代码的姓名时,就要将重写的方法改为



@Override


public int compareTo(Student o) {


    // 新的比较的规则


    return this.name.compareTo(o.name);


}




  • 当我们比较上述代码的分数时,就要将重写的方法改为



@Override


public int compareTo(Student o) {


    // 新的比较的规则


    return int(this.score-o.score);


}





我们发现当我们要修改比较的东西时,就可能要重新修改重写的方法。这个局限性就比较大


为了解决这个缺陷,就出现了下面的接口 Comparator

4. Comparator 接口

我们进入 sort 方法的定义中还可以看到一个比较方法,其中有两个参数数组与 Comparator 的对象



这里就用到了 Comparator 接口


这个接口啥嘞?我们可以先定义一个年龄比较类 AgeComparator,就是专门用来比较年龄,并让他继承这个类


class AgeCompartor implements Comparator<Student>{


}


再通过按住 ctrl 并点击它,我们可以跳转到它的定义,此时我们可以发现它里面有一个方法是



这个与上述 Comparable 中的 compareTo 不同,那我先对它进行重写


class AgeCompartor implements Comparator<Student>{


@Override


public int compare(Student o1, Student o2) {


return o1.age-o2.age;


}


}


我们再按照 sort 方法的描述,写如下代码


public class TestDemo {


public static void main(String[] args) {


Student[] student=new Student[3];


student[0]=new Student("张三",18,96.5);


student[1]=new Student("李四",19,99.5);


student[2]=new Student("王五",17,92.0);


System.out.println("排序前:"+Arrays.toString(student));


AgeComparator ageComparator=new AgeComparator();


Arrays.sort(student,ageComparator);


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


}


}


这样就可以正常的对学生的年龄进行比较了,而此时我们要再对姓名进行排序,我们就可以创建一个姓名比较类 NameComparator


class NameComparator implements Comparator<Student>{


@Override


public int compare(Student o1, Student o2) {


return o1.name.compareTo(o2.name);


}


}


而我们也只需要将 sort 方法的参数 ageComparator 改成 nameComparator 就可以了



我们可以将上述 AgeComparator 和 NameComparator 理解成比较器 ,而使用 Comparator 这个接口比 Comparable 的局限性小很多,我们如果要对某个属性进行比较只要增加它的比较器即可

5. Cloneable 接口和深拷贝

首先我们可以看这样的代码


class Person{


public String name ="LiXiaobo";


@Override


public String toString() {


return "Person{" +


"name='" + name + ''' +


'}';


}


}


public class TestDemo {


public static void main(String[] args) {


Person person=new Person();


}


}


那什么是克隆呢?应该就是搞一个副本出来,比如



那么既然这次讲 Cloneable 接口,我就对其进行继承呗!


class Person implements Cloneable{


public String name ="LiXiaobo";


@Override


public String toString() {


return "Person{" +


"name='" + name + ''' +


'}';


}


}

用户头像

Java高工P7

关注

还未添加个人签名 2021.11.08 加入

还未添加个人简介

评论

发布
暂无评论
【Java 基础语法】万字解析 Java 的多态、抽象类和接口