🎯 Java 继承详解

📌 继承是 Java 面向对象三大特性之一(封装、继承、多态)

理解继承,就理解了 Java 类与类之间的关系本质


📖 本文导读

本文系统讲解 Java 继承的概念、语法、内存模型、构造方法、super/this 关键字、执行顺序、访问规则以及 final/组合等扩展知识点。适合已掌握类与对象基础的读者


文章目录

1️⃣ 继承的概念 🤪

1.1 为什么需要继承?

先看一个场景:定义多个类,每个类都有共同的属性和行为

// 教师类
public class Teacher {
    String name;
    int age;
    String subject;  // 所教科目
    
    public void teach() {
        System.out.println(name + "正在上课");
    }
    
    public void eat() {
        System.out.println(name + "正在吃饭");
    }
}

// 学生类
public class Student {
    String name;
    int age;
    String grade;   // 年级
    
    public void study() {
        System.out.println(name + "正在学习");
    }
    
    public void eat() {
        System.out.println(name + "正在吃饭");
    }
}

发现问题了吗?nameageeat() 在两个类中完全重复!这违背了 DRY 原则(Don’t Repeat Yourself)

1.2 什么是继承?

继承(Inheritance):将多个类中的**共同属性和行为抽取到一个父类(基类/超类)**中,子类只需要继承父类,就能直接拥有父类的成员,无需重复定义

💡 extends 关键字表示"扩展",子类在父类基础上扩展出自己独有的特性

// 父类(Person)
public class Person {
    String name;
    int age;
    
    public void eat() {
        System.out.println(name + "正在吃饭");
    }
}

// 教师类 —— 继承 Person
public class Teacher extends Person {
    String subject;  // 独有属性
    
    public void teach() {
        System.out.println(name + "正在上课");  // 直接使用父类的 name
    }
}

// 学生类 —— 继承 Person
public class Student extends Person {
    String grade;  // 独有属性
    
    public void study() {
        System.out.println(name + "正在学习");  // 直接使用父类的 name
    }
}

1.3 继承的优势

优势 说明
减少重复代码 公共成员只写一次,子类自动拥有
提高复用性 父类定义一次,多个子类共享
便于维护 修改公共代码只需改父类一处
体现层次关系 符合真实世界的分类结构

1.4 继承的类图

在这里插入图片描述


2️⃣ 继承的语法 😏

2.1 基本格式

[访问修饰符] class 子类名 extends 父类名 {
    // 子类独有成员
}

2.2 完整示例

// 动物父类
public class Animal {
    String name;
    int age;
    
    public void eat() {
        System.out.println(name + "正在吃东西");
    }
    
    public void sleep() {
        System.out.println(name + "正在睡觉");
    }
}

// 狗子类 —— 继承 Animal
public class Dog extends Animal {
    String breed;  // 品种(子类独有)
    
    // 子类独有方法
    public void bark() {
        System.out.println(name + "汪汪叫");
    }
}

// 测试
public class Test {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.name = "旺财";
        d.age = 3;
        d.breed = "哈士奇";
        
        d.eat();    // 继承自父类
        d.sleep();  // 继承自父类
        d.bark();   // 子类独有
    }
}

/* 输出:
   旺财正在吃东西
   旺财正在睡觉
   旺财汪汪叫
*/

2.3 继承的特点

🔑 Java 只支持单继承:一个类只能有一个直接父类,不能同时继承多个类

但可以多层继承(A 继承 B,B 继承 C,A 间接继承了 C)

// 单继承:合法
public class A extends B { }

// 多层继承:合法(A → B → C,逐层继承)
public class C { }
public class B extends C { }
public class A extends B { }

// 多继承:非法!Java 不支持
// public class A extends B, C { }  // 编译错误!

2.4 继承的层级关系

        Object(所有类的祖先)
            │
        Person
            │
    ┌───────┴───────┐
    │               │
 Student         Teacher
    │               │
    │          MathTeacher

📌 在 Java 中,所有类都直接或者间接地继承自 java.lang.Object。如果不写 extends,默认就是 extends Object

// 下面两种写法等价:
public class Person { }

// 等价于
public class Person extends Object { }

3️⃣ 继承后成员变量的访问 😎

3.1 访问规则

