Java 面试题集 -- 001

Java 面试题集

1. 请说一下HashSet的底层实现原理

HashSet的本质上是基于hashmap实现的Set接口集合,它的底层是一个HashMap,我们存放的元素是存到了HashMap的key上,而Value则统一用了一个Present 的 Object对象占位;由于HashMap的key值不允许重复,HashSet就是利用这样特性来保证元素不重复。具体判断逻辑是,先比较HashCode是否相同,如果相同则去使用equals方法比较内容是否相同,如果都相同就认为两者是同一个元素,需要实现去重。

2. 既然你说是基于HashMap,既然你说是基于 HashMap,那如果我要往 HashSet 里存自定义的对象(比如 User 类),需要做什么特殊处理吗?为什么

需要重新对象的hashcode值和equal方法,如果不重写hashset会默认根据对象的内存地址来判断对象是否重复,这样即使两个对象的内容完全一致,但是他们是不同的实列对象,就会被当作不重复元素添加进去,导致去重失败。

而且重写了hashcode必须重写equal方法,hashcode比较只是起到快速筛选的作用,如果不重写hashcode,如果不同,则会直接判定不相等,不调用equals方法

而如果只重写hashcode不重写equals方法,就会导致比较两个对象的内存地址,即使内容相等,但是比较也是false,这是因为equals方法比较的是对象的引用地址,两个相同内容的实例对象的引用地址是不同的,所以必须重写equals方法,

3. 在 MySQL 中,索引(Index)是干什么用的?它有什么优缺点?我们在写 SQL 语句时,有哪些情况会导致索引失效?

Mysql的默认索引是采用了B+树的数据结构来实现的,它通过空间换取时间的方式,极大的降低了I/O操作次数,从而方便快速搜索。

它的优缺点,优点是提高了查询速度,缺点是会占用物理存储空间,并执行新增、修改、删除操作时,需要额外去动态的维护索引结构,降低写操作的性能。

在实际开发中,我总结了以下几种常见的索引失效情况:

  • 违反最左前缀原则:在使用联合索引时,查询条件必须从索引的最左列开始,中间不能断开或跳过。
  • 对索引列进行运算或使用函数 :比如 WHERE age + 1 = 10WHERE DATE(create_time) = '2023-01-01',这会导致索引失效。
  • LIKE 以通配符 % 开头 :比如 WHERE name LIKE '%卷卷',这种左模糊查询无法利用 B+Tree 的有序特性。
  • OR 条件导致失效:如果 OR 连接的其中一个字段没有索引,或者存在隐式类型转换,可能导致整个 SQL 放弃使用索引。
  • 隐式类型转换 :比如索引列是字符串类型,查询时却用了数字(WHERE phone = 138...),数据库会自动转换类型,导致索引失效。
  • 使用不等于 (!= 或 <>):查询优化器认为全表扫描可能比走索引更快,从而放弃索引。"

4. Spring 中的声明式事务(@Transactional)是如何实现的?如果在 Service 类的一个普通方法 A 中调用了另一个加了 @Transactional 注解的方法 B,事务失效了,你觉得可能是什么原因?如何解决?

第一,实现原理:

Spring 的声明式事务是基于 AOP(面向切面编程) 实现的。底层默认使用 JDK 动态代理或 CGLIB 来创建代理对象。当外部调用一个标注了 @Transactional 的方法时,实际上是调用的代理对象。代理对象负责在目标方法执行前开启事务,执行后提交事务,如果捕获到异常则进行回滚。

第二,失效原因:

这种失效通常被称为**"自调用"问题。** 当方法 A 调用同一个类中的方法 B 时,由于是通过 this 关键字直接调用的,调用链路并没有经过 Spring 容器生成的代理对象。既然没有经过代理,代理对象中关于 B 方法的事务拦截器(Interceptor)自然也就不会生效,导致 B 的事务注解被忽略。

第三,解决方案:

除了将事务逻辑合并到 A 方法中(给 A 也加上注解),我更推荐以下两种方式:

  1. 服务拆分:将 B 方法抽取到一个新的 Service 类中,通过 Spring 的依赖注入来调用。这是符合单一职责原则的,也是最推荐的做法。
  2. 自我注入 :在当前类中注入自己(需要加上 @Lazy 防止循环依赖),然后通过注入的实例来调用 B 方法,这样就能强制走代理逻辑。
  3. 架构调整:如果业务逻辑允许,尽量避免这种内部方法的事务嵌套,通过上层服务来协调。"

