封装、继承与多态

面向对象程序设计的三大基石

深入理解Java对象的世界观

面向对象的三大特性

封装、继承和多态是Java面向对象编程的核心,它们共同构建了清晰、可维护和可扩展的软件世界。

  • 封装 (Encapsulation): 将数据(属性)和操作数据的方法(行为)捆绑在一起,并对外部隐藏内部实现细节,只提供公共的访问接口。
  • 继承 (Inheritance): 允许一个类(子类)获取另一个类(父类)的属性和方法,实现了代码的复用,并为多态提供了基础。
  • 多态 (Polymorphism): 同一个消息(方法调用)可以根据发送对象的不同而表现出不同的行为。它极大地增强了程序的灵活性和可扩展性。

正是这三大特性,让我们的Java程序更加生动和强大。

类的封装

封装的核心思想是“高内聚,低耦合”。它确保了对象内部数据的安全性,使用者无需关心实现细节,只需通过预定义的接口与对象交互。

如果不封装,类的内部变量可以被随意读写,这可能导致数据不一致或逻辑混乱,给系统带来风险。

最佳实践:将成员变量设为 private,并通过公共的 public Getter 和 Setter 方法来控制访问。


public class Person {
    private String name; // 设为私有,只能在类内部访问
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { // Getter: 对外提供读取name的通道
        return name;
    }

    public void setName(String name) { // Setter: 对外提供修改name的通道
        // 可以在这里加入控制逻辑
        if(name != null && !name.isEmpty()) {
            this.name = name;
        }
    }
    
    public int getAge() {
        return age;
    }
}

封装的作用

封装的真正威力在于,我们可以在Getter和Setter方法中添加额外的逻辑,从而完全掌控属性的读写过程。

例如,我们可以规定名字不允许包含特定字符,或者年龄不能被设置为负数。


public class Person {
    private String name;
    private int age;

    // ... 构造方法 ...

    public void setName(String name) {
        // 名字不能为空,且不能包含"小"字
        if (name == null || name.contains("小")) {
            System.out.println("无效的名字!");
            return;
        }
        this.name = name;
    }

    public void setAge(int age) {
        // 年龄必须在 0 到 120 之间
        if (age < 0 || age > 120) {
            System.out.println("无效的年龄!");
            return;
        }
        this.age = age;
    }
    
    // ... Getters ...
}

通过这种方式,类的设计者可以强制执行业务规则,确保对象状态的有效性。

访问修饰符详解

在深入单例模式之前,我们需要彻底理解Java的四种访问修饰符。它们就像不同级别的"门卫",控制着谁可以访问类的成员。

访问修饰符对比表

修饰符 同一个类 同一个包 不同包的子类 不同包的其他类
private ✓ ✗ ✗ ✗
默认(包私有) ✓ ✓ ✗ ✗
protected ✓ ✓ ✓ ✗
public ✓ ✓ ✓ ✓

实际应用场景

🔒 private - 内部秘密

用于类的内部实现细节,外部完全不可见

  • 敏感数据(密码、密钥)
  • 内部计算方法
  • 缓存变量

📦 默认 - 包内共享

同一个包内的类可以访问,适合紧密协作的类

  • 工具类的辅助方法
  • 包内数据传输对象
  • 内部API

🛡️ protected - 家族传承

子类可以访问,用于继承体系中的共享

  • 父类的核心属性
  • 可被重写的方法
  • 模板方法模式

🌍 public - 公开接口

完全公开,任何地方都可以访问

  • 对外提供的API
  • 常量定义
  • 主要业务方法

访问权限控制演示

通过文件夹和文件的形式,直观展示Java中不同访问修饰符的权限控制效果:

📁 com.example.packageA
☕ Parent.java
public String publicField
protected String protectedField
default String defaultField
private String privateField
☕ SamePackage.java
📁 com.example.packageB
☕ Child.java extends Parent
☕ Other.java

访问权限说明:

public - 所有类都可访问
protected - 同包或子类可访问
default - 仅同包可访问
private - 仅本类可访问

测试结果:

点击上方按钮开始测试访问权限...

为什么需要模块:当 public 力不从心 (*)

你已经会用 private 把字段藏进类里。那项目变大、分成多个包之后,一个类想藏起来怎么办?

场景:学生管理系统里的学号生成器

// school/util/IdGenerator.java:内部工具,学号只该由报名流程统一发
package school.util;

public class IdGenerator {              // 为什么只能 public?往下看
    private static int count = 0;       // 字段藏得住:private 没问题
    public static String next() {
        return "S" + String.format("%03d", ++count);
    }
}

// school/manager/StudentManager.java:对外提供报名功能
package school.manager;
import school.util.IdGenerator;         // 跨包调用 → IdGenerator 必须 public

public class StudentManager {
    public static String enroll(String name) {
        return name + " -> " + IdGenerator.next();   // 报名时统一发学号
    }
}

逐个排查四种修饰符,会发现一个更尴尬的事实:private 和 protected 根本不允许修饰顶级类;能用的只有 default(仅同包可见,跨包就废)和 public(对全项目开放)——还是只剩 public 一条路。

// 于是项目里任何代码都能绕过 StudentManager 直接发学号:
String id = IdGenerator.next();   // 编译通过!学号规则形同虚设
// 而且日后想改学号规则时,根本查不清还有谁在直接调它

症结:访问控制的最小粒度是"类"。private 藏得住字段,却藏不住类;public 一旦跨包就等于对全项目开放。Java 缺一个"仅部分包可见"的中间档。

模块怎么补上这个洞 (*)

在项目源码根目录放一个 module-info.java,只导出 manager 包:

// school/src/module-info.java
module school {
    exports school.manager;   // 只开这一扇门
    // school.util 没写 exports → IdGenerator 对外彻底不可见
}
// 别人的项目再想直接用你的学号生成器:
import school.util.IdGenerator;
// 编译直接报错:
// 错误: 程序包 school.util 不可见
//      (该包位于模块 school 中,但未被导出)

模块化之前

跨包只能 public,而 public = 对整个项目开放,内部工具被迫"裸奔"。

模块化之后

封装从"类级"升到"模块级":private 管类内,跨包靠 public,出模块只认 exports。

模块只是给"包的集合"加了一道带门禁的墙。JDK 当年就因 sun.misc.Unsafe 等 public 内部类被用得不敢删,才在 Java 9 修了这堵墙。

五分钟上手:把项目变成模块 (*)

src/
├── module-info.java                    # ① 模块描述文件,必须放在源码根目录
├── school/manager/StudentManager.java  # 对外功能
└── school/util/IdGenerator.java        # 内部工具
// ② module-info.java
module school {
    exports school.manager;   // 只导出 manager 包
}
// 注:String、ArrayList 所在的 java.base 模块默认可用;
// 以后用到其他模块(如 java.sql)时,才需要用 requires 登记。
// ③ Java 代码本身没有任何新语法(文件头部与第 1 页相同)
package school.manager;
import school.util.IdGenerator;

