Java面向对象核心:继承、super、this与抽象类一次讲透

发布时间:2026/10/9 19:55:08
Java面向对象核心:继承、super、this与抽象类一次讲透
学Java要是没把继承、super、this、抽象类这几个概念弄明白后面但凡涉及到类设计、框架源码、设计模式的代码都会读得很吃力。这不是夸张——我见过太多人循环数组写得飞起一到继承这里就开始犯迷糊super能不能不写this()和super()为什么不能同时出现抽象类到底能不能new这篇文章就是要把这几个问题一次说透。我会从最朴素的代码例子出发讲清楚继承到底解决了什么问题、this和super各自扮演什么角色、抽象类和普通类差在哪、以及抽象类和接口该怎么选。适合正在学JavaSE面向对象这一章的初学者也适合学过一遍但总是“道理都懂、代码写错”的同学。后半部分我还会放一个完整的员工管理Demo带你把知识点串起来跑一遍。1. 继承为什么说它是面向对象复用的基石1.1 没有继承时代码是怎么“膨胀”的很多教材一上来就甩出继承的定义说继承是“子类拥有父类的属性和方法”。这句话背下来容易但你不一定真能感受到它解决的是什么问题。我习惯先让学员写一个“没有继承”的版本体验一下什么叫痛。假设现在要设计三个类学生类、教师类、辅导员类。它们都有的东西是姓名、年龄、工号或学号都要吃饭、睡觉、自我介绍。你写第一版学生会觉得没什么写第二版教师类开始觉得重复写第三版辅导员类的时候光标闪烁半天你已经意识到自己就是在复制粘贴。复制粘贴三个类表面上没什么问题。但需求一变就露馅了比如“自我介绍”要求增加一条“所属院系”你需要同时改三个类改的时候少改了一个类编译不报错、测试测不出来直到上线后某一天用户才发现教师自我介绍里没有院系信息。这就是代码膨胀的典型代价——重复代码越多维护成本越高出错的概率越大。继承解决的就是这个问题把公共的属性和行为抽取到一个父类里让多个子类复用。学生、教师、辅导员都从同一个父类派生父类里改一次所有子类通通生效。1.2 extends语法与继承的关键规则Java里继承用extends关键字语法很简单class Person { String name; int age; void introduce() { System.out.println(我叫 name 今年 age 岁); } } class Student extends Person { String studentId; }这里Student不需要再声明name和age也不需要再写introduce()因为继承了Person。你直接new一个Student就能调用name、age和introduce()。但有几个规则初学者必须刻进脑子里第一Java的类只支持单继承。一个类只能有一个直接父类不能写class A extends B, C。这是Java设计上的取舍为了避开多继承带来的菱形问题。菱形问题简单说就是如果B和C都有同一个方法d()D同时继承B和C那D到底该用谁的d()Java干脆禁止类的多继承从根源上消灭这个问题。第二构造方法不会被继承。父类的构造方法是在子类构造过程中被调用的但子类并不会自动获得同名的构造方法。很多新手以为父类有带参构造子类就可以直接new出来结果编译报错找不到构造方法原因就在这。第三父类的私有成员private修饰的子类访问不到。这个“访问不到”不是说子类没有继承到而是子类对象里确实有这份数据只是没有直接访问权限。你需要通过父类提供的public或protected方法去间接访问。这一点后面讲super的时候还会再提。1.3 不同的继承方式该怎么选继承不是只有“一个类继承另一个类”这一种形态。在实际代码里继承方式大致分这么几种单继承是最常见的一个子类对应一个父类比如Dog继承Animal。多层继承则是A继承B、B继承C形成一条链。比如经理类继承员工类员工类继承人类这种层次结构只要不深可读性还是很好的。问题是很多初学者容易把层级拉得特别长A继承BB继承CC继承D最后整个类体系变得像一口深井调试的时候想找一个字段的声明位置都得翻半天。接口多实现是另一种“继承”方式它规避了Java单继承的限制。一个类可以实现多个接口接口里只定义行为契约不关注具体的属性。所以有一种经验总结说能用接口表达的“能力”不要硬塞进继承体系里。比如“会飞”是一种能力飞鸟继承动物同时实现飞行接口飞机不继承动物但也能实现飞行接口这样设计就灵活得多。横向对比一下其他语言也能加深理解。JavaScript的继承走的是原型链本质上是通过对象的隐式原型逐级向上查找属性Python支持多继承但引入了MRO方法解析顺序来处理菱形问题理解成本比Java高不少。Java选择单继承接口多实现算是平衡了灵活性和复杂度。作为初学者先把Java这一套吃透就够了不用急着横向比较。2. this和super一对容易混淆的“上下级”2.1 this的三种典型用法this在Java里代表的是“当前对象”的引用。注意它只能出现在实例方法、构造方法这些和对象相关的场景里static方法里不能用this因为static不依赖对象存在。第一种用法是区分局部变量和成员变量。最常见的场景就是构造方法参数命名public class Person { private String name; public Person(String name) { this.name name; } }方法参数name和成员变量name重名了Java的局部变量优先级更高直接用name就取不到成员变量。this.name name; 的意思就是“把参数name赋值给当前对象的name成员变量”。有人问变量名不一样不就行了吗为什么一定要用this确实可以但制造大量非标准命名的变量比如pName、nName会让代码越写越别扭不如保持名字一致然后用this来区分。第二种用法是调用本类的成员方法。比如在一个类里写this.showInfo()和直接写showInfo()效果是一样的this可以省略。我建议初学者先不要省略写一段时间之后习惯了再省略也不迟。第三种用法是关键this()调用本类的其他构造方法。注意this()只能写在构造方法的第一行并且不能和super()同时出现。它解决的问题是构造方法之间的代码复用。public class Person { private String name; private int age; public Person() { this(匿名, 0); } public Person(String name, int age) { this.name name; this.age age; } }这样new Person()的时候无参构造会去调用带参构造初始化逻辑只写一遍。2.2 super的三种典型用法super代表的是“父类的空间引用”。它做三件事访问父类成员变量、调用父类成员方法、调用父类构造方法。访问父类成员变量和成员方法通常发生在子类重写了父类方法、或者子类定义了和父类同名成员的时候。你想调用父类的版本不加super就会递归调用子类自己的方法造成逻辑错误class Person { void introduce() { System.out.println(我是一个人); } } class Student extends Person { void introduce() { super.introduce(); // 先调用父类的方法 System.out.println(我也是一名学生); } }如果你在子类introduce()里不写super.introduce()直接调用introduce()那就会无限递归——自己调自己直到栈溢出。super的第三种用法也就是super()调用父类构造方法是重中之重。任何一个子类构造方法编译器都会在它的第一行默认插入super()哪怕你没写。这就是为什么父类必须有一个无参构造方法否则子类编译就会报错。如果父类只有带参构造子类就必须显式调用super()并传入参数。2.3 this和super对比速查表这里我直接把两者放在一起对比方便你随时查阅对比项thissuper本质含义当前对象的引用父类空间的引用使用位置实例方法、构造方法实例方法、构造方法static方法中不能使用不能使用调用构造方法this(...) 调用本类其他构造方法super(...) 调用父类构造方法调用位置限制必须是构造方法第一行必须是构造方法第一行能否同时出现构造方法中this()和super()不能同时出现同上访问成员范围先找本类找不到才找父类直接找父类有一句话特别能帮助记忆this是向上追溯的super是直接向上看的。this找成员时先看自己类里有没有没有再去父类里找super的意图更明确——我就是要找父类的那个成员别在本类里磨蹭。2.4 子类构造方法的隐藏调用链这一节是考试和面试的高频考点也是很多新手真正开始理解面向对象的地方。先看一个经典报错你定义了一个父类只写了带参构造方法然后写了一个子类子类构造方法什么都没写。编译时你会看到这样的错误信息Implicit super constructor Person() is undefined. 必须显式调用父类的某个构造方法。原因是这样的子类构造方法第一行默认是super()它去调用父类的无参构造。但你的父类定义了一个带参构造之后编译器就不再帮你生成那个默认的无参构造了。于是super()找不到目标直接报错。解决方案有两个要么给父类显式补一个无参构造要么在子类构造方法第一行写super(参数)手动指定调用父类哪个构造方法。再往深一层看Java的对象创建过程是有顺序的。你new一个子类对象执行序列是这样的class Person { public Person() { System.out.println(父类构造); } } class Student extends Person { public Student() { super(); // 手动写上和不写效果一样 System.out.println(子类构造); } } public class Demo { public static void main(String[] args) { new Student(); } }运行结果是先打印“父类构造”再打印“子类构造”。原因就是子类构造方法的第一行调用了父类构造方法父类的初始化永远先于子类。这个顺序看起来简单但很多考生一写多层继承就懵A继承BB继承C你new A()打印顺序是C的构造方法、B的构造方法、A的构造方法一层一层往外执行像剥洋葱一样。3. 抽象类把“不完整”变成设计优势3.1 抽象类和普通类的本质区别说到继承就得提抽象类。为什么要引入抽象类我习惯用一个生活中的例子来讲你设计一个“动物类”里面有一个“叫”的方法。问题是不管你怎么写方法体写“动物在叫”都是废话——动物这个概念太泛了真实的动物一定是具体的猫、狗、鸟。你不希望有人new一个“动物”对象出来因为那没有意义。抽象类就是干这个的。用abstract修饰的类叫抽象类抽象类里可以定义抽象方法。抽象方法只有方法声明没有方法体abstract class Animal { String name; public Animal(String name) { this.name name; } public abstract void sound(); }看这段代码Animal类有个明确的属性name有个构造方法但sound()方法没有大括号只有分号这就是抽象方法。它告诉所有子类“凡是继承我的都必须自己实现sound()”。这里有一个对比表格把抽象类和普通类的区别列清楚对比维度抽象类普通类能否实例化new不能能能否包含抽象方法可以有不能有能否包含具体方法能能能否包含构造方法能能子类义务必须实现所有抽象方法除非子类也是抽象类没有强制义务特别注意抽象类虽然不能new但它有构造方法。这个构造方法存在的意义是给子类调用模拟项目里很多新手问“抽象类都不能创建对象要构造方法干嘛”答案是子类构造方法里会调用super()你new的是子类对象但对象的父类部分需要初始化。抽象类的子类分两种如果子类实现了父类全部抽象方法它就是普通类可以被实例化如果子类只实现了一部分甚至一个都没实现那这个子类也必须是抽象类继续把未实现的抽象方法往下传。3.2 抽象类与接口什么时候用哪个这是六月Java面试里被问爆的问题我直接列一个完整的对比对比维度抽象类接口关键字abstract classinterface继承方式单继承一个类只能继承一个抽象类多实现一个类可以实现多个接口成员变量可以定义各种变量默认是public static final常量构造方法有没有抽象方法可以有可以有现在也允许default和static方法设计语义“是什么”的关系“能做什么”的能力适用场景多个子类有公共属性/公共逻辑需要复用代码多个无关类需要统一行为规范不关心公共属性核心判断标准就一句话狗继承动物类是合理的因为狗“是一种”动物但让狗实现一个“飞行能力”接口就不合理因为它不是“能飞”的。反过来飞机实现飞行接口是合理的但飞机去继承动物类就荒谬了。所以抽象类描述的是is-a关系接口描述的是has-a行为两者服务的目标不一样。我见过不少初学者一说要抽象就立刻建接口结果接口里塞了一堆默认方法越写越臃肿。判断标准其实很简单如果你的多个类之间有明显公共的属性和代码逻辑用抽象类如果你只是要给一批互不相干的类统一行为规范用接口。还有一种常见组合是用抽象类做公共骨架同时实现接口约定行为这在框架源码里极其常见。3.3 抽象类的经典应用模板方法抽象类在实战里最经典的应用是模板方法设计模式。所谓模板方法就是抽象类里把算法的骨架写死把可变的部分定义成抽象方法交给子类去实现。这样代码复用了骨架又保留了扩展的空间。举个例子。写一个支付流程不管微信支付、支付宝支付还是银联支付流程都是固定的——下单、验证库存、扣款、发送通知。扣款方式不同但流程骨架完全一样。抽象类可以这样设计abstract class Payment { // 模板方法用final防止子类修改骨架 public final void pay() { step1CreateOrder(); step2Verify(); step3Deduct(); step4Notify(); } private void step1CreateOrder() { System.out.println(创建订单); } private void step2Verify() { System.out.println(验证库存); } private void step4Notify() { System.out.println(发送通知); } abstract void step3Deduct(); } class WeChatPay extends Payment { Override void step3Deduct() { System.out.println(微信扣款); } }在这个例子里整个支付流程的顺序已经由抽象类定死了子类只需要关心“怎么扣款”这一件事。这就是抽象类最大的价值它把设计者的意图明确表达出来任何继承它的子类都必须遵守这份“约定”。4. 封装、继承、多态面向对象三大特性如何配合4.1 三大特性各自管什么面向对象三大特性封装、继承、多态经常被一起提到但很多初学者只记住了名字没想明白它们之间的关系。我打个比方封装是“关起门来做自己的事”继承是“站在巨人的肩膀上做事”多态是“同一个指令不同的人做不同的事”。封装负责隐藏内部细节对外只暴露必要的接口。它的核心手段是访问权限控制private字段配public方法。继承负责抽取公共逻辑建立类之间的层次关系它的核心机制就是前面讲的extends。多态负责让同一个方法调用呈现出不同的行为它的前提有三个有继承关系、子类重写了父类方法、父类引用指向子类对象。这三个特性不是孤立的。继承里如果没有封装配合父类所有的字段都直接对外开放子类可以随意修改那继承体系很快就会被改乱。多态如果没有继承作为前提父类引用根本不可能指向子类对象更谈不上动态绑定。4.2 继承是多态的前提多态的底层机制说穿了就是两个字重写和绑定。方法的重写发生在继承关系里子类用相同的方法签名重新实现父类的方法。而Java在运行时会根据对象的实际类型动态决定调用哪个版本的方法——这个叫动态绑定也叫运行时多态。举个例子下面的代码用Animal引用指向Dog对象class Animal { void sound() { System.out.println(动物发出声音); } } class Dog extends Animal { Override void sound() { System.out.println(汪汪); } } public class Demo { public static void main(String[] args) { Animal a new Dog(); a.sound(); // 输出汪汪 } }编译时期编译器看到的是a的类型是Animal但运行时期JVM实际创建的是Dog对象所以调用的是Dog重写后的sound()。这就是为什么说多态是“编译看左边运行看右边”。为什么需要多态最实际的收益是代码的可扩展性。如果写个方法参数是Animal类型那么任何Animal的子类都可以传进去不必为每一种动物单独写一个方法。以后新增一个Cat类调用方的代码一行都不用改完全符合开闭原则——对扩展开放对修改关闭。这就是继承支撑多态多态反哺设计灵活性的完整闭环。4.3 一个贯穿三者的代码例子我把三大特性在一个例子里串一遍。需求是写一个动物园系统能接收各种动物统一让它们叫唤。abstract class Animal { private String name; // 封装私有属性 public Animal(String name) { this.name name; } public String getName() { // 通过公共方法访问 return name; } public abstract void sound(); // 抽象方法交给子类实现 } class Dog extends Animal { public Dog(String name) { super(name); // super调用父类构造 } Override public void sound() { System.out.println(getName() 汪汪); } } class Cat extends Animal { public Cat(String name) { super(name); } Override public void sound() { System.out.println(getName() 喵喵); } } public class Zoo { public static void makeSound(Animal animal) { // 多态接收Animal任何子类都能进 animal.sound(); } public static void main(String[] args) { makeSound(new Dog(旺财)); makeSound(new Cat(咪咪)); } }这30行代码封装体现在name是private的、通过getName访问继承体现在Dog和Cat都继承Animal复用了name属性和构造逻辑多态体现在makeSound方法接收的是Animal类型却调用了各自的sound()方法而整个设计能在运行期正确地分派行为靠的是Animal被设计为抽象类并通过super把构造参数传递上去。初学者研究这个例子比背十遍概念管用得多。三大特性全部落到了代码层面缺一个这个例子都不成立。5. 实操从零编写员工管理系统的继承结构5.1 需求设计与类结构规划前面讲概念这一节做一个完整的实操。很多初学者看完概念还是不知道该怎么下手我直接模拟一个新员工管理系统的开发过程。需求是这样的公司有三种员工——普通员工、开发人员、经理。所有员工都有姓名、工号、基本工资这些公共属性都有“工作”的行为但工作内容各不相同。另外公司有一个统一的年终奖计算逻辑计算方式在员工之间完全不一样。分析这个需求公共属性是复用点适合抽取到父类“工作”行为虽然名字相同但实现不同适合定义成抽象方法年终奖计算也是各干各的同样是抽象方法。于是类结构就清晰了抽象类Employee公共属性name、id、salary公共方法showInfo()抽象方法work()和calculateBonus()类Developer继承Employee增加属性linesOfCode实现work()和calculateBonus()类Manager继承Employee增加属性teamSize实现work()和calculateBonus()测试类Main创建对象验证继承、super、抽象类、多态的全部知识点5.2 完整代码抽象父类与两个子类先写抽象父类abstract class Employee { private String name; private int id; protected double salary; public Employee(String name, int id, double salary) { this.name name; this.id id; this.salary salary; } public String getName() { return name; } public int getId() { return id; } public void showInfo() { System.out.println(姓名 name 工号 id); } public abstract void work(); public abstract double calculateBonus(); }注意我把salary设为protected这个细节很关键。private对子类不可见子类的calculateBonus()里直接访问salary会编译报错protected允许子类访问但不对外部开放兼顾了封装和继承。这正是第三部分讲的“受保护权限是父类给子类开的专属通道”。再写两个子类class Developer extends Employee { private int linesOfCode; public Developer(String name, int id, double salary, int linesOfCode) { super(name, id, salary); // 必须显式调用因为父类没有无参构造 this.linesOfCode linesOfCode; } Override public void work() { System.out.println(getName() 正在写代码产出了 linesOfCode 行代码); } Override public double calculateBonus() { return salary * 0.2 linesOfCode * 0.5; } } class Manager extends Employee { private int teamSize; public Manager(String name, int id, double salary, int teamSize) { super(name, id, salary); this.teamSize teamSize; } Override public void work() { System.out.println(getName() 正在管理 teamSize 人团队); } Override public double calculateBonus() { return salary * 0.5 teamSize * 1000; } }这两段代码用到了几乎前面所有的知识点extends建立继承关系super(name, id, salary)调用父类带参构造salary因为protected所以子类能直接用两个抽象方法全部被重写子类才能成为一个普通类。5.3 测试运行与设计复盘最后写测试类public class Main { public static void main(String[] args) { Employee emp1 new Developer(小张, 1001, 18000, 8000); Employee emp2 new Manager(小李, 1002, 25000, 8); test(emp1); test(emp2); } public static void test(Employee employee) { employee.showInfo(); employee.work(); System.out.println(年终奖 employee.calculateBonus()); System.out.println(----------------------); } }运行结果姓名小张工号1001 小张正在写代码产出了8000行代码 年终奖7600.0 ---------------------- 姓名小李工号1002 小李正在管理8人团队 年终奖20500.0 ----------------------这个结果里最有价值的部分是test()方法的参数类型是Employee。按普通思维你可能想写两个方法一个接收Developer一个接收Manager。但有了抽象类和多态只需要写一个方法不管是哪个子类对象进来都能处理。以后再加一个HR类test()方法一行代码都不用改。复盘这个设计抽象类Employee定义了整个体系的规范不能实例化但能接收所有子类对象super解决了带参构造方法的传递问题this.name name解决了参数和成员变量重名问题protected权限让salary在不完全暴露的情况下被子类使用了。二十多行代码把这一整章的知识点全部串起来了。6. 常见问题与排查技巧实录6.1 高频报错速查表我在带新人过程中积累了这么一张高频报错速查表基本都是初学者碰到过的报错/现象原因解决方案Implicit super constructor 未定义父类没有无参构造子类默认调用super()失败给父类补无参构造或子类第一行显式super(参数)Cannot instantiate the type xxx试图new一个抽象类对象创建抽象类的具体子类对象抽象方法报错必须实现子类没有实现父类所有抽象方法而子类也不是抽象类补全重写或把子类也声明为abstract静态方法里用了this或superstatic不依赖对象this/super依赖当前对象去掉static或者通过对象引用访问子类方法访问父类private成员private成员对子类不可见改用protected或通过公共get/set方法重写方法加了Override报错方法签名和父类不一致或者父类根本没有这个方法检查方法名、参数列表、返回值类型this()和super()同时出现Java规定构造方法第一行只能有其中一个根据需求选一个保留在构造方法第一行这张表我建议直接截屏存起来。每个报错在初学阶段至少会遇到两次第一次查表第二次就能背下来了。6.2 几条独家避坑经验最后分享几条课堂上看不到、但实战中特别重要的经验。第一继承层次尽量控制在三层以内。类A继承类BB继承C这种三层还能看清超过三层以后你会发现一个方法不知在哪个父类里定义需要IDE的“打开声明”功能到处跳。遇到这种结构优先考虑用组合代替继承——把公共逻辑抽到一个类里用字段引用的方式复用而不是一味的extends。第二构造方法里不要调用可以被重写的方法。举个例子父类构造方法里调用了this.sound()而子类重写了sound()那new子类对象时父类构造阶段就会调用到子类的方法而此时子类的字段还没初始化轻则得到null重则抛出NullPointerException。Java创建的对象的初始化顺序非常严格父类构造在前子类字段初始化在后这个顺序决定了这种调用极其危险。改成在构造里调用private方法或者干脆把所有复杂逻辑放到子类构造完成后显式调用。第三设计父类时把权限放对位置是一种智慧。默认情况下成员变量用private仅供子类使用的用protected本类自己用、不希望别人碰的也用private。不要一上来就把所有字段设为public等到架构膨胀时再来收拾烂摊子付出的成本会成倍增加。第四写抽象类之前先回答自己一个问题这个类的子类们是否真的共享了大量公共代码如果只是需要一个统一的类型用接口就够了。抽象类不是万能的用得太多反而会造成继承层级过重。这里的原则可以简化为有代码要复用用抽象类只需要定规范用接口。我自己在实际教学里的感受是继承、super、this、抽象类这四个知识点最难的不是某个独立概念而是它们在同一个代码文件里交错出现时的理解。所以每次带新人我都建议先画一张类关系图把谁是父类、谁是子类、哪个是抽象类标清楚然后再去看代码。对着图读代码五分钟就能理清思路不画图纯靠脑补代码一多必乱。如果你现在还卡在这几个概念上别慌。先用今天文章里的员工管理系统例子自己敲一遍敲完以后自己加一个HR类、加一个计算年终奖的新规则跑通之后再看下一篇的内容。动手了就真的懂了。