继承与多态

发布于:2024-05-14 ⋅ 阅读:(131) ⋅ 点赞:(0)

一、继承

1.1 为什么需要继承

Java 中使用类对现实世界中实体来进行描述,类经过实例化之后的产物对象,则可以用来表示现实
中的实体,但是现实世界错综复杂,事物之间可能会存在一些关联,那在设计程序是就需要考虑。
比如: 狗和猫,它们都是一个动物
通过观察上述代码会发现,猫和狗的类中存在重复
那能否将这些共性抽取呢? 面向对象思想中提出了继承的概念,专门用来进行共性抽取,实现代码
复用

1.2 继承概念

继承 (inheritance) 机制 :是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保
持原有类特性的基础上进行扩展,增加新功能,这样产生新的类,称 派生类 。继承呈现了面向对象
程序设计的层次结构, 体现了由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,
实现代码复用
例如:狗和猫都是动物,那么我们就可以将共性的内容进行抽取,然后采用继承的思想来达到共
用。
总结:继承他是一种思想,对共性进行抽取,实现代码复用。

1.3 继承的语法

Java 中如果要表示类之间的继承关系,需要借助 extends 关键字,具体如下:
修饰符 class 子类 extends 父类{
    //…
}

注:访问修饰限定符只能决定访问权限,不能决定是否继承

子类会将父类中的成员变量或者成员方法继承到子类中

1.4 父类成员的访问

在继承体系中,子类将父类中的方法和字段继承下来了,那在子类中能否直接访问父类中继承下来
的成员呢?

1.4.1 子类中访问父类成员

1. 子类和父类不存在同名成员变量
public class Base {
    public int a ;
    public int b;
}

class Test extends Base{
    public  int c ;
    public void method(){
        a = 10;
        b = 20;
        c = 30;

    }
}

public class Main {
    public static void main(String[] args) {
        Test test = new Test();
    }
}

2. 子类和父类成员变量名相同

public class Base {
    public int a = 10;
    public int b = 20;
}

class Test extends Base{
    public int a;
    public char  b;
    public  int c ;
    public void method(){
        a = 30;
        b = 40;
        System.out.println("a = "+a);
        System.out.println("b = "+b);
    }
}

public class Main {
    public static void main(String[] args) {
        Test test = new Test();
        test.method();
    }
}

在子类方法中 或者 通过子类对象访问成员时
如果访问的成员变量子类中有,优先访问自己的成员变量。
如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错。
如果访问的成员变量与父类中成员变量同名,则优先访问自己的。
成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找

1.4.2 子类中访问父类的成员方法

1. 成员方法名不同

public class Base {
  public void method1(){
      System.out.println("666");
  }
}

class Test extends Base{
    public void method2(){
        System.out.println("12345");
    }
    public void demo(){
        method1();
        method2();
    }
}
public class Main {
    public static void main(String[] args) {
        Test test = new Test();
        test.demo();
    }
}

2.成员方法名相同

public class Base {
  public void method(){
      System.out.println("666");
  }
}

class Test extends Base{
    public void method(){
        System.out.println("12345");
    }
    public void demo(){
        method();
    }
}
public class Main {
    public static void main(String[] args) {
        Test test = new Test();
        test.demo();
    }
}

通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,
找到则访问,否则编译报错。
通过派生类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同 ( 重载 ) ,根
据调用方法适传递的参数选择合适的方法访问,如果没有则报错;

1.5 super关键字

子类和父类中可能会存在相同名称的成员,如果要在子类方法中访问父类同名成员时,该如何操
作?直接访问是无法做到的,Java 提供了 super 关键字,该关键字主要作用:在子类方法中访问父
类的成员
public class Base {
  public int a = 10;
  public int b = 20;
  public void method(){
      System.out.println("666");
  }
}

class Test extends Base{

