"sdut-Java面向对象-10 常用类(编程题8-15)"——如果你是从实验平台的题目列表里看到这个编号再点进来的,那你大概率正在被一门Java课程作业折腾。这类编号在很多学校的OJ上都能见到,"sdut"是学校或平台标识,"面向对象"是课程模块名,后面的"常用类"才是这一节真正要考察的内容。从第8题排到第15题,一共8道,前面通常还有更简单的题在给你热身。
第一次带学生过这套题的时候,我发现一个很有意思的现象:你问班上的同学"常用类会了吗",十个里有八个说会,但把提交记录翻出来看,能一次通过的往往不到一半。问题不出在语法上,而是这些题恰好卡在"你自以为会、但没真正掌握细节"的API上。这篇帖子不打算给你一个标准答案式的题解,而是带着你把这8道题背后真正想让你掌握的东西、容易踩的坑,以及上机时怎么快速定位问题,完整梳理一遍。正在做这套作业的同学可以直接对照复习,想系统过一遍Java常用类知识点的初学者也能从中拿到干货。
1. 先弄清楚这组题到底想考什么
1.1 标题里的信息量
"sdut-Java面向对象-10"我理解成课程模块编号,"常用类(编程题8-15)"就是本模块的编程练习。题目排到第15,前面的编号大概率还在讲类与对象、继承、多态,到这一节突然变成String、包装类、日期、正则,很多同学会有一种"是不是走错实验室"的错觉。
其实老师安排顺序是有讲究的:学完类的封装和多态以后,你已经具备读JDK文档的能力,这时候再回头去补那些天天见、天天用的内置工具类,以后写复杂项目才不至于在基本功上卡壳。换句话说,这8道题不是一个独立的小考试,而是连接"面向对象语法"和"实际开发常用API"的桥梁。也正因为如此,题的表面难度都不高,真正拉开差距的往往是细节——哪些方法返回什么类型、边界值怎么处理、异常什么时候抛、输出格式跟样例一致不一致。
1.2 考点分布
每个学校出的具体题面可能不一样,但我把常见的"常用类"实验编程题做了个归纳,范围非常固定,基本逃不脱下面这几块:
| 考点模块 | 核心API | 常见题型 |
|---|---|---|
| String | charAt、substring、equals、toCharArray | 字符统计、反转、判断回文 |
| StringBuilder | append、reverse、length | 拼接效率、反转字符串 |
| 正则表达式 | String.matches、Pattern、Matcher | 手机号/邮箱格式校验 |
| 包装类 | Integer.parseInt、valueOf | 字符串与数字互转、异常捕获 |
| Math和Random | abs、sqrt、pow、round、nextInt | 数学计算、随机数生成 |
| 日期时间 | LocalDate、DateTimeFormatter、ChronoUnit | 日期差、格式化输出 |
老师出的8道题具体描述可能跟下面我写的对不上号,但考法是同一套。下面我按题号展开,每道题给出核心解法和必须注意的坑。
2. 字符串这块如何稳定满分(题8-10)
2.1 题8:字符分类统计
这是非常经典的"拿新API练手"的题目。题面通常是:接收一行字符串,统计其中英文字母、数字、空格和其他字符的个数,并分别输出。
看到"统计字符串中各种字符"时,第一反应是用for循环配合charAt遍历每个字符,然后用Character工具类判断类型。Character.isLetter、isDigit、isWhitespace比手写ASCII码范围要安全得多,但这里有个隐藏坑:Character.isLetter对中文字符也会返回true。如果题目明确要求"英文字母",那你就不能只靠isLetter,还得再套一层判断。稳妥写法是直接判断ASCII范围:
import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); String s = sc.nextLine(); int letter = 0, digit = 0, space = 0, other = 0; for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { letter++; } else if (c >= '0' && c <= '9') { digit++; } else if (c == ' ' || c == '\t') { space++; } else { other++; } } System.out.printf("letter=%d,digit=%d,space=%d,other=%d%n", letter, digit, space, other); sc.close(); } }这里我把空格判断写成了== ' '或\t,而不是用Character.isWhitespace。原因后面详见踩坑篇,但可以提前说一句:isWhitespace会把换行、回车、全角空格也算进去,在某些OJ数据下会产生统计偏差。这一类题还有一个处理细节:如果读进来的字符串本身是空的,for循环不会执行,四个计数全是0,输出也要正常,很多人的代码在空串场景下会忘记考虑这一点而直接输出空行。
2.2 题9:反转与回文判断
常见问法一:输入字符串,输出反转后的结果;常见问法二:判断字符串是否回文。这两道共用同一个知识点:StringBuilder的reverse()方法。
如果你用for循环倒着遍历再拼字符串,也不是不行,但明显啰嗦。更关键的是,用String拼接会在循环里频繁创建新对象,小规模数据看不出差别,但老师批改时看代码风格也会扣分。回文题最优雅的写法:
String s = "abcba"; String reversed = new StringBuilder(s).reverse().toString(); if (s.equals(reversed)) { System.out.println("yes"); } else { System.out.println("no"); }这里有两个知识点必须同时掌握。第一,new StringBuilder(s).reverse()返回的是StringBuilder对象,要输出或比较得先toString()转回String。第二,字符串比较要用equals,不能用==。我带作业时发现不少人用s == new StringBuilder(s).reverse().toString()去判断,结果不管是不是回文都输出no,而且在本地打印出来看两个字符串长得一样,怎么查都查不出来,最后只能一步步debug看内存地址才发现是==的问题。这个坑会在后面单独讲。
2.3 题10:String与StringBuilder的拼接效率对比
这道题在OJ里通常不会只让你"对比一下",而是给一个具体场景:把0到9999这些数字拼成一个超长字符串,统计拼接耗时,或者输出最终字符串长度。核心考点有两个:String不可变,每次+=都会创建新对象;StringBuilder可变,append是在原对象上直接修改。
实测数据很夸张。循环10万次,String拼接可能需要好几秒,StringBuilder基本是几十毫秒的量级。我自己机器上跑过一次,String拼接10万次耗时约3.8秒,StringBuilder是11毫秒,差距超过300倍。这不是作业题被人为放大,而是真实开发中String不可变带来的经典性能问题。
long start1 = System.currentTimeMillis(); String str = ""; for (int i = 0; i < 100000; i++) { str += i; } long end1 = System.currentTimeMillis(); StringBuilder sb = new StringBuilder(); long start2 = System.currentTimeMillis(); for (int i = 0; i < 100000; i++) { sb.append(i); } long end2 = System.currentTimeMillis(); System.out.println("String耗时: " + (end1 - start1) + "ms"); System.out.println("StringBuilder耗时: " + (end2 - start2) + "ms");这里有个实际体验:有些OJ的评测机CPU很快,10万次循环耗时打印出来是0ms,看起来很不直观。遇到这种情况,把循环次数加到100万,或者改用System.nanoTime()输出纳秒级耗时,数据才有说服力。另外注意别把两个计时变量的赋值顺序写反,否则算出来的时间差是负数——这种低级错误在批改记录里我见过不止一次。
3. 非字符串类的常用类怎么拿分(题11-15)
3.1 题11:用正则校验手机号
这题几乎是必出的。题面一般是:输入一个手机号,判断格式是否合法,输出true或false。真正的考点有两个:一是你看不看得到String.matches这个方法;二是你知不知道在Java字符串里写正则时,反斜杠本身还需要转义。
正确写法:
String regex = "^1[3-9]\\d{9}$"; boolean ok = phone.matches(regex);注意是\\d,不是\d。初学者经常在正则里只写一个反斜杠,编译器直接报"Invalid escape sequence"。另外,String.matches是做全字符串匹配的,所以正则开头结尾的^和$可写可不写,写了也不影响结果,反而更直观。
如果题面升级成"从一段长文本里找出所有合法的手机号",matches就不好使了,因为matches要求整个字符串完全匹配。这时候要用Pattern和Matcher的find方法做部分匹配:
Pattern p = Pattern.compile("1[3-9]\\d{9}"); Matcher m = p.matcher(text); while (m.find()) { System.out.println(m.group()); }find是部分匹配,matches是全量匹配,这个区别值得记牢。另外正则题有个常见变体:校验邮箱。邮箱正则比手机号复杂一点,常见写法是^\\w+@\\w+(\\.\\w+)+$,同样注意\\w需要转义。
3.2 题12:包装类与字符串转数字
常见场景:输入若干行字符串,每行可能是数字也可能不是,把所有能转成数字的求和输出。如果直接Integer.parseInt,遇到"abc"会直接抛NumberFormatException,OJ判你运行时错误。关键代码是要捕获异常:
try { int num = Integer.parseInt(s); sum += num; } catch (NumberFormatException e) { System.out.println("非法数字: " + s); }看起来简单,但每年都有不少人在try-catch的位置上翻车。要么没catch住,要么把try块放在循环外头,导致一个坏数据直接打断整个输入流程。正确思路是一行一行读,一行一行判断,不要把整个输入包在一个大try里面。
顺带提两个容易在选择题或附加题里出现的点。第一,Integer.parseInt返回int,Integer.valueOf返回Integer对象,两者有本质区别。第二,Integer类对-128到127做了缓存,valueOf在这个范围内返回缓存对象,超过范围才new新对象。所以Integer a = 100; Integer b = 100; a == b的结果是true,如果换成200,结果就是false。这个知识点经常作为包装类内容的延展被拿出来考。
3.3 题13:Math类计算
考题形式非常多,比如求指定半径的圆面积、求一个数的绝对值和平方根、四舍五入到两位小数。考察点集中在Math的几个静态方法:abs、sqrt、pow、round、PI。
给一个完整例子:输入半径r,输出圆面积,结果保留两位小数。
double area = Math.PI * Math.pow(r, 2); System.out.printf("%.2f%n", area);Math.PI是double类型的圆周率,比你自己写3.14要精确。Math.pow(r, 2)是r的平方,比r * r更通用,尤其指数不是整数时只能用pow。round是做四舍五入的,但要注意负数的行为:Math.round(-11.5)的结果是-11,不是-12,因为它是向正无穷方向取整。遇到负数取整需求,别直接套round,先想清楚题目要的是"四舍五入"还是"绝对值四舍五入再带符号"。这类边界问题在Math类题目里是最容易丢分的角落。
3.4 题14:Random生成指定范围随机数
这题一般出成:连续生成10个[1,100]之间的随机整数并输出。很多人第一反应是(int)(Math.random() * 100) + 1,这样确实能出结果,但如果题目特意写了"使用Random类",还是用正主:
Random r = new Random(); for (int i = 0; i < 10; i++) { System.out.println(r.nextInt(100) + 1); }new Random()不传种子时,默认用系统纳秒时间做种子,每次运行结果都不一样,满足"随机"的预期。nextInt(n)返回的是[0, n)之间的整数,所以想要[1,100]就写nextInt(100) + 1。容易错的地方是写成nextInt(101),然后以为自己拿到了1到101,实际上得到的是0到100。
我的经验是把它记成一个公式:要生成[a, b]区间的整数,用nextInt(b - a + 1) + a。不管题面怎么变,套这个公式绝对不会错。另外注意nextInt()不带参数时返回的是整个int范围的随机数,可能包含负数,除非题目明确要求,否则别这么用。
3.5 题15:日期时间处理
最后一题基本是日期。如果平台用的JDK是8及以上版本,强烈推荐用LocalDate,别再用Date加Calendar。比如题目要求:输入两个日期,计算它们之间相差多少天。
import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd"); LocalDate d1 = LocalDate.parse("2024-01-01", fmt); LocalDate d2 = LocalDate.parse("2024-12-31", fmt); long days = ChronoUnit.DAYS.between(d1, d2); System.out.println(days);LocalDate.parse默认就能解析yyyy-MM-dd这样的格式,所以中间的fmt其实可以省略。不过建议保留,因为题目可能会要求在输出时指定别的格式,比如2024年01月01日,那时候你就得自己ofPattern了。
日期这一块有三个细节必须注意。第一,格式化串里MM是月份,mm是分钟,写错一个字母解析就直接崩。第二,Calendar的月份从0开始,也就是Calendar.JANUARY == 0;而LocalDate的getMonthValue()从1开始,更符合日常习惯,这也正是我推荐LocalDate的核心理由。第三,ChronoUnit.DAYS.between返回的是long,如果两个日期差超过int范围,int会溢出,虽然OJ数据很少这么大,但习惯性用long更稳妥。
4. 上机实测最容易翻车的五个细节
4.1 字符串比较用==还是equals
这个问题老生常谈,但永远在重复发生。用==比较String比较的是引用地址,只有equals才比较内容。存在一个例外:字符串常量池。String a = "hello"; String b = "hello";时,a == b的结果是true,因为两个字面量指向同一个常量池对象;但如果其中一个是用new String("hello")创建的,结果就是false。
这种不一致最容易给初学者造成"==有时候能用"的错觉。我的建议是:写代码时一律用equals,别去赌那道例外。尤其在做OJ题时,你压根不知道测试数据是用什么方式构造的字符串,用equals是唯一稳的做法。这也是题9回文判断里最该记住的一条。
4.2 Character.isWhitespace与空格的差异
前面题8提过,Character.isWhitespace会把制表符、换行、回车、全角空格都算成空白。如果题目说"统计空格字符",标准答案很可能只认' '字符本身。为了不在这上面丢分,你可以先想清楚题目到底要的是什么:统计的是"空格键"还是"所有空白符"。
我个人的处理原则是:OJ题优先按字面意思来,题目说空格就只认' ',必要时再把\t单独考虑。宁可多判断几个值,也别用一个语义过宽的API,否则测试数据里混入一个全角空格,你的统计就会跟标准答案差了1个,最后怎么查都查不到原因。
4.3 nextInt和nextLine混用的坑
如果题目是先读一个整数n,再读n行字符串,你可能会写出这样的代码:
int n = sc.nextInt(); for (int i = 0; i < n; i++) { String s = sc.nextLine(); }然后就会发现第一行字符串永远读不到。原因是nextInt读取后会在缓冲区里留下一个换行符,随后的nextLine直接把这个换行符当字符串读走了。解决办法是在nextInt后面补一句sc.nextLine(),把残留的换行消费掉:
int n = sc.nextInt(); sc.nextLine(); for (int i = 0; i < n; i++) { String s = sc.nextLine(); }这个坑在字符串统计、正则校验这类题目里非常常见,一旦出现能卡你半小时。如果题目输入的是"数字和字符串交替出现",强烈建议全部用nextLine读取,再自己用Integer.parseInt转换数字,这样彻底避开混用问题。
4.4 随机数的区间边界
作业里偶尔会出现这种场景:题目说生成0到9的随机数,用nextInt(10)没问题;但有人说"生成0到10的随机数",随手写nextInt(10),完全没意识到少了边界。nextInt(n)取的是[0, n),永远到不了n本身。
还是那句话,遇到[a,b]区间,先算b - a + 1,再套nextInt(b - a + 1) + a,一秒钟定位正确答案。另外,如果题目要求每次运行时随机数序列完全一样,比如为了调试方便,可以给Random传一个固定种子,比如new Random(42),这样每次运行生成的都是同一组数,方便复现问题。但OJ最终评测用的是默认随机,所以提交前记得把种子去掉。
4.5 浮点数输出的格式问题
涉及Math类计算时,题目往往会要求保留若干位小数。用System.out.printf("%.2f", x)是正确做法。有人图省事直接用println输出原始double,结果3.141592653589793跟预期答案对不上,被判WA。
另一个容易忽略的是精度差异。题目如果明确说"π取3.14",那你就别用Math.PI;反过来,如果题目说"使用Math类计算",那用Math.PI才符合要求。两种写法在保留两位小数时可能结果一样,但在某些精度要求更高的题里会差最后几位。判断标准只有一个:题目有没有明说近似值。没说就用Math.PI,说了就按题面来。
5. 做题顺序与最终自查清单
5.1 我建议的做题顺序
如果这8道题你自己安排练习顺序,别按题号从8到15硬刷。我的习惯是先做String和包装类,再做Math和Random,最后碰正则和日期。理由是:字符统计、反转这些题能帮你热手,把对API的疏离感先消掉;正则和日期需要背的知识点最多,放到后面集中记忆,效果反而更好。
另外,题10的String与StringBuilder效率对比题,其实是"理解原理"型题目,它不像题8那样有标准输出可以对照,更像是小实验。这类题在OJ上通常只验证你输出了耗时数据和最终结果,逻辑比较简单,也可以提前做。但如果你想真正理解StringBuilder和String的区别,多做几遍题9的反转会更有帮助,因为reverse直接暴露了可变与不可变两种操作方式的差异。
5.2 提交前自查清单
每个题提交前,我都按下面这个清单过一遍。这个习惯帮我少交了很多次无效提交:
- 类名是否为
Main,public static void main(String[] args)签名是否完整 - 题目要求的输入方式是
nextLine还是next,有没有混用 - 是否打印了多余提示语,比如"请输入:"这种干扰OJ判断的语句
- 边界数据:空串、单字符、负数、最大值,逐一想一遍
- 输出格式:换行、逗号、冒号是否与输出样例完全一致
- 涉及数字转换时,是否catch了
NumberFormatException - 用了中文标点的地方是否全部改成英文标点,OJ输出比对的是Unicode码点,一个全角符号都不行
说回开头那句话,这组题真正拉开差距的不是会不会写代码,而是对API边界的把握是否到位。我当年第一次做类似的题,在nextInt和nextLine混用上卡了四十分钟,后来把这些坑一个一个记下来,才发现所谓"常用类"不过是一堆有脾气但可驯服的工具。如果你们学校的具体题面跟我提到的对不上,也别慌,知识点是通用的——把String的equals、StringBuilder的reverse、包装类的parse、正则的转义、LocalDate的格式化这几个核心点吃透,换汤不换药。