超类与子类续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生成。

posted @ 2024-11-01 11:12  xkfx  阅读(243)  评论(0编辑  收藏  举报