Java 继承与多态


一、什么是继承?

通俗理解,继承就是:子类继承父类
比如谚语“龙生龙,凤生凤,老鼠的儿子会打洞”,所谓继承道理都是继承了亲代的特征和行为(属性和方法)。在代码里就是:把多个类中 重复相同的代码(属性和方法)提取出来,放到父类中,其他类作为子类直接继承使用,不用重复编写。子类可直接拿父类的属性和方法
语法格式

class 子类名 extends 父类名{
    
}

首先有了父类,然后子类才继承父类,即子类 extends 父类
extends是延展的意思,因为在继承关系下,子类会比父类更优秀,更具体,更完善。
. 继承作用干什么?
1.减少重复代码(最重要)
2. 代码统一、方便维护
3. 为多态做铺垫
对于程序员来说,对代码的直接修改跟新是困难的,如果加一个新的类,或改一个类中的方法,要更改许多的代码是不划算的,如果把各个类中相同的特征提出来变成父类,然后让子类继承父类,并让不同的特点体现在子类,形成这样的如同家族族谱的树状联系,使得子类的比父类更具体,更多元,比如:人的概念都有眼睛鼻子耳朵,会吃饭,睡觉,这些是一样的,然后人们又根据年龄划分为年轻,中年,老年,再划分高矮胖瘦,各司职业等等,使得对象一步步具体,一步步鲜明,以实现我们面向对象一切皆对象的的目的,更改时往往只需要更改特殊的部分,相同的部分无需更改,也就可通过增删子类或改变子类实现,便于代码的维护和更新。
简单代码例子:
父类:动物。

// 父类:动物类,提取所有动物的共同属性和方法
public class Animal {
    // 公共属性:所有动物都有名字和年龄
    String name;
    int age;

    // 父类构造方法
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 公共方法:所有动物都会吃和睡
    public void eat() {
        System.out.println(name + "在吃东西");
    }

    public void sleep() {
        System.out.println(name + "在睡觉");
    }
}
  1. 写子类 Dog(继承 Animal,拥有自己的特性)
运行
// 子类:狗类,继承自Animal
// 关键字:extends 表示"继承"
public class Dog extends Animal {
    // 子类独有的属性:品种
    String breed;

    // 子类构造方法:必须先调用父类构造方法(super())
    public Dog(String name, int age, String breed) {
        // super() 调用父类的构造方法,初始化继承来的name和age
        super(name, age);
        this.breed = breed;
    }

    // 子类独有的方法:狗会叫
    public void bark() {
        System.out.println(name + "汪汪叫");
    }
}
  1. 测试运行
public class Test {
    public static void main(String[] args) {
        // 创建Dog对象
        Dog dog = new Dog("旺财", 3, "金毛");

        // 可以直接调用父类继承来的属性和方法
        System.out.println("名字:" + dog.name);
        System.out.println("年龄:" + dog.age);
        dog.eat();
        dog.sleep();

        // 也可以调用子类自己的方法
        System.out.println("品种:" + dog.breed);
        dog.bark();
    }
}

输出结果

名字:旺财
年龄:3
旺财在吃东西
旺财在睡觉
品种:金毛
旺财汪汪叫

继承特点

  • Java 单继承:一个子类只能有一个父类(但子类用父类的父类也可以)
  • 父类私有 private 东西不能继承(实际上是继承了,但语法规定用不了)
  • 子类可以扩展自己的方法
  • 构造方法不能继承
    构造方法绝对不能被继承!这是 Java 的硬性规定。
    很多人会把「子类构造方法里自动调用super()」误以为是「继承了父类的构造方法」,这是最常见的误区。
    如果构造方法能被继承,那么下面的代码应该能运行,但实际上编译直接报错:
