大厂Java面试题实录:阿里面经+字节面经+美团面经,带解题思路+代码+考官心理分析

0 阅读15分钟

大厂Java面试题实录:阿里面经+字节面经+美团面经,带解题思路+代码+考官心理分析

Java_interview_preparation__te_2026-04-19T03-25-28.png

🔥 写在前面:这篇文章不是网上随便搜的"面试题库",是我和几位朋友(分别在阿里、字节、美团、腾讯工作)真实面试经历的复盘。每个问题都标注了难度星级考察意图满分回答踩坑点

⚠️ 郑重声明:面试题因人而异,这篇文章仅供参考。核心是学习解题思路,而不是背答案。


一、先说清楚:大厂面试到底在考什么?

很多人以为大厂面试是"知识问答",错了。面试的本质是:

┌─────────────────────────────────────────────────────────────────┐
│                   面试官的心理活动                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  "这个人能不能解决我没遇到过的实际问题?"                          │
│                                                                 │
│  表面:在问技术知识                                                │
│  实际:在考察:                                                    │
│  ├─ 思维过程:遇到新问题怎么分析?                                 │
│  ├─ 技术深度:只知道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();
        }
    }
}

核心区别总结

特性synchronizedReentrantLock
用法自动加锁释放手动tryLock/unlock
公平锁不支持支持(new ReentrantLock(true))
tryLock不支持支持
条件变量不支持(只能Object.wait)支持多个Condition
底层JVMAQS+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种传播行为:

  1. REQUIRED(默认):加入当前事务,没有则创建新事务
  2. REQUIRES_NEW:挂起当前事务,创建新事务
  3. SUPPORTS:支持当前事务,没有则非事务执行
  4. NOT_SUPPORTED:不支持事务,挂起当前事务
  5. MANDATORY:必须在事务中执行,没有则抛异常
  6. NEVER:必须不在事务中执行,有则抛异常
  7. 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分层,读多写少优化                     │
│                                                                 │
│  面试技巧:                                                        │
│  ├─ 先说结论,再说原因                                            │
│  ├─ 结合项目经验                                                 │
│  └─ 主动补充加分项                                                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

💬 今日话题

你在大厂面试中遇到过哪些印象深刻的问题?或者有什么面试技巧想分享?

欢迎评论区交流!

如果这篇文章对你有帮助,点赞 + 收藏是对我最大的支持!


📚 相关好文推荐


原创不易,转载请注明出处