public class StudentManager {
    public static void main(String[] args) {
        System.out.println(enroll("小明"));   // 小明 -> S001
    }
    public static String enroll(String name) {
        return name + " -> " + IdGenerator.next();
    }
}
# ④ 编译 & 运行
javac -d out src/module-info.java src/school/manager/*.java src/school/util/*.java
java -p out -m school/school.manager.StudentManager
#     └─ 模块路径        └─ 模块名/主类

核心动作只有两个:exports 是"开门给别人用",requires 是"登记我要用谁"。

常用模块指令 (*)

除了 requires 和 exports,module-info.java 还提供了几个更精细的指令:

  • requires transitive 模块名; —— 传递依赖:谁依赖我的模块,就自动可以读我所依赖的这个模块。
  • exports 包名 to 模块A, 模块B; —— 定向导出:只对指定模块开放,其他模块不可见。
  • opens 包名 to 模块A; —— 反射开放:允许指定模块在运行期通过反射访问该包(第 12 讲反射会再次遇到它)。
  • uses 服务接口; 与 provides 服务接口 with 实现类; —— 服务加载:配合 ServiceLoader 实现可插拔的 SPI 解耦。
// module-info.java
module com.example.features {
    requires transitive java.sql;            // 传递依赖

    exports com.example.features.api;        // 对所有模块导出
    exports com.example.features.internal    // 定向导出
        to com.example.test;

    opens   com.example.features.model       // 反射开放(ORM 框架需要)
        to org.hibernate.orm.core;
}

一句话区分:exports 是"编译期可见",opens 是"运行期反射可见"。同一个包可以既 export 又 open。

三种模块与常见混淆 (*)

引入模块系统后,项目里的所有代码都会被归入三种模块之一:

  • 具名模块(Named): 自带 module-info.java,完全遵循模块规则。
  • 自动模块(Automatic): 没有模块描述的普通 JAR 放上模块路径,自动导出并开放所有包——你用的第三方库大多属于这一类。
  • 未命名模块(Unnamed): 仍留在类路径上的类,等价于"模块化之前"的世界,可以读到所有具名模块导出的包。

类路径 classpath(-cp)

  • JAR 平铺,缺类要到运行时才暴露(NoClassDefFoundError)。
  • 第 1 讲命令行中编译运行用的就是它。

模块路径 module-path(-p)

  • 启动时解析模块依赖图,缺失或冲突立刻报错。
  • 错误发现得更早,但对生态兼容性要求更高。

面试高频混淆:JPMS 模块 ≠ Maven 多模块。前者是语言级模块(module-info.java,管封装与可见性),后者是构建级模块(聚合 POM 拆分工程,管构建与依赖管理),两者互补而非替代。

当前主流业务项目仍以 Maven + 类路径为主,模块系统属于加分项——理解概念、能应对面试追问即可。

封装实战:银行账户类

让我们通过一个完整的银行账户类来演示封装的威力。我们将逐步展示如何从一个不安全的类演进到一个完全封装的类。

❌ 第一步:不封装的危险版本


public class BankAccount {
    public String accountNumber;  // 任何人都能修改账号!
    public double balance;        // 余额可以被随意篡改!
    public String ownerName;      // 户主姓名可以被改变!
    
    // 没有任何保护措施
}

// 使用时的问题
BankAccount account = new BankAccount();
account.balance = -1000;  // 😱 余额变成负数!
account.accountNumber = ""; // 😱 账号被清空!

这种设计完全没有安全保障,任何人都可以随意修改账户信息!

⚠️ 第二步:基础封装版本


public class BankAccount {
    private String accountNumber;
    private double balance;
    private String ownerName;
    
    // 基础的getter和setter
    public double getBalance() { return balance; }
    public void setBalance(double balance) { this.balance = balance; }
    
    public String getOwnerName() { return ownerName; }
    public void setOwnerName(String ownerName) { this.ownerName = ownerName; }
}

虽然数据私有化了,但setter方法仍然允许不合理的操作!

✅ 第三步:完全封装的安全版本


public class BankAccount {
    private final String accountNumber;  // 账号一旦设定不可更改
    private double balance;
    private final String ownerName;      // 户主姓名不可更改
    private boolean isActive;            // 账户状态
    
    public BankAccount(String accountNumber, String ownerName) {
        if (accountNumber == null || accountNumber.trim().isEmpty()) {
            System.out.println("账号不能为空");
        }
        if (ownerName == null || ownerName.trim().isEmpty()) {
            System.out.println("户主姓名不能为空");
        }
        this.accountNumber = accountNumber;
        this.ownerName = ownerName;
        this.balance = 0.0;  // 新账户余额为0
        this.isActive = true;
    }
    
    // 只提供查询余额的方法,不提供直接设置余额的方法
    public double getBalance() {
        if (!isActive) {
            System.out.println("账户已被冻结");
        }
        return balance;
    }
    
    // 存款方法:有业务逻辑控制
    public void deposit(double amount) {
        if (!isActive) {
            System.out.println("账户已被冻结,无法存款");
            return; 
        }
        if (amount <= 0) {
            System.out.println("存款金额必须大于0");
            return;
        }
        if (amount > 50000) {
            System.out.println("单次存款不能超过5万元");
            return;
        }
        balance += amount;
        System.out.println("存款成功,当前余额:" + balance);
    }
    
    // 取款方法:有业务逻辑控制
    public void withdraw(double amount) {
        if (!isActive) {
            System.out.println("账户已被冻结,无法取款");
            return; 
        }
        if (amount <= 0) {
            System.out.println("取款金额必须大于0");
            return;
        }
        if (amount > balance) {
            System.out.println("余额不足");
            return;
        }
        if (amount > 20000) {
            System.out.println("单次取款不能超过2万元");
            return;
        }
        balance -= amount;
        System.out.println("取款成功,当前余额:" + balance);
    }
    
    // 冻结账户
    public void freeze() {
        isActive = false;
        System.out.println("账户已被冻结");
    }
    
    // 解冻账户
    public void unfreeze() {
        isActive = true;
        System.out.println("账户已解冻");
    }
    
    // 只读属性的getter
    public String getAccountNumber() { return accountNumber; }
    public String getOwnerName() { return ownerName; }
    public boolean isActive() { return isActive; }
}

通过完全封装,我们不仅保护了数据,还嵌入了业务规则,确保账户操作的安全性和合理性。

封装的应用:单例模式

单例模式是封装思想的高级应用。通过将构造方法私有化,我们完全控制了对象的创建过程。

单例模式的演进历程

🥚 饿汉式(Eager Loading)- 最简单但可能浪费内存


public class EagerSingleton {
    // 类加载时就创建实例(可能浪费内存)
    private static final EagerSingleton instance = new EagerSingleton();
    
    private EagerSingleton() {
        System.out.println("EagerSingleton 实例被创建");
    }
    
    public static EagerSingleton getInstance() {
        return instance;
    }
}

优点:线程安全,实现简单

缺点:类加载时就创建,可能浪费内存

😴 懒汉式(Lazy Loading)- 需要时才创建


public class LazySingleton {
    private static LazySingleton instance;
    
    private LazySingleton() {
        System.out.println("LazySingleton 实例被创建");
    }
    
    // 加synchronized确保线程安全,但性能较差
    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

优点:按需创建,节省内存

缺点:每次调用都要同步,性能较差

🏠 静态内部类(推荐)- 最优雅的解决方案


public class StaticInnerClassSingleton {
    
    private StaticInnerClassSingleton() {
        System.out.println("StaticInnerClassSingleton 实例被创建");
    }
    
    // 静态内部类,只有在被引用时才会加载
    private static class SingletonHolder {
        private static final StaticInnerClassSingleton INSTANCE = 
            new StaticInnerClassSingleton();
    }
    
    public static StaticInnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;  // 触发内部类加载
    }
}

优点:线程安全、按需加载、性能好、代码简洁

缺点:无明显缺点,推荐使用

🌍 真实世界中的单例模式应用

🖥️ 系统配置管理器


public class ConfigManager {
    private static volatile ConfigManager instance;
    private Properties config;
    
    private ConfigManager() {
        config = new Properties();
        loadConfig(); // 一个从磁盘读取配置文件并初始化config的方法
    }

    public static ConfigManager getInstance() {
        if (instance == null) {
            synchronized (ConfigManager.class) {
                if (instance == null) {
                    instance = new ConfigManager();
                }
            }
        }
        return instance;
    }
    
    public String getProperty(String key) {
        return config.getProperty(key);
    }
}

📝 日志记录器


public class Logger {
    private static class LoggerHolder {
        private static final Logger INSTANCE = new Logger();
    }
    
    private Logger() {
        // 初始化日志配置
    }
    
    public static Logger getInstance() {
        return LoggerHolder.INSTANCE;
    }
    
    public void log(String message) {
        System.out.println("[" + new Date() + "] " + message);
    }
}

单例模式展示了封装的强大力量:通过控制构造过程,我们可以精确管理对象的生命周期和数量。

封装的应用:代理模式

代理模式是封装思想的经典应用,通过代理对象控制对真实对象的访问,实现权限控制、缓存、日志记录等功能。

🎭 代理模式的核心思想

现实生活中的代理

  • 房产中介:代理房主与买家交易
  • 律师:代理当事人处理法律事务
  • 经纪人:代理明星处理商业活动

代理的作用:控制访问、增强功能、简化操作

编程中的代理

  • 访问控制:检查权限后再访问
  • 缓存代理:缓存结果提高性能
  • 日志代理:记录操作日志
  • 延迟加载:需要时才创建对象

🏗️ 代理模式结构(基础版)

// 1. 真实的图片显示类
class RealImageDisplay {
    private String filename;
    
    public RealImageDisplay(String filename) {
        this.filename = filename;
        loadImageFromDisk(); // 模拟耗时的图片加载
    }
    
    private void loadImageFromDisk() {
        System.out.println("正在从磁盘加载图片: " + filename);
        // 模拟耗时操作
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    
    public void display() {
        System.out.println("显示图片: " + filename);
    }
}

// 2. 代理类 - 添加缓存和延迟加载功能
class ImageProxy {
    private String filename;
    private RealImageDisplay realImage;
    private static java.util.Set loadedImages = new java.util.HashSet<>();
    
    public ImageProxy(String filename) {
        this.filename = filename;
        // 注意:这里不立即创建真实对象,实现延迟加载
    }
    
    public void display() {
        // 检查是否已经加载过
        if (loadedImages.contains(filename)) {
            System.out.println("从缓存显示图片: " + filename);
            return;
        }
        
        // 第一次访问时才创建真实对象
        if (realImage == null) {
            realImage = new RealImageDisplay(filename);
        }
        
        realImage.display();
        loadedImages.add(filename); // 标记为已加载
    }
}

🚀 代理模式演示

public class ProxyPatternDemo {
    public static void main(String[] args) {
        // 创建代理对象(不会立即加载图片)
        ImageProxy image1 = new ImageProxy("photo1.jpg");
        ImageProxy image2 = new ImageProxy("photo2.jpg");
        ImageProxy image3 = new ImageProxy("photo1.jpg"); // 同一张图片
        
        System.out.println("=== 代理对象已创建,但图片还未加载 ===");
        
        System.out.println("\n=== 第一次显示 ===");
        image1.display(); // 真实加载
        image2.display(); // 真实加载
        
        System.out.println("\n=== 第二次显示 ===");
        image1.display(); // 从缓存获取
        image3.display(); // 从缓存获取(同一张图片)
    }
}

/* 输出结果:
=== 代理对象已创建,但图片还未加载 ===

=== 第一次显示 ===
正在从磁盘加载图片: photo1.jpg
显示图片: photo1.jpg
正在从磁盘加载图片: photo2.jpg
显示图片: photo2.jpg

=== 第二次显示 ===
从缓存显示图片: photo1.jpg
从缓存显示图片: photo1.jpg
*/

