1. 文章
  2. Java 进阶线
  3. 对象与类型设计

多态、抽象类与接口:让业务依赖稳定的约定

更新于 21 分钟阅读 第 2 / 15 站 #java#java-advanced#day02

用饲养员与短信发送作业理解多态、接口能力和模板方法。

我的学习 →

Java 进阶线 · 线路目录
本页导读

day02|面向对象进阶(二)

本日延续 day01 的继承与 static,把面向对象三大特征补完:多态及其成员访问特点、类型转换、final 与常量、抽象类与抽象方法、模板方法设计模式,以及接口(含 JDK 8 之后新增的方法、接口多继承与使用注意事项)。学完后应能说清多态下的「编译期 / 运行期分工」,并能用 abstract 与 interface 把「稳定的约定」和「可替换的实现」分开。

本日 PPT 目录(第 2 页):多态(认识多态 / 使用多态的好处 / 多态下的类型转换问题)、final、枚举、抽象类、接口。目录中列有「枚举」,但本日正文未展开(第 10 页起的目录已去掉该项),枚举留待后续课程。

1. 多态

1.1 什么是多态

多态是在继承 / 实现情况下的一种现象,表现为对象多态与行为多态:

表现形式含义
对象多态父类类型的引用可以指向子类的对象
行为多态同一次方法调用,实际执行的是子类重写后的实现

多态的前提(三条同时满足):

  1. 有继承 / 实现关系;
  2. 存在父类引用指向子类对象;
  3. 存在方法重写。

注意事项:多态是对象、行为的多态,Java 中的属性(成员变量)不谈多态。

People p1 = new Student();   // 对象多态
p1.run();                    // 行为多态:执行 Student 重写的 run()

People p2 = new Teacher();
p2.run();                    // 执行 Teacher 重写的 run()

1.2 多态下的成员访问特点

「属性不谈多态」的具体含义是:成员变量的取值不跟随实际对象,而跟随变量的声明类型。结合课程代码 d1_polymorphism 的注释,可归纳为下表:

访问的内容编译阶段(看左边 = 声明类型)运行阶段
成员变量看左边,取父类中声明的值看左边
实例方法(被子类重写)看左边,确认父类中是否存在该方法看右边,执行子类重写的实现
静态方法看左边看左边
// 父类 Animal 中定义了 String name = "Animal名称"; 与 cry()
Animal a1 = new Cat();
a1.cry();                        // 方法:编译看左边,运行看右边,执行 Cat 重写的 cry()
System.out.println(a1.name);     // 成员变量:编译看左边,运行也看左边,输出 Animal名称

一句话记法:方法「编译看左边,运行看右边」;成员变量「编译看左边,运行也看左边」。

1.3 使用多态的好处

  1. 多态形式下,右边对象是解耦合的,更便于扩展和维护;
  2. 定义方法时使用父类类型的形参,可以接收一切子类对象,扩展性更强、更便利。
public static void main(String[] args) {
    Animal a = new Dog();   // 右边换成别的子类对象,左边代码不用改
    a.cry();

    go(new Cat());
    go(new Dog());
}

// 父类类型形参:一个方法可以接收一切子类对象
public static void go(Animal a) {
    System.out.println("开始。。。");
    a.cry();
    System.out.println("结束。。。");
}

1.4 多态带来的问题:不能调用子类独有功能

多态下变量的类型是父类,编译阶段只认父类中有的成员,因此不能直接调用子类的独有方法。

Animal a = new Dog();
a.cry();           // 可以:Animal 中定义了 cry()
// a.lookDoor();   // 报错:Animal 中没有 lookDoor(),它是 Dog 的独有功能

解决这一问题的办法就是类型转换。

1.5 多态下的类型转换问题

形式写法说明
自动类型转换父类 变量名 = new 子类();向上转型,子类对象赋给父类引用
强制类型转换子类 变量名 = (子类) 父类变量;向下转型,把对象换回它真正的类型

