刚帮人调试完一套 SDUT 的 Java 面向对象作业——就是那份“常用类(编程题1-7)”,发现不少人在 String、包装类、日期格式化这些“看起来很简单”的题上栽跟头。这门课讲到常用类这个阶段,考察的已经不是语法背得熟不熟,而是你有没有真正理解类的行为、方法的设计意图和边界条件。今天就把这套题背后最值得抠的知识点、我踩过的坑、以及怎么在 OJ 上稳定通过的经验一次性说透。
1. 这套常用类题到底在考什么:拆开题目表面的“马甲”
先说结论:SDUT 这套“面向对象-10 常用类”的编程题,题型翻来覆去就是那几类——字符串处理、包装类与数值转换、Math 与随机数、日期格式化与 Calendar 操作。很多同学觉得题难,不是因为语法不会,而是没搞懂每道题真正想测试的知识点,被题目描述里的业务包装带偏了。
1.1 考点集中营:一套题覆盖五个核心类
我统计了一下这类题目的高频考点分布,基本是这样一个格局:
| 核心类 | 典型考法 | 隐藏考点 |
|---|---|---|
| String | 字符统计、子串截取、反转、替换 | 不可变性、equals 与 == 的区别 |
| StringBuffer / StringBuilder | 大量字符串拼接、逆序输出 | 与 String 的性能差异、线程安全 |
| 包装类 | 字符串转 int、进制转换、比较大小 | 自动装箱/拆箱、Integer 缓存机制 |
| Math | 绝对值、幂运算、四舍五入、随机数 | 静态方法特性、random 的区间 |
| Date / Calendar / SimpleDateFormat | 日期输出、计算天数差、格式化 | SimpleDateFormat 线程不安全、月份从 0 开始 |
你去看那 7 道题,基本逃不出这张表的范围。题目可能会包装成“统计一句话里每个字母出现的次数”“把 MM/dd/yyyy 格式的日期转换成 yyyy-MM-dd”“模拟一个抽奖程序”之类的业务场景,但剥开壳子,内核全是上面这些 API 的调用和边界处理。
1.2 为什么说这阶段是“面向对象”的转折点
如果你只把这套题当“API 背诵测试”,那就亏大了。实际上,“常用类”在 Java 课程体系里是一个非常特殊的位置——它不再让你自己定义类,而是让你学会“使用别人定义好的、设计精良的类”。这背后其实是面向对象思维的第二次跃迁:
- 第一阶段是“万物皆对象”:自己写 class、new 对象、调方法;
- 第二阶段是“别人写的类也是对象”:你要去读 JDK 文档,理解 String 为什么重写了 equals,理解 Integer 为什么有缓存,理解 SimpleDateFormat 为什么不是线程安全的。
所以做这 7 道题的时候,别只为了通过 OJ 的测试用例。每道题做完之后,多问自己一句:这个类的作者为什么这么设计?把这句话想明白了,后面学集合框架、IO 流、多线程会顺畅非常多。
2. String 类题型:这三个细节比背 API 重要得多
7 道题里通常有一半左右涉及字符串处理,这是整套题的大头。字符串题最容易出现的雷区,我一个个说,都是 OJ 上真实见过的错误。
2.1 equals 和 ==:一个判断引发的“悬案”
这是老生常谈,但在 OJ 题里依然有大把人中招。先看一段我经常在学生的提交里见到的代码:
Scanner sc = new Scanner(System.in); String flag = sc.next(); if (flag == "yes") { // 错误! // ... }为什么错了?因为==比较的是引用地址,而equals比较的是字符序列内容。这里有个非常隐蔽的细节:如果输入是String flag = "yes",直接字面量赋值,flag == "yes"很可能返回 true(因为都指向常量池里的同一个对象)。但一旦输入来自sc.next(),JVM 返回的就不是常量池里的那个对象了,==返回 false。
这不是“玄学”,而是 String 的 intern 机制。记住一句话:字符串内容比较,永远用equals,不要图省事用==。我见过有同学因为第一次测通过了,后面所有题都写==,结果换一个数据就 WA,还找不到原因,就是这个细节。
2.2 不可变性:为什么字符串拼接别用 String
有一类题是“把一句话里的单词顺序反转”或者“拼接 n 个字符串”。很多新手这样写:
String result = ""; for (int i = 0; i < n; i++) { result += input[i] + " "; }功能上没错,但如果在 OJ 上数据量大、循环次数多,就可能超时。原因是 String 是不可变对象,每次+=都会创建一个新的 String 对象,旧的就被丢弃。循环 10000 次,就创建了 10000 个中间对象,垃圾回收压力巨大。
正确做法是用 StringBuilder(单线程场景下够用)或者 StringBuffer(线程安全,但性能稍差):
StringBuilder sb = new StringBuilder(); for (int i = 0; i < n; i++) { sb.append(input[i]).append(" "); } String result = sb.toString().trim();这套题里凡是涉及“拼接”“反转”的,我都建议直接用 StringBuilder。记住它的常用 API:append、reverse、toString。尤其是reverse(),处理“反转字符串”题简直是一行答案:
String reversed = new StringBuilder(s).reverse().toString();2.3 边界问题:substring 和 charAt 的下标陷阱
字符串题目里另一个高频 WA 原因就是下标越界或者取错区间。substring(beginIndex, endIndex)是左闭右开——取beginIndex位置开始,到endIndex-1位置结束。比如"Hello".substring(1, 3)结果是"el",不是"ell"。
还有一个特别容易错的地方:把字符串反转后和原字符串比较判断回文。如果你用 charAt 逐字符比较,循环条件怎么写?我见过有人写for (int i = 0; i <= s.length(); i++),直接 StringIndexOutOfBoundsException。正确应该是i < s.length(),或者用双指针int left = 0, right = s.length() - 1; while (left < right),这样既避免越界又少比较一半。
做题技巧:凡是涉及下标、长度的题目,先自己手动模拟一遍,把边界情况(空字符串、单个字符、最大长度)写出来测试一遍再提交。这类题 WA 一次代价很高——SDUT 的 OJ 往往有时间限制和提交次数记录,养成“先测边界再提交”的习惯,能省很多事。
3. 包装类与装箱拆箱:数值转换题里的隐形杀手
字符串转数字、数字转字符串、进制转换——这些题看着人畜无害,实际上包装类机制里有好几个隐蔽的坑。
3.1 Integer 缓存:为什么 128 和 127 的结果不一样
如果题目里让你比较两个 Integer 对象的大小,==又出来坑人了。看这段代码:
Integer a = 127; Integer b = 127; System.out.println(a == b); // true Integer c = 128; Integer d = 128; System.out.println(c == d); // false输出结果居然不一样,很多第一次见这个结果的初学者直接懵了。原因:Integer 类内部维护了一个缓存池,默认缓存 -128 到 127 之间的 Integer 对象。Integer a = 127实际上触发自动装箱,调用的是Integer.valueOf(127),而 valueOf 有个判断:如果值在缓存范围内,直接返回缓存对象;超过范围,才 new 一个新对象。
所以a和b指向同一个缓存对象,==是 true;c和d各 new 了一个对象,==是 false。这题如果要求比较两个大数的“值”,一定要用a.intValue()或者a.equals(b)。用 equals 是正确的,因为 Integer 重写了 equals 来比较值。
3.2 parseInt 与 valueOf:别把返回类型搞混
字符串转 int 的题里,Integer.parseInt(String)返回的是原始类型 int,Integer.valueOf(String)返回的是包装类型 Integer。大多数场景两者效果差不多,但有一个关键区别:valueOf 会走缓存机制(如果数值在 -128~127)。虽然这个区别不影响做题答案,但如果你写代码时把一个 Integer 赋给 int,就涉及自动拆箱,一旦这个 Integer 是 null,立刻抛 NullPointerException。
有个在 SDUT 题里很经典的场景——从输入里读一个字符串表示的数字,然后和某个 int 比较大小:
int n = Integer.parseInt(sc.nextLine().trim());这里我特意加了.trim(),因为nextLine()取到的字符串可能带首尾空格,parseInt对空格是敏感的,只要有空格就抛 NumberFormatException。如果用的是nextInt()就没有这个问题,但它不会吃掉末尾换行,后续再用nextLine()会读到空串。这是 OJ 题的经典陷阱。
3.3 进制转换:方法一多就容易选错
进制转换在常用类题目里经常以“10 进制转 16 进制”“8 进制转 2 进制”等形式出现。Java 提供了好几个相关方法,很多人临场容易搞混:
Integer.toHexString(int):转 16 进制,结果是字符串;Integer.toOctalString(int):转 8 进制;Integer.toBinaryString(int):转 2 进制;Integer.parseInt(String, int radix):按 radix 进制解析字符串。
这组方法里最容易翻车的是负数。Integer.toHexString(-1)的结果不是"-1",而是"ffffffff",因为它在内部是按补码来处理的。如果题目没明确说负数的处理规则,提交前一定要测一下负数用例。
另外,转进制后如果题目要求大写形式的十六进制字母,记得调用.toUpperCase(),toHexString默认返回小写 a-f,不转换直接交上去必 WA。
4. Math、日期与格式化:别小看这些“小工具类”
相对字符串和包装类,Math 和日期类的题考查的知识点比较固定,但也正因为“看起来简单”,很多人反而在细节上翻车。
4.1 Math 类题型:记住它全是静态方法就够了
Math 类不需要 new 对象,所有方法都是静态的,直接Math.xxx()调用。高频考点包括:
Math.abs()取绝对值,但要小心:Math.abs(Integer.MIN_VALUE)返回的还是负数,因为 int 范围不对称;Math.pow(a, b)求幂,返回 double,如果需要 int 结果要强转;Math.sqrt()求平方根,输入负数返回 NaN;Math.random()返回[0.0, 1.0)的随机 double,左闭右开。
是不是觉得没什么技术含量?但实际题目里设计陷阱的方式很有意思。比如“生成 1 到 100 之间的随机整数”,很多同学写成:
int num = (int)(Math.random() * 100) + 1;这就错了。Math.random()最大是0.999...,乘 100 后最大是99.999...,强转 int 是 99,+1 之后是 100,这样看没错。但最小的值是0.0,乘 100 后是 0,+1 后是 1。实际上这个式子生成区间确实是[1, 100],看起来没问题。可如果你随手写(int)(Math.random() * 100)不加 1,那生成的是[0, 99],漏掉了 100 这个边界。题目如果要求“包含两端”,一定要检查随机数公式的边界。
还有个隐藏考点:Math.round()的四舍五入规则。Math.round(2.5)返回 3,Math.round(-2.5)返回 -2——注意它不是“远离零四舍五入”,而是“向正无穷方向取最接近的整数”。很多题目涉及金额计算或评分统计,负数场景一出现,保守的同学立马算错。
4.2 日期格式化:SimpleDateFormat 的“线程安全”和月份 0 起点
日期类的题目在 SDUT 常用类编程题里经常占 1 到 2 道,核心考点是 Date、Calendar 和 SimpleDateFormat 的配合使用。第一个大坑是月份从 0 开始计数。
Calendar cal = Calendar.getInstance(); cal.set(2024, 12, 1); // 你以为的 12 月,实际是下一年的 1 月Calendar.MONTH的取值是 0 到 11,0 代表一月。正确写法是cal.set(2024, Calendar.DECEMBER, 1),或者用cal.set(2024, 11, 1)。这是我见过的这阶段最高频的“逻辑错误”之一——编译不报错,运行也不报错,但计算结果差一个月,整个答案就错了。
第二个坑是 SimpleDateFormat 不是线程安全的。虽然初级题里只用单线程,不涉及并发,但我在帮人 review 代码时见过有人把它定义成 static 字段然后在方法里共享使用,这个习惯非常差。如果后面学到多线程再沿用这个习惯,就会出现诡异的结果:时间的数据被冲掉、日期错乱、甚至抛异常。建议每题都 new 一个实例,或者干脆学习使用java.time包下的LocalDate、DateTimeFormatter。不过要提醒一点:很多学校的 OJ 用的 JDK 版本可能比较老,java.time在 Java 8 里才引入。如果 OJ 环境是 Java 7,用了 LocalDate 会直接编译失败。做题前先看下 OJ 支持的 Java 版本,别学了个新特性结果环境不支持。
还有格式化字符串的匹配问题。题目给的输入日期格式可能是yyyy/MM/dd,你要输出yyyy年MM月dd日,中间的“/”和“年”“月”这些分隔符必须和 pattern 完全对应。MM和mm是完全不同的含义——MM是月份,mm是分钟。写错一个字母不会编译报错,但输出结果全错,这种题属于“白给型”,细心一点就满分,粗心一点就 WA。
5. OJ 提交实战:从 WA 到 AC 的完整排错链路
聊完知识点,最后说说这个阶段刷题最实用的“作战流程”。如果你这 7 道题里有几道第一次提交没过,别慌,按下面的排查路径走,基本能把问题定位出来。
5.1 拿到题目先做三件事而不是急着敲代码
我自己的做题顺序是先花 5 分钟做“输入输出分析”,比直接编码高效得多:
- 看清输入格式:是一行一个测试数据,还是多行?有没有 T(表示测试组数)在开头?每行末尾有没有多余空格?
- 看清输出格式:每个结果占一行,还是结果之间有空格/逗号?有没有要求“Case #1:”这样的前缀?
- 找边界条件:字符串可能为空吗?数字可能是负数吗?数值能大到超过 int 范围吗?
这三个问题的答案直接决定你代码里要不要做特殊处理。SDUT 的题目描述一般比较直白,但也不会把边界条件写在显眼的位置,需要你自己判断。比如字符串类题目如果没说“输入非空”,你就得想想空字符串怎么处理;如果没说“n 为正整数”,0 和负数也得有说法。
5.2 提交失败后的“二分定位法”
遇到 WA(Wrong Answer),我的做法是不要盯着代码干瞪眼,而是构造“针对性测试数据”来缩小范围:
- 如果题目有一个样例输入,先把样例跑通,这是基本盘;
- 然后测边界值:最大输入、最小输入、空输入、单字符输入;
- 再测特殊值:负数、0、混合大小写、包含空格和标点符号的输入;
比如字符串反转题,你样例过了,提交 WA,我就怀疑是不是输入里包含空格——sc.next()只读到空格前的内容,读不完整句话。这种题必须用sc.nextLine(),但用了nextLine()又要注意前面有没有nextInt()残留的换行符,需要先sc.nextLine()把换行吃掉。这个“吃掉换行”的操作,我见过的初学者至少有一半人不知道。
如果是 RE(Runtime Exception),优先查数组越界和空指针。定位方法用“二分注释”:把代码里处理字符串的段落注释掉一半,跑一次,再注释另一半,跑一次,快速定位是哪个步骤崩的。虽然原始了一点,但在 OJ 上比加断点调试要快太多。
5.3 一套可以抄作业的输入输出模板
最后送一个我自己写这类题用的基础模板,能覆盖绝大多数 SDUT Java 编程题的输入输出场景:
import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); // 情形一:先读一个整数 T,表示后面有 T 组测试数据 int t = sc.nextInt(); sc.nextLine(); // 吃掉 nextInt 后面的换行符 while (t-- > 0) { String line = sc.nextLine(); // 整行读取,避免空格截断 // 处理逻辑... } // 情形二:循环读取直到 EOF while (sc.hasNext()) { String s = sc.next(); // 或 nextLine() // 处理逻辑... } sc.close(); } }类的名字必须是Main(SDUT OJ 的硬性要求),main 方法签名不能写错,否则直接 CE(Compile Error)。还有一点,Java 代码在 OJ 上跑得比 C/C++ 慢,如果题目的时间限制很紧,尽量不要在循环里反复拼接 String,能用 StringBuilder 就用。
这套题刷完之后,建议把 String、包装类、Math、SimpleDateFormat 的常用方法整理成一张速查表,后面做课程设计、面试刷题都用得上。其实常用类这一章的学习目标就一个:建立“查文档—用 API—处理边界”的思路。把这套题吃透,你离脱离“照着书本敲代码”的阶段就又近了一步。