这页内容要背的有点多
四、多态
4.1 多态(重中之重)
概念
- 多态主要指的是同一事物表现出的多种形态
- 饮料:可乐、雪碧、红牛、脉动、…
- 人:学生、教师、工人、…
语法格式
父类类型 引用变量 = new 子类类型();
- 如:
Shape sr = new Rect();
sr.show(); - 总之一句话:父类类型的引用指向子类类型的对象,形成了多态
特点
- 当父类类型的引用指向子类类型的对象时,父类类型的引用可以直接调用父类独有方法
- 当父类类型的引用指向子类类型的对象时,父类类型的引用不可以直接调用子类独有方法
- 不能直接,但是可以间接(即强转)
- 对于父子类都有的非静态方法来说,编译阶段调用父类版本,运行阶段调用子类重写的版本(动态绑定)
- 对于父子类都有的静态方法来说,编译和运行都调用父类版本
4.2 案例:Shape类和Rect类的实现
- 要点:
- 在多态中,当子类中重写某个非静态方法后,调用该方法时:在编译阶段调用父类的方法,在运行阶段调用的是子类的方法
(记忆方法:披着羊皮的狼,远看(编译)是羊(父类),近看(执行)是狼(子类))
- 在多态中,当子类中重写某个非静态方法后,调用该方法时:在编译阶段调用父类的方法,在运行阶段调用的是子类的方法
/*
* 父类方法Shape
*/
public class Shape {
private int x; // 横坐标
private int y; // 纵坐标
public Shape() {
}
public Shape(int x, int y) {
setX(x);
setY(y);
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public void show() {
System.out.println("横坐标是:" + getX() + ", 纵坐标是:" + getY());
}
}
public class Rect extends Shape {
// 子类Rect独有的成员变量
private int length; // 长度
private int width; // 宽度
public Rect() {
// super(); // 编译器会默认加入此语句,因此可以省略不写
}
public Rect(int x, int y, int length, int width) {
super(x, y); // 继承父类的成员变量
setLength(length);
setWidth(width);
}
// 长度
public int getLength() {
return length;
}
public void setLength(int length) {
if(length > 0) {
this.length = length;
} else {
System.out.println("长度不合理哦!");
}
}
// 宽度
public int getWidth() {
return width;
}
public void setWidth(int width) {
if(width > 0) {
this.width = width;
} else {
System.out.println("宽度不合理哦!");
}
}
// 重写show方法
@Override
public void show() {
super.show(); // 默认调用子类方法时,不用父类方法,所以父类方法只在编译时调用
// 运行阶段如果也要执行父类方法,那就手动加入super.show()
System.out.println("长度是:" + getLength() + ", 宽度是:" + getWidth());
}
}
public class ShapeRectTest {
public static void main(String[] args) {
// 1.声明Shape类型的引用指向Shape类型的对象并打印特征
Shape s = new Shape(10,20);
s.show();
System.out.println("-----------------------------");
// 2.声明Rect类型的引用指向Rect类型的对象并打印特征
Rect r = new Rect(1,2,1000,2000);
r.show();
System.out.println("-----------------------------");
// 3.声明Shape类型的引用指向Rect类型的对象并打印特征
Shape sr = new Rect(1111, 2222, 3333, 4444); // 形成了多态
// 当Rect类中重写show方法后,下面的代码在编译阶段调用Shape类的方法,在运行阶段调用的是Rect类的方法
sr.show();
}
}
4.3 引用数据类型
引用数据类型之间的转换方法(多态)
- 当父类类型的引用指向子类类型的对象时,父类类型的引用不可以直接调用子类独有方法,但是可以间接调用,采用的是引用数据类型的转换
- 引用数据类型的转换方式分为两种:
- 自动类型转换:小类型 -> 大类型,即子类 -> 父类,也称向上转型
如 Shape sr = new Rect(1, 2, 3, 4); // 即正常的 父类类型的引用指向子类类型的对象 - 强制类型转换:大类型 -> 小类型,即父类 -> 子类,也称向下转型或显式类型转型
如 int ib = ((Rect) sr).getLength(); // 父类转成子类,此时可以调用子类的独有方法
- 自动类型转换:小类型 -> 大类型,即子类 -> 父类,也称向上转型
引用数据类型转换的注意事项
- 引用数据类型之间的转换 必须发生在 父子类之间 ,否则编译报错
- 若强转的目标类型并不是该引用真正指向的数据类型时则编译通过,运行阶段发生类型转换异常
- 父类是Shape,再新建一个子类Circle
public class Circle extends Shape { }
如果进行强转:Circle c1 = (Circle)sr;
编译OK,运行时抛出异常:ClassCastException 类型转换异常
- 现在一共出现四种异常:
(1) ArithmeticException
(2) ArraysOutOfBoundsException
(3) NullPointerException
(4) ClassCastException
- 引用类型转换在运行时出现异常的原因:
编译阶段sc调用的是 父类 版本Shape的方法,而Shape和Circle是父子关系,因此编译不报错
运行阶段sc调用的是 子类 版本Rect的方法,但是Rect和Circle不是父子关系,因此运行时报错
public class Circle extends Shape {
int ir; // 半径
public Circle() {
}
public Circle(int x, int y, int ir) {
super(x, y);
setIr(ir);
}
public int getIr() {
return ir;
}
public void setIr(int ir) {
if(ir > 0) {
this.ir = ir;
} else {
System.out.println("半径不正确哦!");
}
}
@Override
public void show() {
super.show(); // 默认调用子类方法时,不用父类方法,所以父类方法只在编译时调用
// 运行阶段如果也要执行父类方法,那就手动加入super.show()
System.out.println("圆的半径是:" + getIr());
}
}
instanceof
- 为了避免强转时发生错误,应该在强转之前进行判断:
if(引用变量 instanceof 数据类型) // 判断引用变量指向的对象是否是后面的数据类型
例如:
if(sr instanceof Circle) {
System.out.println("可以放心地转换!");
Circle c1 = (Circle)sr;
} else {
System.out.println("强转有风险,操作需谨慎!");
}
4.4 案例:Rect对象特征打印(方法的调用作为参数)
public class ShapeTest {
// 自定义成员方法实现将参数指定矩形对象特征打印出来的行为
// static最大的意义在于:调用时直接用 类名.
public static void draw(Rect r) { // 为什么Rect r能当形参?类比 String s
r.show();
}
public static void main(String[] args) {
// Rect r = new Rect(1, 2, 30, 40);
ShapeTest.draw(new Rect(1, 2, 30, 40)); // 注意传实参的方式
}
}
4.5 多态的实际意义(重点)
- 多态的实际意义在于屏蔽不同子类的差异性,实现通用的编程带来不同的效果
- 通过以下例子来说明:自定义成员方法实现分别打印矩形、圆形对象的特征,对象由参数传入,然后自定义成员方法来既能打印矩形也能打印圆形对象的特征,对象有参数传入
- 不采用多态的特性时:(很繁琐)
public class ShapeTest {
// 自定义成员方法实现将参数指定矩形对象特征打印出来的行为,也就是绘制图形的行为
public static void draw(Rect r) {
r.show();
}
// 自定义成员方法实现将参数指定圆形对象特征打印出来的行为
public static void draw(Circle c) { // 重载
c.show();
}
// 自定义成员方法实现既能打印矩形对象又能打印圆形对象的特征,对象由参数传入
public static void draw(Circle c, Rect r) {
c.show();
r.show();
}
public static void main(String[] args) {
// Rect r = new Rect(1, 2, 30, 40);
ShapeTest.draw(new Rect(1, 2, 30, 40));
System.out.println("-----------------------");
ShapeTest.draw(new Circle(5, 6, 700));
System.out.println("-----------------------");
ShapeTest.draw(new Circle(111,222,333), new Rect(444,555,66,777));
}
}
- 采用多态的特性时:(简单)
public class ShapeTest {
/*
// 自定义成员方法实现将参数指定矩形对象特征打印出来的行为,也就是绘制图形的行为
public static void draw(Rect r) {
r.show();
}
// 自定义成员方法实现将参数指定圆形对象特征打印出来的行为
public static void draw(Circle c) { // 重载
c.show();
}
// 自定义成员方法实现既能打印矩形对象又能打印圆形对象的特征,对象由参数传入
public static void draw(Circle c, Rect r) {
c.show();
r.show();
}*/
// 自定义成员方法实现既能打印矩形对象又能打印圆形对象的特征,对象由参数传入
// Shape s = new Rect(1,2,3,4); 父亲类型的引用子类类型的对象,形成了多态
// Shape s = new Circle(1,2,3); 父亲类型的引用子类类型的对象,形成了多态
// 多态的使用场合之一:通过参数传递形成了多态
public static void draw(Shape s) {
// 编译阶段调用父类的版本,运行阶段调用子类重写以后的版本
s.show();
}
public static void main(String[] args) {
// Rect r = new Rect(1, 2, 30, 40);
ShapeTest.draw(new Rect(1, 2, 30, 40));
System.out.println("-----------------------");
ShapeTest.draw(new Circle(5, 6, 700));
System.out.println("-----------------------");
//ShapeTest.draw(new Circle(111, 222, 333), new Rect(444, 555, 666, 777));
ShapeTest.draw(new Circle(111, 222, 333)); // 打印圆形
ShapeTest.draw(new Rect(444, 555, 666, 777)); // 打印矩形
}
}
4.6 抽象方法和抽象类(重点)
抽象方法的概念
- 抽象方法主要指不能具体实现的方法并且使用 abstract 关键字修饰,即没有方法体(抽象方法不能被调用)
- 具体格式:
访问权限 abstract 返回值类型 方法名();
如:
public abstract void cry(); // 注意与 public abstract void cry(){} 的区别
抽象类的概念
- 抽象类主要指不能具体实例化的类并且使用abstract关键字修饰,即不能创建对象
(其实真正意义上的抽象类应该是:具有抽象方法并且使用abstract关键字修饰的类) - 抽象类的类名命名规范:以Abstract开头,如public abstract class AbstractTest { }
抽象方法和抽象类的关系
- 抽象类中可以有成员变量、成员方法、构造方法;
- 其中抽象类的构造方法存在意义:子类可以用super()的方式来调用父类的构造方法
- 抽象类中可以没有抽象方法,也可以有抽象方法;也可以有静态方法
- 拥有抽象方法的类必须是抽象类,因此真正意义上的抽象类是具有抽象方法并且使用abstract关键字修饰的类
- 为什么抽象类不能实例化(不能使用new创建对象):
- 只要是抽象类,类体里就有可能存在抽象方法,而抽象方法是不能调用的,因此为了防止程序员不小心调用到抽象方法引起错误,Java干脆设置抽象类不能使用new来创建对象
抽象类的实际意义
- 抽象类的实际意义在于被继承(多态),而不是在于创建对象
- 当一个类继承抽象类后必须重写抽象方法,否则该类也变成抽象类,即:抽象类对子类具有强制性和规范性,因此也叫做 模板设计模式
/*
创建抽象类AbstractTest
*/
public abstract class AbstractTest {
private int cnt;
public AbstractTest() {
}
public AbstractTest(int cnt) {
setCnt(cnt);
}
public int getCnt() {
return cnt;
}
public void setCnt(int cnt) {
this.cnt = cnt;
}
// 自定义抽象方法
public abstract void show();
public static void main(String[] args) {
// 声明该类类型的引用指向该类类型的对象
//AbstractTest at = new AbstractTest();
//System.out.println("at.cnt = " + at.cnt); // 0
}
}
/*
创建抽象类AbstractTest的子类SubAbstractTest
*/
public class SubAbstractTest extends AbstractTest {
@Override
public void show() { // 去掉了abstract关键字
System.out.println("其实我这个方法是被迫重写的,因为继承了抽象类");
}
public static void main(String[] args) {
// 1.声明本类类型的引用指向本类类型的对象
SubAbstractTest sat = new SubAbstractTest();
sat.show();
// 2.声明AbstractTest类型的引用指向子类的对象,形成了多态
// 多态的使用场合之二:直接在方法体中使用抽象类(父类)的引用指向子类类型的对象
AbstractTest at = new SubAbstractTest();
// 编译阶段调用父类版本,运行阶段调用子类版本
at.show();
// 此 at.show() 与上面的sat.show() 打印结果一样
// 区别在于前者没有使用多态,而后者使用了多态
// 推荐使用多态,理由:只需改变子类(如上面的SubStractTest),修改为其他子类,
// 就可以改变结果,而不需要动其他代码,很方便
}
}
开发经验分享(多态优缺点)
- 由上面的代码引出了以下的开发经验分享
- 优点:在以后的开发中推荐使用多态的格式,此时父类类型直接调用的所有方法一定是父类所拥有的方法,若以后更换子类,只需要将new关键字后面的子类类型修改而其他地方无需改变即可立即生效,从而提高代码的可维护性和可扩展性
- 缺点:父类引用 不能直接调用 子类独有 的方法,若调用需要强制类型转换
案例:抽象类的应用
- 银行有 定期账户 和 活期账户,继承自账户类。
- 账户类中:
public class Account {
private double money;
public double getLixi() { }; // 对于不同的账户,利息计算不一样
// 因此不能写具体方法的功能,而写抽象方法
}
- 创建抽象类Account
public abstract class Account {
private int money;
public Account() {
}
public Account(int money) {
setMoney(money);
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
if(money > 0) {
this.money = money;
} else {
System.out.println("金额不合理哦!");
}
}
// 自定义抽象方法实现计算利息并返回的功能描述
public abstract double getLixi();
}
- 创建抽象类Account的子类FixedAccount
/*
活期账户
*/
public class FixedAccount extends Account {
// 本类没有独有的成员变量
public FixedAccount() {
// super(); // 调用父类的无参构造方法,不写则编译器默认已写上
}
public FixedAccount(int i) {
super(i); // 调用父类的有参构造方式
//this.setMoney(i); // 也可以用这个方法
}
@Override
public double getLixi() {
// 利息 = 本金 * 利率 * 时间
return getMoney() * 0.03 * 1;
}
public static void main(String[] args) {
// 1.声明一个Account类型的引用指向子类类型的对象,形成多态
// Account acc = new FixedAccount(1000); // 有参构造
Account acc = new FixedAccount(); // 无参构造
acc.setMoney(1000);
double res = acc.getLixi();
System.out.println("计算的利息是:" + res);
}
}
笔试考点
- private 和 abstract 关键字不能共同修饰一个方法
- 因为父类的private私有方法不能被子类继承,因此对于(abstract修饰的)抽象方法来说,私有方法是没有意义的
- final 和 abstract 关键字不能共同修饰一个方法
- 因为final方法不能被重写(可以被继承),而abstract方法就是用来继承与重写的,两者冲突
- final 和 abstract 关键字不能共同修饰一个类
- 因为final类不能被继承,而abstract类需要被继承,两者冲突
- static 和 abstract 不能共同修饰一个方法
- 因为本来抽象类里的abstract抽象方法是为了防止程序员直接用new从抽象类里调用方法,而static会将该抽象方法从对象层级提升为类层级,可以用 类名. 调用, 两者冲突
4.7 接口(重点)
接口的基本概念
- 接口就是一种比抽象类还抽象的类,所有方法都为抽象方法(里面没有构造方法)
- 定义接口的关键字:interface (定义类的关键字是class)
public interface InterfaceTest {
public static final int CNT = 1; // 里面只能有常量, 不写static的话,会默认有static
private void show(){} // 从jdk9开始允许接口出现私有方法
public abstract void show(); // 里面只有抽象方法(新特性除外),public abstract关键字可以省略,但建议写上,为了与新特性区分开
//void show(); // public abstract关键字省略了
}
接口的实际意义
- 接口弥补了Java中不支持多继承的不足
public interface Metal {
// 自定义抽象方法来描述发光的行为
public abstract void shine();
}
public interface Money {
// 自定义抽象方法描述购物的行为
public abstract void buy();
}
// 使用 implements关键字 表达实现的关系,支持多实现(implements是实现的意思)
public class Gold implements Metal, Money{
@Override
public void shine() {
System.out.println("金黄色光芒");
}
@Override
public void buy() {
System.out.println("购物");
}
public static void main(String[] args) {
// 1.声明接口类型的引用指向实现类的对象,形成了多态
Metal mt = new Gold();
mt.shine();
Money mn = new Gold();
mn.buy();
}
}
案例:类与接口之间的关系
题目:
- 编程实现Runner接口,提供一个描述奔跑行为的抽象方法
- 编程实现Hunter接口继承Runner接口,并提供一个描述捕猎行为的抽象方法
- 编程实现Man类实现Hunter接口并重写抽象方法,在main方法中使用多态方式测试
public interface Runner {
// 自定义抽象方法来描述奔跑行为
public abstract void run();
}
// 接口只能继承接口,不能继承类
public interface Hunter extends Runner {
// 自定义抽象方法来描述捕猎的行为
public abstract void hunt();
}
public class Man implements Hunter {
@Override
public void hunt() {
System.out.println("捕猎!");
}
@Override
public void run() {
System.out.println("跑!");
}
public static void main(String[] args) {
// 1.声明接口类型的引用指向实现类的对象,形成了多态
Hunter hunter = new Man();
hunter.hunt();
hunter.run();
System.out.println("--------------------");
Runner runner = new Man();
runner.run();
}
}
抽象类和接口的主要区别(笔试题)
- 定义抽象类的关键字:abstract class,定义接口的关键字:interface
- 继承抽象类的关键字:extends,实现接口的关键字:implements
- 继承抽象类支持单继承,实现接口支持多实现
- 抽象类中可以有构造方法,接口中不能有构造方法
- 抽象类中可以有成员变量,接口中只可以有常量
- 抽象类中可以有成员方法,接口中只可以有抽象方法
- 抽象类中增加方法时可以不重写,接口中增加方法时实现类需要重写(Java8以前的版本)
- 从Java8开始增加新特性,接口中允许出现非抽象方法和静态方法,但非抽象方法需要使用default关键字修饰
- 从Java9开始增加新特性,接口中允许出现私有方法