强制类型转换能把对象转换成其真正的类型,从而解决「多态下不能调用子类独有方法」的问题。

注意事项:

  • 只要存在继承 / 实现关系,强制类型转换在编译阶段不会报错;
  • 运行时,如果对象的真实类型与强转后的类型不同,会报类型转换异常 ClassCastException;
  • 因此强转前,Java 建议先使用 instanceof 关键字判断当前对象的真实类型,再进行强转。
Animal a = new Dog();
a.cry();

// 强制类型转换:解决多态下不能调用子类独有方法的问题
Dog d1 = (Dog) a;
d1.lookDoor();

// Cat c1 = (Cat) a;   // 编译不报错,运行时报 ClassCastException

// 方法内部:强转前先用 instanceof 判断对象的真实类型
public static void go(Animal a) {
    a.cry();
    if (a instanceof Dog) {
        ((Dog) a).lookDoor();
    } else if (a instanceof Cat) {
        ((Cat) a).catchFish();
    }
}

课件写法:对象 instanceof 类型,结果为 boolean。

(补充说明)instanceof 判断的是对象的真实类型,而不是变量的声明类型;它表达的是「对象是否具备该类型」。

1.6 本日目录对应的三个小结问答

  1. 使用多态有什么好处?存在什么问题?——可以解耦合、扩展性更强,父类类型形参可以接收一切子类对象;问题是多态下不能直接调用子类的独有方法。
  2. 类型转换有几种形式?能解决什么问题?——自动类型转换与强制类型转换;能把对象转换成其真正的类型,从而解决多态下不能调用子类独有方法的问题。
  3. 强制类型转换需要注意什么?——存在继承 / 实现关系即可强转,编译阶段不报错,运行时真实类型不符会报 ClassCastException;建议先用 instanceof 判断真实类型再强转。

2. final 关键字

2.1 final 的三种修饰位置

final 关键字是「最终」的意思,可以修饰类、方法、变量。

修饰对象特点
类该类被称为最终类,不能被继承
方法该方法被称为最终方法,不能被重写
变量该变量只能被赋值一次
// 1、final 修饰类:不能被继承
// final class A {}
// class B extends A {}     // 报错

// 2、final 修饰方法:不能被重写
// class C { public final void run() {} }
// class D extends C { @Override public void run() {} }   // 报错

public class FinalDemo {
    // 4、final 修饰静态成员变量:称为常量,值只有一个且不能被改变
    public static final String SCHOOL_NAME = "黑马程序员";

    public static void main(String[] args) {
        // 3、final 修饰变量:有且仅能赋值一次
        //    局部变量:方法内变量、形参、for 循环变量、构造器中的变量
        final int a = 12;
        // a = 15;                 // 报错:第二次赋值
        // SCHOOL_NAME = "黑马2";  // 报错:第二次赋值

        buy(0.7);
    }

    public static void buy(final double z) {
        // z = 0.1;                // 报错:第二次赋值
    }
}

2.2 final 修饰变量的注意点

  • final 修饰基本类型的变量:变量存储的数据不能被改变;
  • final 修饰引用类型的变量:变量存储的地址不能被改变,但地址所指向对象的内容是可以被改变的。

(补充说明)作业排错题中 public void addOne(final Other o) { o.i++; } 是合法的,被固定的是引用 o 本身,不是它所指向对象的字段。

2.3 补充知识:常量详解

使用 static final 修饰的成员变量就被称为常量,作用是通常用于记录系统的配置信息。

public class Constant {
    public static final String SCHOOL_NAME = "传智教育";
}

public class FinalDemo2 {
    public static void main(String[] args) {
        System.out.println(Constant.SCHOOL_NAME);
    }
}
要点说明
好处代码可读性更好,可维护性也更好
执行原理程序编译后,常量会被「宏替换」:出现常量的地方全部会被替换成它记住的字面量,这样可以保证使用常量和直接用字面量的性能是一样的
命名规范建议使用大写英文单词,多个单词之间用下划线连接

