系列介绍
欢迎来到"Java面试基础篇"系列!本系列旨在帮助Java开发者系统性地准备面试,每天精选5道经典面试题,涵盖Java基础、进阶、框架等各方面知识。坚持学习21天,助你面试通关!
历史面试题:
每日5题Java面试系列(1)
每日5题Java面试系列(2)
每日5题Java面试系列(3)
每日5题Java面试系列(4)
今日面试题
1. JDK新版本中的新特性
JDK新特性不仅仅是语法糖,更是编程范式和工程实践的演进。以下是几个重要版本的关键特性:
JDK 8 (LTS):
• Lambda表达式:函数式编程的革命性改变,使代码更简洁,为Stream API奠定基础
• Stream API:数据处理管道化,支持并行操作,大幅提升集合处理效率
• 方法引用:进一步简化Lambda表达式
• Optional:优雅处理null值,减少NullPointerException
• 新的日期时间API:解决旧Date/Calendar类的线程安全和设计问题
JDK 11 (LTS):
• 局部变量类型推断(var):减少样板代码,提高可读性
• HTTP Client API:支持HTTP/2和WebSocket,替代HttpURLConnection
• Epsilon GC:无操作垃圾收集器,适合性能测试和短生命周期应用
• ZGC (实验性):低延迟垃圾收集器,暂停时间不超过10ms
JDK 17 (LTS):
• 密封类(Sealed Classes):增强对继承层次的控制,是模式匹配的重要基础
• 模式匹配instanceof:简化类型检查和转换
• 文本块:简化多行字符串处理
• 新的垃圾收集器Shenandoah:与ZGC竞争的低暂停时间GC
JDK 21 (LTS):
• 虚拟线程(Virtual Threads):轻量级线程,大幅提升并发性能
• 结构化并发:简化多线程编程
• 记录模式:增强模式匹配能力
• 分代ZGC:改进内存管理效率
作为架构师,我会评估团队技术栈和业务需求来决定采用哪些特性。例如,对于高并发系统,虚拟线程可能是革命性的;而对于数据密集型应用,Stream API更为关键。
2. 什么是UUID,能保证唯一性吗
UUID (Universally Unique Identifier)是一个128位的标识符,标准格式为32个十六进制数字,分为5段(8-4-4-4-12)。
UUID版本:
• 版本1:基于时间戳和MAC地址
• 版本2:DCE安全UUID
• 版本3:基于MD5哈希和命名空间
• 版本4:随机生成(最常用)
• 版本5:基于SHA-1哈希和命名空间
唯一性保证:
理论上,UUID可以保证"几乎"唯一,但不是绝对唯一。具体分析:
-
版本4(随机)UUID:碰撞概率极低但不为零。根据生日问题,生成约2.71万亿个UUID时,碰撞概率约为百万分之一。
-
版本1(时间+MAC)UUID:如果MAC地址唯一且时钟正确,理论上可以保证唯一性。但存在隐私问题(暴露MAC地址)。
-
分布式系统考虑:在分布式系统中,即使使用版本1,如果时钟回拨或节点ID冲突,也可能导致重复。
建议:
• 对于大多数业务场景,UUID的唯一性已经足够
• 对强唯一性要求的场景(如金融交易ID),可考虑:
• 雪花算法(Snowflake)
• 数据库序列
• 分布式ID生成服务
• 注意UUID作为数据库主键的性能影响(索引碎片化)
3. ClassNotFoundException 和 NoClassDefFoundError 的区别
这两个异常都涉及类加载问题,但发生在不同阶段,反映了不同的根本原因
:
| 特性 | ClassNotFoundException | -NoClassDefFoundError |
|---|---|---|
| 类型 | 受检异常(Checked Exception) | 错误(Error) |
| 抛出时机 | 显式类加载时(Class.forName()等) | JVM隐式加载类时 |
| 根本原因 | 类在classpath中不存在 | 类编译时存在但运行时缺失依赖 |
| 典型场景 | 1. 拼写错误2. 依赖未正确部署3. 类加载器配置错误 | 1. 静态初始化失败2. 依赖变更未重新编译3. 版本冲突 |
| 恢复策略 | 检查classpath配置 | 检查编译环境和运行时一致性 |
深度分析:
-
类加载机制角度:
• ClassNotFoundException发生在加载阶段
• NoClassDefFoundError发生在链接阶段(验证、准备、解析之后) -
微服务环境考量:
• 在容器化部署中,依赖冲突更易导致这些问题
• 需要建立完善的依赖管理策略(如Maven的dependencyManagement) -
解决方案:
• 实现统一的类加载监控
• 建立编译-打包-部署的标准化流程
• 使用模块化(JPMS)减少冲突
4. String是如何实现不可变的
String的不可变设计是Java安全性和性能的基石,其实现机制体现了精妙的设计思想:
实现机制:
- final类:防止子类修改行为
- private final char数组(JDK8及之前)/byte数组(JDK9+)
• 数组引用不可变
• 没有暴露内部数组的方法 - 无修改状态的公共方法:
• 所有看似修改的方法(如concat, replace)都返回新String对象 - 构造函数防御性拷贝:
• 避免外部修改影响内部状态
JDK9优化:
• 引入紧凑字符串:根据内容自动选择Latin-1(1字节)或UTF-16(2字节)编码
• 减少内存占用,同时保持不可变性
独特角度:
-
安全性:
• 作为参数传递时无需担心被修改
• 安全的哈希表键值 -
性能优化:
• 字符串常量池(节省内存)
• 哈希值缓存(提升集合操作性能) -
线程安全:
• 天生线程安全,减少同步开销 -
设计模式:
• 享元模式的经典实现
不可变性的代价:
• 频繁修改时产生大量中间对象
• 解决方案:StringBuilder/StringBuffer
5. 为什么建议多用组合少用继承
"组合优于继承"是面向对象设计的重要原则,其背后有深刻的工程学考量:
技术层面原因:
-
继承破坏封装性:
• 子类依赖父类实现细节(白箱复用)
• 父类变更可能破坏子类(脆弱的基类问题) -
组合提供更大灵活性:
• 运行时动态改变行为(通过切换组件)
• 避免类爆炸(避免为每种组合创建子类) -
单一职责原则:
• 继承容易导致子类承担过多责任
• 组合更易于分离关注点 -
类型层次简化:
• 过深的继承树增加系统复杂度
• 组合保持扁平结构
架构设计影响:
-
领域模型设计:
• 组合更贴近现实世界的关系模型
• 例如:汽车"有"发动机,而不是"是"发动机 -
设计模式应用:
• 策略模式、装饰器模式等都基于组合
• 模板方法模式是继承的合理使用场景 -
API设计考量:
• 组合API更稳定,内部实现可自由变化
• 继承暴露太多内部细节,限制演化
合理使用继承的场景:
- 严格的"is-a"关系
- 框架需要控制行为流程(如模板方法)
- 需要多态但无法修改的第三方类
实践建议:
-
Liskov替换原则:
• 如果考虑继承,必须确保子类能完全替代父类 -
设计评审检查点:
• 超过两层的继承关系需要特别论证
• 检查是否有方法仅为某些子类而存在 -
现代Java特性:
• 利用接口默认方法提供部分实现
• 密封类控制可扩展性
这些原则在微服务架构中尤为重要,因为服务之间更应该通过组合(服务调用)而非继承来构建系统。

被折叠的 条评论
为什么被折叠?



