第九章 多态和特殊类
9.1 多态 (重中之重)
多态的概念
- 多态主要指同一种事物表现出来的多种形态。
- 饮料:可乐、雪碧、红牛、脉动、…
- 宠物:猫、狗、鸟、小强、鱼、…
- 人:学生、教师、工人、保安、…
- 图形:矩形、圆形、梯形、三角形、…
多态的语法格式
父类类型 引用变量名 = new 子类类型();
如:
Shape sr = new Rect();
sr.show();
案例题目
- 编程实现Shape类的封装,特征有:横纵坐标,要求提供打印所有特征的方法。
- 编程实现Rect类的封装并继承自Shape类,特征有:长度和宽度。
- 编程实现ShapeRectTest类,在main方法中分别创建Shape和Rect类型对象并打印特征。
package com.lagou.task09;
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 static void test() {
System.out.println("Shape类中的静态方法!");
}
}
package com.lagou.task09;
public class Rect extends Shape {
private int len;
private int wid;
public Rect() {
}
public Rect(int x, int y, int len, int wid) {
super(x, y);
setLen(len);
setWid(wid);
}
public int getLen() {
return len;
}
public void setLen(int len) {
if(len > 0) {
this.len = len;
} else {
System.out.println("长度不合理哦!!!");
}
}
public int getWid() {
return wid;
}
public void setWid(int wid) {
if (wid > 0) {
this.wid = wid;
} else {
System.out.println("宽度不合理哦!!!");
}
}
@Override
public void show() {
super.show();
System.out.println("长度是:" + getLen() + ",宽度是:" + getWid());
}
// 自定义静态方法
//@Override Error: 历史原因、不是真正意义上的重写
public static void test() {
System.out.println("---Rect类中的静态方法!");
}
}
package com.lagou.task09;
public class Circle extends Shape {
private 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();
System.out.println("半径是:" + getIr());
}
}
package com.lagou.task09;
public class ShapeRectTest {
public static void main(String[] args) {
// 1.声明Shape类型的引用指向Shape类型的对象并打印特征
Shape s1 = new Shape(1, 2);
// 当Rect类中没有重写show方法时,下面调用Shape类中的show方法
// 当Rect类中重写show方法后,下面调用Shape类中的show方法
s1.show(); // 1 2
// 使用ctrl+d快捷键可以复制当前行
System.out.println("------------------------------------");
// 2.声明Rect类型的引用指向Rect类型的对象并打印特征
Rect r1 = new Rect(3, 4, 5, 6);
// 当Rect类中没有重写show方法时,下面调用Shape类中的show方法
// 当Rect类中重写show方法后,下面调用Rect类中的show方法
r1.show(); // 3 4 5 6
// 使用alt+shift+上下方向键 可以移动代码
System.out.println("------------------------------------");
// 3.声明Shape类型的引用指向Rect类型的对象并打印特征
// 相当于从Rect类型到Shape类型的转换 也就是子类到父类的转换 小到大的转换 自动类型转换
Shape sr = new Rect(7, 8, 9, 10);
// 当Rect类中没有重写show方法时,下面调用Shape类中的show方法
// 当Rect类中重写show方法后,下面的代码在编译阶段调用Shape类的方法,在运行阶段调用Rect类中的show方法
sr.show(); // 7 8 9 10
System.out.println("------------------------------------");
// 4.测试Shape类型的引用能否直接调用父类和子类独有的方法呢???
int ia = sr.getX();
System.out.println("获取到的横坐标是:" + ia); // 7
//sr.getLen(); error Shape类中找不到getLen方法,也就是还在Shape类中查找
// 调用静态方法
sr.test(); // 提示:不建议使用引用.的方式访问
Shape.test(); // 推荐使用类名.的方式访问
System.out.println("------------------------------------");
// 5.使用父类类型的引用调用子类独有方法的方式
// 相当于从Shape类型到Rect类型的转换,也就是父类到子类的转换 大到小的转换 强制类型转换
int ib = ((Rect) sr).getLen();
System.out.println("获取到的长度是:" + ib); // 9
// 希望将Shape类型转换为String类型 强制类型转换要求必须拥有父子类关系
//String str1 = (String)sr; Error
// 希望将Shape类型强制转换为Circle类型,下面没有报错
//Circle c1 = (Circle)sr; // 编译ok,但运行阶段发生ClassCastException类型转换异常,Rect类型不可以转化为Circle类型(已经到了运行阶段了)
// ★★在强制类型转换之前应该使用instanceof进行类型的判断
// ★★判断sr指向堆区内存中的对象是否为Circle类型,若是则返回true,否则返回false
if(sr instanceof Circle) {
System.out.println("可以放心地转换了!");
Circle c1 = (Circle)sr;
} else {
System.out.println("强转有风险,操作需谨慎!");
}
}
}
多态的特点
- 当父类类型的引用指向子类类型的对象时,父类类型的引用可以直接调用父类独有的方法。
- 当父类类型的引用指向子类类型的对象时,父类类型的引用不可以直接调用子类独有的方法。
- 对于父子类都有的非静态方法来说,编译阶段调用父类版本,运行阶段调用子类重写的版本(动态绑定)。
- 对于父子类都有的静态方法来说,编译和运行阶段都调用父类版本。
引用数据类型之间的转换
-
引用数据类型之间的转换方式有两种:
自动类型转换
和强制类型转换
。 -
自动类型转换主要指小类型向大类型的转换,也就是子类转为父类,也叫做向上转型(继承树向上)。
-
强制类型转换主要指大类型向小类型的转换,也就是父类转为子类,也叫做
向下转型
或显式类型转换
(继承树向下)。 -
引用数据类型之间的转换必须发生在父子类之间,否则编译报错.
-
若强转的目标类型并不是该引用真正指向的数据类型时则编译通过,运行阶段发生类型转换异常(ClassCastException类型转换异常)。
-
为了避免上述错误的发生,应该在强转之前进行判断,格式如下:
if(引用变量 instanceof 数据类型)
判断引用变量指向的对象是否为后面的数据类型
结果是true or false
多态的实际意义
多态的实际意义在于屏蔽不同子类的差异性实现通用的编程带来不同的效果。
package com.lagou.task09;
/*
多态到底有什么作用呢?
*/
public class ShapeTest {
// 自定义成员方法实现将参数指定矩形对象特征打印出来的行为,也就是绘制图形的行为
// Rect r = new Rect(1, 2, 3, 4);
// public static void draw(Rect r) { // static就是为了使用类名.方式访问
// r.show(); // 1 2 3 4
// }
// 自定义成员方法实现将参数指定圆形对象特征打印出来的行为
// public static void draw(Circle c) {
// c.show(); // 5 6 7
// }
// 自定义成员方法实现既能打印矩形对象又能打印圆形对象的特征,对象由参数传入 子类 is a 父类
// Shape s = new Rect(1, 2, 3, 4); 父类类型的引用指向子类类型的对象,形成了多态
// Shape s = new Circle(5, 6, 7); 多态
// 多态的使用场合之一:通过参数传递形成了多态
public static void draw(Shape s) {
// 编译阶段调用父类的版本,运行阶段调用子类重写以后的版本
s.show();
}
public static void main(String[] args) {
// Rect r = new Rect(1, 2, 3, 4);
// r.show();
ShapeTest.draw(new Rect(1, 2, 3, 4)); // Shape s = new Rect(1, 2, 3, 4);
ShapeTest.draw(new Circle(5, 6, 7)); // Shape s = new Circle(5, 6, 7);还是看s到底指向的是堆区的哪个,实现多态。
}
}
9.2 抽象类(重点)
抽象方法的概念
- 抽象方法主要指不能具体实现的方法并且使用
abstract
关键字修饰,也就是没有方法体。 - 具体格式如下:
访问权限 abstract 返回值类型 方法名(形参列表);
public abstract void cry();
抽象类的概念
抽象类主要指不能具体实例化的类(不能创建对象)并且使用abstract
关键字修饰,也就是不能创建对象。
package com.lagou.task09;
public abstract class AbstractTest { // abstract
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(); 用abstract修饰了就不能实例化
//System.out.println("at.cnt = " + at.cnt); // 0
}
}
抽象类和抽象方法的关系
- 抽象类中可以有成员变量、构造方法、成员方法;
- 抽象类中可以没有抽象方法,也可以有抽象方法;
- 拥有抽象方法的类必须是抽象类,因此真正意义上的抽象类应该是具有抽象方法并且使用
abstract
关键字修饰的类。
(原因:一个抽象类中可能有抽象方法,且抽象类不能实例化对象。抽象类不能实例化对象的原因是抽象类中可能存在抽象方法,抽象方法内是没有方法体的,所以调用抽象方法是没有意义的。为了防止程序员不小心调用抽象方法,所以规定抽象类中不可new对象)
抽象类的实际意义
- 抽象类的实际意义不在于创建对象而在于被继承。(抽象类没法new对象只能让子类来调用)
- 当一个类继承抽象类后必须重写抽象方法,否则该类也变成抽象类,也就是抽象类对子类具有强制性和规范性,因此叫做模板设计模式。
(规范性:指的是所有子类继承的时候,都要按照超类的抽象方法的格式去重写(所以叫模板嘛))
package com.lagou.task09;
public class SubAbstractTest extends AbstractTest/*, Account*/ {
@Override
public void show() {
System.out.println("其实我是被迫重写的,否则我也得变成抽象的呀!");
}
public static void main(String[] args) {
// 1.声明本类类型的引用指向本类类型的对象,没有多态
SubAbstractTest sat = new SubAbstractTest();
sat.show();
System.out.println("-------------------------------");
// 2.声明AbstractTest类型的引用指向子类的对象,形成了多态(因为AbstractTest是抽象类,没法new对象)
// ★★多态的使用场合之二: 直接在方法体中使用抽象类的引用指向子类类型的对象
AbstractTest at = new SubAbstractTest();
// 编译阶段调用父类版本,运行阶段调用子类版本,使用多态方式可以提高代码的维护性,需要更换子类的时候只需要更改new后面的类名
at.show(); // 这里用了多态,上面没用
((SubAbstractTest2) at).test();
System.out.println("-------------------------------");
SubAbstractTest2 sat2 = new SubAbstractTest2();
sat2.test();
}
}
开发经验分享
• 在以后的开发中推荐使用多态的格式,此时父类类型引用直接调用的所有方法一定是父类中拥有的方法,若以后更换子类时,只需要将new
关键字后面的子类类型修改而其它地方无需改变就可以立即生效,从而提高了代码的可维护性和可扩展型。
• 该方式的缺点就是:父类引用不能直接调用子类独有的方法,若调用则需要强制类型转换。(其实就变成了子类的引用指向子类的对象啦,转换之后就不太像多态了)
抽象类的应用
银行有 定期账户和活期账户。继承自账户类。账户类中:
public class Account{
private double money;
public double getLixi(){}
}
package com.lagou.task09;
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(); // 因为利率不一样,所以getLiXi是抽象的方法
// private 和 abstract 关键字不能共同修饰一个方法
//private abstract double getLixi();
// final 和 abstract 关键字不能共同修饰一个方法
//public final abstract double getLixi();
// static 和 abstract 关键字不能共同修饰一个方法
//public static abstract double getLixi();
}
package com.lagou.task09;
public class FixedAccount extends Account {
public FixedAccount(int i) {
super(i); // 表示调用父类的有参构造方法
}
@Override
public double getLixi() {
// 利息 = 本金 * 利率 * 时间
return getMoney() * 0.03 * 1;
}
public static void main(String[] args) {
// 1.声明Account类型的引用指向子类类型的对象,形成了多态
Account acc = new FixedAccount(1000); // 目前只是调用了超类的空构造,所以没有int输入,所以要调用超类的各种构造器,使用super();来初始化
double res = acc.getLixi();
System.out.println("计算的利息是:" + res); // 30.0
}
}
package com.lagou.task09;
public class FixedAccount extends Account {
public FixedAccount() {
}
@Override
public double getLixi() {
// 利息 = 本金 * 利率 * 时间
return getMoney() * 0.03 * 1;
}
public static void main(String[] args) {
// 1.声明Account类型的引用指向子类类型的对象,形成了多态
Account acc = new FixedAccount(); // 也可以这样子初始化,使用setMoney。和构造器初始化的区别就是构造器初始化只能修改他的值一次,set方法可以修改无数次
acc.setMoney(1000);
double res = acc.getLixi();
System.out.println("计算的利息是:" + res); // 30.0
}
}
这两种声明方式都没问题
注意:
private abstract double getLiXi();
是错误的,原因是:private和abstract不可以共同修饰同一个方法,因为private方法是私有的,不可以让子类继承的,但是abstract抽象方法的意义就是让子类继承之后再重写方法,所以二者是冲突的!public final abstract double getLiXi();
也是错误的,原因是:final和abstract不可以共同修饰同一个方法,因为final修饰的方法不可以被重写可以被继承,但是abstract抽象方法的意义就是让子类继承之后再重写方法才能使用,所以二者也是冲突的!public static abstract double getLiXi();
也是错误的,因为static把方法提升到类层级,可以使用类名.的方式访问了,但是abstract本质是抽象方法,没有方法体,调用了是无意义的。为了防止程序员调用抽象方法,抽象方法是不可以用static来修饰的!
9.3 接口(重点)
接口的基本概念
- 接口就是一种比抽象类还抽象的类,体现在所有方法都为抽象方法。
- 定义类的关键字是
class
,而定义接口的关键字是interface
。
package com.lagou.task09;
/*
接口超级抽象,是没有方法体大括号的哦~
*/
public interface InterfaceTest {
public static final int CNT = 1; // 里面只能有常量 ,public static final可以省略的
private void show(){} // 从Java9开始允许接口中出现私有方法
public abstract void show(); // 里面只能有抽象方法(新特性除外),关键字(public abstract)可以省略,但建议写上
}
- 接口存在的目的是:1. 弥补Java中不支持多继承的不足。而且接口存在的目睹也就是实现多态,继承啦。
- 举例子,如:
金属接口
货币接口
黄金类
若是存在三个类:金属类,货币类,黄金类
黄金类属于金属类,黄金类也属于货币类,但是Java只能实现单继承,没法继承两个类
由此产生了接口的概念,将金属类和货币类转变为金属接口和货币接口,由此黄金类既可以实现金属接口,又可以实现货币接口。
满足了其多继承的要求。
·package com.lagou.task09;
/*
金属接口
*/
public interface Metal {
// 自定义抽象方法描述发光的行为
public abstract void shine();
}
package com.lagou.task09;
/*
货币接口
*/
public interface Money {
// 自定义抽象方法描述购物的行为
public abstract void buy();
}
package com.lagou.task09;
/*
黄金类同时实现两个接口
*/
// 使用implements关键字表达实现的关系,支持多实现
public class Gold implements Metal, Money { // implements是实现的意思
@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方法中使用多态方式测试。
package com.lagou.task09;
public interface Runner {
// 自定义抽象方法描述奔跑的行为
public abstract void run();
}
package com.lagou.task09;
// 接口只能继承接口,不能继承类
public interface Hunter extends Runner {
// 自定义成员方法描述捕猎的行为
public abstract void hunt();
package com.lagou.task09;
public class Man implements Hunter {
@Override // 这两个方法都不重写的话就变成抽象类了!!所以必须重写
public void hunt() {
System.out.println("正在追赶一直小白兔...");
}
@Override
public void run() {
System.out.println("正在被一直大熊追赶,玩命奔跑中...");
}
@Override
public void show1() {
System.out.println("为了给你几分薄面,我决定重写一下!");
}
public static void main(String[] args) {
// 1.声明接口类型的引用指向实现类的对象,形成了多态
Runner runner = new Man(); // 提高代码可读性
runner.run();
Hunter hunter = new Man();
hunter.hunt();
System.out.println("-----------------------------------------");
// 2.可以使用接口名称.的方式调用接口中的静态方法
Hunter.test();
}
}
类和接口之间的关系
类和类之间的关系:继承,单继承
接口和接口之间的关系:继承,多继承
类和接口之间的关系:实现,多实现
抽象类和接口的主要区别(笔试题)
- 定义抽象类的关键字是
abstract class
,而定义接口的关键字是interface
。 - 继承抽象类的关键字是
extends
,而实现接口的关键字是implements
。 - 继承抽象类支持单继承,而实现接口支持多实现。
- 抽象类中可以有构造方法,而接口中不可以有构造方法。
- 抽象类中可以有成员变量,而接口中只可以有常量。
- 抽象类中可以有成员方法,而接口中只可以有抽象方法。
- 以下三点要注意!
- 抽象类中增加方法时子类可以不用重写,而接口中增加方法时实现类需要重写(Java8以前的版本)。(因为抽象类增加的方法可能是成员方法,所以不用重写,但是Java8以前接口中只能有抽象方法,所以增加抽象方法时是必须重写的,否则他自己也变成抽象方法了!)
- 从Java8开始增加新特性,接口中允许出现非抽象方法和静态方法,但非抽象方法需要使用
default
关键字修饰。 - 从Java9开始增加新特性,接口中允许出现私有方法。
package com.lagou.task09;
// 接口只能继承接口,不能继承类
public interface Hunter extends Runner {
// 自定义成员方法描述捕猎的行为
public abstract void hunt();
// ★★将两个默认方法中重复的代码可以提取出来打包成一个方法在下面的两个方法(show1,show2)中分别调用即可
private void show() { // ★★本来是public default void show,但是这个方法是为了将重复代码打包给本类使用(不是为了继承,不需要重写,不用外界访问),因此可以写成私有方法,不使用接口中的default默认方式
System.out.println("在以后的开发中尽量减少重复的代码,也就是减少代码的冗余!");
}
// ★★增加一个抽象方法
public abstract void show1();
// ★★增加非抽象方法(使用default关键字,Java8特性):因为接口的所有方法都得被重写,但是有些方法写出来就是直接使用的,重写他毫无意义,所以增加了一个在接口中可以写非抽象方法的原则,此时不重写也不报错,重写也没关系
public default void show1() {
show();
//System.out.println("在以后的开发中尽量减少重复的代码,也就是减少代码的冗余!");
System.out.println("show1方法中:这里仅仅是接口中的默认功能,实现类可以自由选择是否重写!");
}
// 增加非抽象方法
public default void show2() {
show();
//System.out.println("在以后的开发中尽量减少重复的代码,也就是减少代码的冗余!");
System.out.println("show2方法中:这里仅仅是接口中的默认功能,实现类可以自由选择是否重写!");
}
// ★★增加静态方法 隶属于类层级,也就是接口层级:超级抽象,接口不能new对象,接口写出来的对象也不能用引用.的方式调用,但是可以通过接口名来调用。
public static void test() {
System.out.println("这里是静态方法,可以直接通过接口名.的方式调用,省略对象的创建");
}
}
package com.lagou.task09;
public class Man implements Hunter {
@Override // 这两个方法都不重写的话就变成抽象类了!!所以必须重写
public void hunt() {
System.out.println("正在追赶一直小白兔...");
}
@Override
public void run() {
System.out.println("正在被一直大熊追赶,玩命奔跑中...");
}
@Override
public void show1() {
System.out.println("为了给你几分薄面,我决定重写一下!");
}
public static void main(String[] args) {
// 1.声明接口类型的引用指向实现类的对象,形成了多态
Runner runner = new Man(); // 提高代码可读性
runner.run();
Hunter hunter = new Man();
hunter.hunt();
System.out.println("-----------------------------------------");
// ★★2.可以使用接口名称.的方式调用接口中的静态方法
Hunter.test();
}
}