    public int a = 30;
    public char b = 40;
    public void method(){
        System.out.println("12345");
    }
    public void demo1(){
        method();
        System.out.println(this.a);
        System.out.println(this.b);
    }
    public void demo2(){
        super.method();
        System.out.println(super.a);
        System.out.println(super.b);
    }
public class Main {
    public static void main(String[] args) {
        Test test = new Test();
        test.demo1();
        test.demo2();
    }
}

在子类方法中,如果想要明确访问父类中成员时,借助 super 关键字即可
注意事项
1. 只能在非静态方法中使用
2. 在子类方法中,访问父类的成员变量和方法
super 的其他用法在后文中介绍。

1.6 子类构造方法

父子父子,先有父再有子,即:子类对象构造时,需要先调用基类构造方法,然后执行子类的构造
方法。
子类构造方法中默认会调用基类的无参构造方法: super()
用户没有写时 , 编译器会自动添加,而且 super() 必须是子类构造方法中第一条语句
并且只能出现一次
在子类构造方法中,并没有写任何关于基类构造的代码,但是在构造子类对象时,先执行基类的构

造方法,然后执行子类的构造方法,因为:子类对象中成员是有两部分组成的,基类继承下来的以

及子类新增加的部分 。父子父子肯定是先有父再有子,所以在构造子类对象时候 ,先要调用基类

的构造方法,将从基类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新

增加的成员初始化完整

此外,调用父类构造方法是为了帮助初始化子类从父类继承过来的成员,并不会生成父类对象

1.7 super和this

1.7.1 相同点

1. 都是 Java 中的关键字
2. 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
3. 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
1.7.2 不同点
1. this 是当前对象的引用,当前对象即调用实例方法的对象, super 相当于是子类对象中从父类继
承下来部分成员的引用
2. 在非静态成员方法中,this 用来访问本类的方法和属性, super 用来访问父类继承下来的方法和

属性
3. 在构造方法中: this(...) 用于调用本类构造方法, super(...) 用于调用父类构造方法,两种调用不
能同时在构造方法中出现
4.  构造方法中一定会存在 super(...) 的调用,用户没有写编译器也会增加,但是 this(...) 用户不写则
没有

1.8 再说初始化

public class Animal {
    public String name;
    int age;

    static {
        System.out.println("Animal静态");
    }

    {
        System.out.println("Animal实例");
    }

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
public class Cat extends Animal{
    public Cat(String name, int age) {
        super(name, age);
    }

    static {
        System.out.println("Cat静态");
    }

    {
        System.out.println("Cat实例");
    }

    public void miaomiao(){
        System.out.println(this.name+"正在喵喵叫");
    }
}
public class Main {
    public static void main(String[] args) {
        Cat cat = new Cat("咪咪",2);
        Cat cact2 = new Cat("meme",2);
    }
}

通过输出的结果我们可以得出以下结论

1 、父类静态代码块优先于子类静态代码块执行,且是最早执行
2 、父类实例代码块和父类构造方法紧接着执行
3 、子类的实例代码块和子类构造方法紧接着再执行
4 、第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行

1.9 protected关键字

在类与对象文章中,提到为了实现封装特性, Java 中引入了访问限定符,主要限定:类或者类中成
员能否在类外或者其他包中被访问。
package demo1;

public class Test {
    protected int a = 10;
}

package demo2;

import demo1.Test;

public class Test2 extends Test {
    public void func()
    {
        System.out.println(super.a);
    }

    public static void main(String[] args) {
      Test test = new Test();
//        System.out.println(test.a);

    }
}

被protected修饰的父类成员在不同包的子类中不能直接访问

我们希望类要尽量做到 " 封装 ", 即隐藏内部实现细节 , 只暴露出 必要 的信息给类的调用者 .
因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限 . 例如如果一个方法能用 private,
尽量不要用 public.

1.10 继承方式

在现实生活中,事物之间的关系是非常复杂,灵活多样,比如:

但在 Java 中只支持以下几种继承方式:
Java 中不支持多继承

1.11 final关键字

final 关键可以用来修饰变量、成员方法以及类。
1. 修饰变量或字段,表示常量 ( 即不能修改 )
final int a = 10;

a = 30;//编译出错
2. 修饰类:表示此类不能被继承
我们平时是用的 String 字符串类 , 就是用 final 修饰的 , 不能被继承 .
3. 修饰方法:表示该方法不能被重写(后续文章会介绍)

1.12 继承与组合

和继承类似 , 组合也是一种表达类之间关系的方式 , 也是能够达到代码重用的效果。组合并没有涉及
到特殊的语法 (诸如 extends 这样的关键字 ), 仅仅是将一个类的实例作为另外一个类的字段

比如一个学校中有很多学生和老师组成


public class Student {
    
}

public class Teacher {
    
}

public class School {
     public Student[] students;
     public Teacher[] teachers;

