Java继承与多态

文章目录

前言

本人的技术笔记会优先发布在个人站点longyxblg.cn

继承

面向对象思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用。

概念

继承是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性

的基础上进行扩展,增加新功能,这样产生新的类,称子类(也称派生类或扩展类)。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。

语法

在 Java 中如果要表示类之间的继承关系,需要用到 extends 关键字

java 复制代码
修饰符 class 子类 extends 父类 {
// ...  
}

此时有一个 Animal 类, 有 Dog 类和 Cat 类继承他

java 复制代码
public class Animal {
    String name = "默认名称";
    int age = 18;

    public void eat(){
        System.out.println(this.name + "正在吃饭");
    }
}
java 复制代码
public class Cat extends Animal{

}
java 复制代码
public class Dog extends Animal{
    
}

子类会将父类中的成员变量和成员方法继承

java 复制代码
public class Test {
    public static void main(String[] args) {
//       
        Cat cat = new Cat();
        Dog dog = new Dog();
        System.out.println(cat.age);
        System.out.println(cat.name);
        System.out.println(dog.age);
        System.out.println(dog.name);
        cat.eat();
        dog.eat();
    }
}

父类成员变量访问

  • 子类和父类不存在同名成员变量

    参考上文代码。

  • 子类和父类存在同名成员变量

    java 复制代码
    public class Dog extends Animal{
     String name = "旺财";
    }
    java 复制代码
        Dog dog = new Dog();
        Animal animal = new Animal();
        System.out.println(dog.name);
        System.out.println(animal.name);

    如果访问的成员变量子类中有,优先访问自己的成员变量。

父类成员方法的访问

  • 成员方法不同名(或子类中没有该成员方法)

    参考上文中的代码

  • 成员方法同名

    java 复制代码
    public class Dog extends Anima{
           String name = "旺财";
    
        @Override
        public void eat() {
            System.out.println(this.name + "正在吃狗粮");
        }
    }
    java 复制代码
        Dog dog = new Dog();
        Animal animal = new Animal();
        dog.eat();
        animal.eat();

    同名时,子类对象会优先使用自己的成员方法。

super 关键字

在某些情况下,想要在子类中使用父类的成员方法和成员变量,这时候可以用到 super 关键字。

java 复制代码
public class Animal {
    String name = "默认名称";
    int age = 18;
    String variety = "我也不知道写什么";


    public void eat(){
        System.out.println(this.name + "正在吃饭");
    }
}
java 复制代码
public class Dog extends Animal{
    String name = "旺财";
    String variety = "中华田园犬";
    public void eat() {
        System.out.println(this.name + "正在吃狗粮");
    }

    public void test1(){
        System.out.println(super.variety);
        System.out.println("测试用 super 访问父类中的 variety");
    }
}
java 复制代码
        Dog dog = new Dog();
        System.out.println(dog.variety);
        dog.test1();

java 复制代码
public class Animal {
    String name = "默认名称";
    int age = 18;
    String variety = "我也不知道写什么";


    public void eat(){
        System.out.println(this.name + "正在吃饭");
    }
}
java 复制代码
public class Dog extends Animal{
    String name = "旺财";
    String variety = "中华田园犬";
    public void eat() {
        System.out.println(this.name + "正在吃狗粮");
    }
    public void animalEat(){
        super.eat();
    }

    public void test1(){
        System.out.println(super.variety);
        System.out.println("测试用 super 访问父类中的 variety");
    }
}
java 复制代码
        Dog dog = new Dog();
        dog.eat();
        dog.animalEat();

super 关键字也可以访问父类中的成员方法。


需要注意的是 super 关键字不能在静态方法中使用。

父类与子类的构造方法

java 复制代码
public class Animal {
    String name = "默认名称";
    int age = 18;
    String variety = "我也不知道写什么";

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

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

给 Animal 中手写一个构造方法后,编译报错了

这其实是因为子类也会继承父类的构造方法,前文的代码中 Animal、Cat、Dog 中都没有写构造方法,这时编译器会给这三个类中自动生成构造方法。

Animal 中自动生成的是

java 复制代码
   public Animal() {

    }

而到 Cat 和 Dog 类中生成的构造方法则是

java 复制代码
    public Cat(){
        super;
    }
java 复制代码
    public Dog(){
        super;
    }

编译器给子类自动生成的构造方法会用 super 关键字自动调用父类的构造方法。这意味着开发人员也可以用 super 关键字进行调用。

java 复制代码
public class Animal {
    String name = "默认名称";
    int age = 18;
    String variety = "我也不知道写什么";

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

    public Animal() {

    }

