面向对象
问题
Java 面向对象的三大特性是什么?抽象类和接口有什么区别?方法重载和重写的区别?
答案
三大特性
1. 封装(Encapsulation)
将数据和操作数据的方法绑定在一起,对外隐藏内部实现细节,只暴露必要的接口:
Account.java
public class Account {
// 字段私有化,外部无法直接访问
private double balance;
public Account(double initialBalance) {
if (initialBalance < 0) {
throw new IllegalArgumentException("初始余额不能为负");
}
this.balance = initialBalance;
}
// 通过公开方法控制访问,可以加入校验逻辑
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("存款金额必须为正");
this.balance += amount;
}
public double getBalance() {
return balance;
}
}
四种访问修饰符:
| 修饰符 | 同类 | 同包 | 子类 | 不同包 |
|---|---|---|---|---|
private | ✅ | ❌ | ❌ | ❌ |
| 默认(无修饰符) | ✅ | ✅ | ❌ | ❌ |
protected | ✅ | ✅ | ✅ | ❌ |
public | ✅ | ✅ | ✅ | ✅ |
2. 继承(Inheritance)
子类继承父类的属性和方法,实现代码复用:
InheritanceDemo.java
public class Animal {
protected String name;
public void eat() {
System.out.println(name + " is eating");
}
}
public class Dog extends Animal {
public Dog(String name) {
this.name = name;
}
// 子类特有方法
public void bark() {
System.out.println(name + " is barking");
}
}
Java 继承的特点
- 单继承:一个类只能继承一个父类(
extends单个类) - 多层继承:A → B → C 是允许的
- 多实现:一个类可以实现多个接口(
implements多个接口) - 所有类的根类是
Object
3. 多态(Polymorphism)
同一个方法调用,根据对象的实际类型执行不同的实现:
PolymorphismDemo.java
public class Shape {
public double area() { return 0; }
}
public class Circle extends Shape {
private double radius;
public Circle(double radius) { this.radius = radius; }
@Override
public double area() { return Math.PI * radius * radius; }
}
public class Rectangle extends Shape {
private double width, height;
public Rectangle(double w, double h) { this.width = w; this.height = h; }
@Override
public double area() { return width * height; }
}
// 使用多态:统一接口,不同实现
Shape shape1 = new Circle(5); // 父类引用指向子类对象
Shape shape2 = new Rectangle(3, 4);
System.out.println(shape1.area()); // 78.54(运行时调用 Circle.area())
System.out.println(shape2.area()); // 12.0(运行时调用 Rectangle.area())
多态的实现条件:
- 继承或实现接口
- 方法重写(Override)
- 父类引用指向子类对象
多态的绑定时机:
- 静态绑定(编译期):方法重载、
private/static/final方法 - 动态绑定(运行期):方法重写,通过虚方法表(vtable)实现
方法重载 vs 方法重写
| 维度 | 重载(Overload) | 重写(Override) |
|---|---|---|
| 发生位置 | 同一个类中 | 子类与父类之间 |
| 方法名 | 相同 | 相同 |
| 参数列表 | 必须不同 | 必须相同 |
| 返回类型 | 可以不同 | 相同或协变(子类类型) |
| 访问修饰符 | 无要求 | 不能比父类更严格 |
| 异常 | 无要求 | 不能抛出更宽泛的受检异常 |
| 绑定时机 | 编译期 | 运行期 |
OverloadVsOverride.java
// 重载:同一个类中,方法名相同,参数不同
public class Calculator {
public int add(int a, int b) { return a + b; }
public double add(double a, double b) { return a + b; }
public int add(int a, int b, int c) { return a + b + c; }
}
// 重写:子类覆盖父类方法
public class Animal {
public void speak() { System.out.println("..."); }
}
public class Cat extends Animal {
@Override // 推荐加此注解,编译器会校验
public void speak() { System.out.println("Meow!"); }
}
抽象类 vs 接口
| 维度 | 抽象类(abstract class) | 接口(interface) |
|---|---|---|
| 实例化 | 不能 | 不能 |
| 构造方法 | 有 | 没有 |
| 成员变量 | 任意变量 | 只能 public static final 常量 |
| 方法 | 可以有抽象和具体方法 | JDK 8 前只有抽象方法 |
| 默认方法 | — | JDK 8+ 支持 default 方法 |
| 静态方法 | 支持 | JDK 8+ 支持 |
| 私有方法 | 支持 | JDK 9+ 支持 |
| 继承/实现 | 单继承 | 多实现 |
| 设计理念 | is-a(是什么) | has-a / can-do(能做什么) |
AbstractVsInterface.java
// 抽象类:模板方法模式,定义骨架
public abstract class AbstractLogger {
// 具体方法:通用逻辑
public void log(String message) {
String formatted = formatTime() + " " + message;
doLog(formatted);
}
// 抽象方法:子类必须实现
protected abstract void doLog(String message);
private String formatTime() {
return LocalDateTime.now().toString();
}
}
// 接口:定义能力约定
public interface Serializable { }
public interface Comparable<T> {
int compareTo(T o);
}
// JDK 8 接口默认方法
public interface Collection<E> {
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
}
选择抽象类还是接口?
- 抽象类:需要共享代码(模板方法)、需要非
public成员、需要构造器 - 接口:定义行为契约、需要多实现、定义混入能力(Mixin)
- JDK 8+ 趋势:接口有了
default方法后,很多以前需要抽象类的场景可以用接口代替
Object 类的常用方法
所有 Java 类都继承自 Object,其核心方法:
| 方法 | 说明 | 是否建议重写 |
|---|---|---|
equals(Object) | 判断对象是否相等 | 是 |
hashCode() | 返回哈希值 | 是(与 equals 一起) |
toString() | 返回字符串表示 | 是 |
getClass() | 获取运行时类型 | 否(final) |
clone() | 浅拷贝 | 按需 |
finalize() | GC 前调用 | 否(已废弃) |
wait()/notify()/notifyAll() | 线程通信 | 否 |
EqualsAndHashCode.java
public class Person {
private String name;
private int age;
// 重写 equals:比较字段内容
@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);
}
// 重写 hashCode:保证 equals 相等的对象 hashCode 也相等
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
equals 和 hashCode 的契约
equals相等的两个对象,hashCode必须相等hashCode相等的两个对象,equals不一定相等(哈希碰撞)- 重写
equals时必须同时重写hashCode,否则在 HashMap 等集合中会出问题
常见面试问题
Q1: 面向对象和面向过程的区别?
答案:
| 维度 | 面向过程 | 面向对象 |
|---|---|---|
| 核心 | 过程/函数 | 对象 |
| 思维方式 | 按步骤分解任务 | 按对象/职责分解 |
| 数据与行为 | 分离 | 封装在一起 |
| 代码复用 | 函数调用 | 继承、组合、多态 |
| 扩展性 | 修改函数 | 新增子类 |
| 典型语言 | C | Java、C++、Python |
面向对象更适合复杂的业务系统,面向过程在性能敏感的底层代码中有优势。
Q2: Java 是值传递还是引用传递?
答案:
Java 只有值传递。但需要区分:
- 基本类型:传递值的拷贝,方法内修改不影响原变量
- 引用类型:传递引用(指针)的拷贝,指向同一个对象。方法内可以修改对象的字段,但不能让原引用指向新对象
public void swap(Integer a, Integer b) {
Integer temp = a;
a = b;
b = temp;
// 这里只是交换了局部变量的引用,原始变量不变
}
Q3: 深拷贝和浅拷贝的区别?
答案:
- 浅拷贝:
clone()默认行为,基本类型复制值,引用类型复制引用(指向同一个对象) - 深拷贝:递归复制所有引用对象,完全独立的副本
实现深拷贝的方式:
- 重写
clone()方法,对引用类型字段逐个 clone - 通过序列化/反序列化(
ObjectOutputStream→ObjectInputStream) - JSON 序列化/反序列化
Q4: final、finally、finalize 的区别?
答案:
| 关键字 | 用途 |
|---|---|
final | 修饰类(不可继承)、方法(不可重写)、变量(不可重新赋值) |
finally | try-catch-finally 中的代码块,无论是否异常都会执行 |
finalize | Object 的方法,GC 前调用,已废弃(JDK 9+标记为 @Deprecated) |
Q5: 接口中可以有方法实现吗?
答案:
JDK 8 之后可以:
default方法:提供默认实现,实现类可选择重写static方法:通过接口名直接调用
JDK 9 还支持 private 方法,用于 default 方法之间共享代码。
public interface Logger {
void log(String msg); // 抽象方法
default void info(String msg) { // JDK 8 默认方法
log("[INFO] " + msg);
}
static Logger console() { // JDK 8 静态方法
return msg -> System.out.println(msg);
}
private String format(String msg) { // JDK 9 私有方法
return LocalDateTime.now() + " " + msg;
}
}
Q6: 为什么 Java 不支持多继承?
答案:
多继承会引发菱形问题(Diamond Problem):如果 B 和 C 都继承 A,D 同时继承 B 和 C,当调用 D 中从 A 继承的方法时,到底用 B 还是 C 的版本?
Java 通过单继承 + 多接口实现来避免此问题。JDK 8 接口的 default 方法引入了类似的冲突可能,但有明确的解决规则:
- 类优先:类中的方法优先于接口的默认方法
- 子接口优先:更具体的接口默认方法优先
- 显式选择:如果有歧义,必须在实现类中显式重写
Q7: 成员变量和局部变量的区别?
答案:
| 维度 | 成员变量 | 局部变量 |
|---|---|---|
| 位置 | 类中,方法外 | 方法/代码块中 |
| 存储 | 堆(随对象) | 栈(随方法) |
| 默认值 | 有(0/null/false) | 无,必须初始化 |
| 作用域 | 整个类 | 方法/代码块内 |
| 修饰符 | 可用访问修饰符、static 等 | 只能用 final |
| 生命周期 | 随对象创建/销毁 | 随方法调用/结束 |
Q8: this 和 super 关键字的作用?
答案:
-
this:引用当前对象this.field:访问当前对象的字段(区别于同名局部变量)this.method():调用当前对象的方法this(args):在构造方法中调用本类的其他构造方法(必须在第一行)
-
super:引用父类super.field:访问父类字段super.method():调用父类方法(常用于重写中保留父类逻辑)super(args):在构造方法中调用父类构造方法(必须在第一行)
this() 和 super() 不能同时出现在同一个构造方法中。