
最近把 sdut 这套 Java 面向对象第 06 套题做完了题目正好卡在继承和多态、抽象类和接口这个阶段一共 10 道编程题。说实话面向对象前面几章背概念还能蒙混过去一到这种需要自己写继承关系、自己设计抽象层级的题目平时那些“好像懂了”的地方全都会暴露出来。你觉得自己理解了继承但让你动手写一个父类、两个子类、再在 main 里互相调用很多人第一版代码就会被编译器怼得怀疑人生。这篇文章就把我做这套题的全过程复盘一遍从每道题考什么、应该怎么下手到踩过的坑和报错原因都整理出来。给正在学 Java 面向对象的同学作为一个实战参考尤其准备期末、做课程设计或者刷 OJ 的朋友这套思路应该能帮你少走不少弯路。先说结论这 10 道题做完我对“继承是为了复用、多态是为了扩展、抽象类和接口是为了约束”这句话才算真正有了体感。下面我会按知识点把题目归类一一复盘代码都是我自己重新整理过的版本直接能跑。1. 这套题到底在考什么先看清训练目标1.1 继承、多态、抽象类、接口四者的关系很多同学一上来就闷头敲代码结果越敲越乱。我的建议是先搞清楚这套题设计的逻辑。题目虽然叫“继承和多态、抽象类和接口”但实际训练的是四个层级的能力继承解决代码复用和类的层次关系核心关键字是extends。多态解决“同一动作、不同表现”的问题核心是父类引用指向子类对象。抽象类在继承之上加了一层设计约束用abstract定义不能实例化的父类。接口把“能力”和“实现”分离用implements表达一种契约或能力。这四者不是孤立的。抽象类本身也是继承的一种接口实现之后也可以体现多态。所以这套题里很多题目是交叉训练的比如用抽象类设计形状再用父类数组统一调用子类方法这就是“抽象类 多态”的组合。1.2 十道题的总体分类以我做的题目来看基本可以分成这几组题号题目类型核心考点1-2动物类、图形类基本的继承与方法重写3-4员工工资、银行账户继承层次 多态方法5-6形状面积、喂食系统抽象类 多态数组7-8USB 接口、接口引用接口定义与实现9-10防盗门、交通工具接口多实现、接口多态这个分类不是唯一的每个老师出的具体题目可能有差别但考点不会跳出这个框架。所以你看题的时候先判断这道题考的是“继承关系”还是“接口能力”再去动手思路会清晰很多。1.3 为什么 oj 题比书上例题更有价值书上的例子通常是给你一个完整的类图告诉你谁继承谁。但 OJ 题不是这样它只会给你一段文字描述定义一个什么类包含什么属性方法再测试什么结果。你需要自己决定父类写什么、子类怎么重写、用抽象类还是接口。这个“自己设计”的过程恰恰是考试和工作里最需要的。我做完这套题的体会是教材例题是让你看懂OJ 题是让你写对。两者之间的差距就是 “看得懂” 和 “写得出来” 的差距。2. 继承专项题extends 怎么用才不算白写2.1 题目 1 复盘构造器与 super 的调用链题目大概是这样定义一个动物类Animal有name属性和speak()方法定义Dog、Cat两个子类继承Animal重写speak()方法输出不同叫声。很多人第一版代码是这样写的class Animal { String name; public void speak() { System.out.println(动物在叫); } } class Dog extends Animal { public void speak() { System.out.println(汪汪叫); } }这段代码本身没错能编译能运行。但问题出在name的赋值上。如果你在子类里写了一个带参构造器class Dog extends Animal { public Dog(String name) { this.name name; } public void speak() { System.out.println(name 汪汪叫); } }这样写虽然能跑但没有真正理解继承的构造调用链。正确写法应该是class Animal { protected String name; public Animal(String name) { this.name name; } public void speak() { System.out.println(动物在叫); } } class Dog extends Animal { public Dog(String name) { super(name); // 显式调用父类构造器 } Override public void speak() { System.out.println(name 汪汪叫); } }这里有一个关键知识点子类构造器默认会隐式调用父类的无参构造器。一旦父类没有无参构造器子类就必须在构造器第一行用super(...)显式调用父类的有参构造器否则编译直接报错。我当时第一次写就踩了这个坑把super(name)写成了this.name name;。结果父类的构造逻辑被跳过一旦父类里有更复杂的初始化操作比如字段校验、资源分配就会出错。记住一个原则父类负责父类的字段子类负责子类的字段通过 super 完成父类部分的初始化。2.2 题目 2 复盘方法重写不能只改方法体题目 2 通常是设计一个形状类Shape定义一个getArea()方法返回面积然后让Circle、Rectangle继承并重写。很多同学会写出这样的方法签名class Circle extends Shape { public int getArea() { // 想改成 int觉得面积是整数 return 3; } }这在 Java 里是编译不过的。重写方法时返回类型必须与父类一致或者父类方法返回类型的子类型。如果父类getArea()返回double子类返回int也不允许因为int不是double的子类型。正确的做法是保持方法签名完全一致class Circle extends Shape { private double radius; public Circle(double radius) { this.radius radius; } Override public double getArea() { return Math.PI * radius * radius; } }另外还有一个隐藏考点重写方法的访问权限不能比父类更严格。如果父类方法是public子类重写方法不能写成private或protected这样会破坏“子类一定能替代父类使用”的多态原则。2.3 继承常见的三个“不能”做前面几道题时我整理出三个容易出现的问题你也可以自查一下不能把父类引用直接调用子类特有方法。比如Animal a new Dog();之后a只能调用Animal里声明过的方法不能直接调用Dog里的fetch()。这是编译期决定的跟实际对象是什么无关。不能覆盖父类的 private 方法。父类private方法对子类不可见子类里写一个同名同参数的方法只是新方法不是重写。用Override注解会直接报错。不能用子类的静态方法重写父类的实例方法反过来也不行。Override会帮助你识别这些错误。这些都是编译期错误IDE 一般会直接标红。遇到这种红杠不要急着改成 “不报错为止”要想一想它为什么被禁止否则同一个坑下次还会踩。3. 多态专项题父类引用指向子类对象的威力3.1 题目 3 复盘用多态方法统一处理工资题目大概要求定义员工类Employee包含name和calcSalary()方法Manager经理按底薪加奖金算Developer程序员按小时工资乘工时算。最后在测试类里写一个方法传入不同的员工对象输出对应的月薪。我第一次做的时候写了好几个 if 判断类型代码长而且难维护public void printSalary(Employee e) { if (e instanceof Manager) { Manager m (Manager) e; System.out.println(m.getBase() m.getBonus()); } else if (e instanceof Developer) { Developer d (Developer) e; System.out.println(d.getHourSalary() * d.getHours()); } }这种做法在功能上是“对的”但完全没有利用多态。正确做法是把计算逻辑放到各自的类里统一调用父类方法class Employee { protected String name; public Employee(String name) { this.name name; } public double calcSalary() { return 0; } } class Manager extends Employee { private double base; private double bonus; public Manager(String name, double base, double bonus) { super(name); this.base base; this.bonus bonus; } Override public double calcSalary() { return base bonus; } } class Developer extends Employee { private double hourSalary; private int hours; public Developer(String name, double hourSalary, int hours) { super(name); this.hourSalary hourSalary; this.hours hours; } Override public double calcSalary() { return hourSalary * hours; } }这样一来展示工资的方法只需要写成public void printSalary(Employee e) { System.out.println(e.name 的工资是 e.calcSalary()); }传Manager对象就是经理的工资传Developer对象就是程序员的工资。代码从 if-else 变成了“每个类自己做自己的事”这就是多态的核心价值。3.2 题目 4 复盘用父类数组遍历子类对象这套题里很多题目会要求定义 Shape、Animal 这样的父类然后创建几个子类对象放到一个父类类型的数组里统一遍历调用。这个模式很经典我建议你背下来Shape[] shapes new Shape[3]; shapes[0] new Circle(2.0); shapes[1] new Rectangle(3.0, 4.0); shapes[2] new Circle(1.5); for (Shape s : shapes) { System.out.println(s.getArea()); }这里每个shapes[i]的静态类型都是Shape但运行的时候会去调用对应对象的getArea()方法。这个“编译看左边运行看右边”的行为在 Java 里叫动态绑定也叫运行时多态。它是多态能成立的根本原因。为什么能这样因为 JVM 在运行阶段会到“实际对象所属的类”里去找方法。shapes[0]实际是Circle对象所以调用的是Circle的getArea()。不管你怎么转型赋值实际对象是谁调用的方法就跟着谁走。3.3 向下转型别乱用instanceof 是安全保障多态题目里经常还有一个小陷阱如果父类引用要调用子类特有的方法必须向下转型。Animal a new Dog(旺财); a.speak(); // 允许speak 是父类定义的方法 // a.fetch(); // 编译错误Animal 没有 fetch 方法 if (a instanceof Dog) { Dog d (Dog) a; d.fetch(); // 这样才能调用 Dog 特有的方法 }如果不用instanceof判断就直接强转遇到这种情况Animal a new Cat(咪咪); Dog d (Dog) a; // 运行时报 ClassCastExceptionJVM 会抛出类型转换异常程序直接挂掉。所以在向下转型之前用instanceof做一次判断是必需的。这不仅是安全习惯也是很多 OJ 题的隐藏得分点。4. 抽象类专项题从“不能实例化”说起4.1 题目 5 复盘为什么需要抽象方法题目要求定义一个抽象图形类里面有抽象方法getArea()和getPerimeter()再让Circle、Rectangle去实现。我第一次写的时候有疑问父类Shape的getArea()为什么不直接写return 0反正子类都会重写。后来我理解了抽象类的意义父类根本不应该提供实现因为它无法确定实现。比如你定义一个Shape你怎么知道面积怎么算圆面积是圆周率乘半径平方矩形面积是长乘宽形状不同算法完全不同。父类写一个return 0是假的、没有意义的实现还容易让别人误以为“形状面积就是 0”。正确的设计是使用抽象方法abstract class Shape { public abstract double getArea(); public abstract double getPerimeter(); } class Circle extends Shape { private double radius; public Circle(double radius) { this.radius radius; } Override public double getArea() { return Math.PI * radius * radius; } Override public double getPerimeter() { return 2 * Math.PI * radius; } }抽象方法只有声明没有方法体末尾直接是分号。它给子类立了一条“规矩”你继承了我就必须实现我声明的方法。如果子类没有全部实现抽象方法子类自己必须也是抽象类否则编译报错。4.2 题目 6 复盘抽象类也能写普通方法很多同学把抽象类理解成“只能放抽象方法”这是错的。抽象类可以同时拥有普通方法、成员变量和构造器。比如题目里的动物喂食系统你可以在抽象类里把eat()写成抽象方法把sleep()写成普通方法因为所有动物睡觉的方式差不多但吃东西的方式差别很大abstract class Animal { protected String name; public Animal(String name) { this.name name; } public abstract void eat(); public void sleep() { System.out.println(name 正在睡觉); } }这样设计的好处是共性代码放在父类里被复用差异行为通过抽象方法强制子类自己实现。抽象类的构造器虽然不能直接new但被子类构造器调用是完全可以的而且这是常态。4.3 抽象类里的构造器很多人理解错我见过不少同学在抽象类里定义构造器之后问“抽象类不是不能实例化吗为什么还有构造器”其实抽象类的构造器不是为了new抽象类而是为了帮助子类完成初始化。子类构造器第一行会隐式或显式调用super(...)这时候父类构造器就执行了。所以抽象类里写构造器是把公共字段的初始化集中到父类避免每个子类重复写。这个点在很多教材里不强调但 OJ 题是能考出来的。只要题目要求父类有name字段且通过构造器赋值你就必须给抽象父类写构造器并在子类里用super(name)调用。5. 接口专项题implements 只是第一步5.1 题目 7 复盘USB 接口的定义与实现接口部分一般会从现实场景入手比如 USB。题目定义一个IUSB接口包含turnOn()和turnOff()方法再让 U 盘类、键盘类分别实现。基本写法是这样的interface IUSB { void turnOn(); void turnOff(); } class UDisk implements IUSB { Override public void turnOn() { System.out.println(U盘已通电); } Override public void turnOff() { System.out.println(U盘已断电); } } class Keyboard implements IUSB { Override public void turnOn() { System.out.println(键盘已通电); } Override public void turnOff() { System.out.println(键盘已断电); } }这里最容易犯的错误是接口方法默认是public abstract实现类写方法时漏掉public。Java 规定子类的重写方法权限不能比父类更小接口方法是public所以实现类方法必须是public。如果不写会得到“正在尝试分配更低的访问权限”的编译错误。5.2 题目 8 复盘接口的多实现题目如果升级到防盗门的场景通常要求一个类同时实现多个接口。比如实现“锁”接口和“开门”接口interface Lockable { void lock(); void unlock(); } interface Openable { void open(); void close(); } class SecurityDoor implements Lockable, Openable { Override public void lock() { System.out.println(门被锁上了); } Override public void unlock() { System.out.println(门锁已打开); } Override public void open() { System.out.println(门打开了); } Override public void close() { System.out.println(门关上了); } }这个题目背后的核心设计思想是Java 不允许多重继承类但允许一个类实现多个接口。类只能有一个父类但可以有无数个“能力”。用接口来组合能力比单纯靠继承实现层次扩展灵活得多。接下去要理解接口的多态性。如果你有一个电脑主机类方法参数用IUSB类型class Computer { public void connect(IUSB usb) { usb.turnOn(); usb.turnOff(); } } public class Test { public static void main(String[] args) { Computer computer new Computer(); computer.connect(new UDisk()); computer.connect(new Keyboard()); } }传哪个实现类进来方法里调用的就是哪个实现类的逻辑。接口类型的引用变量指向实现类对象这就是接口多态。我不需要知道接入的是键盘还是 U 盘只要它实现了IUSB接口就能被电脑使用。5.3 接口 vs 抽象类一张表说清楚做题做到后面抽象类和接口经常可以互相替换让人纠结“到底用哪个”。我自己的判断标准很简单如果子类和父类是is-a的关系比如 Dog is an Animal优先考虑抽象类。如果只是想表达has-a 能力比如某个东西可以被锁、可以被充电优先考虑接口。对比项抽象类接口关键字abstract classinterface继承方式extends单继承implements可多实现构造器有没有成员变量可以有实例字段字段默认是 public static final普通方法可以定义Java 8 后可以用 default/static侧重点共性代码复用行为契约与解耦这套题里如果你拿不准就回头看看题目描述里的关键措辞。“是一个”“属于”就用抽象类“具有某种功能”“可以被……”就用接口。6. 做题时的报错和排查经验6.1 编译错误速查表10 道题做完我整理了下面几个出现频率最高的编译错误。下次你看到类似报错可以直接对照报错信息原因解决方案构造函数 Animal() 未定义父类没有无参构造器子类没显式调用 super子类构造器第一行写super(...)正在尝试分配更低的访问权限重写方法时把权限从 public 改成了 private/protected把权限改为 public返回类型与父类不一致重写方法改了返回类型保持返回类型一致或改成父类返回类型的子类型抽象方法不能被调用试图 new 抽象类对象创建具体子类对象或用子类类型引用无法从静态上下文引用非静态变量main 方法是静态的直接访问它实例变量在 main 里先创建类对象再调用非静态方法编译错误是打字层面的错能通过编译器快速发现。真正难的是下面这种运行期问题。6.2 运行期异常速查运行期最容易碰到的就是ClassCastException也就是类型转换异常。原因很简单把一个实际对象强制转成了它不兼容的类型。比如你写了Animal a new Cat(喵喵); Dog d (Dog) a; // 运行时报错JVM 在执行强转的时候会检查 a 的实际类型发现它根本不是 Dog直接抛异常。解决办法有两个第一转型前用instanceof判断第二改善设计尽量避免不必要的向下转型。另一个常见问题是NullPointerException多发生在创建对象后没有初始化或者数组里有空引用。我做题时经常先定义一个Shape[]数组填入几个空值然后遍历调用getArea()就崩了。所以循环遍历前要么确认每个元素都赋值了要么在循环里加一个null判断。6.3 如何快速定位自己的错误说实话OJ 平台最烦人的就是报错信息可能只告诉你“答案错误”或“编译错误”不会指出具体哪一行。我的排查顺序是这样先确认编译能过。把所有编译错误都解决掉不要用注释“大法”跳过有问题的代码。在本地用 main 方法做最小测试。把你写的每个类单独写一个小的测试代码看父类方法、子类方法输出是否符合预期。重点检查重写的方法有没有加 Override 注解。这个注解能帮你立刻发现“我以为自己在重写、实际上没有”的隐藏问题。用 print 输出关键变量。多态的题容易在参数传递和转型上出错在这些位置打印一下对象实际类型问题往往一目了然。这套排查流程我到现在刷题都还在用效率很高。7. 做完十道题后我的一些真实感受这套题并不难每道题单独拿出来都不到几十行代码。但真正把它们从头到尾写完、改错、跑通之后有几个以前模糊不清的点变得很明确继承不只是把代码抄到子类而是要理解构造调用链和重写的限制。多态不是为了写一些看起来很“高级”的代码而是为了把变化隔离掉让调用方不需要关心具体类型。抽象类和接口是设计层面的约束它们真正的价值是防止你在每一处都自己“自由发挥”。如果你正在学这个阶段我的建议是不要只满足于“把题跑通”。可以试着做两件事第一把已经跑通的代码里的父子关系换一下比如Animal[]数组里同时放猫猫狗狗再遍历调用speak()感受动态绑定带来的变化第二把某一个“用抽象类实现”的题改成“用接口实现”看看代码有哪些地方要改哪些地方更顺了。这样比盲目刷十道题有用得多。