SDUT-Java面向对象-05,标题里写着“类和对象(函数题:1-11题)”,你是不是也正在面对这11道题?作为一个在OJ平台上带过不少学生刷题的人,我太清楚这种题卡人卡在哪了:不是题本身有多难,而是很多人压根没弄明白“函数题”的玩法。它和那种让你从零写一个完整程序的编程题不一样:题目会先把main函数或者一部分测试代码摆在你面前,你只需要按约定好的接口,把缺失的类、字段、方法补出来。看起来是在填空,实际上考的是你能不能严格按照调用方的要求设计一个类。这篇文章,我就把这11道题涉及的类和对象知识,配合我踩过的坑、总结的套路,一次性讲清楚。
1. 先搞懂“函数题”到底在考什么
1.1 函数题的提交规则,和你想的不太一样
很多第一次接触函数题的人都会犯一个同样的问题:把题目里给出的完整代码原封不动复制粘贴,甚至连public class Main都一起交上去。结果OJ直接给你一个编译错误:类名冲突。原因很简单,函数题的评测机制是把“题目自带框架”和“你提交的代码片段”合并成一个文件再编译,而框架里已经有Main类了,你再提交一个Main,编译器当然不干了。
正确的做法是先看清楚题目要求提交什么。一般题目描述里会有“注意:只提交class Xxx的代码”或者“请勿提交main函数”之类的提示。做题时,我习惯把“裁判测试程序样例”复制到本地IDE,把它当成一个完整项目跑起来,等验证通过之后,再把属于自己写的类单独复制出来提交。这个流程虽然多了一步复制粘贴,但能避开八成以上的低级提交错误。
1.2 为什么出题人偏爱函数题
函数题和编程题最大的区别在于:编程题让你掌控全过程,从输入到输出全是你的代码;函数题则只让你实现一个“零件”,零件之外的调度逻辑早就写好了。这种设计有几个明显的好处。
第一,评测稳定。题目方把输入输出逻辑锁死在框架里,你只需要保证类本身的正确性,不容易出现“整体逻辑对但输出格式飘了”的争议。第二,考查知识点更聚焦。类和对象这一章的重点就是类的定义、构造方法、封装、对象使用,如果让考生写完整程序,很容易被Scanner、循环这些边角料分散注意力,函数题刚好把火力集中在核心概念上。第三,贴合真实开发场景。你在公司里写代码,极少从零开发整个系统,更多是“别人定义好了接口,你来实现”。函数题就是在模拟这种协作关系。
1.3 拿到函数题,第一步永远是“读框架”,不是“写类”
我见过太多同学看到题目里有个“定义一个矩形类”就兴奋地埋头写,写了二十行,一提交全错。为什么?因为题目框架里调用的是new Rectangle(4.5, 7.8),但你把构造方法写成了Rectangle(double length, double width),参数名无所谓,可你如果把构造方法签名中的参数类型或数量写错,框架根本调不到你的构造方法。
所以拿到任何一道函数题,正确操作顺序是:
- 先找到“裁判测试程序样例”这段代码,一字一句读完。
- 从main方法的调用语句反推:需要定义哪些类?构造方法接收什么参数?方法名是什么?方法返回什么类型?
- 把这些“接口信息”用注释写在代码里,然后再填充方法体。
这个习惯看起来简单,实战中能救命。方法签名只要错一个字母,整个程序直接编译失败,而且报错信息经常指向main方法内部,新手根本联想不到是自己定义的类出了问题。
2. 11道题背后的知识地图:从“定义类”到“对象数组”
2.1 前几题:类骨架、成员变量、无参构造
这套题从1到11并不是随便排的,出题人有非常明确的递进设计。前面几道题通常只要求你定义一个最简单的类,字段类型可能是double、int、String,再配一个无参构造方法和几个基础的getter/setter。这部分只要把握住三点就不会丢分:类名和题目要求完全一致、字段名字不能随意改、getter/setter命名遵循驼峰规则。
这里有个新手最容易忽略的细节:无参构造方法里的默认值。题目如果要求“创建一个对象,不传参数时默认宽度为1.0”,很多人会想当然地在定义字段时写private double width = 1.0,这样做虽然也能得到正确结果,但出题人真正想考的是你知不知道在无参构造方法里给字段赋初始值。两种写法结果看似一样,背后的设计思路差距很大。建议养成“默认值在构造方法里设置”的习惯,因为字段初始化语句只负责声明,而构造方法才是对象创建时真正执行逻辑的地方。
2.2 中间几题:构造方法重载与this关键字
大概从第4道题开始,难度会上一个台阶。题目会要求一个类同时具备无参构造和有参构造,这时候就涉及两个高频考点:构造方法重载和this关键字。
先记住一个铁律:构造方法绝对不能写void。public Rectangle(double w, double h)是构造方法,public void Rectangle(double w, double h)只是一个普通方法。你一旦写了void,new Rectangle(...)时就找不到对应的构造方法,编译直接报错。这个错误出现的频率极高,很多人半天找不到原因。
this有两种用法,单靠概念记忆很容易混。第一种是this.成员变量,用来区分形参和成员变量,比如this.width = width,左边的width是对象的字段,右边的width是方法参数,没有this的话,width = width只是自己给自己赋值,对象的字段永远是0。第二种是this(...),用于在另一个构造方法里调用本类其他构造方法,但这条调用语句必须放在构造方法的第一行,而且不能写成一个循环调用,比如无参构造调用有参构造、有参构造又调用无参构造,这种死循环代码编译都不会让你过。
2.3 后面几题:封装、static与对象数组的组合拳
到了第7到第11题,知识点的密度就上来了。你会看到题目把封装、static、对象数组、toString这几个点串在一起考。这类综合题表面上代码量不大,实际上每一步都在挖坑。
封装这一步,要求你把所有字段改成private,对外只暴露getter和setter。注意setter不只是“把一个值塞进去”,更准确地说是一个“检查后才赋值”的入口。比如题目要求年龄不能为负数,setter里就应该做一次判断,非法数据直接忽略或者置为0。
static这个关键字,很多人知道它是“静态的”,但不知道它和对象的关系。static成员属于类本身,就算一个对象都不创建也能访问。反过来,static方法内部不能直接访问非static的成员变量,因为static方法执行时可能根本没有对象存在,你让它去访问“某个对象的字段”,它连对象都找不到,所以编译期就会报“无法从静态上下文引用非静态变量”。这个坑在函数题里出现得很多,因为框架的main方法就是static的,如果你在类里写了static方法又去访问普通字段,很容易触发这个错误。
对象数组是另一个经典坑点。Student[] arr = new Student[3]这句话的本质是“创建一个能存放3个Student引用的数组容器”,而不是“创建3个Student对象”。数组创建完毕后,三个位置全是null,如果你直接调用arr[0].getName(),运行时会抛NullPointerException。这个异常信息只有短短一行,对新手极不友好。解决办法就是每次使用前先arr[i] = new Student(...),或者在声明的时候直接用花括号初始化。
2.4 用“图纸和房子”理解类和对象
聊点抽象的东西。类和对象这两个概念,用“图纸和房子”来想最省力:类就是图纸,规定了房子有几个房间、门窗位置、屋顶坡度;对象就是按图纸盖出来的具体房子。你手里有一张图纸,想盖多少栋房子就盖多少栋,每栋房子都是独立的,住的人可以不同,水电表读数也不同,这就是“同一个类可以创建多个不同对象”的含义。
Java在内存中也是这样工作的。new关键字负责按“图纸”造房子,造好的房子放在堆内存里,而代码中的引用变量,比如Rectangle rect,保存的只是房子的地址,不是房子本身。理解了这一点,后面学到引用传递、equals方法、深拷贝浅拷贝,都是水到渠成的事。
3. 核心代码怎么写:从class声明到方法覆写
3.1 一个可以直接抄的“标准类模板”
刷了这么多函数题,我发现绝大多数题目都能套用一个标准类模板。拿矩形类举例:
class Rectangle { private double width; private double height; public Rectangle() { this(1.0, 1.0); } public Rectangle(double width, double height) { this.width = width; this.height = height; } public double getWidth() { return width; } public void setWidth(double width) { this.width = width; } public double getHeight() { return height; } public void setHeight(double height) { this.height = height; } public double getArea() { return width * height; } }这个模板的核心思路是:字段全部private,构造方法提供重载,无参构造复用有参构造,对外提供getter/setter,业务方法单独写。根据题目要求,你只需要替换类名、字段名和方法逻辑即可。需要注意,如果你只是在测试程序里读取字段,有些getter可以不写,但为了安全,最好把题目框架里调用过的方法全部实现出来,一个都不能少。
3.2 方法重载:同一名字,不同参数表
Java允许你在同一个类里定义多个同名方法,只要参数列表不同,这叫方法重载。构造方法重载是重载的一种特殊情况。这里必须强调一个常见误区:返回值类型不能作为区分重载的依据。public int getValue()和public double getValue()绝对不能共存,编译器会认为你重复定义了相同的方法。只有参数个数、顺序、类型不同才算重载。
实际做题时,方法重载最常见的用途是给使用者提供几种不同的初始化方式。比如学生类可以提供一个Student()无参构造,再提供一个Student(String name, int age)带参构造,前者调用后者并传入默认值。这样写出来的代码,调用方用起来非常舒服,这正是面向对象“对外友好”的体现。
3.3 toString和equals:建议主动覆写
几乎每一套类和对象题里都会遇到输出对象信息的需求。很多人直接写一个show()方法在类里打印,这本身没错,但当测试框架用了System.out.println(obj)这种写法时,你就必须知道println会自动调用对象的toString()方法。
Object类自带的toString返回的是“类名@十六进制哈希码”,比如Rectangle@6bc168e5,这显然不是题目想要的输出格式。正确做法是在类中覆写toString:
@Override public String toString() { return String.format("width:%.1f,height:%.1f", width, height); }覆写时注意两点:方法签名必须是public String toString(),前面的@Override注解可以写可以省略,但建议写上,能帮编译器检查签名。另外,toString方法里不要直接println,而是返回拼接好的字符串,把打印的职责交给调用方。
equals方法同理。Object的equals比较的是引用地址,不是内容。如果题目需要判断两个对象是否相同,比如“判断两个点是否重合”,就必须覆写equals。但注意,有些题目并没有这个需求,别画蛇添足写一个不必要的equals增加出错概率。看清楚main方法里有没有用equals或者==去比较你自己写的对象,再决定动不动手。
3.4 对象数组:完整的三步走写法
对象数组是类与对象章节的压轴考点,我建议严格按照三步来写,少一步都会出问题:
// 第一步:创建数组容器 Student[] students = new Student[3]; // 第二步:为每个数组元素创建对象 students[0] = new Student("张三", 20); students[1] = new Student("李四", 21); students[2] = new Student("王五", 19); // 第三步:遍历访问 for (Student s : students) { System.out.println(s.toString()); }第二步就是新手最容易漏掉的。在学基本类型数组时,int[] arr = new int[3]之后数组元素是0,可以直接用;但对象数组不一样,里面的每个元素都是引用类型的默认值null。只有new出来对象并赋值给元素之后,这个位置才真正有东西可访问。
有一种更简洁的写法是用花括号初始化:
Student[] students = { new Student("张三", 20), new Student("李四", 21) };这种写法在功能上完全等价,而且代码更短。如果题目规定了数组长度和初始值,直接套这种写法很方便。
4. 提交代码前的自查清单与典型报错
4.1 十连坑对照表:编译错误速查
我在辅导过程中整理了函数题里出现频率最高的十个错误,做成一张速查表,遇到报错直接对照:
| 报错现象 | 根本原因 | 解决办法 |
|---|---|---|
| 找不到符号: class Rectangle | 类名拼写或大小写不一致 | 对照框架里的new语句,逐字检查类名 |
| 找不到符号: 方法 getarea() | 方法名大小写/拼写错误 | Java方法名区分大小写,getArea不等于getarea |
| Rectangle() 已经在类中定义 | 构造方法误加了void | 构造方法不能有任何返回类型,去掉void |
| 需要'.'而不是'(' | this()写在了构造方法非第一行 | 把this(...)移到构造方法第一句 |
| 无法从静态上下文引用非静态变量 | static方法里直接访问了成员变量 | 将字段改为static,或把方法改为非static |
| NullPointerException | 对象数组元素未逐个new | 先初始化数组元素再访问对象 |
| 公共类型Rectangle必须在自己文件中声明 | 提交代码里包含了public class | 提交时去掉public,或者删掉整个Main类 |
| 缺少返回语句 | 声明了非void返回类型但没写return | 给所有分支添加return语句 |
| 类Rect是公共的,应在Rect.java中声明 | 与文件名不匹配的public类 | 去掉类前面的public |
| 输出结果多了或少了内容 | 打印了多余信息或漏了字段 | 严格比对输出样例,逐字符检查 |
这些错误里面,最让人无语的是最后一个:输出格式问题。程序逻辑完全正确,但多打了一个空格、把英文括号写成了中文括号、或者println和print没分清,OJ都会判错。我每次提交前都比对输出样例,连末尾有没有空格都注意,别嫌麻烦,这个习惯能帮你省下很多冤枉的提交次数。
4.2 三找法:快速锁定题目需要你写的类
我总结了一个“三找法”,专门用来对付函数题。做题前先花30秒做三件事:
第一找:找类名。看裁判测试程序里都new了哪些类,这些类名就是你必须定义的类。第二找:找构造方法。看new的时候传了几个参数,参数是什么类型,比如new Rectangle(1.0, 2.0)说明至少需要一个两个double参数的构造方法。第三找:找方法调用。看框架调用了什么方法,比如System.out.println(rect.getArea())说明需要getArea方法;if (p.equals(q))说明可能需要覆写equals的类。
把这三项用注释写在代码头部,等于给自己画了一张路线图。后面实现类时就不用猜,只需要填空。这个方法同样适用于未来面试手写代码场景,按接口倒推实现,方向永远是对的。
4.3 本地调试:别在OJ答案框里直接写大段代码
我强烈不建议直接在网站上那个小小的提交框里写代码。缩进混乱、无法实时调试、报错信息不直观,新手体验极差。我的做法是:把“裁判测试程序样例”复制到本地IDE,如果你写的类是独立的,就把它们放一个包或者一个文件里跑一遍。注意,一个文件里最多只能有一个public类,所以运行时需要把框架里的Main类前的public去掉,或者干脆不用public修饰类。
跑通之后,把你写的类片段复制回OJ提交框。这时如果仍然报错,多数情况下是提交框里混入了测试代码造成的——比如不小心把main方法也带上了,或者把import语句放在类里面。提交前再扫一眼,确保只提交题目要求的那一段。
5. 刷完这11道题,我总结的几条实操心得
5.1 别按1到11的顺序死磕,按难度跳着做
不少同学做题有个强迫症,必须按编号顺序,一道做不出来就卡在那里,后面的题全都不看。实际上这套题的难度并不是严格递增的,中间时不时会插一个比较综合的题,再跟一个很基础的题。我的建议是:先快速扫一遍所有题,从最简单的开始做,把能拿的分先拿到,再回头啃硬骨头。这样也不会因为一道题卡太久而心态崩掉,而且更奇妙的是,你做后面题目时,经常会遇到“原来前面那题用这个知识点能解”的顿悟时刻。刷题是为了掌握知识,不是为了一路绿灯。
5.2 每道题做完,多问自己三个问题
一道题拿到满分,只是开始。我会再问自己三遍:
- 这个类能不能去掉setter?如果字段一旦创建就不该改,setter是不是写多了?
- 构造方法能不能少写一个?无参构造和带参构造是不是有重复代码?
- 如果测试数据多一组边界值,我的代码会不会崩?比如宽度为0甚至负数,面积算出来是负数,题目允不允许?
这些问题不会让OJ多给你分,但会让你的代码从“能过测试”变成“像正常人写的代码”。如果你是把这套题当成期末考试的练习题,那就更要这样做,因为考试题往往就喜欢在边界值上做文章。
5.3 类和对象不是“语法知识”,是“设计习惯”
最后聊点题外话。我见过很多同学把类和对象这一章当语法知识背,天天背“类是抽象,对象是具体”“封装是隐藏细节”这些概念,但一写代码就露馅。其实类与对象的核心思想就一句话:把数据和操作绑在一起,并且通过接口对外暴露行为。你想想,你每天用的Scanner、ArrayList,哪一个不是别人设计好的类?你new一个Scanner对象,调它的nextInt()方法,这本身就是在使用对象。从“使用别人写的类”到“自己写一个类被别人使用”,这个身份转变,才是这11道函数题真正要教会你的东西。
如果你现在还在和sdut-Java面向对象-05这11道题死磕,我的建议很简单:先别急着写代码,把每一题的裁判测试程序读到几乎能背下来,再动手。我当年也是被构造方法重载折磨得不行,后来发现根本不是自己不会写,而是没看清调用方到底要什么。等你把题刷完回头看,类、对象、封装这几个概念就像你用惯了的筷子一样自然,考场上看到“定义一个类”的题,闭着眼都能把框架搭出来。