情况 示例 能否访问 原因
子类没有,父类有 this.name 继承而来
子类有,父类没有 this.grade 子类自有
子类和父类都有 this.name 就近原则,优先子类
父类想访问自己的成员 super.name super 明确指定

3.2 就近原则与 super 区分

当父类和子类有同名成员变量时:

public class Father {
    int money = 1000000;
}

public class Son extends Father {
    int money = 100;  // 与父类同名
    
    public void show() {
        int money = 50;  // 与父子类都同名
        
        System.out.println(money);           // 50,局部变量
        System.out.println(this.money);      // 100,子类成员变量
        System.out.println(super.money);      // 1000000,父类成员变量
    }
}

3.3 变量不重名时

public class Father {
    String name = "老张";
}

public class Son extends Father {
    int age = 18;  // 父类没有,子类独有
    
    public void show() {
        System.out.println(name);  // 继承自父类,输出"老张"
        System.out.println(age);     // 子类自有,输出 18
    }
}

4️⃣ super 关键字 😎

4.1 super 的含义

super 代表父类对象的引用,用于在子类中访问父类的成员

4.2 super 的三种用法

用法 示例 说明
访问父类成员变量 super.name 区分父子类同名变量
调用父类成员方法 super.eat() 访问父类被重写的方法
调用父类构造方法 super(参数) 必须在子类构造方法第一行

4.3 super 访问成员变量

public class Father {
    int a = 10;
}

public class Son extends Father {
    int a = 20;
    
    public void test() {
        System.out.println(this.a);     // 20,子类
        System.out.println(super.a);     // 10,父类
    }
}

4.4 super 调用成员方法

public class Father {
    public void eat() {
        System.out.println("爸爸在吃满汉全席");
    }
}

public class Son extends Father {
    @Override
    public void eat() {
        System.out.println("儿子在吃汉堡薯条");
    }
    
    public void callFather() {
        this.eat();       // 调用自己的
        super.eat();      // 调用父类的
    }
}

public class Test {
    public static void main(String[] args) {
        Son s = new Son();
        s.callFather();
    }
}

/* 输出:
   儿子在吃汉堡薯条
   爸爸在吃满汉全席
*/

5️⃣ 子类的构造方法 😏

5.1 构造方法的基本规则

🔑 子类构造方法执行时,必须先调用父类的构造方法!

原因:子类继承自父类,父类的成员也需要初始化。子类的构造方法会默认先调用父类的无参构造

5.2 默认调用父类无参构造

public class Father {
    public Father() {
        System.out.println("父类构造方法执行");
    }
}

public class Son extends Father {
    public Son() {
        // 这里隐含了一句 super();
        System.out.println("子类构造方法执行");
    }
}

public class Test {
    public static void main(String[] args) {
        Son s = new Son();
    }
}

/* 输出:
   父类构造方法执行
   子类构造方法执行
*/

5.3 子类如何调用父类有参构造

如果父类没有无参构造(自定义了有参构造),子类必须显式调用父类的有参构造

public class Father {
    String name;
    
    // 自定义了有参构造,无参构造不再自动生成
    public Father(String name) {
        this.name = name;
        System.out.println("父类有参构造:" + name);
    }
}

public class Son extends Father {
    int age;
    
    // 必须显式调用父类有参构造
    public Son(String name, int age) {
        super(name);  // 调用父类有参构造,必须在第一行
        this.age = age;
        System.out.println("子类构造:" + name + "," + age);
    }
}

public class Test {
    public static void main(String[] args) {
        Son s = new Son("张三", 18);
    }
}

/* 输出:
   父类有参构造:张三
   子类构造:张三,18
*/

5.4 构造方法调用链

public class Grandpa {
    public Grandpa() { System.out.println("Grandpa构造"); }
}
public class Father extends Grandpa {
    public Father() { System.out.println("Father构造"); }
}
public class Son extends Father {
    public Son() { System.out.println("Son构造"); }
}

// new Son() 时构造链:
// Grandpa() → Father() → Son()

6️⃣ super 和 this 的对比 🤓

6.1 对比表格

