超类与子类续2
主要参考《Java核心技术卷1》
阻止继承
有时,我们希望阻止别人定义某个类的子类。
不允许扩展(继承)的类被称为final类。如果在类定义中使用了final修饰符,就表明这个类是final类(不可继承类)。
声明格式如下所示:
public final class Manager extends Employee {
}
也可以将类中的某个特定方法声明为final。如果这样做,那么所有子类都不能覆盖这个方法(final类中的所有方法自动地成为final方法)。例如:
public class Manager extends Employee {
//...
public final void setBonus(double bonus) {
this.bonus = bonus;
}
//...
}
将方法或类声明为final只有一个原因:确保它们不会在子类中改变语义。
有些程序员认为:除非有足够的理由使用多态性,否则应该将所有的方法都声明为final。
例子:改进员工类
试一试,将Employee类设置为final会发生什么?
如果要设置的话,Employee类中的哪些方法应该设置为final?假设getName不设置为final则(理论上)允许下面情况发生:
public class Manager extends Employee {
// ...
@Override
public String getName() {
return super.getName() + "是猪";
}
public static void main(String[] args) {
Manager m = new Manager("小李", 1000, 2024, 11, 1);
System.out.println(m.getName()); // => 小李是猪
}
}
薪水可能随角色的变化有不同的计算方式,get姓名
和get入职日期
不该在子类中改变语义:
package ex1;
import java.time.LocalDate;
public class Employee {
private final String name; // 姓名
private double salary; // 薪水
private final LocalDate hireDay; // 入职日期
public Employee(String name, double salary, int year, int month, int day) {
this.name = name;
this.salary = salary;
hireDay = LocalDate.of(year, month, day);
}
public final String getName() {
return name;
}
public double getSalary() {
return salary;
}
public final LocalDate getHireDay() {
return hireDay;
}
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
}
强制类型转换
语法
// 1 数值的强制类型转换
double x = 3.141592;
int nx = (int) x; // 有"风险"才需要"强制",类似于用小杯子装大被子里的水
System.out.printf("x: %f, nx: %d\n", x, nx);
// 2 对象引用的强制类型转换
Employee e = new Manager("王三狗", 25000, 2020, 9, 18);
Manager m = (Manager) e;
进行强制类型转换的唯一原因
要在暂时忘记对象的实际类型之后使用对象的全部功能。
例子:邮寄物品
邮寄物品.java:
package ex2;
public class MailItem {
// 邮寄物品类
}
信件.java:
package ex2;
public class Letter extends MailItem {
// 信件可视为一种特殊的邮寄物品,满足IS_A关系
private String content = "里面什么也没写"; // 信件内容
public Letter() {
}
public Letter(String content) {
this.content = content;
}
public void openAndReadContent() {
// 打开并读取信件
System.out.println("信件内容是: " + content);
}
}
接收者.java:
package ex2;
public class Recipient {
// 邮寄物品接收者
public void processMailItem(MailItem item) {
// 要调用读取信件的方法就必须将邮寄物品强制转换成更特殊的信件
Letter letter = (Letter) item;
letter.openAndReadContent();
}
}
主逻辑:
package ex2;
public class Main {
public static void main(String[] args) {
MailItem item = new Letter("你好吗");
// 此时信件被视为邮寄物品在各个快递点流转,
// 快递点并不需要知道这个邮寄物品到底是什么
// 即实际类型被暂时忘记了
Recipient recipient = new Recipient();
recipient.processMailItem(item);
}
}
假设这不是信件会发生什么?
package ex2;
public class Main {
public static void main(String[] args) {
// MailItem item = new Letter("你好吗");
MailItem item = new MailItem();
Recipient recipient = new Recipient();
recipient.processMailItem(item);
}
}
将产生一个运行时的异常,叫做类转换异常ClassCastException。(运行时异常是程序跑起来才发生的,编译器发现不了的)
例子:instanceof
因此,应该养成这样一个良好的编程习惯:在进行强制类型转换之前,先查看是否能够成功地转换。为此只需要使用instanceof操作符。
package ex2;
public class Recipient { // 邮寄物品接受者
public void processMailItem(MailItem item) { // 处理邮寄物品
if (item instanceof Letter) {
Letter letter = (Letter) item;
letter.openAndReadContent();
} else {
System.out.println("不是信件,采用其它处理方式~");
}
}
}
Object:所有类的超类
0bject
类是Java中所有类的始祖,Java中的每一个类都扩展了0bject
。但是并不需要这样写:
public class Employee extends Object
如果没有明确地指出超类,那么理所当然0bject
就是这个类的超类。由于在Java中每个类都是由0bject类扩展而来的,所以,熟悉这个类提供的服务十分重要。
如何证明这一点?从继承的特性入手。
Object类型的变量
可以使用Object类型的变量引用任何类型的对象:
package ex2;
public class Main {
public static void main(String[] args) {
MailItem item = new Letter("你好吗");
Recipient recipient = new Recipient();
recipient.processMailItem(item);
Object obj1 = item;
Object obj2 = recipient;
}
}
要想对其中的内容进行具体的操作,还需要清楚对象的原始类型,并进行相应的强制类型转换:
Object obj1 = item;
Object obj2 = recipient;
Letter letter = (Letter) obj1;
letter.openAndReadContent();
探究:为何输出ex2.Letter@1d81eb93
package ex2;
public class Main {
public static void main(String[] args) {
MailItem item = new Letter("你好吗");
Recipient recipient = new Recipient();
System.out.println(item); // => ex2.Letter@1d81eb93
System.out.println(recipient); // ex2.Recipient@7291c18f
}
}
从继承的特性入手,在集成开发环境中可以尝试通过ctrl
+点击对应的方法
阅读源码。
课后作业
根据下图创建继承层次结构:
当运行下面的代码时:
package ex3;
public class Main {
private static void checkCharacter(Character c) {
//这里自行填充
}
public static void main(String[] args) {
System.out.println("*****************************");
Character[] characters = new Character[5];
characters[0] = new Character("aa", 100);
characters[1] = new Warrior("bb", 100);
characters[2] = new Mage("cc", 100);
characters[3] = new IceMage("dd", 100);
characters[4] = new FireMage("ee", 100);
System.out.println("*****************************");
System.out.println(characters[3]);
System.out.println("*****************************");
for(Character c : characters) {
checkCharacter(c);
}
}
}
需要产生如下输出:
*****************************
Character构造器第1次被调用
Character构造器第2次被调用
Warrior构造器第1次被调用
Character构造器第3次被调用
Mage构造器第1次被调用
Character构造器第4次被调用
Mage构造器第2次被调用
IceMage构造器第1次被调用
Character构造器第5次被调用
Mage构造器第3次被调用
FireMage构造器第1次被调用
*****************************
我叫dd,我的生命值是100
*****************************
这是一个普通角色
这是一个战士
这是一个法师
这是一个法师
这是一个法师
该作业只能用之前讲过的方法实现,采用其它“高级方法”视为AI生成。