(补充说明)课件里常量的示例字面量为 "传智教育",配套课程代码中为 "黑马程序员",含义相同,只是配置内容不同。

3. 抽象类与抽象方法

3.1 什么是抽象类

Java 中有一个关键字 abstract,它就是抽象的意思,可以用它修饰类和成员方法:abstract 修饰类,这个类就是抽象类;修饰方法,这个方法就是抽象方法。

修饰符 abstract class 类名 {
    修饰符 abstract 返回值类型 方法名称(形参列表);
}

public abstract class A {
    // 抽象方法:必须用 abstract 修饰,只有方法签名,不能有方法体
    public abstract void test();
}

3.2 抽象类的注意事项与特点

  1. 抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类;
  2. 类该有的成员(成员变量、方法、构造器)抽象类都可以有;
  3. 抽象类最主要的特点:抽象类不能创建对象,仅作为一种特殊的父类,让子类继承并实现;
  4. 一个类继承抽象类,必须重写完抽象类的全部抽象方法,否则这个类也必须定义成抽象类。
public abstract class A {
    private String name;
    private int age;

    public A() {
    }

    public A(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 抽象方法:abstract 修饰,只能有方法签名,没有方法体
    public abstract void go();
    public abstract void run();

    // getter / setter 省略
}

// 继承抽象类,必须重写完全部抽象方法
public class B extends A {
    @Override public void go() { }
    @Override public void run() { }
}

(补充说明)作业简答题问「abstract 不能和哪些关键字共存」:抽象方法需要子类重写,所以抽象方法不能同时是 private(子类看不到,无法重写)、static(静态方法不参与重写)、final(final 不允许重写)。

3.3 使用抽象类的好处

需求:某宠物游戏需要管理猫、狗的数据。猫的数据有名字,行为是「喵喵喵的叫~」;狗的数据有名字,行为是「汪汪汪的叫~」。

父类知道每个子类都要做某个行为,但每个子类要做的情况不一样,父类就把这个行为定义成抽象方法,交给子类去重写实现。抽出这样的抽象类,就是为了更好地支持多态。

// 抽象父类 Animal:有 private String name 与 getter / setter,
// 并定义了抽象方法 public abstract void cry();
public class Dog extends Animal {
    @Override
    public void cry() {
        System.out.println(getName() + "汪汪汪的叫~~~");
    }
}

public class Cat extends Animal {
    @Override
    public void cry() {
        System.out.println(getName() + "喵喵喵的叫~~~");
    }
}

// 多态调用
Animal a = new Cat();
a.setName("小猫");
a.cry();     // 小猫喵喵喵的叫~~~

(补充说明)好处有两层:抽象方法的方法体没有意义时可以直接不写,代码更简洁;同时强制子类重写,保证多态调用时一定有真实实现。

3.4 抽象类的常见应用场景:模板方法设计模式

3.4.1 模板方法设计模式解决了什么问题

解决方法中存在重复代码的问题。 例如 A、B 两个类都有 sing(),内部都是「代码一样 → 代码不同 → 代码一样」,差异代码被夹在相同的骨架之间,只能整段复制。

public class A {
    public void sing() {
        // 代码一样
        // 代码不同    ← 只有这一段各子类不同
        // 代码一样
    }
}

public class B {
    public void sing() {
        // 代码一样
        // 代码不同
        // 代码一样
    }
}

3.4.2 模板方法设计模式的写法