对比项 this super
代表 当前对象的引用 父类对象的引用
访问成员变量 this.变量名 super.变量名
调用成员方法 this.方法名() super.方法名()
调用构造方法 this(参数) super(参数)
出现位置 任意方法/构造 只能在子类中
默认行为 不写也有(构造第一行默认调用父类无参构造) 不写也有

6.2 this 和 this() 的区别

  • this.name → 访问成员变量
  • this() → 调用本类其他构造方法
public class Person {
    String name;
    int age;
    
    public Person() {
        this("未知", 0);  // 调用本类有参构造
    }
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

6.3 super 和 this 的内存模型

在这里插入图片描述


7️⃣ 继承关系上代码块等的初始化 😕

7.1 继承体系下的执行顺序

在继承关系中,静态代码块、实例代码块、构造方法的执行顺序如下:

🔑 执行顺序(总原则):

父类静态代码块 → 子类静态代码块 → 父类实例代码块 → 父类构造方法 → 子类实例代码块 → 子类构造方法

7.2 完整示例

public class Father {
    static {
        System.out.println("1. 父类静态代码块");
    }
    
    {
        System.out.println("3. 父类实例代码块");
    }
    
    public Father() {
        System.out.println("4. 父类构造方法");
    }
}

public class Son extends Father {
    static {
        System.out.println("2. 子类静态代码块");
    }
    
    {
        System.out.println("5. 子类实例代码块");
    }
    
    public Son() {
        System.out.println("6. 子类构造方法");
    }
    
    public static void main(String[] args) {
        System.out.println("=== 第一个对象 ===");
        new Son();
        System.out.println("=== 第二个对象 ===");
        new Son();
    }
}

/* 输出:
   === 第一个对象 ===
   1. 父类静态代码块
   2. 子类静态代码块
   3. 父类实例代码块
   4. 父类构造方法
   5. 子类实例代码块
   6. 子类构造方法
   === 第二个对象 ===
   3. 父类实例代码块
   4. 父类构造方法
   5. 子类实例代码块
   6. 子类构造方法
*/

7.3 顺序解析

阶段 内容 说明
类加载时 父类静态 → 子类静态 只执行一次
创建对象时 父类实例代码块 → 父类构造 → 子类实例代码块 → 子类构造 每次 new 都执行

💡 静态代码块只执行一次(类加载时),实例代码块每次创建对象都执行。

7.4 包含 main 方法时的特殊情况

public class Father {
    static { System.out.println("父类静态代码块"); }
    { System.out.println("父类实例代码块"); }
    public Father() { System.out.println("父类构造方法"); }
}

public class Son extends Father {
    static { System.out.println("子类静态代码块"); }
    { System.out.println("子类实例代码块"); }
    public Son() { System.out.println("子类构造方法"); }
    
    public static void main(String[] args) {
        // main 方法属于 Son 类,Son 类被加载
        // 所以先输出父类和子类的静态代码块
        new Son();
    }
}

/* 输出:
   父类静态代码块    ← Son 类加载,触发父类先加载
   子类静态代码块    ← Son 类加载完成
   父类实例代码块    ← new Son()
   父类构造方法
   子类实例代码块
   子类构造方法
*/

8️⃣ 再认识 protected 关键字 🧐

8.1 四种访问修饰符回顾

修饰符 本类 同包 子类(不同包) 其他
public
protected
默认(无修饰符)
private

8.2 protected 的核心价值

protected 的存在,主要是为了让子类在不同包时也能访问父类成员

// com.example 包
public class Father {
    protected String name = "张三";
    protected void eat() {
        System.out.println("吃饭");
    }
}

// 同一个包
class SamePackageClass {
    void test(Father f) {
        f.name;      // ✅ 同包,可以访问 protected
        f.eat();     // ✅ 同包,可以访问 protected
    }
}

// com.other 包,子类
package com.other;
public class Son extends Father {
    public void test() {
        this.name;      // ✅ 子类,可以访问(继承过来的)
        super.name;     // ✅ 子类,可以用 super
        eat();          // ✅ 子类,可以调用
    }
}

// com.other 包,非子类
package com.other;
public class Other {
    public void test(Father f) {
        f.name;      // ❌ 不同包非子类,protected 不可见
    }
}

8.3 为什么不用 public?

场景 public protected private
同包可访问
不同包子类可继承
不同包非子类不可访问

💡 设计建议:

