大厂Java面试题实录:阿里面经+字节面经+美团面经,带解题思路+代码+考官心理分析
🔥 写在前面:这篇文章不是网上随便搜的"面试题库",是我和几位朋友(分别在阿里、字节、美团、腾讯工作)真实面试经历的复盘。每个问题都标注了难度星级、考察意图、满分回答和踩坑点。
⚠️ 郑重声明:面试题因人而异,这篇文章仅供参考。核心是学习解题思路,而不是背答案。
一、先说清楚:大厂面试到底在考什么?
很多人以为大厂面试是"知识问答",错了。面试的本质是:
┌─────────────────────────────────────────────────────────────────┐
│ 面试官的心理活动 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ "这个人能不能解决我没遇到过的实际问题?" │
│ │
│ 表面:在问技术知识 │
│ 实际:在考察: │
│ ├─ 思维过程:遇到新问题怎么分析? │
│ ├─ 技术深度:只知道what还是也知道why和how? │
│ ├─ 表达沟通:能不能把复杂问题讲清楚? │
│ └─ 价值观匹配:遇到困难会放弃还是死磕? │
│ │
└─────────────────────────────────────────────────────────────────┘
所以不要背答案,要理解思路。
二、Java基础:这些问题你真的理解了吗?
2.1 HashMap的底层实现(⭐⭐ 难度)
面试官问:
"HashMap在JDK 1.8做了哪些优化?为什么链表长度超过8要转成红黑树?"
普通回答:
"JDK 1.8用了数组+链表+红黑树...链表转红黑树的阈值是8..."
问题:只回答了what,没回答why。
满分回答:
"HashMap在JDK 1.7之前用的是数组+链表,头插法。JDK 1.8改成了数组+链表+红黑树,尾插法。
为什么用尾插法? 因为头插法在并发扩容时会导致链表成环,造成死循环。JDK 1.8的尾插法解决了这个问题,但HashMap本身还是线程不安全的,线程安全场景用ConcurrentHashMap。
为什么是8才转红黑树?
这是一个时间和空间的权衡:
- 红黑树的查询时间复杂度是O(log n),链表是O(n)
- 但红黑树的节点比链表节点大(多出颜色、左右指针等)
- 源码注释里说:经过泊松分布计算,链表长度k=8的概率只有0.00000006
也就是说,99.9999%的情况下链表不会超过8,用链表就够了。红黑树只在极端hash碰撞时才启用。
那为什么不用AVL树? 因为AVL树的平衡条件更严格(左右子树高度差≤1),调整更频繁,影响插入/删除性能。红黑树的近似平衡更适合频繁修改的场景。
代码验证:
/**
* HashMap源码关键点解析
*/
public class HashMapSource {
/**
* 1. 数据结构
*/
// JDK 1.7:数组 + 链表(头插法)
// JDK 1.8:数组 + 链表/红黑树(尾插法)
transient Node<K,V>[] table;
/**
* 2. 链表节点
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // hash值
final K key; // key
V value; // value
Node<K,V> next; // 链表下一节点
}
/**
* 3. 红黑树节点
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 父节点
TreeNode<K,V> left; // 左子树
TreeNode<K,V> right; // 右子树
TreeNode<K,V> red; // 颜色
}
/**
* 4. 扩容条件
* - 链表长度 > 8 && 数组长度 >= 64 → 转红黑树
* - 链表长度 > 8 && 数组长度 < 64 → 先扩容
*/
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6; // 红黑树退化为链表
static final int MIN_TREEIFY_CAPACITY = 64; // 最小树化容量
/**
* 5. 扩容机制
* - 每次扩容2倍(n << 1)
* - jdk1.8优化:不用重新计算hash,用位运算判断
* 新位置 = 原位置 或 原位置 + 原数组长度
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int newCap = oldCap << 1; // 扩容2倍
// JDK 1.8的关键优化:
if ((e.hash & oldCap) == 0)
// hash & oldCap == 0,说明新位置在原位置
newTab[j] = e;
else
// hash & oldCap != 0,说明新位置 = 原位置 + oldCap
newTab[j + oldCap] = e;
}
}
2.2 synchronized和ReentrantLock的区别(⭐⭐⭐ 难度)
面试官问:
"你用过synchronized吗?它和ReentrantLock有什么区别?"
普通回答:
"synchronized是自动加锁释放,ReentrantLock需要手动...ReentrantLock支持公平锁..."
问题:没有实际项目经验,说不出应用场景。
满分回答:
"synchronized和ReentrantLock我都有用过,说说我的选型:
简单同步场景用synchronized: 比如单例模式、简单计数、对象属性保护。用法简单,JVM自动处理,不会死锁。
复杂场景用ReentrantLock: 比如需要tryLock超时、需要读写分离、或者锁的粒度需要控制得更细。
举一个我实际踩过的坑:
// 场景:订单超时取消 // 用synchronized没法做到"等N秒就取消" // 用ReentrantLock + Condition完美解决 public class OrderCancelService { private final ReentrantLock lock = new ReentrantLock(); private final Condition orderCondition = lock.newCondition(); public void waitForPayment(String orderId) { lock.lock(); try { // 等3分钟,3分钟后自动取消 orderCondition.await(3, TimeUnit.MINUTES); // 超时被唤醒了,检查订单状态 Order order = getOrder(orderId); if (order.isUnpaid()) { cancelOrder(orderId); // 取消订单 } } finally { lock.unlock(); } } public void onPaymentReceived(String orderId) { lock.lock(); try { // 付款成功,唤醒等待的线程 orderCondition.signal(); } finally { lock.unlock(); } } }核心区别总结:
特性 synchronized ReentrantLock 用法 自动加锁释放 手动tryLock/unlock 公平锁 不支持 支持(new ReentrantLock(true)) tryLock 不支持 支持 条件变量 不支持(只能Object.wait) 支持多个Condition 底层 JVM AQS+CAS 踩坑记录:ReentrantLock有个坑——必须在finally里unlock,否则如果代码抛异常,锁就永远不会释放。我见过有人把unlock写在try块中间,结果异常时锁泄漏。正确写法是try-finally包裹。"
三、Spring全家桶:面试官最爱问的深水区
3.1 Spring Bean的生命周期(⭐⭐⭐ 难度)
面试官问(字节跳动):
"Spring Bean从创建到销毁都经历了什么?你能手动控制Bean的创建顺序吗?"
满分回答:
"Spring Bean的生命周期分为以下几个阶段:
1. 实例化:new创建对象 2. 属性填充:@Autowired注入依赖 3. 初始化:
- BeanNameAware.setBeanName()
- BeanFactoryAware.setBeanFactory()
- ApplicationContextAware.setApplicationContext()
- BeanPostProcessor.postProcessBeforeInitialization()
- @PostConstruct标注的方法
- InitializingBean.afterPropertiesSet()
- BeanPostProcessor.postProcessAfterInitialization() 4. 销毁:
- @PreDestroy标注的方法
- DisposableBean.destroy()
关键问题:如何手动控制Bean的创建顺序?
方法一:@DependsOn
@Component @DependsOn({"dataSource", "jdbcTemplate"}) public class OrderService { // 这个Bean会在dataSource和jdbcTemplate之后创建 }方法二:@Order(对同一类型的Bean排序)
@Component @Order(1) public class A {} @Component @Order(2) // Order值越小越先创建 public class B {}方法三:BeanFactoryPostProcessor(完全控制Bean创建)
@Component public class CustomBeanFactory implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) { // 可以手动注册Bean,完全控制创建顺序 factory.registerSingleton("myBean", new MyBean()); } }踩坑记录:@DependsOn有时候不生效,特别是循环依赖时。更好的做法是在代码里用@PostConstruct检查依赖是否就绪,而不是依赖Spring的创建顺序。"
3.2 Spring事务的传播行为(⭐⭐⭐ 难度)
面试官问(美团):
"你知道Spring事务的7种传播行为吗?什么情况下会用REQUIRES_NEW?"
满分回答:
"Spring事务的7种传播行为:
- REQUIRED(默认):加入当前事务,没有则创建新事务
- REQUIRES_NEW:挂起当前事务,创建新事务
- SUPPORTS:支持当前事务,没有则非事务执行
- NOT_SUPPORTED:不支持事务,挂起当前事务
- MANDATORY:必须在事务中执行,没有则抛异常
- NEVER:必须不在事务中执行,有则抛异常
- NESTED:嵌套事务(需要数据库支持SAVEPOINT)
REQUIRES_NEW的典型应用场景:
/** * 场景:用户注册后要发送欢迎邮件 * 邮件发送失败不应该影响注册成功 */ @Service public class UserService { @Transactional public void register(User user) { userMapper.insert(user); // 主事务:必须成功 // 发送邮件用REQUIRES_NEW // 邮件失败时,邮件事务回滚,但用户注册事务不受影响 emailService.sendWelcomeEmail(user.getEmail()); } } @Service public class EmailService { @Transactional(propagation = Propagation.REQUIRES_NEW) public void sendWelcomeEmail(String email) { // 独立的邮件事务 // 失败时只回滚邮件操作,不影响UserService的事务 emailGateway.send(email, "欢迎注册"); } }踩坑记录:
⚠️ NESTED和REQUIRED的区别:
- REQUIRED:所有操作在一个事务,一个失败全回滚
- NESTED:子事务失败只会滚子事务本身(用SAVEPOINT实现)
但NESTED需要数据库支持(MySQL InnoDB支持,MyISAM不支持)。
⚠️ REQUIRES_NEW的坑:
- 会挂起外层事务,外层事务等待内层事务完成后才能继续
- 嵌套太深会导致事务链过长,调试困难
- 日志记录要在内层事务commit后,否则查不到数据
四、MySQL与Redis:性能优化的核心战场
4.1 索引失效的11种场景(⭐⭐⭐⭐ 难度)
面试官问(阿里巴巴):
"什么情况下索引会失效?你能说10个以上吗?"
满分回答:
"MySQL索引失效的场景,我总结了11种,从实际踩坑经验出发:
1. 左模糊查询
%xxx-- 失效 ❌ SELECT * FROM user WHERE name LIKE '%三丰' -- 生效 ✅ SELECT * FROM user WHERE name LIKE '张%'2. 索引列上使用函数或运算
-- 失效 ❌(对year()结果建索引没用) SELECT * FROM order WHERE YEAR(create_time) = 2024 -- 生效 ✅(改写成范围查询) SELECT * FROM order WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01'3. 字符串不加引号
-- 失效 ❌(类型转换导致索引失效) SELECT * FROM user WHERE phone = 13800138000 -- 生效 ✅ SELECT * FROM user WHERE phone = '13800138000'4. OR连接条件不全有索引
-- 失效 ❌(phone有索引,name没有,OR导致全表扫描) SELECT * FROM user WHERE phone = 'xxx' OR name = 'xxx' -- 生效 ✅(改成两个查询UNION) SELECT * FROM user WHERE phone = 'xxx' UNION SELECT * FROM user WHERE name = 'xxx'5. 复合索引不遵循最左前缀原则
-- 复合索引:(A, B, C) -- 生效 ✅ WHERE A = ? AND B = ? WHERE A = ? -- 失效 ❌ WHERE B = ? AND C = ? WHERE C = ?6. 数据量太小(全表扫描更快) 索引的选择性 = 不同值数量/总行数,如果比值太小(< 0.1),优化器可能直接全表扫描
7. 使用NOT IN、NOT EXISTS
-- 失效 ❌ SELECT * FROM user WHERE id NOT IN (SELECT user_id FROM order) -- 生效 ✅ SELECT * FROM user u WHERE NOT EXISTS (SELECT 1 FROM order o WHERE o.user_id = u.id)8. IN子句包含太多值
-- 失效 ❌(IN 10000个值) SELECT * FROM product WHERE id IN (1,2,...,10000) -- 生效 ✅(先查ID,再IN) SELECT * FROM product WHERE id IN (SELECT product_id FROM sku WHERE status = 1)9. 隐式类型转换(已提到)
10. 统计信息不准确 ANALYZE TABLE更新统计信息
11. 使用SELECT *
-- 低效 ❌(回表次数太多) SELECT * FROM user WHERE name = 'xxx' -- 高效 ✅(覆盖索引,无需回表) SELECT name, age FROM user WHERE name = 'xxx' -- 前提:建联合索引(name, age)实战建议:用EXPLAIN查看执行计划,如果type是ALL说明全表扫描,需要优化。"
4.2 Redis大Key问题(⭐⭐⭐⭐ 难度)
面试官问(字节跳动):
"线上Redis某个Key有100万数据,怎么处理?"
满分回答:
"100万数据的Key属于典型的大Key问题,分两个方向解决:
一、大Key诊断
# 用Redis-cli的bigkeys命令分析 redis-cli -h 127.0.0.1 -p 6379 --bigkeys # 输出示例: # String keys: 0 # Hash keys: 10 (largest 10000000 bytes) # List keys: 5 (largest 20000000 bytes) # 或者用memory usage命令精确计算 redis-cli -h 127.0.0.1 -p 6379 MEMORY USAGE user:10086:followers二、解决方案
方案1:拆分大Key
// 场景:用户粉丝列表100万条 // 之前:user:123:followers → 100万个用户ID // 改成分片存储 public class FollowerCache { // 粉丝列表分片,每片1000个 // user:123:followers:0 → [uid1, uid2, ..., uid1000] // user:123:followers:1 → [uid1001, uid1002, ..., uid2000] public void addFollower(Long userId, Long followerId) { int shard = (int) (followerId % SHARD_COUNT); String key = String.format("user:%d:followers:%d", userId, shard); redisTemplate.opsForSet().add(key, followerId.toString()); } }方案2:改成Hash结构
// 100万个field改成小Hash // user:10086 → Hash // field: "user:10001" → value: "1"(关注了) // field: "user:10002" → value: "1" // 每次HGET/HMSET只操作一个field,网络传输小方案3:设置过期时间
// 热点数据定期访问,设置为短期过期 // 非热点数据自然淘汰 redisTemplate.expire("user:10086:followers", 7, TimeUnit.DAYS);方案4:后端异步加载
// 不一次性加载100万,先返回前1000条 public List<Long> getFollowers(Long userId, int page, int size) { String key = "user:" + userId + ":followers"; // 分页获取(Redis 6.2支持LSET/LSCAN) long start = (long) page * size; long end = start + size - 1; // 使用LRANGE分页,避免一次性加载 List<String> result = redisTemplate.opsForList().range(key, start, end); return result.stream() .map(Long::parseLong) .collect(Collectors.toList()); }踩坑记录:
⚠️ DEL命令也会阻塞!删除100万个元素的Key会阻塞Redis主线程几秒钟!
正确做法:用UNLINK代替DEL(异步删除)
UNLINK user:10086:followers # 异步删除,不阻塞或者用SCAN分批删除:
// 每次删除100个,分批删除 ScanOptions options = ScanOptions.scanOptions() .match("user:10086:followers:*") .count(100) .build(); redisTemplate.execute((RedisCallback<Void>) connection -> { Cursor<byte[]> cursor = connection.commands().scan(options); while (cursor.hasNext()) { byte[] key = cursor.next(); connection.commands().del(key); } return null; });
五、系统设计:高级工程师的必备技能
5.1 设计一个短链接系统(⭐⭐⭐⭐⭐ 难度)
面试官问(字节跳动):
"设计一个短链接系统,比如把
https://www.example.com/product/detail?id=12345转成https://dwz.cn/abc123。要支持10亿次访问/天。"
满分回答框架:
"好的,我分几个维度来分析:
1. 需求拆解
- 写入:生成短链接(用户量不大,假设每天1000万次)
- 读取:访问短链接跳转(10亿次/天)
- 特点:读多写少,需要优化读性能
2. 短链接生成算法
/** * 方法一:哈希后Base62编码 * 缺点:可能碰撞,需要查库 */ public String generateByHash(String url) { String hash = MD5(url); // 32位MD5 String base62 = Base62.encode(hash.getBytes()); return base62.substring(0, 8); // 取前8位 } /** * 方法二:发号器(推荐,无碰撞) * 用分布式ID生成器(雪花算法)生成递增ID,转Base62 */ public String generateById(Long id) { String base62 = Base62.encode(id); return "https://dwz.cn/" + base62; }3. 存储选型
存储 适用场景 原因 Redis 热点数据(访问量Top 1%) QPS可达100万+,毫秒级响应 MySQL 全量数据 10亿条,磁盘存储,成本低 HBase 超大规模+分析需求 适合海量存储+OLAP 推荐:Redis + MySQL分层存储
- 热数据放Redis(命中率>95%)
- 全量放MySQL(Redis miss时回源)
public String resolve(String shortUrl) { // 1. 先查Redis String longUrl = redis.get("short:" + shortUrl); if (longUrl != null) { return longUrl; // 命中,直接返回 } // 2. Redis没命中,查MySQL LongUrl record = mySql.selectByShortUrl(shortUrl); if (record != null) { // 3. 回填Redis,设置过期时间(热点数据缓存1天) redis.setex("short:" + shortUrl, 24 * 3600, record.getLongUrl()); return record.getLongUrl(); } return null; // 不存在 }4. 容量估算
存储空间: - MySQL存储10亿条,每条~200字节 → 200GB - 索引(B+Tree)额外50%空间 → 300GB Redis存储热点数据(1% = 1000万条): - 1000万 × 200字节 = 2GB QPS估算: - 10亿/天 = 10000000000/86400 ≈ 12万QPS - Redis单实例可支持10-20万QPS - 实际需要:1主3从 = 4台Redis 网络带宽: - 每次访问返回~200字节 - 12万QPS × 200B = 24MB/s ≈ 200Mbps - 实际需要:CDN加速5. 高可用设计
用户请求 → CDN → LVS/Nginx → 应用集群 → Redis集群 → MySQL集群 - CDN:全国节点,就近访问 - Redis:Cluster模式,16384个槽,自动分片 - MySQL:读写分离 + 分库分表(按shortUrl哈希分) - 监控:Redis监控 + MySQL慢查询监控6. 防爬/防刷
// 限流:同一个IP每秒最多10次 @Aspect public class RateLimitAspect { private final RedisTemplate redis; @Around("@annotation(RateLimit)") public Object limit(ProceedingJoinPoint point) throws Throwable { String ip = getClientIp(); String key = "ratelimit:short:" + ip; Long count = redis.opsForValue().increment(key); if (count != null && count > 10) { throw new BizException("访问太频繁,请稍后再试"); } if (count == 1) { redis.expire(key, 1, TimeUnit.SECONDS); } return point.proceed(); } }面试加分点:
- 提到雪花算法/百度UidGenerator做发号器
- 提到Redis Cluster vs Codis的选型
- 提到布隆过滤器防止恶意访问不存在的短链接
- 提到CDN预热热点数据"
六、生产环境避坑清单
┌─────────────────────────────────────────────────────────────────────┐
│ ⚠️ 大厂面试避坑清单(面试&实际工作双重避坑) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ❌ 面试不要这样答: │
│ ───────────────── │
│ 1. 只背答案不理解原理 │
│ → 追问就露馅 │
│ │
│ 2. 只说what不说why/how │
│ → 说明没有深入理解 │
│ │
│ 3. 没有项目经验支撑 │
│ → "理论我都懂,但实际用过吗?" │
│ │
│ 4. 过度追求"标准答案" │
│ → 面试官想看的是你的思考过程 │
│ │
│ ✅ 面试应该这样答: │
│ ───────────────── │
│ 1. 先说结论,再解释原因 │
│ 2. 结合项目经验,有数据支撑 │
│ 3. 能主动补充相关知识点(加分项) │
│ 4. 诚实:不懂就说不懂,不要硬扯 │
│ │
│ 📊 面试成功率对比(个人经验): │
│ ───────────────── │
│ 只背答案 → 能过一面,二面被挂 │
│ 理解原理 + 项目经验 → 成功率提升50% │
│ 主动补充 + 架构思维 → offer概率大幅提升 │
│ │
└─────────────────────────────────────────────────────────────────────┘
七、总结
┌─────────────────────────────────────────────────────────────────┐
│ 本文核心知识点回顾 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Java基础: │
│ ├─ HashMap:数组+链表+红黑树,链表8转树,尾插法 │
│ └─ synchronized vs ReentrantLock:自动vs手动,支持tryLock │
│ │
│ Spring全家桶: │
│ ├─ Bean生命周期:实例化→填充→初始化→销毁 │
│ └─ 事务传播:REQUIRED/REQUIRES_NEW/NESTED等7种 │
│ │
│ MySQL与Redis: │
│ ├─ 索引失效11种场景 │
│ └─ 大Key处理:拆分/Hash/UNLINK异步删除 │
│ │
│ 系统设计: │
│ └─ 短链接系统:Redis+MySQL分层,读多写少优化 │
│ │
│ 面试技巧: │
│ ├─ 先说结论,再说原因 │
│ ├─ 结合项目经验 │
│ └─ 主动补充加分项 │
│ │
└─────────────────────────────────────────────────────────────────┘
💬 今日话题
你在大厂面试中遇到过哪些印象深刻的问题?或者有什么面试技巧想分享?
欢迎评论区交流!
如果这篇文章对你有帮助,点赞 + 收藏是对我最大的支持!
📚 相关好文推荐:
原创不易,转载请注明出处