Appearance
面向对象
一、为什么需要面向对象?
面向过程(如 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 的执行过程:
- 编译器检查 Animal 是否有 makeSound() 方法 → 有,编译通过
- 运行时 JVM 查看 a 的实际类型是 Dog
- 查找 Dog 的方法表,找到 makeSound() 的实际地址
- 调用 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。
