1. 项目概述从“继承了什么”到“如何用好继承”在Java的面试场上尤其是面对大厂的层层筛选时“继承”这个话题几乎是一个绕不开的“必答题”。但面试官真正想听的往往不是教科书上那句干巴巴的“子类可以继承父类的非私有成员”。他们更想看到的是你对这个看似基础的概念到底理解到了多深的层次以及在实际编码和系统设计中你是否能清晰地把握住继承的边界与细节。我自己在带团队和面试候选人时就经常抛出“Father fs new Son();时fs到底能调用子类的什么方法”这类问题答案的差异直接反映了开发者对多态、编译时类型与运行时类型理解的扎实程度。今天我们就来彻底拆解“子类到底能继承父类中的哪些内容”这个核心命题。这不仅仅是为了应付面试更是为了在日常开发中写出更健壮、更清晰、更符合面向对象设计思想的代码。我们会从最基础的规则讲起逐步深入到内存模型、方法表等底层原理并结合大厂高频面试题和可运行的源码示例让你不仅“知道”更能“通透”。2. 继承的核心规则不仅仅是“非私有”当我们说“子类继承父类”很多人第一反应就是字段和方法。这个理解没错但太笼统了。我们需要像用显微镜一样仔细审视每一个可继承元素的细节和限制。2.1 可继承内容的详细清单1. 公有public和受保护protected的实例字段与方法这是最直观的部分。子类对象在内存中会包含父类定义的这些字段并且可以直接或通过继承的方法访问它们。对于protected成员子类内部可以访问但外部不行这体现了“家族内部可见”的设计意图。2. 默认访问权限包级私有default的实例字段与方法这是一个关键点也是容易混淆的地方。如果子类和父类在同一个包内那么子类可以继承并使用父类的默认权限成员。如果子类在不同的包则无法继承。这强调了Java中“包”作为访问控制单元的重要性。3. 静态成员static fields methods静态成员属于类本身而非某个实例。子类可以“看到”并访问父类的公有或受保护的静态成员同包下也包括默认权限的。但这里没有“重写”的概念只有“隐藏”。通过子类类名调用静态方法影响的只是编译时的引用解析与多态无关。4. 嵌套类Nested Classes父类中定义的内部类或静态嵌套类如果其访问权限允许public, protected, 或同包default子类同样可以继承并使用它们。这为设计复杂的类结构提供了灵活性。5. 父类的构造器—— 不是构造器链这是一个非常重要的澄清构造器不能被继承。子类不能直接获得父类的构造器。但是子类的每个构造器都必须显式或隐式地调用父类的一个构造器以确保父类部分被正确初始化。这通过super(...)调用实现形成了构造器链。2.2 明确不可继承的内容1. 私有成员private这是铁律。私有意味着“仅对本类可见”。子类无法直接访问父类的私有字段或方法。如果子类需要访问父类必须提供公有的或受保护的getter/setter方法。这封装了内部细节是面向对象“封装性”的核心体现。2. 构造器如前所述子类获得的是调用父类构造器的责任而非父类构造器本身。3. 初始化块Instance Initializer Blocks父类的实例初始化块会随着父类构造器的执行而执行但子类并没有“继承”这个块。它的逻辑是绑定在父类自身的初始化流程中的。4. 被final修饰的方法final方法禁止被重写但可以被继承和调用。这用于明确声明“此方法的行为不可更改”常用于设计关键算法或模板方法模式中的固定步骤。5. 父类的参数化类型泛型如果父类是ClassAT子类是ClassB那么ClassB继承的是ClassA的成员声明但ClassB可以有自己的泛型参数或者为T指定具体类型。泛型信息在编译后会被擦除继承关系主要作用于擦除后的原始类型。注意讨论继承时一定要区分“访问”和“拥有”。子类对象在内存中确实包含了父类定义的所有字段包括private但Java的访问控制机制在语法层面禁止了子类代码直接访问这些private字段。这是语言设计上的约束而非内存模型上的不存在。3. 内存视角与多态原理Father fs new Son()的真相这是面试中最经典、也最能区分水平的问题之一。我们通过一段代码来切入class Father { public String name Father; public void show() { System.out.println(Fathers show: name); } public static void staticShow() { System.out.println(Fathers static show); } } class Son extends Father { public String name Son; // 隐藏了父类的name字段 Override public void show() { System.out.println(Sons show: name); } public static void staticShow() { System.out.println(Sons static show); } public void sonOnly() { System.out.println(Method only in Son); } } public class Test { public static void main(String[] args) { Father fs new Son(); // 编译时类型Father运行时类型Son System.out.println(fs.name); // 输出什么 fs.show(); // 输出什么 fs.staticShow(); // 输出什么 // fs.sonOnly(); // 这行能编译吗 } }3.1 编译时类型 vs 运行时类型理解这个问题的关键在于分清两个概念编译时类型声明类型变量fs被声明为Father类型。编译器在编译代码时只认这个类型。运行时类型实际类型new关键字创建的是一个Son对象。JVM在运行时知道这一点。编译器检查方法调用和字段访问的合法性完全基于编译时类型Father。因此fs.name编译器看到fs是Father类型于是去Father类中查找name字段找到了编译通过。输出结果是Father。字段访问没有多态性它是静态绑定的。fs.show()编译器检查Father类是否有show()方法有编译通过。但在运行时JVM会查找实际对象Son对象的方法表调用被重写过的Son.show()。输出Sons show: Son。实例方法调用具有多态性是动态绑定的。fs.staticShow()静态方法属于类调用哪个版本完全由编译时类型决定。因此即使实际对象是Son这里调用的也是Father.staticShow()。输出Fathers static show。fs.sonOnly()编译器在Father类中根本找不到sonOnly()方法所以直接报编译错误。无论运行时对象是什么这行代码都无法通过编译。3.2 方法表Method Table与动态绑定JVM为每个类维护一个方法表。当创建子类对象时其方法表是父类方法表的扩展和修改版包含父类所有可继承的非私有实例方法。如果子类重写了某个方法则子类方法表中的对应条目指向子类自己的方法代码。添加子类新增的实例方法。当执行fs.show()时JVM会获取对象fs的实际类型Son。查找Son类的方法表。找到show()方法对应的条目该条目指向Son.show()的代码地址。执行该代码。这就是动态绑定的底层机制也是多态得以实现的基础。3.3 字段隐藏Field Hiding子类定义了与父类同名的字段这称为“字段隐藏”。如示例中的name字段。父类字段和子类字段在内存中是两个独立的存储位置。通过父类引用fs.name访问到的是父类中定义的字段。在子类方法内部如Son.show()中直接写name或通过子类引用访问得到的是子类的字段。 这是一种容易导致混淆的设计实践中应尽量避免。实操心得在面试中解释清楚Father fs new Son()的相关问题是证明你理解Java核心机制的试金石。务必从“编译看左边运行看右边”对于方法和“字段都看左边”这两个口诀深入到内存模型和方法表的层面去解释这会让面试官眼前一亮。4. 大厂高频面试题深度剖析与源码实现下面我们结合几道典型的大厂面试题将理论知识应用于实际问题分析。4.1 面试题一构造器调用顺序与继承链题目以下代码的输出顺序是什么并解释原因。class A { public A() { System.out.println(A); } } class B extends A { public B() { this(B); System.out.println(B-); } public B(String s) { System.out.println(s); } } class C extends B { public C() { super(); System.out.println(C); } } public class Main { public static void main(String[] args) { new C(); } }解析与答案创建C对象调用C的无参构造器。C()构造器第一行是显式的super()调用父类B的无参构造器。B()构造器第一行是this(B)调用本类的另一个构造器B(String s)。关键点B(String s)构造器的第一行没有显式super(...)因此编译器会自动插入调用父类无参构造器的指令super()。因此首先执行A()构造器输出A。然后回到B(String s)输出参数s即B。接着完成B()构造器剩余部分输出B-。最后回到C()构造器输出C。最终输出A B B- C考察点构造器调用链、this()和super()的执行顺序、编译器自动添加super()的规则。4.2 面试题二多态、重写与异常题目下面代码能否编译如果能输出是什么注意throws子句。class Parent { public void doSomething() throws IOException { System.out.println(Parent doing something that might IO); } } class Child extends Parent { Override public void doSomething() throws FileNotFoundException { System.out.println(Child doing something more specific); } } public class TestException { public static void main(String[] args) { Parent p new Child(); try { p.doSomething(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }解析与答案编译性可以编译。方法重写时子类方法抛出的异常类型可以是父类方法抛出异常的子类或者更具体如FileNotFoundException是IOException的子类但不能是新的、更通用的检查型异常。这里符合规则。输出Child doing something more specific。因为发生了多态调用的是Child类重写后的方法。catch块在main方法中p.doSomething()的编译时类型是Parent所以编译器认为它可能抛出IOException。我们需要捕获IOException或其父类。由于我们实际知道运行时是Child对象它只抛出FileNotFoundException所以捕获FileNotFoundException的catch块会生效。如果调换两个catch块的顺序先IOException后FileNotFoundException则编译会报错因为FileNotFoundException是IOException的子类永远无法被第二个catch块捕获。考察点重写规则中的异常处理里氏替换原则、多态、catch块的顺序。4.3 面试题三综合挑战——字段、静态、代码块题目分析以下代码给出输出结果并详细说明每一步的执行过程。class Base { static { System.out.println(Base static block); } { System.out.println(Base instance block); } public String baseStr Base Field; public Base() { System.out.println(Base Constructor: baseStr); show(); } public void show() { System.out.println(Base.show: baseStr); } } class Derived extends Base { static { System.out.println(Derived static block); } { System.out.println(Derived instance block); } public String derivedStr Derived Field; public Derived() { System.out.println(Derived Constructor: derivedStr); show(); } Override public void show() { System.out.println(Derived.show: derivedStr); } } public class InitOrder { public static void main(String[] args) { new Derived(); } }解析与执行过程类加载阶段执行main方法JVM需要用到Derived类开始加载Derived。加载Derived时发现其父类Base尚未加载于是先加载Base。加载Base类执行其静态代码块输出Base static block。加载Derived类执行其静态代码块输出Derived static block。对象实例化阶段new Derived()触发对象创建。为对象分配堆内存并将所有实例字段包括继承的设为默认值null、0等。此时baseStr和derivedStr都是null。调用构造器Derived()但其第一行会隐式调用super()即Base()。执行父类Base()构造器在进入Base()构造器函数体之前先执行Base类的实例初始化块输出Base instance block。然后按顺序初始化Base的实例字段baseStr Base Field。执行Base()构造器函数体输出Base Constructor: Base Field。调用show()方法。注意此时对象的实际类型是Derived但Derived的derivedStr字段还未初始化仍是null。由于多态这里调用的是Derived.show()。输出Derived.show: null。这是一个关键陷阱返回执行子类Derived()构造器剩余部分父类构造器执行完毕后开始执行Derived类的实例初始化块输出Derived instance block。然后初始化Derived的实例字段derivedStr Derived Field。执行Derived()构造器函数体输出Derived Constructor: Derived Field。再次调用show()方法。此时derivedStr已初始化输出Derived.show: Derived Field。最终输出Base static block Derived static block Base instance block Base Constructor: Base Field Derived.show: null Derived instance block Derived Constructor: Derived Field Derived.show: Derived Field考察点类加载顺序静态块、对象实例化顺序父类实例块/字段 - 父类构造器 - 子类实例块/字段 - 子类构造器、在构造器中调用可重写方法的风险此时子类字段可能未初始化。5. 继承设计的最佳实践与避坑指南理解了原理和细节我们更要知道如何正确地使用继承。滥用继承是导致系统僵化、难以维护的常见原因。5.1 组合优于继承Composition over Inheritance这是面向对象设计的一条黄金法则。除非确实是“is-a”关系如Dogis anAnimal否则应优先考虑使用组合持有另一个类的实例而非继承。继承的缺点破坏封装子类依赖于父类的实现细节父类的改变可能破坏子类。紧耦合子类与父类高度绑定。类爆炸为了细微的行为差别可能需要创建大量子类。组合的优点更好的封装只需关注接口不依赖内部实现。更灵活可以在运行时动态更换行为。更易测试可以轻松模拟依赖对象。示例假设有一个Logger类你希望给它添加时间戳功能。继承不推荐class TimestampLogger extends Logger { ... }组合推荐class TimestampLogger { private Logger logger; ... }在TimestampLogger的方法中调用logger的方法并添加时间戳。5.2 谨慎在构造器中调用可重写方法正如面试题三所示在父类构造器中调用可重写的方法是极其危险的。因为此时子类的字段可能还未初始化处于不一致状态导致子类方法行为异常或NullPointerException。应尽量避免这种设计或将父类中的该方法标记为final或private以防止被重写。5.3 正确使用Override注解在重写父类方法时务必加上Override注解。这可以让编译器帮你检查方法签名是否正确是否真的是重写避免因拼写错误或参数列表不同而意外创建新方法这种错误很难排查。5.4 理解super关键字的两种用法super()调用父类构造器必须是子类构造器的第一条语句如果显式写出。super.methodName()调用父类中被重写的方法。这在子类想扩展而非完全替换父类行为时非常有用即“在父类行为的基础上添加新功能”。5.5 关于final关键字在继承中的运用final class类不能被继承。常用于工具类或确保核心类不可被修改如String类。final method方法不能被重写。用于锁定关键算法。final field常量字段其值对于基本类型或引用对于对象不能被修改。这有助于保证对象的不变性和线程安全。6. 从继承到面向对象设计思想的跃迁最后我们跳出语法细节从更高的视角看继承。继承不仅仅是代码复用的工具它更是实现多态和构建抽象层次的基石。多态允许我们编写出更通用、更灵活的代码。例如一个接受List接口的方法可以处理ArrayList、LinkedList等任何实现。这使得代码对扩展开放对修改封闭开闭原则。抽象层次通过继承我们可以构建从抽象到具体的类层次结构。顶层的抽象类或接口定义契约做什么下层的具体类提供实现怎么做。这是模板方法模式、策略模式等众多设计模式的基础。当你下次再看到“子类能继承父类什么”这个问题时希望你的脑海中浮现的不再是孤立的条目而是一幅包含内存布局、方法调度、类加载、对象初始化以及最终指向灵活软件设计的完整图景。扎实理解这些基础是应对复杂系统设计和高级面试挑战的底气所在。