运行
class Animal {
    // 父类构造方法
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

class Dog extends Animal {
    // 子类什么都不写
}

public class Test {
    public static void main(String[] args) {
        // ❌ 编译报错!Dog类没有这个构造方法
        Dog dog = new Dog("旺财", 3);
    }
}

报错原因:
Dog类根本没有继承Animal的Animal(String name, int age)构造方法。它自己一个构造方法都没写,所以只有编译器默认给的无参构造Dog()。
----那**super()**到底是什么?
super()不是继承构造方法,而是子类构造方法主动调用父类的构造方法。
这是两个完全不同的概念:
继承:子类拥有了父类的方法,可以直接用自己的对象调用
调用:子类只是 “借用” 父类的构造方法来完成初始化,自己并没有拥有这个方法
为什么必须调用父类构造方法?
构造方法的唯一作用就是初始化对象的属性(给变量赋值),因为子类继承了父类的所有非私有属性(比如name、age),而这些属性是父类定义的,只有父类自己的构造方法才知道怎么正确初始化它们,子类不能直接给父类的变量赋值(尤其是当变量是 private 的时候)
所以 Java 强制规定:
子类构造方法的第一行,必须调用父类的某个构造方法。
如果你不写,编译器会自动帮你加一行super();(调用父类的无参构造),相当于隐藏一个super。
所以创建子类对象的完整过程是这样的:
当你执行 new Cat(“小白”) 时,JVM 会按这个顺序做事:
先分配内存空间,准备创建 Cat 对象
第一步:调用父类 Animal 的构造方法,初始化父类的name属性为 “小白”
第二步:调用子类 Cat 的构造方法,初始化子类自己的属性(如果有的话)

二、方法重写(Override)

子类的方法和父类有所差异,显示出来了新的特点,重新写一遍,这就叫重写。
重写规则

  • 方法名、参数列表必须一模一样
  • 返回值一样
  • 访问权限不能变严格(public 不能改成 private)
  • 注解 @Override,校验重写格式是否正确

重写代码示例

class Dog extends Animal{
    // 重写父类吃饭方法
    @Override
    public void eat() {
        System.out.println("狗吃骨头");
    }
}

三、其他关键字

super
super():调用父类无参构造方法,只能放在子类构造第一行
super.变量名:访问父类的成员变量
super.方法名():调用父类的成员方法
super(实参):调用父类有参构造
this
区分:this 代表本类对象,super 代表父类对象
final
final 类:此类不能被继承(绝后了)
final 方法:方法不能被子类重写

四、什么是多态?

多态就是多种形态,对于同一个行为,不同子类对象执行,表现出不同效果,
而当子类重写了父类的方法时,父类引用会执行子类重写后的版本。这就是多态的本质。
比如动物都有吃饭行为:狗吃饭:啃骨头,猫吃饭:吃鱼,人吃饭:吃米饭
同样是吃饭动作,不同对象结果不一样,这就体现了多态。
用一句话概况就是:父类型引用指向子类型对象,调用重写方法,执行子类逻辑
1.实现多态的三个必备前提(缺一不可)

