面向对象高频面试题
面试中,面向对象是绕不开的话题。本章汇总了最常见的面试题,并给出深入的回答方向。
三大特性
Q: 面向对象四大特性是什么?
大多数人会说「封装、继承、多态」,但更完整的答案是四个:封装、继承、多态、抽象。
| 特性 | 核心思想 | 解决的问题 |
|---|---|---|
| 封装 | 把数据和方法包装在一起 | 数据安全问题 |
| 继承 | 子类复用父类的代码 | 代码复用问题 |
| 多态 | 同一个行为有不同的表现 | 接口统一问题 |
| 抽象 | 忽略细节,抓住本质 | 复杂度控制问题 |
Q: 封装、继承、多态分别解决了什么问题?
- 封装:让数据不被随意修改,隐藏内部实现细节,暴露受控的访问接口
- 继承:让子类复用父类的代码,建立类层次结构
- 多态:让同一段代码在不同对象上产生不同行为,提高可扩展性
重写 vs 重载
Q: 重写和重载的区别?
这是经典送分题,但很多人只背结论,不理解本质。
| 对比项 | 重写 (Override) | 重载 (Overload) |
|---|---|---|
| 发生位置 | 父子类之间 | 同一个类中 |
| 方法名 | 必须相同 | 必须相同 |
| 参数列表 | 必须相同 | 必须不同 |
| 返回类型 | 必须相同或其子类 | 可以不同 |
| 访问修饰符 | 不能缩小 | 无关 |
| 关系 | 同一个方法的不同实现 | 完全不同的方法 |
java
class Parent {
void display(int x) { } // 父类版本
}
class Child extends Parent {
void display(int x) { } // 重写:子类覆盖父类
void display(String s) { } // 重载:新的方法
}Q: 重载遵循什么绑定机制?
重载在编译时确定,遵循静态绑定。编译器根据参数的静态类型选择调用哪个方法。
java
class Parent {
void process(int a) { System.out.println("int"); }
void process(double a) { System.out.println("double"); }
}
class Child extends Parent { }
Parent p = new Child();
p.process(10); // 编译时确定:Parent.process(int)Q: 重写遵循什么绑定机制?
重写在运行时确定,遵循动态绑定。JVM 根据对象的实际类型选择调用哪个方法。
java
class Parent {
void display() { System.out.println("Parent"); }
}
class Child extends Parent {
@Override
void display() { System.out.println("Child"); }
}
Parent p = new Child();
p.display(); // 运行时确定:Child.display()抽象类 vs 接口
Q: 抽象类和接口的区别?
这是被问得最多的问题之一。先看表格:
| 对比项 | 抽象类 | 接口 |
|---|---|---|
| 关键字 | abstract class | interface |
| 继承 | 单继承 | 多实现 |
| 构造方法 | 可以有 | 不能有 |
| 方法 | 抽象 + 普通都可以 | JDK 7 前只能是抽象方法 |
| 字段 | 无限制 | 只能是常量 (static final) |
| 访问修饰符 | 任意 | 默认 public |
| 静态方法 | 可以有 | JDK 8+ 可以有 |
| 默认方法 | 不能有 | JDK 8+ 可以有 |
Q: 什么时候用抽象类,什么时候用接口?
用一个字概括:抽象类是「是什么」,接口是「能做什么」。
java
// 抽象类:建立 is-a 关系
abstract class Animal {
String name;
abstract void eat(); // 每种动物吃的方式不同
}
class Dog extends Animal { } // Dog is an Animal
// 接口:建立 can-do 关系
interface Flyable {
void fly();
}
class Bird implements Flyable { } // Bird can flyQ: 为什么 Java 不允许多继承?
多继承会引发「菱形继承」问题:
java
class A { void display() { } }
class B extends A { }
class C extends A { }
class D extends B, C { } // ❌ D 有两个 display(),歧义!Java 选择单继承 + 多实现来解决这个问题。接口没有状态,只有方法签名,不存在歧义。
this vs super
Q: this 和 super 的区别?
| 对比项 | this | super |
|---|---|---|
| 指向 | 当前对象 | 父类对象 |
| 调用属性 | 当前类的属性(可能遮蔽) | 父类的属性 |
| 调用方法 | 当前类的方法(可能遮蔽) | 父类的方法 |
| 调用构造 | 当前类的其他构造方法 | 父类的构造方法 |
| 静态方法中 | ❌ 不能使用 | ❌ 不能使用 |
java
class Parent {
String name = "Parent";
}
class Child extends Parent {
String name = "Child";
void display() {
System.out.println(this.name); // Child
System.out.println(super.name); // Parent
}
}Q: 子类构造方法中,为什么 super() 必须在第一行?
因为子类需要先完成父类的初始化,才能进行自己的初始化。如果 super() 不在第一行,子类的代码可能在父类初始化之前运行,引发不可预期的错误。
java
class Parent {
Parent() {
System.out.println("Parent 初始化完成");
}
}
class Child extends Parent {
Child() {
// super() 隐式调用,必须在第一行
System.out.println("Child 初始化完成");
}
}多态
Q: 多态的实现原理?
运行时多态通过动态绑定实现:
- 编译时:编译器检查父类中是否有这个方法(只检查签名)
- 运行时:JVM 根据对象的实际类型,找到真正的方法地址并调用
java
class Animal {
void eat() { System.out.println("Animal eat"); }
}
class Dog extends Animal {
@Override
void eat() { System.out.println("Dog eat"); }
}
class Cat extends Animal {
@Override
void eat() { System.out.println("Cat eat"); }
}
Animal a = new Dog();
a.eat(); // 输出: Dog eatQ: 向上转型和向下转型的区别?
- 向上转型:子类转父类,自动转换,安全
- 向下转型:父类转子类,需要强制转换,有风险
java
Animal animal = new Dog(); // 向上转型:自动
Dog dog = (Dog) animal; // 向下转型:需要检查
// 危险!
Animal a = new Animal();
Dog d = (Dog) a; // ClassCastException!所以向下转型前,先用 instanceof 检查。
Q: 为什么需要多态?
三个字:解耦合。
java
// ❌ 没有多态:每种动物写一段代码
void feedAnimal(Dog dog) { dog.eat(); }
void feedAnimal(Cat cat) { cat.eat(); }
// ✅ 有多态:统一处理
void feedAnimal(Animal animal) {
animal.eat(); // 不管是 Dog 还是 Cat,都能正确处理
}final
Q: final 修饰的变量一定是常量吗?
分两种情况:
- 基本类型:值不可变
- 引用类型:引用地址不可变,但对象内容可以变
java
final int NUM = 10;
NUM = 20; // ❌ 编译错误
final StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // ✅ 可以修改对象内容
sb = new StringBuilder(); // ❌ 引用不能改变Q: final 修饰方法有什么意义?
防止子类重写,确保这个方法的行为永远不变。String 类的 trim() 方法就是 final 的,防止被篡改。
Q: 为什么 String 是 final 的?
三个原因:
- 安全:防止被继承后修改,导致字符串比较出现漏洞
- 性能:JVM 可以对 final 方法进行内联优化
- 字符串常量池:String 对象被广泛共享,如果可以被继承修改,会造成安全隐患
内部类
Q: 内部类有什么优势?
- 逻辑封装:一个类只被另一个类使用,逻辑上紧密相关
- 访问私有成员:内部类可以访问外部类的所有成员
- 命名空间:内部类属于外部类,不会与其他类的名字冲突
java
class Outer {
private int data;
class Inner {
void accessOuter() {
data = 10; // ✅ 可以访问外部类的私有成员
}
}
}Q: 为什么局部内部类和匿名内部类访问局部变量时必须是 final 或 effectively final?
因为局部变量在栈上,内部类对象在堆上。如果允许内部类修改局部变量,而局部变量已经出栈消失,就会出问题。所以 Java 要求局部变量是 final 或 effectively final(只赋值一次)。
JDK 8+ 自动为「只赋值一次」的局部变量加上 final 语义。
枚举
Q: 枚举为什么是线程安全的?
枚举值在类加载时就创建好了,JVM 保证 static final 字段在类加载阶段完成初始化,且只创建一次。这是 JVM 的内部机制保证的,不需要程序员额外处理。
总结
面向对象面试的核心在于理解本质,而不是背答案。面试官追问的往往是「为什么」。
记住几个关键问题的思考路径:
- 多态的实现原理 → 动态绑定
- 为什么不能多继承 → 菱形继承问题
- 什么时候用抽象类 → is-a 关系
- 什么时候用接口 → can-do 关系
- 为什么 String 是 final → 安全 + 性能 + 字符串常量池