🔧 代理的常见应用场景

🛡️ 权限控制代理

用途:检查用户权限

实现:在调用真实方法前先验证权限

if (!hasPermission()) return;

⚡ 缓存代理

用途:缓存计算结果

实现:先检查缓存,没有再计算

if (cache.has(key)) return cache.get(key);

📝 日志代理

用途:记录方法调用

实现:在方法调用前后记录日志

log("调用方法: " + methodName);

⏰ 延迟加载代理

用途:需要时才创建对象

实现:第一次使用时才创建真实对象

if (realObj == null) realObj = new RealObj();

✨ 代理模式的优势

🔒 安全控制

在不修改原类的情况下增加安全检查

⚡ 性能优化

通过缓存、延迟加载等技术提升性能

🔧 功能扩展

在不改变原有代码的基础上增加新功能

代理模式完美体现了封装的威力:通过代理对象隐藏复杂的控制逻辑,为客户端提供简洁的使用方式。代理就像现实中的"中介",帮我们处理复杂的细节。

什么是继承

继承是面向对象编程中最重要的概念之一。它不仅仅是代码复用,更是对现实世界中"分类"和"特化"关系的抽象。

继承关系图 默认继承关系图

继承带来的好处

🔄 代码复用

避免重复编写相同的代码,提高开发效率

📊 层次结构

建立清晰的类层次关系,便于理解和维护

🔧 扩展性

在不修改原有代码的基础上添加新功能

🎭 多态基础

为多态提供基础,实现灵活的程序设计

继承的基本规则

  • 单继承:Java中一个类只能直接继承一个父类(但可以实现多个接口)
  • 传递性:如果C继承B,B继承A,那么C也拥有A的特性
  • 访问控制:子类不能访问父类的private成员,但可以访问protected和public成员
  • 构造顺序:创建子类对象时,总是先调用父类的构造方法

类的继承

继承是代码复用的强大机制。它允许我们创建一个新类(子类),该类继承一个已存在类(父类)的属性和方法。

这形成了一种 "is-a" 的关系。例如,“学生”是一个“人”,“工人”也是一个“人”。学生和工人都拥有人的共性(如姓名、年龄),但又各自拥有独特的行为。

Inheritance Diagram

子类继承父类的“遗产”(非私有成员),并可以添加自己的新功能,实现“青出于蓝而胜于蓝”。

继承的语法:extends 关键字

在Java中,我们使用 extends 关键字来实现继承。一个类只能直接继承一个父类(单继承),但继承的层级可以无限延伸。


// 父类:Person
public class Person {
    String name;
    int age;

    public void hello() {
        System.out.println("你好,我叫 " + name);
    }
}

// 子类:Student 继承自 Person
public class Student extends Person {
    public void study() {
        // 可以直接使用从父类继承来的 name 属性
        System.out.println(name + " 正在学习...");
    }
}

// 使用
Student student = new Student();
student.name = "小明";
student.study(); // 调用自己的方法
student.hello(); // 调用从父类继承的方法

被 final 关键字修饰的类不能被继承。

父类构造器会被继承吗?

不同于成员变量和成员方法,父类的构造器并不被子类继承。但在创建子类对象时,会先调用父类构造器,这个调用发生在子类构造器中,通过 super 关键字完成。

父类构造器调用图
  • 如果未显式写出 super(...),编译器会自动插入一个 super() 去调用父类无参构造器。
  • super(...) 必须是子类构造器的第一行语句;否则编译报错。
  • 如果父类没有无参构造器,你又没有显式调用合适的 super(...),也会编译错误。

示例1:未写 super() 时的自动调用

// 父类有无参构造
class Person {
    public Person() {
        System.out.println("Person 无参构造执行");
    }
}

// 子类构造器没有显式写 super()
class Student extends Person {
    public Student() {
        // 这里编译器会自动插入 super()
        System.out.println("Student 无参构造执行");
    }
}

// 使用
new Student();
// 输出:
// Person 无参构造执行
// Student 无参构造执行

示例2:父类没有无参构造,必须显式调用 super(...)

class Animal {
    public Animal(String name) {
        System.out.println("Animal 构造:" + name);
    }
}

class Dog extends Animal {
    public Dog() {
        // 编译错误:没有可用的无参构造可以自动调用
        // 必须在第一行显式写 super(name)
    }
}

// 正确写法:
class Dog2 extends Animal {
    public Dog2(String name) {
        super(name); // 必须是第一行!
        System.out.println("Dog2 构造:" + name);
    }
}

结论:构造器不继承,只能在子类构造器中通过 super(...) 调用父类构造器;且 super 必须写在第一行(不写时系统会尝试补一个 super())。

super 关键字

super 并不是一个独立的对象,它表示“当前对象的父类部分(父类视角)”。因此可以:

  • 在构造器中用 super(...) 调用父类构造器。
  • 在实例方法/实例代码块中用 super.xxx 访问被子类遮盖的父类成员。

为什么 super 能准确定位到父类?

因为 Java 采用 单继承:一个类只能直接继承一个父类。因此 super 的指向是 唯一且确定 的,不存在多重父类的歧义。

super 与 this 的限制

  • super 与 this 都不能出现在 static 方法或 static 代码块中(static 属于类本身,并不依赖某个具体实例)。
  • 只有在 实例方法、实例代码块、构造器 中才能使用 super/this。

遮盖(Shadowing)与 super 的作用

当子类定义了与父类 同名的实例变量 时,子类的变量会 遮盖 父类变量。在子类自己的方法中,默认访问到的是子类变量;若要访问父类变量,需要用 super.变量名。

class Father {
    public int a = 5;
    public void f() { System.out.println("Father"); }
}

class Son extends Father {
    public int a = 7; // 子类的 a 遮盖了父类的 a

    @Override
    public void f() {
        System.out.println(a);      // 访问子类的 a -> 输出 7
    }

    public void g() {
        System.out.println(super.a); // 访问父类的 a -> 输出 5
        super.f();                   // 调用父类的 f() -> 输出 "Father"
    }
}

变量查找(未显式指定时)

在某个方法中访问名为 x 的变量但未显式指定时,查找顺序为:

  1. 先看该方法中是否有名为 x 的 局部变量
  2. 再看当前类中是否包含名为 x 的 成员变量
  3. 逐层向上寻找直接父类中的 可见成员变量
  4. 直到 java.lang.Object,还找不到则编译报错

总结:super 提供父类视角的访问,解决遮盖问题;由于单继承,super 的目标唯一。
记住:super/this 只能用于与实例相关的场景,不能在 static 中使用。

super 关键字深度解析

super 关键字是子类与父类之间的重要纽带,它有三个主要用途,让我们逐一深入了解:

1️⃣ 调用父类构造方法

规则:super(...) 必须是子类构造方法的第一行语句


public class Vehicle {
    protected String brand;
    protected int year;
    
    public Vehicle(String brand, int year) {
        this.brand = brand;
        this.year = year;
        System.out.println("Vehicle 构造方法被调用");
    }
}

public class Car extends Vehicle {
    private int doors;
    
    public Car(String brand, int year, int doors) {
        super(brand, year);  // 必须是第一行!
        this.doors = doors;
        System.out.println("Car 构造方法被调用");
    }
    
    // 如果不显式调用 super(),Java会自动调用父类的无参构造方法
    // 如果父类没有无参构造方法,编译会报错!
}

2️⃣ 访问父类的方法(方法重写中的应用)

场景:子类重写父类方法时,想要在新功能基础上保留父类的原有功能


public class BankAccount {
    protected double balance;
    