    public School() {
        students = new  Student[10];
        teachers = new  Teacher[10];
    }
}

注:组合只是一种设计方式 ,并不是面向对象的特征

组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议:
能用组合尽量用组合

2.多态

2.1 多态的概念

多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会产
生出不同的状 态。
总的来说:同一件事情,发生在不同对象身上,就会产生不同的结果。

当父类引用引用的子类对象不一样的时候,调用重写的方法,所表现出来的行为不一样,把这种思

想称为多态

2.2 多态实现条件

java 中要实现多态,必须要满足如下几个条件,缺一不可:
1. 必须是在继承关系上(存在向上转型)
2. 子类与父类中有同名的 覆盖/重名方法
3. 通过父类的引用调用重写的方法
完成以上三部分,就会发生动态绑定,而动态绑定时多态的基础
public 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+"正在吃饭");
    }

}

public class Cat extends Animal{
    public Cat(String name, int age) {
        super(name, age);
    }

    public void meme(){
        System.out.println(this.name+"正在咪咪叫");
    }
}

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

public class Main {
    public static void main(String[] args) {
              Animal animal = new Animal("小白",2);
              animal.bark();
        }
}

通过父类引用,只能调用父类自己特有的成员,方法或成员变量

2.2.1 什么是向上转型

把子类的对象给到父类,这个过程称为向上转型

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog("大黄",2);
        Animal animal = dog;
    }

可以直接这样写

public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog("大黄",2);
    }

因为AnimalDog之间存在继承关系,所以可以这样赋值

这里表示animal这个引用指向了dog这个引用所指向的对象

这个过程中就把子类对象给到了父类类型的引用,这就是向上转型

2.2.1.1 常见的可以发生向上转型的三个时机:
1. 直接赋值
public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog("大黄",2);
    }
2. 作为方法的参数,传参的时候向上转型

public class Main {
    public static void func1(Animal animal){

    }
    public static void main(String[] args) {
        Dog dog = new Dog("小黄",2);     
        func1(dog);     
    }
3. 作为方法的返回值时向上转型
public class Main {
    public static Animal func2(){
        Dog dog = new Dog("测试",10);
        return dog;
    }
    public static void main(String[] args) {
        Animal animal = new Dog("大黄",2);
        Animal animal1 = func2();
    }
多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。

2.3 重写

这里在Cat与Dog中分别再定义eat方法

public class Cat extends Animal{
    public Cat(String name, int age) {
        super(name, age);
    }

    public void meme(){
        System.out.println(this.name+"正在咪咪叫");
    }
    public void eat(){
        System.out.println(this.name+"正在吃猫粮");
    }
}
public 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 Main {

    public static void main(String[] args) {
        Animal animal = new Dog("大黄", 2);
        Animal animal1 = new Cat("花花", 3);
        animal.eat();
        animal1.eat();
    }
}

那么这里发现,通过父类的引用调用eat方法的时候,调用的是子类的eat方法

这个过程,称之为动态绑定

此时,就满足了上文中所提到的条件

重写 (override) :也称为覆盖。重写是子类对父类非静态、非 private 修饰,非 final 修饰,非构造方法
等的实现过程进行重新编写, 返回值和形参都不能改变 即外壳不变,核心重写! 重写的好处在于
子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
一般建议在重写方法时加上@Override
称为注解, @Override是注解的一种
他可以起到一个提示作用
比如
当不满足重写条件时,他就会报错
那么上文中提到重写的必需条件是 返回值和形参不能改变,访问修饰限定符是什么条件呢
public class Animal {
    public String name;
    public int age;

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


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

}
public class Cat extends Animal{
    public Cat(String name, int age) {
        super(name, age);
    }

    public void meme(){
        System.out.println(this.name+"正在咪咪叫");
    }
    @Override
    public void eat(){
        System.out.println(this.name+"正在吃猫粮");
    }
}
可以看到没有报错
   @Override
    protected void eat(){
        System.out.println(this.name+"正在吃猫粮");
    }
}

也没有报错

  @Override
    private void eat(){
        System.out.println(this.name+"正在吃猫粮");
    }
}

报错了

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

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


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

}

public class Cat extends Animal{
    public Cat(String name, int age) {
        super(name, age);
    }

