每日 Java 面试题分享【第 16 天】
欢迎来到每日 Java 面试题分享栏目!
订阅专栏,不错过每一天的练习
今日分享 3 道面试题目!
评论区复述一遍印象更深刻噢~
目录
- 问题一:Java 运行时异常和编译时异常之间的区别是什么?
- 问题二:什么是 Java 中的继承机制?
- 问题三:什么是 Java 的封装特性?
问题:Java 运行时异常和编译时异常之间的区别是什么?
面试官考察点
- 异常分类理解:对 Java 异常体系(Throwable、Error、Exception、RuntimeException)的掌握程度。
- 处理机制:是否清楚两种异常在代码中的处理方式差异(强制处理 vs 非强制)。
- 设计意图:能否理解 Java 对这两类异常的设计哲学(可控性问题 vs 程序逻辑错误)。
- 实战经验:是否能在项目中正确选择异常类型,避免滥用。
参考答案
1. 定义与继承关系
- 编译时异常(Checked Exception):
- 继承自
Exception
,但不是RuntimeException
的子类。 - 例如:
IOException
、SQLException
、ClassNotFoundException
。 - 必须在代码中显式处理(
try-catch
捕获或throws
声明),否则编译失败。
- 继承自
- 运行时异常(Unchecked Exception):
- 继承自
RuntimeException
(RuntimeException
本身继承Exception
)。 - 例如:
NullPointerException
、ArrayIndexOutOfBoundsException
、IllegalArgumentException
。 - 不需要强制处理,代码中可以不捕获或声明。
- 继承自
2. 核心区别
维度 | 编译时异常 | 运行时异常 |
---|---|---|
处理要求 | 必须显式处理,否则编译失败 | 可不处理,由 JVM 抛出并终止线程 |
设计目的 | 表示程序外部可控问题(如文件不存在、网络中断) | 表示程序内部逻辑错误(如空指针、数组越界) |
代码可读性 | 通过 throws 声明明确调用方需处理的异常类型 | 通常通过代码逻辑规避,而非显式处理 |
3. 底层机制
- 编译时异常:
- 编译器通过语法检查强制约束,确保程序员对可能发生的 " 已知风险 " 进行处理。
- 例如,读取文件时必须处理
IOException
,防止程序因外部资源问题崩溃。
- 运行时异常:
- 通常由程序逻辑错误引发,属于 " 程序员应避免的错误 ",如未做空判断直接调用方法。
- JVM 在运行时会自动抛出,若不捕获则线程终止(可通过全局异常处理器兜底,如 Spring 的
@ControllerAdvice
)。
4. 项目实战结合
场景:在电商项目的订单支付模块中,调用第三方支付接口时可能发生网络超时(编译时异常),而参数校验不通过(如金额为负数)属于运行时异常。
处理方式:
- 编译时异常:
try { PaymentResponse response = paymentClient.call(externalApi); } catch (IOException e) { // 记录日志并触发重试机制 log.error("支付接口调用失败", e); retryPolicy.retry(); }
- 运行时异常:
// 参数校验(通过Preconditions或Assert) public void createOrder(BigDecimal amount) { Preconditions.checkArgument(amount.compareTo(BigDecimal.ZERO) > 0, "金额必须大于0"); // 业务逻辑 }
总结:
- 编译时异常用于外部依赖的容错处理(如 IO、数据库连接)。
- 运行时异常用于快速暴露代码逻辑缺陷,避免无效状态扩散。
5. 高频追问预判
-
为什么 Java 要设计两种异常?
- 答:编译时异常强制处理 " 已知但不可控 " 的问题(如文件丢失),运行时异常用于标识 " 本应通过代码避免 " 的逻辑错误,减少冗余的
try-catch
代码。
- 答:编译时异常强制处理 " 已知但不可控 " 的问题(如文件丢失),运行时异常用于标识 " 本应通过代码避免 " 的逻辑错误,减少冗余的
-
如何自定义异常?应该继承哪一类?
- 答:业务异常通常继承
RuntimeException
(如BusinessException
),避免调用方强制处理;若异常需调用方显式关注(如特定 API 的错误码),可继承Exception
。
- 答:业务异常通常继承
-
实际项目中常见的异常处理误区?
- 答:
- 捕获
Exception
但不处理(如e.printStackTrace()
),导致问题被掩盖。 - 滥用
RuntimeException
传递业务错误,应通过返回错误码或自定义状态对象。
- 捕获
- 答:
通过分层拆解异常的设计哲学、处理机制和实战场景,可以体现对 Java 异常体系的深入理解,这正是大厂面试官期待的答案!
问题:什么是 Java 中的继承机制?
面试官考察点
- 面向对象基础:是否理解继承在面向对象编程中的核心地位。
- 实现细节:对继承的语法、方法重写(Override)、访问控制等机制的掌握。
- 设计思想:能否区分继承与组合的适用场景,避免滥用继承。
- 底层原理:对 JVM 中继承实现机制(如方法表、内存结构)的理解。
参考答案
1. 核心定义与语法
-
定义:继承是面向对象编程中类与类之间的一种关系,允许子类(派生类)复用父类(基类)的属性和方法,并可以通过重写(Override)或扩展实现新功能。
-
语法:
class Parent { public void print() { System.out.println("Parent Method"); } } class Child extends Parent { @Override public void print() { super.print(); // 调用父类方法 System.out.println("Child Method"); } }
-
关键字:
extends
(单继承)、super
(访问父类成员)、@Override
(注解声明方法重写)。
2. 核心机制与规则
- 单继承限制:Java 不支持多继承(一个类只能直接继承一个父类),但可通过接口(
implements
)实现多继承效果。 - 访问权限控制:
- 子类可访问父类的
public
、protected
成员,但无法直接访问private
成员(需通过父类提供的公共方法)。 - 父类的构造方法不继承,但子类构造器必须显式或隐式调用父类构造器(
super()
)。
- 子类可访问父类的
- 方法重写规则(Override):
- 签名一致:方法名、参数列表、返回类型(Java 5+ 允许协变返回类型)必须相同。
- 访问权限:子类方法的访问权限不能比父类更严格(例如父类
protected
,子类不能改为private
)。 - 异常声明:子类方法抛出的异常不能比父类更宽泛(可抛出更具体异常或不抛出)。
3. 底层实现原理
- 内存结构:子类对象在堆中会包含父类的实例变量(即使为
private
,但无法直接访问)。 - 方法调用:
- JVM 通过虚方法表(vtable) 实现动态绑定(多态)。
- 每个类的方法表存储其所有可继承方法的入口地址,子类重写的方法会覆盖父类方法在表中的引用。
- 类加载机制:
- 加载子类时,JVM 会先递归加载其父类(直至
Object
类)。 - 父类的静态代码块优先于子类执行。
- 加载子类时,JVM 会先递归加载其父类(直至
4. 项目实战结合
场景:在电商系统的订单模块中,抽象出
BaseOrder
类,包含订单创建时间、订单状态等公共字段和方法,NormalOrder
(普通订单)和GroupBuyOrder
(团购订单)继承并扩展特定逻辑。public abstract class BaseOrder { protected LocalDateTime createTime; protected OrderStatus status; public void validate() { if (createTime == null) { throw new IllegalArgumentException("创建时间不能为空"); } } } public class GroupBuyOrder extends BaseOrder { private int groupId; @Override public void validate() { super.validate(); // 复用父类校验 if (groupId <= 0) { throw new IllegalArgumentException("团购ID无效"); } } }
设计要点:
- 通过继承实现代码复用,避免重复校验逻辑。
- 使用抽象类定义通用行为,子类通过重写扩展差异化逻辑。
5. 继承 vs 组合
维度 | 继承(is-a) | 组合(has-a) |
---|---|---|
关系 | 强耦合,子类依赖父类实现 | 松耦合,通过持有其他类的对象实现功能复用 |
灵活性 | 父类修改可能破坏子类 | 可动态替换组合对象(如策略模式) |
适用场景 | 明确 " 是一种 " 关系(如 Dog extends Animal ) | 功能复用但无需继承全部能力(如 Car has Engine ) |
最佳实践:优先使用组合,仅在逻辑上严格符合 “is-a” 关系时使用继承(遵循里氏替换原则)。
6. 高频追问预判
-
为什么 Java 不支持多继承?
- 答:避免 " 菱形继承问题 "(多个父类有同名方法时冲突)。Java 通过接口(支持多实现)和内部类间接解决。
-
子类实例化时父类的构造方法如何调用?
- 答:子类构造器默认隐式调用父类无参构造器(
super()
),若父类没有无参构造器,子类必须显式调用super(args)
。
- 答:子类构造器默认隐式调用父类无参构造器(
-
重写(Override)和重载(Overload)的区别?
- 答:
- 重写:子类重新定义父类方法,方法签名相同,实现多态。
- 重载:同一类中方法名相同但参数列表不同,实现方法多样化调用。
- 答:
通过结合语法、底层原理、设计原则和实战案例,可以全面展示对继承机制的掌握,这正是大厂面试中区分候选人的关键点!
问题:什么是 Java 的封装特性?
面试官考察点
- 面向对象基础:是否理解封装在面向对象编程中的核心意义。
- 实现手段:对访问控制修饰符(
private
/protected
/public
)和方法的合理使用。 - 设计思想:能否结合高内聚、低耦合原则,说明封装如何提升代码健壮性。
- 实战经验:是否在项目中正确应用封装解决实际问题(如数据校验、逻辑隔离)。
参考答案
1. 核心定义与目的
- 定义:封装(Encapsulation)是面向对象编程的三大特性之一,指将数据(属性)和行为(方法)绑定为一个类,并对外隐藏内部实现细节,仅通过受控的接口暴露必要功能。
- 目的:
- 安全性:防止外部直接修改对象内部状态(如字段非法赋值)。
- 灵活性:内部实现可独立修改,不影响外部调用方。
- 易用性:通过明确的接口简化复杂逻辑的使用(如
ArrayList
隐藏动态扩容细节)。
2. 核心机制与实现
-
访问控制修饰符:
修饰符 类内 包内 子类 任意位置 private
✔ ✖ ✖ ✖ protected
✔ ✔ ✔ ✖ public
✔ ✔ ✔ ✔ -
典型实现方式:
public class BankAccount { // 私有字段:外部无法直接访问 private double balance; // 公有方法:受控的访问入口 public void deposit(double amount) { if (amount > 0) { balance += amount; } else { throw new IllegalArgumentException("存款金额必须大于0"); } } public double getBalance() { return balance; } }
- 隐藏实现:余额
balance
字段私有,防止外部直接修改。 - 逻辑封装:存款操作通过
deposit()
方法实现校验和计算。
- 隐藏实现:余额
3. 底层原理与设计原则
- 数据隐藏的本质:
- JVM 允许通过反射强制访问私有字段(
setAccessible(true)
),但封装是设计层面的约束,依赖于开发者遵守规范。
- JVM 允许通过反射强制访问私有字段(
- 与设计原则的关联:
- 迪米特法则(最少知识原则):只与直接朋友交互,避免暴露过多细节。
- 开闭原则:通过封装内部实现,使得类可以扩展(新增功能)而无需修改已有接口。
4. 项目实战结合
场景:在电商系统的用户模块中,封装用户敏感信息(如密码),确保数据安全和一致性。
public class User { private String username; private String encryptedPassword; // 加密后的密码 public void setPassword(String plainPassword) { if (plainPassword.length() < 8) { throw new IllegalArgumentException("密码长度至少8位"); } this.encryptedPassword = encrypt(plainPassword); // 加密逻辑封装在内部 } public boolean validatePassword(String input) { return encrypt(input).equals(encryptedPassword); } // 私有方法:隐藏加密算法细节 private String encrypt(String data) { // 使用SHA-256等算法加密 } }
设计优势:
- 密码存储与校验逻辑封装在
User
类内部,外部无法绕过规则直接修改。- 加密算法变更时(如从 MD5 升级为 SHA-256),只需修改
encrypt()
方法,不影响调用方。
5. 封装的多层次性
层级 | 示例 | 封装目标 |
---|---|---|
类级别 | 字段私有化 + 公共方法 | 保护对象状态,隐藏实现细节 |
包级别 | 使用包级私有(无修饰符)类或方法 | 限制跨包访问,实现模块内高内聚 |
模块级 | Java 9 模块化(module-info.java ) | 控制模块间的依赖和暴露(如 Spring Boot) |
6. 高频追问预判
-
封装与抽象的区别?
- 答:
- 封装:隐藏实现细节,控制访问(解决 " 怎么做 " 的暴露问题)。
- 抽象:提取共性,定义接口或抽象类(解决 " 做什么 " 的规范问题)。
- 示例:
List
接口抽象了 " 线性表 " 操作,ArrayList
封装了动态数组的实现细节。
- 答:
-
什么时候该用
protected
修饰符?- 答:当需要允许子类访问父类成员,但禁止非子类的外部访问时(如模板方法模式中的钩子方法)。
-
如何避免过度封装?
- 答:
- 避免为每个字段机械添加 getter/setter,应根据业务需求设计接口。
- 例如,订单的
totalPrice
可能不需要 setter,而是通过calculateTotal()
方法内部计算。
- 答:
通过结合语法规范、设计原则和实战案例,可以清晰展示对封装特性的深入理解,这正是大厂面试中区分候选人的关键!
总结
今天的 3 道 Java 面试题,您是否掌握了呢?持续关注我们的每日分享,深入学习 Java 面试的各个细节,快速提升技术能力!如果有任何疑问,欢迎在评论区留言,我们会第一时间解答!
明天见!🎉