Java 笔试复盘

这次整理的材料来自一次某银行IT子公司Java开发笔试录音,以及笔试时用在线IED编写的代码。
相比单纯记录题目,我更希望把它整理成一篇可以反复阅读的复盘文章:题目考了什么、面试时应该怎样回答、哪些地方容易答错,以及下一步应该如何补强。

原题目内容记忆不完全,下面只记录能够确认、并且对 Java 面试复习有价值的内容。

一、这次笔试考了什么

从能辨认出的题目看,考查范围比较集中,主要包括:

  • Java 面向对象和设计模式;
  • HTTP 基础和 Web 安全常识;
  • JDBC、SQL 注入和数据库访问;
  • Java 字符串、常量池、final 和引用比较;
  • MySQL 联合索引和查询优化;
  • Optional、Stream 和方法引用;
  • 基础字符串算法和复杂度分析。

这类题目的特点是:单个知识点并不一定很难,但容易在“绝对化表述”和边界条件上失分。比如“POST 一定更安全”“GET 长度固定是 2KB”“使用了索引就一定高效”等说法,都需要谨慎判断。

二、Java 面向对象与设计模式

1. 继承和组合有什么区别

继承表达 is-a 关系,组合表达 has-a 关系。

例如:

  • Dog is an Animal:适合用继承表达;
  • Car has an Engine:适合用组合表达。

通常更推荐组合,原因是组合可以在运行时替换依赖,类之间耦合也更低。继承会让子类依赖父类的实现细节,父类修改可能影响所有子类。

但继承不是不能用。如果子类确实属于父类的稳定子类型,并且能够满足父类约定、符合里氏替换原则,就可以使用继承。不能只是为了复用几段代码而盲目继承。

2. 什么是里氏替换原则

如果 S 是 T 的子类型,那么所有使用 T 的地方都应该可以替换成 S,而不破坏程序的正确性。

简单来说,子类不仅要“拥有父类的方法”,还要遵守父类对这些方法的行为约定。子类不能随意加强输入限制、削弱输出保证,或者改变调用方依赖的核心语义。

3. 静态工厂方法和工厂方法模式

静态工厂方法通常是在一个工厂类中通过静态方法和条件判断创建不同产品,例如:

Product product = ProductFactory.create(type);

它实现简单、调用方便,还可以隐藏具体实现类。但新增产品类型时,往往需要修改工厂中的判断逻辑。

工厂方法模式则通常定义工厂抽象,再由不同的具体工厂创建对应的具体产品。新增产品时可以增加新的工厂和产品,较少修改已有代码,更符合开闭原则,但类的数量会增加。

面试时可以这样总结:

静态工厂方法是一种创建对象的技术,工厂方法模式是一种面向扩展的设计模式。前者更简单,后者更适合产品类型经常扩展的场景。

三、HTTP 与 Web 基础

1. GET 和 POST 的区别

GET 通常用于获取资源,参数一般位于 URL 查询字符串中,比较容易被缓存、收藏和记录。POST 通常用于提交数据、创建资源或触发业务操作,数据一般放在请求体中。

但“POST 比 GET 更安全”不能作为完整结论。POST 数据虽然不直接显示在 URL 中,但如果使用的是 HTTP 明文传输,数据仍然可能被窃听。真正的安全措施包括:

  • 使用 HTTPS;
  • 做好身份认证和权限校验;
  • 校验和限制输入;
  • 对日志中的敏感信息脱敏;
  • 不把密钥或密码暴露在 URL 中。

2. GET 参数长度是不是固定 2KB

不是。HTTP 协议没有规定 GET 固定只能传 2KB,也没有规定 POST 固定只能传 2048 个字符。

实际限制可能来自浏览器、Web 服务器、反向代理、网关、框架配置和应用层校验。GET 的 URL 通常更容易受到长度限制,但具体数值不能一概而论。

3. 什么是幂等性