    public void eat(){
        System.out.println(this.name + "正在吃饭");
    }
}
java 复制代码
public class Dog extends Animal{
    String name = "旺财";
    String variety = "中华田园犬";
    public void eat() {
        System.out.println(this.name + "正在吃狗粮");
    }
    public void animalEat(){
        super.eat();
    }

    public Dog() {
    }

    public Dog(String name, int age, String variety) {
        super(name, age, variety);
    }

    public static void test2(){
//        super 关键字不能在静态方法中使用。
//        System.out.println(super.age);
    }

    public void test1(){
        System.out.println(super.variety);
        System.out.println("测试用 super 访问父类中的 variety");
    }
}
java 复制代码
        Dog dog = new Dog("哈吉汪",2,"哈士奇");
        dog.eat();
        dog.animalEat();

通过调试窗口可以看到,在 Dog 类中调用使用 Super 方法后, 是给从 Animal 中继承下来的成员变量进行赋值。Dog 类中存在的同名成员变量并没有进行赋值。

  • this 是当前对象的引用,当前对象即调用实例方法的对象,super 相当于是子类对象中从父类继承下来部分成员的引用

  • 在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性

  • 在构造方法中无论是 this 引用还是 super 引用都要放在构造方法的第一句

  • 在构造方法中不能同时出现 this 引用和 super 引用

  • 构造方法中一定会存在super(...)的调用,用户没有写编译器也会增加,但是this(...)用户不写则没有

  • this 和 super 都只能在类的非静态方法中使用, 访问非静态方法和字段

父类与子类代码执行的顺序

java 复制代码
public class A {
    int a;

    static {
        System.out.println("父类的静态代码块执行了");
    }

    {
        this.a = 10;
        System.out.println("父类的构造代码块执行了");
    }
}
java 复制代码
public class B extends A{
    int b;
    static {
        System.out.println("子类的静态代码块执行了");
    }

    {
        this.b = 20;
        System.out.println("子类的构造代码块执行了");
    }
}

可见是静态代码块先执行,父类的代码块比子类的代码块先执行

java 复制代码
        A a1 = new A();
        B b1 = new B();
        System.out.println("====我是一条分割线====");
        A a2 = new A();
        B b2 = new B();

静态代码块只会执行一次

初始化顺序为父类静态代码块 → 子类静态代码块 → 父类实例代码块 → 父类构造方法 → 子类实例代码块 → 子类构造方法

protected

在类与对象中介绍了 public private default 三个访问修饰限定符。

名称 同一个包同类 同一个包不同类 不同包中子类 不同包中的非子类
public √ √ √ √
protected √ √ √
default √ √
private √

而 protected 主要用于继承中

此时新建一个 Demo2 包, 其中存放 C Test 类 , C 继承 A 类。

java 复制代码
package Demo1;

public class A {
    protected int  a;

    protected void test(){
        System.out.println("这是一个A类中被 protected 修饰的方法");
    }

    public A(){

    }
}
java 复制代码
package Demo2;

import Demo1.A;

public class C extends A {

    public void c_Test(){
        System.out.println(super.a);
        super.test();
    }

    public static void main(String[] args) {
        C c1 = new C();
        c1.c_Test();
    }
}
java 复制代码
package Demo2;

import Demo1.A;

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

那么让 A 中出现一个被 private 修饰的变量

java 复制代码
package Demo1;

public class A {
    protected int  a;

    private int b;

    protected void test(){
        System.out.println("这是一个A类中被 protected 修饰的方法");
    }

 

    public A(){

    }
java 复制代码
    public static void main(String[] args) {
        C c1 = new C();
        System.out.println();
    }

通过调试查看到, 被 private 修饰的成员变量 b 也被集成到了子类中

但是就算被继承到了子类中也不能访问

继承的方式

Java 中只支持四种继承方式:

  • 单继承

    最简单的继承方式,例如

    java 复制代码
        public class A{
            ...
        }
    
        public class B extends A{
            ...
        }
  • 不同类继承同一个类

    多个类继承同一个类,例如

    java 复制代码
        public class A{
            ...
        }
    
        public class B extends A{
            ...
        }
    
        public class C extends A{
            ...
        }
  • 多层继承

    Java 中不支持多继承,例如

    java 复制代码
        public class A{
            ...
        }
    
        public class B {
            ...
        }
    
        public class C extends A,B{
            ...
        }

    可以做到多层继承,例如

    java 复制代码
        public class A{
            ...
        }
    
        public class B extends A {
            ...
        }
    