    public void meme(){
        System.out.println(this.name+"正在咪咪叫");
    }
    @Override
    private void eat(){
        System.out.println(this.name+"正在吃猫粮");
    }
}

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

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


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

}
public class Cat extends Animal{
    public Cat(String name, int age) {
        super(name, age);
    }

    public void meme(){
        System.out.println(this.name+"正在咪咪叫");
    }
    @Override
    public private void eat(){
        System.out.println(this.name+"正在吃猫粮");
    }
}

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

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


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

}

public class Cat extends Animal{
    public Cat(String name, int age) {
        super(name, age);
    }

    public void meme(){
        System.out.println(this.name+"正在咪咪叫");
    }
    @Override
    public final void eat(){
        System.out.println(this.name+"正在吃猫粮");
    }
}

这里形式一致也报错

说明被private或static或final修饰的方法不可以被重写

那么还有没有其他情况呢?

 public Animal test(){
        return null;
    }

@Override
    public Cat test(){
        return null;
    }

并没有报错

这说明

被重写的方法返回值类型可以不同,但是必须是具有父子关系的

实现重写的条件:

1. 最基本的返回值,参数列表,方法名必须一样

2.被重写的方法的访问限定修饰符在子类中要大于等于父类的访问限定修饰符

3.被private或static或final修饰的方法不可以被重写

4.被重写的方法返回值类型可以不同,但是必须是具有父子关系的(协变类型)

5.构造方法不能被重写

注:

认为所有类的父类都是Object

到这里就可以提到之前笔者类与对象中重写toString

public class Main {

    public static void main(String[] args) {
        Dog dog = new Dog("大黄",2);
        System.out.println(dog);
    }
}

2.3.1 重载与重写的区别

区别点 重载 重写
参数列表 一定要修改 一定不能修改
返回类型 可以修改 除非是协变类型,否则不能修改
访问修饰限定符 可以修改 子类中要大于等于父类
即:方法重载是一个类的多态性表现 , 而方法重写是子类与父类的一种多态性表现。

2.3.2 重写的设计原则

对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其
中共性的内容,并且添加或者改动新的内容。
例如:若干年前的手机,只能打电话,发短信,来电显示只能显示号码,而今天的手机在来电显示
的时候,不仅仅可以显示号码,还可以显示头像,地区等。在这个过程当中,我们不应该在原来老
类上进行修改,因为原来的 类,可能还在有用户使用 ,正确做法是: 新建一个新手机的类,对来
显示这个方法重写就好了,这样就达到了我 们当今的需求了

2.3.3 动态绑定与静态绑定

静态绑定 :也称为前期绑定 ( 早绑定 ) ,即在编译时,根据用户所传递实参类型就确定了具体调用那
个方法。典型代表函数重载
动态绑定 :也称为后期绑定 ( 晚绑定 ) ,即在编译时,不能确定方法的行为,需要等到程序运行时,
才能够确定具体调用那个类的方法。
这就是静态绑定
public class Main {

   public static void main(String[] args) {
        Animal animal = new Dog("大黄",2);
        animal.eat();
    }
}

来看一下这段代码的底层

 

可以看到程序在编译的时候,确实调用的是Animal的eat

程序在运行时,调用了Dog的方法

这就是动态绑定

2.4 向上转型和向下转型

2.4.1 向上转型
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
语法格式:父类类型 对象名 = new 子类类型 ()
使用场景于2.2.1.1中已做说明 这里就不赘述
向上转型的优点:让代码实现更简单灵活。
向上转型的缺陷:不能调用到子类特有的方法。
public class Main {

   public static void main(String[] args) {
        Animal animal = new Dog("大黄",2);
        animal.eat();
    }
}

这里看起来好像是调用了子类中的eat,实际是程序在运行的过程中发生了动态绑定

2.4.2 向下转型
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要
调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。
那么是否直接赋值就行了呢?
可以看到是报错的
为什么呢
前文向上转型可以成功,是因为无论是Dog类还是Cat类,他们的父类一定就是Animal类
但在这里,此处的animal一定就是指的狗类吗?
那么在这里强转一下
public class Main {


    public static void main(String[] args) {
        Animal animal = new Dog("大黄",2);
        animal.eat();
        Dog dog = (Dog)animal;
        dog.bark();
    }
}

这里看起来好像没问题

那接下来,我如果想让这个狗喵喵叫呢

