按知识类别整理笔试题,保留原题编号,补充参考答案与解析。目前收录 SQL 和 Java 类题目。
一、SQL 类
1.1 第 8 题:联合索引的列顺序与查询效率
题目
表 T 中,C1 列的不同值数量为 100 万,C2 列的不同值数量为 100。执行下面的 SQL 时,关于索引的说法正确的是:
SELECT COUNT(*)
FROM T
WHERE C1 = :1 AND C2 = :2;原图中的“差异值”按“不同值数量(基数)”理解;
:1、:2表示绑定参数。截图未指定数据库产品与索引类型,以下按常见 B-tree 联合索引讨论。
| 选项 | 内容 |
|---|---|
| A | 索引 (C1, C2) 和索引 (C2, C1) 效率一样 |
| B | 索引 (C1, C2) 比索引 (C2, C1) 效率高 |
| C | 索引 (C2, C1) 比索引 (C1, C2) 效率高 |
考点
联合索引:两个字段都用 = 查询时,索引列的先后顺序是否影响查找效率。
答案
这道题选 A:两种索引查起来基本一样快。
先抓住一个关键点:题目里 C1 和 C2 的值都给了,而且都是用 = 查找。
解析
把题目换成“查学生”,就好理解了。
假设有一张学生表:
- C1 是“学号”,有 100 万个不同的学号。
- C2 是“校区”,只有 100 个不同的校区。
- 索引就是数据库提前整理好的“查找目录”,用来快速找到记录。
- 联合索引就是把两列放在同一份目录里,并按先后顺序排好。
现在要查:学号是 12345,并且校区是北京的学生有几个?
原题里的 :1、:2 就是要传进去的具体值,例如学号 12345 和校区北京;COUNT(*) 就是数一数符合条件的记录有几条。
下面两份目录,存的是同一批学生,只是排列顺序不同:
| 目录一:先按学号,再按校区 | 目录二:先按校区,再按学号 |
|---|---|
| 12344,北京 | 北京,12344 |
| 12345,北京 ← 要找的记录 | 北京,12345 ← 要找的记录 |
| 12346,北京 | 北京,12346 |
| 12347,上海 | 上海,12347 |
- 用
(学号, 校区)索引,查找目标是 “12345,北京”。 - 用
(校区, 学号)索引,查找目标是 “北京,12345”。
两条信息都知道,两份目录都能用这两条信息定位到目标所在的位置。 如果有多条记录符合条件,再把这些记录数出来。
这里的表格只是帮助理解排列方式。真实数据库会沿着索引快速查找,不会像人看名单一样从第一行读到最后一行。
为什么不能说“学号不同值多,学号放前面一定快”?
你可能会这样想:
先查学号,一下就剩很少的人;先查校区,还剩很多人。所以学号放前面更快吧?
这个想法把联合索引想成了“两次分开的筛选”。
实际上,(校区, 学号) 这份目录已经把同一个校区里的学号排好了。数据库知道“北京”和“12345”,就能在目录里查找这个组合,不用把北京校区所有学生都读出来,再挨个检查学号。
所以,本题里“100 万”和“100”很容易让人误选 B。它们表示不同值的数量,但仅凭这两个数字,不能说学号放前面一定更快。
那什么时候前后顺序会影响查询?
把条件减少一个,区别就明显了。
只知道校区是北京,不知道学号:
(校区, 学号):北京的学生排在一起,方便集中查找。(学号, 校区):北京的学生可能散在不同学号下面,不能像前一种目录那样直接找出一整段。
只知道学号,不知道校区时,道理反过来。
这就是常说的“最左前缀”:可以先理解成,只给一个查询条件时,这个条件最好是索引里排在最前面的那一列。
笔试时可以这样回答:
我选 A。因为 C1 和 C2 都是等值查询,不管索引是
(C1, C2)还是(C2, C1),都能用这两个值一起定位,所以效率基本相当。不能只看 C1 的不同值多,就认为它放前面一定更快。如果只查其中一列,索引顺序就需要重点考虑了。
记住这一句:两个值都给全了,两种顺序都能找;只给其中一个值,就要看它排在哪里。
这里按常见的 B-tree 联合索引分析。“基本相当”不表示实际耗时精确相同;有大于、小于等条件时,也需要重新分析。
参考资料:MySQL 官方文档:多列索引。文档说明联合索引可以使用全部索引列,或者从第一列开始的连续几列;以上例子用于帮助理解,本题选 A 是结合这些原理作出的判断。
1.2 第 6 题:哪种表连接方式只能用于等值连接
题目
以下哪种表连接方式只能用于等价连接?
| 选项 | 连接方式 |
|---|---|
| A | NESTED LOOPS |
| B | SORT MERGE JOIN |
| C | HASH JOIN |
考点
三种表连接算法的区别,以及 HASH JOIN 为什么需要等值匹配。
这里的“等价连接”通常叫“等值连接”,就是用 = 把两张表的数据对应起来。题目的术语常见于 Oracle,这里按传统连接算法及 Oracle 的语境理解。
答案
C:HASH JOIN(哈希连接)。
解析
先弄懂什么是等值连接。
假设有员工表和部门表,要查“每个员工属于哪个部门”:
SELECT e.name, d.dept_name
FROM employee e
JOIN department d
ON e.dept_id = d.id; -- 员工的部门编号,等于部门表里的编号例如员工小王的部门编号是 10,就找编号为 10 的部门。这里连接两张表的条件是 =,所以叫等值连接。
如果连接条件换成 e.salary > g.salary,要找的是工资更低的另一张表中的记录,这就不是等值连接。
HASH JOIN:先做一份按编号查找的字典。
可以把它理解成先把部门表整理成类似 Java HashMap 的结构:
10 → 技术部
20 → 财务部
30 → 人事部再读取员工表:小王的部门编号是 10,就用 10 去字典里找技术部。
哈希的作用,是根据编号算出该去哪个位置查找;找到候选记录后,还要核对编号是否真的相等,因为不同编号也可能算到同一个位置。
为什么要求等值条件?
因为这种字典擅长回答“找编号等于 10 的记录”。如果问“找所有编号大于 10 的记录”,单凭 10 算出的位置,就无法知道其他更大的编号在哪里。哈希存放的位置不按编号大小排序。
所以,传统 HASH JOIN 需要等值条件来配对。这就是本题选 C 的原因。
另外两种为什么不选?
| 方式 | 大白话理解 | 为什么不选 |
|---|---|---|
| NESTED LOOPS:嵌套循环连接 | 拿第一张表的一条记录,去第二张表找符合条件的记录,再处理下一条 | 查找时既可以判断相等,也可以判断大于、小于 |
| SORT MERGE JOIN:排序合并连接 | 先把两边的数据按连接字段排好,再利用顺序比较和配对 | 在 Oracle 中,除了相等,也可处理部分大于、小于等条件 |
| HASH JOIN:哈希连接 | 先做按值查找的字典,再用另一张表的值去匹配 | 传统算法需要等值条件,因此选它 |
这三种是数据库执行连接时的算法,和 SQL 里的 INNER JOIN、LEFT JOIN 不是同一种分类。前者讲“怎么查”,后者讲“哪些记录要保留”。
容易误解的一点: HASH JOIN 并不要求整条 SQL 只能出现 =。例如 ON e.dept_id = d.id AND e.salary > d.min_salary,可以先按部门编号做哈希匹配,再判断工资条件。关键是有可用于哈希匹配的等值条件。
记住:哈希像字典,按相同的值找对象;它不靠大小顺序找对象。
参考资料:Oracle 官方文档:Joins。以上是本题常见的 Oracle 考法,不宜直接当成所有数据库版本的限制。
1.3 第 5 题:防止 SQL 注入的最佳方式
题目
下列哪种是最佳防止 SQL 注入的方式?
| 选项 | 内容 |
|---|---|
| A | 对外部输入进行转义 |
| B | 限制外部输入的长度 |
| C | 使用存储过程 |
| D | 使用预编译语句 |
考点
SQL 注入的原因,以及使用预编译语句和参数绑定防止注入的原理。
答案
D:使用预编译语句,并通过参数绑定传入外部输入。
解析
SQL 注入是什么?
本来用户输入的内容只是“要查询的值”,但程序直接把它拼进 SQL,结果其中的特殊内容变成了查询指令,改变了原来的查询意思。
例如,系统允许按用户名查询,程序用下面这种方式拼接 SQL。这里的 username 来自用户输入:
// 错误示例:把用户输入直接拼成 SQL 的一部分。
String sql = "SELECT id FROM users WHERE username = '" + username + "'";正常输入 小王,拼出来是:
SELECT id FROM users WHERE username = '小王';如果输入的是 ' OR '1'='1,拼出来就变成:
SELECT id FROM users WHERE username = '' OR '1'='1';'1'='1' 永远成立,查询就可能把原本不该查出来的其他用户也返回。这就是输入改变了 SQL 的意思。
预编译语句怎么解决?
先把 SQL 的结构写好,需要传值的位置用 ? 留出来,再单独传入参数。
下面是 Java JDBC 的查询片段,假设 connection 是已经取得的数据库连接,username 是外部传入的用户名:
// SQL 结构固定,问号只用于接收用户名这个值,不加引号。
String sql = "SELECT id FROM users WHERE username = ?";
try (java.sql.PreparedStatement ps = connection.prepareStatement(sql)) {
// setString:把 username 作为字符串参数,绑定到第一个问号。
ps.setString(1, username);
// executeQuery:按固定的 SQL 结构查询,参数只作为数据。
try (java.sql.ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getLong("id"));
}
}
}这样,即使传入 ' OR '1'='1,也只是查找“用户名恰好叫这整串文字”的用户,里面的 OR 不会变成查询指令。
可以想成填表:表格格式由程序定好,用户只能填写姓名,不能把填进去的内容变成新的表格规则。
防注入的关键是“SQL 结构和参数分开”。仅仅用了 PreparedStatement,却仍把用户输入拼进 SQL 字符串,也不能解决问题。
其他选项为什么不选?
| 选项 | 原因 |
|---|---|
| 对输入转义 | 容易受数据库语法、字符集等影响而遗漏,不如参数绑定可靠 |
| 限制长度 | 短输入也可能包含改变 SQL 含义的内容,长度限制不能解决根本问题 |
| 使用存储过程 | 正确使用可以防注入,但如果内部仍拼接不可信输入执行动态 SQL,也可能有漏洞 |
| 使用预编译语句 | 配合参数绑定,让输入作为数据处理,是本题最佳选项 |
参数占位符用于传入值,不能随意替代表名、列名或排序方向。确实需要动态选择这些内容时,应由程序从预先允许的选项中选择,不能直接拼接任意用户输入。
记住:用户输入只当数据,不让它变成 SQL 指令;使用占位符,再绑定参数。
参考资料:OWASP:SQL 注入防护指南。
1.4 第 11 题:只存日期时,哪种类型占用空间最小
题目
如果只需要存储日期(比如交易日),以下哪个类型占用存储空间最小?
| 选项 | 类型 |
|---|---|
| A | NUMBER |
| B | TIMESTAMP |
| C | DATE |
| D | VARCHAR2 |
考点
Oracle 数据类型的存储大小,以及“占空间最少”和“最适合表示日期”的区别。
答案
按 Oracle 中用 YYYYMMDD 数字表示交易日的常见考法,选 A:NUMBER。
截图没有给出具体存储格式,题意存在一些省略。这里假设同一天分别存成数字 20261020、日期值或日期字符串来比较,不能理解成 NUMBER 在任何情况下都最小。
解析
先确认:这是 Oracle 的题,不能套用 MySQL 的 DATE 大小。
VARCHAR2 是 Oracle 中常见的字符串类型。Oracle 的 DATE 不只保存年月日,还保存时分秒,固定占 7 字节;即使你只需要日期,也不会因此缩小。
拿交易日“2026 年 10 月 20 日”举例:
| 类型 | 怎么表示这一天 | 值本身大致占用空间 |
|---|---|---|
| NUMBER | 数字 20261020 | 5 字节 |
| TIMESTAMP | 日期时间值,还能表示小数秒 | 官方文档列为 7 或 11 字节,取决于精度 |
| DATE | 日期时间值,只使用其中的年月日 | 固定 7 字节 |
| VARCHAR2 | 字符串 '20261020' | 在常用字符集中,8 个数字字符占 8 字节;带横线的 '2026-10-20' 则占 10 字节 |
表中比较值本身的存储,不包含完整行、索引等额外开销。
为什么 8 位数字用 NUMBER 只要 5 字节?
因为“数字”和“数字组成的文字”存法不同:
- VARCHAR2 像逐字记下
2、0、2、6、1、0、2、0,在常用字符集中要用 8 字节。 - Oracle NUMBER 会把数字压缩编码。对于
20261020,可理解成按两位一组:20、26、10、20,这 4 组加上 1 字节指数信息,共 5 字节。
这个拆分只是帮助理解 Oracle 的内部编码。NUMBER 是变长存储,不是声明成 8 位数字就一定占 8 字节;某些值还会因为末尾的零等因素占得更少。
那项目里是不是都应该用 NUMBER 存日期?
不是。题目问的是“谁更省空间”,实际设计还要看是否方便、是否容易出错。
- NUMBER 可以存进
20261340这种不存在的日期,需要程序额外校验。 - DATE 有日期语义,可以直接做日期运算。比如 DATE 值加 1 是后一天;数字
20261031加 1 却得到20261032,不会自动变成 11 月 1 日。
所以,这道题按存储大小选 NUMBER;实际业务如果需要日期校验和日期计算,通常优先考虑日期类型。
记住:Oracle 的 DATE 固定 7 字节;8 位交易日数字用 NUMBER 通常更省,但数字不会自动遵守日历规则。
参考资料:Oracle 官方文档:数据类型和大小、Oracle 官方文档:NUMBER 的内部编码。
1.5 第 20 题:对结果集分组和汇总的子句
题目
下面哪个子句用于对结果集进行分组和汇总?
| 选项 | 内容 |
|---|---|
| A | ORDER BY |
| B | HAVING |
| C | WHERE |
| D | GROUP BY |
考点
区分分组、汇总、筛选和排序,尤其是 GROUP BY 与 HAVING 的区别。
答案
D:GROUP BY。
GROUP BY 负责把数据分组,再配合 COUNT()、SUM()、AVG() 等函数统计每组的数据。截图中选中的 HAVING 不是本题正确答案。
解析
用“统计各部门人数”来理解。
假设员工表 employee 里有这些记录:
| name(姓名) | dept(部门) |
|---|---|
| 小王 | 技术部 |
| 小李 | 技术部 |
| 小张 | 财务部 |
现在想知道每个部门有多少人:
SELECT dept, COUNT(*) AS employee_count
FROM employee
GROUP BY dept; -- 相同部门的员工分成一组,COUNT(*) 数每组有几人。结果是下面两组,显示顺序不作保证:
| dept | employee_count |
|---|---|
| 技术部 | 2 |
| 财务部 | 1 |
可以理解为:先让同部门的人站在一起,再分别数人数。 GROUP BY dept 负责站队分组,COUNT(*) 负责数人数。不是 GROUP BY 自己负责求和、计数。
HAVING 是干什么的?为什么不能选它?
如果只想看“人数大于 1 的部门”,要先统计每个部门人数,再筛选统计结果:
SELECT dept, COUNT(*) AS employee_count
FROM employee
GROUP BY dept
HAVING COUNT(*) > 1; -- 分组统计后,只保留人数大于 1 的组。这时只剩技术部,人数为 2。GROUP BY 负责分组,HAVING 负责筛选分组后的结果。
四个选项放在一起记。
| 子句 | 大白话作用 | 示例 |
|---|---|---|
| WHERE | 分组前,先筛掉不符合条件的原始记录 | 只统计在职员工 |
| GROUP BY | 按某个标准分组 | 同一个部门的员工归为一组 |
| HAVING | 分组后,筛选符合条件的组 | 只保留人数大于 1 的部门 |
| ORDER BY | 给查询结果排顺序 | 部门人数从多到少排列 |
假设 employee 表还有 status 字段,用 '在职' 表示在职,可以把它们连起来看:
SELECT dept, COUNT(*) AS employee_count
FROM employee
WHERE status = '在职' -- 先挑出在职员工。
GROUP BY dept -- 再按部门分组,并统计人数。
HAVING COUNT(*) > 1 -- 只保留在职人数大于 1 的部门。
ORDER BY employee_count DESC; -- 最后按人数从多到少显示。这里描述的是帮助理解的逻辑处理过程,数据库实际执行时可能优化执行方式。
记住:WHERE 先挑人,GROUP BY 分队,COUNT 数人数,HAVING 挑队,ORDER BY 排顺序。
参考资料:Oracle 官方文档:SELECT 中的 GROUP BY、HAVING 和 ORDER BY。这几个子句的基本用途在 MySQL 中也适用。
1.6 第 7 题:修改序列时,哪个语句会报错
题目
下面哪个语句会报错?
| 选项 | 语句 |
|---|---|
| A | ALTER SEQUENCE seq1 MAXVALUE 90; |
| B | ALTER SEQUENCE seq1 NOMINVALUE; |
| C | ALTER SEQUENCE seq1 START WITH 1; |
| D | ALTER SEQUENCE seq1 NOCACHE; |
考点
Oracle 序列的用途、修改序列的语法,以及起始值与最大值、缓存等设置的区别。
答案
C:ALTER SEQUENCE seq1 START WITH 1;。
题目通常假设 seq1 已存在、有修改权限,而且其他设置不冲突,考的是语法。不能直接用这个写法修改已有序列的起始值。
解析
序列是什么?
可以把 Oracle 的序列想成一台“自动取号机”,名字叫 seq1。假设设定从 1 开始,每次加 1,那么取号时会依次得到 1、2、3……,经常用于生成记录编号。
ALTER SEQUENCE 就是修改这台取号机的设置。
逐个选项看。
| 选项 | 大白话意思 | 判断 |
|---|---|---|
| A:MAXVALUE 90 | 把允许生成的最大号码设为 90 | 语法正确,但要满足当前号码、最小值、缓存等约束 |
| B:NOMINVALUE | 不再使用手动指定的最小值,改用数据库默认最小值 | 语法正确;普通递增序列的默认最小值为 1 |
| C:START WITH 1 | 想直接把已有序列的起始号码改成 1 | 这个 ALTER 写法不正确,因此选 C |
| D:NOCACHE | 不预先缓存一批号码 | 语法正确 |
为什么 C 不行?
START WITH 表示“从哪个号码开始”,常见于创建序列时。已经存在的取号机,不能只靠 ALTER SEQUENCE ... START WITH 1 这句话重新设置起点。
Oracle 19c 支持通过 RESTART START WITH 表达“重新从指定号码开始”。下面仅作语法对比,未连接数据库执行:
-- 题目中的错误写法:缺少 RESTART。
ALTER SEQUENCE seq1 START WITH 1;
-- Oracle 19c 支持的重启写法,仍需满足序列的其他约束。
ALTER SEQUENCE seq1 RESTART START WITH 1;因此不能笼统记成“所有 Oracle 版本都不能重置序列”,应记住:本题 C 的写法不对;支持 RESTART 的版本,要用相应的重启语法。
NOCACHE 又是什么?
可以把缓存想成提前准备一批待发的号码,以减少取号时的开销。NOCACHE 表示不采用这种预先缓存方式,不是“不生成号码”,也不能保证号码永远连续无缺口。例如业务事务回滚,也可能造成号码跳过。
A 会不会也报错?
有可能。如果序列当前号码已经超过 90,就不能再把最大值改为 90;其他参数冲突也可能失败。但截图没有给出这些状态,因此这道单选题按“哪句语法不正确”理解,选 C。
记住:MAXVALUE 改上限,NOMINVALUE 恢复默认下限,NOCACHE 关缓存;直接 ALTER 加 START WITH 不行。
参考资料:Oracle 19c 官方文档:ALTER SEQUENCE。
1.7 第 15 题:直接赋值与 SELECT INTO 的效率
题目
下面的赋值语句哪个效率更高?
| 选项 | 语句 |
|---|---|
| A | v := 1; |
| B | SELECT 1 INTO v FROM dual; |
考点
Oracle PL/SQL 的直接赋值,以及避免为了简单赋值执行不必要的 SQL 查询。
答案
A:v := 1;,直接赋值效率更高。
假设 v 是已经声明、能够接收数字的变量。两种写法都可以把 1 放进 v,但直接赋值不需要执行一次 SQL 查询。
解析
先看这些符号是什么意思。
v:变量,可以理解为一个存放数据的小盒子。:=:PL/SQL 的赋值符号,意思是“把右边的值放进左边变量”,作用类似 Java 的赋值=。INTO v:把查询得到的值放进变量 v。dual:Oracle 提供的一张特殊的单行表,常用于查询常量或表达式,不是本题的业务数据表。
所以两句话分别是:
v := 1; -- 直接把数字 1 放进变量 v。
SELECT 1 INTO v FROM dual; -- 先执行查询取得数字 1,再把查询结果放进 v。为什么直接赋值更快?
就像你已经知道答案是 1:直接写到纸上就行,没有必要再找人问一次“1 是多少”,等对方回答后才写下来。
PL/SQL 负责执行变量赋值、循环等程序逻辑;SQL 引擎负责执行 SELECT 等数据库语句。第二种写法通常需要把查询交给 SQL 引擎处理,再把结果交回来,这个交接叫“上下文切换”,会产生额外开销。
这里的切换发生在数据库内部,不等于一定要通过网络再请求数据库一次。即使数据库优化了对 dual 的访问,也不需要为了一个已知常量引入查询。
只执行一次,差别可能不明显;放在大量重复的循环中,不必要的查询开销就可能累积。具体差多少要看环境,不能简单说固定快几倍。
什么时候才需要 SELECT INTO?
如果值已经知道,或者可以直接计算,就直接赋值。如果需要从业务表中查出数据,再用 SELECT INTO。
例如,已知 employee 表里 id 为 1001 的员工恰好有一条记录,需要读取他的工资:
-- 此处假设 v 已声明为合适的数字变量。
SELECT salary INTO v
FROM employee
WHERE id = 1001;这次工资是多少需要查表才能知道,使用 SELECT INTO 就有实际意义。普通 SELECT INTO 要求结果恰好一行;没有记录或返回多条记录时,需要处理对应异常。
完整的直接赋值例子。
下面是 PL/SQL 匿名块,只演示变量赋值和输出,不修改业务表,也未在数据库中执行:
DECLARE
v NUMBER; -- 声明一个存放数字的变量。
BEGIN
v := 1; -- 直接赋值,不用 SELECT。
DBMS_OUTPUT.PUT_LINE(v); -- 输出变量值;在客户端启用 DBMS Output 后可看到 1。
END;记住:已知的值直接赋,表里的值才去查。
参考资料:Oracle 官方:PL/SQL 与 SQL 引擎之间的切换开销。
1.8 第 26 题:哪些语句会隐式提交事务
题目
下面哪些语句会隐式提交事务?(多选)
| 选项 | 语句 |
|---|---|
| A | CREATE |
| B | TRUNCATE |
| C | UPDATE |
| D | SELECT |
考点
Oracle 中 DDL 的隐式提交,以及它与普通数据修改、客户端自动提交的区别。
答案
按 Oracle 普通表的常见考法,选 A、B:CREATE、TRUNCATE。
截图中 UPDATE 也被选中了,这一项不应选。题目讨论语句自身的提交行为,默认不把客户端的自动提交设置算进去。
解析
先理解“事务”和“提交”。
可以把事务想成一批准备一起确认的修改。修改后还没提交,通常可以用 ROLLBACK 撤销这批修改;执行 COMMIT,就是正式确认,之后不能再靠普通回滚撤销。
- 显式提交:你自己写
COMMIT。 - 隐式提交:你没写
COMMIT,数据库因为执行某类语句,自动进行了提交。
为什么 CREATE、TRUNCATE 会提交?
它们属于 DDL,也就是管理数据库对象的语句:CREATE 用来创建表等对象,TRUNCATE 用来清空表的数据。
在 Oracle 中,普通 DDL 执行前会提交当前事务,执行成功后还会提交 DDL 的结果。因此,它不只影响当前这一句,还可能把之前没提交的修改一起确认。
下面只是过程说明,没有在数据库中执行任何操作:
1. 用 UPDATE 修改小王的工资,还没提交。
2. 在同一个会话中,用 CREATE 创建一张普通表。
3. Oracle 在执行这条 DDL 前,先提交之前的事务。
4. 这时再执行 ROLLBACK,也不能撤销第 1 步的工资修改。即使 DDL 后来执行失败,执行前的提交也可能已经发生。因此,不能认为“建表失败,之前的修改就一定还可以回滚”。
为什么 UPDATE 不选?
UPDATE 是修改表中记录的 DML 语句。普通情况下,它修改数据后不会自己提交,可以继续执行其他修改,再一起提交或回滚。
为什么 SELECT 不选?
普通 SELECT 只是查询,不会因此提交已有事务。即使是 SELECT ... FOR UPDATE,也不能把它理解成提交命令。
| 语句 | 大白话作用 | 是否因语句自身触发本题所说的隐式提交 |
|---|---|---|
| CREATE | 创建表等数据库对象 | 是 |
| TRUNCATE | 清空普通表的数据 | 是 |
| UPDATE | 修改记录 | 否 |
| SELECT | 查询记录 | 否 |
TRUNCATE 和 DELETE 都能删数据,有什么不同?
在 Oracle 普通表的场景中:
- DELETE 是 DML,可以带 WHERE 只删除符合条件的记录;未提交时通常可以回滚。
- TRUNCATE 是 DDL,清空整张表,不能带 WHERE;会发生隐式提交,不能靠普通 ROLLBACK 恢复清空的数据。
那为什么有时 UPDATE 后,没写 COMMIT 数据也保存了?
可能是工具或 JDBC 连接开启了自动提交。那是客户端在执行后帮你提交,和 CREATE、TRUNCATE 自身触发的隐式提交是两个概念。例如 JDBC 连接通常默认开启自动提交,不能因此在本题中选择 UPDATE。
这里按前面题目的 Oracle 语境及普通表说明;特殊临时表、其他数据库的 DDL 事务规则可能不同,不能直接套用。
记住:Oracle 普通 DDL 会提交;UPDATE 改数据,COMMIT 才确认;自动提交设置要另算。
参考资料:Oracle 官方:COMMIT 的隐式提交规则。
1.9 第 13 题:INSERT 未提交时,谁能看到新记录
题目
某会话对 T 表执行了一个 INSERT 操作,但还没有提交,下面说法正确的有:
| 选项 | 内容 |
|---|---|
| A | 该会话可以查询 T 表,但只有提交后才能查到这些新插入的记录 |
| B | 其他会话对 T 表的 SELECT 操作将会被阻塞,直到该会话提交或回滚 |
| C | 其他会话对 T 表的 INSERT 操作将会被阻塞,直到该会话提交或回滚 |
| D | 即使未提交,该会话也可以查询到这些新插入的记录 |
考点
事务对自身修改的可见性、Oracle 的一致性读,以及“看不到”和“被阻塞”的区别。
答案
D。自己可以查到自己尚未提交的新记录。
按前面题目的 Oracle 语境,讨论普通 INSERT、普通 SELECT,不包括直接路径插入等特殊场景。截图选中的 A 不正确。
解析
会话是什么?
可以先把它理解为一条独立的数据库连接。假设你开了两个查询窗口,分别使用两条独立连接,就有会话 A 和会话 B。仅仅打开两个标签页不一定是两个会话,要看工具有没有共用连接。
用“添加小王”来理解。
假设 T 表原来只有小李、小张。会话 A 插入小王,但先不提交:
| 谁来查 | 能不能看到小王 | 原因 |
|---|---|---|
| 会话 A 自己查 | 能 | 自己可以看到自己在当前事务中做的修改 |
| 会话 B 普通查询 | 看不到 | Oracle 不允许读取其他事务尚未提交的数据 |
会话 B 不需要一直等,它仍能查到原来已经提交的小李、小张,只是结果里没有小王。
所以,“没提交”不等于“谁都看不见”。正确理解是:自己能看,别人暂时看不到。
下面仅说明过程,没有连接数据库执行任何操作:
1. 会话 A 插入小王,不提交。
2. 会话 A 查询:能看到小王。
3. 会话 B 普通查询:正常返回,但没有小王。
4. 会话 A 提交。
5. 在 Oracle 默认 READ COMMITTED 隔离级别下,
会话 B 在 A 提交后新发起的查询可以看到小王。第 5 步指的是“会话 A 提交之后,会话 B 再发起查询”,不是要求会话 B 必须先提交。已经运行中的查询不会因为别人刚提交,就在中途自动刷新结果。其他隔离级别下,可见时间也可能不同。
如果会话 A 最后选择回滚,小王这条新记录就被撤销,其他会话也不会因为它曾经插入过就能查到它。
逐项看为什么。
| 选项 | 判断 | 大白话原因 |
|---|---|---|
| A | 错 | 自己不需要等提交,就能查到自己新插入的记录 |
| B | 错 | 普通 SELECT 通常不会被这个未提交的 INSERT 挡住,只是看不到新记录 |
| C | 错 | 不是整张表都不准插入;其他会话通常还能插入不冲突的记录 |
| D | 对 | 当前事务能看到自身修改 |
那其他会话插入时,真的不会等待吗?
有可能等待,但不是“只要往同一张表插入就必须等”。例如 T 表的 id 有唯一约束:
- 会话 A 插入 id 为 1001 的记录,未提交。
- 会话 B 也想插入 id 为 1001 的记录。
- 会话 B 可能需要等 A 提交或回滚,才能确定这个编号还能不能使用。
- A 提交后,B 通常因重复编号报错;A 回滚后,B 才可能成功。
如果 B 插入的是不冲突的另一条记录,通常不必因为 A 的普通插入而等待。因此 C 的说法太绝对。
普通 SELECT 与 SELECT ... FOR UPDATE 也要区分:后者要加锁,遇到冲突时可能等待,不能直接套用普通查询的结论。
记住:自己未提交的数据自己能看;别人普通查询不等你,但看不到这条新数据;写入冲突才可能等待。
参考资料:Oracle 官方:数据并发与一致性。
二、Java 类
2.1 第 2 题:ArrayList 初始化与扩容
题目
下面这条语句中的 list 集合扩容了几次?
ArrayList list = new ArrayList(20);考点
区分 ArrayList 的“初始容量”“实际元素数量”和“扩容”。
答案
0 次。 这条语句只是创建集合,并把初始容量设为 20,没有发生扩容。此时实际元素数量 size() 为 0。
解析
把 ArrayList 想成一个装东西的箱子:
new ArrayList(20):一开始就准备好能装 20 件东西的箱子。- 此时还没放东西,所以实际元素数量是 0。
- 后来东西放不下了,需要换更大的箱子,这才叫“扩容”。
一开始就准备 20 个位置,不等于扩容了 20 次,也不等于已经放了 20 个元素。
| 操作 | 实际元素数量 | 扩容次数(从创建起累计) |
|---|---|---|
new ArrayList(20) | 0 | 0 |
| 连续添加 20 个元素 | 20 | 0 |
| 再添加第 21 个元素 | 21 | 1 |
什么时候才开始扩容?
对于本题的 new ArrayList(20),添加第 21 个元素时,才第一次扩容。
可以把它想成一开始准备了 20 个座位:
- 添加第 1~20 个元素:还有位置,不用扩容;第 20 个放进去后刚好坐满。
- 添加第 21 个元素:已经没有空位,先扩容,再放入新元素。
- 常见 JDK 实现会将容量从 20 扩到 30,因此继续添加到第 30 个元素都不用再次扩容,添加第 31 个时才第二次扩容。
为什么是 20 扩到 30?
因为常见 JDK 实现中,ArrayList 在正常逐个添加元素时,会优先按“原容量再增加一半”来扩容,也就是通常说的 1.5 倍扩容。
新容量 = 原容量 + 原容量的一半
= 20 + 10
= 30因此,添加第 21 个元素时,虽然只差 1 个位置,ArrayList 通常会一次多准备一些,扩到 30。添加完成后,实际元素数量是 21,容量是 30,还空着 9 个位置。
接下来逐个添加,容量变化是:
| 触发时机 | 计算过程 | 新容量 |
|---|---|---|
| 添加第 21 个元素 | 20 + 10 | 30 |
| 添加第 31 个元素 | 30 + 15 | 45 |
| 添加第 46 个元素 | 45 + 22 | 67 |
45 的一半是 22.5,这里按整数计算舍去小数,所以增加 22,得到 67。
为什么不只从 20 扩到 21?
因为 ArrayList 底层用数组存东西,原数组的长度不能直接变大。扩容时,需要新建一个更大的数组,再把原来存的元素引用复制过去。
如果每次只增加 1 个位置,后面每添加一个元素都可能要重新建数组、复制原来的内容。一次多留些空位,就能减少这种搬家的次数;但也会暂时多占一些空间。
源码里的“一半”怎么看?
OpenJDK 21 的扩容代码用 oldCapacity >> 1 表示优先增加的容量。对这里的正整数来说,>> 1 可以理解为“除以 2,舍去小数”。所以 20 的一半是 10,新容量就是 30。
如果一次批量加入很多元素,1.5 倍还装不下,就要至少扩到能装下所需元素的容量。因此,1.5 倍是通常的增长方式,不是任何情况下都固定乘 1.5。它也是具体 JDK 的实现规则,不是 Java API 对所有实现的强制要求。
源码参考:OpenJDK 21:ArrayList 的 grow 方法。
记住:刚好装满不扩容,再放一个才扩容。
上表及上述过程假设只依次添加元素,没有其他调整容量的操作。
记住:初始化是准备箱子,扩容是箱子不够用后换大箱子。
参考资料:Java 官方文档:ArrayList。带容量参数的构造方法创建的是空集合,参数指定初始容量。
2.2 第 24 题:哪些是 Thread 类的方法
题目
下面哪些是 Thread 类的方法?(多选)
| 选项 | 方法 |
|---|---|
| A | getPriority() |
| B | run() |
| C | start() |
| D | exit() |
考点
Thread 类的常用公开方法,以及 run() 和 start() 的区别。
答案
A、B、C:getPriority()、run()、start()。
exit() 不是供业务代码调用的 Thread 公开方法;常见的 System.exit(0) 属于 System 类。
解析
先把这三个方法的作用分清楚。
| 方法 | 大白话作用 |
|---|---|
getPriority() | 查看这个线程的优先级数值 |
run() | 写线程要执行的任务内容 |
start() | 启动线程,让它去执行任务 |
线程可以理解为程序里的一名“干活的人”。run() 描述要干什么,start() 才是让另一名干活的人开始工作。
直接调用 run(),为什么不算启动线程?
对于下面这种通过普通构造方法创建的线程,直接调用 run(),只是当前线程执行了一次普通方法;调用 start(),才会启动新线程,由新线程执行任务。
把下面代码保存为 ThreadDemo.java,可用 javac ThreadDemo.java 编译,再用 java ThreadDemo 运行:
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
// 创建线程并指定任务;此时尚未启动线程。
Thread worker = new Thread(() -> {
// currentThread:获取当前真正执行这段代码的线程。
// getName:查看线程名字,帮助判断是谁在执行。
System.out.println("执行任务的线程:" + Thread.currentThread().getName());
}, "worker");
// getPriority:读取线程优先级,不会启动线程。
System.out.println("优先级:" + worker.getPriority());
// 直接调用普通方法,执行任务的仍是 main 线程。
worker.run();
// 启动 worker 线程,由 worker 执行任务。
worker.start();
// join:让 main 等待 worker 完成任务。
worker.join();
}
}直接在普通命令行环境运行时,任务部分依次输出:
执行任务的线程:main
执行任务的线程:worker第一行说明直接调用 run() 没有启动新线程;第二行说明 start() 启动的新线程执行了任务。同一个线程对象只能启动一次,再次调用 start() 会抛出 IllegalThreadStateException,即使它已经执行完也不能重新启动。
getPriority() 是不是优先级越高,就一定先执行?
不是。它只是读取优先级,不能靠优先级保证线程的执行先后顺序。普通平台线程的优先级范围是 1~10,通常 main 线程默认是 5,新建线程会继承创建它的线程的优先级。
exit() 为什么不选?
你可能见过 System.exit(0),但它属于 System 类,表示终止整个 JVM,不是只结束某一个线程。普通线程的任务执行完,线程自然结束,不需要调用 exit()。
个别 JDK 版本的 Thread 源码中存在私有的 exit() 清理方法,但外部不能调用。此类笔试通常考公开可用的方法,因此不选 D;如果题目严格追问“源码里是否声明过”,则应区分 JDK 版本和访问权限。
记住:run 写任务,start 启线程,getPriority 看优先级;System.exit 退出整个程序。
参考资料:Java 官方文档:Thread。
2.3 第 1 题:父子类构造方法的执行顺序
题目
下面代码的输出结果是什么?(将截图代码整理排版,字符串使用 Java 的英文双引号。)
class Base {
Base() {
System.out.print("Base");
}
}
public class Alpha extends Base {
public static void main(String[] args) {
new Alpha();
new Base();
}
}考点
默认构造方法、super() 对父类构造方法的调用,以及 print() 不自动换行。
答案
BaseBase两个 Base 连在一起,中间没有空格,也没有自动换行。
解析
先看 Base() 是什么。
Base() 与类名相同,并且没有返回类型,因此它是构造方法。创建对象时,会调用构造方法完成初始化。本题每执行一次 Base(),就打印一次 Base。
第一步:执行 new Alpha()。
Alpha extends Base 表示 Alpha 是子类,Base 是父类。Alpha 没有写任何构造方法,编译器会自动补一个默认的无参构造方法,在本题中可以理解为:
// 这是解释编译器默认行为的等价写法,不是需要额外添加的代码。
public Alpha() {
super(); // 调用父类 Base 的无参构造方法。
}所以,虽然写的是 new Alpha(),仍然会调用父类的 Base(),打印第一次 Base。随后 Alpha 的构造方法没有其他内容,也就没有别的输出。
可以理解为:要初始化子类对象,得先完成它的父类部分的初始化。 这不表示另外创建了一个独立的 Base 对象;这里创建的仍是一个 Alpha 对象。
第二步:执行 new Base()。
这次直接创建 Base 对象,调用 Base(),打印第二次 Base。
| main 中的执行顺序 | 为什么打印 | 输出 |
|---|---|---|
new Alpha() | 默认构造方法通过 super() 调用 Base() | Base |
new Base() | 直接调用 Base() | Base |
为什么不是分两行?
因为题目使用 System.out.print("Base"),只打印内容,不自动换行。如果改为 println("Base"),才会每次打印后换行。
怎么自己验证?
将题目完整代码保存为 Alpha.java(文件名与公开类 Alpha 一致),用 javac Alpha.java 编译,再用 java Alpha 运行,输出就是 BaseBase。
记住:本题 new 子类也会调用父类构造方法;两次调用 Base(),就打印两次 Base。
补充边界:只有类里完全没声明构造方法,编译器才自动提供默认构造方法。如果父类只有带参数的构造方法,子类就不能直接依赖默认的 super(),需要明确传入参数,否则会编译失败。
参考资料:Java 语言规范:默认构造方法。
2.4 第 22 题:ArrayList、LinkedList 和 HashMap 的特点
题目
Java 中的集合类包括 ArrayList、LinkedList、HashMap 等类,下列关于集合类描述正确的是哪些?(多选)
| 选项 | 内容 |
|---|---|
| A | ArrayList 和 LinkedList 均实现了 List 接口 |
| B | ArrayList 的访问速度比 LinkedList 快 |
| C | 添加和删除元素时,ArrayList 的表现更佳 |
| D | HashMap 实现 Map 接口,它允许任何类型的键和值对象,并允许将 null 用作键或值 |
考点
List 与 Map 的区别、数组与链表的查找和增删特点,以及 HashMap 对 null 的支持。
答案
按常见笔试题意,选 A、B、D。
B 中的“访问”按“根据下标取元素”理解;不是说任何访问方式下 ArrayList 都一定更快。D 中的“任何类型”指可使用不同类型的对象,但实际添加时仍要符合声明的泛型类型。
解析
A 为什么对?List 可以理解为一份按位置排列的清单。
ArrayList 和 LinkedList 都实现 List 接口,都能按顺序存放元素、允许重复,并支持 get(下标)。
例如清单为 [小王, 小李, 小张],get(0) 取小王,get(1) 取小李。下标从 0 开始。
两者都能做这些事,但底层存法不同:
- ArrayList:底层是数组,像一排有位置编号的格子。
- LinkedList:底层是双向链表,像一串前后相连的节点,每个节点记住前一个、后一个节点在哪里。
B 为什么通常判对?按下标找,数组能直接定位,链表需要沿着节点找。
假设要取第 500 个元素,也就是 get(499):
- ArrayList 根据下标直接找到对应数组位置,不需要从第一个元素数过去。
- LinkedList 从头或尾较近的一端出发,沿着节点逐个找到目标位置。
所以大量按下标取元素时,ArrayList 通常更合适。技术上,ArrayList 的 get(index) 是 O(1),LinkedList 的一般下标访问是 O(n):前者的定位工作量不随元素数量增长,后者最坏需要走过与元素数量成比例的节点。
但如果只是取第一个、最后一个元素,LinkedList 也能直接找到;用迭代器顺序遍历时,两者都只需依次访问元素。因此不能把 B 扩展成“所有访问方式都必然更快”。
C 为什么不选?增删快不快,要看在哪里操作、是否已经找到位置。
假设有 [小王, 小李, 小张],现在要在最前面插入小赵:
- ArrayList:要把原来的元素往后挪,给小赵腾出第一个位置。
- LinkedList:新建小赵节点,接到原来的头节点前面,不需要把其他元素整体搬动。
但也不能反过来背成“LinkedList 所有增删都快”。如果要在中间某个下标插入,LinkedList 也要先花时间找到那里。
| 操作场景 | ArrayList | LinkedList |
|---|---|---|
| 按下标取中间元素 | 可直接定位 | 需要沿节点查找 |
| 在开头插入或删除 | 需要移动后面的元素 | 调整头部节点连接即可 |
| 在末尾添加 | 通常很高效,空间不足时需要扩容 | 通过尾节点添加,通常也很高效 |
| 按中间下标插入或删除 | 定位快,但可能移动后面的元素 | 先查找位置,再修改节点连接 |
| 迭代器已定位到中间位置后增删 | 可能需要移动后面的元素 | 可直接修改附近节点的连接 |
因此,C 这种没有说明场景、直接说 ArrayList 增删表现更佳的说法不能成立。实际运行速度还会受内存布局、对象分配等影响,不能只凭一个口诀判断。
D 为什么对?HashMap 是“根据键查值”的字典。
比如把学号作为键、姓名作为值:
1001 → 小王
1002 → 小李HashMap 实现 Map 接口,存的是一对一对的“键和值”。它允许一个 null 键,也允许多个键对应的值为 null。
下面是可直接运行的例子,保存为 MapDemo.java,用 javac MapDemo.java 编译,再用 java MapDemo 运行:
import java.util.HashMap;
import java.util.Map;
public class MapDemo {
public static void main(String[] args) {
// 键声明为 Integer,值声明为 String,因此不能随意放入不匹配的类型。
Map<Integer, String> students = new HashMap<>();
students.put(1001, "小王");
students.put(null, "未分配学号"); // 允许 null 作为键。
students.put(1002, null); // 允许 null 作为值。
students.put(1003, null); // 不同的键可以都对应 null 值。
students.put(null, "待确认"); // null 键已经存在,替换它对应的值,不是新增第二个 null 键。
System.out.println(students.get(1001)); // 小王
System.out.println(students.get(null)); // 待确认
System.out.println(students.size()); // 4
}
}HashMap 的键不能重复:再次放入相同的键,会替换原来的值。它不保证遍历时按插入顺序排列。
这里“允许任何类型的键和值对象”不表示可以无视泛型。例如上面的 Map<Integer, String>,键应是 Integer 或 null,值应是 String 或 null。基本类型不能直接写成泛型参数,要用 Integer 等包装类型;自定义对象作为键时,还应正确实现 equals() 和 hashCode()。
记住:List 按位置放,Map 按键找;ArrayList 按下标取值快,增删要看场景;HashMap 一个 null 键,多个 null 值。
参考资料:Java 官方文档:ArrayList、LinkedList、HashMap。
2.5 第 21 题:哪些集合是线程安全的
题目
下列哪些集合是线程安全的?(多选)
| 选项 | 集合 |
|---|---|
| A | ConcurrentHashMap(原图写作 ConcurrentHashmap) |
| B | Vector |
| C | ArrayList |
| D | Hashtable |
考点
常见集合是否支持多线程安全访问,以及“单次操作安全”与“多步业务操作安全”的区别。
答案
A、B、D:ConcurrentHashMap、Vector、Hashtable。
普通 ArrayList 没有内置同步保护,不能直接让多个线程同时修改同一个实例。
解析
线程安全是什么意思?
想象两名工作人员同时往同一份名单里加名字。如果没有协调,两人可能同时认为“下一个空位是第 5 个”,结果写入互相干扰,导致数据丢失或状态错误。
线程安全集合在内部提供协调机制,让它支持的并发操作按规定正确完成。这里讨论的是多个线程操作同一个集合对象;每个线程各用自己的 ArrayList,不共享,就不是这种冲突场景。
逐个看选项。
| 集合 | 线程安全? | 大白话解释 |
|---|---|---|
| ConcurrentHashMap | 是 | 专门支持多线程同时访问的 Map,读取通常不加锁,更新也提供并发保护 |
| Vector | 是 | 与 ArrayList 一样基于可变长数组,主要访问和修改方法通过同步保护 |
| ArrayList | 否 | 普通版本没有内置的同步保护,共享修改需要额外协调 |
| Hashtable | 是 | 较早的 Map 实现,主要访问和修改方法通过同步保护 |
可以把同步保护想成“操作某部分数据时先拿到钥匙”,避免其他线程同时破坏这一步操作。不过 ConcurrentHashMap 的实现更复杂,不是所有操作都锁住整张表,不能简单等同于一把大锁。
最容易漏选的是 Hashtable。
HashMap 和 Hashtable 名字相近,但特点不同:
| 特点 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 内置线程安全支持 | 无 | 有 | 有 |
| 是否允许 null 键或 null 值 | 允许 | 都不允许 | 都不允许 |
线程安全,是否代表所有写法都安全?
不是。单次调用安全,不代表拆成多次调用的业务逻辑会自动变成一个整体。
例如,两个人都想“名单里没有小王,就加一个”:
// 假设 names 是多个线程共享的 Vector<String>。
// contains 和 add 各自有同步保护,但这两步合起来不是一次不可分割的操作。
if (!names.contains("小王")) {
names.add("小王");
}两个线程可能都先检查出“没有小王”,然后各添加一次,最终出现两个小王。Vector 没有被破坏,但“不重复添加”的业务要求没有得到保证。
对于 Vector,上面的检查和添加应在同一个同步范围内完成,而且其他访问也要遵守相同的协调规则:
// 假设 names 是已经创建的共享 Vector<String>。
synchronized (names) {
// 检查和添加使用同一把锁,避免其他线程插进这两步之间。
if (!names.contains("小王")) {
names.add("小王");
}
}对于 ConcurrentHashMap,“不存在才放入”可以直接用它提供的原子方法 putIfAbsent,不用拆成先检查、再 put 两次调用。例如下面是可独立运行的单线程演示,展示该方法的行为:
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentMapDemo {
public static void main(String[] args) {
ConcurrentHashMap<String, String> users = new ConcurrentHashMap<>();
// putIfAbsent:只有该键不存在时才放入,检查和放入作为一次原子操作完成。
users.putIfAbsent("1001", "小王");
users.putIfAbsent("1001", "小李"); // 键已存在,不替换原来的小王。
System.out.println(users.get("1001")); // 输出:小王。
}
}保存为 ConcurrentMapDemo.java,用 javac ConcurrentMapDemo.java 编译,再用 java ConcurrentMapDemo 运行。示例只是帮助理解方法效果,其并发保证来自 API 约定,不是靠单线程示例验证的。
此外,集合线程安全,也不表示放进去的可变对象会自动线程安全;遍历、跨多个键的操作等场景仍需按各自 API 的规则处理。
记住:ConcurrentHashMap、Vector、Hashtable 选上;ArrayList 不选。单步安全,不代表多步业务自动安全。
参考资料:Java 官方:ConcurrentHashMap、Vector、Hashtable、ArrayList。
2.6 第 23 题:String、Integer 的 == 比较
题目
给定以下代码,下列选项中结果为 true 的有哪些?(多选)
String a = "aaaa";
String b = new String("aaaa");
Integer c = 100;
Integer d = 300;
Integer e = 100;
Integer f = 300;| 选项 | 表达式 |
|---|---|
| A | b == "aaaa" |
| B | c == e |
| C | d == f |
| D | a == "aaaa" |
考点
对象的 == 比较、字符串常量池、Integer 自动装箱与缓存,以及 equals() 的用途。
答案
按常见默认 Integer 缓存范围,选 B、D。
| 表达式 | 结果 | 原因 |
|---|---|---|
b == "aaaa" | false | b 指向 new 出来的新对象,与字符串字面量对应的对象不是同一个 |
c == e | true | 100 在 Integer 保证缓存的范围内,两次装箱复用同一对象 |
d == f | 通常为 false | 300 超出默认缓存范围,常见默认环境下两次装箱得到不同对象 |
a == "aaaa" | true | a 与相同字符串字面量指向常量池中的同一对象 |
严格来说,300 是否缓存受实现和配置影响;若运行环境扩大了 Integer 缓存范围,C 也可能为 true。因此笔试通常默认未调整缓存,按 B、D 作答。
解析
先记住:对象用 ==,问的是“是不是同一个对象”。
两本书内容完全一样,也不代表它们是同一本书。对象比较也是如此:
==:是不是同一个对象?- 对于 String、Integer,
equals():里面的文字或数字是不是相同?
这里 String 和 Integer 都是对象类型,所以不能只看内容相同,就判断 == 是 true。普通 int 是基本类型,两个 int 用 == 才是直接比较数值。
先看字符串 a 和 b。
String a = "aaaa" 直接使用字符串字面量。Java 会让相同的字符串字面量共享字符串常量池里的对象。
new String("aaaa") 则明确创建一个新的 String 对象。它的文字也是 aaaa,但对象是新的。
a ──────────────→ 常量池中的 String 对象:aaaa
"aaaa" ─────────→ 同一个常量池对象
b ──────────────→ new 创建的另一个 String 对象:aaaa因此:
a == "aaaa"; // true:同一个对象。
b == "aaaa"; // false:内容相同,对象不同。
a.equals(b); // true:String.equals 比较文字内容。上面是表达式讲解,实际验证时可放进 System.out.println(...) 打印,不是可直接独立执行的 Java 语句。
再看 Integer:它为什么有时会复用对象?
Integer c = 100 右边是基本类型数字,左边是 Integer 对象。Java 会自动把数字包装成 Integer,这叫“自动装箱”,通常通过 Integer.valueOf(100) 完成。
为避免常用的小整数反复创建对象,Integer 缓存了 -128~127 的对象,可以理解为预先准备好一排“公用数字盒子”。
- c 要 100,拿到缓存里的 100 对象。
- e 也要 100,拿到同一个缓存对象。
- 所以
c == e是 true。
300 不在默认的这排盒子里。常见默认环境下,d 和 f 分别得到一个装着 300 的对象,数值一样,但不是同一个对象,所以 d == f 为 false。
注意:这里 c == e 两边都是 Integer,不会为了比较而自动拆成 int。若写 c == 100,另一边是 int,则会将 c 拆成数值比较;如果 c 为 null,这种拆箱会抛出空指针异常。
真正想比较内容时怎么写?
String 和 Integer 都已经实现按内容比较的 equals。若还要兼顾变量可能为 null,可以使用 java.util.Objects.equals(x, y)。
下面是完整验证程序,保存为 EqualityDemo.java,用 javac EqualityDemo.java 编译,再用 java EqualityDemo 运行:
import java.util.Objects;
public class EqualityDemo {
public static void main(String[] args) {
String a = "aaaa";
String b = new String("aaaa");
Integer c = 100;
Integer d = 300;
Integer e = 100;
Integer f = 300;
// == 比较这些引用是否指向同一个对象。
System.out.println(b == "aaaa"); // false。
System.out.println(c == e); // true。
System.out.println(d == f); // 常见默认环境下为 false。
System.out.println(a == "aaaa"); // true。
// Objects.equals:兼顾 null,并调用对象的 equals 比较内容。
System.out.println(Objects.equals(a, b)); // true,文字相同。
System.out.println(Objects.equals(d, f)); // true,数值相同。
}
}不要把“equals 比较内容”套用到所有自定义类:自定义类如果没有重写 equals,默认仍按对象身份比较。
记住:对象 == 看是不是同一个;字符串字面量会复用,小整数装箱有缓存;比较 String、Integer 的内容用 equals。