每次刷SDUT的Java面向对象题目,走到“05 类和对象”这一节的函数题(题号23-34)时,很多人都会卡一阵子。这套题的形态很特别:评测系统已经把主方法或者调用方代码写死了,学生要做的不是从零搭一个完整程序,而是把题目指定的类、方法、构造器补齐。换句话说,系统考的不是你能不能写出整个程序,而是你明不明白“类和对象”这套语法到底怎么组织、怎么协作。
题号23到34正好处在“类和对象”专题的中间偏后位置,知识点高度集中:类的属性设计、构造方法的重载、private封装与setter/getter、格式化输出、以及简单的对象状态管理。没有文件读写,没有复杂算法,没有输入输出解析,考的就是你对“类的定义—对象的创建—方法的调用”这个链条的肌肉记忆。
这篇文章适合三类人看:正在SDUT OJ上刷题、被某道函数题反复折磨的在校生;自学Java想找一套标准化基础练习的初学者;准备面试前想快速把面向对象基础概念落实到手写代码层面的人。下面我把这套题的考察逻辑、核心知识点、通用解题模板以及排错经验一次讲透。
1. 这个题集到底考什么:先看清SDUT的类和对象函数题
1.1 函数题和编程题的区别,为什么OJ偏爱函数题
SDUT这类课程平台的Java题目通常分成两种:完整编程题和函数题。编程题需要你自己写import、定义主类、处理输入输出、组织整个流程;而函数题完全不同,系统给你一个固定的运行框架,比如main方法已经调用了new Student("张三", 20)和stu.getName(),你只需要补全Student这个类,让它能编译、能跑、能返回预期结果。
这种出题方式对评分系统非常友好。评测机只需要准备固定的测试数据,调用你实现的类或方法,比对返回值或者输出内容即可。对出题人来说,函数题能精准锁定某个知识点的掌握程度,避免了“学生用一堆奇技淫巧绕过知识点的检测”。举一个最直白的例子:如果考的是“构造方法能否正确初始化对象”,编程题里学生可以写一个巨大的main方法把所有逻辑堆进去,考察点很容易被稀释;但函数题里你只能补构造器,系统一调用,行不行立刻见分晓。
从学生的角度看,函数题其实更贴近真实工程里的工作方式。工作中很少有人让你从零写一个完整系统,更多时候是“这个模块的接口已经定好了,你照着契约把实现写了”。类和对象的函数题,本质上就是在训练“按接口实现功能”的能力。你写好的类,交给谁调用、怎么调用,你都得预先想清楚。我经常跟学弟学妹说一句不太好听但很真实的话:函数题代码看起来短,但它的容错率比完整编程题更低。编程题你还能用正确性掩盖一部分坏味道,函数题只要签名对不上、格式差一个空格、输出多一个换行,评测机直接判错。
1.2 23-34这12道题的知识点覆盖范围
以我刷同系列题目的经验来看,23-34这12道题大体遵循一个循序渐进的梯度,而不是随机打乱考点。放在“类和对象”这个专题的中间段,意味着基础属性与简单方法已经考过了,后面还没进入继承和多态,所以这组题的重点一定是“把类写得完整、标准、可用”。
常见的覆盖范围包括:
- 无参构造器、带参构造器以及构造器重载。
- 私有属性的封装:private + getter/setter,有的题目还会要求setter里做合法性校验。
- toString方法的重写,尤其是自定义输出格式。
- 简单业务方法:比如日期加一天、坐标移动、计算圆的面积、复数加减,这类题目往往会把操作细节和格式化输出放在一起考。
- 对象数组的创建与遍历,以及对数组元素调用方法。
题号段和考点的对应关系,我是这样推断的:23-26题很可能集中在类的定义和构造器调用上,重点考察“对象能不能被正确创建”;27-29题大概率进入封装阶段,看看你会不会用getter/setter保护属性;30-32题开始有业务方法,比如日期、坐标、面积这类带计算和格式化的东西;33-34题则是综合一点的状态管理,比如对象数组或者多个对象的交互。
这个推断不一定和实际题号完全一致,但覆盖的知识点范围八九不离十。理解这个梯度之后,你的刷题策略就清晰了:先保证类的骨架正确,再处理格式细节,最后才去抠边界条件。
2. 动手前的知识基石:类和对象避不开的几个关键细节
2.1 类的成员设计:属性、方法、构造器各司其职
在函数题里,一个类通常由三部分构成:属性描述状态,方法描述行为,构造器负责初始化。很多第一次接触函数题的人犯的错误是“想太多”,比如题目只需要一个getArea()方法,他非要在类里额外写一堆辅助方法;也有人“想太少”,连属性都忘了声明,直接在方法里用局部变量糊弄。
怎么判断一个类需要哪些成员?不要靠猜,去看main方法或者题目给出的调用代码。如果系统写的是Circle c = new Circle(3.5);,那你就必须提供一个double或int参数的构造器;如果随后写了c.getArea(),那你就必须提供一个名为getArea的无参方法。函数题里最重要的规则是:调用代码就是需求文档,它调什么,你实现什么;它没调,你可以不用写,写了反而可能踩命名冲突的坑。
属性的设计也要慎重。如果题目里明确说了“圆的半径radius是私有属性”,那你就不能写成public double radius;。如果题目没有明确说明属性可见性,最常见的做法是把属性设为private,然后通过public方法暴露访问入口。这既符合面向对象封装的要求,也避免了评测中的隐藏测试点因为你改了可见性而出现诡异错误。方法实现上,一个方法只做一件事,不要在getArea()里顺手输出一堆调试内容,输出多余的东西在OJ里就是白送一个错误答案。
2.2 封装不是摆设:private加getter/setter的真正原因
很多初学者不理解,为什么一个属性要搞得这么麻烦:明明stu.age = 20一行能解决,非要写stu.setAge(20)。类与对象函数题里,封装恰恰是高频考点,而且一旦考到,必然藏坑。
封装的本质是“把数据的访问权收回来,由类的内部决定数据怎么变”。举例:一个学生类的年龄属性,如果不加控制,外部代码可以写stu.age = -100,程序照样运行,但明显不合理。如果你用private属性加setter,就可以在setter里做校验:如果传入的值小于0,就设为0,或者在无法处理时抛出异常。测试代码往往就等在这个地方,传一个-1或者200进去,看看你的类会不会正确处理。
封装还有一个工程层面的理由:隐藏内部实现。外部代码调用getName(),它不需要关心你的name是存在String里,还是用字符数组拼接出来的;只要你返回的格式稳定,你内部怎么改都不影响调用方。我在实际刷题时发现,凡是涉及封装的函数题,只要我在setter里加了边界处理,即使题目没有明说,也能多过几个隐藏测试点。这不只是应试技巧,也是面试时回答“面向对象三大特性之一封装”的最佳实战素材。
2.3 构造方法的默认值和重载规律
构造方法的坑是函数题的重灾区。一个最经典的问题:你在类里写了带参构造器public Person(String name),然后主方法里调用new Person(),系统直接报编译错误“找不到符号 Person()”。原因很简单:Java规定,如果类里没有写任何构造器,系统会提供一个无参构造器;但只要你写了任意一个有参构造器,这个默认的无参构造器就消失了。你需要无参构造时,必须自己显式写出来。
构造方法的重载在函数题里也很常见。重载的规则一句话:方法名相同,参数列表不同。参数列表不同可以是类型不同、数量不同、顺序不同。在类与对象作业里,最常见的重载组合是“无参构造器 + 全参数构造器”。无参构造器把属性设为默认值,全参数构造器一次性完成初始化。还有一个小技巧:如果你想让代码更简洁,可以在一个构造器里用this(...)调用另一个构造器,但是this(...)必须写在构造器方法体的第一行,否则编译报错。这个细节很多教材一笔带过,但函数题里只要你用了,写错位置编译器会非常不给面子。
3. 函数题的标准解法和实操模板
3.1 学生类考核模板:拿到题目先拆三个部分
以最典型的学生类(或者叫人名类)来演示。测试代码一般长这样:
public class Main { public static void main(String[] args) { Person p = new Person("张三", 18); p.setAge(20); System.out.println(p.toString()); } }看到这种题目,我建议你按三步走。第一步,读调用代码,数一下调用方使用了你哪些方法。上面这段代码用了带两个参数的构造器、setAge、toString,那你的类里就至少要有这三个成员。第二步,确定属性。既然有setAge,必然有age属性;既然构造器接收name和age,必然有name属性。第三步,严格对齐方法签名。setAge的参数是int,返回值是void,你不能写个setAge()无参版本,也不能返回boolean。
参考实现框架:
class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public void setAge(int age) { this.age = age; } public int getAge() { return age; } @Override public String toString() { return String.format("姓名:%s 年龄:%d", name, age); } }这段代码里有一个非常容易被忽略的点:toString()里面的输出格式,必须和题目样例完全一致。姓名:张三 年龄:18和姓名:张三年龄:18在OJ眼里是两个结果。我一般先把样例输出复制到注释里,然后一个字符一个字符地对。用String.format比用字符串拼接更安全,至少空格不会漏。
3.2 日期类:格式化输出和先自增还是先返回
日期类是类和对象函数题里的常客,考点主要分布在两个地方:日期加一天的进位逻辑,以及格式化输出。
日期加一天看着简单,实际上全是边界。以年月日三个私有属性为例,核心逻辑是:day先加1,然后判断这个day是否超过了当前月份的最大天数。如果超过了,day归1,month加1;如果month跟着超过了12,month归1,year加1。每个月的最大天数不是固定的,1、3、5、7、8、10、12月是31天,4、6、9、11月是30天,2月还要单独判断闰年。闰年判断规则是:能被4整除但不能被100整除,或者能被400整除。
格式化输出这里,大多数人栽在补零上。题目要求输出2025-03-05,结果你输出2025-3-5,答案错误。正确做法是用System.out.printf("%04d-%02d-%02d%n", year, month, day);。%04d表示至少输出4位数字,不够前面补0;%02d同理,保证月和日是两位数。我建议这种输出统一用printf或者String.format,不要自己写if(month<10) str += "0",又啰嗦又容易错。
还有一个细节:如果日期类提供了类似nextDay()的方法,你要先确认题目的方法是返回新的日期对象,还是直接修改当前对象。从调用方式就能判断:Date d2 = d1.nextDay();说明返回新对象,原对象不能变;d1.nextDay();后面继续用d1,说明是原地修改。这个区别不搞清楚,后面所有断言都会炸。
3.3 几何类和复数类:精度与结果展示的经验
几何类(圆、矩形)在函数题里主要考两个点:常量的使用和结果保留小数位。计算圆面积时,如果有人图省事自己写3.14,那大概率要掉进精度陷阱。题目预期用Math.PI,你手写3.14,结果在小数点后第二位就开始分叉,OJ判题一比对就错。正确写法是Math.PI * radius * radius。
保留小数位也有讲究。如果题目说“结果保留两位小数”,就用System.out.printf("%.2f", area);,或者提前用String.format("%.2f", area)构造输出。千万不要直接System.out.println(area),double有时候会输出一长串小数,甚至科学计数法。
复数类的重点则在于实部虚部的运算和符号处理。复数加法是实部加实部、虚部加虚部;减法是实部减实部、虚部减虚部。输出格式的坑比较隐蔽:虚部为正输出3+2i,虚部为负应该输出3-2i,你要是写成3+-2i,结果答案错误。这种题我在本地IDE上经常测不出来,因为自己看习惯了,但OJ是逐字符比对的,一个小加号就是天壤之别。
矩形类或者坐标类可能会考“移动”操作。判断要不要返回新对象,和日期类的逻辑一模一样,看调用链。有些题目还顺带考对象数组:创建若干个矩形对象放进数组,然后遍历算总面积。这种题的循环体里很容易写错对象下标,多调试几次就好。
4. 提交后常见的报错和排错手记
4.1 编译错误:类名、方法签名、拼写问题
函数题的编译错误是有规律可循的,我见过最多的三类:找不到符号、构造器不匹配、公共类声明冲突。
“找不到符号”八成是方法名或属性名拼写不一致。Java是大小写敏感的,getname和getName不是同一个方法。还有可能是你写了getAge(),调用代码写的是age(),签名对不上。解决办法很简单:把题目代码里的方法名列一个清单,逐个对着写,不要凭感觉。
构造器不匹配前面已经说过,最常见情况是写了带参构造器导致无参构造器消失。如果你报错信息里有“无法将类中的构造器应用于给定类型”,先把构造器清单列出来,看看调用方使用的参数个数、类型和你写的一不一样。
公共类声明冲突也很典型。Java规定一个源文件里只能有一个public类,而且这个类的名字要和文件名一致。函数题提交的场景里,一般评测系统会生成一个Main.java去调用你提交的类,所以你自己定义的类通常不能加public修饰。如果你非要在类前面加个public,评测系统大概率直接编译失败。记住这句话:函数题里,自定义类保持默认可见性,除非题目明确让你提交的类就是入口类。
编译错误的排查可以用下面这个表格快速定位:
| 报错常见关键词 | 可能原因 | 解决动作 |
|---|---|---|
| 找不到符号 | 方法名/变量名拼写不一致 | 对照调用代码逐字检查 |
| 构造器不匹配 | 缺少对应参数的构造器 | 检查无参构造器是否存在 |
| 类X是公共的,应在名为X.java的文件中声明 | 多public类或类名与文件名不符 | 去掉自定义类的public |
| 需要返回int类型 | 方法签名返回值写错 | 对照方法声明看返回值 |
4.2 答案错误:输出格式、换行、空格、保留位数
排除编译问题后,最磨人的就是答案错误(Wrong Answer)。这类错误说明程序能跑,但评测机不认你的输出结果。绝大多数函数题的答案错误,都出在三个地方。
第一是输出格式差一个字符。OJ比对字符串是逐字符的,多加一个空格、少写一个冒号、英文冒号写成中文冒号,都是错。甚至有题目样例里用的是全角符号,你对着写的时候没注意,肉眼看不出来,判题机一看就是不一样的码值。我的习惯是:样例输出先复制到文本编辑器,把自己代码的输出也跑一遍,然后放在一起做逐字符比对。
第二是对象输出的问题。如果你System.out.println(p),而这个类没有重写toString,那输出的是类似Person@1b6d3586的哈希码,这种答案铁定错。只要题目里要求输出对象的描述信息,就老老实实重写toString。注意方法签名必须是public String toString(),并且加上@Override注解可以帮你提前检查签名错误。
第三是保留小数的格式。有的题要求5.20,你输出5.2,错;有的题要求四舍五入保留两位,你直接截断,还是错。用String.format("%.2f", value)是四舍五入的,基本都符合OJ预期。但如果你用DecimalFormat,它的默认舍入方式在某些场景下可能和预期不一致,建议能用printf解决就不换工具。
4.3 本地能跑OJ报错的典型差异
“我在IDEA里运行好好的,一提交就错”是刷题社区里出现频率最高的求助。这里面的原因通常不是题目抽风,而是本地环境与判题环境之间的几个差异。
第一个差异是包名。本地学习时有人习惯在代码开头写package com.example;,提交函数题之前一定要删掉。OJ会把自己的测试类和你提交的代码放在同一个默认包下,如果你写了package,它连你的类都找不到。
第二个差异是编码。Windows下记事本默认GBK,OJ判题环境一般是UTF-8。如果代码里包含中文注释或者中文字符串,编码不一致可能导致乱码,甚至编译错误。解决办法:编辑器统一用UTF-8编码保存,IDE也可以设置成UTF-8。
第三个差异是JDK版本。如果你的本地环境是Java 21,你可能会用var或者record这类新语法;但OJ的判题环境可能还停留在Java 8或者Java 11,编译直接报错。稳妥的做法是只用基础语法:声明类型老老实实写全,不要用太新的语言特性。
第四个差异是多余的输出。本地调试时,你可能会在代码里到处写System.out.println("debug: " + value),提交前忘了删。本地看到这些输出只觉得烦,OJ会把这些调试信息全部算进标准输出,结果全错。提交前最后一遍检查:所有非题目要求输出内容的print语句全部清除。
排查这类问题,最有效的办法是先做减法:把代码整理成最小可以提交的状态,去掉所有调试输出、去掉包名、去掉不必要的import,然后重新读一遍调用代码,确认类和方法的拼写无一例外。函数题的代码量本来就不大,花三分钟做减法,比反复盲交十次都管用。
我个人刷完这套题之后的体会是:类与对象的函数题并不考智商,它考的是细心和对基础语法的虔诚程度。23到34这十二道题,每一道都在提醒你同一件事——类的实现不是写给自己看的,是写给调用方用的。你必须在动手前想清楚别人会怎么new你、怎么调你、怎么输出你。能把这一组题一遍刷过的人,后面学继承、多态、接口的时候会轻松很多。最后再分享一个小习惯:每次写完一个类,自己额外构造几个边界对象测一下,比如年龄传-1、日期传2月30号、半径为0,很多隐藏的扣分点都是这样自己试出来的。