Inheritance and Polymorphism⚓︎
约 2544 个字 192 行代码 预计阅读时间 15 分钟
Inheritance⚓︎
继承(inheritance) 是面向对象设计方法论的重要组成部分,是一种将一个类的行为或实现定义为另一个类的超集的能力。通过继承,可以在类之间分享字段和方法。而在 Java 中,继承自然是一个关键的技术。
继承的语法如下:
-
继承与构造函数 (ctor)
- 将继承特性视为嵌入的对象
- 子类对象的一部分就是超类对象
- 因此超类的那部分必须在子类初始化之前进行初始化,也就是说要先构造好基类
- 如果没有向基类显式传递参数,那么就会调用默认构造函数
- 对于带参的超类构造函数,使用
super
关键字来调用这个构造函数并传参
-
没有名称隐藏(name hiding):Java 允许子类定义和超类名称相同但参数列表不同的方法,此时这个方法不会覆盖超类的同名方法
-
不会被继承的东西:
- 构造函数
- 私有数据被隐藏,但仍然存在
-
初始化和类加载
- TBD
-
向上转型(upcasting):
- 将对象句柄当作其基类型的句柄来处理
- 子类和超类的关系为:新类是现有类的一种类型
-
方法调用绑定(method call binding):连接方法调用与方法体,应调用哪个函数?
- 静态绑定(static binding):按代码调用函数
- 动态绑定(dynamic binding):调用对象的函数
-
覆盖(override)
- 当超类和子类的方法名和函数均相同时,子类方法会覆盖超类方法
- 对字段、构造函数和私有方法不适用
Abstract Functions and Abstract Classes⚓︎
- 抽象类(abstract class) 是为了创建一个对所有从它派生的类的公共接口
- 抽象方法(abstract method) 是不完整的,只有声明,没有方法体;包含抽象方法的类就是抽象类
-
接口(interface):完全的抽象类
- 接口中的所有方法都是
public
的 - 接口中所有的数据成员都是
public static final
-
定义接口
-
从接口继承
-
接口可作为类使用
- 一个接口可以继承自多个接口,但不能继承自类
- 一个类可实现多个接口
- 接口中的所有方法都是
-
default
:接口可以拥有包含函数体的“默认”函数public interface Greeting { // 普通抽象方法 void sayHello(String name); // default 方法:带默认实现,实现类可复用也可覆盖 default void sayHi(String name) { System.out.println("Hi, " + name + " (from default method)"); } } class EnglishGreeting implements Greeting { @Override public void sayHello(String name) { System.out.println("Hello, " + name); } }
-
默认方法和子类化
public interface Parent { public void message(String body); public default void welcome() { message("Parent: Hi!"); } public String getLastMessage(); } public interface Child extends Parent { public default void welcome() { message("Child: Hi!"); } }
-
更深层的情况:
-
-
多继承(multiple inheritance):接口支持多重继承,可能会遇到两个接口都提供了具有相同签名的默认方法的情况
public interface Jukebox { public default String rock() { return "... all over the world!"; } } public interface Carriage { public default String rock() { return "... from side to side"; } } public class MusicalCarriage implements Carriage, Jukebox { @Override public String rock() { return Carriage.super.rock(); } }
-
三条法则:
- 任何类都优于任何接口。所以如果超类链中有一个带有方法体或抽象声明的函数,我们可以完全忽略接口。
- 子类型优于超类型。如果我们有两个接口抢着提供默认方法,并且其中一个接口扩展另一个接口,此时子类获胜。
- 没有规则 3。如果前两条规则不能给我们答案,子类必须实现该方法或声明它为抽象。
-
接口中的静态方法
- 属于接口本身,不被实现类继承,只能通过接口名调用,必须带方法体
- 默认
public
,不允许写abstract
,default
,synchronized
- 不能被实现类覆盖(因为压根不属于实现类)
例子
public interface MathUtils { int add(int a, int b); // 普通抽象方法 static int max(int a, int b) { // 静态方法:工具方法,直接挂在接口上 return a >= b ? a : b; } } class Demo implements MathUtils { @Override public int add(int a, int b) { return a + b; } public static void main(String[] args) { // 1. 抽象方法必须通过实例调用 Demo d = new Demo(); System.out.println(d.add(3, 4)); // 7 // 2. 静态方法只能通过接口名调用 System.out.println(MathUtils.max(5, 2)); // 5 // 3. 以下写法均编译错误 // d.max(5, 2); // 错误:静态方法不属于实现类 // Demo.max(5, 2); // 错误 } }
POJO and Records⚓︎
- POJO(简单老式 Java 对象 (plain old Java object))是一种设计约定 / 编程风格
: “除了业务字段和普通的 getter/setter,不依赖任何特定框架的接口、注解或继承” - POJO = 只有私有字段 + 无参构造 + getter/setter + 不继承 / 实现框架类的纯净对象
- 2005 年 Gavin King 提出 POJO 概念,倡导“业务对象应该干净,框架功能通过配置或 AOP 织入”,随后 Hibernate、Spring 崛起,POJO 成为主流
例子
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) { this.x = x; this.y = y; }
public int x() { return x; }
public int y() { return y; }
@Override public boolean equals(Object o) { ... }
@Override public int hashCode() { ... }
@Override public String toString() { ... }
}
字段都是 final
,类也是 final
。
记录(record):
- 编译器自动帮你生成:
- 两个
private final
字段x
,y
- 全参构造器
Point(int x, int y)
- 只读访问器(不叫
getX()
,就叫x()
) equals
,hashCode
, toString
(按字段顺序实现)- 类本身被
final
修饰,不能再继承
- 两个
- 记录是一种“数据载体”类的语法糖(从 Java 14 开始引入)
- 帮你少写样板代码,专门用来干净地、不可变地保存一组值
-
追加自定义:
record Point(int x, int y) { // 1. 自定义构造器(必须最终调用自动生成的全参构造) public Point { if (x < 0 || y < 0) throw new IllegalArgumentException(); } // 2. 额外方法 public double distance(Point p) { return Math.sqrt(Math.pow(this.x - p.x, 2) + Math.pow(this.y - p.y, 2)); } // 3. 实现接口 public static final Comparator<Point> BY_X = Comparator.comparingInt(Point::x); }
- 不能显式再声明字段(只能追加
static
字段) - 不能继承别的类(且已经隐式
final
)
- 不能显式再声明字段(只能追加
-
适用场景:
- DTO / VO(接口返回体、MapStruct 映射)
- 复合主键、坐标、经纬度、RGB 值等“小对象”
- 函数式代码里需要快速组合 / 解构的数据块
- 语义清晰,线程安全,自动
equals
/hashCode
/toString
,是 Java 走向现代简洁语法的重要一步
Enums⚓︎
语法:
- 运算
- 继承
- Java 的
enum
是一种类 enum
中的常量项是这种类的固定对象enum
不能new
对象
enum
可以有成员变量和成员函数enum
可以有构造函数,对象创建时可以指定构造函数参数- 对象创建时可以声明匿名子类
-
用途:
- 单件模式:
enum
的对象在类装载时创建,保证其唯一性 - 命令驱动模式:
enum
可以方便地从字符串转换成枚举量,从而展开进一步的运算
- 单件模式:
-
valueOf()
- Java 为每个枚举自动生成的静态工具方法,用来把字符串转成对应的枚举常量
Inner Classes⚓︎
- 可以将一个类的定义放在另一个类的定义里面
- 方法内定义的类
- 方法内部作用域中定义的类
-
匿名内部类
- 实现接口
- 扩展带有非默认构造函数的类
- 执行字段初始化
- 通过实例初始化执行构造
-
作为成员,内部类可访问外部类的任何东西
- 覆盖内部类
Lambda Expression⚓︎
- lambda 表达式是一段可以传递的代码块,以便稍后单次或多次执行
- lambda 表达式是一种值,能够存储在变量中并传递给函数
例子
利用比较器 (comparator) 排序:
import java.util.*;
public class LambdaExample1 {
public static void main(String[] args) {
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
// Before Java 8
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
System.out.println("Sorted (old way): " + names);
// With Lambda
Collections.sort(names, (a, b) -> a.compareTo(b));
System.out.println("Sorted (lambda): " + names);
}
}
使用 Predicate
过滤:
import java.util.*;
import java.util.function.Predicate;
public class LambdaExample2 {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
// Define a predicate using a lambda
Predicate<Integer> isEven = n -> n % 2 == 0;
// Use the predicate to filter numbers
for (Integer n : numbers) {
if (isEven.test(n)) {
System.out.println(n + " is even");
}
}
}
}
-
左边的三种参数形态可以和右边的三种代码形态交叉组合成一个 lambda 表达式
左 -> 右 ()
x + y
x
{;}
(x, y)
{; return x}
Closure⚓︎
-
闭包(closure) 是由一个函数及其从周围作用域捕获的自由变量组成的组合
- 函数:一段可执行的代码块
- 自由变量:在函数内部使用但定义在函数外部的变量
- 捕获:函数“记住”变量的值(或引用
) ,即使在原始作用域消失后也是如此
-
简而言之:闭包是一个与其环境捆绑在一起的函数
-
闭包作为匿名类
-
闭包作为 lambda 表达式
-
Java 对闭包的限制
-
Java 只捕获
final
或实际上是final
的变量(意味着变量在被捕获后不能重新赋值) -
Java 捕获变量的值,而不是变量引用本身
- 而 JavaScript 的闭包可以直接修改外部变量
-
Functional Interface⚓︎
-
函数式接口(functional interface):有一个函数的接口
-
任意 lambda 表达式的类型是一个函数式接口
-
系统库中的函数式接口
├── 0 参数 │ └── Supplier<T> (返回值:T) ├── 1 参数 │ ├── Predicate<T> (返回值:boolean) │ ├── Function<T,R> (返回值:R) │ ├── Consumer<T> (无返回值) │ ├── UnaryOperator<T> (T -> T, Function 特例) │ └── 基本类型专用 │ ├── IntPredicate / LongPredicate / DoublePredicate │ ├── IntFunction<R> / LongFunction<R> / DoubleFunction<R> │ ├── IntConsumer / LongConsumer / DoubleConsumer │ ├── IntSupplier / LongSupplier / DoubleSupplier │ └── ToIntFunction<T> / ToLongFunction<T> / ToDoubleFunction<T> ├── 2 参数 │ ├── BiPredicate<T,U> (返回值:boolean) │ ├── BiFunction<T,U,R> (返回值:R) │ ├── BiConsumer<T,U> (无返回值) │ ├── BinaryOperator<T> (T × T -> T, BiFunction 特例) │ └── 基本类型专用 │ ├── ObjIntConsumer<T> │ ├── ObjLongConsumer<T> │ └── ObjDoubleConsumer<T> │ └── 应用场景...
-
基础型
-
Predicate<T>
:接收一个参数,返回boolean
,常用来做条件判断 -
Function<T, R>
:接收一个参数,返回一个结果,常用于数据转换 -
Consumer<T>
:接收一个参数,没有返回值,常用于执行某些操作(打印、存储等) -
Supplier<T>
:不接收参数,返回一个结果,常用于提供数据
-
-
运算型
-
UnaryOperator<T>
:继承自Function<T, T>
,接收一个参数,返回相同类型的结果,常用于自我变换 -
BinaryOperator<T>
:继承自BiFunction<T, T, T>
,接收两个类型相同的参数,返回一个相同类型的结果,常用于聚合运算(比如 max/min)
-
-
带两个参数的
-
BiPredicate<T, U>
:接收两个参数,返回boolean
-
BiFunction<T,U,R>
:接收两个参数,返回一个结果 -
BiConsumer<T,U>
:接收两个参数,没有返回值
-
-
基本类型专用:Java 为了性能,提供了对
int
,long
,double
的专门版本,比如:IntPredicate
,IntFunction<R>
,IntConsumer
,IntSupplier
LongPredicate
,LongFunction<R>
,LongConsumer
,LongSupplier
DoublePredicate
,DoubleFunction<R>
,DoubleConsumer
,DoubleSupplier
ToIntFunction<T>
,ToDoubleFunction<T>
,ToLongFunction<T>
ObjIntConsumer<T>
(接收一个对象和一个int
)
-
- 急切调用(eager call):在 Java 中,当调用形如 \(y = f(x)\) 的函数时,\(x\) 的值会在调用 \(f()\) 之前就会被求解出来。特别地,当 \(x\) 是一个调用另一个函数 \(g()\) 的表达式时,即 \(y = f(g(x))\) 时,\(g(x)\) 应该要在调用 \(f()\) 之前被执行和调用。这就是急切调用。
- 懒惰调用(lazy call):另一种方式是将整个函数调用 \(g(x)\) 传递给 \(f()\),然后让它在 \(f()\) 的内部求解,因此叫做懒惰调用。此时 \(f()\) 接收的是函数接口而不是求解好的参数值
例子:日志系统
TBD
关键点
- lambda 表达式是一种没有名称的方法,用于传递行为,就像它是数据一样
-
lambda 表达式看起来像这样:
-
函数式接口是一个具有单个抽象方法的接口,用作 lambda 表达式的类型
- lambda 表达式使得懒惰求值成为可能
与 Python 的比较
- Python 的 lambda 表达式的结果是函数,而 Java 的是(实现了函数式接口的匿名子类的)对象
- Python 的 lambda 表达式的结果可以存入任何变量,而 Java 的只能存入所实现的函数式接口的变量
- 所以 Java 的 lambda 表达式的参数表必须符合其所实现的函数式接口中的函数
评论区