前言
面向对象编程(Object-Oriented Programming,OOP)是 Java 语言的设计根基,也是 Java 后端面试开篇必问知识点。很多求职者可以脱口说出 “封装、继承、多态”,但面试官一旦深入追问底层原理、工程取舍、边界异常场景,很容易思路混乱、答不到要点。
面向过程编程思维:关注步骤,先做 A,再做 B,一步步完成业务;
面向对象编程思维:关注实体,把业务抽象为一个个对象,对象持有自身属性与行为,对象之间通过方法交互协作。
Java 的设计哲学是一切皆对象,而支撑整个 OOP 体系的三大支柱就是封装、继承、多态。
一句话精炼概括三大特性:
- 封装:信息隐藏,对象管控自身数据,对外提供可控访问入口,保证数据合法性;
- 继承:表达 is-a(是一种)关系,抽取公共逻辑实现代码复用,但会带来类之间强耦合;
- 多态:同一行为,不同对象拥有不同实现,依托动态绑定,实现程序的可扩展能力。
OOP 设计的终极目标:高内聚,低耦合。高内聚:一个类只负责一类职责;低耦合:类与类之间依赖尽量弱化。
一、封装(Encapsulation)
1. 核心本质
封装的本质是信息隐藏 + 访问权限控制。 把类的成员变量私有化,禁止外部代码直接读写对象内部字段;对外暴露有限的公共方法,所有读写操作都必须通过这些公共方法完成。开发者可以在方法内部增加参数校验、日志埋点、权限校验、数据格式化逻辑,以此保证对象内部的数据永远合法。
重要误区:封装不等于
private + get/set。get/set 只是封装最常见的实现手段,不是封装本身。封装的核心思想是控制信息对外暴露范围,而不是无脑生成 get、set。
2. 完整代码示例
/** * 用户领域实体,演示封装特性 */ public class User { // 私有成员变量,外部类无法直接访问 private String username; private Integer age; private String phone; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Integer getAge() { return age; } public void setAge(Integer age) { // 封装核心价值:写入数据前增加业务校验,拦截非法数据 if (age == null || age <= 0 || age >= 150) { throw new IllegalArgumentException("用户年龄非法,取值范围必须在1~149之间"); } this.age = age; } public String getPhone() { return phone; } public void setPhone(String phone) { // 手机号格式正则校验 if (phone != null && !phone.matches("^1[3-9]\\d{9}$")) { throw new IllegalArgumentException("手机号格式不符合规范"); } this.phone = phone; } }测试类:
public class EncapsulationTest { public static void main(String[] args) { User user = new User(); // user.age = -5; // 编译报错,private字段外部无法直接访问 user.setAge(25); user.setPhone("13800138000"); System.out.println("用户年龄:" + user.getAge()); System.out.println("用户手机号:" + user.getPhone()); // user.setAge(200); // 抛出非法参数异常,拦截非法数据 } }3. 工程价值
- 保护数据完整性:非法数据无法直接写入对象,校验逻辑收敛在类内部;
- 内部实现解耦:类内部逻辑修改,只要对外 public 方法签名不变,上层调用代码无需改动;
- 简化调用成本:隐藏内部复杂实现,调用方只需要关心对外暴露 API,不用关心底层细节;
- 便于统一扩展:可以在 get/set 方法统一增加日志、埋点、权限判断。
4. 真实业务使用场景
- DDD 领域实体:领域对象强依赖封装,业务规则内置在对象内部,对象自己维护自身状态;
- 工具类:内部辅助方法定义为 private,仅对外暴露核心能力;
- 配置类:敏感内部状态不对外暴露,只对外提供经过加工处理后的结果。
5. 开发编码规范
- 字段优先私有化,遵循最小权限原则;
- 不要一键生成全部字段 get/set:只读字段只提供 get 方法,内部状态字段不提供任何访问方法;
- 业务校验逻辑收敛在类内部,不要把校验散落在各个调用处;
- 对外暴露的方法尽量少,减少类对外暴露的接口。
二、继承(Inheritance)
1. 核心本质
继承用于描述is-a(是一种)关系:子类是父类的一种特例。通过extends关键字,子类可以复用父类中非私有属性与方法;同时子类可以扩展自己独有的属性与行为。
关键判断:只有满足 is-a 关系,才适合使用继承。
狗是动物,可以继承;员工和部门、人和车属于 has-a(拥有)关系,不能用继承,优先选择组合。
Java 继承硬性语法规则:
- Java 是单继承:一个类只能直接继承一个父类;一个父类可以被多个子类继承;
private私有成员,子类无法继承、无法访问;- 构造方法不能被继承;子类构造器第一行默认隐式调用
super()父类无参构造;如果父类不存在无参构造,子类必须手动写super(参数)调用父类有参构造; super关键字:代表父类实例引用,用于调用父类构造、父类成员变量、父类成员方法;final修饰的类不能被继承;final修饰的实例方法不能被子类重写。
2. 基础代码示例
// 父类Animal,抽取所有动物公共属性与公共行为 class Animal { protected String name; public void eat() { System.out.println("动物需要进食"); } } // 子类Dog:Dog is a Animal class Dog extends Animal { // 子类独有方法 public void bark() { System.out.println("汪汪汪"); } } // 子类Cat:Cat is a Animal class Cat extends Animal { public void meow() { System.out.println("喵喵喵"); } }测试代码:
public class InheritTest { public static void main(String[] args) { Dog dog = new Dog(); dog.name = "旺财"; dog.eat(); // 复用父类继承过来的方法 dog.bark();// 子类自己扩展的方法 Cat cat = new Cat(); cat.name = "咪咪"; cat.eat(); cat.meow(); } }3. 方法重写 Override
子类对父类已经定义的实例方法,重新自定义实现,这个行为叫做重写。
@Override注解作用:编译期校验,防止手敲方法名写错、参数写错,避免误以为重写成功。
重写约束规则:
- 方法名、参数列表必须和父类完全一致;支持协变返回值;
- 子类方法访问权限不能低于父类;父类 public,子类不能改成 protected/private;
- 子类抛出的受检异常范围不能大于父类抛出的异常;
- private、static、final 修饰的父类方法,不能重写;static 方法只是静态隐藏,不属于重写。
super 关键字演示重写场景:
class Animal { protected String name; public Animal(String name){ this.name = name; } public void eat(){ System.out.println(name + ":动物吃东西"); } } class Dog extends Animal{ public Dog(String name){ super(name); // 调用父类有参构造,必须写在构造方法第一行 } @Override public void eat(){ super.eat(); // 调用父类原有eat逻辑 System.out.println(name + ":狗啃骨头"); } }4. 使用场景 & 开发规范
适用场景:多个类存在大量公共逻辑,并且严格满足 is-a 关系。
禁止场景:单纯为了复用代码强行继承。has-a 关系优先使用组合。
经典设计原则:组合优于继承(Composition over Inheritance)继承是强耦合关系,父类代码一旦修改,所有子类全部受影响,风险扩散;组合是弱依赖,在类内部持有另一个类对象来复用能力,耦合更低。
组合示例(不使用继承实现能力复用)
public class Person { // has-a 关系:人拥有一辆车,使用组合 private Car car; public void drive(){ car.run(); } }三、多态(Polymorphism)【重中之重】
1. 核心本质
多态:同一个行为,不同类型对象拥有不同实现。
Java 多态最核心实现形式:父类引用指向子类对象
父类 引用变量 = new 子类();核心口诀:编译看左边,运行看右边
编译期:编译器看左边引用类型(父类),只能调用父类中声明存在的方法;
运行期:JVM 识别堆内存中真实对象(右边子类),执行子类重写后的方法。
多态成立三大前提(缺一不可)
- 存在继承 / 接口实现关系;
- 子类重写父类实例方法;
- 父类引用指向子类对象。
2. 完整代码示例
class Animal { public void eat() { System.out.println("动物进食"); } } class Dog extends Animal { @Override public void eat() { System.out.println("狗吃骨头"); } } class Cat extends Animal { @Override public void eat() { System.out.println("猫吃鱼"); } }多态调用测试:
public class PolymorphismTest { public static void main(String[] args) { // 父类引用指向子类对象 Animal a1 = new Dog(); Animal a2 = new Cat(); a1.eat(); // 输出:狗吃骨头 a2.eat(); // 输出:猫吃鱼 } }3. 向下转型 & instanceof 安全判断
父类引用无法直接调用子类独有的方法,如果需要调用子类特有方法,需要向下强制转型。
强制转型前,必须使用
instanceof做类型判断,否则运行时抛出ClassCastException类型转换异常。
Animal animal = new Dog(); if (animal instanceof Dog) { Dog dog = (Dog) animal; dog.bark(); }4. JVM 底层原理:虚方法表(vtable)【深挖点】
很多面试会追问:多态底层是如何实现的?
类加载阶段,JVM 会为每个类生成虚方法表 vtable,存放所有可重写实例方法的入口地址。
- 编译阶段:父类引用绑定方法符号引用;
- 运行阶段:JVM 根据堆中真实对象类型,查找对应虚方法表,执行子类重写后的方法,这个过程叫做动态绑定(晚期绑定)。
核心重点:成员变量不存在多态!成员变量属于静态绑定,编译期、运行期都看左边引用类型;只有实例方法存在动态绑定,才有多态。
静态方法属于类,同样不具备多态,只是静态方法隐藏。
5. 使用场景 & 开发规范
- 业务场景:策略模式、工厂模式、Spring IOC 容器、Java 集合框架,底层大量使用多态思想,面向抽象编程。
- 设计价值:符合开闭原则,对扩展开放,对修改关闭。新增子类时,原有调用代码不需要修改。
- 编码规范:向下转型前必须使用 instanceof 做类型校验;尽量面向父类 / 接口编程,不要直接依赖具体实现类。
四、重写 Override 与 重载 Overload 快速区分(对比)
重载与重写经常和三大特性一起连环考察,放在本篇一并梳理。
| 对比项 | 重载 Overload | 重写 Override |
|---|---|---|
| 发生位置 | 同一个类内部 | 父类与子类之间 |
| 方法签名 | 方法名相同,参数列表不同(个数 / 类型 / 顺序) | 方法名、参数列表完全一致 |
| 返回值 | 返回值不能作为区分重载的依据,无强制约束 | 支持协变返回,返回值必须兼容父类 |
| 异常声明 | 无异常限制 | 子类抛出受检异常不能大于父类 |
| 访问权限 | 无权限限制 | 子类权限不能小于父类 |
| 绑定时机 | 编译期静态绑定(静态多态) | 运行期动态绑定(真正多态) |
| 注解 | 不需要注解 | 推荐添加 @Override |
重载是同一个类里同名方法,不同参数;重写是子类改写父类已经存在的实例方法。
五、三大特性汇总总表
| 特性 | 核心思想 | 关键字 | 核心要点 | 设计目标 |
|---|---|---|---|---|
| 封装 | 信息隐藏,管控数据访问 | private / protected / public | 私有化字段,对外暴露可控 API,内置校验;不要无脑 get/set | 保证对象数据完整性 |
| 继承 | is-a 关系,代码复用 | extends | Java 单继承;构造不可继承;耦合高;遵循组合优于继承 | 抽取公共逻辑,谨慎使用 |
| 多态 | 同一行为多种实现 | 依赖继承 + 重写 | 父类引用指向子类对象;编译看左运行看右;虚方法表动态绑定;变量无多态 | 解耦、可扩展,面向抽象 |
六、生产环境踩坑清单(真实项目高频 bug)
- 无脑一键生成所有字段 get/set,破坏封装原则。不需要对外暴露的内部状态禁止提供访问入口;
- 滥用继承,不是 is-a 关系强行继承,造成类层级臃肿、耦合爆炸;优先使用组合复用能力;
- 重写父类方法,缩小访问权限(父类 public,子类改为 private),直接编译报错;
- 误以为成员变量有多态,业务取值出现不符合预期结果;只有实例方法有多态;
- 父类没有无参构造,子类没有显式调用 super (有参构造),直接编译失败;
- 向下转型不做 instanceof 判断,线上抛出类型转换异常;
- final 修饰的方法尝试重写、final 类尝试继承,编译报错;
- 混淆静态方法重写:静态方法属于类,不存在多态,子类静态方法只是静态隐藏,不是重写。
七、深度问答
Q1:说说面向对象三大特性,以及各自作用?
A:封装、继承、多态。 封装核心是信息隐藏,私有化对象内部数据,对外提供受控访问方法,增加校验保证数据安全; 继承基于 extends 实现 is-a 关系,复用父类非私有代码,但是耦合度高,开发遵循组合优于继承; 多态是父类引用指向子类对象,依靠继承 + 重写实现运行期动态绑定,同一行为不同对象不同实现,降低耦合,满足开闭原则。
Q2:多态成立的前提是什么?
A:①存在继承或者接口实现关系;②子类重写父类实例方法;③父类引用指向子类对象。口诀:继承、重写、父引子。
Q3:成员变量存在多态吗?为什么?
A:成员变量没有多态。成员变量是静态绑定,编译阶段绑定到引用类型;只有实例方法存在动态绑定,依靠虚方法表,运行期匹配真实对象,因此只有实例方法具备多态。
Q4:Java 为什么是单继承?
A:多继承会产生方法二义性。假设一个类同时继承 A、B 两个父类,如果 A、B 包含同名方法,编译器无法确定调用哪一个方法。Java 使用接口弥补单继承的限制:一个类可以实现多个接口,接口只定义抽象方法,没有实现逻辑,不会产生二义性。
Q5:组合和继承怎么选型?
A:满足 is-a(是一种)关系才考虑继承;has-a(拥有)关系一律使用组合。继承耦合高,父类改动会影响所有子类;组合是弱依赖,推荐优先使用组合。
八、拓展:OOP 三大特性的工程设计思考
很多人学习 OOP 仅仅停留在语法层面,但是面向对象本质是抽象思维,而不是简单写类和对象。
- 封装教会我们:对象要对自己的数据负责,外部不能随意篡改内部状态;
- 继承教会我们:代码复用是有代价的,不要单纯为了复用牺牲代码可维护性;
- 多态教会我们:编程面向抽象,而不是面向具体实现。
Spring 框架、各种中间件源码,大量依赖抽象、接口、多态,而不是堆砌继承,正是基于这套设计思想。
结语
封装、继承、多态是 Java 面向对象体系的基石,是后端第一道关卡。 学习不要死记硬背概念,要理解背后的设计取舍:封装保护数据,继承实现代码复用同时带来耦合,多态赋予程序扩展性。
工程开发牢记组合优于继承,合理使用封装,善用多态面向抽象编程,才能写出高内聚低耦合的代码。