又到了《Java面向对象》第五章的作业时间。如果你正在刷“sdut-Java面向对象-05 类和对象(函数题:23-34题)”,大概率已经被题目框里那几段残缺代码折腾得有点上头:明明上课听懂了什么是类、什么是对象,真到了OJ上,却连一个构造方法都补不完整,要么报“找不到符号”,要么答案错误。这篇就是解决这个问题的。
我会围绕“类和对象”这个核心,把函数题 23-34 的命题逻辑、补全技巧和踩坑点完整拆一遍,适合三大人群:正在做这组作业的大学生、准备Java基础面试的求职者、以及刚学完封装但做题总WA的初学者。看完你不会成为理论大师,但至少能把这12道函数题稳定拿分。
1. 函数题真正的玩法:它不是让你写整个程序
1.1 先看懂题面给了你什么
函数题和编程题最大的区别在于:OJ已经帮你写好了主程序和一部分类结构,你只需要补上指定的方法、构造器或字段。
我见过很多第一次做函数题的同学,直接往答题框里扔了一整个public class Main,结果编译直接挂了。原因很简单:系统会把你的代码和它隐藏的主程序拼接在一起再编译,你重复定义了Main类,两个类撞在一起,不报错才怪。
所以拿到题的第一件事,不是开写,而是读:
- 题面开头通常有一段“已有代码”或“参考代码”,看清楚哪些方法已经有了签名,哪些只有注释没有实现。
- 注意题目说明,比如“请补全
Circle类的构造方法,使得主函数能正确创建对象”。这句话已经告诉你,要写的部分是构造方法,而不是getArea()。 - 把题面中的类名、方法名原样复制进答题框,哪怕你觉得它命名不规范也别改——OJ的测试程序是按名字反射调用的,名字错一个字母就是“找不到符号”。
1.2 补全类和补全方法的两种姿势
函数题有两种常见形态,应对方法不一样。
第一种是补全函数的题。题目给了一个类,里面有一个残缺的方法,比如:
class Calculator { public int add(int a, int b) { // 请在这里实现加法 } }这种最直接,你把return a + b;填上就行,不需要动其他部分。但要注意方法和变量作用域,别在方法内部随便声明一个同名局部变量把参数盖住。
第二种是补全整个类的题。题目只给你主函数代码,让你把Circle、Student、Date这类类整体补出来。这种题在23-34里更常见,需要你根据主函数的调用方式来反推类的结构。比如主函数里有new Circle(2.5),那就必须有一个带double参数的构造方法;如果你只写了一个无参构造器,等着你的就是“构造函数无法应用”。
1.3 23-34这批题目的常见骨架
如果你把这12道题铺开看,会发现命题人没什么新招,就是Classic三件套:
- 描述现实实体的类:人、学生、银行账户、图书、手机。
- 几何计算类:圆、矩形、三角形、线段。
- 日期时间类:日期合法性、天数差、具体星期几。
这类题的主函数无非是“创建对象 → 调用方法 → 输出结果”。所以你的类只要满足主函数调用链,基本就能拿分。
2. 这12道题命题者想考的四块骨头
2.1 类设计型:字段、构造、方法的默认约定
类设计型题目是最多的,比如“定义一个学生类,包含学号、姓名、成绩三个字段,要求能创建对象并输出信息”。这类题考的不是你会不会写代码,而是你有没有遵守Java的类设计约定。
一个完整的最小类长这样:
class Student { private String id; private String name; private double score; public Student(String id, String name, double score) { this.id = id; this.name = name; this.score = score; } public String getId() { return id; } public void setId(String id) { this.id = id; } // getName/setName、getScore/setScore 同理 public String toString() { return id + " " + name + " " + score; } }有几个注意点:
- 字段必须是
private,除非题面明确要求public。这是面向对象封装的基本要求,也是OJ判分时偶尔人工审题的部分。 - 构造方法名必须和类名完全一致,首字母大小写都不能差。
- 写了带参构造之后,无参构造不会自动存在。如果主函数里用了
new Student(),你又只写了new Student(...),会直接编不过。解决方案是自己再补一个无参构造。
2.2 封装型:为什么私有字段必须配 getter/setter
封装这个知识点,在函数题里最常见的考法是:题目把字段设成private,然后在主函数里通过getId()、setScore()这样的方法访问。如果你只定义了私有字段却没写公共方法,主函数一编译就是“id 在 Student 中是 private 访问控制”。
我的习惯是看到private字段,立刻成套写出 getter 和 setter,甭管题面有没有用到一半。因为有时候主函数第一版没用,测试点第二组数据却用了。反正也就几行代码,多写不扣分,少写必WA。
setter 里还有个容易翻车的点:方法不能有返回值。我记得有一次批改同学代码,他写的是public boolean setAge(int age),然后函数体内return true;。语法上没错,但主函数调用stu.setAge(18)的预期根本没人接收返回值,方法签名对不上就编译失败。setter 老老实实void,别发挥。
2.3 计算型类:几何和日期类的输出格式陷阱
几何题和日期题除了考类设计,还考数值计算和格式控制。
几何题常见的坑是这个:**圆周率用Math.PI,而不是自己写3.14。**你写3.14,前几个测试点可能对,一旦题目要求输出到小数点后两位,误差立刻暴露,结果WA。Math.PI精度足够,直接用。
日期题更像是一个边界条件收集器。判断日期合法性时,至少要考虑:
- 年份最小为1,月份在1到12之间。
- 日的上限要按月份区分,尤其二月——闰年29天,平年28天。
- 闰年判断标准:能被4整除但不能被100整除,或者能被400整除。
输出格式方面,很多题要求“保留两位小数”或“日期格式为yyyy-MM-dd”。推荐直接用String.format:
System.out.println(String.format("%.2f", area)); System.out.println(String.format("%04d-%02d-%02d", year, month, day));%04d这个写法能让月份和日期自动补零,比如2024-01-05,不会输出成2024-1-5。
2.4 对象数组与引用型:null和销毁问题
23-34后期一定有对象数组的题,通常是这样:读入n个学生信息,存入对象数组,然后按成绩排序或输出。这里最经典的错误是忘记给数组里的每个元素单独new。
Student[] students = new Student[3]; students[0].setName("张三"); // NullPointerException!new Student[3]只是创建了一个长度为3的引用数组,里面每个位置默认是null。你必须先循环:
for (int i = 0; i < n; i++) { students[i] = new Student(); }然后再调用方法。这个坑能卡住相当一部分人,而且报错信息还比较隐蔽,看起来很像是“数组越界”或者“对象没建”。
此外对象比较也有讲究。函数题里偶尔会出现“判断两个日期对象是否相同”之类的需求。如果你写date1 == date2,那比较的是引用地址,不是内容。大多数情况下题目希望你重写equals方法,或者逐个比较字段。我一般建议直接比较字段值,简单不易错。
3. 类和对象底层逻辑:老师不讲但你绕不开的几个点
3.1 成员变量默认值和初始化时机
函数题最常见的初始化戏法就是“故意不给你构造方法,让你猜成员变量的初值”。Java成员变量有默认值:整型是0,浮点型是0.0,布尔型是false,引用类型是null。
这意味着:
class Counter { private int count; // 没有写构造方法 }主函数直接new Counter()也能编译,count的最初值是0。有些题利用这点,让你补一个increment()方法,在原来的0基础上加1。你要是非得写个构造器把count初始化为1,恭喜,答案错。
局部变量则完全不同,没有默认值,不赋值直接用就会编译报错“可能尚未初始化”。我当时刷题时经常把这两者搞混,所以特意提醒一下:方法内部的变量,必须显式初始化。
3.2 this 到底是谁
this在函数题里的核心用途就两个:给成员变量传值、调用本类的其他构造器。
第一种最常用,因为构造函数参数名经常和字段名重名:
public void setName(String name) { this.name = name; }去掉this.,就变成了把参数赋给参数自己,成员变量什么都没变。这种Bug编译器不报错,因为它合法,但你的对象字段永远是null或0。
第二种用在构造器重载场景。假如题里既要支持new Student(),又要支持new Student("张三", 90),可以这样:
public Student() { this("无名", 0); } public Student(String name, double score) { this.name = name; this.score = score; }无参构造里调用this(...)必须写在第一行,这算语法硬性要求。它的作用是让无参构造复用带参构造的初始化逻辑,省得写两遍。
3.3 构造方法和普通方法的区别
函数题里很容易把构造方法写成普通方法,多写一个void或者写错了返回类型。构造方法不能显式声明返回值类型,连void都不能写。
public Student(String name) { // 这是构造方法,正确 this.name = name; } public void Student(String name) { // 这是普通方法,但名字和类名相同,非法! this.name = name; }第二种写法编译会直接提示“方法名冲突”,或者干脆被认为是“无效的方法声明”。我见过一位同学在补全构造器时多打了个空格,类名和方法名长得一样,OJ报错“非法开始表达式”,整整找了三十分钟。
3.4 对象的比较:== 和 equals 不可混用
这算是Java面向对象里非常经典的考点了,到了函数题里也会以“判断两个对象是否相等”的形式出现。基础类型用==比较值,对象类型用==比较的是引用,也就是两个变量是否指向同一块堆内存。
函数题里大多数情况是两个对象内容相同就算相等。最稳妥的实现方式是重写equals:
public boolean equals(Student other) { if (other == null) return false; return this.id.equals(other.id) && this.name.equals(other.name); }注意字符串比较也要用equals,不能==。这块是面试高频,也是现在刷题的高频,专门写在提醒清单里。
4. 本地模拟OJ测试的完整流程
4.1 把题目代码完整粘贴进本地IDE
函数题正确姿势从来不是直接在答题框里盲写。我刷23-34的时候是这样操作的:
- 把题面给出的主函数代码完整复制到本地IDEA一个
Main.java文件里。 - 根据主函数的调用,先写出类的基本骨架(字段+构造方法签名)。
- 在本地写完所有代码,自己跑一遍,确认输出正常。
- 提交时只复制“类定义部分”,不复制主函数类。
这个流程看起来多了一步,但能帮你拦住80%的编译错误。因为OJ的报错只有一行英文,你很难看出自己的类是不是写错了;但本地IDEA右下角红色波浪线直接告诉你哪一行出了问题。
4.2 自己构造测试输入,覆盖边界值
只跑题目样例远远不够。我自己的经验是,样例能过,往往只是运气好,OJ的测试点比你想象的狠:
- 如果题面说成绩是整数,你至少要测一次0分和一次100分。
- 如果日期类要求判断合法性,测一下2月29日(闰年和平年各一次)。
- 如果一个类设计了
setRadius,测一次传入负数,看你的方法有没有做出合理反应。
很多答案错误(WA)就是死在边界值上。比如月薪计算题,有人没处理“天数为0”,有人没处理“离职当月只按15天算”,这种测试点一打一个准。函数题不会明着告诉你需要处理边界,但题面如果有“如果输入不合法,返回-1”这类描述,千万别忽视。
4.3 提交前删掉所有调试输出
本地跑的时候,我习惯加System.out.println看中间变量,比如打印一下radius、打印一下score,这很正常。但如果提交前忘了删,OJ就会把你的调试输出当成答案的一部分,和期望输出匹配不上,必然WA。
这里有个经验:你可以把所有调试输出统一放在一个固定位置,提交前Ctrl+F搜一下println,看除了题目要求的输出以外还有没有多余的。我因为这种东西在别的题目上WA过至少两次,后来就养成了提交前强制搜索的习惯。
另外,函数题里你补全的方法一般不需要自己println,只需要return一个值。主函数会负责打印。很多同学习惯性地在方法里System.out.println(...),结果输出格式错乱,直接答案错误。
5. 提交之后常见报错全排查
5.1 编译错误:找不到符号、构造器冲突、类重复
在OJ上编译错误(CE)是最让人烦的,因为报错信息还简略。我总结出三个最常出现的编译错误及对应解法:
| 报错片段 | 原因 | 解法 |
|---|---|---|
cannot find symbol | 类名或方法名拼写错误,字段不在你写的类里 | 逐字对照题面代码,尤其注意大小写 |
constructor X cannot be applied to given types | 主函数要求的构造器参数和你的不一致 | 检查构造器参数类型和数量,必要时补全无参构造 |
class Main is already defined | 你多写了一个Main类 | 删除自己的主类,只提交类定义 |
cannot find symbol这个错误我见得最多,一半以上是因为写错单词,比如getArae()这种手误。所以如果你看到这个报错,先不要怀疑玄学,回去看看题面方法签名是不是多了一个字母。
5.2 运行错误:NullPointerException 和数组越界
函数题里的运行错误(RE)主要就两种。
NullPointerException最常见的来源有两个:一是对象数组里的元素没有new,前面已经说过;二是你返回或访问了一个null对象的字段。比如题面让你“根据学号查询学生”,如果查不到,可能要求你返回null,而主函数拿到null后调用了.getName(),就炸了。
数组越界则多发生在日期题里。比如你想把月份对应的天数存到一个数组里:
int[] days = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};如果你直接用days[month],而month从1开始编号,1月会取出31,但12月会越界。正确做法是days[month - 1],或者在访问前先判断month的范围。
5.3 答案错误:精度、格式、多余输出
答案错误(WA)是函数题的另一种主旋律,而且往往最憋屈,因为代码能跑,只是输出某个字符不对。
最隐蔽的是精度问题。几何题要求保留两位小数,你用Math.round(area * 100) / 100.0,看起来好像“更准确”,但OJ期望的可能是printf("%.2f")的四舍五入规则。两者在逢5进位时有时表现不一致。我建议纯输出类问题一律用printf或String.format,不要自己手写四舍五入逻辑。
多余输出我已经在前面说过,这里再补一个细节:末尾换行。有些题目明确要求每个结果占一行,主函数可能已经输出了println,你补全的方法里就千万别再加换行了。多个\n看似不影响阅读,但OJ的判题是逐字符比对的,多出来的一个换行符就是WA。
6. 刷完23-34之后你该有的“类设计直觉”
6.1 一个类的健康检查标准
连续刷完这12道题后,你看到一道函数题,应该能在30秒内做出一个判断:题目需要的是一个纯数据类(只有字段和getter/setter),还是一个行为类(字段+计算逻辑),还是一个工具类(全是静态方法)。
判断方法很简单:看主函数怎么用。
如果主函数创建对象后只调用get/set,那就是纯数据类。如果创建对象后调用了getArea()、getTotalSalary(),那就是行为类,需要把计算逻辑封装进类里。
我建议你在提交前按这个清单自查:
- 类名和文件名是否一致,类名首字母是否大写。
- 所有字段是否符合题面要求的访问修饰符。
- 带参构造器是否覆盖了主函数会调用的所有参数组合。
- 需要
toString或equals的地方有没有实现,实现时输出格式是否和样例一致。 - 方法内部有没有
println,如果只需要返回值就删掉。
6.2 做题顺序建议和现场检查节奏
如果23-34对你来说是第一次接触函数题,我的建议是别按题号顺序刷,按类型刷:
- 先做最简单的“学生类”“员工类”这类纯数据类题目,把类、构造、封装的流程跑通。
- 再做“圆类”“矩形类”这种带计算逻辑的题目,重点解决输出精度和计算式。
- 最后做日期类和对象数组题,这种综合性最强,需要在边界条件和引用对象上多花心思。
按难度递进的最大好处是,你不会在前几题就被打击到失去兴趣。很多同学做这类作业,往往是第一题卡了很久,后面就放弃了,实际上第一题卡不是因为难,而是因为不熟悉函数题的“答题姿势”。一旦姿势对了,后面都是一路畅通。
6.3 补全类时先写出方法签名再填内容
这里分享一个我实际写题时的小技巧:先把所有方法签名空着填好,再逐行写函数体。
比如说题目要你写一个Circle类,主函数里有这几行:
Circle c = new Circle(2.5); double area = c.getArea(); double perimeter = c.getPerimeter();你先在类里写:
class Circle { private double radius; public Circle(double r) { } public double getArea() { } public double getPerimeter() { } }这个时候编译虽然报错(方法体没返回),但你已经搭好了框架,剩下的工作只是往每个方法里填计算式子。这样做比一边想字段一边想构造一边想计算公式要快得多,也更容易检查接口是否满足主函数调用。
6.4 刷完这套题后,你其实已经学会了什么
这12道函数题做完,你可能觉得:“这不就是写一堆模板类吗?”但从面试角度来看,这些题目恰恰覆盖了Java面向对象最重要的基本功:封装思想、构造器重载、this的使用、对象数组管理、引用传递和边界处理。
我做Java这么多年,面试新人时最怕的不是他不会Spring、不会MyBatis,而是他写一个最基础的Student类都漏getter、setter,或者在equals里写出==。23-34这组题就像是提前把这些坑都踩了一遍,刷完后再写任何实体类,肌肉记忆都会逼你把字段设成private、构造器补全、明明方法签名确认一遍。
最后再说一件我自己经历过的事:当年刷这种函数题,我有一道题怎么都WA,本地测了七八组数据全都正常,提交上去怎么都是“答案错误”。后来才发现,我为了调试方便,在getArea()里顺手写了一句System.out.println("radius = " + radius),忘了删。交叉看一遍,这段调试代码赫然躺在答题框里。删掉重交,一次过。
所以,如果这一类题再卡你,请先怀疑这些“小事”:多余的输出、多一个换行、少一个return、写成了==。把小事先排干净,再去纠结算法和逻辑。函数题考的就是面向对象的基础素养,这些素养就藏在这些“小事”里。