多态、抽象类与接口:让业务依赖稳定的约定
用饲养员与短信发送作业理解多态、接口能力和模板方法。
继续上次小节 →Java 进阶线 · 线路目录
Java 进阶线
对象与类型设计3 站
文件、配置与并发4 站
网络、框架与复习3 站
本页导读
day02|面向对象进阶(二)
本日延续 day01 的继承与 static,把面向对象三大特征补完:多态及其成员访问特点、类型转换、final 与常量、抽象类与抽象方法、模板方法设计模式,以及接口(含 JDK 8 之后新增的方法、接口多继承与使用注意事项)。学完后应能说清多态下的「编译期 / 运行期分工」,并能用 abstract 与 interface 把「稳定的约定」和「可替换的实现」分开。
本日 PPT 目录(第 2 页):多态(认识多态 / 使用多态的好处 / 多态下的类型转换问题)、final、枚举、抽象类、接口。目录中列有「枚举」,但本日正文未展开(第 10 页起的目录已去掉该项),枚举留待后续课程。
1. 多态
1.1 什么是多态
多态是在继承 / 实现情况下的一种现象,表现为对象多态与行为多态:
| 表现形式 | 含义 |
|---|---|
| 对象多态 | 父类类型的引用可以指向子类的对象 |
| 行为多态 | 同一次方法调用,实际执行的是子类重写后的实现 |
多态的前提(三条同时满足):
- 有继承 / 实现关系;
- 存在父类引用指向子类对象;
- 存在方法重写。
注意事项:多态是对象、行为的多态,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 使用多态的好处
- 多态形式下,右边对象是解耦合的,更便于扩展和维护;
- 定义方法时使用父类类型的形参,可以接收一切子类对象,扩展性更强、更便利。
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 本日目录对应的三个小结问答
- 使用多态有什么好处?存在什么问题?——可以解耦合、扩展性更强,父类类型形参可以接收一切子类对象;问题是多态下不能直接调用子类的独有方法。
- 类型转换有几种形式?能解决什么问题?——自动类型转换与强制类型转换;能把对象转换成其真正的类型,从而解决多态下不能调用子类独有方法的问题。
- 强制类型转换需要注意什么?——存在继承 / 实现关系即可强转,编译阶段不报错,运行时真实类型不符会报
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 抽象类的注意事项与特点
- 抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类;
- 类该有的成员(成员变量、方法、构造器)抽象类都可以有;
- 抽象类最主要的特点:抽象类不能创建对象,仅作为一种特殊的父类,让子类继承并实现;
- 一个类继承抽象类,必须重写完抽象类的全部抽象方法,否则这个类也必须定义成抽象类。
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 模板方法设计模式的写法
- 定义一个抽象类;
- 在里面定义 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 应用:黑马员工管理系统
需求:需要管理讲师、咨询师的数据。讲师的数据有姓名、年龄、具备的技能;咨询师的数据有姓名、年龄、回答问题的总人数。讲师和咨询师都需要提供打印个人介绍的方法。
模板方法模式的实现步骤:
- 定义一个抽象类;
- 在里面定义 2 个方法(模板方法放相同代码,抽象方法交给子类实现);
- 定义子类继承抽象类,重写抽象方法;
- 创建子类对象,调用模板方法完成功能。
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 | 只能接口名调用 |
| 私有方法 | private | JDK 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 使用接口的注意事项(了解)
- 一个接口继承多个接口,如果多个接口中存在方法签名冲突,则此时不支持多继承;
- 一个类实现多个接口,如果多个接口中存在方法签名冲突,则此时不支持多实现;
- 一个类继承了父类,又同时实现了接口,父类中和接口中有同名的默认方法,实现类会优先用父类的;
- 一个类实现了多个接口,多个接口中存在同名的默认方法,可以不冲突,这个类重写该方法即可。
// 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 class | interface |
| 能否创建对象 | 不能 | 不能 |
| 继承 / 实现关系 | 类之间是单继承 | 一个类可以多实现,接口之间可以多继承 |
| 成员变量 | 各种成员变量都可以有 | 只能是常量,默认 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. 复习要点
- 多态的前提三条:有继承 / 实现关系、父类引用指向子类对象、存在方法重写;表现为对象多态与行为多态;成员变量不谈多态。
- 多态下的成员访问:实例方法「编译看左边、运行看右边」,成员变量与静态方法「编译看左边、运行也看左边」。
- 多态的好处:右边对象解耦合、父类形参可接收一切子类对象;弊端:不能直接调用子类独有功能。
- 类型转换两种形式:自动类型转换(向上)与强制类型转换(向下);有继承 / 实现关系就能编译通过,运行时真实类型不符抛
ClassCastException;强转前先用instanceof判断。 final修饰类 / 方法 / 变量分别表示不可继承 / 不可重写 / 只能赋值一次;final引用固定的是地址,对象内容仍可改变。- 常量是
static final成员变量,用于记录系统配置信息;编译后会被宏替换(字面量内联);命名建议全大写 + 下划线。 - 抽象类不能创建对象,可以有成员变量、方法、构造器;抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类;继承抽象类的类必须重写全部抽象方法,否则也要定义成抽象类。
- 抽象方法不能与
private、static、final共存。 - 模板方法设计模式的写法:抽象类中定义模板方法(相同代码与固定流程)与抽象方法(变化部分),建议用
final修饰模板方法,防止流程被重写破坏。 - 接口用
interface定义、被类implements实现;接口不能创建对象、没有构造器;成员变量隐含public static final,方法隐含public abstract;一个类可实现多个接口,接口之间可多继承。 - 接口的好处:弥补类单继承的不足;支持面向接口编程,便于切换实现、解耦合。
- JDK 8 起接口支持默认方法与静态方法,JDK 9 起支持私有方法;默认方法用实现类对象调用,静态方法只能用接口名调用。
- 接口使用注意事项:方法签名冲突时既不能多继承也不能多实现;父类与接口同名默认方法优先用父类的;多个接口同名默认方法需在实现类重写(可用
接口名.super.方法名分别调用)。