  • 子类可能需要继承的成员protected
  • 仅本类内部使用的成员private
  • 完全开放给所有人public

9️⃣ 继承的方式 😃

9.1 继承方式的分类

继承并非只有一种结构,根据继承链的形态不同,可以分为以下四种方式:

继承方式 结构特征 Java 支持 典型场景
单继承 一个类直接继承一个父类 最常用
多层继承 继承链(A→B→C) 分层结构
Hierarchical 继承 多个子类继承同一个父类 同级分类
多重继承 一个类同时继承多个父类 ❌(用接口模拟)

9.2 单继承(Single Inheritance)

定义:一个子类只有一个直接父类,是最简单也是最推荐的继承方式

public class Animal {
    String name;
    public void eat() { System.out.println("吃东西"); }
}

public class Dog extends Animal {
    String breed;
    public void bark() { System.out.println("汪汪叫"); }
}
            Animal
              │
              │ extends
              ▼
             Dog

💡 Java 官方推荐的继承方式。一个类直接继承一个父类,结构清晰,耦合度可控

9.3 多层继承(Multi-Level Inheritance)

定义:继承链上有三个或更多层。A 继承 B,B 继承 C,那么 A 间接继承了 C

public class LivingThing {           // 第 1 层:根
    String alive = "活着";
}

public class Animal extends LivingThing {  // 第 2 层
    String name;
    public void breathe() {
        System.out.println("呼吸");
    }
}

public class Dog extends Animal {           // 第 3 层
    String breed;
    public void bark() {
        System.out.println("汪汪叫");
    }
}

public class Test {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.name = "旺财";      // 继承自 Animal
        d.alive;              // 继承自 LivingThing(间接继承)
        d.breathe();         // 继承自 Animal
        d.bark();            // 自己定义
    }
}
        LivingThing(曾祖父)
              │
              │ extends
              ▼
           Animal(祖父)
              │
              │ extends
              ▼
            Dog(子类)

⚠️ 继承层次不要超过 3 层,否则耦合过深,维护成本急剧上升

超过 3 层时的典型症状:找一个方法要顺着继承链查好几层,改一处影响半辈子

9.4 Hierarchical 继承(层次继承)

定义同一个父类多个子类继承,每个子类以自己的方式实现父类方法

public class Shape {                  // 唯一父类
    public double area() {
        return 0;
    }
}

public class Circle extends Shape {     // 子类1
    double r;

    @Override
    public double area() {
        return Math.PI * r * r;
    }
}

public class Rectangle extends Shape {   // 子类2
    double width, height;

    @Override
    public double area() {
        return width * height;
    }
}

public class Triangle extends Shape {   // 子类3
    double base, height;

    @Override
    public double area() {
        return 0.5 * base * height;
    }
}
                  Shape(父类)
           ┌────────┴────────┬─────────┐
           │                 │         │
           ▼                ▼          ▼
        Circle        Rectangle   Triangle

典型应用场景

  • 图形系统:ShapeCircle / Rectangle / Triangle
  • 动物系统:AnimalDog / Cat / Bird
  • 支付系统:PaymentAlipay / WechatPay / CreditCardPay

9.5 多重继承 —— 为什么 Java 不支多继承?

定义:一个子类同时继承多个父类(如 C++ 的多继承)。

        Father1        Father2
           │               │
           └───────┬───────┘
                    ▼
                 Son(同时继承两个父亲)

⚠️ Java 不支持多重继承!

为什么?假设 Son 同时继承 Father1 和 Father2,而两者都有一个 eat() 方法:

public class Father1 { public void eat() { } }
public class Father2 { public void eat() { } }
public class Son extends Father1, Father2 { }  // 编译错误!

Son 调用 eat() 时,编译器不知道该调用哪个版本,产生菱形继承问题(Diamond Problem)

9.6 用接口模拟多重继承

虽然类不能多继承,但 Java 提供了**接口(interface)**来弥补这一缺陷

// 两个"能力接口"(相当于两个"父类")
interface Flyable {
    void fly();
}

interface Swimmable {
    void swim();
}

// Duck 同时拥有两个能力:多实现(implements)
public class Duck implements Flyable, Swimmable {
    @Override
    public void fly() {
        System.out.println("鸭子飞");
    }

