- 封装(Encapsulation)
为什么需要封装?
我们不妨想想一下我们去银行存钱,
❌ 没有封装的世界:
你直接走进金库,自己往里面放钱、拿钱
没人监督 → 可能放少拿多 → 乱套了
✅ 有封装的世界:
你把钱交给柜台,柜员帮你操作金库
柜员会检查金额是否合法、记录流水
你不能直接碰金库 → 安全可控
所以封装就是把数据"锁"起来,只留一个受控的入口和出口。
没有封装的问题例如:
public class Person {
String name;
int age;
}
// 使用
Person p = new Person();
p.name = "张三";
p.age = -200; // ❌ 年龄怎么可能是-200?但编译器不报错!
这就导致了外部代码可以直接访问成员变量,想赋什么值就赋什么值,没有任何限制。
封装的实现:private + getter/setter
public class Person {
// 1. 成员变量用 private 修饰 → 外面不能直接访问
private String name;
private int age;
// 2. 提供 public 的 getter(获取)和 setter(修改)
// getter:获取姓名
public String getName() {
return name;
}
// setter:设置姓名(带校验)
public void setName(String name) {
if (name == null || name.trim().length() == 0) {
System.out.println("姓名不能为空!");
return;
}
this.name = name;
}
// getter:获取年龄
public int getAge() {
return age;
}
// setter:设置年龄(带校验)
public void setAge(int age) {
if (age < 0 || age > 150) {
System.out.println("年龄必须在0-150之间!");
return;
}
this.age = age;
}
}
// 使用
public class TestPerson {
public static void main(String\[\] args) {
Person p = new Person();
p.setName("张三");
p.setAge(20);
System.out.println(p.getName() + "," + p.getAge() + "岁");
// 输出:张三,20岁
p.setAge(-200); // 年龄必须在0-150之间!
System.out.println("年龄仍然是:" + p.getAge()); // 20
}
}
所以封装有三大好处:
| 好处 | 说明 |
|---|---|
| 数据安全 | 外部不能直接改数据,必须通过 setter,setter 里可以加校验 |
| 隐藏实现 | 外部只需要知道"怎么用",不需要知道"内部怎么存" |
| 易于维护 | 想改内部逻辑,只需要改 setter/getter,外面代码不用动 |
封装的命名规范:
private 成员变量:直接写名字
public getter:get + 首字母大写的变量名(boolean 可以用 is)
public setter:set + 首字母大写的变量名
示例:
private String name; → getName() / setName(String name)
private int age; → getAge() / setAge(int age)
private boolean alive; → isAlive() / setAlive(boolean alive)
- 继承
为什么需要继承呢?
想象你在设计游戏角色:
// ❌ 没有继承:每个类重复写相同的代码
class Warrior {
String name;
int hp;
int level;
void move() { System.out.println("移动"); }
void attack() { System.out.println("攻击"); }
}
class Mage {
String name; // 重复
int hp; // 重复
int level; // 重复
void move() { System.out.println("移动"); } // 重复
void attack() { System.out.println("攻击"); } // 重复
void castSpell() { System.out.println("施法"); }
}
就会遇到name、hp、level、move()、attack() 在每个类里都写了一遍。
那有继承的写法呢
// 父类:提取公共部分
class Character {
String name;
int hp;
int level;
void move() { System.out.println(name + "移动"); }
void attack() { System.out.println(name + "攻击"); }
}
// 子类:只写自己特有的
class Warrior extends Character {
void useSword() { System.out.println(name + "挥剑"); }
}
class Mage extends Character {
void castSpell() { System.out.println(name + "施法"); }
}
继承的核心概念
| 概念 | 说明 |
|---|---|
| 父类(超类/Super Class) | 被继承的类,提供公共特征和行为 |
| 子类(派生类/Sub Class) | 继承父类的类,拥有父类所有非private成员 |
| extends 关键字 | 表示"继承"关系 |
| 继承关系 | is-a 关系:子类 "是一个" 父类 |
Character(角色) ← 父类
├── Warrior(战士) ← 子类,"战士是一个角色"
├── Mage(法师) ← 子类,"法师是一个角色"
└── Archer(弓箭手) ← 子类,"弓箭手是一个角色"
继承语法; 修饰符 class 子类名 extends 父类名 {
// 子类特有的成员
}
示例:
// 父类
public class Animal {
String name;
int age;
public void eat() {
System.out.println(name + "在吃东西");
}
public void sleep() {
System.out.println(name + "在睡觉");
}
}
// 子类
public class Dog extends Animal {
String breed; // 品种(子类特有)
public void wagTail() { // 子类特有方法
System.out.println(name + "摇尾巴");
}
}
// 测试
public class TestInheritance {
public static void main(String\[\] args) {
Dog dog = new Dog();
dog.name = "旺财"; // 继承自 Animal
dog.age = 3; // 继承自 Animal
dog.breed = "金毛"; // Dog 自己的
dog.eat(); // 调用继承来的方法 → 旺财在吃东西
dog.sleep(); // 调用继承来的方法 → 旺财在睡觉
dog.wagTail(); // 调用自己的方法 → 旺财摇尾巴
}
}
什么能被继承?什么不能?
| 成员 | 能否被继承 | 说明 |
|---|---|---|
| public 成员 | ✅ 能 | 完全继承 |
| protected 成员 | ✅ 能 | 后面学访问修饰符时详细讲 |
| default(包访问) | ⚠️ 同包能 | 不同包不能 |
| private 成员 | ❌ 不能 | 子类看不到 |
| 构造方法 | ❌ 不能 | 子类必须自己写,但可以调用父类构造 |
| static 成员 | ✅ 能(但不是继承,是共享) | 通过类名访问 |
- 编码练习
设计思路:
Animal(父类)
├── 属性:name, age
├── 方法:eat(), sleep(), makeSound()
│
├── Dog(子类)
│ ├── 特有属性:breed
│ ├── 重写 makeSound()
│ └── 特有方法:fetch()
│
└── Cat(子类)
├── 特有属性:color
├── 重写 makeSound()
└── 特有方法:climb()
完整代码:
// ========== 父类 Animal ==========
public class Animal {
// 成员变量
protected String name; // protected:子类可以直接访问
protected int age;
// 构造方法
public Animal() {
System.out.println("Animal 无参构造被调用");
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
System.out.println("Animal 有参构造被调用");
}
// 通用方法
public void eat() {
System.out.println(name + "在吃东西");
}
public void sleep() {
System.out.println(name + "在睡觉");
}
// 通用方法(设计为可被重写的)
public void makeSound() {
System.out.println(name + "发出声音");
}
// getter/setter
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
// ========== 子类 Dog ==========
public class Dog extends Animal {
private String breed; // 品种
// 构造方法
public Dog() {
super(); // 调用父类无参构造(可省略,编译器自动加)
System.out.println("Dog 无参构造被调用");
}
public Dog(String name, int age, String breed) {
super(name, age); // 调用父类有参构造
this.breed = breed;
System.out.println("Dog 有参构造被调用");
}
// 重写 makeSound()
@Override
public void makeSound() {
System.out.println(name + ":汪汪汪!");
}
// 子类特有方法
public void fetch() {
System.out.println(name + "在捡球");
}
public String getBreed() { return breed; }
public void setBreed(String breed) { this.breed = breed; }
}
// ========== 子类 Cat ==========
public class Cat extends Animal {
private String color; // 颜色
// 构造方法
public Cat() {
super(); // 调用父类无参构造
System.out.println("Cat 无参构造被调用");
}
public Cat(String name, int age, String color) {
super(name, age); // 调用父类有参构造
this.color = color;
System.out.println("Cat 有参构造被调用");
}
// 重写 makeSound()
@Override
public void makeSound() {
System.out.println(name + ":喵喵喵~");
}
// 子类特有方法
public void climb() {
System.out.println(name + "在爬树");
}
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
}
// ========== 测试类 ==========
public class TestAnimal {
public static void main(String\[\] args) {
System.out.println("===== 创建 Dog =====");
Dog dog = new Dog("旺财", 3, "金毛");
dog.eat(); // 继承来的
dog.sleep(); // 继承来的
dog.makeSound(); // 重写后的 → 旺财:汪汪汪!
dog.fetch(); // 自己的
System.out.println("品种:" + dog.getBreed());
System.out.println("\n===== 创建 Cat =====");
Cat cat = new Cat("咪咪", 2, "橘色");
cat.eat(); // 继承来的
cat.sleep(); // 继承来的
cat.makeSound(); // 重写后的 → 咪咪:喵喵喵~
cat.climb(); // 自己的
System.out.println("颜色:" + cat.getColor());
System.out.println("\n===== 多态预览 =====");
// 父类引用指向子类对象(后面多态章节会详细讲)
Animal a1 = new Dog("小黑", 1, "拉布拉多");
Animal a2 = new Cat("小花", 4, "黑白");
a1.makeSound(); // 小黑:汪汪汪!
a2.makeSound(); // 小花:喵喵喵~
}
}
- 方法重写(Override)
什么是方法重写?
子类对父类已有的方法重新实现,方法签名完全相同。
就像你继承了爸爸的"做饭"技能,但你有自己的做法:
爸爸的做饭 → 炒青菜
你的做饭 → 做红烧肉(同名,但内容不同)
重写规则(必须满足):
| 规则 | 说明 |
|---|---|
| 方法名必须相同 | 这是重写的前提 |
| 参数列表必须完全相同 | 类型、个数、顺序都一样 |
| 返回值类型必须兼容 | 相同,或是父类返回值的子类 |
| 访问修饰符不能更严格 | 父类 public → 子类不能 private |
| 不能重写 private 方法 | 子类看不到父类的 private 方法 |
| 不能重写 static 方法 | static 方法属于类,不参与重写 |
@Override 注解(强烈推荐):
@Override
public void makeSound() {
System.out.println("汪汪汪!");
}
@Override 告诉编译器:"我这个方法是要重写父类的"。如果写错了(比如方法名拼错),编译器会报错提醒你。
重写 vs 重载 ------ 对比速查
| 对比项 | 重写(Override) | 重载(Overload) |
|---|---|---|
| 发生位置 | 父子类之间 | 同一个类中 |
| 方法名 | 相同 | 相同 |
| 参数列表 | 必须完全相同 | 必须不同 |
| 返回值 | 必须兼容 | 可以不同 |
| 访问修饰符 | 不能更严格 | 随便 |
| 目的 | 子类改变/扩展父类行为 | 同名方法适应不同参数 |
- super修饰词
super 是什么?
super 代表"父类对象",用来访问父类的成员。
| 用法 | 语法 | 说明 |
|---|---|---|
| 调用父类构造方法 | super(参数) | 必须写在子类构造方法第一行 |
| 调用父类普通方法 | super.方法名() | 当子类重写了父类方法,想调用父类原版 |
| 访问父类成员变量 | super.成员变量 | 当子类有同名变量时(较少用) |
用法1:调用父类构造方法
public class Dog extends Animal {
private String breed;
public Dog(String name, int age, String breed) {
super(name, age); // 调用父类 Animal(String, int) 构造
this.breed = breed;
}
}
new Dog("旺财", 3, "金毛")
│
├─ 先执行 super(name, age) → 调用 Animal 的构造
│ └─ Animal 构造:this.name = "旺财"; this.age = 3;
│
└─ 再执行 Dog 构造剩余部分:this.breed = "金毛";
super(参数) 必须写在构造方法第一行。如果不写,编译器自动加 super()(调用父类无参构造)。如果父类没有无参构造,子类必须手动写 super(参数),否则编译报错。
用法2:调用父类被重写的方法
public class Dog extends Animal {
@Override
public void makeSound() {
super.makeSound(); // 先调用父类的 makeSound()
System.out.println(name + ":汪汪汪!");
}
}
调用 dog.makeSound()
→ 先执行 Animal 的 makeSound() → 输出 "旺财发出声音"
→ 再执行 Dog 自己的逻辑 → 输出 "旺财:汪汪汪!"
这在扩展父类行为时非常有用------不是完全替换,而是在父类基础上加东西。
用法3:访问父类成员变量(同名时)
public class Animal {
protected String name = "动物";
}
public class Dog extends Animal {
private String name = "狗"; // 和父类同名
public void printNames() {
System.out.println("子类的 name:" + this.name); // "狗"
System.out.println("父类的 name:" + super.name); // "动物"
}
}
- super 和 this 的区别(重点对比)
| 对比维度 | this | super |
|---|---|---|
| 代表什么 | 当前对象 | 父类对象 |
| 调用构造方法 | this(参数) → 调用本类其他构造 | super(参数) → 调用父类构造 |
| 调用成员方法 | this.方法() → 调用本类方法 | super.方法() → 调用父类方法 |
| 访问成员变量 | this.变量 → 本类变量 | super.变量 → 父类变量 |
| 能否同时用 | this() 和 super() 不能同时出现在构造方法中 | 同上 |
| 必须写在第一行 | this() 必须第一行 | super() 必须第一行 |
| 默认行为 | 不写时编译器自动加 this. | 不写时如果找不到本类成员,自动向上找父类 |
图解:this 和 super 在内存中的关系
栈内存 堆内存
───── ─────
dog ──→ 0x100 ──→ Dog 对象 {
Animal部分 ← super 指向这里
name = "旺财"
age = 3
Dog部分 ← this 指向这里
breed = "金毛"
}
this 指向整个对象,super 指向对象中"属于父类的那部分"。
- 综合实战:员工管理系统
// ========== 父类:Employee ==========
public class Employee {
protected String id;
protected String name;
protected double salary;
public Employee() {}
public Employee(String id, String name, double salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
// 通用方法
public void work() {
System.out.println(name + "在工作");
}
public void showInfo() {
System.out.println("工号:" + id + ",姓名:" + name + ",月薪:" + salary);
}
// 计算年薪(通用逻辑)
public double calculateYearlySalary() {
return salary * 12;
}
}
// ========== 子类:Manager ==========
public class Manager extends Employee {
private double bonus; // 奖金(经理特有)
public Manager() {}
public Manager(String id, String name, double salary, double bonus) {
super(id, name, salary); // 调用父类构造
this.bonus = bonus;
}
// 重写工作方法
@Override
public void work() {
System.out.println(name + "在管理团队");
}
// 重写年薪计算(加奖金)
@Override
public double calculateYearlySalary() {
return super.calculateYearlySalary() + bonus; // 父类逻辑 + 奖金
}
// 经理特有方法
public void holdMeeting() {
System.out.println(name + "在开会");
}
public double getBonus() { return bonus; }
public void setBonus(double bonus) { this.bonus = bonus; }
}
// ========== 子类:Developer ==========
public class Developer extends Employee {
private String programmingLanguage; // 编程语言
public Developer() {}
public Developer(String id, String name, double salary, String language) {
super(id, name, salary);
this.programmingLanguage = language;
}
// 重写工作方法
@Override
public void work() {
System.out.println(name + "在写" + programmingLanguage + "代码");
}
// 开发者特有方法
public void debug() {
System.out.println(name + "在调试代码");
}
public String getProgrammingLanguage() { return programmingLanguage; }
public void setProgrammingLanguage(String lang) { this.programmingLanguage = lang; }
}
// ========== 测试 ==========
public class TestEmployee {
public static void main(String\[\] args) {
Manager mgr = new Manager("M001", "王经理", 20000, 50000);
Developer dev = new Developer("D001", "李开发", 15000, "Java");
mgr.work(); // 王经理在管理团队
mgr.holdMeeting();
mgr.showInfo();
System.out.println("经理年薪:" + mgr.calculateYearlySalary());
System.out.println();
dev.work(); // 李开发在写Java代码
dev.debug();
dev.showInfo();
System.out.println("开发者年薪:" + dev.calculateYearlySalary());
}
}
- 每日练习
练习1(基础):定义 Vehicle 父类
属性:brand(品牌)、speed(速度)
方法:run() 打印行驶信息
子类 Car 和 Bike 继承 Vehicle
Car 重写 run() 输出"汽车在公路上行驶"
Bike 重写 run() 输出"自行车在自行车道上行驶"
练习2(进阶):定义 Shape 父类
方法:getArea() 和 getPerimeter()(返回0,作为默认实现)
子类 Rectangle 和 Circle 继承 Shape
各自重写 getArea() 和 getPerimeter()
在子类构造方法中使用 super 调用父类构造
练习3(挑战):定义 Account 银行账号体系
父类 Account:账号、余额、存款、取款
子类 SavingAccount(储蓄账户):有利息率,取款有限制
子类 CreditAccount(信用账户):有信用额度,可以透支
在子类中重写 withdraw() 方法
使用 super 调用父类的 withdraw() 复用逻辑