    public void deposit(double amount) {
        balance += amount;
        System.out.println("存款 " + amount + ",余额:" + balance);
    }
}

public class SavingsAccount extends BankAccount {
    private double interestRate = 0.02;
    
    @Override
    public void deposit(double amount) {
        super.deposit(amount);  // 先执行父类的存款逻辑
        
        // 然后添加储蓄账户特有的利息计算
        double interest = amount * interestRate;
        balance += interest;
        System.out.println("获得利息:" + interest + ",最终余额:" + balance);
    }
}

3️⃣ 访问父类的属性(解决命名冲突)

场景:当子类和父类有同名属性时,明确指定访问父类的属性


public class Animal {
    protected String name = "动物";
    
    public void showInfo() {
        System.out.println("我是:" + name);
    }
}

public class Dog extends Animal {
    private String name = "狗狗";  // 与父类同名
    
    public void showAllNames() {
        System.out.println("父类的 name:" + super.name);  // 输出:动物
        System.out.println("子类的 name:" + this.name);   // 输出:狗狗
        System.out.println("直接访问 name:" + name);       // 输出:狗狗(默认是this.name)
    }
    
    @Override
    public void showInfo() {
        super.showInfo();  // 调用父类的方法
        System.out.println("我还是:" + this.name);
    }
}

🎯 完整示例:构造方法链的执行顺序


public class SuperDemo {
    public static void main(String[] args) {
        System.out.println("=== 创建 SavingsAccount 对象 ===");
        SavingsAccount account = new SavingsAccount("张三", 1000);
        
        System.out.println("\n=== 执行存款操作 ===");
        account.deposit(500);
        
        System.out.println("\n=== 显示信息 ===");
        account.showAllInfo();
    }
}

/* 输出结果:
=== 创建 SavingsAccount 对象 ===
BankAccount 构造方法:初始余额 1000.0
SavingsAccount 构造方法:设置账户持有人 张三

=== 执行存款操作 ===
存款 500.0,余额:1500.0
获得利息:10.0,最终余额:1510.0

=== 显示信息 ===
账户持有人:张三
当前余额:1510.0
*/

💡 super 关键字使用要点

  • 构造方法:super() 必须是第一行,且只能在构造方法中使用
  • 方法调用:super.methodName() 可以在任何实例方法中使用
  • 属性访问:super.fieldName 用于访问父类的非私有属性
  • 静态上下文:不能在静态方法或静态代码块中使用 super
  • 链式调用:super 只能访问直接父类,不能跨级访问祖父类

方法的重写 (Override)

大部分时候子类总是以父类为基础,再额外增加新的变量和方法,但有情况例外:

子类具有父类声明的功能,但是具体实现有所不同:多鳍鱼是鱼却可用肺呼吸,鸵鸟是鸟却不会飞。

class Bird {
    public void fly(){
        System.out.println(“自由飞翔”);
    }
}
class Ostrich extends Bird{
    public void fly(){
        System.out.println(“我只能在地上跑...”);
    }
}

重写 (Override) 是指子类可以提供一个与父类相同的方法签名的新实现,从而“覆盖”父类的行为。

什么是“方法签名” (Method Signature)?

方法签名 = 方法名 + 参数列表(参数的个数、类型、顺序)。

注意:方法签名不包含以下信息:

  • 返回值类型(例如 int / String)
  • 参数名称(例如 a / b)
  • 方法修饰符(public、static 等)
  • 异常声明(throws ...)
  • 泛型参数本身(重载基于擦除后的参数类型)

重写 vs 重载 与方法签名的关系

  • 重写 (Override):父子类之间,方法签名必须相同;返回类型允许协变(子类方法可以返回更窄的引用类型)。
  • 重载 (Overload):同一个类中,方法名相同,但参数列表不同(数量/类型/顺序至少有一处不同)。
  • 不能被重写的情况:static 方法(只能隐藏)、final 方法、private 方法(对子类不可见)。

辨析题:下面哪些是重写?哪些是重载?哪些会编译错误?

// 1) 重写允许“协变返回类型”
class A { Number f() { return 1; } }
class B extends A { Integer f() { return 2; } } // ✅ 重写(签名相同,返回类型更窄)

// 2) 参数列表不同 -> 重载
class Demo {
    void m(int a) {}
    void m(double a) {}             // ✅ 重载(类型不同)
    void m(int a, double b) {}
    void m(double a, int b) {}      // ✅ 重载(顺序不同)
}

// 3) 仅改返回类型 -> 编译错误(签名相同,无法仅凭返回类型区分)
class C {
    int add(int a, int b) { return a + b; }
    // double add(int a, int b) { return a + b; } // ❌ 编译错误:签名相同
}

// 4) 可变参数本质是数组 -> 与同类型数组签名相同,不能同时存在
class E {
    int sum(int... nums) { return 0; }
    // int sum(int[] nums) { return 0; } // ❌ 编译错误:签名等同(int... -> int[])
}

// 5) equals 的常见坑:以下是重载,不是重写
class Person {
    boolean equals(String s) { return false; }   // ⚠️ 重载(未重写 Object.equals(Object))
    @Override
    public boolean equals(Object o) { return super.equals(o); } // ✅ 正确重写
}

总结:判断重写/重载的核心在于“方法签名”。重写要求签名一致(返回类型可协变),重载要求参数列表发生变化。
仅靠返回类型、修饰符或异常声明都无法区分重载。

这与重载 (Overload) 不同:重载是在同一个类中定义多个同名但参数列表不同的方法。

方法重写的规则

重写的“两同两小一大”规则

  • 两同:方法名相同、参数列表相同(签名一致)。
  • 两小:
    • 返回类型更小或相等:子类方法可以返回更窄的引用类型(协变返回),例如 Number → Integer。
    • 抛出异常更小或相等:子类方法声明的受检异常集合必须是父类受检异常的子集(更具体,更少)。
  • 一大:访问权限更大或相等(≥):private < 默认(package-private) < protected < public。
  • 一致:父子类的方法要有同样的静态标识。实际上,子类的static方法只是隐藏父类的static方法。

实验:静态方法只能隐藏,调用按“引用类型”静态绑定

class Base {
    public static void s() { System.out.println("Base.s"); }
}
class Sub extends Base {
    public static void s() { System.out.println("Sub.s"); } // 隐藏(不是重写)

    public void demo() {
        Base.s(); // 调用父类静态方法
        Sub.s();  // 调用子类静态方法
    }
}

Base b = new Sub();
b.s();        // 输出:Base.s   —— 静态绑定,按“引用类型”决定
Sub.s();      // 输出:Sub.s
new Sub().demo(); // Base.s 然后 Sub.s

实验:两小和一大

// 协变返回类型示例
class A { Number f() { return 1; } }
class B extends A { @Override Integer f() { return 2; } } // ✅ 返回类型更窄

// 受检异常示例
import java.io.*;
class P { void read() throws IOException {} }
class C extends P { @Override void read() throws FileNotFoundException {} } // ✅ 更具体
// class C2 extends P { @Override void read() throws Exception {} } // ❌ 更宽,编译错误

// 访问权限示例
class Base {
    protected void m() {}
}
class Sub extends Base {
    @Override public void m() {} // ✅ 权限更大(protected -> public)
    // @Override void m() {}     // ❌ 权限更小(protected -> package-private),编译错误
}

final 修饰符:"最终的" → 不可改

final 关键字表示"最终的"、"不可改变的",可以修饰类、方法和变量,用于限制继承、重写和修改。

🔒 final 的三种用法

final 类

不能被继承

final class Math {
    // 数学工具类
    // 不允许被继承
}

例:String、Integer、Math 等

final 变量

表示常量,必须初始化且不能被修改

final static double PI = 3.14159;
final int MAX_SIZE = 100;

// 编译错误:
// PI = 3.14; // ❌ 不能修改

常量命名通常使用大写字母

final 方法

在子类中不能被重写

class Parent {
    final void display() {
        System.out.println("不能被重写");
    }
}

class Child extends Parent {
    // void display() {} // ❌ 编译错误
}

确保方法行为不被改变

💡 实际应用示例

// 1. 配置常量
public class Config {
    public static final String APP_NAME = "MyApp";
    public static final int MAX_CONNECTIONS = 100;
    public static final double VERSION = 1.0;
}

// 2. 不可变类设计
public final class ImmutablePoint {
    private final int x;
    private final int y;
    
    public ImmutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    public int getX() { return x; }
    public int getY() { return y; }
    // 没有 setter 方法,对象创建后不可变
}

// 3. 模板方法模式
public abstract class Template {
    // 算法骨架,不允许子类修改
    public final void execute() {
        step1();
        step2();
        step3();
    }
    
    protected abstract void step1();
    protected abstract void step2();
    protected abstract void step3();
}

总结:final 修饰符提供了强制的不可变性约束。final 类不能被继承(如 String),final 变量是常量(必须初始化),final 方法不能被重写。合理使用 final 可以提高代码的安全性和设计的清晰度。