        public class C extends B{
            ...
        }

一般不希望出现超过三层的继承关系. 如果继承层次太多, 就需要考虑对代码进行重构了。如果想从语法上进行限制继承, 就可以使用 final 关键字

final 关键字

可以用来修饰变量、成员方法以及类。

修饰变量

被 final 修饰的变量就会被视为常量,不允许修改,类似于 C 语言中的 const 关键字。

修饰类

被 final 修饰的类将不被允许继承

java 复制代码
package Demo1;

final public class A {
}
java 复制代码
package Demo1;

 public class B extends A {
}

修饰成员方法

被 final 修饰的成员方法不允许被重写,会在后文介绍。

多态

面向对象的三大特性之一,简单点说就是不同的对象去完成同一个行为,会产生不同的状态。例如同样都是学习 Java , 张三学得非常轻松,李四学得非常痛苦。这就是多态。

Java 中多态的实现条件

在 Java 中想要实现多态,必须满足以下条件,缺一不可:

  • 必须在继承体系下

  • 子类必须要对父类中方法进行重写

  • 通过父类的引用调用重写的方法

方法的重写

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

即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。


规则

子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 、方法名 、(参数列表) 要完全一致。

java 复制代码
package Demo1;

 public class A {

     public void test1(){
         System.out.println("这是 A 类中的test1方法");
     }
}
java 复制代码
package Demo1;

public class B extends A{
    @Override
    public void test1() {
        System.out.println("这是 B 类中的test1方法");
    }
}
java 复制代码
        A a1 = new A();
        B b1 = new B();
        a1.test1();
        b1.test1();

@Override 表示该方法被重写,编译器会多一层校验

  • 被重写的方法返回值类型可以不同,但是必须是具有父子关系的
  • 访问权限不能比父类中被重写的方法的访问权限更低

这里 B 类中没写修饰符默认为 default , 编译报错。

重写和重载

重写\重载 参数列表 方法名 返回值 访问修饰限定符
重写 必须相同 必须相同 必须相同,不同必须具有父子类关系 权限大于等于被重写的方法
重载 必须不同 必须相同 可以不同 可以不同

重写的原则

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


通过重写,可以让同一个方法在被不同类型的对象使用时发生不同的行为,这正是多态的体现。

向上转型与动态绑定

现在新创建 Animal Dog Cat 类

java 复制代码
package Demo2;

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

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

    public void eat(){
        System.out.println(this.name + "正在吃饭");
    }
}
java 复制代码
package Demo2;

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

    @Override
    public void eat() {
        System.out.println(this.name + "正在吃狗粮");
    }
}
java 复制代码
package Demo2;

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

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

实际就是创建一个子类对象,将其当成父类对象来使用。发生在三种情况中:

  • 直接在创建对象时发生向上转型
java 复制代码
        Animal animal = new Dog("大黄",1,"出去玩");
        animal.eat();

在这里 new Dog 是实例化的一个 Dog 类对象,而 Animal 类对象 animal 却能引用这个对象,就是在此处发生了向上转型。

而 Animal 类型的对象 animal 在调用 eat 方法时输出的是 Dog 类中的 eat 方法。这是发生了动态绑定,JVM 发现 animal 实际是 Dog,就调用 Dog 的 eat。

  • 方法传参时发生向上转型:
java 复制代码
package Demo2;


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

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

    public Animal(){

    }

    public void test1(Animal animal){
        System.out.println(animal.name + "这是 Animal 类的 test1 方法");
    }

    public void eat(){
        System.out.println(this.name + "正在吃饭");
    }
}
java 复制代码
package Demo2;

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

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

    @Override
    public void test1(Animal animal) {
        System.out.println(animal.name + "这是 Dog 类的 test1 方法");
    }
}
java 复制代码
        Dog dog = new Dog("大黄",1,"出去玩");
        Animal animal = new Animal();
        dog.test1(dog);
  • 方法返回值发生向上转型
java 复制代码
package Demo2;

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

    public Dog() {
        super();
    }

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

    @Override
    public void test1(Animal animal) {
        System.out.println(animal.name + "这是 Dog 类的 test1 方法");
    }

    public static Dog test2(){
        return new Dog();
    }
}
java 复制代码
        Animal animal = Dog.test2();
        System.out.println();

调试窗口可以看到也成功赋值了。


需要注意的是,不能调用到子类特有的方法

java 复制代码
package Demo2;

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

    public Dog() {
        super();
    }

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

    @Override
    public void test1(Animal animal) {
        System.out.println(animal.name + "这是 Dog 类的 test1 方法");
    }

    public static Dog test2(){
        return new Dog();
    }

    public void test3(){
        System.out.println("这是 Dog 类独有的 test3 方法");
    }
}
java 复制代码
        Animal animal = new Dog();
        animal.test3();

向下转型

将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转型。

java 复制代码
        Animal animal = new Dog();
        Dog dog = (Dog) animal;
        dog.test3();