  1. 定义一个抽象类;
  2. 在里面定义 2 个方法:
    • 一个是模板方法:把相同代码放里面去;
    • 一个是抽象方法:具体实现交给子类完成。

多学一招:建议使用 final 关键字修饰模板方法。因为模板方法是给对象直接使用的,不能被子类重写;一旦子类重写了模板方法,模板方法就失效了。

3.4.3 完整示例

public abstract class People {
    // 模板方法:流程固定,用 final 保护,子类不能重写
    public final void write() {
        System.out.println("\t\t\t《我的爸爸》");
        System.out.println("\t我的爸爸还是很棒的,他是这样的人,听我介绍一下");
        writeMain();   // 变化的部分,交给子类
        System.out.println("\t有这样的爸爸真好!");
    }

    // 抽象方法:正文的具体实现交给子类
    public abstract void writeMain();
}

public class Student extends People {
    @Override
    public void writeMain() {
        System.out.println("\t(学生自己写的正文)");
    }
}

public class Teacher extends People {
    @Override
    public void writeMain() {
        System.out.println("\t(老师自己写的正文)");
    }
}

// 写作文的步骤和架构统一:标题一样、第一段一样、正文随意发挥、结尾一样
People p = new Teacher();
p.write();

3.4.4 应用:黑马员工管理系统

需求:需要管理讲师、咨询师的数据。讲师的数据有姓名、年龄、具备的技能;咨询师的数据有姓名、年龄、回答问题的总人数。讲师和咨询师都需要提供打印个人介绍的方法。

模板方法模式的实现步骤:

  1. 定义一个抽象类;
  2. 在里面定义 2 个方法(模板方法放相同代码,抽象方法交给子类实现);
  3. 定义子类继承抽象类,重写抽象方法;
  4. 创建子类对象,调用模板方法完成功能。

4. 接口

4.1 接口概述

Java 提供了关键字 interface,用它可以定义出一个特殊的结构:接口。

  • 接口不能创建对象;
  • 接口是用来被类实现(implements)的,实现接口的类称为实现类;
  • 一个类可以实现多个接口(接口可以理解成「干爹」);
  • 实现类实现多个接口,必须重写完全部接口的全部抽象方法,否则实现类需要定义成抽象类。

课件说法:JDK 8 之前,接口中只能定义成员变量和成员方法。

// 接口的定义结构
public interface 接口名 {
    // 成员变量(常量)
    // 成员方法(抽象方法)
}

// 类实现接口的结构
修饰符 class 实现类 implements 接口1, 接口2, 接口3, ... {
}

public interface A {
    // 常量:可以省略 public static final 不写,默认会加上
    String SCHOOL_NAME = "黑马程序员";

    // 抽象方法:可以省略 public abstract 不写,默认会加上
    void run();
    void go();
}

public interface B {
    void eat();
}

// 实现类:实现多个接口,必须重写完全部接口的全部抽象方法,否则这个类必须是抽象类
public class BImpl implements A, B {
    @Override public void run() { }
    @Override public void go() { }
    @Override public void eat() { }
}

(补充说明)作业选择题的对应结论:接口没有提供构造器;接口中的方法默认使用 public abstract 修饰;接口中的成员变量默认使用 public static final 修饰;而「接口不允许多继承」是错误的说法(接口之间可以多继承,见 4.4)。

4.2 接口的好处(重点)

  • 弥补了类单继承的不足,一个类同时可以实现多个接口;
  • 让程序可以面向接口编程,这样程序员就可以灵活方便地切换各种业务实现(更利于程序的解耦合)。

(补充说明)「面向接口编程」是指调用方只依赖接口中声明的方法,运行期再传入不同的实现类对象,因此切换实现时不用修改主流程代码。

4.3 接口的应用案例:班级学生信息管理模块

需求:设计一个班级学生的信息管理模块。学生的数据有姓名、性别、成绩。功能 1 要求打印出全班学生的信息;功能 2 要求打印出全班学生的平均成绩。

业务实现是有多套方案的:

  • 第 1 套方案:能打印出班级全部学生的信息;能打印班级全部学生的平均分;
  • 第 2 套方案:能打印出班级全部学生的信息(包含男女人数);能打印班级全部学生的平均分(要求去掉最高分、最低分)。

要求:系统可以支持灵活地切换这些实现方案。

public class Student {
    private String name;
    private char sex;
    private double score;

    public Student(String name, char sex, double score) {
        this.name = name;
        this.sex = sex;
        this.score = score;
    }

    public String getName() { return name; }
    public char getSex() { return sex; }
    public double getScore() { return score; }
}

// 业务约定用接口描述
public interface ClassData {
    void printAllStudentInfos();
    void printAllStudentAverageScore();
}

// 第 1 套实现方案
public class ClassDataImpl1 implements ClassData {
    private ArrayList<Student> students;

    public ClassDataImpl1(ArrayList<Student> students) {
        this.students = students;
    }

    @Override
    public void printAllStudentInfos() {
        System.out.println("===展示全部学生信息===");
        for (int i = 0; i < students.size(); i++) {
            Student s = students.get(i);
            System.out.println(s.getName() + " " + s.getSex() + " " + s.getScore());
        }
    }

    @Override
    public void printAllStudentAverageScore() {
        System.out.println("===展示全部学生的平均分===");
        double allScore = 0;
        for (int i = 0; i < students.size(); i++) {
            allScore += students.get(i).getScore();
        }
        System.out.println(allScore / students.size());
    }
}

// 第 2 套实现方案:打印时区分男女人数,平均分去掉最高分、最低分
// (其余部分与 ClassDataImpl1 相同)
public class ClassDataImpl2 implements ClassData {
    private ArrayList<Student> students;

    public ClassDataImpl2(ArrayList<Student> students) {
        this.students = students;
    }

    @Override
    public void printAllStudentInfos() {
        System.out.println("===展示全部学生信息===");
        int count = 0;
        for (int i = 0; i < students.size(); i++) {
            Student s = students.get(i);
            System.out.println(s.getName() + " " + s.getSex() + " " + s.getScore());
            if (s.getSex() == '男') {
                count++;
            }
        }
        System.out.println("男人数:" + count);
        System.out.println("女人数:" + (students.size() - count));
    }

    // 平均分去掉一个最高分与一个最低分
    @Override
    public void printAllStudentAverageScore() {
        double allScore = 0;
        double max = students.get(0).getScore();
        double min = max;
        for (int i = 0; i < students.size(); i++) {
            double sc = students.get(i).getScore();
            allScore += sc;
            if (sc > max) { max = sc; }
            if (sc < min) { min = sc; }
        }
        System.out.println("最高分:" + max + ",最低分:" + min);
        System.out.println("平均分" + (allScore - max - min) / (students.size() - 2));
    }
}

// 测试类
public class Test {
    public static void main(String[] args) {
        ArrayList<Student> students = new ArrayList<>();
        students.add(new Student("木婉清", '女', 80));
        students.add(new Student("钟灵", '女', 95));
        students.add(new Student("虚竹", '男', 90));
        students.add(new Student("乔峰", '男', 100));

        ClassData data = new ClassDataImpl1(students);   // 多态:切换方案只改这一行
        data.printAllStudentInfos();
        data.printAllStudentAverageScore();
    }
}

(补充说明)「去掉最高分、最低分」的实现需要额外考虑:学生人数不足 3 人时除数会为 0 或为负;所有成绩相同时「去掉极值」的业务含义也要事先约定。

4.4 接口的多继承

接口可以继承多个接口,作用是便于实现类去实现。

// 一个接口可以同时继承多个接口
interface A extends B, C {
    void a();
}

interface B {
    void b();
}

interface C {
    void c();
}

// 接口的多继承让实现类只实现一个接口,就相当于实现了很多个接口
class D implements A {
    @Override public void a() { }
    @Override public void b() { }
    @Override public void c() { }
}

课件写法:public interface C extends B, A { }。

类别关系小结:类与类是单继承的(一个类只能直接继承一个父类);类与接口是多实现的(一个类可以同时实现多个接口);接口与接口是多继承的(一个接口可以同时继承多个接口)。

4.5 JDK 8 开始,接口中新增的三种方法

JDK 8 开始,接口新增了三种形式的方法:

方法类型修饰符起始版本调用方式
默认方法(实例方法)default(默认加 public)JDK 8接口的实现类对象
静态方法(类方法)static(默认加 public)JDK 8只能接口名调用
私有方法privateJDK 9只能在接口内部被调用

为什么新增这些方法:增强了接口的能力,更便于项目的扩展和维护。

public interface A {
    // 默认方法(实例方法):使用 default 修饰,只能使用接口的实现类对象调用
    default void run() {
        go();
        System.out.println("跑的贼快~~~");
    }