    @Override
    public void swim() {
        System.out.println("鸭子游");
    }
}
     Flyable(接口1)        Swimmable(接口2)
           │                       │
           └───────────┬───────────┘
                       ▼
                    Duck(同时实现两个接口)

🔑 类继承(extends)和接口实现(implements)的区别:

对比项 extends implements
父类数量 只能一个 可以多个
关系 “是-a”(is-a) “能-a”(can-do)
内容 继承属性+方法 只定义方法签名
复用方式 直接复用代码 子类提供实现

9.7 四种继承方式总结

① 单继承(Single)           ② 多层继承(Multi-Level)
        A                         LivingThing
        │                              │
        │ extends                  Animal(间接继承)
        ▼                              │
        B                              │
                                Dog(间接继承)

③ Hierarchical 继承          ④ 多重继承(Java不支持)
       Shape                Father1  Father2
     ┌──┴──┐                └────┬────┘
     ▼     ▼                  Son ❌
  Circle  Rectangle

④' 替代方案(接口)
  Flyable         Swimmable
     └────────┬────────┘
              ▼
            Duck ✅

🔟 方法重写(Override) 😃

10.1 什么是方法重写?

方法重写(Override):子类继承父类后,可以重新定义父类中已有的方法,从而覆盖父类的实现,子类提供自己的特殊行为

10.2 方法重写的规则

规则 要求
方法名 必须与父类方法名完全相同
参数列表 必须与父类方法完全相同(个数、类型、顺序)
返回值类型 必须与父类方法相同或是其子类型(协变返回)
访问修饰符 子类访问修饰符 ≥ 父类(不能更严格)
抛出的异常 不能抛出比父类更新的更宽的受检异常

[!IMPORTANT]

🔑 私有方法(private)不能重写! 子类中定义与父类 private 方法同名的方法,是新方法,不是重写

10.3 重写示例

public class Phone {
    public void call(String number) {
        System.out.println("用手机打电话:" + number);
    }
    
    // private 方法 —— 不能被重写
    private void connect() {
        System.out.println("建立通话连接");
    }
}

public class SmartPhone extends Phone {
    
    // ✅ 这叫重写:方法名、参数、返回值都一致
    @Override
    public void call(String number) {
        System.out.println("用智能手机打电话:" + number);
        System.out.println("同时打开免提...");
    }
    
    // ✅ 这是新方法,不是重写(因为父类是 private)
    private void connect() {
        System.out.println("建立4G视频通话连接");
    }
}

10.4 @Override 注解

@Override编译器注解,加在重写方法上,告诉编译器"这个方法应该是重写父类的"。如果重写不合法(比如方法名拼错了),编译器会直接报错。

public class Father {
    public void play() {
        System.out.println("父亲在打太极拳");
    }
}

public class Son extends Father {
    
    // ✅ 正确重写,加了 @Override 编译通过
    @Override
    public void play() {
        System.out.println("儿子在打游戏");
    }
    
    // ❌ 拼错了!编译器会报错(因为父类没有这个方法)
    @Override
    public void ploy() {  // 编译错误!没有匹配到父类方法
        System.out.println("儿子在打游戏");
    }
}

[!TIP]

💡 强烈建议始终加上 @Override,不要省略。这是防御性编程,可以帮你尽早发现错误

10.5 super.方法名() 调用父类版本

重写后,如果子类的重写方法想复用父类的实现,可以用 super.方法名() 调用:

public class Animal {
    public void cry() {
        System.out.println("动物发出叫声");
    }
}

public class Dog extends Animal {
    @Override
    public void cry() {
        super.cry();                   // 先调父类
        System.out.println("汪汪汪");   // 再加自己的
    }
}

public class Cat extends Animal {
    @Override
    public void cry() {
        System.out.println("喵喵喵");
        // 这里没调用 super.cry(),完全替换了父类行为
    }
}

public class Test {
    public static void main(String[] args) {
        new Dog().cry();
        new Cat().cry();
    }
}

/* 输出:
   动物发出叫声
   汪汪汪
   喵喵喵
*/

10.6 重写与重载的区别

