返回文章列表
技术·2026.08.05

Java 面试八股文:高频题与我的答题思路

从 == 和 equals、装箱拆箱,到 Spring 的 IoC/AOP、左右连接、三层架构、短 URL 设计——把面试常问的题按我自己的理解重新讲一遍。

Java 面试八股文:高频题与我的答题思路

又到跳槽季。去年这个时候我还在背题,今年趁着空窗期,把面试里反复被问到的 Java 问题重新过了一遍,顺手整理成这份笔记。不是标准答案,只是我答题时的思路,尤其是一些基础得让人容易轻敌的小题。

Java 基础:最容易翻车的题

== 和 equals,到底差在哪

记住一句话就行:== 比较的是"是不是同一个东西",equals 比较的是"内容相不相等"。

== 用在基本类型上比数值,用在引用类型上比内存地址——也就是看两个引用是不是指向同一个对象。而 equalsObject 自带的方法,默认实现其实也是比地址,但 String 重写了它,改成了比字符串内容。

有个特别经典的坑,我面试的时候第一轮就栽过:

Integer a = 127, b = 127;
a == b;          // true
Integer c = 128, d = 128;
c == d;          // false

原因是 Integer 默认缓存了 -128 到 127,这个范围里的值直接复用同一个对象;超出去就是 new 出来的新对象,== 自然不相等。所以代码里比较两个 Integer,老老实实用 equals

装箱和拆箱

int 是基本类型,Integer 是它的包装类。把 int 变成 Integer 叫装箱,反过来叫拆箱,JDK 5 之后都是自动完成的,所以写起来几乎感觉不到它们是两种东西。

坑在拆箱:Integer x = null; int y = x; 这一行直接 NPE。集合里存不了基本类型,只能存包装类,于是到处都在悄悄装箱拆箱。遇到那种"明明没写错什么,就是报空指针"的情况,先想想是不是这里。

为什么 String 要设计成不可变

问这个多半是想看你有没有往深处想过。不可变带来的好处很实在:字符串常量池可以放心复用、HashMap 里当 key 不会被意外改坏、多线程下天然安全。StringBuilder 就是为"要改"的场景准备的。

equals 和 hashCode 的关系

规则就一条:重写了 equals,就必须重写 hashCode,而且要保证"equals 相等的两个对象,hashCode 一定相等"。反过来不要求。违反这条,HashMap 里同一个 key 会落到不同的桶,明明存进去了却取不出来,查这种 bug 最磨人。

集合

HashMap 的底层结构、扩容、为什么线程不安全这些,我单独写了一篇,这里只讲一句话结论:数组 + 链表 + 红黑树,JDK 8 之前并发扩容会死循环,之后会丢数据,并发场景请直接上 ConcurrentHashMap。详见《HashMap 面试题,这一次彻底讲明白》。

Spring

IoC 和 AOP 到底是什么

这是 Spring 面试的必考题,但也是最容易被"背概念"带偏的题。我不喜欢按书上的定义念,更喜欢这么讲:

IoC(控制反转)说的其实是一件事:对象的创建和依赖关系不归你管了,交给容器。你要一个 UserService,直接写字段声明,Spring 在启动的时候替你 new 好、把依赖塞进来。你不用 new,也不用担心顺序——这就是"反转",控制权从你手里转到了容器手里。DI(依赖注入)就是实现这件事的手段。

AOP(面向切面)解决的是另一类问题:日志、事务、权限这种跟业务无关、又散落在各个方法里的逻辑。AOP 把这些横切逻辑抽出来,在方法执行前后帮你织入,代码里不用反复写。实现上靠代理——接口用 JDK 动态代理,没接口用 CGLIB 生成子类,Spring Boot 2.x 之后默认 CGLIB。

一个常见的追问:同一个类里方法互相调用,事务为什么失效?因为 AOP 靠代理生效,this.a()this.b() 走的是对象自己,没走代理,注解自然不认。要生效得把自己注入进来,或者把 b() 拆到另一个 Bean 里。

循环依赖怎么解决

Spring 靠三级缓存:创建中的 Bean 提前暴露一个半成品,供依赖它的 Bean 引用。构造器注入的循环依赖解不了,因为连对象都还没造出来。

