Java多态从入门到通关:考点精讲+面试考点+项目实战
目录
一.引言
(一).什么是多态?
(二).多态在面向对象三大特征中的重要性
(三).为什么多态重要?解决了什么问题?
二.多态的两种类型
(一).编译时多态
(二).运行时多态
(三).二者区别对比
三.运行时多态的实现条件
四.核心机制讲解
(一).动态绑定
(二).JVM在运行时决定调用哪个方法
(三).instanceof关键字的使用
(四).向上转型vs向下转型,ClassCastException风险
五.接口与抽象类中的实现
(一).通过接口实现多态
(二).抽象类VS接口在多态场景下的选择
(三).代码对比
六.多态的优缺点
七.常见误区与陷阱
八.总结
九.面试常见考点
十.实战演练
一.引言
(一).什么是多态?
简单来说,同一个方法,不同的对象调用所产生的结果也不一样,就叫多态。
我们来看这样一段代码:
class Animal{ String name; int age; public Animal(String name,int age){ this.name = name; this.age = age; } public void move(){ System.out.println(this.name + "正在跑"); } } //Fish类继承Animal类,并且重写move()方法 class Fish extends Animal{ public Fish(String name,int age){ super(name,age); } public void move(){ System.out.println(this.name + "正在游泳"); } } //Dog类也继承Animal类,并且重写move()方法 class Dog extends Animal { public Dog(String name,int age){ super(name,age); } public void move(){ System.out.println(this.name + "正在跑步"); } } public class Test{ public static void main(String[] args){ Animal fish = new Fish("彩鳞",1);//这里发生了向上转型 Animal dog = new Dog("小白",4); fish.move(); dog.move(); } }运行结果如下:
同样调用move()方法,但是对象不同,一个是fish,一个是dog,结果也不同,这个就 叫多态。
我们注意到,上面的代码发生了这几件事情:
1.Dog类和Fish类都继承了Animal类
2.Dog类和Fish类重写了Animal类的mova()方法
3.发生了向上转型。(下面会讲解,这里先知道有这个就可以)这就是触发多态的条件:
1.有继承关系
2.子类重写父类的方法
3.发生了向上绑定
(二).多态在面向对象三大特征中的重要性
面向对象三大特征的关系为:
封装(基础) → 继承(前提) → 多态(核心体现),多态建立在封装和继承之上,是最能 体现面向对象的设计价值的特征。
如果没有多态,我们就只能这样设计:
class Animal{ String name; int age; public Animal(String name,int age){ this.name = name; this.age = age; } } //Fish类继承Animal类 class Fish extends Animal{ public Fish(String name,int age){ super(name,age); } } //Dog类也继承Animal类 class Dog extends Animal { public Dog(String name,int age){ super(name,age); } } public class Test{ public static void main(String[] args){ Fish fish = new Fish("彩鳞",1);/ Dog dog = new Dog("小白",4); if (animal instanceof Fish) { System.out.println(animal.name + "正在游泳"); } else if (animal instanceof Dog) { System.out.println(animal.name + "正在跑步"); } } }每次新增一个类,都要判断新的对象隶属于哪个类,大大降低了代码的灵活度。
总结来说,封装实现了对内部实现的隐藏,保护数据安全。继承实现了代码的复 用,建立类之间的层级关系,多态让程序灵活应对变化,对外保持统一接口。
(三).为什么多态重要?解决了什么问题?
多态的核心价值:
1.降低耦合,对外统一接口。
无论什么动物,只需要animal.move();
2.对扩展开放,对修改关闭。
比如代码新增一个bird类,我们只需要在bird类中也重写move方法,完全不用 改动其他的move方法
3.是所有设计模式的基础
几乎所有设计模式的核心都依赖多态来实现灵活替换。
补充对比:多态的不足之处:
1.多态的代码效率比较低
2.成员变量和构造方法不能使用多态
二.多态的两种类型
(一).编译时多态
编译时多态的代表是方法的重载。编译器在编译是就能确定调用哪个方法。
重载是在一个类中,方法名相同,参数不同(参数个数不同或参数的顺序不同或参数的类型不同),通过传入不同的参数来调用对应方法。
这里声明一下,学术界对于重载算不算多态存在争议。因为多态是不同对象调用同一个方法所产生的不同的行为。而重载是同一个对象的不同方法,所以我们认为重载是“最弱 的”多态
我们可以理解为,重载是同一个接口(方法名),不同的表现形式。
(二).运行时多态
重写是运行时多态的经典代表。JVM 在运行阶段才能确定调用哪个方法。
Animal fish = new Fish("彩鳞", 1); Animal dog = new Dog("小白", 4); fish.move(); // 运行时才知道:fish 实际是 Fish,调用 Fish 的 move() dog.move(); // 运行时才知道:dog 实际是 Dog, 调用 Dog 的 move()编译器看到的是
Animal类型,它不知道具体是哪个子类,只有程序真正跑起来之后,JVM 才查看对象的真实类型,再决定调用谁的方法。
(三).二者区别对比
编译时多态和运行时多态对比 编译时多态 运行时多态 实现方式 方法重载 方法重写 决定时机
编译阶段 运行阶段 判断依据 根据参数列表 对象的实际类型 是否需要继承 不需要 需要 灵活性 差 好 典型场景 同一个类中的多种操作 向上转型
三.核心机制讲解
(一).动态绑定
1.动态绑定的定义:
在继承的大条件下,当运行代码的时候,调用了父类和子类中重写的那个方法,结果实际调用了子类的方法,我们把这个情况叫做叫做动态绑定。
2.动态绑定的触发条件:
(1).父类的引用 指向了 子类的对象
(2).父类和子类存在重写的方法
3.动态绑定注意事项:
如果父类的构造方法中,调用了父类和子类发生重写的方法,那么会发生动态绑定
(二).instanceof关键字的使用
我们可以通过 inStanceOf 关键字来判断对象是否属于类,是则返回true,否则返回flase;
//定义Animal类,设置name和age属性,给构造方法和move()方法 class Animal { String name; int age; public Animal(String name, int age) { this.name = name; this.age = age; } public void move() { System.out.println(this.name + " 正在移动"); } } //Fish类继承Animal类,在构造方法中调用父类的构造方法,重写move()方法,给呼吸的方法 class Fish extends Animal { public Fish(String name, int age) { super(name, age); } @Override public void move() { System.out.println(this.name + " 正在游泳"); } public void breatheInWater() { System.out.println(this.name + " 在水中呼吸"); } } //Dog类继承Animal类,在构造方法中调用父类的构造方法,重写move()方法,给叫的方法 class Dog extends Animal { public Dog(String name, int age) { super(name, age); } @Override public void move() { System.out.println(this.name + " 正在跑步"); } public void bark() { System.out.println(this.name + " 汪汪叫"); } } //定义Fish和Dog类的对象,判断对象是否属于某个类 public class Test { public static void main(String[] args) { Animal fish = new Fish("彩鳞", 1); Animal dog = new Dog("小白", 4); // 判断对象是否属于某个类 System.out.println("=== 基本判断 ==="); System.out.println(fish instanceof Fish); // true System.out.println(fish instanceof Animal); // true,Fish 继承了 Animal System.out.println(fish instanceof Dog); // false // 实际用途:向下转型前的安全检查 // 父类引用无法直接调用子类独有的方法 // fish.breatheInWater(); 编译报错,Animal 没有这个方法 System.out.println("\n=== 安全向下转型 ==="); if (fish instanceof Fish) { Fish f = (Fish) fish; // 确认是 Fish 之后再强转,安全 f.breatheInWater(); // 现在可以调用 Fish 独有的方法 } if (dog instanceof Dog) { Dog d = (Dog) dog; d.bark(); // 调用 Dog 独有的方法 } // 不用 instanceof 直接强转会怎样? System.out.println("\n=== 不检查直接强转的后果 ==="); try { Dog d = (Dog) fish; // fish 实际是 Fish,强转成 Dog d.bark(); } catch (ClassCastException e) { System.out.println("报错了:" + e.getMessage()); // 运行时抛出异常 } } }运行结果如下:
(三).向上转型vs向下转型,ClassCastException风险
向上转型:
1.定义:
向上引用指得是子类的对象 赋值给 父类的引用,例如:
Animal animal = new Dog();2.特点:
(1).安全,无风险
(2).引用是父类,对象是子类
(3).可以使用父类的属性和方法(如果子类重写了,优先执行子类的重写方法 ),但是不能使用子类独有的属性。
3.三种实现方法:
(1).直接赋值:
Animal animal = new Dog();(2).方法传参:
public void test(Animal animal){ }(3).返回值:
public Animal test(){ Dog dog = new Dog(); return dog; } publlic static void main(){ Animal animal = test(); }向下转型:
1.定义:
向下转型是重新定义一个引用,创建一个新的子类与父类引用指向同一个对象。通过强制类型转换把父类的引用指向强制类型转换后的引用,代码如下:
Animal animal = new Dog();//向上引用 Dog dog = (Dog)animal;//向下引用 dog.worf;//强转后的引用可以调用子类本身的方法,也可以调用父类继承下来的方法2.注意事项:
(1).不是所有的向下转换都能成功(强制类型转换存在风险)
(2).向下转换可以让引用使用子类对象的成员方法
四.接口与抽象类中的实现
(一).通过接口实现多态
// 定义接口 interface Animal { void move(); // 接口中的方法默认是 public abstract void eat(); } // Fish 实现 Animal 接口 class Fish implements Animal { String name; public Fish(String name) { this.name = name; } @Override public void move() { System.out.println(this.name + " 正在游泳"); } @Override public void eat() { System.out.println(this.name + " 在吃水草"); } } // Dog 实现 Animal 接口 class Dog implements Animal { String name; public Dog(String name) { this.name = name; } @Override public void move() { System.out.println(this.name + " 正在跑步"); } @Override public void eat() { System.out.println(this.name + " 在啃骨头"); } } // Bird 实现 Animal 接口 class Bird implements Animal { String name; public Bird(String name) { this.name = name; } @Override public void move() { System.out.println(this.name + " 正在飞翔"); } @Override public void eat() { System.out.println(this.name + " 在吃虫子"); } } public class Test { // 参数类型是接口,任何实现了 Animal 接口的对象都可以传进来 public static void doAction(Animal animal) { animal.move(); animal.eat(); } public static void main(String[] args) { // 接口引用指向实现类对象,和父类引用指向子类对象是同样的道理 Animal fish = new Fish("彩鳞"); Animal dog = new Dog("小白"); Animal bird = new Bird("小黄"); System.out.println("=== 直接调用 ==="); fish.move(); dog.move(); bird.move(); System.out.println("\n=== 通过方法统一调用 ==="); doAction(fish); System.out.println("---"); doAction(dog); System.out.println("---"); doAction(bird); } }(二).抽象类VS接口
1.抽象类的概念和实现:
抽象类的概念:抽象类是被abstract修饰的类,抽象类不能被实例化,只能作为其他类的父类而使用。
2.抽象方法:抽象方法是被abstract修饰的方法,抽象方法可以没有具体的实现
(1).抽象类和抽象方法的实现:
public abstract class Animal{ public String name; public int age; public abstract eat(){ System.out.println(this.name + "正在吃……"); } }(2).抽象方法和抽象类的关系:
一个抽象类中可以没有抽象方法,但是抽象方法必须在抽象类中。抽象类中可以包含其他普通的成员方法和成员变量
3.抽象类和抽象方法的注意事项:
(1).我们不能实例化抽象类,因为抽象类是不完整的
(2).抽象类的抽象方法在继承后必须重写,否则违反了非抽象类中不能有抽象方法的准则
(3).抽象类就是为了被继承而生
(4).抽象方法必须满足重写所需要的五个条件:不能被final,static,private修饰,可以构成赋值关系,权限限定符
// 定义抽象类 abstract class Shape { String color; public Shape(String color) { this.color = color; } // 抽象方法:没有方法体,子类必须重写 public abstract double getArea(); public abstract double getPerimeter(); // 普通方法:子类直接继承,不需要重写 public void printInfo() { System.out.println("图形:" + this.getClass().getSimpleName() + ",颜色:" + this.color + ",面积:" + this.getArea() + ",周长:" + this.getPerimeter()); } } // 圆形 class Circle extends Shape { double radius; public Circle(String color, double radius) { super(color); this.radius = radius; } @Override public double getArea() { return Math.PI * radius * radius; } @Override public double getPerimeter() { return 2 * Math.PI * radius; } } // 矩形 class Rectangle extends Shape { double width; double height; public Rectangle(String color, double width, double height) { super(color); this.width = width; this.height = height; } @Override public double getArea() { return width * height; } @Override public double getPerimeter() { return 2 * (width + height); } } // 三角形 class Triangle extends Shape { double a, b, c; // 三条边 public Triangle(String color, double a, double b, double c) { super(color); this.a = a; this.b = b; this.c = c; } @Override public double getArea() { // 海伦公式 double s = (a + b + c) / 2; return Math.sqrt(s * (s - a) * (s - b) * (s - c)); } @Override public double getPerimeter() { return a + b + c; } } public class Test { public static void main(String[] args) { // 抽象类不能实例化 // Shape shape = new Shape("红色"); // 编译直接报错 // 抽象类引用指向子类对象 Shape circle = new Circle("红色", 5); Shape rectangle = new Rectangle("蓝色", 4, 6); Shape triangle = new Triangle("绿色", 3, 4, 5); System.out.println("=== 各自调用抽象方法 ==="); // 同一个方法名,不同子类,结果不同 —— 多态 System.out.printf("圆形面积:%.2f%n", circle.getArea()); System.out.printf("矩形面积:%.2f%n", rectangle.getArea()); System.out.printf("三角形面积:%.2f%n", triangle.getArea()); System.out.println("\n=== 调用继承来的普通方法 ==="); // printInfo() 是抽象类里的普通方法,三个子类都没有写这个方法 // 但是都能用,因为继承了抽象类 circle.printInfo(); rectangle.printInfo(); triangle.printInfo(); } }
八.面试常见考点
1. 什么是多态?多态有哪几种类型?
2. 多态的实现条件是什么?
3. 重载和重写(的区别?
4. 什么是动态绑定?与静态绑定有什么区别?
5.
instanceof关键字的作用与多态中的使用场景6.static修饰的方法能重写嘛?为什么?
7.. 多态的好处与弊端?
8.抽象类和接口在多态中的角色?
九.实战演练
灵活的支付系统
背景
你正在开发一个电商系统的支付模块。系统需要支持多种支付方式:支付宝支付、微信支付、银行卡支付。未来还可能增加新的支付方式。每种支付方式的处理流程略有不同,但都包含两个基本动作:扣款和退款。
需求
定义一个支付接口
Payment,包含两个方法:
boolean pay(double amount):支付指定金额,返回是否成功。
void refund(double amount):退款指定金额(直接输出退款信息即可)。实现三种具体的支付类,分别命名为
AliPay、WeChatPay、CardPay,都实现Payment接口。
每个类的构造方法可以接收必要的身份标识。
pay方法中:输出“通过 [支付方式] 支付了 xx 元”,并返回true(。
refund方法中:输出“通过 [支付方式] 退款了 xx 元”。多态场景:
创建一个
PaymentProcessor类,其中包含一个void processPayment(Payment payment, double amount)方法,接收Payment接口引用,完成支付操作(调用pay)。额外提供一个
void cancelOrder(Payment payment, double amount)方法,接收Payment引用,完成退款操作(调用refund)。特殊需求:
对于
CardPay类,额外增加一个方法void deductPoints(int points),表示使用银行卡积分抵扣。在
processPayment方法中,如果传入的是CardPay类型,除了支付外,自动调用deductPoints(100)(模拟本次支付赠送积分,或使用积分)。要求使用
instanceof检查并向下转型。测试场景:编写主类
PaymentTest
创建支付宝、微信、银行卡三种支付对象。
使用
PaymentProcessor分别处理它们的支付(多态方式调用)。单独测试退款方法(多态方式)。
特意传入银行卡支付对象,观察积分抵扣逻辑是否正确执行。
扩展要求(可选):
新增一种支付方式
CryptoPayment(加密货币支付)。pay方法中输出“通过加密货币支付了 xx 元”,refund输出对应退款。在不修改
PaymentProcessor和主类核心逻辑的前提下,验证新的支付方式能否被无缝集成。框架如下:
// Payment.java public interface Payment { boolean pay(double amount); void refund(double amount); } // AliPay.java public class AliPay implements Payment { private String account; public AliPay(String account) { this.account = account; } // TODO 实现pay和refund方法 } // WeChatPay.java public class WeChatPay implements Payment { private String openId; public WeChatPay(String openId) { this.openId = openId; } // TODO 实现pay和refund方法 } // CardPay.java public class CardPay implements Payment { private String cardNumber; public CardPay(String cardNumber) { this.cardNumber = cardNumber; } // TODO 实现pay和refund方法 // 额外方法 public void deductPoints(int points) { System.out.println("银行卡积分抵扣 " + points + " 积分"); } } // PaymentProcessor.java public class PaymentProcessor { public void processPayment(Payment payment, double amount) { // TODO: 调用pay方法,并利用instanceof处理CardPay特有的积分抵扣 } public void cancelOrder(Payment payment, double amount) { // TODO: 调用refund方法 } } // PaymentTest.java public class PaymentTest { public static void main(String[] args) { PaymentProcessor processor = new PaymentProcessor(); Payment alipay = new AliPay("alice@example.com"); Payment wechat = new WeChatPay("wx_123456"); Payment card = new CardPay("6222****1234"); System.out.println("=== 支付场景 ==="); processor.processPayment(alipay, 100.0); processor.processPayment(wechat, 50.0); processor.processPayment(card, 200.0); System.out.println("=== 退款场景 ==="); processor.cancelOrder(alipay, 20.0); processor.cancelOrder(card, 50.0); Payment crypto = new CryptoPayment("0x..."); processor.processPayment(crypto, 300.0); } }
十.小结
这个实战题目涵盖了多态的核心应用场景:接口统一、动态绑定、类型判断与扩展性。动手敲一遍代码,你会更清楚地理解“父类引用指向子类对象”到底带来了怎样的灵活性。现在就去运行它,然后试着增加一种新的支付方式吧——你会看到,多态让代码几乎不需要改动就能自然扩展。