对比项 重写(Override) 重载(Overload)
英文名 Override Overload
发生位置 父子类之间 同一类中
方法名 必须相同 必须相同
参数列表 必须相同 必须不同
返回值类型 相同或协变 可以不同(不关心)
访问修饰符 子类 ≥ 父类 可以不同(不关心)
抛出异常 不能比父类更宽 可以不同(不关心)
public class Father {
    public void test() { }              // 原始版本
    public void test(int a) { }        // 重载(参数不同)
}

public class Son extends Father {
    @Override
    public void test() { }              // 重写(无参数)
    // public void test() { }          // 重载 ❌ 参数相同=重写,不是重载
}

10.7 不能被重写的方法

以下方法不能被重写:

情况 说明 示例
final 修饰的方法 父类明确声明不允许重写 public final void show() { }
private 方法 私有方法对子类不可见,根本不存在重写 private void show() { }
static 方法 静态方法属于类,不存在多态override public static void show() { }
构造方法 构造方法不能被继承,更不能重写

[!CAUTION]

⚠️ 常见误区:子类写了与父类 static 方法同名的方法,不是重写,是子类重新定义了一个新的 static 方法。 通过子类引用调用时,调用的是子类的版本,而非父类版本(但这与重写的多态行为完全不同,实践中容易产生困惑,应避免这样写)


1️⃣1️⃣ final 关键字 😕

11.1 final 的作用

final 表示"最终的,不可改变的"。一旦赋值,就不能再改变。

11.2 final 修饰变量

11.2.1 基本类型变量

final 修饰基本类型变量时,其不能改变:

public class Test {
    final int NUM = 10;
    
    public void method() {
        // NUM = 20;  // ❌ 编译错误!final 变量不能重新赋值
        System.out.println(NUM);  // ✅ 可以读取
    }
}
11.2.2 引用类型变量

final 修饰引用类型变量时,引用本身(地址)不能改变,但对象内部的属性可以修改:

public class Dog {
    String name = "旺财";
}

public class Test {
    final Dog DOG = new Dog();
    
    public void method() {
        // DOG = new Dog();  // ❌ 编译错误!引用不能重新指向
        DOG.name = "大黄";    // ✅ 可以修改对象的属性
        System.out.println(DOG.name);  // 输出:大黄
    }
}
11.2.3 成员变量的赋值时机

final 修饰的成员变量,必须在以下四个时机之一完成赋值:

时机 示例 说明
声明时直接赋值 final int A = 10; 最简单
构造方法中赋值 在构造方法里赋值 每个构造都要赋
构造代码块中赋值 {} 代码块中赋值 一次赋给所有构造
初始化方法中赋值 final + 静态方法(static final) 静态常量场景
public class Person {
    // 方式1:声明时赋值
    final String NAME = "未知";
    
    // 方式2:构造代码块中赋值
    final int AGE;
    {
        AGE = 0;  // 在实例代码块中赋值
    }
    
    // 方式3:构造方法中赋值
    final String GENDER;
    public Person(String gender) {
        this.GENDER = gender;  // 在构造方法中赋值
    }
}

11.3 final 修饰方法

final 修饰方法时,该方法不能被重写,但可以正常继承和调用:

public class Father {
    public final void show() {
        System.out.println("父类的 show 方法");
    }
    
    public void run() {
        System.out.println("父类的 run 方法");
    }
}

public class Son extends Father {
    // ✅ 可以继承 show,但不能重写
    // @Override public void show() { }  // ❌ 编译错误!final 方法不能重写
    
    // ✅ 普通方法可以重写
    @Override
    public void run() {
        System.out.println("子类的 run 方法");
    }
}

[!TIP]

💡 什么时候用 final 方法?当你确定某个方法不需要被子类修改,且子类重写可能产生错误或安全隐患时,标记为 final

11.4 final 修饰类

final 修饰类时,该类不能被继承,没有子类:

public final class String {
    // java.lang.String 是 final 类
    // public class MyString extends String { }  // ❌ 编译错误!
}
public final class Constants {
    public static final double PI = 3.14159;
    public static final String APP_NAME = "MyApp";
}

// ❌ 编译错误!不能继承 final 类
// public class MyConstants extends Constants { }

[!IMPORTANT]

🔑 常见 final 类java.lang.Stringjava.lang.System所有包装类(Integer、Double 等)