需要注意的是向下转型会用到强制类型转换,这本来就是不安全的。如果转换失败,运行时会抛出异常。Java 中为了提高向下转型时的安全性,引入了 instanceof 关键字。

java 复制代码
        Animal animal = new Animal();
        if(animal instanceof Dog){
            Dog dog = (Dog) animal;
        }
        else
        {
            System.out.println("不能这么转换!");
        }

多态的使用

此时新建另一个包 Demo3,在这个包下新建 Animal Dog Cat Bird Fish 类,Dog Cat Bird Fish 继承 Animal 。

java 复制代码
package Demo3;

public class Animal {

    public void eat(){
        System.out.println("吃饭!");
    }
}
java 复制代码
package Demo3;

public class Dog extends Animal{
    @Override
    public void eat() {
        System.out.println("吃狗粮!");
    }
}
java 复制代码
package Demo3;

public class Cat extends Animal{
    @Override
    public void eat() {
        System.out.println("吃猫粮");
    }
}
java 复制代码
package Demo3;

public class Bird extends Animal{
    @Override
    public void eat() {
        System.out.println("吃虫子!");
    }
}
java 复制代码
package Demo3;

public class Fish extends Animal{
    @Override
    public void eat() {
        System.out.println("吃蚯蚓!");
    }
}

现在想输出每个类的 eat 方法。采用分支语句可能要这么写

java 复制代码
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        Bird bird = new Bird();
        Fish fish = new Fish();
        Animal[] animals = {dog,cat,bird,fish};
        for (Animal animal: animals){
            if(animal.equals(dog)){
                dog.eat();
            }else if(animal.equals(cat)){
                cat.eat();
            }else if(animal.equals(bird)){
                bird.eat();
            }else if(animal.equals(fish)){
                fish.eat();
            }
        }

而是用多态的话就简单多了

java 复制代码
        Dog dog = new Dog();
        Cat cat = new Cat();
        Bird bird = new Bird();
        Fish fish = new Fish();
        Animal[] animals = {dog,cat,bird,fish};
        for(Animal animal: animals){
            animal.eat();
        }
    }

新增一个 Monkey 类

这时要输出这个 Monkey 类的 eat() 方法使用多态完成也更简单

java 复制代码
        Dog dog = new Dog();
        Cat cat = new Cat();
        Bird bird = new Bird();
        Fish fish = new Fish();
        Monkey monkey = new Monkey();
        Animal[] animals = {dog,cat,bird,fish,monkey};
        for(Animal animal: animals){
            animal.eat();
        }

但多态并非没有缺点;

  • 属性没有多态性,当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性

  • 构造方法没有多态性

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

java 复制代码
public class A {
    public A(){
        func();
    }

    public void func(){
        System.out.println("A类中的func方法执行了");
    }
}
java 复制代码
public class B extends A{
    private int num = 1;

    @Override
    public void func() {
        System.out.println("B类中的func方法执行了"+num);
    }
}
java 复制代码
        B b = new B();

在实例化 b 时, 会调用 A 中的构造方法,而构造方法中又调用了 func 方法。这个过程是由 b 这个对象调用的,触发动态绑定,直接调用了 B 类中被重写的 func 方法,而此时 b 对象还没有被构造完成,num 处于未初始化的状态,所以为 0 。如果具有多态性的话,num 值应该为 1 。

相关推荐
企业数字化笔记1 小时前
视频字幕如何烧录到成片?硬字幕、软字幕、样式控制与播放验收
java·ffmpeg
小朱爱编程1231 小时前
我用 Jev 做了三个实用工具:整理标签页、分诊飞书反馈、找回 GitHub 收藏
java·开发语言·人工智能·后端·python·架构·ai编程
酷虎软件2 小时前
视频加标题字幕 API 接口文档
java·数据库·mysql
谢亮_vipxieliang2 小时前
Java 8/11/17/21/25 怎么选?一篇讲清 LTS 升级路线
java·开发语言
Cindy_cd2 小时前
IDEA2021 配置 JDK+Maven
java·开发语言·maven
我要神龙摆尾2 小时前
IDEA 阅读插件
java·ide·intellij-idea·idea-plugs·plugs
涉密IT资质笔记3 小时前
涉密人员脱密期管理规范:期限分级模型、就业限制边界与违规认定标准
java·服务器·前端·网络·数据库
mmsx3 小时前
Android Dialog 插槽模式:用一套壳统一全应用的弹窗风格
android·java·dialog·开发语言
u0111026753 小时前
工具页案例图如何编写 alt让图片说明与处理场景对应
java·前端·javascript·图像处理·人工智能·算法·ai作画