幂等性是指同一个请求执行一次或执行多次,对服务器预期最终状态的影响相同。

  • GET 通常是幂等的;
  • PUT 通常按幂等语义设计;
  • DELETE 通常也按幂等语义设计;
  • POST 通常用于创建或触发操作,一般不保证幂等。

幂等不代表每次响应文本完全一样。例如第一次 DELETE 可能返回删除成功,第二次返回资源不存在,但资源最终状态都是“不存在”。

四、JDBC 与 SQL 注入

1. Statement 和 PreparedStatement

Statement 通常执行拼接后的完整 SQL,PreparedStatement 使用带占位符的 SQL,再通过参数绑定传入值。

PreparedStatement 的优点主要有:

  • 减少 SQL 字符串拼接;
  • 提高代码可读性和可维护性;
  • 通过参数绑定防止常见的 SQL 注入;
  • 对重复执行的同结构 SQL 通常更适合。

对于只执行一次的简单 SQL,Statement 可能少一步预编译,理论上有时开销更低。但不能绝对地说 Statement 一定更快,实际结果取决于数据库驱动、数据库版本、执行计划和缓存策略。

2. 为什么参数绑定可以防止 SQL 注入

参数绑定会把用户输入作为参数值处理,而不是重新解析成 SQL 语法的一部分。即使输入中包含引号或 SQL 关键字,也不会因为字符串拼接而改变原 SQL 的结构。

不过,表名、列名和排序字段通常不能直接通过普通参数绑定传入。这些内容需要使用白名单映射,不能直接拼接用户输入。

实际开发中还需要配合最小权限数据库账号、输入校验和日志脱敏,不能把安全性完全寄托在一个 API 上。

3. JDBC 资源如何管理

数据库连接、语句和结果集都属于有限资源,使用后必须释放。Java 7 以后推荐使用 try-with-resources 自动关闭资源。

如果涉及事务,还要明确提交和回滚边界。发生异常时不能只关闭连接,还要确认事务是否已经回滚,避免留下部分成功的脏状态。

五、Java 字符串与常量池

1. == 和 equals() 的区别

== 比较两个引用是否指向同一个对象,equals() 通常比较字符串内容是否相同。判断字符串内容时应该使用 equals()。

例如:

String a = "ab";
String b = new String("ab");

System.out.println(a == b);      // false
System.out.println(a.equals(b)); // true

字符串字面量通常会进入字符串常量池,所以相同字面量可能引用同一个对象。但这不是使用 == 比较字符串内容的理由。

2. final String 一定是编译期常量吗

不一定。一般需要同时满足:变量是 final,类型是基本类型或 String,并且初始化表达式可以在编译期求值。

final String a = "a";
final String b = "b";
String c = a + b;

这里 a + b 可以在编译期确定为 "ab"。但是:

final String d = getB();

d 虽然不能重新赋值,但它的值来自方法调用,通常不是编译期常量。

分析字符串题时,建议按以下顺序:

  1. 判断变量是不是编译期常量;
  2. 判断是否使用了 new String;
  3. 区分 == 和 equals();
  4. 如果有方法调用,不能直接按常量池规则推断。

六、MySQL 索引与查询优化

1. 联合索引的最左前缀原则

对于联合索引 (a, b, c),数据库通常可以利用 a、a,b 或 a,b,c 这样的前缀进行查找。只使用 b 或 c,通常不能充分利用这个联合索引进行定位。

例如,索引为:

INDEX idx_student_course (student_id, course_code)

按 student_id 查询,或同时按 student_id 和 course_code 查询,通常更容易使用该索引。只按 course_code 查询,则不符合最左前缀。

最终是否使用索引,仍要结合数据分布、统计信息和执行计划,通过 EXPLAIN 验证。

2. 为什么对索引列使用函数可能降低效率

例如:

WHERE MOD(YEAR(enrollment_date), 2) = 0

数据库可能无法直接根据 enrollment_date 的原始索引值定位,而需要逐行计算函数结果,导致扫描范围扩大或退化为扫描。