    // 私有方法:必须用 private 修饰(JDK 9 开始才支持),只能在接口内部被调用
    private void go() {
        System.out.println("开始跑~~");
    }

    // 类方法(静态方法):使用 static 修饰,只能用接口名来调用
    static void inAddr() {
        System.out.println("我们都听磊哥吹 Java!");
    }
}

public class B implements A {
}

public class Test {
    public static void main(String[] args) {
        B b = new B();
        b.run();      // 默认方法:用实现类对象调用

        A.inAddr();   // 静态方法:用接口名调用
    }
}

课件说法:标题写「JDK 8 开始,接口中新增了三种方法」;更准确的表述:JDK 8 新增的是默认方法与静态方法,private 方法从 JDK 9 才开始支持。复习时按上表区分版本。

4.6 使用接口的注意事项(了解)

  1. 一个接口继承多个接口,如果多个接口中存在方法签名冲突,则此时不支持多继承;
  2. 一个类实现多个接口,如果多个接口中存在方法签名冲突,则此时不支持多实现;
  3. 一个类继承了父类,又同时实现了接口,父类中和接口中有同名的默认方法,实现类会优先用父类的;
  4. 一个类实现了多个接口,多个接口中存在同名的默认方法,可以不冲突,这个类重写该方法即可。
// 3、父类与接口有同名默认方法:优先用父类的
class Animal {
    public void run() {
        System.out.println("动物跑的贼溜~~");
    }
}

interface Go {
    default void run() {
        System.out.println("动物 go 的贼快~~");
    }
}

class Cat extends Animal implements Go {
    public void test() {
        run();            // 父类的
        Go.super.run();   // 接口的:接口名.super.方法名
    }
}

// 4、多个接口有同名默认方法:实现类重写即可,也可用 接口名.super 分别调用
//    (A3、B3 都声明了 default void run())
class C3 implements A3, B3 {
    @Override
    public void run() {
        System.out.println("C3 - run");
        A3.super.run();
        B3.super.run();
    }
}

// 1、2、方法签名冲突的情形(编译报错)
// interface A1 { String run(); }
// interface B1 { void run(); }
// interface C1 extends A1, B1 {}   // 签名冲突,不支持多继承
// class C1 implements A2, B2 {}    // 签名冲突,不支持多实现

4.7 接口与抽象类的区别

对比项抽象类接口
关键字abstract classinterface
能否创建对象不能不能
继承 / 实现关系类之间是单继承一个类可以多实现,接口之间可以多继承
成员变量各种成员变量都可以有只能是常量,默认 public static final
构造器可以有构造器没有构造器
成员方法抽象方法 + 普通方法抽象方法 + 默认 / 静态 / 私有方法(JDK 8 起)
表达的关系「是不是」:共享状态、固定流程「有没有这种能力」:可组合的行为约定
常见用途模板方法设计模式等面向接口编程、解耦合、灵活切换实现

(补充说明)课件本日正文没有单独列出这张对照表,上表依据本日 PPT 中抽象类与接口各自的规则归纳,并对照作业简答题「接口和抽象类有何区别」整理。

5. 作业与练习

作业文档:day02-面向对象进阶(二)/day02作业/day02作业.md