11.5 final 的四大场景总结

修饰对象 效果 示例
变量(基本类型) 值不可变,等价于常量 final int NUM = 100;
变量(引用类型) 引用不可变(指向不变),对象内部可变 final Dog D = new Dog();
方法 方法不能被重写,但可继承和调用 public final void show() { }
类不能被继承(没有子类) public final class A { }

11.6 static final —— 类常量

static final 组合表示类级别的常量:整个类只有一份,且不可改变。命名规范为全大写,用下划线分隔:

public class MathUtils {
    // 类常量:通过类名直接访问
    public static final double PI = 3.1415926;
    public static final int MAX_COUNT = 1000;
    
    // 访问方式
    public void test() {
        System.out.println(PI);            // ✅ 本类中直接访问
        System.out.println(MathUtils.PI);  // ✅ 类名.常量名
    }
}

public class Test {
    public static void main(String[] args) {
        System.out.println(MathUtils.PI);   // ✅ 类名.常量名
        // MathUtils.PI = 3.14;  // ❌ 编译错误!类常量不可改
    }
}

1️⃣2️⃣ 继承与组合 🤔

12.1 组合的概念

组合(Composition):在类中直接使用另一个类的对象作为成员变量,而不是通过继承来复用代码

12.2 继承 vs 组合

对比项 继承 组合
关系 “是”(is-a) “有”(has-a)
语法 extends 直接成员变量
耦合度 高(父子紧耦合) 低(松耦合)
灵活性 低(单继承限制) 高(可以组合多个)
适用场景 真正的分类关系 临时组合复用

12.3 组合示例

// 继承方式:"学生是动物"(语义上不太对)
public class Student extends Animal {
    String grade;
}

// 组合方式:"学生有一台电脑"(语义更准确)
public class Computer {
    String brand;
    int price;
    
    public void start() {
        System.out.println(brand + "电脑开机");
    }
}

public class Student {
    String name;
    int age;
    
    // 组合:学生有一台电脑
    Computer computer = new Computer();
    
    public void coding() {
        System.out.println(name + "在写代码");
        computer.start();
    }
}

11.4 为什么优先用组合而不是继承?

⚠️ 继承的坑:子类继承了父类所有方法,即使父类的某个方法在子类语义下完全不合理

组合可以选择性使用父类的功能,而继承则是全盘接收

// 继承的坑:假设我们想让学生有"站立"的功能
public class Human {
    public void stand() { System.out.println("站立"); }
}

// 用继承:学生继承了 stand,但同时也继承了 stand 的所有实现细节
public class Student extends Human { }

// 用组合:只获取需要的部分
public class Student {
    Human human = new Human();  // 需要时调用 human.stand()
}

12.5 组合的高级用法(依赖注入)

public class Student {
    // 组合:可以注入不同类型的动物
    private Animal animal;
    
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }
    
    public void play() {
        animal.cry();  // 多态调用
    }
}

public class Test {
    public static void main(String[] args) {
        Student s = new Student();
        s.setAnimal(new Dog());  // 玩狗
        s.play();
        
        s.setAnimal(new Cat());  // 换成猫
        s.play();
    }
}

1️⃣3️⃣ 继承的内存模型 😲

13.1 对象在内存中的结构

public class Father {
    int fatherVar = 10;
    
    public void fatherMethod() {
        System.out.println("父类方法");
    }
}

public class Son extends Father {
    int sonVar = 20;
    
    public void sonMethod() {
        System.out.println("子类方法");
    }
}

public class Test {
    public static void main(String[] args) {
        Son s = new Son();
        s.fatherVar = 30;  // 继承来的
        s.sonVar = 40;     // 自有的
        s.fatherMethod();  // 调用继承的方法
        s.sonMethod();     // 调用自有的方法
    }
}

13.2 内存图解

在这里插入图片描述

🔑 关键点:

  • 子类对象在堆中同时包含父类和子类两部分区域
  • s 变量在栈中,存储的是对象的首地址
  • fatherMethod()sonMethod() 都存在方法区,子类对象不复制方法,只引用方法区

1️⃣4️⃣ 继承的注意事项 😵

14.1 子类不能继承父类的哪些成员?