public class Main {


    public static void main(String[] args) {
        Animal animal = new Dog("大黄",2);
//        animal.eat();
//        Dog dog = (Dog)animal;
//        dog.bark();
    Cat cat = (Cat) animal;
    cat.meme();
    }
}

这里也没报错,运行一下

类型转换异常错误

强制类型转换本来就是不安全的

那么如何解决呢,这里可以用到instanceof

public class Main {


    public static void main(String[] args) {
        Animal animal = new Dog("大黄",2);
      // 如果animal引用的对象是Cat对象的实例,返回true
        if (animal instanceof Cat){
            Cat cat = (Cat) animal;
            cat.meme();
        }
        else {
            System.out.println("不是对应实例");
        }
    }
}

所以,向下转型是非常不安全的

2.5 多态的优缺点

2.5.1 优点

1.能够降低代码的 "圈复杂度", 避免使用大量的 if - else

假如现在要求画一些几何图形

public class Shape {
    public void draw(){
        System.out.println("画一个图形!");
    }
}
public class Triangle extends Shape{
    @Override
    public void draw() {
        System.out.println("画了一个三角形");
    }
}
public class Rect extends Shape{
    @Override
    public void draw() {
        System.out.println("画了一个矩形!");
    }
}
public class Cycle extends Shape {
    @Override
    public void draw() {
        System.out.println("画了一个圆形!");
    }
}

如果不使用多态,那么代码可能会这样写

public class Test {
    public static void main(String[] args) {
        Cycle cycle = new Cycle();
        Triangle triangle = new Triangle();
        Rect rect = new Rect();
        String[] strings = {"cycle","cycle","rect","triangle","rect"};
        for(String x: strings){
            if(x.equals("cycle")){
                cycle.draw();
            } else if (x.equals("rect")) {
                rect.draw();
            } else if (x.equals("triangle")) {
                triangle.draw();
            }
        }
    }
}

这样写确实可以满足要求,但是在代码中存在大量的if-else语句

如果用多态的思想来解决

public class Test {
    public static void drawMap(Shape shape){
        shape.draw();
    }

    public static void main(String[] args) {
        Shape[] shapes = {new Cycle(),new Cycle(),new Rect(),new Triangle(),new Rect()};
        for (Shape shape :shapes){
            shape.draw();
        }
    }
}

一样的效果,但是简化了很多,没有前者那么冗杂。

2. 可扩展能力更强
如果要新增一种新的形状 , 使用多态的方式代码改动成本也比较低 .
假如这里再想输出一朵花
public class Flower extends  Shape{
    @Override
    public void draw() {
        System.out.println("❀!");
    }
}
public class Test {
    public static void drawMap(Shape shape){
        shape.draw();
    }

    public static void main(String[] args) {
        Shape[] shapes = {new Cycle(),new Cycle(),new Rect(),new Triangle(),new Rect(),new Flower()};
        for (Shape shape :shapes){
            shape.draw();
        }
    }
}

对于类的调用者来说 (drawShapes 方法 ), 只要创建一个新类的实例就可以了 , 改动成本很低 .
而对于不用多态的情况 , 就要把 drawShapes 中的 if - else 进行一定的修改 , 改动成本更高 .

2.5.2 缺点

1. 属性没有多态性
当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性
2. 构造方法没有多态性

2.6 避免在构造方法中调用重写方法

package demo2;

class B {
    public B() {
// do nothing
        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 Test {
    public static void main(String[] args) {
        D d = new D();
    }
}

这说明,在父类的构造方法中,是可以调用子类和父类重写的方法,此时会调用子类重写的方法,

发生动态绑定

那么看一下D中num的值

package demo2;

class B {
    public B() {
// do nothing
        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() "+num);
    }
}
public class Test {
    public static void main(String[] args) {
        D d = new D();
    }
}

可以看到num的值是0

构造 D 对象的同时 , 会调用 B 的构造方法 .
B 的构造方法中调用了 func 方法 , 此时会触发动态绑定 , 会调用到 D 中的 func
此时 D 对象自身还没有构造 , 此时 num 处在未初始化的状态 , 值为 0. 如果具备多态性, num 的值
应该是 1.
所以在构造函数内,尽量避免使用实例方法,除了 final private 方法。

网站公告

今日签到

点亮在社区的每一天
去签到