Appearance
Java 面向对象编程(OOP)完整技术笔记
一、概述
面向对象编程(Object-Oriented Programming, OOP) 是 Java 语言的核心编程范式,它将现实世界的事物抽象为对象,通过封装、继承、多态三大特性组织代码,使程序具备模块化、可复用、易扩展的特点。
解决的问题:相比面向过程编程,OOP 更适合构建大型、复杂的软件系统,降低耦合度,提升可维护性。
适用场景:企业级应用开发、框架设计(Spring、MyBatis)、设计模式实现、API 设计等几乎所有 Java 开发场景。
面向对象 vs 面向过程
维度 面向过程 面向对象 思维方式 以步骤为中心,拆解为函数调用链 以对象为中心,抽象出实体及其交互 代码组织 函数 + 数据结构 类 + 对象 扩展性 修改影响面大 通过继承/多态轻松扩展 典型语言 C Java、C++、Python
二、核心概念与原理
2.1 类与对象
- 类(Class):对象的模板/蓝图,定义了对象的属性(字段)和行为(方法)。
- 对象(Object):类的实例,通过
new关键字在堆内存中创建,对象引用存放在栈内存中。
java
// 类定义
class Person {
String name; // 实例变量
int age;
Person(String name, int age) { // 构造方法
this.name = name;
this.age = age;
}
void introduce() { // 实例方法
System.out.println("I'm " + name + ", age " + age);
}
}
// 创建对象
Person p1 = new Person("Alice", 30);
Person p2 = p1; // 引用拷贝,p2 和 p1 指向同一个对象关键区别:
- 对象相等:比较的是内存中存放的内容是否相等
- 引用相等:比较的是指向的内存地址是否相等
2.2 类的成员结构
类由以下五种成员组成:
mermaid
graph LR
A[类的成员结构] --> B[字段 Field]
A --> C[方法 Method]
A --> D[构造方法 Constructor]
A --> E[代码块 Block]
A --> F[内部类 Inner Class]
B --> B1[实例变量:每个对象独立副本]
B --> B2[类变量 static:所有对象共享]
C --> C1[实例方法:操作对象状态]
C --> C2[静态方法:不依赖对象]
E --> E1[静态代码块:类加载时执行一次]
E --> E2[实例代码块:每次 new 时执行]三、关键知识点详解
3.1 static 成员的特性与使用规范
static 修饰的成员属于类而非对象,存储在方法区(JDK 8+ 为元空间)。
| 特性 | static 成员 | 实例成员 |
|---|---|---|
| 归属 | 类 | 对象 |
| 内存分配 | 类加载时 | 对象创建时 |
| 访问方式 | 类名.成员 或 对象.成员 | 对象.成员 |
| 能否访问实例成员 | ❌ 不能 | ✅ 能 |
使用规范:
static方法不能访问this和实例成员static方法不能被重写(只能被隐藏)static final常用于定义常量(编译期常量会被内联到使用处)static块在类加载初始化阶段执行一次,可用于复杂的静态初始化逻辑
java
public class Constants {
public static final double PI = 3.14159265358979; // 编译期常量,内联优化
static {
System.out.println("类加载时执行一次");
}
public static double circleArea(double r) {
// static 方法中不能使用 this
return PI * r * r;
}
}常见误区:在 static 方法中访问实例变量会编译报错。
3.2 类的初始化顺序(面试高频)
完整初始化顺序(存在继承关系时):
父类 static 块 → 子类 static 块 → 父类实例块 → 父类构造方法 → 子类实例块 → 子类构造方法java
class Parent {
static { System.out.println("1. 父类静态块"); }
{ System.out.println("3. 父类实例块"); }
Parent() { System.out.println("4. 父类构造方法"); }
}
class Child extends Parent {
static { System.out.println("2. 子类静态块"); }
{ System.out.println("5. 子类实例块"); }
Child() { System.out.println("6. 子类构造方法"); }
}
// new Child() 输出:
// 1. 父类静态块
// 2. 子类静态块
// 3. 父类实例块
// 4. 父类构造方法
// 5. 子类实例块
// 6. 子类构造方法注意:
static块只在类首次加载时执行一次,第二次new Child()不会再执行static块。
3.3 final 关键字的三种用法
| 用法 | 说明 | 示例 |
|---|---|---|
| final 变量 | 基本类型:值不可变;引用类型:引用不可变(对象内容可变) | final List<String> list = new ArrayList<>(); list.add("ok"); ✅ |
| final 方法 | 不能被子类重写,JIT 可内联优化 | public final void doSomething() {} |
| final 类 | 不能被继承 | String、Integer、Math |
java
final int x = 10;
// x = 20; // ❌ 编译报错
final List<String> list = new ArrayList<>();
list.add("hello"); // ✅ 对象内容可变
// list = new ArrayList<>(); // ❌ 引用不可重新赋值3.4 构造方法与对象初始化
构造方法是一种特殊的方法,用于完成对象的初始化工作。
核心规则:
- 名字与类名相同,无返回值(不能用
void声明) - 若未定义任何构造方法,编译器自动生成无参构造
- 一旦定义了有参构造,无参构造不再自动生成(这是常见坑!)
- 构造方法可重载(不同参数列表),不能被重写
this()调用本类其他构造方法,super()调用父类构造方法(都必须是第一条语句)
java
public class User {
private String name;
private int age;
// 无参构造(定义了有参构造后,必须手动补上)
public User() {
this("unknown", 0); // 调用其他构造方法
}
// 有参构造
public User(String name, int age) {
super(); // 默认隐式调用,可省略
this.name = name;
this.age = age;
}
}建造者模式替代多参数构造方法
当构造参数超过 4 个时,推荐使用 Builder 模式:
java
public class UserConfig {
private final String host; // 可以是 final,保证不可变
private final int port;
private final String username;
private final String password;
private final int timeout;
private UserConfig(Builder builder) {
this.host = builder.host;
this.port = builder.port;
this.username = builder.username;
this.password = builder.password;
this.timeout = builder.timeout;
}
public static class Builder {
private String host;
private int port = 3306; // 默认值
private String username;
private String password;
private int timeout = 3000; // 默认值
public Builder host(String host) { this.host = host; return this; }
public Builder port(int port) { this.port = port; return this; }
public Builder username(String username) { this.username = username; return this; }
public Builder password(String password) { this.password = password; return this; }
public Builder timeout(int timeout) { this.timeout = timeout; return this; }
public UserConfig build() {
return new UserConfig(this);
}
}
}
// 使用:含义清晰,参数可选
UserConfig config = new UserConfig.Builder()
.host("localhost")
.username("root")
.password("123456")
.build();Lombok 的
@Builder注解可自动生成以上样板代码。
3.5 对象创建与内存布局
new 关键字触发的完整流程:
mermaid
flowchart LR
A[new 指令] --> B[类加载检查]
B --> C[堆内存分配]
C --> D[内存初始化为零值]
D --> E[设置对象头]
E --> F[执行构造方法]内存分配方式:
- 指针碰撞(Bump the Pointer):堆内存规整时使用(Serial、ParNew 等带压缩的收集器)
- 空闲列表(Free List):堆内存不规整时使用(CMS 等收集器)
对象在堆中的内存布局:
| 区域 | 内容 | 大小 |
|---|---|---|
| 对象头(Header) | Mark Word(哈希码/锁状态/GC 年龄)+ 类型指针 | 64 位 JVM:12~16 字节 |
| 实例数据(Instance Data) | 对象的字段数据 | 取决于字段数量和类型 |
| 对齐填充(Padding) | 补齐为 8 字节的倍数 | 0~7 字节 |
对象头 Mark Word 与锁机制
Mark Word 是 64 位 JVM 中 8 字节的多用途字段:
| 锁状态 | Mark Word 存储内容 | 标志位 |
|---|---|---|
| 无锁 | 哈希码 + GC 年龄 | 01 |
| 偏向锁 | 线程 ID + epoch | 01(偏向位 1) |
| 轻量级锁 | 指向栈锁记录的指针 | 00 |
| 重量级锁 | 指向 Monitor 的指针 | 10 |
| GC 标记 | 空 | 11 |
锁升级过程就是 Mark Word 中锁标志位变化的过程,这是深入理解
synchronized的关键。
3.6 封装
封装 = 数据隐藏 + 接口暴露。通过访问控制修饰符隐藏内部实现细节,仅暴露必要的公共接口。
四种访问级别
| 修饰符 | 本类 | 同包 | 子类(不同包) | 其他包 |
|---|---|---|---|---|
private | ✅ | ❌ | ❌ | ❌ |
| default(无修饰符) | ✅ | ✅ | ❌ | ❌ |
protected | ✅ | ✅ | ✅ | ❌ |
public | ✅ | ✅ | ✅ | ✅ |
最佳实践:字段设为 private,通过 public 的 getter/setter 暴露;过度使用 public 是破坏封装的常见问题。
不可变对象(Immutable Object)
不可变对象天然线程安全,推荐在值对象、DTO 设计中使用。
java
public final class Money {
private final String currency;
private final BigDecimal amount;
public Money(String currency, BigDecimal amount) {
this.currency = currency;
this.amount = amount;
}
public String getCurrency() { return currency; }
public BigDecimal getAmount() { return amount; }
// 无 setter 方法
}Java 16+ Record 类自动生成不可变对象:
java
public record Money(String currency, BigDecimal amount) {}
// 自动生成:构造方法、getter、equals()、hashCode()、toString()模块系统(JPMS)对封装的增强
Java 9 引入模块系统,通过 module-info.java 将封装从类级别提升到包级别:
java
module com.example.myapp {
requires java.sql; // 依赖的模块
exports com.example.api; // 对外暴露的包
opens com.example.internal to spring.core; // 允许反射访问
}未 exports 的包,即使其中的类是 public,外部模块也无法访问。
3.7 继承
继承通过 extends 关键字实现,建立 "is-a" 关系。Java 只支持单继承(一个类只能有一个直接父类)。
所有类都隐式继承 java.lang.Object。
方法重写(Override)
| 规则 | 说明 |
|---|---|
| 方法签名 | 必须完全相同(方法名 + 参数列表) |
| 返回类型 | 相同或协变(子类型,JDK 5+) |
| 访问级别 | 不能更严格(如父类 protected,子类不能改为 private) |
| 异常 | 不能抛出更宽泛的受检异常 |
@Override | 应始终添加,让编译器帮你验证 |
java
class Animal {
protected Animal create() {
return new Animal();
}
}
class Dog extends Animal {
@Override
protected Dog create() { // 协变返回类型:Dog 是 Animal 的子类
return new Dog();
}
}重要:static 方法不参与重写(只是隐藏),private 方法不能被重写。
重写与重载的本质区别
| 维度 | 重写(Override) | 重载(Overload) |
|---|---|---|
| 发生位置 | 父子类之间 | 同一个类中 |
| 方法签名 | 完全相同 | 方法名相同,参数列表不同 |
| 绑定时机 | 运行时(动态分派) | 编译时(静态绑定) |
| 多态类型 | 运行时多态 | 编译时多态 |
| 返回类型 | 相同或协变 | 无限制(但仅改变返回类型不构成重载,会编译报错) |
方法签名只包含方法名和参数列表,不包含返回类型和访问修饰符。
super 关键字
java
class Child extends Parent {
Child() {
super(); // 调用父类构造方法,必须是第一条语句
}
@Override
void doWork() {
super.doWork(); // 调用父类被重写的方法
// 子类额外逻辑
}
}若子类构造方法未显式调用
super(),编译器会自动插入super()调用父类无参构造。若父类无无参构造,则编译报错。
Object 类的核心方法
Object 是所有类的根,提供了 11 个关键方法:
| 方法 | 作用 | 注意事项 |
|---|---|---|
equals(Object) | 比较对象内容是否相等 | 重写 equals() 必须同时重写 hashCode() |
hashCode() | 返回对象哈希码 | equals 相等 → hashCode 必须相等 |
toString() | 返回对象字符串表示 | 建议所有子类重写 |
clone() | 创建对象副本 | 需实现 Cloneable 接口 |
getClass() | 返回运行时 Class 对象 | final native,不可重写 |
wait()/notify()/notifyAll() | 线程间通信 | final native,不可重写,需在同步块中调用 |
finalize() | GC 回收前调用 | JDK 9 已弃用,不推荐使用 |
equals() 与 hashCode() 的契约:
java
// 重写 equals 必须重写 hashCode
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}3.8 多态
多态是 OOP 最强大的特性,使得上层代码依赖抽象而非具体实现。
运行时多态与动态分派
java
Animal a = new Dog(); // 向上转型
a.speak(); // 运行时调用 Dog 的 speak(),而非 Animal 的JVM 实现原理:通过 invokevirtual 指令,在运行时查找对象实际类型的方法表(vtable),动态绑定到正确实现。
虚方法表(vtable)原理
mermaid
graph TB
subgraph Animal vtable
A1["speak() → Animal.speak"]
A2["eat() → Animal.eat"]
end
subgraph Dog vtable
D1["speak() → Dog.speak(重写)"]
D2["eat() → Animal.eat(继承)"]
end- 每个类维护一张 vtable,存储所有虚方法的实际实现地址
- 子类重写方法 → 对应位置存子类实现地址;否则继承父类地址
- 调用时通过固定偏移量直接寻址,$O(1)$ 时间完成
JIT 内联缓存优化
| 状态 | 描述 | 性能 |
|---|---|---|
| 单态(Monomorphic) | 调用点只出现一种类型 | 最优,直接内联 |
| 双态(Bimorphic) | 出现两种类型 | 较优,生成两个分支 |
| 超多态(Megamorphic) | 超过阈值 | 退化为普通虚分派 |
实战建议:尽量保持接口调用点的类型稳定,有助于 JIT 优化。
instanceof 与类型转换
java
// 传统写法
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.bark();
}
// Java 16+ 模式匹配(推荐)
if (animal instanceof Dog dog) {
dog.bark(); // 无需单独强转
}向下转型是设计问题的信号,应优先通过多态避免。
方法重载(编译时多态)
重载在编译期通过静态类型解析:
java
void process(int x) { System.out.println("int"); }
void process(long x) { System.out.println("long"); }
void process(double x) { System.out.println("double"); }
process(10); // 输出 "int"
process(10L); // 输出 "long"
process(10.0); // 输出 "double"
// process(10) 如果没有 int 重载版本,会自动提升到 long注意:自动类型提升会影响重载选择;可变参数 (String... args) 优先级最低。
3.9 抽象类与接口
抽象类
java
public abstract class AbstractTemplate {
// 模板方法(final 防止子类重写算法骨架)
public final void execute() {
step1();
step2(); // 抽象方法,由子类实现
step3();
}
private void step1() { System.out.println("通用步骤1"); }
protected abstract void step2(); // 子类必须实现
private void step3() { System.out.println("通用步骤3"); }
}模板方法模式是抽象类的经典应用。JDK 中的
AbstractList、InputStream都采用了这种设计。
接口
java
public interface Cacheable {
// 常量(默认 public static final)
int MAX_SIZE = 1024;
// 抽象方法(默认 public abstract)
void put(String key, Object value);
Object get(String key);
// 默认方法(Java 8+)
default void clear() {
System.out.println("默认清空实现");
}
// 静态方法(Java 8+)
static Cacheable createDefault() {
return new InMemoryCache();
}
// 私有方法(Java 9+,供 default 方法复用)
private void log(String msg) {
System.out.println("[Cache] " + msg);
}
}接口 vs 抽象类选择原则
| 场景 | 选择 | 原因 |
|---|---|---|
| 强 "is-a" 关系(Dog 是 Animal) | 抽象类 | 共享状态和公共实现 |
| 定义能力契约(Serializable、Comparable) | 接口 | 多实现,面向接口编程 |
| 需要共享字段/构造方法逻辑 | 抽象类 | 接口不能有实例字段和构造方法 |
| 需要多继承能力 | 接口 | Java 只支持单继承 |
| API 设计,对外暴露契约 | 接口 | 解耦,依赖倒置 |
现代 Java 设计:优先考虑接口 + 默认方法,仅在需要共享状态时才用抽象类。
默认方法冲突(菱形问题)
java
interface A { default void hello() { System.out.println("A"); } }
interface B { default void hello() { System.out.println("B"); } }
// 实现类必须显式解决冲突
class C implements A, B {
@Override
public void hello() {
A.super.hello(); // 显式选择 A 的实现
}
}3.10 内部类
四种内部类对比
| 类型 | 是否持有外部类引用 | 创建方式 | 典型用途 |
|---|---|---|---|
| 静态内部类 | ❌ 不持有 | new Outer.Inner() | Builder、Entry 节点 |
| 成员内部类 | ✅ 持有(Outer.this) | outer.new Inner() | 需要访问外部实例状态 |
| 局部内部类 | ✅ 持有 | 方法内直接 new | 方法内部的一次性逻辑 |
| 匿名内部类 | ✅ 持有 | new Interface() {...} | 快速实现接口/抽象类 |
成员内部类的内存泄漏问题
java
// ❌ 危险:成员内部类持有外部类的强引用
class Activity {
private byte[] largeData = new byte[1024 * 1024]; // 1MB
class Handler {
void handle() {
// 隐式持有 Activity.this
}
}
}
// 如果 Handler 的生命周期比 Activity 长,Activity 无法被 GC 回收
// ✅ 解决:改为 static 内部类
class Activity {
private byte[] largeData = new byte[1024 * 1024];
static class Handler {
private WeakReference<Activity> activityRef;
Handler(Activity activity) {
this.activityRef = new WeakReference<>(activity);
}
void handle() {
Activity activity = activityRef.get();
if (activity != null) {
// 安全使用
}
}
}
}Lambda 与匿名内部类的本质区别
| 维度 | Lambda | 匿名内部类 |
|---|---|---|
this 指向 | 外部类实例 | 匿名类自身实例 |
| 实现机制 | invokedynamic 指令 | 生成独立 .class 文件 |
| 是否持有外部引用 | 无状态时不持有 | 始终持有 |
| 内存泄漏风险 | 低 | 高 |
| 性能 | 更轻量 | 较重 |
java
// 匿名内部类
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println(this.getClass()); // 匿名类自身
}
};
// Lambda(推荐)
Runnable r2 = () -> {
System.out.println(this.getClass()); // 外部类
};3.11 深拷贝与浅拷贝
| 类型 | 描述 | 共享内部对象? |
|---|---|---|
| 引用拷贝 | 只复制引用,指向同一个对象 | 是(同一个对象) |
| 浅拷贝 | 创建新对象,但内部引用类型字段共享 | 是(共享内部对象) |
| 深拷贝 | 创建新对象,内部引用类型也递归复制 | 否(完全独立) |
java
// 浅拷贝示例
class Address {
String city;
}
class Person implements Cloneable {
String name;
Address address; // 引用类型
@Override
protected Person clone() throws CloneNotSupportedException {
return (Person) super.clone(); // 浅拷贝:address 仍指向同一个对象
}
}
// 深拷贝:手动递归复制
@Override
protected Person clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.address = new Address();
cloned.address.city = this.address.city;
return cloned;
}四、Object 类详解
4.1 == 与 equals() 的区别
| 特性 | == | equals() |
|---|---|---|
| 基本类型 | 比较值 | 不适用(基本类型无此方法) |
| 引用类型 | 比较内存地址 | 默认比较地址,可重写为比较内容 |
| 可重写 | 否 | 是 |
java
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false(地址不同)
System.out.println(s1.equals(s2)); // true(String 重写了 equals,比较内容)4.2 hashCode() 的作用
hashCode() 返回对象的哈希码,用于确定对象在哈希表(如 HashMap)中的索引位置。
hashCode 与 equals 的契约:
equals 相等 → hashCode 必须相等
hashCode 相等 → equals 不一定相等(哈希碰撞)
hashCode 不相等 → equals 一定不相等面试考点:重写
equals()不重写hashCode()会导致HashMap中出现逻辑相等但找不到的问题。
java
// 错误示范
class Key {
int id;
@Override
public boolean equals(Object o) {
return this.id == ((Key) o).id;
}
// 未重写 hashCode()!
}
Map<Key, String> map = new HashMap<>();
Key k1 = new Key(); k1.id = 1;
map.put(k1, "value");
Key k2 = new Key(); k2.id = 1;
map.get(k2); // 返回 null!因为 k1 和 k2 的 hashCode 不同,定位到不同的桶五、String 类详解
5.1 String、StringBuffer、StringBuilder 对比
| 特性 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变(final char[] / JDK 9+ final byte[]) | 可变 | 可变 |
| 线程安全 | 是(不可变天然线程安全) | 是(方法加 synchronized) | 否 |
| 性能 | 频繁修改时低(每次生成新对象) | 较高 | 最高 |
| 适用场景 | 文本不频繁改变 | 多线程频繁拼接 | 单线程频繁拼接 |
| 默认容量 | — | 16 | 16 |
5.2 String 不可变的原因
java
public final class String { // final 类,不能被继承
private final char value[]; // JDK 8,private final 数组
// JDK 9+: private final byte[] value;
}- 存储字符的数组被
private final修饰,且 String 类未提供修改方法 - String 类被
final修饰,不能被继承,防止子类破坏不可变性
不可变的好处:
- 天然线程安全
- 可以安全地作为
HashMap的 Key(hashCode 可缓存) - 字符串常量池的基础
5.3 字符串拼接:"+" vs StringBuilder
JDK 8:+ 拼接在编译后会转为 StringBuilder.append()
java
// 源码
String s = str1 + str2 + str3;
// 编译后等价于
String s = new StringBuilder().append(str1).append(str2).append(str3).toString();⚠️ 循环中的 "+" 拼接有坑(JDK 8):
java
// ❌ 每次循环都创建一个新的 StringBuilder
String s = "";
for (int i = 0; i < arr.length; i++) {
s += arr[i]; // 每次 new StringBuilder()
}
// ✅ 手动创建一个 StringBuilder 复用
StringBuilder sb = new StringBuilder();
for (String value : arr) {
sb.append(value);
}
String s = sb.toString();JDK 9+:
+改为使用invokedynamic调用makeConcatWithConstants(),性能更优,可以放心使用+。
5.4 字符串常量池
字符串常量池是 JVM 为了减少内存消耗、提升性能而专门开辟的区域,位于方法区(JDK 7+ 移至堆中)。
java
String s1 = "abc"; // 字符串常量池
String s2 = "abc"; // 直接返回常量池中已有的引用
String s3 = new String("abc"); // 堆内存中创建新对象
System.out.println(s1 == s2); // true(同一个常量池对象)
System.out.println(s1 == s3); // false(一个常量池,一个堆)
System.out.println(s1.equals(s3)); // true(内容相同)new String("abc") 创建了几个对象?
- 如果常量池中已有
"abc":创建 1 个(堆中的 String 对象) - 如果常量池中没有
"abc":创建 2 个(常量池中的"abc"+ 堆中的 String 对象)
String.intern() 方法:将字符串对象的引用保存到字符串常量池中,如果常量池已存在相同内容的字符串,则返回常量池中的引用。
5.5 编码转换
java
String originalStr = "Hello, 世界";
// UTF-8 → 字节数组
byte[] bytes = originalStr.getBytes(StandardCharsets.UTF_8);
// 字节数组 → 指定编码的字符串
String newStr = new String(bytes, StandardCharsets.UTF_8);注意:不是所有字符都能在不同编码间无损转换(如 ISO-8859-1 不支持中文)。
5.6 StringUtils 工具类
| 方法 | 判断逻辑 | null | "" | " " |
|---|---|---|---|---|
isEmpty() | str == null || str.length() == 0 | true | true | false |
isBlank() | str == null || str.trim().length() == 0 | true | true | true |
六、异常体系
6.1 异常分类
mermaid
graph TB
A[Throwable] --> B[Error]
A --> C[Exception]
C --> D[受检异常 Checked Exception]
C --> E[非受检异常 RuntimeException]
B --> B1[OutOfMemoryError]
B --> B2[StackOverflowError]
D --> D1[IOException]
D --> D2[SQLException]
D --> D3[ClassNotFoundException]
E --> E1[NullPointerException]
E --> E2[ArrayIndexOutOfBoundsException]
E --> E3[ClassCastException]
E --> E4[IllegalArgumentException]| 类型 | 编译检查 | 常见异常 | 处理方式 |
|---|---|---|---|
| Checked Exception | ✅ 必须处理 | IOException、SQLException | try-catch 或 throws |
| Unchecked Exception | ❌ 不强制 | NPE、ClassCastException | 编码规范预防 |
| Error | ❌ 不应处理 | OOM、StackOverflow | 系统级问题,无法恢复 |
6.2 try-catch-finally 规则
try不可省略catch和finally至少保留一个finally块总是会执行(除非 JVM 退出),常用于资源释放
java
// Java 7+ try-with-resources(推荐)
try (InputStream is = new FileInputStream("file.txt")) {
// 自动关闭资源
} catch (IOException e) {
e.printStackTrace();
}6.3 避免空指针异常(NPE)的 6 种方法
- 使用前检查 null:
if (obj != null) - 方法参数校验:
Objects.requireNonNull(param, "param不能为null") - 返回空集合而非 null:
Collections.emptyList()替代return null - 字段初始化:确保字段有默认值
- 使用 Optional:
Optional.ofNullable(value).orElse(defaultValue) - 字符串比较常量在前:
"expected".equals(variable)而非variable.equals("expected")
七、泛型
7.1 三种使用方式
java
// 1. 泛型类
public class Box<T> {
private T item;
public void set(T item) { this.item = item; }
public T get() { return item; }
}
// 2. 泛型接口
public interface Generator<T> {
T generate();
}
// 3. 泛型方法
public static <E> void printArray(E[] arr) {
for (E element : arr) {
System.out.printf("%s ", element);
}
}注意:
static方法不能使用类上声明的泛型(因为静态方法加载在类实例化之前),只能使用自己声明的<E>。
7.2 类型擦除
Java 泛型是编译期的特性,在运行时会被擦除为原始类型:
java
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // true,运行时都是 ArrayList八、反射
8.1 核心概念
反射允许程序在运行时动态地获取类信息、创建对象、调用方法、访问字段。
java
// 获取 Class 对象的三种方式
Class<?> clazz1 = Class.forName("com.example.Person");
Class<?> clazz2 = Person.class;
Class<?> clazz3 = person.getClass();
// 动态创建对象
Constructor<?> constructor = clazz1.getConstructor(String.class, int.class);
Object obj = constructor.newInstance("Alice", 30);
// 调用方法
Method method = clazz1.getMethod("introduce");
method.invoke(obj);
// 访问私有字段
Field field = clazz1.getDeclaredField("name");
field.setAccessible(true); // 突破 private 限制
String name = (String) field.get(obj);8.2 反射在框架中的应用
- Spring:
@Component→ 反射扫描注解 → 创建 Bean 实例 - MyBatis:Mapper 接口 → 反射 + 动态代理 → 生成 SQL 执行器
- JDBC:
Class.forName("com.mysql.cj.jdbc.Driver")→ 反射加载驱动
九、SPI 机制
SPI(Service Provider Interface) 是 JDK 内置的服务发现机制,将接口的定义与实现解耦。
mermaid
flowchart LR
A[服务接口定义] --> B[ServiceLoader.load]
B --> C[扫描 META-INF/services/ 目录]
C --> D[加载配置文件中指定的实现类]
D --> E[返回实现类实例]java
// 1. 定义接口
public interface Search {
List<String> searchDoc(String keyword);
}
// 2. 提供实现
public class FileSearch implements Search {
@Override
public List<String> searchDoc(String keyword) {
System.out.println("文件搜索: " + keyword);
return Collections.emptyList();
}
}
// 3. 在 META-INF/services/com.example.Search 文件中写入:
// com.example.FileSearch
// 4. 使用 ServiceLoader 发现并加载实现
ServiceLoader<Search> loader = ServiceLoader.load(Search.class);
for (Search search : loader) {
search.searchDoc("hello world");
}十、对比与易混淆点汇总
| 概念 A | 概念 B | 关键区别 |
|---|---|---|
== | equals() | == 比较地址,equals() 可重写比较内容 |
| 重写 Override | 重载 Overload | 重写是运行时多态,重载是编译时多态 |
| 抽象类 | 接口 | 抽象类可有状态和构造方法,接口定义能力契约 |
| 浅拷贝 | 深拷贝 | 浅拷贝共享内部引用对象,深拷贝完全独立 |
String | StringBuilder | 前者不可变,后者可变且性能更高 |
| Checked 异常 | Unchecked 异常 | 前者编译期必须处理,后者不强制 |
| Lambda | 匿名内部类 | Lambda 用 invokedynamic,不持有外部引用,更轻量 |
| 静态内部类 | 成员内部类 | 前者不持有外部引用,后者持有(可能内存泄漏) |
十一、最佳实践与常见坑
✅ 最佳实践
- 字段
private,通过 getter/setter 暴露 — 封装的基本要求 - 重写
equals()必须同时重写hashCode()— 否则HashMap会出问题 - 始终添加
@Override注解 — 编译器帮你验证签名 - 优先组合而非继承 — 过深的继承层次增加复杂性
- 优先接口而非抽象类 — 面向接口编程,依赖倒置
- 循环中拼接字符串用
StringBuilder(JDK 8) - 不可变对象优先 — 天然线程安全(考虑使用
Record) - 用静态内部类替代成员内部类 — 避免内存泄漏
- 字符串比较常量放前面 —
"abc".equals(variable)防 NPE
❌ 常见坑
- 定义了有参构造后忘记补无参构造 — 框架(Spring、MyBatis)反射创建对象失败
static方法中访问实例变量 — 编译报错- 只重写
equals()不重写hashCode()—HashMap数据丢失 - 向下转型不检查
instanceof—ClassCastException - 成员内部类/匿名内部类导致内存泄漏 — 持有外部类强引用
- 循环中用
+拼接字符串(JDK 8) — 大量创建StringBuilder - 混淆方法签名 — 方法签名不包含返回类型,仅改变返回类型不构成重载
十二、面试高频问题
Q1: 面向对象的三大特征是什么?分别解释。
A:封装(隐藏内部实现,暴露公共接口)、继承(子类复用父类属性和方法,"is-a" 关系)、多态(同一操作作用于不同对象产生不同行为,运行时动态绑定)。封装是基础,继承是手段,多态是目标。
Q2: 接口和抽象类的区别?如何选择?
A:抽象类可有构造方法、实例字段、protected 方法,适合 "is-a" 关系和共享实现;接口定义能力契约,支持多实现,适合 "can-do" 关系。现代 Java 优先接口 + 默认方法,仅在需要共享状态时用抽象类。
Q3: 重写和重载的区别?
A:重写发生在父子类之间,方法签名相同,运行时动态分派;重载发生在同一个类中,方法名相同但参数列表不同,编译时静态绑定。方法签名不包含返回类型。
Q4: 为什么重写 equals() 必须重写 hashCode()?
A:equals 相等的对象 hashCode 必须相等,这是 HashMap/HashSet 等哈希容器的基本契约。如果不重写 hashCode(),逻辑相等的两个对象可能映射到不同的桶,导致 get() 找不到已存在的键。
Q5: String 为什么设计为不可变?
A:(1) 安全地用于 HashMap 的 Key(hashCode 可缓存);(2) 天然线程安全;(3) 支持字符串常量池复用,节省内存;(4) 安全性(如类加载时的类名不可被篡改)。实现方式:private final 数组 + final 类 + 不暴露修改方法。
Q6: 类的初始化顺序是什么?
A:父类 static 块 → 子类 static 块 → 父类实例块 → 父类构造方法 → 子类实例块 → 子类构造方法。static 块仅在类首次加载时执行一次。
十三、总结
mermaid
mindmap
root((Java OOP))
类与对象
类的成员结构
static 成员
初始化顺序
final 关键字
构造方法与 Builder 模式
对象内存布局与 Mark Word
三大特性
封装
访问控制修饰符
不可变对象
模块系统 JPMS
继承
方法重写与 @Override
协变返回类型
super 关键字
Object 类
多态
运行时多态与 vtable
JIT 内联缓存
instanceof 模式匹配
方法重载
抽象与接口
抽象类与模板方法模式
接口与默认方法
函数式接口
内部类
静态内部类 vs 成员内部类
内存泄漏问题
Lambda vs 匿名内部类
相关核心知识
Object 类 11 个方法
String 不可变性与常量池
异常体系
泛型与类型擦除
反射与 SPI核心要点回顾:
- OOP 三大特性(封装 → 安全性、继承 → 复用性、多态 → 灵活性)是 Java 编程的基石
- 类的初始化顺序是面试必考题:父 static → 子 static → 父实例块 → 父构造 → 子实例块 → 子构造
- 重写
equals()必须重写hashCode()是 Java 编程铁律 - 优先组合而非继承、优先接口而非抽象类 是现代 Java 设计原则
- String 不可变 是线程安全和字符串常量池的基础
- 静态内部类优于成员内部类 — 避免内存泄漏
- Lambda 不是匿名内部类的语法糖 — 实现机制、
this指向、性能都不同