不能继承的 原因
private 成员 私有,访问不到
构造方法 构造方法是用来创建对象的,每个类用自己的构造
同一包中默认修饰符的成员 包访问权限
final 修饰的父类 final 类不能被继承
public class Father {
    private String secret = "秘密";     // ❌ 子类不能继承
    String defaultVar = "默认";          // 同包可继承
    public String publicVar = "公开";   // ✅ 可以继承
}

public class Son extends Father {
    public void test() {
        // secret 不可见!
        System.out.println(defaultVar);    // ✅
        System.out.println(publicVar);      // ✅
    }
}

14.2 继承的层次不宜过深

// 不推荐:层次过深(继承链太长)
class A { }
class B extends A { }
class C extends B { }
class D extends C { }
class E extends D { }
class F extends E { }

⚠️ 继承层次过深的危害:

  1. 理解成本高:查一个方法要沿着继承链往上找很久
  2. 耦合度高:改父类影响所有子类
  3. JVM 加载慢:层次深意味着要加载更多类

14.3 继承与多态的关系

继承是实现多态的基础之一。关于多态的详细内容,将在下一篇文章中详细讲解

// 多态:父类引用指向子类对象
public class Test {
    public static void main(String[] args) {
        // 父类引用 f,指向子类对象(new Dog())
        Animal a = new Dog();  // 多态
        
        a.cry();  // 运行的是 Dog 的 cry()(父类引用调用子类重写方法)
        // a.bark(); // ❌ 编译错误!a 的编译类型是 Animal,Animal 没有 bark()
    }
}

1️⃣5️⃣ 最佳实践与设计建议 🧠

15.1 继承的"is-a"原则

💡 继承关系判断标准:子类"是一种"父类吗?

  • Dog is an Animal ✅ 继承合理
  • Student is a School ❌ 继承不合理
  • Son is a Father ❌ 继承不合理(儿子不是父亲)

15.2 设计原则

原则 说明
里氏替换原则(LSP) 子类对象可以替换父类位置,程序仍然正确
优先组合 能用组合解决的问题,不要轻易用继承
最小权限原则 成员尽量用 private,需要时通过 protected 或方法暴露
接口分离 一个类继承太多功能 → 拆分成多个接口

15.3 经典错误示例

// ❌ 错误:数学老师不是学生,却继承了 Student 类
public class Student {
    String name;
    int grade;
}

public class MathTeacher extends Student {  // 语义混乱!
    String subject;
}

// ✅ 正确:数学老师是教师的一种
public class Teacher extends Person {
    String subject;
}

15.4 正确的继承设计

// 正确的继承体系
public abstract class Person {
    protected String name;
    protected int age;
    
    public abstract void work();  // 不同人有不同工作
}

public class Student extends Person {
    String grade;
    
    @Override
    public void work() {
        System.out.println("学习");
    }
}

public class Teacher extends Person {
    String subject;
    
    @Override
    public void work() {
        System.out.println("教书");
    }
}

📊 小结

知识点 核心内容
继承概念 extends,子类继承父类,拥有父类成员
继承特点 单继承,多层继承;所有类间接继承 Object
成员访问 就近原则,super 访问父类
super 关键字 访问父类变量、方法、构造方法
构造方法 默认调用父类无参构造;父类无有参构造时需显式 super()
执行顺序 父静态 → 子静态 → 父实例 → 父构造 → 子实例 → 子构造
protected 同包 + 不同包子类可访问
方法重写 @Override,子类覆盖父类方法,super.方法名() 调用父版
final 不可变:修饰变量/方法/类
继承 vs 组合 is-a 用继承,has-a 用组合
内存模型 堆中子类对象包含父类区域

✍️ 写在最后

继承是 Java 代码复用的核心手段,但也是最容易被滥用的特性。优先使用组合,谨慎使用继承,是面向对象设计的经验之谈

继承的下一个知识点是多态 —— 让"父类引用指向子类对象"成为可能,真正发挥 OOP 的威力。敬请期待!

ps: IDEA 快捷键 Ctrl+Alt+L 可以格式化对齐代码
可能读者代码敲着就会产生其他的疑惑 这些疑惑可能是后续多态 接口的知识

如有问题或建议,欢迎在评论区留言交流! 🙌

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