这篇介绍 Java 的一些进阶特性,主要在面向对象和设计模式的代码中会用到。如果你暂时只想刷算法题,可以先跳过这篇,等需要的时候再回来看。
继承与多态
继承
继承让你在已有类的基础上扩展新功能,用 extends 关键字:
super 关键字用来调用父类的构造方法或方法。子类的构造方法中,super(...) 必须写在第一行。
方法重写与多态
子类可以重写(Override)父类的方法,提供自己的实现:
多态的关键点:Animal a1 = new Dog("旺财") 这行代码,变量类型是 Animal(父类),但实际指向的是 Dog 对象(子类)。调用 a1.speak() 时,执行的是 Dog 重写后的 speak 方法,而不是 Animal 的。
这在设计模式中非常重要——你可以用父类类型来统一处理不同的子类对象,而不用关心具体是哪个子类。
抽象类
如果一个类只是用来被继承的,自己不需要实例化,可以声明为抽象类。抽象类中可以有抽象方法(没有方法体,交给子类实现):
接口
接口(interface)定义了一组方法签名,类通过 implements 来实现接口。和抽象类不同,一个类可以实现多个接口。
接口 vs 抽象类
简单来说:
- 抽象类:描述「是什么」。Dog 是 Animal,Circle 是 Shape。Java 只允许单继承(一个类只能 extends 一个父类)。
- 接口:描述「能做什么」。Duck 能飞、能游泳。一个类可以 implements 多个接口。
在设计模式中,接口用得比抽象类更多,因为它更灵活。
默认方法
Java 8 开始,接口可以有默认实现(default 方法),实现类可以选择是否重写:
interface Logger {
void log(String message);
// 默认方法:提供一个默认实现
default void warn(String message) {
log("WARNING: " + message);
}
}函数式接口与 Lambda
只有一个抽象方法的接口叫函数式接口,可以用 Lambda 表达式简写。前面排序时用的 (a, b) -> a - b 就是一个 Lambda 表达式,它实现的是 Comparator 接口:
Lambda 表达式的本质就是匿名函数。(a, b) -> a + b 等价于创建了一个实现了 MathOp 接口的匿名对象,只是写法更简洁。
泛型
泛型让你写出不限定具体类型的代码。前面用过的 ArrayList<Integer>、HashMap<String, Integer> 里的尖括号就是泛型。
泛型类
泛型方法
方法也可以有自己的类型参数:
类型擦除
Java 的泛型是在编译期检查的,运行时会把类型信息擦除。也就是说,ArrayList<Integer> 和 ArrayList<String> 在运行时其实是同一个类。
这意味着你不能在运行时做 new T() 或 instanceof T 这样的操作。了解这一点就行,日常使用中很少碰到。
通配符
泛型的通配符 ? 用于限制类型参数的范围,在看框架源码时会经常遇到:
// ? extends Number:只接受 Number 及其子类(Integer, Double 等)
void process(List<? extends Number> list) { ... }
// ? super Integer:只接受 Integer 及其父类(Number, Object)
void add(List<? super Integer> list) { ... }通配符在算法题中几乎用不到,在看设计模式或框架代码时可能会碰到,知道是什么意思就行。
异常处理进阶
实际项目里写 try-catch 往往不够用,还需要自定义异常类型、自动关闭资源、保留异常源头。
自定义异常一般继承 RuntimeException(非受检),不强制调用方处理;如果一定要让上层必须处理,就继承 Exception(受检)。业务层通常选前者,底层 IO 或协议相关的场景才考虑后者。
try-with-resources 圆括号里的对象必须实现 AutoCloseable 接口,代码块结束后会自动调用 close(),即使中途抛了异常也会关。这比在 finally 里手动 close 干净得多,也不会漏写。
还有一个细节:资源关闭本身也可能失败,比如网络中断、磁盘写入异常。如果 try 块抛了异常 A,close() 自己又抛了异常 B,B 不会覆盖 A,而是作为「被抑制的异常」(suppressed exception)挂在 A 上,通过 Throwable.getSuppressed() 能取出来。
手写 finally 很容易丢掉第一个异常,try-with-resources 帮你避开了这个坑。
异常链是实战里最容易被忽略的一点。业务层捕获底层异常后如果重新抛出,一定要用构造器的 cause 参数把原异常带上,否则根因就丢了,线上排查会很抓狂。
排查时 getCause() 能拿到下一层,Throwable.printStackTrace() 会把整条异常链一并打印。这几个写法在框架和服务层代码里很常见,出问题时能一路追到根因。
内部类与匿名类
内部类
前面的示例中我们一直在用 static class 定义内部类。其实 Java 的内部类有两种:
class Outer {
// 静态内部类:不依赖外部类的实例
static class StaticInner { ... }
// 非静态内部类:持有外部类的引用
class Inner { ... }
}算法题中如果需要定义辅助类(比如链表节点),用静态内部类就行:
public class Main {
// 链表节点
static class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
}
}匿名类
匿名类可以在创建对象的同时定义类的实现,常用于创建比较器等一次性使用的对象:
匿名类是 Lambda 出现之前的写法。现在有了 Lambda,对于函数式接口的场景就不再需要匿名类了。但在旧代码或者非函数式接口的场景中还是会见到匿名类。
小结
这篇覆盖了 Java 面向对象的核心机制。继承与多态让你用统一的方式处理不同类型的对象,接口比抽象类更灵活,设计模式中大量使用。泛型让代码不限定具体类型,集合类都靠它。异常处理是 Java 错误处理的标准方式,Lambda 则是匿名类的简洁替代。
这些知识足够你看懂本站的算法、数据结构和设计模式代码了。如果你对多线程编程感兴趣,可以继续阅读下一篇。