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 = 10或WHERE 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 也加上注解),我更推荐以下两种方式:
- 服务拆分:将 B 方法抽取到一个新的 Service 类中,通过 Spring 的依赖注入来调用。这是符合单一职责原则的,也是最推荐的做法。
- 自我注入 :在当前类中注入自己(需要加上
@Lazy防止循环依赖),然后通过注入的实例来调用 B 方法,这样就能强制走代理逻辑。 - 架构调整:如果业务逻辑允许,尽量避免这种内部方法的事务嵌套,通过上层服务来协调。"
🚀 进阶追问(准备一下)
面试官可能会继续追问你对事务传播行为的理解:
"如果必须在同一个类中处理,且 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 COMMITTED或READ UNCOMMITTED理论上能解决可见性问题,但这会带来脏读、不可重复读的风险,严重破坏数据一致性,在生产环境中是绝对禁止为了这种业务逻辑去调整的。"