MySQL

左连接和内连接

我理解内连接就是"两边都有的才留下",左连接是"以左边为准,右边没有就补 NULL"。写 SQL 的时候先想清楚主表是谁、你想保留哪些行,比背"LEFT JOIN 返回左表所有记录"这种话靠谱。

-- 内连接:只有两边都匹配的行
SELECT u.name, o.id FROM users u JOIN orders o ON u.id = o.user_id;
-- 左连接:左边的人都在,没下单的 orders 是 NULL
SELECT u.name, o.id FROM users u LEFT JOIN orders o ON u.id = o.user_id;

实际开发里左连接比内连接用得多,因为"人没下单也要展示出来"的场景太常见了。注意一点:左连接后右边可能有一堆 NULL,记得想清楚 WHERE 条件写在 ON 里还是 WHERE 里——写在 WHERE 里一过滤,可能就把"补出来的 NULL 行"又删掉了,等于变回内连接。

索引为什么用 B+ 树

一句话:矮。树矮意味着查一次走的层数少,落盘也就是几次 IO;叶子节点之间还有链表连着,范围查询顺着链走就行。这一块我后面会单独写。

三层架构:controller / service / mapper

这是我天天写的架子,也是最常被问"为什么这么分"的题。我的理解是:每一层只干一件事,改哪一层不牵连别人。

  • Controller:只负责接请求、回响应。参数的校验、状态的码,到此为止。
  • Service:真正的业务逻辑在这。下单要查库存、扣库存、写订单、发通知,这一串编排全在 service 里。
  • Mapper:只跟数据库打交道,写 SQL、做映射,不掺和业务。

为什么不直接在 controller 里写 SQL?因为业务复杂起来,一个下单可能被支付、售后、报表到处引用,你把它摊在 controller 里,改一次接口就要翻遍所有调用方。分层以后,业务逻辑是"一个能被复用的方法",而不是"一段贴在接口上的代码"。

MyBatis 和 Spring Boot

这俩其实是两个层面的东西。Spring Boot 是"把你从配置地狱里捞出来的脚手架"——内嵌 Tomcat、自动装配、约定大于配置,项目从零到能跑只用几分钟。MyBatis 是"帮你写数据库访问的框架",半自动 ORM,SQL 你自己写,它负责把参数填进去、把结果映射回对象。

一个必问的细节:#{}${} 的区别。

#{} 是预编译参数,MyBatis 会把参数用 ? 占位,走 PreparedStatement,数据库层面就防住了 SQL 注入;${} 是直接拼字符串,把值原样怼进 SQL 里。表名、列名这种不能预编译的地方才用 ${},但传进来的值一定得自己严格校验。凡是"拼接用户输入"的地方用 ${},基本就是给注入留后门,别这么干。

设计题:一个短 URL 服务怎么设计

这种题面试官要的不是一个现成系统,是你拆问题的思路。我一般按三步答:

  1. 发号器:长链接对应一个唯一 ID,再把这个 ID 转成 62 进制(26 大写 + 26 小写 + 10 数字),就是短码。62 进制的 7 位能表示 62^7 左右,够用了。ID 怎么生成?简单点用数据库自增,要求高就用雪花算法(Snowflake)——趋势递增、不依赖单点、并发下不重复。
  2. 存储:一张表,短码和长链接一一对应。短码当主键或唯一索引,访问时一次查出来,302 跳过去。
  3. 读多写少:加缓存,热点短码放 Redis,命中就少一次 DB;再防一下同一长链接重复入库,加个唯一索引或者先查后写。

最后可以补一句:短码要用"不可预测"的,避免别人拿短码乱猜爬别人家的链接——这点在公开场景很重要。

还应该看什么

并发那套(volatile、synchronized、线程池)我写了《JVM 内存与垃圾回收,面试官最爱问的几件事》。另外 2026 年大模型这块我也在追,感兴趣可以看《2026 年大模型,我关注到的几个方向》。

面试说到底不是考你背了多少,是看你能不能把一个东西讲明白。能把"为什么"讲清楚的人,通常也不缺"是什么"。

阅读 0·点赞 0