优化时可以考虑:

  • 改写成原始列上的范围条件;
  • 增加生成列并对生成列建立索引;
  • 根据数据库版本使用函数索引;
  • 使用 EXPLAIN 检查实际执行计划。

3. LIKE、LEFT 和 OR 对索引的影响

LEFT(course_code, 3) 这类函数可能影响普通索引的直接查找。相比之下,LIKE 'CS%' 是左侧固定前缀,在条件合适时更容易利用索引;LIKE '%CS' 以通配符开头,通常难以使用普通 B-Tree 索引定位。

OR 不一定必然导致索引失效。数据库可能使用 Index Merge,也可能因为选择性不高而选择全表扫描。应该结合 EXPLAIN 和实际数据量判断,不能只看 SQL 表面形式。

4. WHERE、GROUP BY 和 HAVING

  • WHERE 在分组前过滤行;
  • GROUP BY 将数据聚合成组;
  • HAVING 在分组后过滤聚合结果。

例如 HAVING COUNT(*) > 10 必须等分组完成后才能判断。能写到 WHERE 中的普通过滤条件,通常应该尽量提前过滤,以减少后续参与分组的数据量。

七、Optional 与 Stream

视频中的典型代码逻辑是:取得用户邮箱,过滤空邮箱,取出实际字符串,再取第一个有效值。

Optional<String> find = users.stream()
    .map(User::getEmail)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .findFirst();

System.out.println(find.orElse("No email found"));

第一个用户的邮箱是 tom@qq.com,所以输出为:

tom@qq.com

Optional.of() 和 ofNullable()

Optional.of(value) 要求 value 非空,传入 null 会抛出 NullPointerException。Optional.ofNullable(value) 允许传入 null,空值时返回 Optional.empty()。

如果已经明确保证非空,可以使用 of;如果数据来自数据库、接口或用户输入,可能为空,通常使用 ofNullable。

findFirst() 和 findAny()

findFirst() 返回流中的第一个元素,在有序流中有明确的顺序语义;findAny() 只保证返回任意一个元素,更适合不关心顺序、可能并行执行的场景。两者返回类型都是 Optional。

八、编程题:删除字符形成交替字符串

题目要求:只包含 A、B 的字符串,通过删除最少字符,使剩余字符串变成交替格式。合法格式只有两种:

  • ABAB...;
  • BABA...。

对两种起始模式分别扫描字符串。对于固定模式,如果当前字符等于期望字符,就保留它并切换期望字符;如果不相等,就删除当前字符,但不能切换期望字符,因为删除操作不会改变目标结果中的位置。

伪代码如下:

deletions(s, first):
    expected = first
    deleted = 0
    for c in s:
        if c == expected:
            expected = the other character
        else:
            deleted++
    return deleted

answer = min(deletions(s, 'A'), deletions(s, 'B'))

时间复杂度是 O(n),额外空间复杂度是 O(1)。例如 BAAABA,分别计算两种起始模式后,最小删除数为 2。

这道题不能只统计 A 和 B 的数量,因为交替字符串还要求顺序。AABB 和 ABAB 都有两个 A、两个 B,但只有后者满足交替格式。

九、test.java 源码复盘

1. 题目要求

test.java 判断两个句子是否为“变形句”:忽略大小写后,两个句子中的单词及其出现次数完全相同,单词顺序可以不同。

核心方法是:

public static boolean areAnagramSentences(String A, String B)

实现步骤:

  1. 对句子调用 toLowerCase();
  2. 使用 split(" ") 切分单词;
  3. 使用 HashMap<String, Integer> 统计单词频率;
  4. 对另一个句子重复相同操作;
  5. 使用 Map.equals() 比较两张频率表。

源码中的测试结果是:

true
false

第一组虽然包含 love2,与注释中“只包含字母和空格”的描述不完全一致,但当前算法会把 love2 当作普通单词处理,因此仍然可以正确返回 true。

2. 复杂度

