Java软件开发面试题小结(一) 📅 2026/8/7 10:22:28 1.mysql为什要用B树主要核心归结为两点“减少磁盘 I/O”和“高效范围查询”。简要理由如下磁盘读写代价低B树存的数据地址不是数据本身数据多层级少,B 树的节点大小固定且与磁盘页Page大小匹配每个节点可存储大量键值。范围查询性能极佳B 树的叶子节点通过链表相连。一旦找到范围的起点只需沿着链表顺序扫描即可无需像 B 树那样反复回溯父节点非常适合BETWEEN、或ORDER BY操作。全表扫描更快由于数据全部集中在叶子节点且有序遍历整个索引就是遍历链表顺序 I/O 开销小。稳定性高所有数据查询的路径长度高度完全相等保证每次查询耗时稳定避免了系统性能波动。2.mysql为什么要建索引哪些建议建索引索引是不是建的越多越好1为什么要建索引大幅减少磁盘I/O次数提升查询速度。没有索引时mysql会全表扫描慢如大海捞针有索引B树结构查询路径固定为树的层高通常2-4次I/O快如按拼音查字典。2哪些字段建议建索引主键唯一键频繁作为查询条件的字段where子句经常用于排序order by分组group by的字段多表联查join的外键字段。注意对于性别、状态这样区分度极低的字段只有男/女两种值不建议建索引因为扫描一半数据和全表扫描没什么区别还有维护开销。3索引是不是建的越多越好先回答当然不是原因如下写入变慢每次插入、更新、删除数据时所有索引都必须同步更新。索引过多会严重拖慢写入速度占用磁盘空间索引本身就是物理文件会消耗大量存储优化器负担索引太多mysql查询优化器在选择最优路径时计算成本增加反而可能选错索引。核心原则“少而精”。只在高频查询的关键字段上建立索引避免为每个字段都建索引。通常建议单表索引数控制在 5~6 个以内。3.什么叫序列化为什么要序列化序列化简单说就是把内存中的“活”对象转换成“死”的字节流或JSON、XML等字符串的过程反序列化则是把字节流还原回内存对象。为什么要序列化原因有1久存储持久化内存数据断电就丢序列化后存到硬盘下次重启还能读回来2网络传输进程间或不同服务器通信时必须把内存对象序列化成二进制流才能在网络上“搬运”3跨平台/语言交互把对象序列化成标准格式如JSON实现Java调用Python、Go服务等异构系统间的数据交换。4.为什么不用自动编号而用雪花编号1不用自增编号的核心原因1分布式下ID会冲突分库分表时无法保证全局唯一2存在安全风险连续ID容易泄露业务量3高并发有瓶颈依赖数据库锁性能受限。2改用雪花编号的核心理由1天然支持分布式靠时间戳机器码生成不依赖数据库2性能极高内存计算每秒百万级3趋势递增对B树索引友好避免页分裂4不泄露信息ID不连续更安全。5.开闭原则和单一职责原则1开闭原则OCP定义对扩展开放对修改关闭好处不破坏已有稳定代码降低风险2单一职责原则定义一个类只做一件事且只有一个引起它变化的原因好处改动时只影响一个模块代码更清晰、易维护。6.实体类中字段类型为什么用引用类型包装类型Integer而不是int核心原因Integer是对象引用类型int是基本数据类型。1集合框架必须用对象ListSetMap等集合只能存放对象不能存放基本数据类型Listint编译直接报错2需要表示“空值”null数据库里字段可能是NULL用int只能默认表示0无法区分“值为0”和“没有值”用Integer可以直接赋值null 表示空3泛型不支持基本数据类型定义泛型时T必须是类不能是int只能是Integer4提供了实用的工具方法Integer.parseInt、Integer.valueOf等方便做类型转换int没有这些方法。7.枚举类型什么时候使用为什么要写枚举类型1什么时候用枚举状态SUCCESSFAILPENDING性别MALEFEMALE订单类型ONLINEOFFLINE季节SPRINGSUMMERAUTUMNWINTER。2为什么要写枚举不用常量1类型安全用int或String常量时可以传入任意值如status 999编译器不报错。枚举只能传入定义号的那几个值编译期就能发现错误2代码可读性强OderStatus.SUCCESS比1或者“s”直观得多见名知意魔法数字的可读性极差而且不好维护例如想要修改时要挨个去把1换成0效率很低但枚举类型只需要改一下枚举的定义就行一劳永逸3自带校验杜绝脏数据接收前端传参时用枚举直接反序列化非法值会直接抛异常不用手动写if校验4可附加属性和方法枚举不只是常量还可以带字段和方法比如public enum Status { SUCCESS(1, 成功), FAIL(0, 失败); private int code; private String desc; // 构造器、getter... }这样状态码和描述绑定在一起比分开定义多个常量数组更内聚。8.token到底是放在字段中返回还是请求头里面返回标准做法放在请求头Header里返回。1为什么放在请求头1符合规范HTTP设计了专门的Authorization头标准用法是AuthorizationBearer token2安全性更好放在请求头里不会出现在URL中也就不会被浏览器历史、服务器日志、代理服务器记录下来降低泄露风险3避免GET请求限制URL长度有限制Token可能很长放Header没问题。2什么时候放请求胡体或字段里1文件上传/下载等特殊场景下部分网关可能不解析Header可临时放Body或URL参数2WebSocket等协议不支持自定义Header时可放请求参数里。9.工具类有些为什么用component丢入容器中有些为什么又不需要核心分歧在于这个工具类到底有没有“状态”成员变量。1什么时候需要用Component丢进容器当工具类需要依赖Spring的其他Bean时比如需要用到RedisTemplate、RestTemplate、JdbcTemplate 或 Value配置参数。Component public class RedisUtil { Autowired private RedisTemplateString, Object redisTemplate; // 需要注入依赖 public void set(String key, Object value) { redisTemplate.opsForValue().set(key, value); } }原因这个工具类自己无法创建RedisTemplate必须由 Spring 容器把准备好的 Bean 注入进来所以它自己也得是个 Bean受容器管理。2什么时候不需要Component当工具类只依赖JDK原生类无状态全是静态方法时不需要交给Springpublic class StringUtil { public static boolean isEmpty(String str) { return str null || str.length() 0; } }原因1它不依赖任何外部Bean不需要Spring帮忙注入2方法全是static直接类名.方法名调用不需要先创建对象3丢进容器反而增加Spring管理的开销且没必要。3误区须知有些项目会把“纯静态工具类”也加上Component然后通过Autowired注入使用Component public class DateUtil { public static String format(Date date) { ... } } Service public class OrderService { Autowired private DateUtil dateUtil; // 多此一举 public void test() { dateUtil.format(new Date()); // 明明可以直接 DateUtil.format() } }这样做的坏处1增加Spring容器负担2多写了注入代码没有必要3如果工具类里只有静态方法注入进来的对象其实根本没用到实例变量名纯属浪费。4一个特殊场景Component PostConstruct 初始化有时候即使工具类依赖外部Bean也可以用PostConstruct 把注入的实例赋值给静态变量从而既能用静态方法又能依赖SpringComponent public class RedisUtil { private static RedisTemplateString, Object redisTemplate; Autowired private RedisTemplateString, Object redisTemplateAutowired; PostConstruct public void init() { redisTemplate redisTemplateAutowired; } public static void set(String key, Object value) { redisTemplate.opsForValue().set(key, value); // 静态方法使用静态变量 } }辅助理解Component public class RedisUtil { // 第1步定义一个静态变量用来存放 RedisTemplate // 因为静态方法只能访问静态变量 private static RedisTemplateString, Object redisTemplate; // 第2步通过 Autowired 把 Spring 容器里的 RedisTemplate 注入进来 // 注意这个是实例变量非静态Spring 可以注入 Autowired private RedisTemplateString, Object redisTemplateAutowired; // 第3步用 PostConstruct 做“收尾工作” // PostConstruct 的作用当前类的实例创建完成、依赖注入完成后自动执行这个方法 PostConstruct public void init() { // 把注入进来的实例变量赋值给静态变量 RedisUtil.redisTemplate redisTemplateAutowired; } // 第4步对外提供静态方法直接用静态变量 public static void set(String key, Object value) { redisTemplate.opsForValue().set(key, value); } public static Object get(String key) { return redisTemplate.opsForValue().get(key); } }Spring 启动 │ ▼ 1. 扫描到 Component创建 RedisUtil 的实例对象 │ ▼ 2. 看到 Autowired把 RedisTemplate 注入到 redisTemplateAutowired 字段 │ 此时 redisTemplateAutowired 有值了但 redisTemplate 还是 null ▼ 3. 看到 PostConstruct自动执行 init() 方法 │ 把 redisTemplateAutowired 的值赋给静态变量 redisTemplate ▼ 4. 启动完成redisTemplate 静态变量有了值 │ ▼ 调用方执行 RedisUtil.get(key) │ ▼ 静态方法里的 redisTemplate 已经有值了正常执行。10.拦截器和过滤器的区别是Filter 是 Servlet 管道的“大门守卫”Interceptor 是 Spring MVC 的“安检员”且 Interceptor 能用 Spring 的 Bean。1拦截器Filter触发时机请求进入Servlet前最早作用范围所有请求含静态资源能否注入Bean不能直接Autowired典型场景编码、跨域处理CORS、压缩。2拦截器Intercept触发时机请求进入Controller前稍晚作用范围只对 Controller方法生效能否注入Bean可以典型场景登录校验、权限、业务日志。3执行顺序请求到达 │ ▼ 【过滤器 Filter】 ← 最早属于 Servlet 层面 │ doFilter() ▼ DispatcherServletSpring 前端控制器 │ ▼ 【拦截器 Interceptor】 ← 属于 Spring MVC 层面 │ preHandle() ▼ Controller业务方法 │ ▼ 返回结果 │ ▼ 【拦截器 Interceptor】 │ postHandle()、afterCompletion() ▼ 【过滤器 Filter】 │ 返回给客户端 ▼ 客户端收到响应11.java怎么连接到数据库的JDBCJDBCJava Database Connectivity是 Java 提供的一套标准数据库连接 API底层通过数据库驱动Driver与具体数据库通信。核心流程6步1加载驱动JDBC 4.0后可以省略2获取连接建立TCP连接3创建StatementSQL执行器4执行SQL发送到数据库执行5处理结果集6关闭资源通过 DriverManager 获取 Connection → 创建 Statement 执行 SQL → 处理 ResultSet → 关闭资源。实际项目中会用MyBatis或Spring JdbcTemplate帮我们封装这些重复代码让我们专注写 SQL。12.StringStringBuilderStringBuffer的区别1String可变性不可变线程安全安全只读性能最低使用场景少量字符串操作、常量2StringBuilder可变性可变线程安全不安全性能最高适用场景单线程下大量拼接最常用3StringBuffer可变性可变线程安全安全同步锁性能较低适用场景多线程下大量拼接String底层字符数组被final修饰每次拼接都会生成新字符串对象效率差。StringBuilder可变无锁单线程性能高。StringBuffer方法带同步锁多线程安全性能较差。13.List扩容机制(1)ArrayList的扩容机制最常用初始容量默认 10 无参构造时或指定初始容量触发条件添加元素时当前元素个数 1 数组容量时触发扩容扩容大小新容量 旧容量 旧容量 1 即 1.5 倍底层实现Array.copyOf将旧数组元素拷贝到新数组耗时操作。ArrayListInteger list new ArrayList(); // 初始容量 10 for (int i 0; i 15; i) { list.add(i); // 第 11 个元素插入时触发扩容10 → 15 }扩容过程1容量 10 满 — 创建新数组容量 15 10 10 1 15 2把旧数组 10 个元素拷贝到新数组3插入第11个元素。关键点扩容是“懒加载”new ArrayList()时内部数组其实是空数组第一次add是才真正初始化为容量为 10 指定初始容量可以避免频繁扩容如果预知数据量用new ArrayList(1000)可提升性能。// 预知 1000 条数据直接指定容量避免多次扩容 ListString list new ArrayList(1000);2Vector扩容机制线程安全版与ArrayList类似但默认扩容为 2 倍可配置增量线程安全方法用synchronized修饰但性能差实际项目中基本不用。3LinkedList没有扩容机制底层是双向链表节点在内存中离散存储每添加一个元素只新建一个节点并调整前后指针不需要连续内存空间也不存在“扩容拷贝”。4为什么ArrayList要扩容1.5 倍而不是 2 倍2倍扩容每次扩容后之前占用的空间很可能无法被复用容易造成内存浪费1.5倍扩容后续释放的内存更容易被利用是一种空间和时间上的平衡。5性能优化建议场景预知数据量 建议指定初始容量避免多次扩容拷贝场景频繁插入/删除中间位置 建议考虑LinkedList场景频繁随机访问 建议用ArrayList场景大量尾部追加 建议ArrayList效率高但预分配容量更佳14.面向服务架构面向服务架构SOA是一种软件设计理念核心思想是将应用程序的不用功能单元拆分为独立的“服务”这些服务通过标准化的接口和协议如WebService、SOAP相互通信共同完成业务功能。通俗理解把一个大系统拆成多个“小服务”每个服务做一件事并通过网络调用互相协作。核心特点1松耦合服务之间通过简单的接口进行通信不直接依赖彼此的实现降低了系统之间的依赖关系2服务复用每个服务都是一个独立的功能单元可以被多个应用和系统调用和复用提高了功能的复用率3标准化接口服务通常使用标准协议如HTTP、SOAP、REST进行通信确保跨平台和跨语言的兼容性4自治性每个服务拥有自己的数据和逻辑能够自主处理请求降低了对其他服务的依赖5可组合性服务可以按照需求组合成新的服务或业务流程通过编排和合成实现复杂的业务功能。15.数据库连接池是什么数据库连接为什么一定要数据库连接池1数据库连接池是什么连接池就是预先创建好一批数据库连接放到一个“池子”里缓存起来需要时直接借用用完归还而不是用完就关闭销毁。2为什么一定要用连接池不用的代价1不用连接池每次请求都要走一遍完整的TCP三次握手 数据库认证握手耗时 50 ms ~ 100 ms高并发下数据库瞬间被压垮2用连接池连接是预先创建好的借用—执行SQL—归还省去了重复建立和销毁连接的网络开销响应时间大幅降低。总结连接池的核心作用就是“复用连接”避免频繁创建销毁带来的巨大网络开销和资源浪费。没有连接池高并发系统根本扛不住。主流实现如HikariCPSpringBoot 默认、Druid阿里。16.二次索引1什么是二次索引二次索引是相对于主键索引聚簇索引而言的指建立在非主键字段上的索引。它的叶子节点存储的是主键值或行指针而不是完整的数据行。2具体查询过程1场景1按主键查直接命中SELECT * FROM user WHERE id 3;过程走主键索引B树的叶子节点直接挂载着完整数据行(3, 王五, 28)。一次I/O直接返回不需要回表。2场景2按 name 查走二级索引SELECT * FROM user WHERE name 李四;过程走二级索引idx_nameB树的叶子节点存的是(李四, 2)—— 即 name 值 对应的主键 id。拿到主键id 2后再去主键索引查完整数据行(2, 李四, 30)。一共 两次I/O这就是回表查询。3场景3覆盖索引不用回表SELECT id, name FROM user WHERE name 李四;过程走二级索引idx_name叶子节点已经有(李四, 2)了。查询需要的是id和name二级索引里刚好都有直接返回不需要再去主键索引回表。只用了一次I/O这就是覆盖索引。3总结主键索引 按页码查书直接翻到正文。二级索引 按目录查书先查目录得页码再翻正文。多翻一次正文就叫回表。如果目录上已经写明了你要的内容覆盖索引就不用再翻正文了。17.OLAP和OLTP1OLTP在线事务处理定义面向日常业务操作的数据库系统处理大量的增删改查事务。核心特点1事务性强一致性ACID要么全成功要么全失败2高并发同时处理成千上万的用户请求3低延迟毫秒级响应如查询余额、扣库存4数据量相对小操作通常只涉及少量记录如一条订单5频繁写入增删改远多于查询代表数据库MySQL、PostgreSQL、Oracle。2OLAP在线分析处理定义面向数据分析决策的数据库系统处理大量的复杂查询和聚合计算。核心特点1面向分析查询通常是海量数据的聚合如SUM、GROUY BY2读多谢少数据批量导入后主要是复杂查询3响应时间不敏感查询可能耗时数秒甚至数分钟4数据量大历史数据通常以亿级、十亿级起步5历史数据为主分析过去的数据很少修改代表数据库ClickHouse、Doris、Greenplum、Hive大数据场景3总结OLTP 管“干活”面向一线业务要快、要准、要稳如用户下单扣库存。OLAP 管“看盘”面向老板和分析师要能算得动海量数据如上个月总营收。4旁伸MySQL 只能做 OLTP不能做 OLAP。OLAP 用 MySQL 跑复杂聚合查询性能极差会拖垮业务库所以通常用专门的 OLAP 引擎。一个系统只能选一种。实际架构中通常OLTP 库 存热数据如 MySQLOLAP 库 存历史归档数据如 ClickHouse两者之间通过ETL同步数据。18.注册中心和配置中心1注册中心Nacos作为注册中心核心作用维护服务实例的动态地址簿1注册服务启动时上报IP 端口 服务名2发现消费者向注册中心拉取可用实例列表3健康检查心跳超时则自动剔除故障节点4负载均衡返回实例列表供客户端自行选择总结让服务互相找得到且只找的到活着的。2配置中心Nacos作为配置中心核心作用集中管理所有服务的配置并支持动态刷新。1集中化所有服务的配置文件统一存放在Nacos2热更新修改配置后实时推送给服务无需重启3环境隔离支持dev / test / prod等多环境配置隔离4版本管理配置变更可追溯、可回滚总结改配置不用重启服务秒级生效。3两者合体Nacos 独特优势大多数注册中心如 Eureka只管注册配置中心如 Apollo只管配置。Nacos 把两者整合到一个产品中是它的核心卖点。19.OpenFeign 和 gRpc1OpenFeignOpenFeign是 Spring Cloud 生态中的声明式 HTTP 客户端简化了基于 RESTful API 的微服务间调用。开发者通过定义接口和注解即可实现远程服务调用无需手动编写 HTTP 请求代码。适用于基于 HTTP/REST 的微服务通信常用于 Spring Cloud 生态如结合 Eureka/Nacos 实现服务发现。优点代码简洁开发效率高无需手动处理序列化、HTTP 连接等天然适配 Spring Cloud 生态如服务发现、熔断器等。缺点仅支持 HTTP/REST性能低于二进制协议如 gRPC对多语言支持较弱主要面向 Java 生态。2gRpcgRPC是 Google 开源的高性能 RPC远程过程调用框架基于 HTTP/2 协议和 Protocol BuffersProtobuf 序列化协议。适用于高性能、低延迟的内部服务通信尤其适合多语言混合的微服务架构。优点性能优异低延迟、高吞吐适合大规模内部服务通信。缺点需要预编译 .proto 文件开发流程稍显复杂对浏览器端支持较弱。20.什么是RESTful APIRESTful API 简单说就是一套利用 HTTP 协议规范来操作数据的接口设计套路。它把一切数据都看作“资源”并通过固定的网址URL和标准的动作HTTP方法来操作这些资源。1三大核心要素记住这三点就够了资源URL万物皆资源每个资源有唯一地址。比如https://api.example.com/users/123就是“ID为123的用户”这个资源。动作HTTP方法用标准动词表示操作。GET查、POST增、PUT/PATCH改、DELETE删。状态状态码用标准HTTP状态码表示结果。200成功、201创建成功、400请求错误、404找不到、500服务器错误。2优秀RESTful API的样子最佳实践网址用名词复数不用动词/orders✅/getOrders❌。版本号放URL里/v1/users。用JSON格式传数据。3两个关键原则无状态服务器不记住客户端状态每个请求都自带“身份证”如Token。这方便横向扩展统一接口无论操作什么资源方法GET/POST...和格式JSON都是统一的。4易混淆点提醒RESTful API ≠ 数据库CRUD它是面向客户端App/网页的“资源操作接口”不只是对数据库的增删改查。用了JSONHTTP≠RESTful如果不满足“无状态”和“统一接口”等约束只能叫HTTP API。21.微服务远程调用和消息传递1远程调用同步通信服务A主动调用服务B的接口并阻塞等待结果返回。实现方式1基于HTTP的RESTful API最主流服务间通过HTTP客户端调用彼此的API端点2RPC框架如Dubbo、gRpc。性能更高适合内部服务间高频调用但通常不对外开放。特点1实时性立即得到结果2强依赖服务B必须在线且响应快否则服务A会超时或失败3耦合性调用方需要知道被调用方的地址和接口细节。适用场景需要立即返回结果的交互如用户登录验证、实时查询订单详情。2消息传递异步通信服务A发送一条消息到消息中间件如RabbitMQ、Kafka后立即返回不等待服务B处理。服务B从中间件消费信息并自行处理。实现方式1引入消息队列MQ作为中转站。特点1流量削峰能应对突发流量消息暂存在队列中消费者按自身能力处理2解耦服务A不需要知道服务B是谁、在哪、只需把消息发送到队列。3最终一致性不要求实时一致保证最终状态正确即可即支付后发短信。适用场景不需要实时响应的操作如订单支付成功后发送通知邮件、更新缓存、数据同步。3在微服务中的配合使用成熟系统会混合使用两者1前端请求如查询商品使用RESTful远程调用立即拼接数据返回给用户。2后端异步任务如下单后扣减库存使用消息传递确保订单生成后库存、积分、物流等系统最终收到指令并执行保证系统高可用。