Java继承和多态入门
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 + "在睡觉");
}
}
- 写子类 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 + "汪汪叫");
}
}
- 测试运行
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.实现多态的三个必备前提(缺一不可)
- 必须存在继承关系(子类继承父类)
- 子类必须重写父类方法
- 父类型引用指向子类型对象(向上转型)
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.静态方法
-没有多态,调用左边父类静态方法
我们实现父类型引用指向子类型对象就是为了多态,因为这样可以调用子类重写的方法(子类特有的方法不可调用! (一定要记住!))而且还带来了三个方面的优势:
- 代码解耦
上层代码只依赖父类,不依赖具体的子类。以后新增类,不需要修改任何上层代码,直接继承即可。 - 统一接口
所有子类都必须遵守父类定义的方法规范,保证了代码的一致性。 - 扩展性极强
你可以写一个通用的方法,处理所有子类对象。
没有多态的写法(子类引用)
如果我们要养很多不同的动物,需要为每个动物写单独的代码:
运行
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;
}
多态的优势
- 提高代码扩展性
新增子类时,原有代码不用修改,直接使用 - 简化代码,统一调用
统一使用父类类型接收所有子类对象,写法统一 - 降低耦合度
父类定义规范,子类各自实现,互不干扰 - 方法参数通用
方法形参写父类,所有子类对象都能传入
更多推荐



所有评论(0)