Day 3 · 继承、多态、抽象类与接口
继承
public class 子类名 extends 父类名 {
}- Python 里靠 MRO(方法解析顺序) 决定执行链,类和类可以多继承(能继承两个父类)
- Java 里类和类是单继承(只能继承一个)
继承注意事项
Java 中如果 C 想同时拥有 A 和 B 的能力,只能链式继承(先继承 B 再让 B 继承 A)。 不过 Java 里确实存在多继承,只是发生在接口和接口之间:
- 类与类:单继承
- 类与接口:多实现
- 接口与接口:多继承
补充整理:链式继承不是「多继承的替代品」,硬凑
A → B → C会把不相关的类绑死, 是典型的误用。Java 故意不支持类多继承,是为了避开菱形继承带来的字段 / 方法冲突。 正确思路是「组合优于继承」:把能力拆成接口或成员字段,需要什么就实现 / 持有哪个。 另外别为了复用几个方法就继承——先问一句「子类 is-a 父类吗」,答案不是就别用继承。
Object 类
补充整理:原稿这里只有标题。补一句定位:
java.lang.Object是所有类的最终父类, 没写extends的类默认继承它,因此任何对象都能调用toString()/equals()/hashCode()/getClass()等方法,也都能被Object类型的变量接收(这正是多态的基础)。 它的五个常用方法、为什么要重写equals和hashCode,放在 Day 4 · 常用核心类 里展开。
super
super 是当前对象里的父类引用区域,并不是一个实际存在的独立对象。 主要用来解决:
- 变量的隐藏(子类有和父类同名的字段)
- 方法的重写(子类覆盖父类方法后还想调父类版本)
super()
子类没有继承父类的构造器。但子类调用自己的构造器创建对象时,也会调用父类的无参构造器—— 因为无参构造器负责初始化成员变量,而子类继承了父类的成员变量,所以这部分必须由父类自己初始化。
super();调用父类的无参构造器(不写也默认有)- 必须写在构造器第一行
- 若父类只手动写了有参构造器、没有无参构造器,就必须显式调用有参的,例如
super("张三"); this();调用本类的其他构造器(有参或无参)
this 与 super 的区别
| 用法 | this | super |
|---|---|---|
| 访问成员变量 | this.变量名 优先找本类成员;本类没有,向上父类查找 | super.变量名 直接访问父类的成员变量,跳过子类同名变量(解决变量隐藏) |
| 访问成员方法 | this.方法() 优先本类方法;找不到向上找父类 | super.方法() 直接调用父类版本方法,跳过子类重写方法 |
| 调用构造器 | this(...) 调用本类其他构造方法 | super(...) 调用父类构造方法 |
| 使用位置 | 只能放在构造方法第一行 | 只能放在构造方法第一行 |
补充整理:因为两者都要求「第一行」,所以
this(...)和super(...)不能在同一个构造器里同时出现。 另外this(...)链式调用最终一定会走到某个含super(...)(隐式或显式)的构造器, 保证父类初始化一定会执行且只执行一次。补充两个原稿没写的用法:
this单独作为当前对象引用可以当参数传递、可以return this做链式调用;而super不能单独当对象用(return super;非法),它只是访问父类成员的关键字。
方法重写
方法重载 ≠ 方法重写:
- 方法重载(overload):在同一个类中,方法名相同、参数列表不同
- 方法重写(override):在子父类中出现相同的方法,因为父类功能不能满足子类需求了
@Override
标记当前方法是一个重写方法。注意事项:
- 必须存在继承关系,有继承才能重写
- 子类方法和父类方法方法名必须一样、参数列表一样(父类什么样,子类也得什么样)
- 返回值类型:可以和父类一样,也可以是父类返回值的子类,但不能是父类的父类(协变返回)
- 修饰符:子类的修饰符不能比父类更严格
- 异常:子类抛出的异常不能比父类大
补充整理:
@Override不写也能重写,但强烈建议一律写上—— 编译器会帮你校验,一旦把方法名拼错、参数写错(一不小心变成重载),会直接报错, 否则这个 bug 能藏很久。「修饰符不能更严格」指访问修饰符:父类
protected,子类不能改成private或包私有, 只能保持protected或放宽到public。异常同理:父类声明抛IOException, 子类不能抛Exception,可以抛FileNotFoundException或不抛。
哪些方法不能被重写
- 私有方法:私有方法没有被继承,所以不能重写
- 静态修饰的方法:属于类,不属于对象,静态方法是「隐藏」不是「重写」
- final 修饰的方法:final 表示最终功能,不能被重写
多态
一个事物的多种表现形式。例:USB 接口都能存储数据,但 U 盘、移动硬盘各存各的。
三要素
- 要有继承关系
- 子类重写方法
- 父类引用指向子类对象:
Father father = new Son()
多态访问成员方法
流程:编译看左边,执行看右边。 先看等号左边(父类)有没有这个方法,没有就编译报错;运行时看右边(子类),右边没有再沿继承关系向上找。
多态访问成员变量
成员变量没有方法重写,算「变量覆盖」。
不满足方法重写,编译看左边,运行也看左边。 子类有没有这个变量没有任何影响,运行时取的是
father那一侧的字段。
多态访问静态成员方法
静态方法没有重写,不满足三要素的第二个条件。
编译看左边,运行看左边。
补充整理:把这三种情况放一起记最清楚,唯一「运行看右边」的只有普通实例方法:
访问目标 编译 运行 实例方法 看左边 看右边(重写生效) 成员变量 看左边 看左边 静态方法 看左边 看左边 private/final方法看左边 看左边 这也解释了为什么字段一般不对外暴露、要
private+ getter:字段没有多态, 一旦父类和子类定义了同名字段,读到的值会取决于变量的声明类型而不是真实对象,非常反直觉。
多态应用场景
补充整理:原稿只有标题,补两个最常见的落地场景——
- 父类 / 接口作为参数,一个方法服务所有子类:
public void pay(Payment p) { p.pay(); },以后加支付宝、微信支付都不用改这个方法。- 集合统一存放:
List<Animal> list里同时放Dog、Cat,遍历时统一调a.eat(),各自执行各自的实现。框架里的依赖注入、JDBC 的
Connection/Statement、Servlet 的HttpServletRequest全是这个套路:面向接口编程,运行时才决定具体是哪一种实现。
多态好处和弊端
好处
- 提高代码的维护性(继承保证)
- 提高代码的扩展性(多态保证)
弊端
- 用不了子类的特有成员
- 解决办法:向下强制类型转换
补充整理:向下转型前一定要先判断,否则抛
ClassCastException。 这也是instanceof存在的意义(下一节)。JDK 16 起可以用模式匹配一步到位:javaif (animal instanceof Dog d) { d.watchHouse(); // d 已是 Dog 类型,不用再强转 }顺带一提:如果代码里到处都是
instanceof+ 强转,通常说明抽象没设计好, 更该考虑把那个「特有行为」提到父类或接口里。
instanceof 关键字
用来判断对象的类型是什么,主要是为了避免类型转换异常。
补充整理:
obj instanceof Type在obj为null时返回false(不抛异常), 且要求两者在继承树上有关系,毫无关系的两个类型直接编译报错。 注意它判断的是运行时真实对象的类型,不是变量声明类型。
final 关键字
最终的、不可变的,可以修饰变量、方法、类。
final方法可以继承,但不能重写final修饰的类是最终形态,不会有子类;给类加final目的就是不让有子类
补充整理:三种用法的语义其实不完全一样——
修饰对象 效果 变量 值 / 引用只能赋值一次(引用不可变,但引用指向的对象内部仍可改) 方法 不能被重写,但仍可继承、可重载 类 不能被继承(如 String、Math、所有包装类)「
final对象内容不可变」是常见误解:final List<String> list只是不能再list = ..., 照样能list.add(...)。要真正不可变得用List.of(...)或Collections.unmodifiableList。 另外final字段必须在声明时、实例块中或每个构造器里完成赋值,编译器会检查。
抽象类
当父类里的某个方法,每个子类都用得上但实现各不相同,就把它定义成抽象方法; 而抽象方法必须定义在抽象类里。
抽象方法没有方法体;也就是说,定义类的时候无法确定方法的功能,就定义成抽象方法。
注意:
继承抽象类的类,要么是普通子类(必须重写父类中的所有抽象方法)
要么是抽象子类:不会强制要求实现父类的抽象方法,可以手动写
抽象类和抽象方法都用
abstract修饰抽象类不能创建对象(不能
new)抽象类中既可以有抽象方法,也可以有普通方法
抽象类的成员特点
- 抽象类有构造器,但构造器不能用
abstract修饰(用来给子类初始化父类成员)
抽象类的引用场景:定义父类、抽取子类共性的时候。
补充整理:抽象类 vs 接口怎么选,一句话—— 抽象类回答「是什么」(is-a,共享状态和代码),接口回答「能做什么」(can-do,只定契约)。
维度 抽象类 接口 继承 单继承 多实现 / 接口间多继承 成员变量 普通字段、静态字段都行 只能是 public static final常量构造器 有 没有 方法 抽象方法 + 普通方法 抽象方法 + default+static+private若父类里既有要复用的字段 / 方法实现,又有要求子类各自实现的行为,用抽象类; 只是想给一批不相关的类加同一种能力(可比较、可序列化、可关闭),用接口。
接口的子类
语法上可以理解成更严格的抽象类,更倾向表达「你能做什么」。 用 interface 关键字定义,对外提供规则。
- 普通子类实现接口 →
implements,强制重写接口中所有抽象方法 - 抽象子类实现接口 → 不必重写方法,可以留给它的子类去实现
接口注意事项
| 要点 | 说明 |
|---|---|
接口不能 new | 只能由实现类创建对象 |
| 多实现 | 子类和接口可以多实现 |
| 多继承 | 接口和接口可以多继承 |
接口的成员特点
- 成员变量:只能是静态常量,默认修饰符
public static final(就算不写,编译器自动补上) - 成员方法:只能是抽象方法,必须由
public abstract修饰 - 构造方法:没有。接口中的变量全部是
public static final常量,属于接口、在方法区,不存在实例变量,不需要构造器去初始化实例成员
补充整理:原稿这三条是 JDK 7 及以前的规则,JDK 8 之后接口已经能放方法实现了; 另外「实现抽象类 / 继承接口」那两句也写反了,正确说法是 类
implements接口、接口extends接口、类extends类。按现行 JDK 整理如下:
成员 现在的规则 字段 一律 public static final,必须初始化,属于接口本身抽象方法 默认 public abstract,实现类必须全部重写(除非是抽象类)default方法JDK 8 起可有方法体,实现类可直接用也可重写 static方法JDK 8 起可有方法体,只能通过 接口名.方法名()调用,不被实现类继承private方法JDK 9 起可用,给接口内部的 default方法抽公共逻辑构造器 没有(因为无需初始化实例字段) 还有两处措辞更新:常量现在随类元数据存放在元空间(Metaspace), 「方法区」是 JDK 8 之前的概念;
public static final这些修饰符可以省略但不能改 (写成private会编译报错)。
default 方法
JDK 1.8 新特性。子类可以直接使用默认方法,也可以重写它。
补充整理:
default方法最大的价值是接口演进——给老接口加新方法时, 所有实现类不必跟着改,不会一升级就全网编译报错(Collection.stream()就是这么加进来的)。冲突规则要记:一个类实现的两个接口有同名
default方法时,必须在实现类里重写该方法来消除歧义; 若父类的方法和接口的default方法重名,类优先(class wins),走父类的实现。 想显式调用某个接口的默认实现,写接口名.super.方法名()。
接口的静态方法
补充整理:原稿只有标题,补一条。接口静态方法用
接口名.方法名()调用, 不能通过实现类或实现类对象调用(这点与类的静态方法不同),也不能被重写。 典型用途是把和接口强相关的工厂 / 工具方法收在接口旁边,比如Comparator.comparing(...)、List.of(...)、Stream.of(...), 这样就不用再单独造一个XxxUtils类。
内部类
成员内部类
定义在成员变量的位置(类中、方法外,非 static)。必须先创建外部类对象:
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();对象名
.new内部类(),类要用必须是对象。
静态成员内部类
同样在成员变量的位置,但用 static 修饰,new 放前面:
Outer.Inner inner = new Outer.Inner();局部内部类
把类定义到方法中(局部变量的位置),在方法里创建类对象,比较少见。
匿名内部类
没有类名,定义类和创建对象一步完成,只能用一次的局部内部类,用一次就会被标记为待回收。 常用来操作接口。
原来要操作接口得分两步:
- 定义子类实现接口
- 创建子类对象
用匿名内部类可以合成一步:
new 接口名() {
@Override
public void 重写的方法() {
// 方法体
}
};这一整个就相当于一个对象,即接口的子类对象。接口不能创建对象,要通过类实现接口。
注意:这个「匿名」指的是没有类名,对象是可以有名字的:
父类引用 = new 子类() { ... }; // 多态:先看左边再看右边这个匿名内部类可以是接口、抽象类,也可以是普通类(相应地把要重写的方法写上)。
补充整理:原稿说「用一次就会被标记为待回收」不太准确—— 匿名内部类对象就是个普通对象,是否回收取决于还有没有引用指向它。 把它赋给一个变量、或注册成监听器,它就会一直活着;只有创建后立刻用完、没人引用才会被回收。
另外两个实战要点:
- JDK 8 之前,匿名内部类 / 局部内部类访问的外部局部变量必须显式写
final; JDK 8 起只要「事实上不变」(effectively final)即可,编译器自动补。原因是变量值被 拷贝了一份存进内部类,若外部还能改就会出现两份不一致。- 只有一个抽象方法的接口(函数式接口),优先用 Lambda 代替匿名内部类, 代码量差好几倍——这正是 Day 4 的内容。
