尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java继承与多态详解:从零基础到牛客刷题通关

Java继承与多态详解:从零基础到牛客刷题通关 1. 继承零基础理解 Java 代码复用第一课1.1 为什么需要继承 —— 先看一个改代码改到崩溃的场景我记得当年带一个学弟入门 Java 的时候他写过一段特别“原始”的代码先写了一个 Dog 类里面有 name、age 两个字段还有 eat() 和 sleep() 两个方法。后来需求加了要一个 Cat 类他直接把 Dog 类复制了一份把“狗”改成“猫”。再后来要一个 Bird 类又复制一份。三个类加起来 200 多行其中 180 行是一模一样的。真正让他崩溃的是第四次改需求所有动物都要加一个 weight 字段。于是他花了半小时改了三个文件改了九处地方还漏改了一处程序直接跑不通。我告诉他你缺的不是方法是思维。这种“复制粘贴再改名”的写法在 Java 里有一个非常明确的反对方案——继承。继承解决的问题很简单把多个类共有的字段和方法抽到一个“父类”里子类用 extends 关键字“继承”这些内容然后只写自己独有的部分。用继承重写上面的动物场景核心逻辑就变成了这样// Animal.java public class Animal { protected String name; protected int age; public Animal(String name, int age) { this.name name; this.age age; } public void eat() { System.out.println(name 在吃东西); } public void sleep() { System.out.println(name 在睡觉); } }// Dog.java public class Dog extends Animal { public Dog(String name, int age) { super(name, age); } public void bark() { System.out.println(name 汪汪叫); } }子类 Dog 只写了 bark() 这一个自己独有的方法name、age、eat()、sleep() 全部来自父类 Animal。以后需求再变加 weight 字段只需要改 Animal 一个文件所有子类自动生效。这就是继承最直接的价值复用共性保留个性。1.2 extends 语法与构造方法调用顺序继承的语法非常简单子类声明时加 extends 加父类名就行。但简单背后有几个必须搞清楚的规则这是牛客刷题和面试里反复出现的点。第一个规则子类构造方法第一行必须调用父类构造方法。如果你不写编译器会自动加一个 super() 调用父类的无参构造方法。如果父类只有带参构造方法而子类构造方法里没有显式调用带参的 super(...)编译就会直接报错。所以刚才的 Dog 类构造方法里写 super(name, age) 不是可写可不写而是父类没有无参构造时这一行就是必须的。public class Cat extends Animal { // 错误示范父类没有无参构造这里会编译失败 public Cat() { } }第二个规则继承的关系是单向的Java 只支持单继承。一个类只能有一个直接父类这是刻意的设计——多继承会带来菱形继承问题也就是两个父类都有同一个方法时子类到底继承哪一个Java 用“接口多实现 类单继承”的组合拳绕开了这个坑。你可以在牛客上看到很多关于菱形继承的题本质都是在考这个语法限制。第三个规则private 成员不会被继承。父类里如果用 private 修饰字段子类代码里直接用这个字段名会编译失败。你需要把字段改成 protected 或者提供 public 的 getter/setter。这也引出一个很常见的初学者误区继承和访问权限是强相关的不是“父类所有东西子类都能用”。protected 这个权限位就是专门为继承设计的它允许子类直接访问但外部其他类不行。1.3 牛客题 21/22 的解题思路拆解牛客题库里 21、22 这两题围绕的核心就是“定义父类、创建子类、重写方法”这个套路。典型题型是给一个 Person 父类有 name 和 age 字段以及一个 speak() 方法要求定义 Student 子类继承 Person新增 school 字段重写 speak() 方法输出学生的完整信息。拿到这种题我建议你按下面四步走先写父类。字段私有化提供构造方法和一个 speak() 方法方法体输出通用的信息。写子类用 extends 继承父类。构造方法用 super(name, age) 先把父类的字段初始化掉再给自己的新增字段赋值。重写 speak() 方法。方法签名必须和父类完全一致返回类型、方法名、参数列表都不能变。输出内容加上子类特有的字段。在主方法里测试。用父类引用指向子类对象的方式创建对象也就是 Animal a new Dog()后面讲多态时会重点展开。重写方法时有个细节可以在方法上加上 Override 注解。这个注解不加代码也能跑但加了之后如果你把方法签名写错了编译器会直接报错提醒你。我从学 Java 第一天起就习惯加 Override这个习惯帮我避掉了无数的坑。提示刷题时如果看到“子类继承父类但无法访问父类的私有成员”这类说法判断题答案是“错误”。子类确实不能直接访问,但可以通过父类提供的 public 方法间接访问。题目经常在这里挖坑。2. 多态让同一个方法调用出不同行为2.1 多态的三个必要条件多态这个词听起来高大上其实在 Java 里就是指一句话同一个父类引用指向不同的子类对象调用同一个方法表现出来的是不同子类各自的行为。最经典的例子就是Animal a1 new Dog(旺财, 2); Animal a2 new Cat(咪咪, 1); a1.eat(); // 输出旺财 在吃东西 a2.eat(); // 输出咪咪 在吃东西看起来 a1 和 a2 都声明成 Animal 类型调用同一个 eat() 方法但因为实际指向的对象不同执行的结果也不同。这就是“一种接口多种实现”的直观体现。要触发多态需要满足三个条件缺一不可有继承关系。父类引用可以指向子类对象这是前提。子类重写了父类的方法。如果子类没有重写调用到的还是父类的方法体现不出差异。通过父类引用调用被重写的方法。注意如果你用子类引用调用子类独有的方法那不算多态多态的核心是“声明类型”和“实际类型”不一致。我在教学中见过一个常见的困惑为什么 a1 明明指向的是 Dog 对象却不能调用 bark() 方法答案很实在——编译期间编译器看的是声明类型 AnimalAnimal 类里没有 bark() 方法所以编译都过不了。想在运行时调 bark()只能先强制类型转换if (a1 instanceof Dog) { Dog d (Dog) a1; d.bark(); }instanceof 关键字用来判断对象的实际类型做向下转型前一定要先判断否则很容易抛 ClassCastException。2.2 重载与重写的区分以及动态绑定原理重载Overload和重写Override是牛客高频考点也是零基础最容易混的一对概念。我总结了一个最简单的记法重写发生在继承关系中子类把父类的方法“换皮”方法签名完全一样。重载发生在同一个类中方法名一样参数列表不一样本质是多个不同的方法。重载的例子public class Calculator { public int add(int a, int b) { return a b; } public double add(double a, double b) { return a b; } }这两个 add 方法方法名相同但参数列表不同调用时根据传入参数的个数和类型来决定走哪一个。注意只改返回类型不算重载比如 add(int, int) 返回 int再写个 add(int, int) 返回 double编译器直接报错——方法签名只由方法名和参数列表决定。理解重写背后的“动态绑定”是真正吃透多态的关键。我打个比方你给朋友发消息说“周末一起吃饭”发给程序员朋友时他会回“好帮我带杯咖啡”发给健身朋友时他会回“可以顺便练个背”。消息内容一样但不同的人收到后做出的回应不同。“绑定”就是在运行时决定到底执行哪个方法版本的过程。Java 默认的非静态方法调用都是动态绑定也就是运行时才根据对象的实际类型来决定。为什么非静态方法要动态绑定而 static 方法不用因为静态方法属于类本身不依赖对象的存在所以它是静态绑定编译期就定死了。这意味着通过父类引用调用一个 static 方法执行的一定是父类的方法哪怕实际对象是子类。这个坑在牛客题里出现过不止一次记住这句话能帮你拿分。2.3 牛客题 23 的解题思路拆解23 这题通常是给你一个父类 Shape里面定义一个计算面积的方法 calculateArea()子类 Circle 和 Rectangle 分别重写这个方法最后用父类引用的数组存储多个子类对象循环调用计算总面积。这道题的考察点非常集中就是你有没有真正理解多态。我看到不少零基础同学卡在这里原因不是不会写 Circle 的面积公式而是不知道怎么组织代码。最佳写法是public class Main { public static void main(String[] args) { Shape[] shapes new Shape[2]; shapes[0] new Circle(3.0); shapes[1] new Rectangle(4.0, 5.0); double totalArea 0; for (Shape s : shapes) { totalArea s.calculateArea(); } System.out.printf(总面积: %.2f%n, totalArea); } }这个写法的妙处在于Shape[] 数组的声明类型是 Shape但你放进去的是 Circle 和 Rectangle。循环里每调用一次 calculateArea()JVM 都会根据当前对象的实际类型去执行对应子类的面积计算方法。以后你要加一个 Triangle 子类只需要创建一个 Triangle 对象放进数组主循环一行代码都不用改。这就是多态从“能跑”到“好用”的分水岭。注意写这道题时很多人会忘记在 Circle 和 Rectangle 类里加 Override。如果你想快速确认自己是不是真的重写了就看注解有没有生效。Override 提示“找不到要重写的方法”说明要么方法签名写错要么父类根本没有这个方法。3. 接口与抽象类设计层面的两个利器3.1 抽象类把“共性”和“差异”分开讲完了继承和多态接下来是接口和抽象类。这两个概念在零基础阶段特别容易混淆因为它们看起来都“不能直接实例化”都“要被子类实现”。但它们的定位和使用场景完全不同。抽象类用 abstract 关键字声明。它最核心的特点是可以包含抽象方法也可以包含普通方法。抽象方法只有方法签名没有方法体需要子类强制实现。换句话说抽象类是“半成品”类——它把一部分共性逻辑写成具体方法把一部分差异逻辑留成抽象方法让不同的子类各自补全。我用一个现实场景来类比你想开一家餐厅需要招聘厨师但“厨师”这个概念太抽象了你不能直接雇佣一个“厨师”对象你必须雇“川菜厨师”或者“粤菜厨师”。但是所有厨师应该知道“如何颠勺”这个动作。这里的“厨师”就是抽象类颠勺是抽象方法每个菜系厨师根据自己的方法实现。public abstract class Chef { public void prepareIngredients() { System.out.println(准备食材清洗、切配); } public abstract void cook(); }子类川菜厨师必须实现 cook() 方法而 prepareIngredients() 可以直接继承使用。这样设计的好处是共性逻辑写一次差异逻辑强制每个子类自己实现不会出现某个子类“忘记实现核心方法”的情况。抽象类还有一个细节构造方法存在但不能用 new 直接创建对象。有个经常被问到的面试题是“抽象类有构造方法吗”答案是有。它存在的意义是给子类调用子类创建对象时会先通过 super() 链调用抽象父类的构造方法初始化父类的字段。3.2 接口能力契约与默认方法接口用 interface 关键字定义。它比抽象类更“纯粹”——在 Java 8 之前接口里只能有抽象方法和常量。接口本质是一份能力契约它规定“实现这个接口的类必须拥有哪些能力”但不关心你怎么实现。我给学生举过一个很直观的例子USB 接口。一台电脑上不需要知道 U 盘、键盘、打印机各自内部怎么运作只要它们都遵守 USB 接口标准插上就能用。代码里也是一样——一个类可以实现多个接口这弥补了 Java 单继承的不足。public interface Flyable { void fly(); } public interface Singable { void sing(); } public class Bird implements Flyable, Singable { Override public void fly() { System.out.println(鸟儿飞翔); } Override public void sing() { System.out.println(鸟儿歌唱); } }一个类可以同时实现 Flyable 和 Singable 两个接口这种能力在类的单继承下是不可能实现的。接口之间也可以用 extends 互相继承形成接口的层次。Java 8 以后接口里新增了 default 方法和 static 方法。default 方法允许你在接口里写出带方法体的方法实现类可以不用强制重写。这个设计主要是为了兼容性——在不破坏已有实现类的前提下给接口增加新方法。但我要提醒你刷题时如果看到接口里有 default 方法别觉得奇怪这是合法语法。同时default 方法也带来一个经典面试题当一个类实现的两个接口里有同名的 default 方法类必须重写这个方法来消除冲突。3.3 抽象类和接口该如何选择抽象类和接口功能上有重叠也有很多细节区别。我给你整理了一个对比表刷题和面试都照这个记比较项抽象类接口关键字abstract classinterface继承/实现数量单继承一个类只能继承一个抽象类多实现一个类可以实现多个接口字段可以有实例字段普通变量只能定义常量public static final构造方法有没有方法可以有抽象方法和普通方法主要是抽象方法Java 8 后有 default 和 static 方法设计定位“是什么”的关系is-a“具备什么能力”的关系like-a / has-a选择逻辑其实很简单如果你要表达的是“这个类是那个类的一种”比如 Student 是一种 Person用继承抽象类。如果你要表达的是“这个类具备某种能力”比如一个类能飞、能游泳用接口。零基础阶段我看到的最多问题就是纠结“我该用抽象类还是接口”。我一般的建议是默认优先考虑接口。这是因为接口更灵活一个类可以实现多个接口而且接口天然适合定义行为规范。只有当你确实需要共享字段和具体方法、且这些字段方法有明确的父子层级关系时才考虑抽象类。3.4 牛客题 24~26 的解题思路拆解第 24~26 题是接口和抽象类的直接应用。常见题型有定义抽象类、定义接口、创建一个类同时继承抽象类并实现接口然后按要求输出。我拿一个典型的综合题来说明。题目要求定义一个抽象类 Animal2包含 name 字段和一个抽象方法 eat()定义一个接口 Pet包含一个普通方法 play()定义 Dog2 类继承 Animal2 并实现 Pet重写所有必须实现的方法。标准写法的解题顺序是这样的public abstract class Animal2 { protected String name; public Animal2(String name) { this.name name; } public abstract void eat(); } public interface Pet { void play(); } public class Dog2 extends Animal2 implements Pet { public Dog2(String name) { super(name); } Override public void eat() { System.out.println(name 在吃狗粮); } Override public void play() { System.out.println(name 在玩飞盘); } }这类题的注意点是类继承抽象类并实现接口时先写 extends再写 implements顺序不能反过来。这是语法规定。此外凡是抽象父类里的抽象方法以及接口里的抽象方法子类必须全部实现否则子类自己也要声明成 abstract而一旦声明成抽象类就不能 new 对象了。第 24~26 题之间可能还存在递进关系比如第 24 题只是让你创建抽象类和子类第 25 题加入接口第 26 题要求用父类引用和接口引用同时指向同一个对象来验证多态。最后这种考法很经典Animal2 a new Dog2(旺财); Pet p new Dog2(旺财); a.eat(); // 通过父类引用调用 eat() p.play(); // 通过接口引用调用 play()同一个对象 Dog2既可以被 Animal2 引用指向也可以被 Pet 引用指向。通过不同的引用类型能调用到的方法也不同——Animal2 引用只能调用 eat() 和继承下来的方法Pet 引用只能调用 play()。这个设计把今天讲的所有核心概念继承、接口、多态、抽象类全部串起来了理解透这里你的 Java 面向对象基础就算真正立住了。4. 刷题过程中常见的坑和排查心得4.1 编译报错的三大高频原因我在带学生刷这六道题的过程里发现零基础同学报错高度集中在三个地方。第一个是构造方法问题。报错信息里常见的是“There is no default constructor available in class Animal”之类。原因就是我前面说的父类定义了带参构造方法后就不再有默认的无参构造子类构造方法第一行必须显式调用 super(...)。这个错看懂了就是一行代码的事看不懂就反复编译失败。我的建议是写子类构造方法时第一句话永远是 super(...)形成肌肉记忆。第二个是访问权限问题。报“Cannot find symbol”或者“x has private access in y”基本就是访问权限用错了。记住父类的 private 成员子类不可直接访问需要 protected 或者 getter/setter。实践里我喜欢把需要被子类访问的字段设为 protected这既保证了封装的封闭性又给继承留了口子。第三个是类型转换问题。前面提过父类引用不能直接调用子类特有方法。如果你强转时不写 instanceof 判断代码运行到一半极可能抛 ClassCastException这是运行时异常编译期根本发现不了。稳妥写法是先判断再转换。这里我分享一个排查思路。收到报错后先看报错发生在编译期还是运行期。编译期的错编译器会指出具体行号直接定位到那一行对照我在上面列的三类问题逐一排查。运行期的错往往和对象的实际类型有关这时候优先检查 instanceof 和类型转换相关的代码。把这两条思路理清楚牛客上的报错你能解决掉百分之八十。4.2 面试更容易追问的三个隐藏考点刷完这几道题知识部分已经覆盖了面向对象最核心的几个概念。但如果目标是准备面试我建议你额外把这三个隐藏考点也挖一遍。第一个是 equals 和 hashCode。很多牛客题里都会涉及对象比较但初学者往往只用 比较基本类型。对于引用类型 比较的是地址equals 默认也是地址比较除非重写了。实践中重写 equals 时必须同时重写 hashCode否则基于哈希的集合HashSet、HashMap会出现逻辑错误。这两个方法在面试中被问到的概率极大。第二个是 static 方法不参与多态。前面提到过这个点面试官经常考用父类引用调用被重写的 static 方法输出的是谁的答案是父类的。因为 static 方法属于类在编译期就完成了绑定。第三个是接口中变量的隐含修饰符。接口里声明的变量默认就是 public static final也就是常量。你写 int x 5; 其实等同于 public static final int x 5;。面试时如果问接口能不能有实例变量回答不能因为接口里的所有变量都是常量。这三个点不需要你现在立刻做深度研究但做完继承、多态、接口、抽象类这六道题之后顺着去了解一下会非常有帮助。它们属于同一个知识网络里的延伸节点。4.3 一个值得刻意养成的刷题习惯最后说一个我特别想安利给零基础同学的刷题习惯每做完一道题强制自己用一句话说出“这道题考了什么抽象概念”。比如做完第 21 题你告诉自己这道题考的是继承子类通过 extends 复用父类字段和方法。做完第 23 题你告诉自己这道题考的是多态父类引用指向子类对象调用方法时执行子类重写版本。做完第 26 题你告诉自己这道题考的是抽象类和接口的组合使用一个类可以同时继承抽象类并实现接口。这个方法看起来简单但效果出奇地好。因为刷题的核心目的不是背题而是建立“题目现象”和“概念本质”之间的映射。你每做一道题就强迫自己把这个映射说一遍一个月后你会发现很多题目在你眼里不再是冷冰冰的代码而是几个熟悉概念的不同组合方式。我在带学生的过程中反复验证过这件事能坚持做这个动作的同学学完继承多态这部分之后再看后面的集合、异常、IO相关内容接受速度明显快一截。因为面向对象的思想一旦内化Java 的一切都会变得顺理成章。
返回列表