牛客Java刷题21-26:继承、多态、接口与抽象类深度解析

牛客Java刷题21-26:继承、多态、接口与抽象类深度解析 从“照着敲都会一运行就报错”到真正理解面向对象是每个Java零基础学习者必须跨过的一道坎。这个系列的牛客刷题指南走到第21~26题正好切入Java里最核心也最容易让人绕晕的一组概念继承、多态、接口和抽象类。这篇文章就把这6道题背后真正需要掌握的东西拆开揉碎讲清楚不背概念、不念定义用实际代码和踩坑经历带你一步步把这块硬骨头啃下来。不管你是刚学完类和对象、正在被各种术语折磨的新手还是学了一遍但总觉得哪里没通透、想在刷题中查漏补缺的初学者这篇都适合你。1. 零基础学Java为什么总在“类”这里栽跟头先聊一个现象。我见过很多自学Java的人前面变量、循环、数组、方法都学得挺顺一到类和对象就开始发懵再往后学到继承多态基本就是“每个字都认识连起来不知道在说什么”。这不怪你因为从这一章开始你要切换的不只是语法而是一整套思维模型。1.1 从“复制粘贴代码”到“面向对象思维”的坎在学类和对象之前你写程序的方式是这样的需要什么功能就写一个方法把数据传进去得到结果。这种方式叫面向过程适合小程序但一旦项目体量变大代码之间会越来越纠缠改一个功能常常牵连一大片。面向对象不一样它把数据和操作数据的方法打包在一起成为一个“对象”。你可以把对象理解成一个微型机器人里面有它自己的属性变量也有它自己的行为方法。你不需要关心它内部怎么干活只需要告诉它“你去做饭”或者“你把成绩单打出来”它自己会处理。这种组织方式的好处代码量一大就体现出来了每个对象各管各的一摊事互相之间通过明确的方式协作出了问题也容易定位。继承、多态、接口、抽象类这四个概念本质上都是在解决“对象之间怎么协作、怎么组织”的问题。它们不是四个独立的知识点而是一条链路继承是父类和子类之间的纵向关系多态是利用这个关系让代码更灵活接口和抽象类则是从另一个维度定义“类应该长什么样、应该遵守什么规则”。1.2 牛客这6道题到底在考什么21~26题这个区间题目设计是有讲究的。它不会一上来就让你写一个完整的项目而是通过一组精心设计的小题逼你把以下能力点逐个过一遍你能不能准确判断两个类之间是不是继承关系你能不能看出一段代码在运行时调用的到底是哪个方法你能不能区分重载和重写并且知道重写有哪些约束你能不能理解接口和抽象类各自的定位并在设计中做出选择说白了这6道题就是在检验你有没有建立起“面向对象”的直觉。刷题之前这个直觉得有刷题之后这个直觉会被强化。所以这篇文章先讲透概念再逐题拆解顺序不能反。2. 继承先搞懂它到底是“省代码”还是“认关系”很多新手理解继承第一个想到的就是“子类可以复用父类的代码”然后就把继承当成了一个省代码的工具。这个方向其实已经偏了。继承首先是一种关系代码复用只是这个关系的副产品。2.1 is-a关系才是继承的灵魂继承关系用一句话概括子类是一个父类。猫是一种动物学生是一个人正方形是一种形状。这种关系在英文里写作is-a是判断能不能用继承的第一标准。如果你说“我想让Student类复用Person类里的name和age字段所以让Student继承Person”这个理由不够充分。正确的理由是“学生是一个人所以Student天然拥有人的所有共性”。代码复用是结果不是原因。那么问题来了怎么在代码里体现这种关系看下面这个例子class Animal { protected String name; public Animal(String name) { this.name name; } public void eat() { System.out.println(name 正在吃东西); } } class Dog extends Animal { public Dog(String name) { super(name); } public void bark() { System.out.println(name 汪汪叫); } }Dog类通过extends关键字继承Animal类Dog对象就自动拥有了name属性和eat()方法。你在测试类里可以这样写Dog dog new Dog(旺财); dog.eat(); // 旺财正在吃东西 dog.bark(); // 旺财汪汪叫Dog类自己只写了bark()方法但它能调用eat()这就是继承带来的能力。2.2 super()这一行新手最容易漏写看上面Dog的构造器第一行是super(name)。这行代码的意思是调用父类的构造器把name传上去。很多新手会问我不写super()行不行答案是行但要分情况。如果你不写Java编译器会自动在子类构造器第一行插入一个无参的super()调用也就是调用父类的无参构造器。如果父类没有无参构造器编译就会报错提示你something like“constructor Animal in class Animal cannot be applied to given types”。我见过不少新手在这个地方卡住怎么检查都没发现语法问题就是编译不过。原因就是父类只写了有参构造器没有写无参构造器。所以这里有个实操建议如果你定义了带参构造器编译器就不再提供默认的无参构造器了。这时候你有两个选择一是显式写出无参构造器二是让子类构造器第一行调用父类的带参构造器。从设计角度我更推荐后者因为name这种必填信息走构造器传进去更安全能避免创建出名字为null的对象。2.3 访问修饰符的“收”与“放”继承里还有一个细节需要特别留意父类的private成员子类能不能直接访问答案是不能。看个例子class Animal { private String secret 动物的小秘密; } class Dog extends Animal { public void reveal() { // System.out.println(secret); // 编译错误secret是private的 } }这是因为private成员的可见范围仅限于本类内部子类也看不到。那怎么办两种方案一种是改成protectedprotected的语义是“同包下的类可见还有子类可见哪怕是不同包的子类”另一种是提供public的getter方法这也是更符合封装思想的做法。说到protected有个细节值得展开即使Dog是Animal的子类Dog类内部也只能通过“Dog对象自己的继承来的成员”访问protected成员不能随便拿一个Animal对象去访问它的protected字段。这点在刷题时偶尔会以判断题形式出现建议留意。2.4 为什么说继承是“强关系”不能乱用继承的耦合度非常高。子类绑定了父类父类改了子类跟着受影响。这就是为什么业界一直有“组合优于继承”的说法能用组合表达的关联关系尽量别用继承硬套。比如“汽车”和“发动机”它们之间是has-a关系汽车拥有一台发动机不是is-a关系所以不该让Car去继承Engine而应该在Car里声明一个Engine类型的字段。明白了这个边界你再去判断牛客里那些“下列哪组类适合用继承”的题就不会光凭直觉乱选了。3. 多态同一个方法调用凭什么走不同实现继承是基础多态是继承的进阶运用。多态这个词听起来玄乎其实用一句话就能说透同一个类型的引用指向不同的对象调用同一个方法会有不同的表现。3.1 多态成立的三个条件少一个都不行要在Java里实现多态必须同时满足三个条件有继承关系或者接口实现关系子类重写了父类的方法父类引用指向子类对象看这段代码Animal a1 new Animal(动物); Animal a2 new Dog(旺财); a1.eat(); // 动物正在吃东西 a2.eat(); // 旺财正在吃东西如果Dog重写了eat方法这里a2的编译类型是Animal运行类型是Dog。调用eat()时Java虚拟机做的是动态绑定它不看a2声明成什么类型而是看它实际指向的对象是哪个类然后调用那个类里真正的方法。用大白话说就是代码写的是a2.eat()但a2到底是谁要等程序运行到这一行才知道。这就叫“运行期才确定”也就是多态的底层机制。3.2 编译看左边运行看右边“编译看左边运行看右边”这句话是刷多态题的金钥匙。它说的是调用一个方法时编译阶段检查的是左边引用类型里有没有这个方法运行阶段调用的却是右边实际对象所属类里的方法。所以如果父类里没有某个方法就算子类里面有你也调不了Animal a new Dog(旺财); // a.bark(); // 编译错误Animal类型里没有bark方法这个例子新手特别容易踩。明明a指向的是Dog对象Dog类里有bark()方法凭什么不能调就是因为编译阶段只看Animal这个引用类型Dog里那套东西它不认。要是想让子类特有的方法被调用你得先把引用类型转回DogDog d (Dog) a; d.bark(); // 现在可以了这个强制类型转换也有风险如果a实际上不是Dog运行时会抛ClassCastException。用之前最好用instanceof先判断一下。3.3 重载和重写牛客最爱考的一对概念多态的题十道里有七八道是围绕“重写”展开的。跟它对应的还有“重载”这两个词名字像但含义完全不同。对比项重写Override重载Overload发生位置子类和父类之间同一个类里方法名必须相同必须相同参数列表必须相同必须不同返回值类型可以相同也可以是父类返回值的子类型可以不同Java不要求访问修饰符子类不能比父类更严格随意绑定方式运行期动态绑定编译期静态绑定常见标记建议加Override注解不需要注解重写是父子类之间的一场“改造”重载是同一个类里“同名不同参”的多个版本。我见过不少人把这两者搞混一看到方法名相同就说是重写完全没看两个方法到底在哪个类里。刷题时只要先把“这两个方法在不同类还是同类”判断清楚再去看参数列表基本就不会错。另外有个小细节容易被漏掉重写时方法的返回值可以是原来返回类型的子类型这叫协变返回类型。父类返回Animal子类重写时可以返回Dog。这在JDK里很常见但作为零基础你只需要有个印象就行。3.4 多态到底有什么实际价值可能有人会觉得多态不就是绕来绕去的嘛直接new一个Dog再调用方法不就行了干嘛非要声明成Animal类型问得好。多态的核心价值在于“面向抽象编程”。你可以写这样一段代码完全不知道具体的实现类是谁public void feed(Animal a) { a.eat(); }feed方法接收一个Animal类型的参数但它能接收Animal的任何一个子类对象。传入Dog它就让狗吃传入Cat它就让猫吃。如果将来你新写了一个Bird类继承Animalfeed方法一行都不用改直接就能传Bird对象进来。这就是设计原则里说的“开闭原则”对扩展开放对修改关闭。新增功能靠添加新类而不是改动既有代码。多态是支撑这个原则的基石也是你以后学框架时到处都能见到“依赖注入”“策略模式”这些概念的思想源头。4. 接口和抽象类撕开它们最容易被混淆的那层纸继承和多态算是父子关系这条纵轴接口和抽象类则是另一条横轴。很多人学到这里崩溃是因为把两个概念放在了同一个维度里去死记硬背区别。其实它们的定位完全是两回事。4.1 抽象类一个不完整的类用来当模板抽象类用abstract关键字修饰。它的特点是类本身不能被new但可以拥有抽象方法没有方法体只有声明也可以拥有普通方法、字段、构造器。抽象类的本质是“模板”。它是让你去继承的不是让你直接使用的。打个比方你开了一家快餐店菜单上有一道“招牌套餐”但具体是什么菜没定可能今天是咖喱鸡饭套餐明天是红烧牛肉面套餐。这个“招牌套餐”就是抽象方法它一定存在但具体内容由各家店自己决定。来看一个例子abstract class Shape { protected String color; public Shape(String color) { this.color color; } public abstract double getArea(); public void showColor() { System.out.println(颜色是 color); } }Shape是个抽象类里面有一个抽象方法getArea()。任何继承Shape的具体类都必须实现getArea()否则这个子类也得是抽象类。但Shape里也可以有showColor()这种完整实现的方法子类可以直接继承使用。为什么抽象类要有构造器既然不能new构造器拿来干嘛答案是给子类用的。子类在构造时会先调用父类构造器把color这种公共字段初始化好。这是抽象类一个比较隐蔽但也比较重要的小细节牛客里偶尔会考。4.2 接口一份纯契约规定你必须能干哪些事接口用interface关键字修饰它比抽象类更彻底。Java 8之前接口里只能有抽象方法和常量Java 8之后加了default方法和static方法Java 9之后还允许private方法。但对零基础来说你只需要把接口理解成“一份合同”就够了。合同上写着凡是实现我这个接口的类必须提供这些能力。至于你怎么实现我不管那是你的事。interface Flyable { void fly(); } class Bird implements Flyable { Override public void fly() { System.out.println(扇动翅膀飞行); } } class Plane implements Flyable { Override public void fly() { System.out.println(喷气引擎推进); } }Flyable接口规定了fly()这个能力Bird和Plane各自去实现。注意这里用implements关键字一个类可以implements多个接口这就绕开了Java单继承的限制。接口能多实现是接口和抽象类最大的区别之一。一个类只能继承一个抽象类但可以实现多个接口。这也是设计上的一种折中Java为了保证类关系的简单性不允许多继承但现实世界中确实存在“某种东西拥有多方面的能力”的需求接口就是为此准备的。4.3 到底什么时候用接口什么时候用抽象类这个问题可以说是面试八股文里的常青树牛客这个阶段的题里也一定会碰到。我的回答是看你想要“契约”还是“模板”。如果你需要定义一组能力、行为规范让完全不相干的类也能统一表现用接口。比如dragon能飞飞机能飞鸟能飞这三个类没什么血缘关系但它们都需要fly()那就定义一个Flyable接口谁想能飞就implement它。如果你需要抽取多个类的公共代码并且它们之间确实有is-a关系用抽象类。比如所有形状都有颜色都有计算面积的行为但每个形状算面积的方式不一样那Shape设计成抽象类就很合适颜色和显示颜色的代码全部复用面积计算留给子类各自实现。在实际工程里还有个更常见的组合姿势先定义接口作为对外契约再写一个抽象类作为“骨架子”把一些通用逻辑实现掉然后让具体业务类去继承这个抽象类并实现剩余的方法。这是很多框架像Spring里的一些模板类都采用的模式。零基础阶段不需要写这么复杂但刷题时能在选择题里认出这种设计意图很有帮助。5. 牛客21~26题逐题拆解看到题目先想到哪一层概念铺垫到这里来拆题。这一组题我没有办法把原题逐字复现每期牛客的题面会有调整但根据系列进度和考察范围这类题目围绕的考点分布是很稳定的。我按照“题型破题思路易错点”的方式带你把这一组题过一遍你自己做题时对号入座就行。5.1 第21、22题判断继承关系与访问控制这个阶段的开头两道题通常会给几个类让你判断“哪两个类之间适合用继承”或者直接给你一段代码问你能不能编译通过、输出是什么。破题顺序很重要。我一般建议按三步走第一步判断两个类之间是否是is-a关系。不是is-a直接排除继承选项别被“能复用代码”这种选项迷惑。第二步看访问修饰符。注意父类private成员子类能不能看到。题目如果给了一段在子类里访问父类private字段的代码大概率编译报错。第三步看构造器。子类构造器有没有通过super调用父类合适的构造器。如果父类只有带参构造器而子类构造器第一行没有调用super和父类有没有默认构造器这往往就是隐藏的坑。举个例子题目给你class A { protected int x; public A(int x) { this.x x; } } class B extends A { public B() { // 此处没有写super } }问能否编译通过。答案是编译失败因为父类A定义了带参构造器后默认无参构造器消失而B的构造器第一行隐式调用super()时找不到无参构造器。这个考点出现频率不低请务必记住。5.2 第23题多态输出写运行结果多态类的题一般会甩给你一段类继承关系代码里面有几个类、几个重写方法然后让你写出main方法执行后的输出。做这种题最忌讳的就是“目测”一定要在草稿纸上画出继承链条。以这样一段代码为例class Parent { public String show() { return Parent; } } class Child extends Parent { public String show() { return Child; } } public class Test { public static void main(String[] args) { Parent p new Child(); System.out.println(p.show()); } }输出是Child因为p的编译类型是Parent但运行类型是Child动态绑定让实际执行的方法来自Child。如果题目里加一个重载方法比如Parent里再加一个show(String s)难度瞬间提升。这种“重写重载混合”的题目破题方法就一句话先把重载区分开看参数列表确定调用的是哪个签名再按重写规则编译看左边运行看右边确认最终执行的是哪个类的方法。我刷题的经验是多态题里最容易错的不是知识本身而是粗心。很多人看到p是Parent类型就直接认定调用Parent的方法忘了看右边new的是谁。所以做题的时候建议养成一个习惯每个对象引用都先把这两个类型写出来左边是什么类型、右边是什么类型再开始分析。5.3 第24题重写考点的各种变体这题通常围绕重写规则展开。常见的出题方式包括以下哪个方法可以正确重写父类方法以下关于重写的说法正确的是哪项给出一个子类重写方法问哪里有问题重写规则需要背牢的点其实就五个方法名相同、参数列表相同、返回值类型兼容、访问权限不能更严格、不能抛出比父类更宽泛的受检异常。前四个还好理解第五个“异常”零基础阶段可能还没接触到但牛客这个阶段偶尔会提前出判断题。你不用深入掌握异常体系只需要记住子类重写方法时抛出的受检异常范围不能比父类更大可以更小或者不抛。打个比方父类说“我想办法做到”子类可以更牛直接做到并且不出错但如果子类说“我做不到要出大错”那就不行。这里还有个考前特别容易忽略的细节static方法不存在重写。子类里写一个和父类static方法签名一样的方法不叫重写叫隐藏。调用时看引用类型用父类引用调执行父类的用子类引用调执行子类的。私有方法也不存在重写那纯粹是子类自己定义了一个新方法。5.4 第25题接口与抽象类的辨析到了这一题牛客基本是把接口和抽象类放一起横向比较。出题形式常常是给你一堆关于两者的描述让你选出正确的那项。我把比较维度列成一张表刷题前对照着过一遍对比维度抽象类接口关键字abstract classinterface继承/实现单继承extends多实现implements构造器有子类super调用没有字段可以是普通字段也可以有常量默认public static final只能放常量抽象方法可以有可以有Java 8前都是普通方法有Java 8后default方法设计语义is-a模板can-do契约我个人的记忆方法是抽象类里可以藏隐私普通字段、可以有家底已实现方法像一个传家宝只能传给一个后代接口则是一张能力清单什么东西都能来认领而且可以认领多张清单。做题时还有一个很实用的技巧看到选项里说“接口里可以定义普通字段”直接排除。接口字段默认是public static final的也就是说它本质上是常量不是能随便改的普通属性。这个知识点老手可能觉得简单但对零基础来说确实是最容易模糊的地方。5.5 第26题综合设计与代码填空最后一题往往是综合应用让你补全代码或者设计类结构。常见的场景有给一个形状类层次结构Shape是抽象类里面有抽象方法getArea()让你定义一个实现类或者定义一个接口要求某个类实现它的方法。这种综合题核心考察的是“把概念落成代码”的能力。我建议拿到题先做三件事第一读清楚要求实现的是“继承”还是“实现”。关键词看extends还是implements别写反。第二看抽象方法/接口方法有几个一个都不能漏。漏一个子类没全部实现要么报错要么子类也必须是抽象类。第三写实现方法时必须加上Override注解。这个注解不是强制要求不写编译也能过但它能在你写错方法签名时立刻给出编译错误提示。写对方法名但参数列表写错不亚于挖坑埋自己。给你一个典型的设计题模板类似这样abstract class Shape { protected double area; public abstract double getArea(); } interface Describable { String getDescription(); } class Circle extends Shape implements Describable { private double radius; public Circle(double radius) { this.radius radius; } Override public double getArea() { return Math.PI * radius * radius; } Override public String getDescription() { return 一个半径为 radius 的圆; } }这段代码演示了“同时继承抽象类并实现接口”的写法。Circle既是Shape所以用extends又具备“可描述”的能力所以用implements Describable。两个关键字可以同时使用extends要写在implements前面顺序错了编译也会报错。6. 刷完这组题你大概率会踩的坑这一节是经验总结。我陪着不少新人刷过这个区间几乎每个人都会踩到下面几个坑。提前知道了你就能少浪费几个小时在无意义的报错里。6.1 构造器继承这个伪命题有人以为子类会“继承”父类的构造器所以new子类时可以直接传父类构造器的参数。这完全是误解。构造器不参与继承子类必须自己定义构造器并在第一行显式或隐式调用父类的某个构造器。记住父类构造器带参、子类构造器不写super编译必挂。这条规则属于“踩一次就再也不忘”的经典坑。6.2 重写时把访问权限越收越紧父类方法是public子类重写时一不留神写成默认访问权限不加修饰符或者改成private编译直接报错“attempting to assign weaker access privileges”。原因很好理解父类的public方法承诺了“谁都能来调”子类如果把它改成private等于单方面撕毁承诺那些通过父类引用调用这个方法的代码就全崩了。所以重写规则里有这么一条访问权限只能一样或者更开放不能更严格。反过来说你想从private改成public之类扩大可见性允许。6.3 返回值类型的问题重写时方法名、参数列表都对了但返回值写错类型编译也能报错。Java 5之后允许协变返回类型子类重写方法的返回值可以是父类方法返回值类型的子类型。比如父类返回值是Shape子类返回值写成Circle可以但如果父类返回值是Shape子类写成String绝对不行两者没有任何继承关系编译不通过。做题和写代码时如果看到返回值类型不匹配别慌先判断两者有没有父子类关系。6.4 接口字段被当普通字段用接口里的字段默认是public static final这意味着它必须被初始化而且初始化之后不可变。有的题会故意写出这样的代码interface Test { int num; // 编译错误常量必须赋值 }这行代码在Java里编译不过因为num没赋值。接口不会像类那样给字段一个默认值常量必须显式初始化。刷到这个考点时我想你已经记住了。6.5 忽略Override注解的预警作用 Override这个注解平时写不写都能运行但它最大的价值是“白送的编译期检查”。如果方法签名写错了比如少传一个参数编译器会立刻告诉你“这个方法并没有覆盖父类的方法”。这比运行时报错好找得多。我的习惯是凡是重写一律标Override。这是零成本、高收益的防御性写法刷题时要是输出代码片段让你挑错看到该标却漏标的方法也要留个心眼先检查它的签名到底对不对。7. 刷题之后的进阶思路别停在“会做题”最后说点实在的个人体会。这一组题刷完你可能会有一个错觉概念都懂了选择判断题基本能对。但这离真正的会用还有一段距离。检验标准很简单不给你题目让你自己从头设计一个稍微复杂一点的类结构比如“一个动物园系统有动物、饲养员、喂食行为孔雀能飞大象不能飞”你能否独立写出来如果能说明你是真的掌握了如果还是无从下手建议回头再把4.3那一节看两遍然后把第26题的模板题多练几遍。我在实际带新人的过程中总会让他们在刷完这一组后自己建一个小项目练手什么系统都行学生成绩管理、图书借阅、宠物商店唯一的要求是必须用到继承、多态、接口或抽象类中的至少三项。这么做不是为了完成任务而是让这些抽象概念真正和代码产生连接。等他们做完再回头看牛客这套题普遍反映是“简单了很多”。还有一个经验想分享这一组题学完你对后续的集合框架、异常处理、泛型理解会顺畅很多。ArrayList为什么能存任何对象Comparator为什么能用匿名内部类实现这些你以后会遇到的“神奇语法”底层都是这组题里的概念在支撑。所以这一步值得慢一点、稳一点宁可多花几天把它吃透也别赶进度。基础打牢了后面的路会越走越快。