假设两个句子总共包含 n 个单词,不同单词数为 k:

  • 平均时间复杂度:O(n);
  • 空间复杂度:O(k)。

这里的时间复杂度基于 HashMap 正常哈希分布的平均情况。

3. 代码可以如何改进

如果严格遵守题目约束,当前实现已经能够完成任务。若要支持更宽松的输入,可以考虑:

String[] words = sentence.toLowerCase(Locale.ROOT)
    .trim()
    .split("\\s+");

同时应该明确处理 null。当前代码对 null 调用 toLowerCase() 会抛出 NullPointerException。

还可以使用一张差分表:第一个句子的单词计数加一,第二个句子的单词计数减一,最后检查所有计数是否为零。两种写法的复杂度相同。笔试中使用两张 HashMap 更直观,更容易解释和排查。

4. 源码中的注释问题

源码注释中有两处相同的句子示例,却分别标注了 true 和 false,这与正式规则矛盾。判断标准应以“单词及其出现次数是否完全相同”为准:

  • 只是顺序不同:true;
  • 单词不同或次数不同:false。

十、面试时可以怎样回答

问:你怎么看继承和组合?

可以这样回答:

继承表达 is-a,组合表达 has-a。组合通常具有更好的运行时灵活性和更低的耦合,因此我会优先考虑组合。但如果子类确实满足父类的行为约束,存在稳定的类型层次,并且需要多态,就会使用继承。

问:PreparedStatement 一定比 Statement 快吗?

可以这样回答:

不能绝对这样说。PreparedStatement 的主要优势是参数绑定、防止 SQL 注入、提高可维护性,并且对重复执行的同结构 SQL 通常更合适。只执行一次的简单 SQL 可能少一步预编译,但实际性能还要看驱动、数据库和执行计划。

问:为什么这个 SQL 没有使用索引?

可以这样回答:

我会先看索引列是否符合最左前缀,是否对索引列做了函数运算,是否存在前导通配符或复杂 OR 条件,再用 EXPLAIN 查看执行计划。同时还要考虑数据量和选择性,不能仅凭 SQL 文字判断索引一定会使用。

问:如果让你改进 test.java,你会怎么做?

可以这样回答:

如果题目约束不变,当前双 HashMap 实现已经满足要求,我会先修正注释并补充测试。如果需要支持更通用的输入,我会处理 null,使用 Locale.ROOT 进行大小写转换,使用 trim 和 \\s+ 支持连续空白,并增加空字符串、重复单词、大小写混合和不同长度句子的测试。

问:你不会某个知识点时怎么办?

不要直接猜。可以先说确定的部分,再说明不确定点和验证方法:

我确定 GET 通常用于获取资源,POST 通常用于提交数据;但具体长度不是 HTTP 统一规定的标准,需要结合浏览器、服务器和网关配置确认。如果是在项目中遇到,我会通过接口配置、抓包和实际测试验证。

这种回答比给出一个绝对但可能错误的结论更可靠。

十一、个人复盘重点

这次题目暴露出的重点不是某一道题不会,而是一些知识点容易停留在“背结论”层面。后续复习应重点改进以下能力:

  1. 遇到“以下说法正确/不正确”时,先识别绝对化表述,再结合条件判断;
  2. Java 字符串题先分析编译期常量、对象创建方式和比较方式,不凭直觉判断;
  3. SQL 索引题不能只背最左前缀,要结合函数、选择性和 EXPLAIN;
  4. 代码题除了写出结果,还要能说明复杂度、边界条件和为什么算法正确;
  5. 面试回答采用“先结论、再原因、最后边界”的结构;
  6. 对不确定的内容明确说出假设和验证方法,不把猜测当成事实。

这份复盘的目标不是记住某套题的答案,而是把题目背后的知识点转化为能够在面试中讲清楚、在代码中写出来、在实际项目中验证的能力。

posted @ 2026-10-14 17:34  YukiRinLL  阅读(8)  评论(0)    收藏  举报