封装、继承与多态
面向对象程序设计的三大基石
深入理解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中不同访问修饰符的权限控制效果:
访问权限说明:
测试结果:
为什么需要模块:当 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" 的关系。例如,“学生”是一个“人”,“工人”也是一个“人”。学生和工人都拥有人的共性(如姓名、年龄),但又各自拥有独特的行为。
子类继承父类的“遗产”(非私有成员),并可以添加自己的新功能,实现“青出于蓝而胜于蓝”。
继承的语法: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 的变量但未显式指定时,查找顺序为:
- 先看该方法中是否有名为
x的 局部变量 - 再看当前类中是否包含名为
x的 成员变量 - 逐层向上寻找直接父类中的 可见成员变量
- 直到
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 的特有方法
}
多态的精髓:编译时类型决定了能访问的成员范围;运行时类型决定了方法调用的实际实现(动态绑定)。
实现多态的三个条件
- 继承关系:必须有父类和子类的继承关系
- 方法重写:子类必须重写父类的方法
- 向上转型:父类引用指向子类对象
多态的核心价值
- 🔧 降低耦合:调用者不需要知道具体的实现类,只需要知道接口
- 📈 提高扩展性:新增子类无需修改现有代码
- 🎯 统一处理:可以用统一的方式处理不同类型的对象
- 🔄 代码复用:同一套代码可以处理多种类型
多态与动态绑定
编译器明明只知道 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:设计选择题
场景:你需要设计一个图形绘制系统,包含圆形、矩形、三角形等图形。每种图形都能计算面积、周长,但计算方式不同。部分图形还具有旋转、缩放等特殊能力。
问题:你会如何设计这个系统?
理由: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应用的基石。
- 封装是基础,它保护数据,隐藏细节,提供了清晰的接口。
- 继承是扩展,它复用代码,建立了类之间的层次关系。
- 多态是灵魂,它提升了灵活性和可扩展性,让程序“活”了起来。
同时,我们也了解了抽象类、接口和枚举等重要概念,它们都是实现这些特性的重要工具。
请务必多加练习,在实践中去体会这些思想的精髓。编程之路,知易行难,唯有实践出真知!