- 掌握抽象类和抽象方法
- 掌握final修饰符的用法
- 掌握接口的用法
- 理解面向对象设计原则
问题: 星沐生态农场”游戏中,以下代码是否存在问题?
Crop crop= new Crop(); crop.print();
分析: 实例化Crop对象无意义,因为农作物是一个抽象的概念,而不是具体的某个实例对象。
方案: 使用抽象类
语法:
[访问修饰符] abstract class <类名> { //省略代码…… }
示例:
public abstract class Crop { //省略代码…… }
测试:
结论:
- 抽象类不能被实例化(Crop is abstract; cannot be instantiated)
问题: 下面代码图的执行结果,有什么问题?
分析:
- 从图中不难发现 Crop中print()方法体中输出的作物特性信息是无实际意义的,简单来说就是无用代码。
方案:
- 抽象类中使用抽象方法优化代码
什么是抽象方法?
- 使用**abstract**关键字修饰的方法称之为抽象方法
- 抽象方法没有方法体
- 抽象方法必须在抽象类里
- 抽象方法必须在子类中被实现,除非子类是抽象类
语法:
[访问修饰符] abstract class 类名 { [访问修饰符] abstract <返回类型> <方法名>([参数列表]); }
示例:
//抽象类 public abstract class Crop { //抽象方法,只有方法的声明,没有方法体中具体的实现 public abstract void print(); //普通方法 public void test(){ } }
结论
- 有抽象方法的类必然是抽象类。
- 但抽象类中的方法并不一定都是抽象方法。
- 抽象方法只有方法声明,无具体实现。
需求: 为农场增加除虫功能,具体要求如下
- 在“星沐生态农场”补充为作物除虫的相关功能,每种作物输出不同的除虫信息
- 苹果树:萌芽前和开花时打药3次,果实生长期打药2次,采摘期打药1次
- 玉米:种子喷洒农药后再播种,防止地下害虫咬食,果实生长期打药1次
分析:
- 每种作物有各自的除虫方案
- 在父类Crop中,无法抽取出其所有子类共同的除虫信息
- 在父类中定义抽象方法disinfestation()
- 在每个子类中重写该方法,实现该类对象的除虫操作
示例:
Crop类
public abstract class Crop{ //省略属性和方法... //定义除虫的抽象方法 public abstract void disinfestation(); }
AppleTree类
public class AppleTree extends Crop { // 没有重写父类的抽象方法disinfestation() }
如图所示,当子类继承父类,父类是一个抽象类的时候,会出现红色的警告线,告诉我们编译错误,这也恰好论证了我们前面提出的观点:
- 抽象方法必须在子类中被实现,除非子类是抽象类
语法:
方案:
- 在子类中重写父类定义的抽象方法disinfestation()
- AppleTree类
@Override public void disinfestation() { System.out.println(super.getName()+"萌芽前和开花时打药3次," + "果实生长期打药2次,采摘期打药1次。"); }
- Corn类
@Override public void disinfestation() { System.out.println(super.getName()+"种子喷洒农药后再播种," +"防止地下害虫咬食,果实生长期打药1次。"); }
- 测试类关键代码
public static void main(String[] args) { Crop crop = new AppleTree("富士");//苹果树 crop.disinfestation(); crop = new Corn(50);//玉米 crop.disinfestation(); }
- 运行结果
抽象类的优势
提高可重用性
- 抽象类可以看作是类的一个模版,定义了子类的行为,可以为子类提供默认实现,无需子类中重复实现这些方法
代码松耦合,更易于维护
- 子类可以分别实现抽象父类中定义的抽象方法,将方法定义和方法实现的分离
方便实现多态
- 抽象类作为继承关系下的抽象层,不能被实例化,通常定义抽象类类型变量,其具体引用是实现抽象类的子类对象
抽象类的应用场合
抽象类与抽象方法使用总结
抽象方法只有方法声明,没有方法实现
有抽象方法的类必须声明为抽象类
子类必须重写所有的抽象方法才能实例化;否则,子类也必须声明成抽象类
抽象类中可以没有、有一个或多个抽象方法,甚至可以定义全部方法都是抽象方法
抽象类可以有构造方法,其构造方法可以被本类的其他构造方法调用
- 不是由private修饰构造方法,可以被其子类的构造方法调用
abstract可以用来修饰类和方法,不能用来修饰属性和构造方法
问题:
Dog类不希望再被其他类继承,应该怎么做?
使用final类public final class Puppy extends Dog { //… }
方法不希望被重写?
使用final方法public class Student { public final void print() { System.out.println("我是一个学生!"); } }
属性值不希望被修改?
使用常量public class Dog { final String name ="帅帅"; public void setName(String name) { this.name=name; //编译错误,不可再赋值 } }
常见错误:
1.使用final修饰引用型变量,变量的值是固定不变的,而变量所指向的对象的属性值是可变的
public class Student { String name; public Student(String name) { this.name = name; } public static void main(String[] args) { final Student stu = new Student("李明"); stu.name = "李明航"; stu = new Student("王亮");//编译错误,使用final修饰的引用型变量不可以再指向其他对象 } }
2.使用final修饰的方法参数,这些参数的值是固定不变的
class Value { int v; } public class Test { public void changeValue(final int i, final Value value) { i = 8; //编译错误:使用final修饰的方法参数,在整个方法中不能改变参数值 value.v = 8; } }
3.常见错误示例:
抽象方法只有声明无具体实现,static方法可通过类名直接访问,但无法修饰一个没有实现的方法
抽象方法需在子类中重写,但private方法不能被子类继承,自然无法进行重写
抽象方法需要在子类中重写,但final修饰的方法表示该方法不能被子类重写,前后是相互矛盾的
总结:
final可以用来修饰类、方法和属性,不能修饰构造方法
Java提供的很多类都是final类,不能重写
- 如:String类、Math类
Object类有一些final方法,只能被子类继承而不能被重写
- 如:getClass( )、notify( )、wait( )
Object类的hashCode( )、toString( )、equals(Object obj)方法不是final方法,可以被重写
经验:
- abstract和static不能结合使用
- abstract和private不能结合使用
- abstract和final不能结合使用
训练要点
- 抽象类和抽象方法
需求说明
玩家将弹弓拉到极限后发射,小鸟就飞出去进行攻击
不同类型的小鸟具有不同的攻击方式
模拟分裂鸟和火箭鸟飞行、叫和攻击的行为
要求
- 分裂鸟会分裂攻击
- 火箭鸟会加速冲撞攻击
- 分裂鸟和火箭鸟飞行过程中都伴有“嗷嗷叫”的声音
实现思路
按照面向对象分析的方法,通过提取名词和动词的方法分别找出类的属性和方法
- 鸟类具有统一的行为
- “飞行”、“叫”、“攻击”
因为每个鸟的攻击行为不同,父类中将攻击行为定义为抽象方法,在子类中给出具体实现
①.定义抽象类Bird,实现Bird类的飞行方法fly()、叫方法twitter()、抽象的攻击方法attack()
②.定义分裂鸟类,继承抽象类Bird,实现自己的攻击方法attack()
③.定义火箭鸟类,继承抽象类Bird,实现自己的攻击方法attack()
④.编写测试类,模拟游戏
//定义抽象类Bird package com.aiden.angrybirds; /** * 抽象类:鸟类 */ public abstract class Bird { public void fly() { System.out.println("弹射飞"); } public void twitter() { System.out.println("嗷嗷叫"); } /** * 抽象方法 */ public abstract void attack(); }
//定义分裂鸟类 package com.aiden.angrybirds; /** * 分裂鸟 */ public class SplitBird extends Bird { public void attack() { System.out.println("分裂攻击!"); } }
//定义火箭鸟类 package com.aiden.angrybirds; /** * 火箭鸟 */ public class RocketBird extends Bird { public void attack() { System.out.println("加速冲撞!"); } }
//编写测试类 package com.aiden.angrybirds; public class TestAngryBirds { /** * 愤怒的小鸟测试类 */ public static void main(String[] args) { //火箭鸟 Bird rocketBird = new RocketBird(); rocketBird.fly(); rocketBird.twitter(); rocketBird.attack(); //分裂鸟 Bird splitBird = new SplitBird(); splitBird.fly(); splitBird.twitter(); splitBird.attack(); } }
问题:
假设“愤怒的小鸟”游戏需求发生变更
- 当弹弓拉到极限时,鸟飞出进行攻击,并发出“嗷嗷叫”的声音
- 不同的鸟具有不同的能力,配备不同的装备,可采取不同的攻击方式,如表中所示
名称 装备 能力 攻击方式 炸弹鸟 炸弹 - 使用炸弹进行爆炸攻击 喷火分裂鸟 喷火器 分裂能力 分裂并喷射火焰进行攻击 旋转鸟 旋转引擎 - 旋转攻击 超级鸟 加速马达 分裂能力、放大能力 加速冲撞、分裂并膨胀攻击 分析:
鸟类具有飞行、叫和攻击行为;
装备有炸弹、喷火器、旋转引擎、加速马达;
将鸟类和装备分别定义为抽象类???
- 愤怒的小鸟可以继承鸟类的同时又继承装备吗?如何解决这个问题?
此时发现单继承已经满足不了我们需求了 (ಥ﹏ಥ)
- 所以我们的==接口==从此诞生:
- 将鸟类定义为抽象类,装备定义为接口
- 各类愤怒的小鸟继承自鸟类,实现装备的接口==
结论
- 类只能单继承,接口才可以改变这种囧境。
生活中的接口就是一套规范
Java 中,接口
是一种规范和标准
- 可以约束类的行为,使得实现接口的类(或结构)在形式上保持一致
是一些方法特征的集合
可看作是一种特殊的“抽象类”
但采用与抽象类完全不同的语法
抽象类利于代码复用,接口利于代码的扩展和维护
是一个不能实例化的类型
定义接口
//如果是public,则在整个项目中可见,如果省略,则只在该包中可见 [访问修饰符] interface 接口名 { // 接口成员 }
接口中的变量都是全局静态常量
- 自动使用public static final修饰
- 必须在定义时指定初始值
public interface Cirlcle { int P=5; public static final int P = 5; int P; }
public class 类名 implements 接口名 { //类成员 }
实现类必须实现接口的所有方法
实现类可以实现多个接口
注意:JDK1.8版本之前,接口中只能定义抽象方法,自JDK1.8版本开始,接口还允许定义静态方法和默认方法
- 向后兼容
允许开发者在已有接口里添加新的方法时不需改动已经实施该接口的所有实现类接口表示一种能力
“做这项工作需要一个钳工(木匠/程序员)”
钳工是一种“能力”,不关心具体是谁
接口是一种能力
体现在接口的方法上
面向接口编程
示例:定义MyInterface接口
package com.aiden.interfaces; /** * @author Aiden */ public interface MyInterface { int P = 5; //抽象方法 void function1();//接口中抽象方法系统自动添加public abstract修饰 //默认方法,如果不能满足某个实现类的需求,可在实现类中重写 default void function2() { System.out.println("这是一个默认方法"); } //静态方法,不允许在接口的实现类中重写,只能通过接口名称调用 static void function3() { System.out.println("这是一个静态方法"); } }
示例:定义MyClass类实现MyInterface接口
public class MyClass implements MyInterface { @Override public void function1() { System.out.println("实现MyInterface接口的function1()!"); } }
测试调用
package com.aiden.interfaces; /** * @author Aiden */ public class TestMyClass { public static void main(String[] args) { MyClass myClass = new MyClass(); myClass.function1(); //调用实现类中重写方法 myClass.function2(); //调用接口中默认方法 MyInterface.function3(); //调用接口中静态方法 } }
示例:
模拟将声卡、显卡和网卡装配到计算机的PCI插槽进行工作
分析:
PCI插槽本身没有实现任何功能
PCI插槽规定了启动和停止各种卡的要求
PCI插槽以被多种设备实现
- 使用Java接口实现
实现步骤:
- 定义PCI接口,具有开始、停止和输出信息的功能
- 定义声卡类,符合PCI插槽标准,可以发声
- 定义显卡类,符合PCI插槽标准,可以显示图像
- 定义网卡类,符合PCI插槽标准,可以传输网络数据
- 定义装配类,安装网卡、声卡和显卡并模拟工作
实现代码:
1.定义PCI接口,具有开始、停止和输出信息的功能
package com.aiden.interfaces.pci; /** * 定义PCI接口 */ public interface PCI { //开始 public void start(); //停止 public void stop(); //输出信息 public default void print() { System.out.println("符合PCI插槽标准!"); } }
2.定义声卡类,符合PCI插槽标准,可以发声
package com.aiden.interfaces.pci; /** * 声卡 */ public class SoundCard implements PCI { @Override public void start() { System.out.println("声卡发出声音!"); } @Override public void stop() { System.out.println("声卡停止发出声音!"); } }
3.定义显卡类,符合PCI插槽标准,可以显示图像
package com.aiden.interfaces.pci; /** * 显卡 */ public class GraphicCard implements PCI { @Override public void start() { System.out.println("显卡显示图像!"); } @Override public void stop() { System.out.println("显卡停止显示图像!"); } }
4.定义网卡类,符合PCI插槽标准,可以传输网络数据
package com.aiden.interfaces.pci; /** * 网卡 */ public class NetworkCard implements PCI { @Override public void start() { System.out.println("网卡开始传输数据!"); } @Override public void stop() { System.out.println("网卡停止传输数据!"); } }
5.定义装配类,安装网卡、声卡和显卡并模拟工作
package com.aiden.interfaces.pci; /** * 装配类 */ public class Assembler { /** * 装配方法 * @param pci PCI接口 */ public static void assemble(PCI pci){ pci.print(); pci.start(); pci.stop(); } public static void main(String[] args) { //装配网卡 System.out.println("***装配网卡***"); PCI networkCard = new NetworkCard(); Assembler.assemble(networkCard); //装配声卡 System.out.println("***装配声卡***"); PCI soundCard = new SoundCard(); Assembler.assemble(soundCard); //装配显卡 System.out.println("***装配显卡***"); PCI graphicCard = new GraphicCard(); Assembler.assemble(graphicCard); } }
6.测试调用
package com.aiden.interfaces.pci; public class TestPCI { public static void main(String[] args) { System.out.println("***模拟网卡工作***"); PCI networkCard = new NetworkCard(); networkCard.print(); networkCard.start(); networkCard.stop(); System.out.println("***模拟声卡工作***"); PCI soundCard = new SoundCard(); soundCard.print(); soundCard.stop(); soundCard.stop(); System.out.println("***模拟显卡工作***"); PCI graphicCard = new GraphicCard(); graphicCard.print(); graphicCard.stop(); graphicCard.stop(); } }
7.运行效果
定义复杂的接口
接口的多继承语法:
//通过extends实现接口的继承关系 [ 访问修饰符 ] interface 接口名 extends 父接口 1, 父接口 2,……{ //常量定义 //方法定义 }
注意事项1: 一个接口可以继承多个接口,但接口不能继承类
类实现多个接口:
//通过implements实现多个接口 [ 访问修饰符 ] class 类名 extends 父类名 implements 接口 1, 接口 2,……{ //类的成员 }
注意事项2:
- 一个普通类只能继承一个父类,但能同时实现多个接口
- extends 关键字必须位于 implements关键字之前
- 类必须实现所有接口(接口1、接口2…)的全部抽象方法,否则必须定义为抽象类
注意事项3:
如果一个类实现了多个接口,且这些接口具有相同的默认方法,应该如何处理?
在实现类中必须提供自己的默认方法,来覆盖接口中的默认方法。例如:接口A中定义了默认方法print(),接口B中也定义了不同实现的默认方法print()。如果类C实现了接口A和接口B,则类C中必须定义自己的print()方法;否则,在调用C对象的print()方法时无法确定是访问接口A的print()方法,还是访问接口B的print()方法
interface A { default void print () { System.out.println("这是A接口的一个默认方法"); } }
interface B { default void print () { System.out.println(“这是B接口的一个默认方法"); } }
class C implements A,B { //C类中必须定义自己的print()方法 void print () { System.out.println(“这是C类的print方法"); } }
public static void main(String[] args) { C cObject = new C(); //无法确定是访问接口A或接口B的print()方法 cObject.print();//调用C类print()方法 }
重新设计“愤怒的小鸟”游戏,要求效果如下:
请选择鸟的类型:1.炸弹鸟 2.喷火分裂鸟 3.旋转鸟 4.超级鸟 5.退出 1 弹射飞 嗷嗷叫 炸弹鸟攻击:炸弹爆炸攻击! 请选择鸟的类型:1.炸弹鸟 2.喷火分裂鸟 3.旋转鸟 4.超级鸟 5.退出 2 弹射飞 嗷嗷叫 喷火分裂鸟攻击:分裂攻击!使用喷火器喷射火焰! 请选择鸟的类型:1.炸弹鸟 2.喷火分裂鸟 3.旋转鸟 4.超级鸟 5.退出 3 弹射飞 嗷嗷叫 旋转鸟攻击:使用旋转引擎旋转攻击! 请选择鸟的类型:1.炸弹鸟 2.喷火分裂鸟 3.旋转鸟 4.超级鸟 5.退出 4 弹射飞 嗷嗷叫 超级分裂鸟攻击:分裂攻击!膨胀攻击!加速冲撞! 请选择鸟的类型:1.炸弹鸟 2.喷火分裂鸟 3.旋转鸟 4.超级鸟 5.退出 5 您已退出游戏!
分析
各类鸟 (is a 的关系)
- 炸弹鸟是一只鸟
- 喷火分裂鸟是一只鸟
- 旋转鸟是一只鸟
- 超级鸟是一只鸟
每类鸟具备相应的装备(has a 的关系)(协议和规范或能力)
- 返回装备名称
- 展示装备功能
每个鸟类在继承父类Bird的同时,根据需求实现不同的能力接口
实现过程
- 定义装备接口IEquipment(具备返回装备名称、展示装备功能的能力)
- 定义4个装备的实现类(旋转引擎装备:SpinEngine、炸弹装备:Bomb、加速马达装备:AccelerationMotor、喷火器装备:FlameThrower)
- 定义分裂能力接口ISpill
- 定义放大能力接口ISwell
- 实现抽象类Bird类 (抽象类,实现IEquipment接口,具备特定的装备和鸣叫、飞行、攻击、显示鸟行为的能力)
- 实现具体的鸟类
- 超级鸟(SuperBird): 继承自Bird类实现ISwell、ISplit接口
- 喷火分裂鸟(FlameSplitBird): 继承自Bird类,实现ISplit接口
- 炸弹鸟 (BombBird): 继承自Bird类
- 旋转鸟 (SpinBird): 继承自Bird类
- 编写测试类
愤怒小鸟(优化版)代码实现
接口与类的设计图
- 定义装备接口IEquipment(具备返回装备名称、展示装备功能的能力)
package com.aiden.angrybirdplus; /** * 装备接口 */ public interface IEquipment { /** * 返回装备名称 * @return 装备名称 */ String getName(); /** * 展示装备功能 */ void show(); }
- 定义4个装备的实现类(旋转引擎装备:SpinEngine、炸弹装备:Bomb、加速马达装备:AccelerationMotor、喷火器装备:FlameThrower)
package com.aiden.angrybirdplus; /** * 实现旋转引擎装备 */ public class SpinEngine implements IEquipment { @Override public String getName() { return "旋转引擎"; } @Override public void show() { System.out.println("使用"+this.getName()+"旋转攻击!"); } }
package com.aiden.angrybirdplus; /** * 实现炸弹装备 */ public class Bomb implements IEquipment { @Override public String getName() { return "炸弹"; } @Override public void show() { System.out.println(this.getName()+"爆炸攻击!"); } }
package com.aiden.angrybirdplus; /** * 实现加速马达装备 */ public class AccelerationMotor implements IEquipment { @Override public String getName() { return "加速马达"; } @Override public void show() { System.out.println("加速冲撞!"); } }
package com.aiden.angrybirdplus; /** * 实现喷火器装备 */ public class FlameThrower implements IEquipment { @Override public String getName() { return "喷火器"; } @Override public void show() { System.out.println("使用"+this.getName()+"喷射火焰!"); } }
- 定义分裂能力接口ISpill
package com.aiden.angrybirdplus; /** * 分裂能力接口 */ public interface ISplit { public default void split(){ System.out.print("分裂攻击!"); } }
- 定义放大能力接口ISwell
package com.aiden.angrybirdplus; /** * 放大能力接口 */ public interface ISwell { public default void swell(){ System.out.print("膨胀攻击!"); } }
- 实现抽象类Bird类 (抽象类,实现IEquipment接口,具备特定的装备和鸣叫、飞行、攻击、显示鸟行为的能力)
package com.aiden.angrybirdplus; public abstract class Bird { private IEquipment equipment;//装备 public Bird(){} public Bird(IEquipment equipment){ this.equipment = equipment; } public void twitter(){ System.out.println("嗷嗷叫"); } public void fly(){ System.out.println("弹射飞"); } /** * 抽象方法:攻击 */ public abstract void attack(); /** * 显示鸟的行为 */ public void show(){ fly(); twitter(); attack(); } public IEquipment getEquipment() { return equipment; } public void setEquipment(IEquipment equipment) { this.equipment = equipment; } }
实现具体的鸟类
- 超级鸟(SuperBird): 继承自Bird类实现ISwell、ISplit接口
- **喷火分裂鸟(FlameSplitBird): **继承自Bird类,实现ISplit接口
- **炸弹鸟 (BombBird): **继承自Bird类
- 旋转鸟 (SpinBird): 继承自Bird类
package com.aiden.angrybirdplus; /** * 超级鸟 */ public class SuperBird extends Bird implements ISplit, ISwell { public SuperBird(){ super(new AccelerationMotor()); //加速马达 } @Override public void attack() { System.out.print("超级分裂鸟攻击:"); this.split(); this.swell(); this.getEquipment().show(); } }
package com.aiden.angrybirdplus; /** * 喷火分裂鸟 */ public class FlameSplitBird extends Bird implements ISplit { public FlameSplitBird(){ super(new FlameThrower()); //装备喷火器 } @Override public void attack() { System.out.print("喷火分裂鸟攻击:"); this.split(); this.getEquipment().show(); } }
package com.aiden.angrybirdplus; /** * 炸弹鸟 */ public class BombBird extends Bird { public BombBird(){ super(new Bomb()); //装备炸弹 } @Override public void attack() { System.out.print("炸弹鸟攻击:"); this.getEquipment().show(); } }
package com.aiden.angrybirdplus; /** * 旋转鸟 */ public class SpinBird extends Bird { public SpinBird(){ super(new SpinEngine());//装备旋转引擎 } @Override public void attack() { System.out.print("旋转鸟攻击:"); this.getEquipment().show(); } }
- 编写测试类Game
package com.aiden.angrybirdplus; import java.util.Scanner; public class Game { public static Bird chooseBird(int type){ Bird bird = null; switch (type) { case 1: bird = new BombBird(); break; case 2: bird = new FlameSplitBird(); break; case 3: bird = new SpinBird(); break; case 4: bird = new SuperBird(); break; } return bird; } public static void main(String[] args) { Scanner input = new Scanner(System.in); int type; while(true) { System.out.println("请选择鸟的类型:1.炸弹鸟 " + "2.喷火分裂鸟 3.旋转鸟 4.超级鸟 5.退出 "); type = input.nextInt(); if(type>=1 && type<=4) { Bird bird = Game.chooseBird(type); bird.show(); }else{ System.out.println("您已退出游戏!"); break; } } } }
为了让代码更具灵活性,更能适应变化,需遵循的原则
摘取代码中变化的部分,形成接口
多用组合,少用继承
面向接口编程,不依赖于具体实现
针对扩展开放,针对改变关闭
- ——开闭原则
经验 面向接口编程可以实现接口和实现的分离,能够在客户端未知的情况下修改实现代码
如何理解接口是一种能力?
接口有比抽象类更好的特性
- 可以被多继承
- 设计和实现完全分离
- 更自然的使用多态
- 更容易搭建程序框架
- 更容易更换实现
- ……