Skip to content

面向对象

一、为什么需要面向对象?

面向过程(如 C 语言)以函数为中心,数据和操作分离。当系统变大,数据与操作脱节导致维护困难。面向对象将数据和行为封装在一起,通过对象之间的协作完成功能,更符合人类对世界的认知——"世界由对象组成,对象之间通过消息通信"。

三大特性:封装(隐藏细节)、继承(复用代码)、多态(灵活扩展)。

二、封装

2.1 原理

封装是将数据和操作数据的方法绑定在一起,对外隐藏内部实现,只暴露有限接口。核心手段是访问控制修饰符:

修饰符本类同包子类全局
private
default
protected
public

2.2 为什么用 private + getter/setter?

直接暴露 public 字段,后续想加校验逻辑(如年龄不能为负)时,所有调用方都要改。用 getter/setter 封装,修改只在类内部,调用方无感知。

java
public class User {
    private int age;

    public void setAge(int age) {
        if (age < 0) throw new IllegalArgumentException("年龄不能为负");
        this.age = age;
    }
}

三、继承

3.1 原理

子类继承父类的所有非 private 成员(字段和方法)。JVM 在加载子类时,将父类的字段和方法合并到子类的内存布局中。方法调用时,先从子类查找,找不到再沿继承链向上查找。

3.2 为什么用组合优于继承?

继承是白盒复用——子类知道父类的内部细节,耦合度高。组合是黑盒复用——只知道接口,不知道内部实现。父类修改可能破坏子类行为(脆弱基类问题)。

java
// 继承:强耦合
class Stack extends ArrayList {
    // Stack 不应该有 add/remove 等 ArrayList 方法,但不小心继承了
}

// 组合:松耦合
class Stack {
    private List<Integer> data = new ArrayList<>();
    public void push(int v) { data.add(v); }
    public int pop() { return data.remove(data.size() - 1); }
}

3.3 方法重写 vs 重载

重写(Override)重载(Overload)
发生位置父子类之间同一类中
方法签名完全相同方法名相同,参数不同
返回类型相同或协变可不同
运行时/编译时运行时多态编译时确定
注解@Override无需

重写规则: 访问权限不能更低(父类 public,子类不能 private),异常不能更宽泛,static/private/final 方法不能被重写。

四、多态

4.1 原理

多态的核心是动态绑定:编译时类型是父类,运行时类型是子类。JVM 通过方法表(虚方法表/vtable)实现动态分派——每个类在方法区维护一张方法表,记录每个虚方法对应的实际代码入口地址。

java
Animal a = new Dog();     // 编译时类型 Animal,运行时类型 Dog
a.makeSound();            // 调用 Dog 的 makeSound,不是 Animal 的

执行过程:

  1. 编译器检查 Animal 是否有 makeSound() 方法 → 有,编译通过
  2. 运行时 JVM 查看 a 的实际类型是 Dog
  3. 查找 Dog 的方法表,找到 makeSound() 的实际地址
  4. 调用 Dog.makeSound()

4.2 为什么需要多态?

多态让代码依赖抽象而非具体实现,符合开闭原则(对扩展开放,对修改关闭)。新增子类时,使用父类引用的代码无需修改。

java
// 不依赖具体类型,新增 Cat 时此方法无需修改
void makeItSound(Animal a) {
    a.makeSound();
}

五、抽象类 vs 接口

5.1 为什么分抽象类和接口?

抽象类用于"is-a"关系,强调代码复用(有构造器、实例变量、方法实现);接口用于"can-do"能力,强调行为契约(只有方法声明,Java 8+ 可有默认方法)。

抽象类接口
继承数量单继承多实现
构造器
实例变量无(只有常量)
方法体可有Java 8+ 可有 default 方法
设计意图模板复用能力契约

5.2 接口默认方法背后的动机

Java 8 引入 default 方法是为了在给 Collection 接口添加 stream() 方法时,不破坏所有已有的实现类。默认方法自带实现,实现类可以选择重写或不重写。

java
interface Animal {
    void eat();
    default void breathe() {  // 默认实现
        System.out.println("呼吸氧气");
    }
}

六、内部类

6.1 四种内部类的原理与用途

类型定义位置特点用途
成员内部类类内部持有外部类引用,可访问外部类所有成员紧密关联的逻辑封装
静态内部类类内部 + static不持有外部类引用,只能访问静态成员工具类、Builder 模式
局部内部类方法内部作用域仅限于方法内临时封装
匿名内部类表达式位置没有类名,只能使用一次回调、事件监听

6.2 为什么匿名内部类访问局部变量要 final?

原理: 匿名内部类编译后生成独立的 .class 文件。局部变量在栈上,方法执行完就销毁;而内部类对象可能存活更久。Java 的做法是:在内部类中复制一份局部变量的值(必须是 final 的,否则两个变量不同步会导致歧义)。Java 8 后放宽为"effectively final"(只要不重新赋值即可)。

java
int x = 10;  // effectively final
Runnable r = () -> System.out.println(x);  // OK,x 没有被重新赋值

七、Object 类

所有类的根。核心方法:

java
public final native Class<?> getClass();   // 返回运行时类型(native 方法)
public native int hashCode();              // 返回哈希码(native 方法)
public boolean equals(Object obj) {        // 默认比较引用地址
    return (this == obj);
}
protected native Object clone();           // 浅拷贝,需实现 Cloneable
public String toString();                  // 默认返回 类名@哈希码十六进制
protected void finalize();                 // GC 前调用(已废弃)

为什么重写 equals 必须重写 hashCode? HashMap 先用 hashCode 定位桶,再用 equals 精确匹配。如果两个对象 equals 相等但 hashCode 不同,HashMap 会把它俩放到不同桶里,导致 map.get() 找不到正确的值。

八、面试要点

Q:面向对象三大特性?

封装:隐藏内部实现,只暴露接口;继承:子类复用父类代码;多态:同一方法调用在不同对象上表现不同行为,通过动态绑定实现。

Q:重写和重载的区别?

重写是子类重新定义父类方法(运行期多态),重载是同一类中同名不同参的方法(编译期确定)。重写时访问权限不能更低,异常不能更宽泛。

Q:抽象类和接口的区别?什么时候用哪个?

抽象类有构造器和实例变量,支持单继承,用于模板复用(is-a);接口只有常量和抽象方法(Java 8+ 可有 default),支持多实现,用于能力契约(can-do)。需要多继承行为时用接口,需要共享代码时用抽象类。

Q:为什么匿名内部类只能访问 final 局部变量?

内部类编译后是独立 .class 文件,局部变量在栈上会销毁。Java 在内部类中复制一份变量值,要求 final 确保复制后的值不会不一致。Java 8 放宽为 effectively final。