  1. 必须存在继承关系(子类继承父类)
  2. 子类必须重写父类方法
  3. 父类型引用指向子类型对象(向上转型)

2.核心语法格式

父类类型 引用变量名 = new 子类对象();

示例

Animal animal = new Dog();

即:父类型引用指向子类型对象

3.核心口诀(必背)
编译看左边,运行看右边
编译时期:只看左边父类,父类有这个方法就不报错,总是先找父类,再找父类的父类,最后找不到,报错。
运行时期:先执行右边子类重写后的方法,如若没有,再执行父类方法。

完整代码演示
(1. 父类

class Animal{
    public void eat(){
        System.out.println("动物吃东西");
    }
}

(2. 多个子类 继承并重写

// 狗类
class Dog extends Animal{
    @Override
    public void eat() {
        System.out.println("小狗吃骨头");
    }
}

// 猫类
class Cat extends Animal{
    @Override
    public void eat() {
        System.out.println("小猫吃鱼");
    }
}

(3. 测试实现多态

public class Test {
    public static void main(String[] args) {
        // 父类引用 指向 子类对象
        Animal a1 = new Dog();
        Animal a2 = new Cat();

        // 调用同一个方法,执行不同结果
        a1.eat();
        a2.eat();
    }
}

运行结果

小狗吃骨头
小猫吃鱼

左边是父类的引用获得的的子类对象的地址,右边是具体的子类对象,而再构建子类对象的过程中,都会先勾勒父类的特征(父类的属性和方法),然后再扩展子类独有的属性和方法。换句话说,一个 父类对象里面,其实包含了一个完整的子类对象,再加上 自己特有的部分。
类比一下就像是盖房子,父类是地基,子类是在地基上盖的二楼。你不可能先盖二楼,再打地基。

 栈内存:Animal an
           │
           │ 指向
           ▼
堆内存:new Dog()
    ┌───────────────────────────┐
    │  子类Dog扩展区(再画) 
    │  - 属性:tailLength      
    │  - 方法:breed()         
    ├───────────────────────────┤
    | 父类Animal特征区(先画) 
    │  - 属性:name, age       
    │  - 方法:eat()          
    └───────────────────────────┘

多态访问成员规则也差不多
1.访问成员方法
-编译:看父类有没有该方法
-运行:先执行子类重写的方法,没有再执行父类方法。
2.访问成员变量
编译、运行 全都看左边父类
-变量不存在多态效果,只调用父类变量
3.静态方法
-没有多态,调用左边父类静态方法

我们实现父类型引用指向子类型对象就是为了多态,因为这样可以调用子类重写的方法(子类特有的方法不可调用! (一定要记住!))而且还带来了三个方面的优势:

  1. 代码解耦
    上层代码只依赖父类,不依赖具体的子类。以后新增类,不需要修改任何上层代码,直接继承即可。
  2. 统一接口
    所有子类都必须遵守父类定义的方法规范,保证了代码的一致性。
  3. 扩展性极强
    你可以写一个通用的方法,处理所有子类对象。

没有多态的写法(子类引用)
如果我们要养很多不同的动物,需要为每个动物写单独的代码:

运行
Dog dog = new Dog("大黄", 2, "金毛");
Cat cat = new Cat("咪咪", 1, "橘色");
Bird bird = new Bird("小鸟", 0.5, "麻雀");

dog.eat();
cat.eat();
bird.eat();

如果再新增一个Pig类,你需要再写一行pig.eat(),非常冗余。

有多态的写法(父类引用)
用父类引用可以统一处理所有子类对象:

运行
// 一个数组可以装所有动物
Animal[] animals = {
    new Dog("大黄", 2, "金毛"),
    new Cat("咪咪", 1, "橘色"),
    new Bird("小鸟", 0.5, "麻雀")
};

// 一个循环处理所有动物
for (Animal animal : animals) {
    animal.eat(); // 自动执行对应子类的eat()方法
}
输出:
plaintext
狗在啃骨头
猫在吃鱼
鸟在吃虫子

如果再新增一个Pig类,你只需要在数组里加一行new Pig(),其他代码完全不用改。这就是开闭原则:对扩展开放,对修改关闭。

五、向上转型 & 向下转型

子类框架永远比父类大,因为它完整包含了父类的所有内容

┌─────────────────────────┐
│ 父类 Animal             │  ← 小框架(所有动物的共性)
│ 属性:name, age         │
│ 方法:eat(), sleep()    │
└─────────────────────────┘

┌─────────────────────────────────────────┐
│ 子类 Dog                                │  ← 大框架(狗的独有特性)
│ ┌─────────────────────────┐             │
│ │ 父类 Animal 全部内容    │  继承而来     │
│ └─────────────────────────┘             │
│                                         │
│ ┌─────────────────────────┐             │
│ │ 狗的独有部分            │  自己新增     │
│ │ 属性:breed(品种)       │              │
│ │ 方法:bark()(汪汪叫)    │              │
│ └─────────────────────────┘             │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│ 子类 Cat                                │  ← 另一个大框架
│ ┌─────────────────────────┐             │
│ │ 父类 Animal 全部内容    │  继承而来     │
│ └─────────────────────────┘             │
│                                         │
│ ┌─────────────────────────┐             │
│ │ 猫的独有部分            │  自己新增     │
│ │ 属性:color(毛色)       │              │
│ │ 方法:meow()(喵喵叫)    │              │
│ └─────────────────────────┘             │
└─────────────────────────────────────────┘

1. 向上转型(自动转换)

// 向上转型:自动完成,永远安全
Animal animal = new Dog("大黄", 2, "金毛");

// ✅ 可以调用所有动物都有的方法
animal.eat();
animal.sleep();

// ❌ 不能调用只有狗才有的方法,编译报错
animal.bark();
System.out.println(animal.breed);
                        转型前
┌─────────────────────────────────────────┐
│ 完整的 Dog 对象(大黄)                  │
│ ┌─────────────────────────┐             │
│ │ name="大黄", age=2      │  ✅ 可见    │
│ │ eat(), sleep()          │             │
│ └─────────────────────────┘             │
│                                         │
│ ┌─────────────────────────┐             │
│ │ breed="金毛"            │  ✅ 可见    │
│ │ bark()                  │             │
│ └─────────────────────────┘             │
└─────────────────────────────────────────┘
                          ↓
                        向上转型(自动)
Animal animal = new Dog("大黄", 2, "金毛");
                          ↓
                        转型后
┌─────────────────────────────────────────┐
│ 完整的 Dog 对象(大黄)                   │
│ ┌─────────────────────────┐             │
│ │ name="大黄", age=2      │  ✅ 可见    │  ← animal引用只能看到这么大
│ │ eat(), sleep()          │             │
│ └─────────────────────────┘             │
│                                         │
│ ┌─────────────────────────┐             │
│ │ breed="金毛"            │  ❌ 隐藏    │
│ │ bark()                  │             │
│ └─────────────────────────┘             │
└─────────────────────────────────────────┘
        ↑
        └─────────────────────────────────┐
                                          │
                        animal引用的可见范围(小框架)
                     
  • 自动完成,就是多态写法
  • 缺点:只能调用父类方法,不能调用子类独有方法

2. 向下转型(强制转换)

用途:转回子类,调用子类独有的方法
语法

子类类型 对象名 = (子类类型)父类引用;
                        转型前
┌─────────────────────────────────────────┐
│ 完整的 Dog 对象(大黄)                   │
│ ┌─────────────────────────┐             │
│ │ name="大黄", age=2      │  ✅ 可见    │
│ │ eat(), sleep()          │             │
│ └─────────────────────────┘             │
│                                         │
│ ┌─────────────────────────┐             │
│ │ breed="金毛"            │  ❌ 隐藏    │
│ │ bark()                  │             │
│ └─────────────────────────┘             │
└─────────────────────────────────────────┘
        ↑
        └─────────────────────────────────┐
                                          │
                        animal引用的可见范围(小框架)
                          ↓
                        向下转型(强制)
Dog dog = (Dog) animal;
                          ↓
                        转型后
┌─────────────────────────────────────────┐
│ 完整的 Dog 对象(大黄)                   │
│ ┌─────────────────────────┐             │
│ │ name="大黄", age=2      │  ✅ 可见    │
│ │ eat(), sleep()          │             │
│ └─────────────────────────┘             │
│                                         │
│ ┌─────────────────────────┐             │
│ │ breed="金毛"            │  ✅ 可见    │
│ │ bark()                  │             │
│ └─────────────────────────┘             │
└─────────────────────────────────────────┘
        ↑
        └─────────────────────────────────────────────┐
                                                      │
                        dog引用的可见范围(大框架)

示例

// 先向上转型
Animal animal = new Dog("大黄", 2, "金毛");

// 向下转型:需要强制转换
Dog dog = (Dog) animal;

// ✅ 现在可以调用狗独有的方法了
dog.bark();
System.out.println("品种:" + dog.getBreed());

3. 安全转型:instanceof

防止类型转换异常,先判断对象真实类型

if(an instanceof Dog){
    Dog d = (Dog)an;
}

多态的优势

  1. 提高代码扩展性
    新增子类时,原有代码不用修改,直接使用
  2. 简化代码,统一调用
    统一使用父类类型接收所有子类对象,写法统一
  3. 降低耦合度
    父类定义规范,子类各自实现,互不干扰
  4. 方法参数通用
    方法形参写父类,所有子类对象都能传入

更多推荐