Inheritance, Polymorphism and Classes of Other Kinds⚓︎
约 2548 个字 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 + yx{;}(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 表达式的参数表必须符合其所实现的函数式接口中的函数
 
评论区

