Java面向对象进阶:封装、继承、多态、接口如何串成一条线
大二那年Java课程终于进入面向对象进阶封装、继承、多态、static、接口、重写。说实话第一章听下来我是有点虚的——类、对象、构造器这些名词单个拿出来都认识可真让我独立设计一个类的继承体系我只会把所有字段写成 public方法一股脑塞进同一个类。这篇记录就是想复盘我从能读懂例子到能自己写出来的全过程给同样卡在进阶阶段的同学一点参考。我不会去复读教科书里说滥的定义重点放在当时真正让我卡壳的几个问题封装到底保护了什么继承为什么不是为了省代码多态怎么让代码变得灵活接口和抽象类到底什么时候该用哪个。如果你刚学完语法、正往面向对象过渡或者准备面试想把基础打牢这篇应该能帮上忙。1. 从会写类到懂对象进阶阶段到底卡在哪1.1 语法都能看懂代码却还是面向过程的刚进入进阶阶段的那两三周我的状态很像很多人吐槽的那种上课每种语法都听懂了一写练习就打回原形。记得第一次做用Java计算不同图形面积的练习我的第一反应是写一个ShapeUtil类里面放circleArea(double r)、rectangleArea(double w, double h)、triangleArea(double a, double b, double c)三个方法然后在main里用switch根据用户输入的类型去调用对应方法。代码能跑老师也没给低分但我知道这是典型的面向过程写法因为只要再增加一个梯形我就要去打开这个类在switch里多塞一个case多写一个方法。问题不出在语法而出在我根本还没有它是对象的思维方式。后来我把代码改成定义一个Shape父类内部有一个calculateArea()方法再让Circle、Rectangle各自继承并重写这个方法。主程序要计算面积时不需要知道具体是什么形状只要拿到一个Shape引用调用calculateArea()就行。新增梯形时我只写一个Trapezoid extends Shape文件主程序一行都不改。同样是完成作业前后两种做法的维护成本完全不一样。这个对比让我第一次意识到面向对象不是 Java 的一种格式而是一种拆分责任的方式。1.2 面向对象真正的核心把职责划分清楚很多教材讲解封装、继承、多态时喜欢把它们拆成三个孤立知识点来讲导致初学者觉得这是三个必须分别背诵的东西。我的体会是这三个词其实都在回答同一个问题一段代码里谁该负责什么谁不该知道谁封装说的是对象自己管理自己的数据继承说的是子类对父类说我是你的一种多态说的是调用方对实现类说你不用告诉我你具体是谁我按接口做事就行。把它们放在职责划分这一个框架里理解就不再是死记硬背了。我到现在还喜欢用一个类比去食堂打饭。你不需要知道后厨具体是哪个师傅切的菜、哪个窗口掌勺你只要知道 3 号窗口负责盖浇饭把餐盘递过去就行。后厨的细节是封装起来的3 号窗口能出盖浇饭是约定好的服务能力对应接口今天 3 号窗口实际站的是张师傅还是李师傅不影响你打饭这就是多态。面向对象设计的很多原则本质都是在模拟这种分工明确、互不干扰的现实协作。1.3 六个关键词其实是一条线封装、继承、多态、static、接口、重写这六个词放在一节课里看似互不相干实际上它们的逻辑是一条线的封装让类的内部结构稳定继承让类之间出现层次重写让子类能替换父类的行为多态让调用方安心面向父类/接口编程接口把能力从继承树中单独抽出来static 则负责描述类级别的共享数据与工具行为。下面的内容我会按这个线索展开每一项都配合我当年踩过的坑和后来验证过的写法这样复习起来不至于零散。2. 封装把属性藏起来把操作交出去2.1 private不是不让别人用而是不让你乱用很多人第一次学封装时都会有个疑问属性设成 private外面访问不了还得写 getter/setter这不是脱裤子放屁吗我的体验是直接暴露字段短期看确实省事代码多写两行但项目一旦超过几百行或者有另一个人跟你协作直接给字段赋值就会变成一件非常危险的事。举个我自己的案例。大二做课程设计我写了一个Student类第一次偷懒把所有字段都设成 public。当时有个需求是给学生的平时分、实验分、期末总分加权求和我在另一个类里直接写了student.avgScore -1;这种代码——因为这批学生的成绩还没录入完我想先用一个占位值结果后面所有统计计算全出现了负数成绩。排查了一整晚最后才发现是某段初始化逻辑把一个非法值塞了进去。改回私有字段在 setter 里加入值必须在 0~100 之间的判断后这个坑就再也没出现过。public class Student { private String name; private int age; public void setAge(int age) { if (age 0 || age 150) { throw new IllegalArgumentException(年龄不合法: age); } this.age age; } public int getAge() { return age; } }这段代码看起来简单但它表达了一个关键思想对象的字段是否合法、是否一致只能由对象自己保证外部没有资格直接改动。setter 不是铺一层访问通道而是一道校验关卡。2.2 封装真正保护的是不变量教科书不太会讲不变量这个词但它特别重要。所谓不变量就是一个对象在生命周期的任意时刻都必须满足的性质。比如学生对象的年龄不能是负数银行账户对象的余额不能变成负数除非允许透支订单对象的状态只能在固定几个值之间切换。如果字段是 public调用方可以随手把它设置成任何值对象的不变量就立刻崩溃了。封装的方法setter、getter、业务方法就是把这些规则的执行权收回类内部。我还做过一个更典型的练习Account类。如果用public double balance;这种写法调用方既能直接加钱也能直接改成负数。正确的做法是只提供deposit(double amount)和withdraw(double amount)两个方法在方法内部判断amount 0、balance amount这些条件。这样调用方想做任何操作都只能走这两条通道规则就没法绕过去。封装的核心价值不在隐藏而在统一入口、拒绝非法状态。理解这一点之后你看那些 Lombok 全都换成 Data 把字段敞开的做法就会有判断力了——小 demo 无所谓但正式项目中无脑 Data 很容易把自己的业务规则暴露没了。2.3 封装不到位排查 bug 的时间能翻好几倍第二个学期我给社团写一个简单的图书借阅系统里面有Book类一开始我把borrowedTimes、available这些字段都设成 public。结果有一天发现统计页面的借阅次数莫名其妙多了几倍。排查过程非常痛苦一会儿怀疑是 SQL 查询写错了一会儿怀疑是循环里 add 了多次——最后才定位到是有个同学在某个类里直接写book.borrowedTimes而那个统计逻辑也读了同一个字段两边叠加自然就变成双份。那次以后我立了个规矩所有字段一律先写 private哪怕暂时用不到 getter/setter。这不是教条而是给未来的自己减少谁改了我的数据这种取证工作。Java 里字段的访问权限从private到public每放宽一级就要有一个确实需要暴露的理由。这是我在封装上学到的第一课也是后来写大型作业时最受益的一课。3. static属于类的数据与行为别让对象背锅3.1 static变量一份数据全体对象共享static 是我当时觉得理解起来最简单、用得却最容易出错的知识点。先说过关的部分static 修饰的成员变量不属于任何某个对象实例而是属于类本身。类加载到 JVM 时它就分配好内存所有对象共同访问这一份数据。典型应用是统计创建了多少个对象public class Student { private static int count 0; public Student() { count; } public static int getCount() { return count; } }每new Student()一次count 就在原来基础上加 1不管你创建的是哪一个学号的学生大家读到的 count 是同一个。如果这里你不小心把 count 写成了实例变量统计就会归零或者乱七八糟。这个知识点面试也爱问本质就是一实多 vs 一实一的区别。3.2 static方法里为什么不能直接访问实例成员static 方法的问题比 static 变量复杂一点。我当时写过下面这种代码直接被编译器拦下来public class Demo { private int x 10; public static void printX() { System.out.println(x); // 编译报错无法从静态上下文中引用非静态变量 x } }后来我把对象还没有被创建时static 方法就可以被调用这句话嚼透了。Demo.printX()可以在不 new Demo 的情况下直接调用可此时x属于哪个对象没有人知道。全局环境里根本没有当前对象的概念所以 static 方法里没有this自然也无法访问实例成员。这个理解放在实际中就是如果你需要读取对象的属性这个方法就该是实例方法不应该用 static。一个常见的坏味道是工具类方法里偷偷依赖了某个全局静态变量比如下面 3.4 会说的状态污染源头就是这个。3.3 静态代码块和工具类static 的正确用法静态代码块static { ... }用于在类第一次加载时完成一次性的初始化。比如读取配置文件、初始化数据库连接池、加载本地词库等。它的特点是只会执行一次JVM 加载类时才执行。注意它和构造器的区别构造器是每次new都执行静态块是整个类生命周期只执行一次。还有一个知识点容易被忽略多个 static 块按代码书写顺序执行父类的 static 块先于子类的 static 块执行。这个顺序如果你以后面试被问类加载过程会经常用到。static 方法最正确的打开方式是工具类比如Math.max()、Collections.sort()。工具类里的方法往往不依赖任何对象状态输入参数、返回结果干净利落。我后来的习惯是如果一个工具类内部没有任何成员变量只有一堆 public static 方法就把它的构造器私有化防止别人 new 它public final class MyStringUtils { private MyStringUtils() { } public static boolean isEmpty(String s) { return s null || s.length() 0; } }这样别人一看就知道这是个纯工具类也不会误用。3.4 把static当全局状态用的坑static 变量虽然所有对象共享但共享并不总意味着正确。我大二下学期学 Servlet 和 JSP简单写了个在线留言板把待显示的消息存在一个static ListMessage messageList里。本地测试只有一个用户一切正常后来打开两个浏览器窗口同时访问一个窗口添加的留言跑到了另一个窗口的页面里数据互相串。起初我以为是缓存问题后来反应过来static 数据是进程级共享的JVM 里所有线程看到的是同一份数据两个用户请求相当于两个线程共享同一个 List互相污染太正常了。那次踩坑让我形成两个认知第一static 数据天然是全局可见、线程不安全的在多线程环境下要非常谨慎第二不是所有想要两个地方共用一份数据的需求都该用 static很多场景其实应该用实例共享、依赖注入、数据库访问等方式。static 适合描述类本身的固有属性比如常量、计数器、工具方法而不是用来当全局垃圾桶。面试中如果被问到为什么很多框架不推荐使用 static 字段保存状态现在你也能答上来了。4. 继承与重写复用只是开始改写才是核心4.1 继承的初衷是复用但真正价值是is-a刚开始学继承时最容易产生的误解是继承 复用代码。我见过同学写一个DBUtil extends Student的操作全班哄堂大笑——明显只是为了使用 Student 里的某个方法才继承两者毫无血缘关系。正确判断继承是否成立就看一句子类是父类的一种吗Dog is an Animal成立DBUtil is a Student不成立。不成立却硬要继承会带来非常严重的语义污染父类的方法全部暴露给子类接口变得混乱后续维护的人完全猜不到这个继承是干什么的。教科书上常说的代码复用其实只是继承的副产品真正该继承的场景是同一类事物的公共属性和行为。Animal 有name、age有eat()、sleep()Dog 和 Cat 都继承了这些东西然后各自重写sound()。这样写有一个巨大好处公共代码只写一份变化的部分留给子类各自实现。如果我只是想复用代码优先选择组合而不是继承比如class Car { private Engine engine; }Car 不继承 Engine 但也拥有了 Engine 提供的启动能力。组合优于继承这个原则在我后来的设计中被反复验证。4.2 重写的规则清单我到现在还在对照检查重写是继承里最容易写错、但 IDE 提示又特别委婉的知识点。我先说规则再讲我踩过的坑。重写必须满足对比项重写Override要求方法签名子类方法名、参数列表必须与父类方法完全一致返回类型可以是父类返回类型的子类型协变返回不能是父类返回类型的父类访问权限不能比父类方法的访问权限更严格比如父类是 public子类不能是 protected异常声明不能抛出比父类方法更宽泛的受检异常可以更少或更窄不可重写的方法private 方法、static 方法、final 方法不能重写构造器构造器不能重写只能通过 super 调用我在上学期写过最典型的错误是父类方法public void sound()我在子类里写成了void sound()当时一点报错都没有因为 Java 编译器会直接把它当成子类独有的新方法而不是重写。程序运行后多态调用时父类引用调用的还是父类那个 sound子类新方法根本没人理。我排查了很久气得够呛。后来我养成了一个好习惯在子类方法上写上Override注解。这个注解不是为了好看而是让编译器帮你检查到底是不是真的重写了。没写 Override 时方法签名写错是静默的写了之后一旦签名对不上编译直接报错。这也是我强烈建议所有新手养成的习惯——省下的调试时间远超打这几个字母的时间。4.3 super与this的纠缠构造器里的隐藏问题继承体系下构造器调用顺序是个特别容易出 bug 的点。Java 规则是子类构造器必须调用父类构造器且super(...)必须写在子类构造器的第一行。如果父类有无参构造器子类可以不写编译器自动加一句super()如果父类没有无参构造器比如public class Animal { private String name; public Animal(String name) { this.name name; } }那么子类构造器就必须显式super(name)否则编译报错。我当时写Dog类时偷懒没写构造器直接收获一片报错后来才明白父类初始化是子类初始化的前提你创建一个DogJVM 执行顺序是这样的先分配内存、初始化 Object → Animal 的构造器 → Dog 的构造器体。所以super必须第一条因为父类还没初始化完子类不可能先干活。另外注意this(...)和super(...)不能同时出现在同一个构造器里因为它们都要占据第一行。如果子类构造器用this(...)调用了另一个子类构造器另一个构造器第一行再写super(...)最终父类仍只被初始化一次。这个链式关系理解透了为什么构造器调用顺序是先父后子这类面试题就不会再怕。4.4 继承过度带来的耦合一个真实的连锁事故继承看起来省事但它的代价是父类和子类深度耦合。我在做课程设计时吃过一次亏父类Animal里我写了一个getInfo()方法返回格式是名称 name 年龄 age。后来需求变化需要在名称后面加上昵称我就改了父类getInfo()的实现。结果第二天发现Bird子类输出的信息格式全乱了因为它自己也重写过一个getInfo()里面super.getInfo()返回的字符串格式变了它这边的拼接逻辑就全错。改父类一行代码波及了三个子类。这不是说不能继承而是说继承层级越深、父类公开方法越多牵一发而动全身的概率就越高。我现在设计类层次时第一反应是这个继承真的必要吗然后控制继承深度最多三层以内。同时父类中会被子类重写的方法尽量少依赖父类其他可重写方法。比如父类getInfo()内部调用了另一个可重写方法getNickname()子类一旦重写getNickname()getInfo()的输出也随之变化——这种隐式联动在设计时要格外当心。如果你遇到想复用方法但不想背上继承的耦合优先考虑组合或者接口默认方法。5. 多态编译看左边运行看右边5.1 向上转型一开始真的不理解多态是我学面向对象时最玄学的部分。第一次看到Animal a new Dog();这种代码我心里想的是我既然要 new Dog为什么不直接写Dog d new Dog();呢绕着弯用一个父类引用指向子类对象除了让我看不到子类特有方法之外还有什么好处直到我写了下面这段代码才明白它的价值Animal dog new Dog(); Animal cat new Cat(); dog.sound(); // 输出汪汪 cat.sound(); // 输出喵喵你想一下如果用Dog dog new Dog(); Cat cat new Cat();的确也能得到同样的输出。但如果你要把这些动物放进一个 List 统一管理呢ListDog和ListCat是两个不同类型的容器你要分别遍历而ListAnimal可以同时装下 Dog 和 Cat一次遍历搞定。多态的第一层好处就是让不同的子类对象可以装进同一个集体被统一处理。这在大二刚学的时候真的很震撼因为之前我从来没有用容器的视角想过代码结构。5.2 动态绑定方法调用的真相多态能成立靠的是 JVM 的动态绑定机制。编译阶段编译器看到的是声明类型Animal a它会检查Animal里有没有sound()这个方法有就通过编译。运行阶段JVM 看的是对象new Dog()的真实类型它发现 Dog 重写了sound()就执行 Dog 版本。可以用一句口诀牢牢记住编译看左边运行看右边。左边声明类型决定你有哪些方法可以调用右边实际类型决定这些方法真正的实现是谁的。我一开始犯过一个很典型的错误用Animal animal new Dog();后试着调用animal.swim()因为 Dog 实现了游泳接口结果编译直接报错。原因就是编译器只看 Animal 类里有没有swim()没有就拒绝。这个错误让我最早理解了编译看左边的含义。而运行看右边则体现在异常堆栈和日志里明明调同一个方法不同子类打印出的日志完全不同说明找的是子类实现。理解动态绑定后多态不再是玄学而是一套明确的 JVM 规则。5.3 多态最大的价值面向抽象编程多态有什么用最经典的回答是开闭原则对扩展开放、对修改关闭。回到第 1 章那个图形面积计算如果我用 if-else 判断类型那每次新增图形都要改原来的判断代码如果我用Shape引用接收所有图形对象调用calculateArea()那么新增Trapezoid时原来调用处的代码一行都不用动。这就是面向抽象编程方法参数要尽量写父类型或接口类型而不是写死某个具体子类。public static void printArea(Shape shape) { System.out.println(面积为 shape.calculateArea()); }这个方法的参数是Shape意味着以后不管新增多少个 Shape 的子类printArea都不变。调用方不需要关心你传进来的是 Circle 还是 Rectangle它只关心这是一个 Shape它有 calculateArea 能力。这不只是语法上的技巧更是架构层面的思路让上层代码依赖抽象而不是依赖实现。面试中经常被问多态的意义是什么你可以回答三层统一处理不同类型、增加新类型不改旧代码、调用方与实现类解耦。5.4 向下转型与instanceof强转前先判断有向上转型自然就有向下转型。向上转型让对象失去了子类特有方法如果之后确实需要调用子类独有的方法就要向下转回具体类型Animal animal new Dog(); if (animal instanceof Dog) { Dog dog (Dog) animal; dog.fetch(); }如果没有instanceof判断直接强转遇到Animal animal new Cat();再(Dog) animal运行时会抛出ClassCastException程序直接崩溃。我在这里摔过一跤在动物园管理系统的循环里拿到一个 Animal 就强制转成 Dog 去调用一个只有 Dog 才有的看门方法结果列表里出现一只 Cat整个页面 500 报错。后来我养成了向下转型前必须 instanceof的肌肉记忆也顺便记住了instanceof会先判断右边类型是不是左边的父类/接口/同类是才返回 true否返回 false。值得提一句的是Java 16 之后instanceof支持模式匹配可以直接写if (animal instanceof Dog dog) { dog.fetch(); }省掉一次强转。但大二阶段用传统写法理解原理更扎实等到面试时能说出新版模式匹配可以让强转更安全这种话反而是加分项。6. 接口把能力从继承体系里拆出来6.1 为什么有了抽象类还需要接口学了抽象类之后我一度觉得接口是多余的东西抽象类也能定义抽象方法也能让子类实现接口能做的抽象类都能做。直到老师讲了一句Java 类只能单继承我意识到接口的不可替代性。一个类只能继承一个父类但可以实现多个接口。比如一只Dog要继承自Animal同时它还会游泳、还会看门、还会搜救这时就不可能把游泳、看门、搜救全部塞进 Animal 这个父类里——会游泳的不只是狗鸟会飞鱼会游这些能力横跨多个继承分支。接口的作用就是把这种能力从继承树里单独剥出来谁有这个能力谁就去实现它。public interface Swimmable { void swim(); } public interface Runnable { void run(); } public class Dog extends Animal implements Swimmable, Runnable { Override public void sound() { ... } Override public void swim() { ... } Override public void run() { ... } }这种设计对应一个经典原则组合/能力优先于继承。面向对象的世界里你说一个东西是什么只能用一次继承但你说一个东西能做什么却可以有很多次接口。6.2 默认方法与静态方法JDK8 之后接口的大变化我学接口时已经是 JDK8 时代但很多网上教程还在讲接口里只能有抽象方法这其实已经过时了。Java 8 给接口增加了两个重要的新成员默认方法default method和静态方法static method。默认方法是解决给已发布接口新增方法会不会破坏所有实现类的问题的。你想一个场景一个接口已经被十个类实现这时需求方要求接口再增加一个log()方法如果按老规矩这十个实现类全部要补上实现代码否则编译失败这是巨大的改动。默认方法允许接口提供一个带默认实现的方法体实现类可以不去实现直接继承默认逻辑也可以重写覆盖。比如public interface Logger { void log(String message); default void warn(String message) { log([警告] message); } }这样只新增一个warn默认方法所有实现类一行都不用改就能使用新能力。静态方法则用于提供与接口相关的工具方法比如Comparator.comparing(...)。这两个特性学完之后我对接口的认识就从纯抽象契约升级成带默认行为的扩展契约。不过要提醒一句默认方法虽然方便但接口里塞太多业务逻辑也会让接口变重、变复杂实际的团队开发中应该谨慎使用默认方法更多用来做兼容旧实现而不是堆积公共代码。6.3 抽象类 vs 接口到底怎么选这是大二学生必会遇到的选择题。我当年靠一个粗暴的方法记住有关系用抽象类有能力用接口。再展开一点维度抽象类接口语义is-a子类是父类的一种can-do实现类具备某种能力构造器与字段可以有构造器、实例字段、非抽象方法不能有实例字段只能有 public static final 常量JDK8 可以有默认/静态方法继承限制一个类只能继承一个抽象类一个类可以实现多个接口典型场景模板方法模式、抽取出公共状态策略模式、回调、模块与模块之间的契约修改成本加新抽象方法要改动所有子类用默认方法可降低对实现类的破坏我接过一个实际例子做一个员工管理系统所有的Employee子类都有name、salary等共同状态工资计算也有共同模板这种场景用抽象类比如BaseEmployee更合适。但如果系统需要一个Auditable审计能力允许能审计的对象全部实现它那就该用接口。判断标准永远是语义和扩展方向而不是哪个类方法多。面试官问接口和抽象类有什么区别你把这张表从头到尾说一遍基本稳了。6.4 接口的多实现设计时的能力混合接口带来的另一个思路是能力混合mixin。假设我在做一个动物园系统动物们有不同技能狗会游泳、鸟会飞、鸽子既能飞又能走。如果完全依赖继承这个能力矩阵很难表达因为这些能力会出现在不同分支里强行用继承做会弄出一堆双亲重复继承的麻烦。接口就可以很干净地组合public class Pigeon extends Bird implements Flyable, Walkable { // ... }写到这里我还想提醒一个实战经验接口命名最好用形容词或能力含义的词比如Runnable、Serializable、Comparable这比AnimalInterface这种命名规范得多。一个接口就是一个承诺承诺实现类具备run()、compareTo()这些能力。设计良好接口比写满优雅实现的类更重要因为整条业务链上游依赖的都是接口接口一旦定下来下游实现才稳定。7. 小项目复盘动物园管理系统怎么把这些串成一条线7.1 需求与类设计学完上述概念后我做了一个很小的动物园管理系统作为课程设计代码量不大但每个概念都用上了。需求是园区里有 Dog狗、Cat猫、Bird鸟、Duck鸭每种动物都能发出叫声部分动物会游泳部分动物会飞主程序需要统一让所有动物轮流自我介绍并展示才艺。类图设计是这样的abstract class Animal存放name、age定义抽象方法sound()提供getName()、getAge()。interface Swimmable定义swim()。interface Flyable定义fly()。Dog extends Animal implements Swimmable重写sound()为汪汪实现swim()为狗刨式游泳。Cat extends Animal重写sound()为喵喵。Bird extends Animal implements Flyable重写sound()为叽叽喳喳实现fly()。Duck extends Animal implements Swimmable, Flyable重写sound()为嘎嘎实现swim()和fly()。Zoo类持有ListAnimal animals提供addAnimal(Animal animal)和showAll()。这个设计里抽象类把有哪些动物公共属性沉淀下来接口把会游泳、会飞这些能力切出来重写在每个子类里体现多态则体现在 Zoo 类只依赖Animal这个抽象类型。static 也没有缺席我在 Animal 里加了一个private static int totalAnimals每次创建动物totalAnimals用来统计园区里的动物总数。7.2 核心代码与运行效果核心代码摘关键部分省去 getter/setter 的篇幅abstract class Animal { protected String name; protected int age; protected static int totalAnimals 0; public Animal(String name, int age) { this.name name; this.age age; totalAnimals; } public abstract void sound(); public void introduce() { System.out.println(我是 name age 岁); sound(); } public static int getTotalAnimals() { return totalAnimals; } } interface Swimmable { void swim(); } interface Flyable { void fly(); } class Dog extends Animal implements Swimmable { public Dog(String name, int age) { super(name, age); } Override public void sound() { System.out.println(name 发出汪汪声); } Override public void swim() { System.out.println(name 用狗刨式游泳); } } class Zoo { private ListAnimal animals new ArrayList(); public void addAnimal(Animal animal) { animals.add(animal); } public void showAll() { for (Animal animal : animals) { animal.introduce(); if (animal instanceof Swimmable) { ((Swimmable) animal).swim(); } if (animal instanceof Flyable) { ((Flyable) animal).fly(); } } System.out.println(园区当前动物总数 Animal.getTotalAnimals()); } }运行结果类似我是旺财2岁 旺财发出汪汪声 旺财用狗刨式游泳 我是雪球1岁 雪球发出喵喵声 我是小飞1岁 小飞发出叽叽喳喳声 小飞翅膀扑腾着飞了起来 园区当前动物总数4注意一个设计细节showAll()里用instanceof判断当前动物是否具备游泳/飞行能力没有就不展示对应才艺。这样未来无论往园区里加什么新动物比如加一只企鹅只要让它 implementsSwimmable主程序的 showAll 一行都不用改多态和接口就已经把扩展点留好了。7.3 我在这个小项目里踩过的三个坑第一个坑是Override缺失。写Duck的sound()时我一不留神写成了sound(String voice)编译不报错、运行出来鸭子不嘎嘎而是静默无声。排查了好几分钟才发现这根本不是重写而是一个全新的方法。后来我把所有子类方法都补上OverrideIDE 立刻在名字旁给出警告这个坑就再没出现过。第二个坑是向下转型没有 instanceof。我最初在 showAll 里写的是((Swimmable) animal).swim();对所有动物强转Cat 没有实现 Swimmable运行到猫就抛ClassCastException。把猫放进动物园导致整个系统崩溃真是刻骨铭心。后来我改成先 isinstance 判断再强转。大二阶段考这个知识点的人很多看我文章的应该不少是天坑区的难友。第三个坑是 static 变量的共享。我在 Zookeeper 里写了一段新增动物时顺便totalAnimals结果发现 Animal 构造器已经加过一次又加一次总数翻倍。教训非常直接static 状态被多个入口修改时一定要确保只有一处负责更新最好把计数逻辑集中放在构造器里外部不要再干预。这也呼应了第 3 章讲的 static 滥用问题。7.4 回头看六个关键词如何串成一条线等这个项目跑通之后我再回头翻课本原本分散的知识点瞬间就串起来了。封装让每个动物的 name、age 只能通过构造器和方法设置别人没法把一个动物改成负年龄继承把 Animal 公共的结构沉淀下来让 Dog、Cat 不需要重复写 name/age 相关代码重写让每种动物发出不同的叫声同一个introduce()在运行时走向不同子类实现多态让 Zoo 只依赖 Animal 这个抽象类型加新动物根本不需要改 Zoo接口把会游泳会飞从继承树里拆出来让 Duck 这种多重能力不会撞单继承static 负责统计园区总动物数它属于动物类这个整体而不是属于任何一只具体的动物。这六个关键词从来不是独立考点它们共同构成一套如何设计稳定、可扩展代码的方法论。7.5 给学弟学妹的实操建议这篇记录的最后我想用自己的经验给大家几条特别实用的建议。第一学到封装、继承、多态后一定要立刻动手做一个几十个类的小项目哪怕是最土的动物园、学生管理系统都行光看代码和手写代码的理解深度完全不在一个量级。第二所有涉及继承的方法都写上 Override让编译器帮你抓错误。第三多态调用的调试技巧在 IDE 里打断点看实际对象类型是 Dog 还是 Animal你会发现动态绑定非常直观。第四接口的抽象层次不要做太深接口定义 3~5 个方法就够了方法太多要么是一个大而全的上帝接口要么就该考虑拆分成多个能力接口。第五也是最重要的一条面向对象不是说把所有东西都变成类就完事了而是让你思考每个类该承担什么责任、对外暴露什么能力、如何减少类与类之间的耦合。代码里出现大量为了用而用的继承、接口比不用面向对象更可怕。现在回头看大二这学期的 Java 学到最后就浓缩成几句话对象不要裸奔字段要封装继承要建立在 is-a 关系上重写是子类表达差异的方式多态让面向抽象成为可能接口负责切开复杂的能力矩阵static 只描述类级别的东西别拿来当全局垃圾桶。如果你也想把这份面向对象进阶真正吃透最好的办法就是找个小项目亲手把这些概念都写一遍踩完这些坑之后你再看面试题、再看别人的框架源码思路会清晰很多。