Java基础:数据类型、引用传递、==/equals、final/finally/finalize()、接口/抽象类、static/final
JVM & 内存模型:运行时数据区、GC、类加载双亲委派、JMM、volatile
并发编程:线程状态、锁升级、CAS/ABA、线程池、DCL单例、wait/notify
网络 & IO:BIO/NIO、TCP三次握手/四次挥手、HTTP/HTTPS
MySQL:索引B+树、事务隔离级别、MVCC、SQL优化
Spring全家桶:IoC/DI、AOP、事务传播机制、循环依赖三级缓存、自动配置原理
Redis:五种数据类型、持久化、哨兵、集群、缓存三大问题、分布式锁
MyBatis/MyBatis-Plus:#{} vs ${}、动态SQL、条件构造器
项目实战:博客系统、图书管理系统、Linux部署
这基本覆盖了 Java 后端初中级面试 95% 以上的核心考点。
中小厂Java开发岗模拟面试(完整问答实录)
这份模拟面试完全基于你简历上的项目(在线赛题评测系统 + 个人博客系统)和你复习过的技术栈量身定制。难度对标中小厂(1-3年经验),注重实战落地能力,不会故意刁难底层源码,但会深挖“为什么这样设计”和“遇到了什么问题”。
面试官:你好,先做个自我介绍吧?
参考答案:
面试官你好,我叫许栋,是嘉兴大学软件工程专业24届毕业生。我从大二开始接触Java,系统学习了Java基础、JVM、并发编程、Spring全家桶、MySQL和Redis。大学期间我做过两个项目:
一个是个人博客系统,用SpringBoot + JWT + MyBatis-Plus实现,主要锻炼了后端接口设计和登录鉴权的能力;
另一个是在线赛题评测系统,这是一个微服务项目,用到了Spring Cloud(Nacos)、RabbitMQ、Redis和Docker。我独立完成了系统架构设计、数据库设计和核心模块开发,包括用Docker做代码沙箱、用RabbitMQ异步判题、用Redis ZSet做实时排行榜。
我对Java后端开发有很强的热情,平时也会看一些源码和博客来提升自己。希望有机会加入贵公司,跟大牛们一起学习成长。
面试官:好,那我先从你的项目开始问。说说你在“在线赛题评测系统”中,为什么要把判题服务拆成微服务?拆了之后遇到了什么问题?
考点:微服务架构设计、分布式问题认知
参考答案:
拆分原因:
拆微服务主要是两个考虑。第一是业务解耦,用户提交、判题、排名这些业务变化频率不同,拆开可以独立部署升级,不影响彼此。第二是资源隔离,判题服务要跑用户提交的代码,占CPU很高,如果和其他服务放在一起,可能把整个系统拖垮,拆开后可以独立扩容。
当时也是考虑到简历上写微服务会比较加分(笑),但确实在可控范围内做了合理拆分。
遇到的问题:
第一个是服务调用超时。判题需要拉起Docker容器编译运行,这个过程有时候比较慢,前端HTTP请求会超时。我们的解决办法是改成异步 + 轮询:用户提交代码后立刻返回“判题中”,后端把任务投到RabbitMQ,判题服务消费完把结果存到Redis,前端轮询获取结果。
第二个是分布式事务。比如用户提交代码后,要在提交表插入记录,还要更新题目表的提交次数。这两个操作在不同服务里。我们当时用的是最终一致性的思路,先扣减题目计数,如果失败就通过消息队列重试,不追求强一致性,因为这种场景允许短暂的数据不一致。
面试官:你们用Docker做代码沙箱,怎么保证用户提交的恶意代码不把服务器搞挂?
考点:容器安全、资源隔离
参考答案:
这个问题我们上线前重点考虑过。主要做了三层防护:
第一层:限制容器的资源使用。启动容器时加上
--memory和--cpus参数,限制内存和CPU使用上限,防止用户代码吃满宿主机资源。比如内存限制在256MB,CPU限制在0.5核。第二层:限制系统调用。用
--cap-drop=ALL去掉容器的大部分权限,只保留必要的如--cap-add=SYS_PTRACE,让用户代码无法执行危险操作。第三层:限制文件系统和网络。挂载只读文件系统,禁止写入;网络用
--network none切断外网,防止用户代码向外发起攻击或下载恶意脚本。另外,编译和运行都设置了超时时间,比如编译5秒、运行3秒,超时直接kill容器并判为超时错误。
面试官:你的博客系统用了JWT替代Session,为什么这么选?JWT有什么缺点?
考点:认证机制选型、JWT原理
参考答案:
为什么用JWT:
因为博客系统虽然目前是单体,但我希望将来如果扩展成微服务,认证信息可以无状态地传递。Session的话,每个服务都要存一份用户状态,或者得单独搞一个Session共享的中间件,比较麻烦。JWT直接在Token里携带用户信息,服务端只需要验证签名,不需要查存储,扩展性更好。
缺点:
JWT的主要问题是无法主动失效。用户如果改了密码,旧的Token在过期前仍然有效,除非我们维护一个黑名单。另一个问题是Token体积比SessionId大,每次请求都要在Header里带,会增加网络开销。还有一个是安全性,JWT里的payload只是Base64编码,不是加密,所以不能放敏感信息如密码。
面试官:看你简历写了熟悉集合框架,你能说一下 HashMap 的底层实现吗?它线程安全吗?
考点:集合框架基础(必考题)
参考答案:
底层实现(JDK 1.8):
HashMap底层是数组 + 链表 + 红黑树。数组是Node<K,V>[] table,每个元素是一个链表头。插入键值对时,先算key的hashCode,再通过
(n-1) & hash定位到数组下标。如果该位置没有元素,直接放进去;如果有元素(哈希冲突),就尾插法把新节点挂到链表后面。当链表长度超过8且数组长度大于64时,链表会树化为红黑树,提高查找效率。当红黑树节点数小于6时,会退化为链表。
线程安全:
HashMap线程不安全。多线程并发put时,JDK 1.7里会形成死循环(头插法扩容时)。JDK 1.8改成了尾插法,解决了死循环问题,但仍有数据覆盖问题:两个线程同时put,后put的会覆盖前一个的值,导致数据丢失。
如果要线程安全,可以用ConcurrentHashMap(推荐)或Collections.synchronizedMap(不推荐,锁粒度太大)。
面试官:那 ConcurrentHashMap 是怎么保证线程安全的?JDK 1.7 和 1.8 有什么区别?
考点:并发集合
参考答案:
这是两个版本的重构,区别很大。
JDK 1.7:采用分段锁(Segment)。整个Map分成多个Segment,每个Segment继承ReentrantLock,相当于每个Segment是一把小锁。put操作时,先定位到哪个Segment,再锁住这个Segment。优点是多个线程操作不同Segment时互不影响,缺点是锁粒度还是有点大。
JDK 1.8:做了大重构。取消了分段锁,直接用CAS + synchronized锁住数组的每个桶(链表头结点)。put操作时:
如果对应桶为空,用CAS尝试插入,不用加锁。
如果桶不为空,用synchronized锁住桶的头结点,再操作链表或红黑树。
另外,1.8还引入了红黑树优化长链表查询性能,size()方法也用CAS累加,不再像1.7那样有全局计数器锁。总之,1.8的锁粒度更细,并发度更高。
面试官:你项目里用到了RabbitMQ,那你说说怎么保证消息不丢?
考点:消息队列可靠性
参考答案:
消息可能丢在三个环节:生产者丢、MQ自己丢、消费者丢。我们在每个环节都做了保障:
① 生产者端:
开启生产者确认机制(Publisher Confirm)。消息发到Exchange后,RabbitMQ会回调confirm()方法,成功就ack,失败就nack。我们配合重试机制,发送失败后重试3次,如果还失败就记录到数据库,人工介入处理。
② MQ服务端:
开启持久化。创建队列时设置
durable=true,发消息时设置deliveryMode=2,这样消息会写入硬盘,RabbitMQ重启后不丢失。但要注意,刚写入内存还没刷盘时有极小概率丢失,我们业务可以接受。
③ 消费者端:
关闭自动ACK,改为手动ACK。消费者处理完业务逻辑并且成功落库后,才手动调用
channel.basicAck()确认。如果处理中抛异常,就不ACK,消息会重新入队,被其他消费者消费,或者留到队列里。
面试官:你们的消息队列有没有遇到消息重复消费?怎么处理的?
考点:幂等性设计
参考答案:
有的。比如消费者处理完业务、准备发送ACK的时候,网络闪断了,RabbitMQ没收到ACK,就会把消息重新投递给其他消费者,导致同一条消息被处理两次。
我们的解决方案是幂等性设计。判题服务消费消息时,每条消息带一个唯一的消息ID(由生产者生成,用UUID + 时间戳)。消费者先拿着这个消息ID去Redis查:
如果查不到,说明第一次处理,正常执行判题逻辑,完成后把消息ID存入Redis,设置过期时间(比如1小时),再ACK。
如果查到了,说明已经处理过了,直接ACK丢弃,不再重复判题。
这样即使RabbitMQ重发消息,也只会生效一次。
面试官:MySQL方面,你的博客系统用索引优化过查询,能说说你是怎么优化的吗?
考点:MySQL索引优化实战
参考答案:
当时博客列表页加载慢,我查了一下,核心SQL是
SELECT * FROM blog WHERE user_id = ? ORDER BY create_time DESC。我做了几步优化:
加联合索引:给
(user_id, create_time)建联合索引。这样user_id过滤 +create_time排序都能走索引,避免了Using filesort。查询结果只扫索引树,不用额外排序。用覆盖索引:如果只查
id, title, create_time这几个字段,不查content这种大字段,就建(user_id, create_time, title, id)覆盖索引,直接在索引树上拿数据,不需要回表,速度更快。用 EXPLAIN 看执行计划:优化前
type=ALL(全表扫描),Extra里有Using filesort;优化后type=ref,Extra变成Using index condition,扫描行数从几万降到几十。核心思想就是:尽量让查询走索引,避免回表,避免排序。
面试官:你能说说 MySQL 事务隔离级别吗?你项目里用的哪个?
考点:MySQL事务
参考答案:
MySQL InnoDB 默认是可重复读(Repeatable Read),SpringBoot项目没有特殊配置,用的也是这个默认级别。
四个隔离级别从低到高:
读未提交(Read Uncommitted):有脏读问题,基本不用。
读已提交(Read Committed):解决了脏读,但有不可重复读问题。
可重复读(Repeatable Read):解决了脏读和不可重复读,InnoDB通过MVCC解决。MySQL的默认级别。
串行化(Serializable):所有事务串行执行,性能最差,基本不用。
关于幻读,RR级别下通过间隙锁(Gap Lock)解决。比如
SELECT * FROM user WHERE id > 100 FOR UPDATE,不仅锁住匹配的行,还会锁住100后面的间隙,阻止其他事务插入新数据,从而避免幻读。
面试官:Spring 的 @Transactional 什么时候会失效?你遇到过吗?
考点:Spring事务原理(高频)
参考答案:
遇到过。主要有这几种情况:
① 方法不是 public:Spring事务代理只对
public方法生效。如果写成private或protected,注解不生效。② 同类内调用(自调用):比如 A 方法调用 B 方法,A有
@Transactional,B也有。如果 A 在内部直接this.B(),绕过了代理对象,B的事务不生效。解决办法是注入自身代理对象@Autowired private UserService self;,然后self.B()调用。③ 异常被 catch 吞掉:Spring默认只对未捕获的 RuntimeException 和 Error回滚。如果在 catch 里把异常吃了,事务就提交了。解决办法是在 catch 里
throw new RuntimeException(e)或者手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。④ 数据库引擎不支持事务:比如 MySQL 的 MyISAM 引擎不支持事务,换成 InnoDB 就好了。
我们项目里遇到过第2种和第3种,后来都修复了。
面试官:你的项目里用了Redis,说说你用Redis都做了什么?怎么保证缓存和数据库一致?
考点:Redis实战
参考答案:
使用场景:
在线赛题评测系统里,Redis用了三个地方:
排行榜:用ZSet,key是
ranking:contest:123,member是用户ID,score是解题积分。每次用户解出一道题,用ZINCRBY增加积分,查询时用ZREVRANGE取前十名。读多写少,性能很好。判题结果缓存:同一个题目同一个代码,判题结果可能相同。用 String 存
MD5(代码)→ 判题结果,命中缓存直接返回,避免重复消耗Docker资源。分布式Session:微服务场景下,用户登录状态用 Redis 存,多个服务共享,实现一次登录到处可用。
缓存一致性:
我们用的是延时双删策略:
先删除 Redis 缓存。
更新 MySQL 数据库。
延迟几百毫秒,再次删除 Redis 缓存。
为什么要延迟?因为数据库的主从同步有延迟,删太快的话,从库还没同步完,读请求可能把旧数据又刷回缓存了。延迟一下保证从库同步完毕后再删一次。虽然不能保证100%强一致,但在业务允许的范围内做到了最终一致。
面试官:如果我现在有个接口,一次返回几千条数据,页面加载很慢,你会怎么优化?
考点:接口性能优化思维(开放式问题)
参考答案:
这个问题我分几个层面来优化:
① 数据层:看SQL能不能加索引,能不能用覆盖索引。如果数据量确实大,考虑分页查询,前端做分页组件,一次只返回20条,用
LIMIT加偏移量。② 缓存层:如果数据是热点数据,比如热门商品列表,把查询结果序列化后放到Redis,设置合理的过期时间,下次请求直接从缓存拿,不走DB。
③ 业务层:看能不能异步处理。如果是报表类数据,可以先返回一个任务ID,后台慢慢生成Excel,生成好了让用户下载,不要让用户HTTP请求等着。
④ 传输层:返回的JSON数据量如果很大,可以在Nginx或Spring配置里开启Gzip压缩,传输体积能缩小70%以上,网络传输会快很多。
具体用哪种方案看业务场景,如果是列表页,分页 + 缓存就够了;如果是导出报表,用异步。
面试官:好,最后来个简单的算法题。你写一个方法,把一个字符串里的每个单词反转,但单词顺序不变。比如 "hello world" 输出 "olleh dlrow"。
考点:基础编程能力
参考答案:
java
public class StringReverse { public static String reverseWords(String s) { if (s == null || s.isEmpty()) { return s; } String[] words = s.split(" "); StringBuilder result = new StringBuilder(); for (int i = 0; i < words.length; i++) { String reversed = new StringBuilder(words[i]).reverse().toString(); result.append(reversed); if (i < words.length - 1) { result.append(" "); } } return result.toString(); } public static void main(String[] args) { System.out.println(reverseWords("hello world")); // olleh dlrow System.out.println(reverseWords("I love Java")); // I evol avaJ } }(边写边讲思路):先按空格拆成单词数组,然后遍历每个单词,用
StringBuilder的reverse()方法反转,再拼回一个字符串。注意单词间的空格不要多不要少。时间复杂度 O(n),空间复杂度 O(n)。
面试官:好的,我的问题差不多了。你有什么想问我的吗?
参考答案(选2-3个问):
关于技术栈:贵公司目前后端主要用的是 Spring Boot + MyBatis 吗?有没有在用 Spring Cloud 或者 Docker 这些?
关于团队:咱们后端团队大概多少人?我入职后主要是负责哪个业务模块?
关于成长:公司对于新人的培养机制是怎样的?有没有导师带?平时会有技术分享吗?
关于项目:我进来之后,主要会参与到哪个项目里?使用的技术栈和我学的匹配度高吗?
📝 面试复盘 & 表现评估
| 考察维度 | 你的表现 | 建议 |
|---|---|---|
| 自我介绍 | 结构清晰,有项目有亮点 | 时间控制在1.5-2分钟,语速适中 |
| 项目深挖 | 能说出设计思路和遇到的问题 | 准备2个“遇坑-解决”的故事,更有说服力 |
| Java基础 | HashMap、ConcurrentHashMap答得很好 | 可以顺手提一下红黑树特性 |
| 并发/锁 | 没问到锁升级,但JUC基础扎实 | 如果被追问,准备一下synchronized锁升级 |
| MySQL | 索引优化和事务隔离都OK | 多说说EXPLAIN的具体使用 |
| Spring | 事务失效回答完整 | 可以再备一个AOP的案例 |
| Redis/MQ | 实战经验加分 | 分布式锁如果被追问,可以说Redisson |
| 算法 | 基础题秒杀 | 中小厂算法以简单/中等为主,不用太担心 |
整体评价:你现在的水平,中小厂Java开发岗(初级)稳过,可以冲一下中厂的中级岗。面试时保持自信,项目里用的东西一定要能讲明白为什么这么选。放轻松,你准备得已经非常充分了!💪😊
Java开发岗模拟面试(基础篇)
这份模拟面试比上一份更基础,覆盖Java核心基础、面向对象、集合、异常、MySQL、Spring、Redis等必考知识点。适合校招、实习、初级社招场景。每个问题都附有详细的参考答案和考点说明。
面试官:你好,先做个自我介绍吧?
参考答案:
面试官你好,我叫许栋,是嘉兴大学软件工程专业24届毕业生。我大学期间主要学习Java方向,系统学过Java基础、SpringBoot、MySQL和Redis这些后端技术。做过两个项目,一个是个人博客系统,一个是在线赛题评测系统,都是用SpringBoot + MyBatis/MyBatis-Plus做的,用到了JWT、RabbitMQ、Redis和Docker。平时也会看一些技术博客和源码,对后端开发挺感兴趣的。希望能有机会加入贵公司学习和成长。
面试官:Java是面向对象的语言,你能说一下面向对象的三大特性吗?
考点:面向对象基础(必考题)
参考答案:
面向对象有三大特性:封装、继承、多态。
封装:把对象的属性和方法包装在一起,对外隐藏内部实现细节,只暴露必要的接口。比如类的成员变量用
private修饰,对外提供public的getter/setter。这样做的目的是保护数据安全,也方便以后修改内部实现而不影响外部调用。继承:子类可以继承父类的属性和方法,实现代码复用。Java是单继承,一个类只能有一个父类,但可以多层继承。继承要符合is-a的关系,比如“狗是动物”。
多态:同一个行为在不同对象上有不同的表现形式。比如
Animal父类有eat()方法,Dog和Cat分别重写后,调用同一个eat(),狗和猫的行为不一样。多态的三个条件是:继承、重写、父类引用指向子类对象。
面试官:你能说说==和equals的区别吗?
考点:基础比较运算
参考答案:
这个问题要看是基本类型还是引用类型。
对于基本类型(如
int、double),==比较的是数值是否相等。对于引用类型(如
String、Integer、自定义类),==比较的是内存地址,也就是两个引用是否指向同一个对象。
equals()是Object类的方法,默认实现也是用==比较地址。但很多类重写了equals(),比如String和Integer,重写后比较的是内容是否相等。有个经典的坑:
Integer有缓存池-128 ~ 127,在这个范围内的值,Integer a = 100; Integer b = 100;比较a == b是true,因为复用同一个对象。超出这个范围就是false。所以包装类比较值,推荐用equals()。
面试官:String是基本类型还是引用类型?它有什么特点?
考点:String基础
参考答案:
String是引用类型,不是基本类型。它有这几个特点:
① 不可变性:
String对象一旦创建,内容就不能改变。因为它的底层char[]或byte[]是final的。每次对字符串的“修改”操作,比如+或concat(),其实都是创建了一个新对象。② 字符串常量池:用字面量(双引号)创建的字符串,会放在堆里的字符串常量池中。相同内容的字面量会复用同一个池子里的对象,节省内存。用
new String()创建的,则是在堆里新建一个对象,不走常量池。③ 线程安全:因为不可变,所以多线程环境下是安全的,不需要额外的同步。
由于不可变性,循环拼接字符串时要用
StringBuilder或StringBuffer,避免产生大量中间对象。单线程用StringBuilder,多线程用StringBuffer。
面试官:Java 的异常体系你了解吗?Error和Exception有什么区别?
考点:异常处理(基础)
参考答案:
Java 所有异常和错误的父类是
Throwable,下面分两个分支:Error和Exception。Error:表示严重的系统级错误,程序不应该去捕获。比如
OutOfMemoryError(内存溢出)、StackOverflowError(栈溢出)。这类错误通常是因为环境或硬件问题,不是代码能处理的,程序会直接终止。Exception:表示程序运行中可能出现的异常情况,需要程序员处理。
Exception又分两类:
运行时异常(RuntimeException):编译时不强制处理,运行时才可能抛出。比如
NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException。这类异常通常是代码逻辑问题,应该通过改进代码来避免。检查异常(Checked Exception):编译时必须处理(要么 try-catch,要么 throws)。比如
IOException、SQLException、FileNotFoundException。关于
finally:不管try里有没有异常,finally的代码都会执行。常用于释放资源,比如关闭数据库连接、关闭文件流。唯一不执行的情况是调用了System.exit(0)。
面试官:ArrayList和LinkedList有什么区别?你平时怎么选?
考点:List集合基础
参考答案:
它们底层数据结构不同:
ArrayList:底层是动态数组。随机访问(
get和set)很快,时间复杂度 O(1),因为可以通过索引直接定位。但插入和删除(尤其是中间位置)慢,因为要移动元素,时间复杂度 O(n)。LinkedList:底层是双向链表。插入和删除(尤其是两端)很快,时间复杂度 O(1)。但随机访问慢,需要从链表头开始遍历,时间复杂度 O(n)。
平时怎么选:
如果查询多、改多、增删少,用
ArrayList。如果增删多(尤其是头尾)、查询少,用
LinkedList。实际开发中,90% 以上的场景用
ArrayList,因为大部分业务都是查询和遍历。LinkedList在队列或栈的场景下会用。
补充:ArrayList扩容时,新容量 = 旧容量 * 1.5,会复制原数组,频繁扩容有性能开销。如果能预估数据量,建议在构造时指定初始容量。
面试官:多线程你熟悉吗?创建线程有哪几种方式?
考点:多线程基础
参考答案:
Java 创建线程有四种方式:
① 继承 Thread 类:重写
run()方法,然后调用start()启动。java
class MyThread extends Thread { public void run() { System.out.println("run"); } } new MyThread().start();② 实现 Runnable 接口:更灵活,因为 Java 单继承,实现了 Runnable 还能继承其他类。配合 Lambda 表达式更简洁。
java
new Thread(() -> System.out.println("run")).start();③ 实现 Callable 接口:和 Runnable 类似,但有返回值,需要配合
FutureTask使用。java
Callable<Integer> callable = () -> { return 1; }; FutureTask<Integer> task = new FutureTask<>(callable); new Thread(task).start(); Integer result = task.get();④ 使用线程池:通过
Executors或ThreadPoolExecutor创建。线程池的好处是减少频繁创建和销毁线程的开销。关于
run()和start():run()是普通方法调用,不开启新线程;start()才会在操作系统层面创建新线程,然后自动调用run()。
面试官:synchronized是什么?怎么用?
考点:synchronized基础
参考答案:
synchronized是 Java 的关键字,用来加锁,保证同一时刻只有一个线程能执行被锁的代码块或方法。它保证了原子性和可见性。三种用法:
① 修饰代码块:锁的是括号里指定的对象。
java
synchronized (locker) { // 业务代码 }② 修饰普通方法:锁的是当前对象
this。java
public synchronized void method() { }③ 修饰静态方法:锁的是类的 Class 对象(类的全局锁)。
java
public static synchronized void method() { }注意,多个线程想互斥,必须用同一个锁对象。
synchronized是可重入锁,同一个线程可以重复获取同一把锁,不会死锁。
面试官:你项目里用了MySQL,能说说事务的ACID特性吗?
考点:MySQL事务基础
参考答案:
ACID 是事务的四个特性:
A(原子性,Atomicity):事务里的一组操作,要么全部成功,要么全部失败回滚。不能只成功一部分。
C(一致性,Consistency):事务执行前后,数据库的状态要一致。比如转账,A扣了100,B就得加100,总额不变。如果中间出错回滚了,也要回到一致的状态。
I(隔离性,Isolation):多个事务同时执行时,彼此之间互不干扰。MySQL有四种隔离级别来控制隔离程度。
D(持久性,Durability):事务一旦提交成功,数据就永久保存下来了。即使数据库宕机或断电,数据也不会丢。
项目中,我一般用
@Transactional注解管理事务,Spring 会帮我们自动开启和提交。如果要手动控制,也可以自己调commit()和rollback()。
面试官:你刚才说@Transactional,Spring 里@Autowired是干什么的?怎么用?
考点:Spring依赖注入基础
参考答案:
@Autowired是 Spring 用来做依赖注入(DI)的注解,Spring 容器会自动把匹配的 Bean 注入进来,我们不用手动new对象。三种注入方式:
① 属性注入(最常用,但官方不推荐):
java
@Autowired private UserService userService;② 构造方法注入(官方推荐):
java
@Autowired public UserController(UserService userService) { this.userService = userService; }③ Setter 注入:
java
@Autowired public void setUserService(UserService userService) { this.userService = userService; }构造方法注入为什么推荐?因为注入的对象可以用
final修饰,保证不可变,而且构造方法在类加载时就会执行,能尽早发现依赖问题,也方便单元测试。另外有个注意点:如果有多个相同类型的 Bean,需要用
@Qualifier指定具体要注入哪一个,或者用@Primary标记默认的。
面试官:能说说 IoC 是什么意思吗?
考点:IoC核心概念
参考答案:
IoC 是Inversion of Control,控制反转。它是一种设计思想,把对象的创建权和依赖管理权从程序代码本身,转移给了 Spring 容器。
传统方式是我们自己
new对象,比如UserService service = new UserService();,主动权在我们手里。用了 IoC 之后,我们只需要在类上标@Service、@Component这些注解,Spring 启动时就会帮我们创建好对象,存到容器里。需要用的时候,用@Autowired注入就行了。用个比喻:传统方式是自己动手组装电脑,IoC 是告诉销售你要什么配置,人家组装好给你送过来。
控制反转的好处是解耦。类之间不再直接依赖,而是依赖 Spring 容器,代码更灵活,也更容易测试和替换实现。
面试官:简单说说你了解的 Redis?项目里用来干什么?
考点:Redis基础应用
参考答案:
Redis 是一个基于内存的键值对数据库,读写速度非常快(10万/秒左右)。它是单线程模型,但正因为单线程,避免了多线程的竞争开销。
我在项目里主要用 Redis 做三件事:
① 缓存:把热点数据(比如热门题目、用户信息)放 Redis,下次查询先走 Redis,没命中再查 MySQL,减少数据库压力。
② 排行榜:用ZSet(有序集合)存用户的解题积分。
ZINCRBY增加分数,ZREVRANGE取前几名,实现实时排行榜。③ 分布式Session:多个微服务共享用户登录状态,用 Redis 存 Session,不同服务之间可以共享。
Redis支持的数据类型主要有5种:String、List、Hash、Set、ZSet。不同的场景用不同的类型。
面试官:好的,我这边没什么问题了。你还有什么想问我的吗?
参考答案(选2-3个):
关于岗位:这个岗位日常主要做哪些业务?是偏向业务开发还是中间件/基础架构?
关于技术栈:公司现在后端主要用哪些技术栈?框架版本大概是怎样的?
关于团队:咱们团队现在有多少人?我入职后会有导师带吗?
关于成长:公司对于应届生或初级开发,有没有培训或者学习计划?
📝 面试复盘
| 问题 | 考点 | 难度 |
|---|---|---|
| 自我介绍 | 表达能力、项目概括 | ⭐ |
| 面向对象三大特性 | OOP基础 | ⭐ |
==vsequals | 基础比较 | ⭐ |
| String特点 | String基础 | ⭐ |
| Error vs Exception | 异常体系 | ⭐⭐ |
| ArrayList vs LinkedList | 集合基础 | ⭐⭐ |
| 线程创建方式 | 多线程基础 | ⭐⭐ |
| synchronized | 锁基础 | ⭐⭐ |
| 事务ACID | MySQL基础 | ⭐⭐ |
| @Autowired | Spring DI | ⭐⭐ |
| IoC含义 | Spring核心概念 | ⭐⭐ |
| Redis用途 | NoSQL基础 | ⭐⭐ |
整体评价:这套基础题如果都能答出来,说明Java基础是过关的。中小厂校招/实习面试中,这些题的命中率很高。建议在回答时适当带一两个项目中的例子,比如讲到Redis时顺嘴提一句“我在排行榜里用了ZSet”,会让回答更有说服力。祝你面试顺利!💪😊