  • 一、选择题(12 题):接口描述与接口定义合法性、super 用法、重载与重写的分派结果、多态前提、编译错误判断、instanceof 判断、抽象类描述、抽象类子类定义、构造器与继承、Parent p = new Parent() 能否编译等。
  • 二、今日单词:instanceof、super、abstract、final、interface、implement。
  • 三、简答题(6 题):多态及其好处 / 弊端 / 解决方式;abstract 不能与哪些关键字共存;抽象类与普通父类的区别;final 各修饰位置的特点;调用父类构造器的方式与目的;接口与抽象类的区别。
  • 四、排错题(3 题):两个接口都有 static 与 default 方法时接口多继承是否可行;子类同时继承父类字段与实现接口常量时的名称歧义;final 修饰引用类型形参能否修改其内部字段。
  • 五、代码题(3 题):动物 / 游泳接口 / 饲养员(多态 + 接口能力判断);TV 与遥控器(面向对象综合);选做题「短信验证码发送」用模板方法设计模式抽取公共流程。

排错题要点提示:

  • 排错题 1:interface C extends A, B,两个父接口都定义 static void method() 与 default void show()。静态方法属于各自接口、不参与实现类的继承冲突;但两个同名的 default void show() 会让 C 产生默认方法冲突,需要在子接口(或最终实现类)中重写 show() 来解决。
  • 排错题 2:class C extends B implements A 同时拿到父类字段 B.x 与接口常量 A.x,直接写 x 会产生名称歧义,需要明确写成 super.x 或 A.x。
  • 排错题 3:final Other o 只固定引用 o 本身,o.i++ 修改的是对象内部字段,因此是合法的。

6. 复习要点

  1. 多态的前提三条:有继承 / 实现关系、父类引用指向子类对象、存在方法重写;表现为对象多态与行为多态;成员变量不谈多态。
  2. 多态下的成员访问:实例方法「编译看左边、运行看右边」,成员变量与静态方法「编译看左边、运行也看左边」。
  3. 多态的好处:右边对象解耦合、父类形参可接收一切子类对象;弊端:不能直接调用子类独有功能。
  4. 类型转换两种形式:自动类型转换(向上)与强制类型转换(向下);有继承 / 实现关系就能编译通过,运行时真实类型不符抛 ClassCastException;强转前先用 instanceof 判断。
  5. final 修饰类 / 方法 / 变量分别表示不可继承 / 不可重写 / 只能赋值一次;final 引用固定的是地址,对象内容仍可改变。
  6. 常量是 static final 成员变量,用于记录系统配置信息;编译后会被宏替换(字面量内联);命名建议全大写 + 下划线。
  7. 抽象类不能创建对象,可以有成员变量、方法、构造器;抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类;继承抽象类的类必须重写全部抽象方法,否则也要定义成抽象类。
  8. 抽象方法不能与 private、static、final 共存。
  9. 模板方法设计模式的写法:抽象类中定义模板方法(相同代码与固定流程)与抽象方法(变化部分),建议用 final 修饰模板方法,防止流程被重写破坏。
  10. 接口用 interface 定义、被类 implements 实现;接口不能创建对象、没有构造器;成员变量隐含 public static final,方法隐含 public abstract;一个类可实现多个接口,接口之间可多继承。
  11. 接口的好处:弥补类单继承的不足;支持面向接口编程,便于切换实现、解耦合。
  12. JDK 8 起接口支持默认方法与静态方法,JDK 9 起支持私有方法;默认方法用实现类对象调用,静态方法只能用接口名调用。
  13. 接口使用注意事项:方法签名冲突时既不能多继承也不能多实现;父类与接口同名默认方法优先用父类的;多个接口同名默认方法需在实现类重写(可用 接口名.super.方法名 分别调用)。