万物之源: Object 类

在Java中,如果一个类没有明确地 extends 另一个类,那么它就默认继承自 java.lang.Object 类。

因此,Object 类是所有类的“祖先”,它提供了一些所有对象都应具备的基本方法。

常用方法包括:

  • String toString(): 返回对象的字符串表示。
  • boolean equals(Object obj): 比较两个对象是否“相等”。
  • int hashCode(): 返回对象的哈希码。
  • Class getClass(): 返回对象的运行时类。

重写示例:equals() 和 toString()

默认的 equals() 比较的是内存地址,而我们通常希望比较对象的内容。默认的 toString() 输出的是类名和哈希码,我们希望看到更有意义的信息。


public class Person {
    private String name;
    private int age;
    // ... 构造方法 ...

    @Override
    public String toString() {
        return "Person{" + "name='" + name + ''' + ", age=" + age + '}';
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true; // 是同一个对象
        if (obj == null || getClass() != obj.getClass()) return false; // 类型不同
        Person person = (Person) obj;
        // 比较核心属性
        return age == person.age && name.equals(person.name);
    }
}

重写后,即使是两个不同的对象,只要内容相同,equals() 也会返回 true。

继承链:生物分类系统

让我们通过一个生物分类系统来深入理解继承链的工作原理。我们将从最抽象的"生物"开始,逐步细化到具体的"狗"。

🌍 第一层:生物 (Organism)


public class Organism {
    protected String name;
    protected int age;
    protected boolean isAlive;
    
    public Organism(String name) {
        this.name = name;
        this.age = 0;
        this.isAlive = true;
        System.out.println("一个新的生物诞生了:" + name);
    }
    
    public void grow() {
        if (isAlive) {
            age++;
            System.out.println(name + " 长大了,现在 " + age + " 岁");
        }
    }
    
    public void die() {
        isAlive = false;
        System.out.println(name + " 死亡了");
    }
    
    // 所有生物都需要呼吸,但方式不同(留给子类实现)
    public void breathe() {
        System.out.println(name + " 在呼吸");
    }
}

🦴 第二层:动物 (Animal)


public class Animal extends Organism {
    protected double weight;
    protected String habitat;  // 栖息地
    
    public Animal(String name, String habitat) {
        super(name);  // 调用父类构造方法
        this.habitat = habitat;
        this.weight = 1.0;
        System.out.println("这是一只动物,生活在:" + habitat);
    }
    
    // 重写父类的呼吸方法
    @Override
    public void breathe() {
        System.out.println(name + " 用肺呼吸");
    }
    
    // 动物特有的行为
    public void move() {
        if (isAlive) {
            System.out.println(name + " 在移动");
        }
    }
    
    public void eat(String food) {
        if (isAlive) {
            System.out.println(name + " 吃了 " + food);
            weight += 0.1;
        }
    }
    
    public void sleep() {
        System.out.println(name + " 在睡觉");
    }
}

🐕 第三层:哺乳动物 (Mammal)


public class Mammal extends Animal {
    protected boolean hasFur;
    protected double bodyTemperature;
    
    public Mammal(String name, String habitat, boolean hasFur) {
        super(name, habitat);
        this.hasFur = hasFur;
        this.bodyTemperature = 37.0;  // 恒温动物
        System.out.println("这是一只哺乳动物," + (hasFur ? "有毛发" : "无毛发"));
    }
    
    // 哺乳动物特有的行为
    public void feedMilk() {
        System.out.println(name + " 在哺乳");
    }
    
    public void regulateTemperature() {
        System.out.println(name + " 调节体温到 " + bodyTemperature + "°C");
    }
    
    // 重写移动方法,更具体化
    @Override
    public void move() {
        if (isAlive) {
            System.out.println(name + " 用四肢在 " + habitat + " 中移动");
        }
    }
}

🐶 第四层:狗 (Dog)


public class Dog extends Mammal {
    private String breed;  // 品种
    private String owner;  // 主人
    
    public Dog(String name, String breed, String owner) {
        super(name, "家庭", true);  // 狗生活在家庭,有毛发
        this.breed = breed;
        this.owner = owner;
        System.out.println("这是一只 " + breed + ",主人是 " + owner);
    }
    
    // 狗特有的行为
    public void bark() {
        System.out.println(name + " 汪汪叫");
    }
    
    public void wagTail() {
        if (isAlive) {
            System.out.println(name + " 摇尾巴表示开心");
        }
    }
    
    public void fetch(String item) {
        System.out.println(name + " 去捡回 " + item);
    }
    
    // 重写吃的方法,更具体化
    @Override
    public void eat(String food) {
        if (food.equals("骨头") || food.equals("狗粮")) {
            super.eat(food);
            wagTail();  // 吃到喜欢的食物会摇尾巴
        } else {
            System.out.println(name + " 不喜欢吃 " + food);
        }
    }
    
    // 重写移动方法
    @Override
    public void move() {
        if (isAlive) {
            System.out.println(name + " 在 " + habitat + " 中奔跑和散步");
        }
    }
}

🎯 使用示例:看看继承链的威力


public class InheritanceDemo {
    public static void main(String[] args) {
        // 创建一只狗
        Dog myDog = new Dog("旺财", "金毛", "小明");
        
        // 狗可以使用所有祖先类的方法
        myDog.grow();           // 来自 Organism
        myDog.breathe();        // 来自 Animal (重写了 Organism)
        myDog.eat("狗粮");       // 来自 Dog (重写了 Animal)
        myDog.feedMilk();       // 来自 Mammal
        myDog.bark();           // 来自 Dog
        myDog.move();           // 来自 Dog (重写了 Mammal)
        
        System.out.println("继承链演示完成!");
    }
}

/* 输出结果:
一个新的生物诞生了:旺财
这是一只动物,生活在:家庭
这是一只哺乳动物,有毛发
这是一只金毛,主人是小明
旺财 长大了,现在 1 岁
旺财 用肺呼吸
旺财 吃了 狗粮
旺财 摇尾巴表示开心
旺财 在哺乳
旺财 汪汪叫
旺财 在 家庭 中奔跑和散步
继承链演示完成!
*/

通过这个完整的继承链例子,我们看到了继承如何让代码层次清晰,每一层都在前一层的基础上添加新的特性,形成了一个完整的类族谱。狗类可以使用所有祖先类的功能!

什么是多态

多态(Polymorphism)是面向对象编程的精髓:同一个方法调用可以根据对象的实际类型产生不同的行为。

  • Java 引用变量有两种类型:编译时类型(由声明决定)与运行时类型(由实际赋给该变量的对象决定)。
  • 当编译时类型与运行时类型不一致时,就产生了多态。
  • 语法:SuperType x = new SubType(构造器参数);
  • 多态意味着父类变量可以指向子类对象(俗称“爸爸打儿子”)。
  • 问题:下方代码 a.speak() 到底执行哪个?
class Animal {
    public void speak() { System.out.println("Animal speak"); }
    public void eat() { System.out.println("Animal eat"); }
}
class Dog extends Animal {
    @Override
    public void speak() { System.out.println("Dog speak"); }
    public void wagTail() { System.out.println("Dog wag tail"); }
}

Animal a = new Dog();        // 编译时类型:Animal;运行时类型:Dog
a.speak();                   // 运行时调用 Dog.speak —— 多态(动态绑定)
// a.wagTail();              // 编译错误:编译时类型是 Animal,无法访问 Dog 独有方法

if (a instanceof Dog) {      // instanceof 关键字用于检测某对象是否是某类的实例
    Dog d = (Dog) a;         // 向下转型
    d.wagTail();             // 正常调用 Dog 的特有方法
}

多态的精髓:编译时类型决定了能访问的成员范围;运行时类型决定了方法调用的实际实现(动态绑定)。

实现多态的三个条件

  1. 继承关系:必须有父类和子类的继承关系
  2. 方法重写:子类必须重写父类的方法
  3. 向上转型:父类引用指向子类对象

多态的核心价值

  • 🔧 降低耦合:调用者不需要知道具体的实现类,只需要知道接口
  • 📈 提高扩展性:新增子类无需修改现有代码
  • 🎯 统一处理:可以用统一的方式处理不同类型的对象
  • 🔄 代码复用:同一套代码可以处理多种类型

多态与动态绑定

编译器明明只知道 a 是 Animal类型,它怎么“智能”地调用了 Dog 的方法呢?
这背后就是Java实现多态的核心机制:动态绑定。

绑定:把一个“方法调用”(如 a.speak())和“方法具体的实现代码”关联起来的过程。绑定分为两种:

静态绑定 (Static Binding)
  • 时机: 编译时
  • 特点: 编译器100%确定调用哪个方法
  • 适用: private, static, final 方法
  • 关键: 看 引用的声明类型
动态绑定 (Dynamic Binding)
动态绑定
  • 时机: 运行时
  • 特点: 运行时JVM检查对象实际类型再决定
  • 适用: 所有可重写的实例方法(虚方法)
  • 关键: 看 对象的实际类型

动态绑定与虚方法

动态绑定得以实现,首先要感谢 虚方法 的概念。

  • 在C++等语言中,需要用 virtual 关键字标记一个方法,告诉编译器“它可能被重写”。
  • 而Java的设计更直接:
    每个类所有非 static、非 private、非 final 的实例方法,默认全部都是虚方法!

JVM在运行时通过方法表 (vtable)来实现动态绑定。

JVM为每个类维护一个方法表,其中包含方法签名到实际方法实现的映射:

// Animal 的方法表
class Animal {
  /*
    sepak()     -> Animal.speak
    eat()       -> Animal.eat
  */
}

// Dog 的方法表
class Dog extends Animal {
  /*
    speak()     -> Dog.speak (已重写)
    eat()       -> Animal.eat (继承而来)
    swagTail()  -> Dog.swagTail
  */
}

// 运行时调用过程:
// Animal a = new Dog();

// 1. 创建 Dog 对象时,其内部会包含一个
//    指向 Dog 类方法表的引用。

// a.speak();
// 2. JVM通过对象引用找到 Dog 的方法表。
// 3. 在表中查找 speak() 签名。
// 4. 执行该签名指向的实际方法实现,
//    即 Dog.speak()。

多态实战:构建游戏世界👾(1)

让我们通过一个“地牢探险”游戏的角色系统来深入理解多态的强大威力。

⚔️ 敌人的设计 (Enemy.java)

首先,我们定义所有敌人的“共同特征”。它们都有生命值,都能被攻击,但它们各自的攻击方式不同。


// 敌人基类
public class Enemy {
    protected String name;
    protected int health;

    public Enemy(String name, int health) {
        this.name = name;
        this.health = health;
    }

    // 这就是一个“空方法”,等待子类去具体实现,因为不同敌人的攻击方式不同
    public void attack(){}

    // 具体方法:所有敌人受到伤害的逻辑是相同的
    public void takeDamage(int damage) {
        this.health -= damage;
        System.out.println(name + " 受到了 " + damage + " 点伤害,剩余 " + health + " 生命值。");
        if (this.health <= 0) {
            System.out.println(name + " 被击败了!");
        }
    }

    public boolean isAlive() {return this.health > 0;}
}

🧟 具体的敌人:哥布林 (Goblin.java)


public class Goblin extends Enemy {
    public Goblin() {
        // 调用父类构造方法
        super("哥布林", 50); 
    }

    // 重写(Override)父类的方法
    @Override
    public void attack() { System.out.println("哥布林挥舞着生锈的匕首,造成了 5 点攻击!"); }
}

🐲 具体的敌人:巨龙 (Dragon.java)


public class Dragon extends Enemy {
    public Dragon() {
        super("邪恶巨龙", 500); // 巨龙血量更多
    }

    // 重写(Override)父类的方法
    @Override
    public void attack() {
        System.out.println("巨龙喷射出毁灭性的烈焰,造成了 40 点范围攻击!");
    }

    // 巨龙甚至可以有自己独特的方法
    public void fly() {
        System.out.println("巨龙飞向天空,暂时无法被近战攻击!");
    }
}

多态实战:构建游戏世界👾(2)

游戏主循环 (GameManager.java):我们不需要关心“具体”是什么敌人,只需要把它们都当作“敌人”来看待。

🔥 战斗开始!

想象一下,玩家进入了一个满是敌人的房间。我们用一个数组来管理所有敌人:


public class GameManager {
    public static void main(String[] args) {
        // 1. 创建一个“敌人”数组,我们用父类 Enemy 作为数组类型
        Enemy[] enemies = {new Goblin(), new Dragon(), new Goblin()};
        System.out.println("--- 玩家回合:所有敌人受到10点伤害 ---");
        for (Enemy e : enemies) {
            // 调用所有敌人“共有”的方法
            e.takeDamage(10); 
        }

        System.out.println("n--- 敌人回合:所有敌人开始攻击! ---");
        
        // 2. 动态绑定的魔力!
        for (Enemy e : enemies) {
            if (e.isAlive()) {
                // 编译器:只知道 e 是 Enemy 类型,它有 attack() 方法。
                // 运行时:JVM 检查 e 到底指向谁?
                // 如果 e 是 Goblin,就执行 Goblin 的 attack()
                // 如果 e 是 Dragon,就执行 Dragon 的 attack()
                e.attack(); // 动态绑定发生在这里
            }
        }
    }
}

向上转型与向下转型:多态的体现

向上转型 (Upcasting): 将子类对象赋值给父类引用。这是自动且安全的。

Person person = new Student("小明", 18, "男");

向上转型后,该引用只能调用父类中定义的方法,但如果该方法在子类中被重写,实际执行的是子类的版本。这就是多态!

向下转型 (Downcasting): 将父类引用强制转换回子类引用。这需要显式转换,并且有风险。

Student student = (Student) person;

向下转型前,必须使用 instanceof 关键字进行检查,否则可能在运行时抛出 ClassCastException。


if (person instanceof Student) {
    Student student = (Student) person;
    student.study(); // 安全地调用子类特有的方法
}

抽象类

当一个父类只知道其子类应该包含什么方法,但无法确定这些方法的具体实现时,就可以将父类定义为抽象类。

核心规则

  • 定义: 使用 abstract 关键字修饰的类是抽象类,修饰的方法是抽象方法。
  • 抽象方法: 只有方法声明,没有方法体 { },以分号结尾。如abstract void myMethod();
  • 包含抽象方法的类 必须 被声明为抽象类。
  • 抽象类 不能 被实例化 (不能 new),但可以作为引用类型,用于实现多态。
  • 继承抽象类的子类 必须 实现父类所有的抽象方法,除非该子类自己也是一个抽象类。
  • 一个抽象子类可以重写父类的具体方法,并将其重新声明为抽象方法。

// 1. 定义抽象类和抽象方法
public abstract class Animal {
    public abstract void makeSound(); // 抽象方法

    public void run() { // 具体方法
        System.out.println("动物在跑...");
    }
}

// 2. 子类继承并实现抽象方法
public class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("汪汪!");
    }
}

// 3. 使用:不能实例化父类,但可用于多态
Animal myPet = new Dog();
myPet.makeSound(); // 输出: 汪汪!

// Animal shelter = new Animal(); // 编译错误!抽象类不能被实例化

接口

接口是比抽象类更纯粹的抽象。它只定义了一组行为规范(方法签名),而不关心任何实现。

接口体现了 "has-a" 或 "can-do" 的关系。

  • 接口中的方法默认是 public abstract 的。
  • 接口中的变量默认是 public static final 的(常量)。
  • 实现接口的类必须实现其所有抽象方法。


// 定义“会游泳”的能力
public interface Swimmable {
    void swim();
}

// Duck 类实现了这两种能力
public class Duck implements Swimmable {

    @Override
    public void swim() { System.out.println("鸭子在游泳..."); }
}

接口的继承

接口不仅能被类实现,还可以像类一样继承其他接口,从而在父接口的基础上扩展能力。接口也支持多重继承(extends A, B)。

// 父接口:通用“可移动”能力
public interface Movable {
    void move();
}

// 子接口:在父接口基础上扩展“会飞/会游泳”
public interface Flyable extends Movable {
    void fly();
}

public interface Swimmable extends Movable {
    void swim();
}

// 子接口可以多重继承,组合多种能力
public interface Amphibious extends Flyable, Swimmable { }

// 实现类只需实现组合后的全部抽象方法
public class DuckHybrid implements Amphibious {
    @Override public void move() { System.out.println("移动中..."); }
    @Override public void fly()  { System.out.println("鸭子在飞..."); }
    @Override public void swim() { System.out.println("鸭子在游泳..."); }
}

要点

  • 子接口会继承父接口的抽象方法与默认方法。
  • 当多父接口存在同名默认方法冲突时,子接口或实现类必须显式覆盖。
  • 接口可多重继承(extends A, B),类不可多重继承但可实现多个接口。

接口的“菱形问题”

一个类可以实现多个接口。

当一个类实现多个接口,而这些接口恰好包含同名、同参数的默认方法时,编译器会陷入困惑:究竟该继承哪一个?这就是著名的“菱形问题”。Java 要求开发者必须明确解决这个冲突。

场景:飞行与游泳的冲突

假设我们有两个接口,Flyer 和 Swimmer,它们都有一个名为 takeOff() 的默认方法。


public interface Flyer {
    default void takeOff() {
        System.out.println("扇动翅膀,飞向天空!");
    }
}

public interface Swimmer {
    default void takeOff() {
        System.out.println("双腿一蹬,潜入水中!");
    }
}

冲突的产生

现在,一个“水陆两栖飞机”AmphibiousPlane 试图同时实现这两个接口。此时,编译器会报错,因为它不知道该选用哪个 takeOff() 实现。


// 下面的代码会导致编译错误:AmphibiousPlane inherits unrelated defaults for takeOff()
// public class AmphibiousPlane implements Flyer, Swimmer {}

解决方案:显式覆盖

解决方法是在实现类 AmphibiousPlane 中重写 takeOff() 方法,并明确指定要调用哪个父接口的默认实现,或者提供一个全新的实现。


public class AmphibiousPlane implements Flyer, Swimmer {
    @Override
    public void takeOff() {
        // 方案一:选择其中一个父接口的实现
        System.out.println("决策:优先选择飞行模式...");
        Flyer.super.takeOff(); // 使用 Flyer 的默认方法

        // 方案二:或者,你也可以调用 Swimmer 的
        // Swimmer.super.takeOff();

        // 方案三:提供一个全新的实现
        // System.out.println("水陆两栖模式启动,涡轮喷射,水花四溅,冲上云霄!");
    }
}

核心规则

当一个类面临来自多个父接口的同名默认方法冲突时,它必须通过重写(@Override)该方法来消除歧义。在方法内部,可以使用 InterfaceName.super.methodName() 语法来精确调用特定父接口的默认实现。

接口与多态

接口也可以实现多态。当一个类实现一个接口时,它的对象实例就可以被这个接口类型的引用所指向。

这允许我们编写更通用的代码,操作的是接口而不是具体的类,从而实现“面向接口编程”,极大地提高了代码的灵活性和可扩展性。


// 1. 定义一个接口,作为行为契约
public interface Flyable {
    void fly();
}

// 2. 不同的类实现这个接口
public class Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("小鸟在空中振翅飞翔...");
    }
}

public class Airplane implements Flyable {
    @Override
    public void fly() {
        System.out.println("飞机在高空引擎轰鸣地飞行...");
    }
}

// 3. 使用接口引用实现多态
public class TestPolymorphism {
    public static void main(String[] args) {
        // 接口引用指向不同的实现类对象
        Flyable flyer1 = new Bird();
        Flyable flyer2 = new Airplane();

        makeItFly(flyer1); // 输出: 小鸟在空中振翅飞翔...
        makeItFly(flyer2); // 输出: 飞机在高空引擎轰鸣地飞行...
    }

    // 这个方法接受任何实现了Flyable接口的对象
    public static void makeItFly(Flyable f) {
        f.fly();
    }
}

接口深度解析:现代Java接口特性

Java 8+ 为接口引入了新特性,使接口更加强大和灵活。

🚀 现代接口的完整特性


public interface ModernInterface {
    // 1. 常量(隐式 public static final)
    int MAX_SIZE = 100;
    String DEFAULT_NAME = "Unknown";
    
    // 2. 抽象方法(隐式 public abstract)
    void abstractMethod();
    
    // 3. 默认方法(Java 8+)- 可以有实现
    default void defaultMethod() {
        System.out.println("这是默认实现");
        // 可以调用其他方法
        abstractMethod();
    }
    
    // 4. 静态方法(Java 8+)
    static void staticMethod() {
        System.out.println("接口的静态方法");
    }
    
    // 5. 私有方法(Java 9+)- 用于代码复用
    private void privateHelper() {
        System.out.println("私有辅助方法");
    }
    
    // 6. 私有静态方法(Java 9+)
    private static void privateStaticHelper() {
        System.out.println("私有静态辅助方法");
    }
}

📈 接口的演进历程

  • Java 7及之前:只能有抽象方法和常量
  • Java 8:引入默认方法和静态方法
  • Java 9:支持私有方法和私有静态方法
  • 现在:接口几乎具备了类的所有功能(除了实例变量和构造方法)

综合练习:引用类型判断

根据下图的类与接口关系,思考问题:___ c2 = new Class2(); 中,横线处可以填写哪些类型?

类与接口继承关系图

答案解析

根据Java的多态规则,一个引用可以指向该类本身的对象,或其任意父类/实现的接口的对象。因此,可填写的类型有:

  • Class2: 对象自身的类型。
  • Object: 所有类的根父类。
  • Interface2_1: Class2 实现的接口。
  • Interface2_2: Class2 实现的另一个接口。

// 对应的代码结构
interface Interface2_1 {}
interface Interface2_2 {}
class Class2 implements Interface2_1, Interface2_2 {}

public class Main {
    public static void main(String[] args) {
        // 以下所有赋值都是合法的
        Class2       ref1 = new Class2();
        Object       ref2 = new Class2();
        Interface2_1 ref3 = new Class2();
        Interface2_2 ref4 = new Class2();

        System.out.println("所有引用赋值均有效。");
    }
}

抽象类 vs 接口:深度对比分析

抽象类和接口都是实现抽象的重要手段,但它们有着不同的设计理念和使用场景。

📊 详细对比表

对比维度 抽象类 (Abstract Class) 接口 (Interface)
设计理念 "is-a" 关系,表示本质 "can-do" 关系,表示能力
继承数量 单继承(extends) 多实现(implements)
方法类型 抽象方法 + 具体方法 抽象 + 默认 + 静态 + 私有方法
成员变量 任意访问修饰符,可变 public static final(常量)
构造方法 可以有构造方法 不能有构造方法
实例化 不能直接实例化 不能直接实例化

🎯 设计原则和选择指南

🏗️ 选择抽象类的场景

  • 有共同的代码需要复用
  • 需要定义非public的方法
  • 需要定义实例变量
  • 类之间有明确的"is-a"关系
  • 需要提供构造方法

🔌 选择接口的场景

  • 定义一组能力或行为规范
  • 需要多重继承的效果
  • 不同类需要相同的行为
  • 强调"can-do"的能力关系
  • API设计和框架开发

实战案例:智能设备管理系统

通过一个完整的案例来理解抽象类和接口的协同使用。

🌍 系统设计思路


// 抽象类:定义设备的共同本质(is-a关系)
public abstract class SmartDevice {
    protected String deviceId;
    protected String brand;
    protected boolean isOnline;
    
    // 构造方法
    public SmartDevice(String deviceId, String brand) {
        this.deviceId = deviceId;
        this.brand = brand;
        this.isOnline = false;
    }
    
    // 具体方法:所有设备的共同行为
    public void connect() {
        isOnline = true;
        System.out.println(brand + " 设备 " + deviceId + " 已连接");
    }
    
    public void disconnect() {
        isOnline = false;
        System.out.println("设备 " + deviceId + " 已断开");
    }
    
    // 抽象方法:不同设备的特定行为
    public abstract void performMainFunction();
    public abstract String getDeviceType();
}

// 接口:定义设备的能力(can-do关系)
interface Controllable {
    void turnOn();
    void turnOff();
    boolean isOn();
}

interface Schedulable {
    void setSchedule(String time, String action);
    void executeScheduledTask();
}

interface VoiceControlled {
    void processVoiceCommand(String command);
    default void enableVoiceControl() {
        System.out.println("语音控制已启用");
    }
}

// 具体设备类:智能灯泡
public class SmartBulb extends SmartDevice 
                      implements Controllable, Schedulable, VoiceControlled {
    private boolean isLightOn = false;
    private int brightness = 50;
    private String scheduledAction;
    
    public SmartBulb(String deviceId, String brand) {
        super(deviceId, brand);
    }
    
    @Override
    public void performMainFunction() {
        System.out.println("智能灯泡提供照明功能");
    }
    
    @Override
    public String getDeviceType() {
        return "Smart Bulb";
    }
    
    // 实现 Controllable 接口
    @Override
    public void turnOn() {
        isLightOn = true;
        System.out.println("灯泡已开启,亮度:" + brightness + "%");
    }
    
    @Override
    public void turnOff() {
        isLightOn = false;
        System.out.println("灯泡已关闭");
    }
    
    @Override
    public boolean isOn() {
        return isLightOn;
    }
    
    // 实现 Schedulable 接口
    @Override
    public void setSchedule(String time, String action) {
        scheduledAction = action;
        System.out.println("已设置定时任务:" + time + " - " + action);
    }
    
    @Override
    public void executeScheduledTask() {
        if (scheduledAction != null) {
            System.out.println("执行定时任务:" + scheduledAction);
        }
    }
    
    // 实现 VoiceControlled 接口
    @Override
    public void processVoiceCommand(String command) {
        switch (command.toLowerCase()) {
            case "开灯":
                turnOn();
                break;
            case "关灯":
                turnOff();
                break;
            case "调亮":
                brightness = Math.min(100, brightness + 20);
                System.out.println("亮度调至:" + brightness + "%");
                break;
            default:
                System.out.println("无法识别的语音命令:" + command);
        }
    }
}

系统使用示例:多态的威力

看看如何通过多态和接口实现灵活的设备管理。


public class SmartHomeManager {
    private List<SmartDevice> devices = new ArrayList<>();
    
    public void addDevice(SmartDevice device) {
        devices.add(device);
        device.connect();
    }
    
    // 多态:统一管理所有设备
    public void showAllDevices() {
        for (SmartDevice device : devices) {
            System.out.println("设备类型:" + device.getDeviceType());
            device.performMainFunction();  // 多态调用
        }
    }
    
    // 接口:只控制可控制的设备
    public void turnOnAllControllableDevices() {
        for (SmartDevice device : devices) {
            if (device instanceof Controllable) {
                Controllable controllable = (Controllable) device;
                controllable.turnOn();
            }
        }
    }
    
    // 接口:为支持语音的设备执行语音命令
    public void executeVoiceCommand(String command) {
        for (SmartDevice device : devices) {
            if (device instanceof VoiceControlled) {
                VoiceControlled voiceDevice = (VoiceControlled) device;
                voiceDevice.processVoiceCommand(command);
            }
        }
    }
}

// 使用示例
public class SmartHomeDemo {
    public static void main(String[] args) {
        SmartHomeManager manager = new SmartHomeManager();
        
        // 添加设备
        SmartBulb bulb1 = new SmartBulb("BULB001", "海尔");
        SmartBulb bulb2 = new SmartBulb("BULB002", "美的");
        
        manager.addDevice(bulb1);
        manager.addDevice(bulb2);
        
        // 多态:统一管理
        manager.showAllDevices();
        
        // 接口:能力导向的操作
        manager.turnOnAllControllableDevices();
        manager.executeVoiceCommand("调亮");
        
        // 设置定时任务
        bulb1.setSchedule("22:00", "关灯");
        bulb1.executeScheduledTask();
    }
}

🎯 设计洞察

  • 抽象类 SmartDevice:定义了所有智能设备的共同本质和行为
  • 接口 Controllable/Schedulable/VoiceControlled:定义了设备的不同能力
  • 多态:允许统一管理不同类型的设备
  • 接口分离:每个接口专注于一种特定能力,符合单一职责原则
  • 灵活扩展:新设备只需继承抽象类并实现相应接口即可

枚举类

当一个变量的取值范围是有限且固定的集合时(如星期、季节、状态),使用枚举类是最佳选择。

枚举 (Enum)提供了类型安全,避免了使用字符串或整数常量可能导致的错误,并使代码更具可读性。


// 定义一个表示状态的枚举
public enum Status {
    RUNNING("跑步中"), 
    STUDYING("学习中"), 
    SLEEPING("睡觉中");

    private final String description;

    // 枚举的构造方法默认是 private
    Status(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }
}

// 使用枚举
Status currentStatus = Status.STUDYING;
System.out.println("当前状态: " + currentStatus.getDescription()); // 输出: 当前状态: 学习中

枚举的本质是继承自 java.lang.Enum 的特殊类,功能远比简单的常量强大。

🎯 互动练习:检验你的理解

通过这些练习题来检验你对面向对象三大特性的掌握程度。

📝 练习1:设计选择题

场景:你需要设计一个图形绘制系统,包含圆形、矩形、三角形等图形。每种图形都能计算面积、周长,但计算方式不同。部分图形还具有旋转、缩放等特殊能力。

问题:你会如何设计这个系统?

A. 创建一个 Shape 接口,所有图形实现这个接口
B. 创建一个 Shape 抽象类,所有图形继承这个类
C. 创建 Shape 抽象类 + Rotatable、Scalable 接口
D. 直接创建具体的图形类,不需要抽象
💡 推荐答案:C
理由:Shape 抽象类定义图形的共同本质(都有面积、周长),Rotatable、Scalable 接口定义特殊能力,体现了"is-a"和"can-do"的设计理念。

🔧 练习2:代码实现

任务:基于上面的设计,实现一个简单的 Circle 类

要求:

  • 继承 Shape 抽象类
  • 实现 Rotatable 接口(圆形旋转不改变外观)
  • 正确实现面积和周长计算
  • 提供合适的构造方法

// 你的代码框架
public abstract class Shape {
    protected String color;
    
    public Shape(String color) {
        this.color = color;
    }
    
    public abstract double getArea();
    public abstract double getPerimeter();
    
    public void display() {
        System.out.println(color + "的图形");
    }
}

interface Rotatable {
    void rotate(double angle);
}

// 请完成 Circle 类的实现
public class Circle extends Shape implements Rotatable {
    // 你的实现...
}

🤔 练习3:多态理解

代码分析:下面代码的输出是什么?为什么?


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

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("汪汪汪");
    }
    
    public void wagTail() {
        System.out.println("摇尾巴");
    }
}

public class Test {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.makeSound();        // 输出什么?
        // animal.wagTail();       // 这行代码会怎样?
    }
}
💡 答案:
1. animal.makeSound() 输出 "汪汪汪"(动态绑定,调用子类重写的方法)
2. animal.wagTail() 编译错误(父类引用无法访问子类特有方法)

⚠️ 常见错误和陷阱

学习过程中容易犯的错误,提前了解可以避免很多坑。

🚫 陷阱1:封装破坏

❌ 错误示例


public class Student {
    public String name;           // 直接暴露字段
    public List<String> courses;   // 暴露可变集合
    
    public List<String> getCourses() {
        return courses;           // 返回内部集合的直接引用
    }
}

// 使用时可能出现的问题
Student student = new Student();
student.name = "";               // 可以设置无效值
student.getCourses().clear();    // 外部可以修改内部状态

✅ 正确做法


public class Student {
    private String name;
    private List<String> courses = new ArrayList<>();
    
    public void setName(String name) {
        if (name != null && !name.trim().isEmpty()) {
            this.name = name;
        } else {
            throw new IllegalArgumentException("姓名不能为空");
        }
    }
    
    public String getName() {
        return name;
    }
    
    public List<String> getCourses() {
        return new ArrayList<>(courses);  // 返回副本,保护内部状态
    }
    
    public void addCourse(String course) {
        if (course != null && !courses.contains(course)) {
            courses.add(course);
        }
    }
}

🚫 陷阱2:继承滥用

❌ 错误示例:为了复用代码而强行继承


// 错误:汽车不是一种房子!
public class Car extends House {
    // 只是为了复用 House 中的 address 字段
    private String address;  // 停车位置
    
    public void drive() {
        System.out.println("汽车在行驶");
    }
}

✅ 正确做法:使用组合而非继承


public class Location {
    private String address;
    private double latitude;
    private double longitude;
    
    // 构造方法和getter/setter...
}

public class Car {
    private Location currentLocation;  // 组合:汽车"有一个"位置
    private String brand;
    
    public void drive() {
        System.out.println("汽车在行驶");
    }
    
    public void parkAt(Location location) {
        this.currentLocation = location;
    }
}

public class House {
    private Location location;  // 组合:房子"有一个"位置
    private int rooms;
    
    // 房子的相关方法...
}

🚫 陷阱3:多态误用

❌ 常见错误


Animal animal = new Dog();
Dog dog = animal;              // 编译错误:不能直接向下转型
dog.wagTail();

// 或者
if (animal instanceof Dog) {
    Dog dog = (Dog) animal;
    dog.wagTail();             // 虽然可以,但破坏了多态的意义
}

✅ 正确做法:通过接口或抽象方法实现


// 定义能力接口
interface TailWagger {
    void wagTail();
}

class Dog extends Animal implements TailWagger {
    @Override
    public void wagTail() {
        System.out.println("摇尾巴");
    }
}

// 使用时
Animal animal = new Dog();
if (animal instanceof TailWagger) {
    TailWagger wagger = (TailWagger) animal;
    wagger.wagTail();          // 基于能力的调用,更符合OOP原则
}

💡 最佳实践总结

  • 封装:永远使用private字段 + public方法,验证输入,保护内部状态
  • 继承:确保真正的"is-a"关系,优先考虑组合而非继承
  • 多态:通过接口和抽象类设计,避免强制类型转换
  • 接口:保持接口小而专注,遵循单一职责原则
  • 抽象类:提供合理的默认实现,为子类留出扩展空间

总结:封装、继承与多态

今天我们学习了面向对象编程的三大核心特性,它们是构建高质量Java应用的基石。

  • 封装是基础,它保护数据,隐藏细节,提供了清晰的接口。
  • 继承是扩展,它复用代码,建立了类之间的层次关系。
  • 多态是灵魂,它提升了灵活性和可扩展性,让程序“活”了起来。

同时,我们也了解了抽象类、接口和枚举等重要概念,它们都是实现这些特性的重要工具。

请务必多加练习,在实践中去体会这些思想的精髓。编程之路,知易行难,唯有实践出真知!

1 / X