Java 之继承与多态
继承与多态是 Java 面向对象的核心,其内容会比较难懂,所以我专门开一篇博文来厘清一下这些知识点。
目录
1. 继承
1.1 为什么需要继承
1.2 继承的概念
1.3 如何实现继承
1.3.1 父类:Stationery(提取共性)
1.3.2 子类 1:Pen(继承 + 扩展)
1.3.3 子类 2:Notebook(继承 + 扩展)
1.3.4 继承的效果
1.4 super 关键字
1.4.1 访问父类的成员变量
1.4.2 调用父类的构造方法
1.4.3 调用父类的成员方法
1.5 final 关键字
1.5.1 final 修饰类
1.5.2 final 修饰方法
1.5.3 final 修饰变量
1.5.4 思考
2. 多态
2.1 概念
2.2 多态的实现条件
2.3 多态的实现机制
2.4 多态的好处
2.5 重写
2.5.1 定义
2.5.2 重写的条件
2.5.3 重写的特点
2.6 向上转型和向下转型
2.6.1 向上转型(自动转换,安全)
2.6.2 向下转型(强制转换,需谨慎)
2.6.3 思考
1. 继承
1.1 为什么需要继承
先看看下列代码:
1)Pen 类
public class Pen {// 共性属性:所有文具都有品牌、价格private String brand;private double price;// 钢笔专属属性:笔尖粗细private double tipThickness;// 共性构造方法:无参、带品牌+价格public Pen() {}public Pen(String brand, double price) {this.brand = brand;this.price = price;}// 共性行为:展示文具信息public void showInfo() {System.out.println("品牌:" + brand + ",价格:" + price + "元");}// 钢笔专属行为:写字public void write() {System.out.println(brand + "钢笔(笔尖粗细:" + tipThickness + "mm)正在写字");}// 共性getter/setter:品牌、价格public String getBrand() { return brand; }public void setBrand(String brand) { this.brand = brand; }public double getPrice() { return price; }public void setPrice(double price) { this.price = price; }// 钢笔专属getter/setter:笔尖粗细public double getTipThickness() { return tipThickness; }public void setTipThickness(double tipThickness) { this.tipThickness = tipThickness; }
}
2)Notebook 类
public class Notebook {// 重复的共性属性:品牌、价格private String brand;private double price;// 笔记本专属属性:页数private int pageCount;// 重复的共性构造方法public Notebook() {}public Notebook(String brand, double price) {this.brand = brand;this.price = price;}// 重复的共性行为:展示文具信息public void showInfo() {System.out.println("品牌:" + brand + ",价格:" + price + "元");}// 笔记本专属行为:记录内容public void record(String content) {System.out.println(brand + "笔记本(共" + pageCount + "页)记录:" + content);}// 重复的共性getter/setterpublic String getBrand() { return brand; }public void setBrand(String brand) { this.brand = brand; }public double getPrice() { return price; }public void setPrice(double price) { this.price = price; }// 笔记本专属getter/setter:页数public int getPageCount() { return pageCount; }public void setPageCount(int pageCount) { this.pageCount = pageCount; }
}
我们会发现,这两个类重复的内容非常的多,如果后续要添加 “尺子” “橡皮” 类,这些重复代码会反复出现,会增加开发的工作量,这时候,继承就能完美解决问题。
1.2 继承的概念
继承是面向对象编程中的重要概念,它允许一个类(子类)基于另一个类(父类)来构建。在继承中,子类会继承父类的属性和方法,同时可以添加新的属性和方法,或者重写父类的方法。比如,Pen 和 Notebook 的共性都是 Stationery。Pen 类和 Notebook 类都可以继承 Stationery 类。
1.3 如何实现继承
我们可以将所有文具的共性属性和行为提取出来,定义一个 “父类”Stationery(文具),然后让Pen(钢笔)、Notebook(笔记本)作为 “子类”,通过extends关键字继承父类的功能,同时保留自己的专属逻辑。
语法格式:
修饰符 class 子类 extends 父类{
//...
}
以上面的例子为基础进行修改:
1.3.1 父类:Stationery(提取共性)
父类封装了所有文具的通用属性和行为,作为子类的模板。
public class Stationery {// 所有文具的共性属性private String brand; // 品牌private double price; // 价格// 共性构造方法:满足子类初始化需求public Stationery() {}public Stationery(String brand, double price) {this.brand = brand;this.price = price;}// 所有文具的共性行为:展示基础信息public void showInfo() {System.out.println("文具品牌:" + brand + ",售价:" + price + "元");}// 共性属性的getter/setter(子类通过这些方法访问父类私有属性)public String getBrand() {return brand;}public void setBrand(String brand) {this.brand = brand;}public double getPrice() {return price;}public void setPrice(double price) {this.price = price;}
}
1.3.2 子类 1:Pen(继承 + 扩展)
子类通过 extends Stationery 继承父类,无需重复写品牌、价格相关代码,只需专注于钢笔的专属逻辑。
public class Pen extends Stationery {// 钢笔专属属性:笔尖粗细(父类没有的)private double tipThickness;// 子类构造方法:可调用父类构造方法初始化共性属性public Pen() {}// 通过super()调用父类的“品牌+价格”构造方法public Pen(String brand, double price, double tipThickness) {super(brand, price); // 先初始化父类的属性this.tipThickness = tipThickness; // 再初始化子类专属属性}// 钢笔专属行为:写字(父类没有的)public void write() {// 子类可通过父类的getter方法获取私有属性(如brand)System.out.println(getBrand() + "钢笔(笔尖:" + tipThickness + "mm):流畅书写中...");}// 子类专属属性的getter/setterpublic double getTipThickness() {return tipThickness;}public void setTipThickness(double tipThickness) {this.tipThickness = tipThickness;}
}
1.3.3 子类 2:Notebook(继承 + 扩展)
同样的,笔记本子类也只需关注 “页数” 和 “记录” 的专属逻辑。
public class Notebook extends Stationery {// 笔记本专属属性:页数private int pageCount;// 子类构造方法:调用父类构造public Notebook() {}public Notebook(String brand, double price, int pageCount) {super(brand, price); // 复用父类构造,初始化品牌和价格this.pageCount = pageCount;}// 笔记本专属行为:记录内容public void record(String content) {System.out.println(getBrand() + "笔记本(" + pageCount + "页):" + content);}// 专属属性的getter/setterpublic int getPageCount() {return pageCount;}public void setPageCount(int pageCount) {this.pageCount = pageCount;}
}
1.3.4 继承的效果
我们用一个测试类:StationeryTest 来验证子类如何使用父类的功能。
public class StationeryTest {public static void main(String[] args) {// 1. 创建钢笔对象:使用子类构造,初始化共性+专属属性Pen myPen = new Pen("得力", 15.9, 0.5);// 子类直接调用父类的方法(showInfo()是父类的)myPen.showInfo();// 子类调用自己的专属方法myPen.write();System.out.println("-------------------");// 2. 创建笔记本对象Notebook myNotebook = new Notebook("晨光", 8.5, 60);// 复用父类的showInfo()myNotebook.showInfo();// 调用子类专属方法myNotebook.record("今天学习了Java继承的核心逻辑!");System.out.println("-------------------");// 3. 动态修改属性(父类的setter方法)myPen.setPrice(12.9); // 修改父类的price属性myPen.showInfo(); // 再次调用父类方法,展示修改后的值}
}
运行结果:
1.4 super 关键字
当子类和父类有同名的成员(变量或方法),或者需要调用父类构造方法时,用来明确指定访问父类的成员或构造方法。程序根据 super(参数) 中传递的参数列表来确定调用父类的哪一个有参构造方法,这遵循构造方法重载的匹配规则。
方法重载的规则
1、方法名必须相同:这是方法重载的基本前提,比如在一个类中可以定义多个名为 print 的方法。
2、参数列表必须不同:
1)参数的个数不同,比如一个 print 方法接收一个整数参数 print(int num) ,另一个 print 方法接收两个整数参数 print(int num1, int num2);
2)参数类型不同,比如一个 print 方法接收整数参数 print(int num) ,另一个接收字符串参数 print(String str) ;
3)参数顺序不同,比如 print(int num, String str) 和 print(String str, int num) 。
注意:方法重载和返回类型无关。也就是说,不能仅通过返回类型的不同来区分重载方法。
1.4.1 访问父类的成员变量
当子类和父类有同名的成员变量时,子类中的变量会隐藏父类的变量。如果想在子类中使用父类的那个变量,就用 super.变量名。
示例:
class Father {int num = 10; // 父类的成员变量
}class Son extends Father {int num = 20; // 子类和父类同名的成员变量void show() {System.out.println("子类自己的num:" + num); // 输出子类的num,20System.out.println("父类的num:" + super.num); // 用super访问父类的num,10}
}public class Test {public static void main(String[] args) {Son son = new Son();son.show();}
}
运行结果:
1.4.2 调用父类的构造方法
子类构造方法中,默认会先调用父类的无参构造方法(如果父类没有无参构造,子类必须显式调用父类的有参构造)。如果想指定调用父类的某个有参构造,就用 super(参数),而且 super(参数) 必须放在子类构造方法的第一行。
示例:
class Father {int num;Father(int num) {this.num = num;}
}class Son extends Father {int num;Son(int fatherNum, int sonNum) {super(fatherNum); // 调用父类的有参构造,给父类的num赋值this.num = sonNum; // 给子类的num赋值}void show() {System.out.println("父类的num:" + super.num);System.out.println("子类的num:" + num);}
}public class Test {public static void main(String[] args) {Son son = new Son(10, 20);son.show();}
}
运行结果:
1.4.3 调用父类的成员方法
当子类重写了父类的方法(方法名、参数列表、返回值都相同),子类中默认调用的是自己重写后的方法。如果想调用父类原来的方法,就用 super.方法名(参数)。
示例:
class Father {void eat() {System.out.println("爸爸吃饭");}
}class Son extends Father {@Overridevoid eat() {System.out.println("儿子吃饭");}void show() {eat(); // 调用自己重写的eat方法,输出“儿子吃饭”super.eat(); // 调用父类的eat方法,输出“爸爸吃饭”}
}public class Test {public static void main(String[] args) {Son son = new Son();son.show();}
}
运行结果:
1.5 final 关键字
在 Java 中,final 是一个重要的关键字,它可以用于修饰类、方法和变量,具有 "不可改变" 的含义。理解final的用法对于编写健壮、安全的代码非常重要。
1.5.1 final 修饰类
当一个类被final修饰时,表示这个类不能被继承,即它不能有子类。
1.5.2 final 修饰方法
当一个方法被final修饰时,表示这个方法不能被子类重写。
1.5.3 final 修饰变量
当一个变量被final修饰时,表示这个变量一旦被赋值就不能再被修改,即它成为了一个常量。
1.5.4 思考
或许有人会思考这样一个问题:final 关键字修饰的方法已经不能被子类重写了,final 关键字和 static 关键字一起使用表示静态的最终方法,也不可被重写。那这两个有什么区别呢?
简单来说,final 普通方法:是对象的方法,能被继承但不能被重写,通过对象调用;final static 静态方法:是类的方法,不会被继承也不能被重写,通过类名调用,与对象无关。下面用一个表格来清晰展示其区别:
特性 | final 修饰的普通方法 | final static 修饰的静态方法 |
---|---|---|
归属 | 实例方法(属于对象) | 静态方法(属于类) |
调用方式 | 通过对象调用(对象.方法() ) | 通过类名调用(类名.方法() ) |
是否被继承 | 能被子类继承(子类对象可直接调用) | 不会被继承(子类需用父类名调用) |
final 的作用 | 禁止子类重写该实例方法 | 禁止子类 “重写” 该静态方法(本身也无法重写) |
与多态的关系 | 可参与多态(但因 final 无法重写,多态表现固定) | 不参与多态(静态方法属于类,不依赖对象类型) |
值得一提的是,在 Java 中,父类的静态方法不能被子类继承,也就是子类调用父类的普通方法时,传入的参数是子类的属性,但是如果调用父类的静态方法,这个方法并不属于子类,所以使用的属性依旧是父类的。
当然,子类可以通过以下两种方式访问父类的静态方法:
1)直接通过 父类名.静态方法名() 调用;
2)通过 子类名.静态方法名() 调用。
调用父类的普通方法(非静态)时:因为方法被子类继承,调用时依赖于子类对象,所以方法中访问的属性会遵循 “就近原则”—— 如果子类有和父类同名的属性,会优先使用子类的属性;如果没有,则使用父类的属性。
示例:
class Parent {String name = "父类属性";// 父类的方法void printName() {// 这里的name默认是Parent类的属性(当前方法所在类的属性)System.out.println("父类方法中访问的name:" + name);}
}class Child extends Parent {String name = "子类属性";// 子类重写父类方法@Overridevoid printName() {// 这里的name默认是Child类的属性(当前方法所在类的属性)System.out.println("子类方法中访问的name:" + name);// 用super访问父类的属性System.out.println("子类方法中访问父类的name:" + super.name);}
}public class Test {public static void main(String[] args) {Parent parent = new Parent();parent.printName(); // 父类方法访问父类属性Child child = new Child();child.printName(); // 子类方法优先访问子类属性(就近原则)// 向上转型:父类引用指向子类对象Parent poly = new Child();poly.printName(); // 调用子类重写的方法,仍优先访问子类属性}
}
运行结果:
调用父类的静态方法时:静态方法属于父类,不依赖任何对象,因此方法中访问的属性只能是父类的静态属性(静态方法只能访问静态成员),与子类的属性无关(即使子类有同名静态属性,也不会被使用)。
示例:
class Parent {static String name = "父类静态属性";static void printName() {System.out.println(name); // 只能访问父类的静态属性}
}class Child extends Parent {static String name = "子类静态属性"; // 子类同名静态属性
}public class Test {public static void main(String[] args) {Child.printName(); // 调用父类静态方法,输出"父类静态属性"// 原因:printName()属于Parent类,内部的name只能是Parent的静态属性}
}
2. 多态
2.1 概念
多态是面向对象编程的三大核心特性之一(封装、继承、多态),它允许同一操作作用于不同的对象上时,可以产生不同的执行结果。简单来说,就是 "同一接口,不同实现"。本质是:父类引用指向子类对象,并通过这个引用调用方法时,实际执行的是子类重写后的方法。
2.2 多态的实现条件
要实现多态,必须满足一下三个条件:
1)存在继承关系。
2)子类重写了父类的方法。
3)父类引用指向子类对象 。
示例:
父类:Shape
// 父类:形状
public class Shape {// 父类方法:绘制public void draw() {System.out.println("绘制形状");}// 父类方法:计算面积public double getArea() {return 0;}
}
子类:Rectangle
// 子类:矩形(继承自Shape)
public class Rectangle extends Shape {private double width; // 宽度private double height; // 高度public Rectangle(double width, double height) {this.width = width;this.height = height;}// 重写父类的draw方法@Overridepublic void draw() {System.out.println("绘制矩形");}// 重写父类的getArea方法@Overridepublic double getArea() {return width * height; // 矩形的面积公式}
}
子类:Circle
// 子类:圆形(继承自Shape)
public class Circle extends Shape {private double radius; // 半径public Circle(double radius) {this.radius = radius;}// 重写父类的draw方法@Overridepublic void draw() {System.out.println("绘制圆形");}// 重写父类的getArea方法@Overridepublic double getArea() {return Math.PI * radius * radius; // 圆的面积公式}
}
测试类:PolymorphismDemo
// 测试类:演示多态
public class PolymorphismDemo {public static void main(String[] args) {// 父类引用指向子类对象(多态的体现)Shape shape1 = new Circle(5.0); // 圆形Shape shape2 = new Rectangle(4.0, 6.0); // 矩形// 调用draw方法,实际执行的是子类重写后的方法shape1.draw(); // 输出:绘制圆形shape2.draw(); // 输出:绘制矩形// 调用getArea方法,实际执行的是子类重写后的方法System.out.println("圆形面积:" + shape1.getArea()); // 输出:圆形面积:78.5398...System.out.println("矩形面积:" + shape2.getArea()); // 输出:矩形面积:24.0// 可以将多态对象放入数组,方便统一处理Shape[] shapes = {new Circle(3.0), new Rectangle(2.0, 3.0)};for (Shape shape : shapes) {shape.draw();System.out.println("面积:" + shape.getArea());}}
}
运行结果:
2.3 多态的实现机制
Java 中多态是通过动态绑定实现的:在程序运行时,JVM 会根据对象的实际类型来决定调用哪个类的方法,而不是根据引用变量的类型。
这就是运行 Shape shape1 = new Circle(5.0); 这句代码时,调用 shape1.draw() 时会执行 Circle类中的 draw() 方法的原因了。
2.4 多态的好处
1)提高代码的可扩展性:当需要添加新的子类(如三角形)时,不需要修改使用这些类的代码。
2)提高代码的复用性:可以用统一的方式处理不同的对象,如示例中将所有形状放入数组统一处理。
3)降低代码的耦合度:使用方只需要知道父类接口,不需要了解具体的子类实现。
4)符合 "开闭原则":对扩展开放(可以新增子类),对修改关闭(不需要修改原有代码)。
2.5 重写
2.5.1 定义
重写(Override)是面向对象编程中的一个重要概念,它允许子类重新定义(覆盖)父类中的方法。当子类重写父类的方法时,子类中的方法会覆盖父类中同名的方法,从而实现多态性。
2.5.2 重写的条件
要实现方法重写,必须满足以下条件:
1)必须存在继承关系。
2)方法名必须完全相同。
3)参数列表必须完全相同(包括参数类型、顺序、数量)。
4)返回值类型必须相同。
5)访问权限不能严于父类(例如父类是 public,子类不能是 private)。
6)子类方法不能抛出比父类方法更多或更广泛的异常。
2.5.3 重写的特点
1)@Override 注解:虽然不是必须的,但建议使用,它可以帮助编译器检查是否正确重写了父类方法
2)不能重写的方法:
- final 修饰的方法(最终方法)
- static 修饰的方法(静态方法)
- private 修饰的方法(私有方法,子类无法访问)
3)重写与多态的关系:重写是实现多态的基础,正是因为有了方法重写,才能实现父类引用指向子类对象时调用子类方法的效果。
2.6 向上转型和向下转型
2.6.1 向上转型(自动转换,安全)
本质:子类的实例对象赋值给父类的引用,不需要强制转换。
特点:父类引用只能调用父类中定义的方法(包括子类重写的方法),不能调用子类独有的方法(因为编译器认为 “父类引用指向的就是父类对象”,不知道子类特有方法的存在)。
示例:
class Animal {void eat() { System.out.println("动物吃饭"); }
}class Dog extends Animal {void eat() { System.out.println("狗吃骨头"); } // 重写父类方法void bark() { System.out.println("狗叫"); } // 子类独有方法
}public class Test {public static void main(String[] args) {// 向上转型:Dog对象 → 赋值给Animal类型的变量Animal animal = new Dog(); animal.eat(); // 可以调用,执行子类重写的方法(多态体现)// animal.bark(); // 报错!父类引用不能调用子类独有的方法}
}
2.6.2 向下转型(强制转换,需谨慎)
本质:已经向上转型的父类引用强制转换为子类类型以后,再赋值给子类引用。
(!!注意:不能直接把父类对象转成子类,必须是父类引用指向的原本就是子类对象才能转,否则会报错)
特点:转换后,子类引用可以调用子类独有的方法(因为此时明确了是子类对象)。
示例:
public class Test {public static void main(String[] args) {// 先向上转型:Dog对象 → Animal引用Animal animal = new Dog(); // 向下转型:Animal引用(实际是Dog对象)→ 强制转为Dog类型Dog dog = (Dog) animal; dog.eat(); // 可以调用重写的方法dog.bark(); // 可以调用子类独有的方法(这就是向下转型的意义)}
}
为了安全,可以先用 instanceof 判断父类引用指向的对象是否为目标子类类型:
if (animal instanceof Dog) {Dog dog = (Dog) animal; // 先判断再转换,避免报错
}
2.6.3 思考
或许你会好奇这样一个问题:那向下转型的作用是什么呢,为什么不一开始就直接创建一个子类的实例对象?
向下转型的核心作用是在多态场景下,重新获取子类特有的功能。如果代码中一开始就明确知道要使用子类对象,确实可以直接创建子类实例;但实际开发中,很多场景需要先通过父类引用统一管理所有子类对象,后续再根据需求还原子类身份,调用子类独有的方法。
这里用两个常见用法来说明:
1、父类作为方法参数,统一接收所有子类对象(多态核心用法)
假设要写一个 “喂养动物” 的方法,动物有 Dog、Cat、Bird 等子类,每个子类除了通用的 “吃饭” 方法,还有自己的特有方法(比如 Dog 会 bark(),Cat 会 catchMouse())。
如果直接用子类作为参数,需要写多个方法(feedDog(Dog dog)、feedCat(Cat cat)...),代码会非常冗余;而用父类 Animal 作为参数,可以用一个方法统一接收所有子类对象。但如果后续需要调用子类特有的方法,就必须向下转型。
class Animal {void eat() { System.out.println("动物吃饭"); }
}class Dog extends Animal {@Overridevoid eat() { System.out.println("狗吃骨头"); }void bark() { System.out.println("狗叫:汪汪!"); } // 子类特有
}class Cat extends Animal {@Overridevoid eat() { System.out.println("猫吃鱼"); }void catchMouse() { System.out.println("猫抓老鼠"); } // 子类特有
}// 工具类:用父类作为参数,统一喂养所有动物
class AnimalFeeder {// 统一方法:接收父类引用,利用多态调用不同子类的eat()static void feed(Animal animal) {animal.eat(); // 多态:自动执行子类的eat()// 需求:喂养后,如果是狗就让它叫,是猫就让它抓老鼠if (animal instanceof Dog) {Dog dog = (Dog) animal; // 向下转型:还原Dog身份dog.bark(); // 调用子类特有方法} else if (animal instanceof Cat) {Cat cat = (Cat) animal; // 向下转型:还原Cat身份cat.catchMouse(); // 调用子类特有方法}}
}public class Test {public static void main(String[] args) {Dog dog = new Dog();Cat cat = new Cat();// 用同一个方法喂养不同动物(多态)AnimalFeeder.feed(dog); AnimalFeeder.feed(cat); }
}
运行结果:
2、父类集合存储所有子类对象,后续按需获取子类特有功能
实际开发中,经常用父类类型的集合(比如 List<Animal>)存储所有子类对象,这样可以统一管理(比如遍历所有动物吃饭)。但如果需要从集合中取出某个对象,调用它的子类特有方法,就必须向下转型。
import java.util.ArrayList;
import java.util.List;// 父类:动物
class Animal {// 父类的通用方法void eat() {System.out.println("动物吃饭");}
}// 子类:狗
class Dog extends Animal {// 重写父类方法@Overridevoid eat() {System.out.println("狗吃骨头");}// 子类特有方法void bark() {System.out.println("狗叫:汪汪!");}
}// 子类:猫
class Cat extends Animal {// 重写父类方法@Overridevoid eat() {System.out.println("猫吃鱼");}// 子类特有方法void catchMouse() {System.out.println("猫抓老鼠");}
}// 测试类
public class Test {public static void main(String[] args) {// 父类集合:存储所有Animal子类对象List<Animal> animalList = new ArrayList<>();animalList.add(new Dog());animalList.add(new Cat());animalList.add(new Dog());// 1. 统一遍历:所有动物吃饭(多态)System.out.println("===== 所有动物吃饭 =====");for (Animal animal : animalList) {animal.eat(); // 自动调用对应子类的实现}// 2. 按需处理:找出所有狗,让它们叫(需要向下转型)System.out.println("\n===== 所有狗叫 =====");for (Animal animal : animalList) {if (animal instanceof Dog) { // 先判断类型,确保安全Dog dog = (Dog) animal; // 向下转型dog.bark(); // 调用子类特有方法}}}
}
运行结果:
总结:向下转型的核心价值
向下转型是配合多态使用的 “补充手段”:
先通过 “父类引用 / 父类集合” 实现 “统一管理所有子类对象”(这是多态的核心优势,让代码更灵活、可扩展);
后续需要调用子类特有方法时,再通过向下转型 “还原子类身份”,获取子类特有的功能。
以上就是我今天想要分享的内容,希望大家看到后对继承与多态能有更深刻的理解~