🧩 面向对象
封装、继承、多态的本质理解 · 抽象类与接口 · 重载与重写 · 内部类 · 初始化顺序 · equals/hashCode
1. 什么是面向对象?说说封装、继承、多态的本质
面向对象(OOP)以对象为中心组织代码,把数据和行为绑定在一起。三大特性是面试必问,关键在于讲出"本质"而不只是定义:
- 封装:把数据(属性)和操作数据的方法包装在类内部,通过
private隐藏细节,对外只暴露稳定的public接口。本质:信息隐藏,隔离变化,降低耦合。外部无法直接篡改内部状态,所有修改必须经过方法校验。 - 继承:子类复用父类代码,并能扩展或重写父类行为。本质:代码复用 + 表达"is-a"关系。但继承会破坏封装(子类依赖父类实现),所以组合优先于继承。
- 多态:同一方法调用,在不同对象上有不同表现。分两种:
- 编译时多态(重载 Overload)
- 运行时多态(重写 Override + 向上转型)
interface Animal { void speak(); }
class Dog implements Animal {
public void speak() { System.out.println("汪汪"); }
}
class Cat implements Animal {
public void speak() { System.out.println("喵喵"); }
}
// 编译时类型是 Animal,运行时实际调用 Dog/Cat 的方法 → 运行时多态
Animal a = new Dog();
a.speak(); // 输出:汪汪
a = new Cat();
a.speak(); // 输出:喵喵
🎯 面试要点
- 多态的三个必要条件:继承/实现、方法重写、父类引用指向子类对象
- 多态的好处:代码可扩展,新增类型无需修改调用方(开闭原则)
- Java 中多态依赖"动态绑定"(虚方法表),final/static/private 方法无法被重写,属于静态绑定
2. 抽象类和接口的区别?(必考)
JDK 8 之后接口可以拥有默认方法和静态方法,两者界限变模糊,但设计语义依然不同:
- 抽象类:强调"是什么"(is-a)。可以拥有成员变量、构造方法、普通方法;单继承,一个类只能继承一个抽象类。用于抽取多个类的公共代码。
- 接口:强调"能做什么"(has-a / can-do)。JDK8 前只有常量和方法声明;JDK8 引入 default/static 方法;JDK9 允许 private 方法。接口可以多实现。用于定义行为契约、能力规范。
选择准则:有"is-a"关系且需要共享状态/代码 → 抽象类;定义能力契约、需要多实现、面向扩展 → 接口。JDK8 后接口有 default 方法,很多"模板方法"场景也可以用接口 + 默认方法实现。
interface Flyable {
void fly(); // 抽象方法
default void hover() { // 默认方法:实现类可选择性重写
System.out.println("悬停");
}
static Flyable none() { // 静态方法:接口名直接调用
return () -> {};
}
}
🎯 面试要点
- 接口字段默认是 public static final;JDK8 起接口方法可加 default 实现
- 一个类实现两个接口,若默认方法冲突,必须重写解决
- 抽象类本质还是类:有构造方法(供子类 super 调用)、可以有 static/实例代码块
3. 重载(Overload)和重写(Override)的区别?
- 重载:同一个类中,方法名相同、参数列表不同(个数/类型/顺序)。与返回值、访问修饰符无关。属于编译期多态。
- 重写:子类中,方法签名与父类一致。属于运行期多态。要求:
- 方法名、参数列表必须完全一致
- 返回值类型可以是父类返回值的子类型(协变返回)
- 访问权限不能比父类更严格(如父类 protected → 子类不能 private)
- 抛出的异常不能比父类更宽泛
- static/final/private 方法不能被重写(static 方法重写实为"方法隐藏")
class Parent {
protected Number calc(int a) throws IOException { ... }
}
class Child extends Parent {
// 重写:权限不更严格 ✓ 协变返回 Integer ✓ 异常更具体/不抛 ✓
@Override
protected Integer calc(int a) { ... }
// 重载:同名不同参数(与父类方法无关)
public double calc(int a, double b) { ... }
}
🎯 面试要点
- 重载看"参数",重写看"签名+权限+异常",两者判断依据完全不同
- 重写必须加 @Override 吗?不必需,但强烈建议——编译器可校验签名
- 常见陷阱:构造方法不能重写;重写后调用用的是"运行时类型"(动态分派)
4. 内部类有哪几种?为什么局部内部类访问局部变量必须是 final 的?
四种内部类:
- 静态内部类(static nested class):不持有外部类引用,可以独立 new 出来。相当于"放错位置的外部类"。
- 成员内部类(inner class):必须依赖外部类实例,持有外部类引用(Outer.this)。
- 局部内部类(定义在方法内):生命周期只在方法内。
- 匿名内部类:没有类名的局部类,通常用于一次性实现接口/抽象类,Lambda 的早期替代。
为什么局部变量必须 final(或 effectively final)?因为局部变量在方法栈上,方法结束后即销毁;而内部类对象可能被引用到方法外继续存活。Java 的做法是把局部变量的副本捕获进内部类对象。若变量可变,副本与原变量就不一致了,产生数据同步问题。JDK8 起放宽为 effectively final(只要不重新赋值即可,编译器自动补 final)。
public Runnable createTask(final int id) {
// 编译器将 id 的副本传入内部类,两份值必须一致 → 要求 final
return new Runnable() {
public void run() {
System.out.println("task-" + id); // 操作的是捕获的副本
}
};
}
🎯 面试要点
- 静态内部类 vs 成员内部类:前者不持有外部引用,可独立创建,更省内存
- 匿名内部类在 JDK8 后优先用 Lambda 替代(更简洁)
- 内部类加载不依赖外部类,但内部类编译后是独立的 .class 文件(Outer$Inner.class)
5. 类的初始化顺序(经典陷阱题)
创建一个子类对象时,执行顺序为:
- 父类静态成员/静态代码块(按代码顺序,只执行一次)
- 子类静态成员/静态代码块
- 父类实例成员/实例代码块 → 父类构造方法
- 子类实例成员/实例代码块 → 子类构造方法
口诀:先静态后实例、先父后子;静态只加载一次,实例每次 new 都执行。
class A {
static { System.out.print("A-static "); }
{ System.out.print("A-instance "); }
A() { System.out.print("A-ctor "); }
}
class B extends A {
static { System.out.print("B-static "); }
{ System.out.print("B-instance "); }
B() { System.out.print("B-ctor"); }
}
new B();
new B();
输出:A-static B-static A-instance A-ctor B-instance B-ctor A-instance A-ctor B-instance B-ctor
🎯 面试要点
- 静态块只在类首次加载时执行一次(第二次 new 不再执行)
- 实例代码块在构造方法之前执行(编译期会被拷贝进每个构造方法开头)
- 子类构造方法第一行会隐式调用 super(),所以父类构造一定先于子类
6. 为什么重写 equals 必须同时重写 hashCode?
hashCode 与 equals 的约定:
- 两个对象 equals 相等 → hashCode 必须相等
- 两个对象 hashCode 相等 → equals 不一定相等(哈希冲突)
- equals 用到的字段变了 → hashCode 应随之变化(否则 HashMap 中会找不到,即"哈希槽错位")
原因:HashMap/HashSet 查找时先算 hashCode 定位桶,再用 equals 比较桶内元素。若只重写 equals 不重写 hashCode,两个相等对象会分到不同桶,导致 contains 找不到、set 中允许重复。反之 hashCode 相同但 equals 不同则无碍(只是冲突变多)。
public class User {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User u = (User) o;
return age == u.age && Objects.equals(name, u.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
🎯 面试要点
- String 是重写 equals 的经典例子(比较内容而非地址)
- HashMap 的 key 必须是不可变的(变则丢失元素),String/Integer 天然合适
- 面试延伸:equals 与 == 的区别 → == 比较引用地址(基本类型比较值)