🚀 进阶追问(准备一下)

面试官可能会继续追问你对事务传播行为的理解:

"如果必须在同一个类中处理,且 A 方法已经有事务,B 方法必须独立提交(比如记日志),即使 A 回滚了 B 也不能回滚,这个时候 @Transactional 的传播行为应该设置成什么?"

  • 考察点Propagation.REQUIRES_NEW
  • 答案 :应该设置为 REQUIRES_NEW。但这依然解决不了"自调用"问题,必须配合上面的"提取 Service"才能生效。

5. 除了刚才说的自调用问题,如果在 Spring 事务中,我们执行了一条 SQL 更新语句,紧接着查询这条数据,发现查不到更新后的值,导致后续业务逻辑出错。除了代码逻辑问题,你觉得数据库层面可能是什么原因导致的?如何解决?

这个问题非常典型,通常是由 MySQL 的 MVCC(多版本并发控制)机制事务隔离级别共同导致的。

1. 原因分析

MySQL 的默认隔离级别是 REPEATABLE READ 。在这个级别下,事务在执行过程中会维护一个一致性视图(Read View)。当我们在这个事务中先执行更新(Update),紧接着执行查询(Select)时:

  • 更新操作:会生成一个新的数据版本。
  • 后续查询 :如果是一个普通的快照读(Snapshot Read,即普通的 SELECT),它读取的依然是事务开启时的那个快照,而不是最新的版本。因此,查询结果可能还是旧的,导致看起来像是'更新失效'或者'查不到数据'。

2. 解决方案

针对这个问题,我通常有以下几种解决思路:

  • 方案一(推荐):使用"当前读"
    在查询 SQL 后面加上 FOR UPDATE 或者 LOCK IN SHARE MODE 。加锁的查询属于当前读(Current Read),它会忽略 MVCC 的快照,直接读取最新的数据版本,这样就能查到刚刚更新的值了。
  • 方案二(代码优化):利用缓存或变量
    既然是在同一个事务、同一个方法流中,刚更新完的数据其实已经在内存里了(比如 MyBatis 的一级缓存/Session 缓存)。我们可以直接从缓存中获取,或者直接使用之前 set 好的 Java 对象,避免无谓的数据库回查,这样性能也更高。
  • 方案三(不推荐):调整隔离级别
    虽然将隔离级别改为 READ COMMITTEDREAD UNCOMMITTED 理论上能解决可见性问题,但这会带来脏读、不可重复读的风险,严重破坏数据一致性,在生产环境中是绝对禁止为了这种业务逻辑去调整的。"
相关推荐
Anastasiozzzz7 分钟前
Java Lambda 揭秘:从匿名内部类到底层原理的深度解析
java·开发语言
骇客野人9 分钟前
通过脚本推送Docker镜像
java·docker·容器
刘琦沛在进步11 分钟前
【C / C++】引用和函数重载的介绍
c语言·开发语言·c++
机器视觉的发动机22 分钟前
AI算力中心的能耗挑战与未来破局之路
开发语言·人工智能·自动化·视觉检测·机器视觉
铁蛋AI编程实战26 分钟前
通义千问 3.5 Turbo GGUF 量化版本地部署教程:4G 显存即可运行,数据永不泄露
java·人工智能·python
HyperAI超神经30 分钟前
在线教程|DeepSeek-OCR 2公式/表格解析同步改善,以低视觉token成本实现近4%的性能跃迁
开发语言·人工智能·深度学习·神经网络·机器学习·ocr·创业创新
晚霞的不甘37 分钟前
CANN 编译器深度解析:UB、L1 与 Global Memory 的协同调度机制
java·后端·spring·架构·音视频
SunnyDays101138 分钟前
使用 Java 冻结 Excel 行和列:完整指南
java·冻结excel行和列
R_.L40 分钟前
【QT】常用控件(按钮类控件、显示类控件、输入类控件、多元素控件、容器类控件、布局管理器)
开发语言·qt
Zach_yuan1 小时前
自定义协议:实现网络计算器
linux·服务器·开发语言·网络