如果你不是零基础也建议别跳过这一天——至少花20分钟把这四个东西串成一条线再走。很多干了两年的人写抽象类还是一脸懵一问到为什么这个类不能new就开始支支吾吾。原因就是当初这一章迷迷糊糊过去的。1. 继承到底解决了什么问题一个订单系统的三种写法对比先说一个最容易被忽略的事实继承不是Java的语法炫技它是为了解决代码复用和模型统一这两件事才出现的。很多人把继承背成子类继承父类六个字但根本不知道这六个字解决的是什么痛苦。1.1 没有继承的时代代码是怎么膨胀的假设你在做一个典型的电商订单系统。现在有一种订单叫普通订单里面有三个核心方法创建订单、计算金额、发送通知。你写了一个类跑得很顺。过了一个月产品经理说我们需要会员订单会员有折扣下单之后还要加积分。没有继承的写法是这样的复制一份普通订单的代码把类名改成会员订单然后在计算金额的方法里加上折扣逻辑在创建订单的方法里加上积分逻辑。再过了两个月产品经理又说我们要有企业订单支持月结支持多级审批。你又复制一份改改改。三份代码三个文件90%的核心逻辑是重复的。等你想改一个通用bug比如下单之前要查库存你得在三个类里改三遍。漏改一个就是线上事故。这就是没有继承的情况下代码自然膨胀的走向。它不是可能膨胀是必然膨胀。因为你在用复制粘贴对抗系统复杂度。1.2 继承登场把公共代码往上抽把差异代码往下沉继承的思路非常朴素把三个订单类里相同的部分抽到一个类里作为父类每个订单类型只关注自己特有的差异逻辑。// 父类只放通用逻辑 public class BaseOrder { protected String orderId; protected double amount; public BaseOrder() { this.orderId OID- System.currentTimeMillis(); } // 公共方法所有订单类型都能用 public void validStock() { System.out.println(校验商品库存订单号 this.orderId); } public void sendNotice() { System.out.println(发送下单通知订单号 this.orderId); } // 这个方法需要子类差异化但父类也得有保证协议统一 public double calculateAmount() { return this.amount; } }// 会员订单只写跟普通订单不同的部分 public class MemberOrder extends BaseOrder { private double discountRate 0.8; Override public double calculateAmount() { // 会员打八折amount是父类的成员变量子类直接能用 return this.amount * discountRate; } public void addPoints() { System.out.println(会员积分 (int) (this.amount * 1)); } }你发现关键点没有MemberOrder里面加了一个addPoints重写了calculateAmount其他方法比如validStock、sendNotice压根没写但在调用的时候照样能用。这就是继承的第一层价值——子类复用父类的非私有方法不需要重新实现。而重写calculateAmount这一步是整个面向对象的转折点。因为同一个方法名不同的子类可以表现不同的行为这不是代码复用这是协议统一。后续所有的高级特性——多态、接口、设计模式——全从这个转折点长出来的。1.3 Java只能单继承语法限制背后的设计逻辑有人会问为啥Java不让一个类继承多个父类Python里不是有多继承吗Java的设计者选了受限的单继承换来的是类型系统的简单可靠。如果允许一个类同时继承两个父类而这两个父类有同名同参的方法子类到底继承哪个这种冲突在C里就有处理起来极度痛苦。Java的取舍是类只能有一个直接父类但可以通过接口实现行为上的多重继承。也就是说一个子类只能是谁但可以同时会做很多事。你是一个Student继承自Person同时你也可以是Runner实现接口是Singer实现另一个接口。这套逻辑到今天依旧是主流设计。不是单继承不够用而是先确定你是什么再通过接口叠加你能干什么这件事用单继承加多接口的组合已经比多继承干净太多了。2. super和this这两兄弟的调用规则90%的报错都出在这里继承讲完之后紧接着就是super和this。这一对家伙是初学者第一个崩溃点。我见过太多人把super理解成父类的一个对象然后就开始胡思乱想。今天我直接给你最准确的那层理解。2.1 this的本质当前对象的引用this的本质是当前正在执行这个方法的那个对象的引用。只要你是在一个实例方法内部this就指向那个调用方法的对象。public class Order { private String orderId; public void setOrderId(String orderId) { // 左边this.orderId是成员变量右边orderId是方法参数 this.orderId orderId; } }这个例子大家都懂不加thisorderId orderId就变成了自己赋值给自己等于一点用没有。this在这里干的活很简单——区分成员变量和局部变量。this还有一个作用调用本类的其他构造器。这个后面跟super()一起讲。记住一句话this永远不会指向父类this永远指向当前这个对象。哪怕当前对象的类型是被父类引用着的运行时它依然指向那个实际的对象。2.2 super的本质不是父类对象的引用而是对父类成员的访问通道super是最容易被误解的关键字。不少初学者以为super就是父类对象其实不对。一个子类对象在内存里并不是一个父类对象加一个子类部分拼起来的。它就是一个完整的对象只不过对象的类型是子类而子类从父类那里继承了一堆成员而已。super干的活不是拿到父类对象而是给子类代码开了一个访问通道——帮我调用父类里那个被隐藏或重写的成员。举个例子public class Animal { protected String species 动物; public void eat() { System.out.println(动物在吃东西); } } public class Dog extends Animal { protected String species 犬科; Override public void eat() { // 调父类的eat()再补充自己的逻辑 super.eat(); System.out.println(狗啃骨头); } public void printSpecies() { // 当前类的species System.out.println(本类物种 this.species); // 犬科 // 父类的species System.out.println(父类物种 super.species); // 动物 } }printSpecies方法里this.species拿到的是子类自己的字段super.species拿到的是父类的那个字段。这正好印证了一件事super不是对象它只是让你能触达到父类的成员。重点super不能出现在静态上下文里。静态方法中压根没有当前对象这个概念自然也没有父类成员访问通道。谁写super.xxx放静态方法里谁编译报错。2.3 构造器调用链this()和super()为什么必须写在第一行这个规则几乎每个Java新手都背过但不理解就永远会犯错。规则是这样的构造器里调用另一个构造器——不管是this(...)还是super(...)——必须放在第一行。原因是Java语言在字节码层面定义了一套构造链的初始化顺序必须先完成父类构造再继续初始化子类自身。而this(...)虽然写的是调用本类构造器但它的终点往往也是super(...)。为了保证编译器能严格按先父后子的顺序生成字节码它强制要求这第一个调用出现在构造器最开始。一个典型的错误示范是这样的public class Student extends Person { private String school; public Student(String name, String school) { this.school school; super(name); // 错误super()不是第一行 // 会因为super(name)必须位于构造器第一行而编译失败 } }还有一个非常隐蔽的坑如果父类没有无参构造器子类构造器不显式写super(...)也会编译报错。public class Person { private String name; public Person(String name) { this.name name; } } public class Student extends Person { public Student() { // 编译报错父类没有无参构造器可调用 // 编译器默认帮你加super()但Person类没有无参构造函数 } }解决办法是子类构造器里显式调用super(name)。public Student(String name) { super(name); // 必须这样写 }这两条规则是连在一起的要么父类有无参构造器编译器默认补super()要么父类只有有参构造器子类必须在自己的第一行显式写super(...)。中心思想就一句话——子类诞生之前父类必须先完整初始化。2.4 成员变量隐藏与方法重写的差异讲清楚super和this必须顺带讲一个很多人毕业了都踩的坑成员变量的隐藏和方法的重写行为完全不一样。成员变量是隐藏hide的。子类和父类有同名成员变量时父类方法中访问的是父类的变量子类方法中访问的是子类的变量各看各的谁也不会覆盖谁。变量访问靠的是编译类型也就是你写这个引用时它被声明成了什么类型。Animal a new Dog(); System.out.println(a.species); // 动物走的是Animal这个编译类型方法则完全不一样无论你用什么类型的引用去接收只要实际对象是子类调用的就是子类重写后的方法。这就是动态绑定也叫运行时多态。Animal a new Dog(); a.eat(); // 输出动物在吃东西狗啃骨头走的是Dog的实现这个差异非常重要表格总结一下对比项成员变量成员方法表现形式隐藏hide重写override决定权编译类型运行类型行为差异各用各的不影响子类统一覆盖父类访问方式this.变量/super.变量明确指定this.方法调用重写后行为3. 抽象类把必须实现和可复用一起写进代码继承语法学完紧接着被引入的就是抽象类。抽象类的出现不是为了创造一个不能用的类而是为了给你一份带有缺口的需求说明书。3.1 从普通类到抽象类一个方法要不要有方法体这是个关键选择父类里有些方法你根本不想给它方法体因为不同子类的实现完全不一样你写死了反而碍事。典型例子就是图形类public abstract class Shape { protected String color; public Shape(String color) { this.color color; } // 所有形状都能描述自己但怎么画只有具体形状才知道 public abstract void draw(); // 所有形状都有面积但计算公式各不相同 public abstract double getArea(); // 这个方法是具体方法子类直接继承复用 public String getColor() { return this.color; } }重点来了abstract修饰的方法只有声明没有方法体。它的含义是——凡是继承我的具体类必须给我实现这个方法否则这个类还是抽象类。一个类里面只要至少有一个抽象方法这个类就必须声明成abstract。注意抽象类里是可以有普通具体方法的比如getColor()。这就是抽象类的核心价值它把大家都一样的部分用具体方法写好把大家都不一样的部分用抽象方法留出来强制子类补齐。3.2 继承抽象类后第一件事是补全所有抽象方法我见过不少新手继承抽象类之后忘了实现方法或者实现不全然后看着编译器的红线发呆。规则很简单子类继承抽象类必须实现父类的所有抽象方法除非子类自己也声明为抽象类。public class Circle extends Shape { private double r; public Circle(String color, double r) { super(color); // 调用父类构造器 this.r r; } Override public void draw() { System.out.println(画一个半径为 r 的圆形颜色 getColor()); } Override public double getArea() { return Math.PI * r * r; } }一个比较容易被忽略的点抽象类可以有构造方法但不能通过new创建实例。这个构造方法是你super(...)用的。你创建一个子类对象时子类构造器里调用super(color)抽象父类的构造器就被执行了。所以抽象类虽然不能new但它依然参与了对象的完整初始化过程。这一点想明白之后底下这句话你就理解了抽象类的构造方法不是为了创建父类对象而是为了把父类持有的字段初始化好。3.3 抽象类与普通类的区别一张表格全部划清很多人对抽象类发怵是因为搞不清楚它到底跟普通类差在哪。本质上就差在三点对比维度普通类抽象类能否new可以不可以只能被继承抽象方法不允许有可以有也可以没有子类约束不强制做什么强制实现所有抽象方法设计意图直接拿来创建对象作为基础模板被扩展注意最后一行的差异。普通类你也可以声明一个方法让子类重写但你不强制。抽象类则用abstract关键字把这个要求写进语法里——不实现就报错。强制这件事就是抽象类最重要的价值把设计上的约定变成编译器里的规则。4. 抽象类和接口怎么选一个被问了十年的经典问题教学目标到这一步通常会把抽象类和接口拉出来对比。这个对比的意义不是让你背区别而是让你在真实项目中能做出正确的选型。4.1 从设计意图出发抽象类是是什么接口是能做什么如果你去问一个资深开发者他大概率不会跟你背语法差异而是说一句抽象类描述的是本质接口描述的是能力。AbstractList是你的本质——它是一个列表骨架Comparable是你的能力——你可以比较大小。一个类只能有一个本质但可以有很多种能力。这就是为什么Java允许继承一个类实现多个接口。举个不太恰当但容易理解的例子你是一个员工继承自抽象类Employee这决定了你有工号、工资卡、工资计算逻辑。同时你还可以是Trainer能带新人也可以是Reviewer能评审代码。如果你只能有一个身份你想再多个能力就只能靠接口。这是选型的第一原则先确定这个类是什么再想它能做什么。本质不会变能力随便叠。4.2 四张对比表把差异罗列清楚第一张语法维度对比项抽象类接口Java 8之后关键字abstract classinterface成员变量各种修饰符的变量都有默认public static final常量普通方法可以写具体方法default方法可以写具体实现抽象方法abstract方法无方法体方法构造方法可以有new不行但super可以不能有构造方法继承数量单继承implements可以多个第二张语义维度对比项抽象类接口核心语义is-a是什么can-do能做什么设计视角模板复用契约达成扩展方向纵向扩展体现血缘关系横向扩展体现能力组合第三张实战选型维度场景特征倾向多个类有大量公共字段和方法优先抽象类需要把公共代码沉淀复用优先抽象类多个不相关类需要同一行为优先接口需要让某个类实现多种能力必须接口不强制实现但要统一协议优先接口第四张常见误区误区澄清接口里不能有方法体Java 8之后有default和static方法抽象类不能有构造方法可以有用于子类初始化父类字段接口比抽象类高级不存在谁更高级要看场景抽象类里必须都有抽象方法也可以全是具体方法但一般不会4.3 实际项目中的应用套路模板方法设计思路抽象类最经典的应用是把一个业务流程的骨架固定在父类里把可变的步骤留给子类实现。这一步可以说是模板方法模式的雏形也是抽象类设计价值的完美示范。举个支付场景public abstract class Payment { // 模板方法定义业务流程骨架是this写的细节是子类补的 public final void processOrder() { authenticate(); validateOrder(); pay(); postProcess(); } protected void authenticate() { System.out.println(统一身份认证); } protected void validateOrder() { System.out.println(校验订单有效性); } // 支付方式各不相同让子类去实现 protected abstract void pay(); // 支付成功后的操作默认什么都不做子类按需覆盖 protected void postProcess() { System.out.println(支付流程处理完成); } }这时候你发现子类只关心一件事pay()到底怎么做。public class AlipayPayment extends Payment { Override protected void pay() { System.out.println(发起支付宝支付); } } public class WeChatPayPayment extends Payment { Override protected void pay() { System.out.println(发起微信支付); } Override protected void postProcess() { super.postProcess(); System.out.println(微信支付额外发放立减金); } }调用的时候统一走processOrder()流程长度不会因为业务差异而改变需要变的只会在子类里变。这就是为什么抽象类在开源框架里遍地都是——它天然适合一套流程多种实现的业务形态。5. day09之后该往哪里走一条清晰的后继学习路径在前面已经完成了知识铺垫的情况下最后聊一聊学完这一天之后该接触什么。我给你的建议有三条递进路线。5.1 结合多态把继承和抽象类放进更完整的上下文中day09这天的内容不是孤立的它是面向对象三大特性封装、继承、多态的中段枢纽。day09学完多态就只在门口了。多态的核心场景是这样的一个父类引用指向子类对象调用那个被子类重写的方法表现出来的是子类行为。如果你把抽象类作为父类那多态的威力会立刻放大——你可以写一个方法参数用抽象类类型调用时传任意子类实例进去方法内部调那些抽象方法时天然就是各跑各的实现。public static void processShape(Shape s) { // 不管传Circle还是Rectangledraw和getArea都会执行对应的子类版本 s.draw(); System.out.println(面积 s.getArea()); }这个场景里Shape本身不能创建实例但它的引用却可以承载所有的子类。这种从父类角度统一处理由子类决定具体表现的能力就是多态。把这个知识融会贯通之后你需要去看看什么是Object类、equals()和hashCode()的规范、Override注解的真正作用。5.2 认识模板方法模式上面那个支付例子就是模板方法模式骨架最纯粹的演示。这个模式的本质是父类定义算法的骨架把某些步骤延迟到子类实现。它和抽象类天然合拍甚至可以说抽象类往往就是模板方法模式的实现载体。去翻一遍经典的开源框架你会发现无数模板方法的身影。某个框架的BaseActivity里公共初始化逻辑都被固定好子类只补几个空方法某个接口调度器的骨架方法把鉴权、限流、日志都写死真正干活的留给子类。你在学完day09后去观察这些代码理解速度会非常快。5.3 动手做一个小练习设计一个员工管理系统跟你说个最有效的巩固方法不是刷题而是做一个综合小练习设计一个员工管理系统的类。要求不复杂——抽象类Employee带两个属性工号、姓名一个抽象方法calculateSalary()一个具体方法getInfo()输出基本信息。然后定义三种员工正式工、小时工、实习生。三种员工都继承Employee各自实现calculateSalary()。再写一个管理类提供一个方法打印所有员工的工资参数列表是Employee[]。做完这个练习你会同时把今天的四个知识点全串起来了继承三个子类、super调用父类构造器、this区分参数和成员变量、抽象类Employee的设计。这个过程你要是能自己走一遍就比你看五遍教程管用。实际带队带学员这些年这个练习我让至少上百人做过效果一直稳定。它考察的不是背了多少定义而是你能不能真的把抽象类的缺口设计用到业务里去。能用到你就真的过关了。