Skip to content

面向对象编程(OOP)

Java 面向对象编程(OOP)完整技术笔记


一、概述

面向对象编程(Object-Oriented Programming, OOP) 是 Java 语言的核心编程范式,它将现实世界的事物抽象为对象,通过封装、继承、多态三大特性组织代码,使程序具备模块化、可复用、易扩展的特点。

解决的问题:相比面向过程编程,OOP 更适合构建大型、复杂的软件系统,降低耦合度,提升可维护性。

适用场景:企业级应用开发、框架设计(Spring、MyBatis)、设计模式实现、API 设计等几乎所有 Java 开发场景。

面向对象 vs 面向过程

维度面向过程面向对象
思维方式步骤为中心,拆解为函数调用链对象为中心,抽象出实体及其交互
代码组织函数 + 数据结构类 + 对象
扩展性修改影响面大通过继承/多态轻松扩展
典型语言CJava、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 类不能被继承StringIntegerMath
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 + epoch01(偏向位 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 中的 AbstractListInputStream 都采用了这种设计。

接口

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.thisouter.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 对比

特性StringStringBufferStringBuilder
可变性不可变final char[] / JDK 9+ final byte[]可变可变
线程安全是(不可变天然线程安全)是(方法加 synchronized
性能频繁修改时低(每次生成新对象)较高最高
适用场景文本不频繁改变多线程频繁拼接单线程频繁拼接
默认容量1616

5.2 String 不可变的原因

java
public final class String {  // final 类,不能被继承
    private final char value[];  // JDK 8,private final 数组
    // JDK 9+: private final byte[] value;
}
  1. 存储字符的数组被 private final 修饰,且 String 类未提供修改方法
  2. 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() == 0truetruefalse
isBlank()str == null || str.trim().length() == 0truetruetrue

六、异常体系

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、SQLExceptiontry-catchthrows
Unchecked Exception❌ 不强制NPE、ClassCastException编码规范预防
Error❌ 不应处理OOM、StackOverflow系统级问题,无法恢复

6.2 try-catch-finally 规则

  • try 不可省略
  • catchfinally 至少保留一个
  • finally总是会执行(除非 JVM 退出),常用于资源释放
java
// Java 7+ try-with-resources(推荐)
try (InputStream is = new FileInputStream("file.txt")) {
    // 自动关闭资源
} catch (IOException e) {
    e.printStackTrace();
}

6.3 避免空指针异常(NPE)的 6 种方法

  1. 使用前检查 nullif (obj != null)
  2. 方法参数校验Objects.requireNonNull(param, "param不能为null")
  3. 返回空集合而非 nullCollections.emptyList() 替代 return null
  4. 字段初始化:确保字段有默认值
  5. 使用 OptionalOptional.ofNullable(value).orElse(defaultValue)
  6. 字符串比较常量在前"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 执行器
  • JDBCClass.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重写是运行时多态,重载是编译时多态
抽象类接口抽象类可有状态和构造方法,接口定义能力契约
浅拷贝深拷贝浅拷贝共享内部引用对象,深拷贝完全独立
StringStringBuilder前者不可变,后者可变且性能更高
Checked 异常Unchecked 异常前者编译期必须处理,后者不强制
Lambda匿名内部类Lambda 用 invokedynamic,不持有外部引用,更轻量
静态内部类成员内部类前者不持有外部引用,后者持有(可能内存泄漏)

十一、最佳实践与常见坑

✅ 最佳实践

  1. 字段 private,通过 getter/setter 暴露 — 封装的基本要求
  2. 重写 equals() 必须同时重写 hashCode() — 否则 HashMap 会出问题
  3. 始终添加 @Override 注解 — 编译器帮你验证签名
  4. 优先组合而非继承 — 过深的继承层次增加复杂性
  5. 优先接口而非抽象类 — 面向接口编程,依赖倒置
  6. 循环中拼接字符串用 StringBuilder(JDK 8)
  7. 不可变对象优先 — 天然线程安全(考虑使用 Record
  8. 用静态内部类替代成员内部类 — 避免内存泄漏
  9. 字符串比较常量放前面"abc".equals(variable) 防 NPE

❌ 常见坑

  1. 定义了有参构造后忘记补无参构造 — 框架(Spring、MyBatis)反射创建对象失败
  2. static 方法中访问实例变量 — 编译报错
  3. 只重写 equals() 不重写 hashCode()HashMap 数据丢失
  4. 向下转型不检查 instanceofClassCastException
  5. 成员内部类/匿名内部类导致内存泄漏 — 持有外部类强引用
  6. 循环中用 + 拼接字符串(JDK 8) — 大量创建 StringBuilder
  7. 混淆方法签名 — 方法签名不包含返回类型,仅改变返回类型不构成重载

十二、面试高频问题

Q1: 面向对象的三大特征是什么?分别解释。

A封装(隐藏内部实现,暴露公共接口)、继承(子类复用父类属性和方法,"is-a" 关系)、多态(同一操作作用于不同对象产生不同行为,运行时动态绑定)。封装是基础,继承是手段,多态是目标。

Q2: 接口和抽象类的区别?如何选择?

A:抽象类可有构造方法、实例字段、protected 方法,适合 "is-a" 关系和共享实现;接口定义能力契约,支持多实现,适合 "can-do" 关系。现代 Java 优先接口 + 默认方法,仅在需要共享状态时用抽象类。

Q3: 重写和重载的区别?

A:重写发生在父子类之间,方法签名相同,运行时动态分派;重载发生在同一个类中,方法名相同但参数列表不同,编译时静态绑定。方法签名不包含返回类型。

Q4: 为什么重写 equals() 必须重写 hashCode()?

Aequals 相等的对象 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

核心要点回顾

  1. OOP 三大特性(封装 → 安全性、继承 → 复用性、多态 → 灵活性)是 Java 编程的基石
  2. 类的初始化顺序是面试必考题:父 static → 子 static → 父实例块 → 父构造 → 子实例块 → 子构造
  3. 重写 equals() 必须重写 hashCode() 是 Java 编程铁律
  4. 优先组合而非继承、优先接口而非抽象类 是现代 Java 设计原则
  5. String 不可变 是线程安全和字符串常量池的基础
  6. 静态内部类优于成员内部类 — 避免内存泄漏
  7. Lambda 不是匿名内部类的语法糖 — 实现机制、this 指向、性能都不同