1. 封装
概念
面向对象的封装把对象的属性和方法组合在一起,统一提供对外的访问权限,封装就是将对象的使用者和设计者分开,设计者可以设计出外部可以操作 的内容和只能内部操作的内容。使用者只能使用设计好的内容,却看不见设计者是如何实现的。就像计算机是由 cpu、内存和各种外设组成的,对于计算机的用户来说,只能使用这些部件,但是却看不见这些部件的内部结构。
简单来说封装就像是把东西打包在一个盒子里,只留下几个特定的 "开口" 让外部使用。在 Java 中,我们通过将类的属性设为private
(私有),并提供public
(公共)的方法来访问和修改这些属性。这样可以:
-
保护数据:防止外部直接修改属性,避免非法数据。
-
简化使用:外部只需调用方法,无需关心内部实现。
为什么需要封装?
想象一个银行账户类,如果直接暴露balance
(余额)属性,任何人都可以随意修改,这会导致安全问题。通过封装,我们可以控制如何修改余额(例如,取款时检查余额是否充足)。
java
public class BankAccount {
private double balance; // 私有属性,外部无法直接访问
// 构造方法:初始化账户余额
public BankAccount(double initialBalance) {
if (initialBalance > 0) {
this.balance = initialBalance;
} else {
System.out.println("初始余额必须大于0");
this.balance = 0;
}
}
// 存款方法
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
System.out.println("存款成功,当前余额: " + balance);
} else {
System.out.println("存款金额必须大于0");
}
}
// 取款方法
public boolean withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
System.out.println("取款成功,当前余额: " + balance);
return true;
} else {
System.out.println("取款失败:余额不足或金额无效");
return false;
}
}
// 获取余额(只读访问)
public double getBalance() {
return balance;
}
}
在该代码中我们无法直接对balance
(余额)属性进行修改,而是通过下列方法去对balance
(余额)属性进行修改
java
BankAccount account = new BankAccount(1000.0);
account.deposit(500.0); // 存款500,余额变为1500
account.withdraw(200.0); // 取款200,余额变为1300
account.withdraw(2000.0); // 取款失败:余额不足
System.out.println("当前余额: " + account.getBalance()); // 输出1300
关键点:
- private 属性 :
balance
只能通过类内部的方法访问。 - public 方法 :
deposit()
、withdraw()
和getBalance()
是外部与内部数据交互的唯一途径。
修饰符的权限表
在 Java 中,访问修饰符用于控制类、方法、变量的访问权限。以下是 Java 中四种访问修饰符的权限表:
修饰符 | 同一个类 | 同一个包 | 不同包子类 | 不同包非子类 |
---|---|---|---|---|
public |
✅ | ✅ | ✅ | ✅ |
protected |
✅ | ✅ | ✅ | ❌ |
default |
✅ | ✅ | ❌ | ❌ |
private |
✅ | ❌ | ❌ | ❌ |
说明:
public
:所有类都可以访问。protected
:同一个包内的类和不同包中的子类可以访问。default
(默认,无修饰符):只有同一个包内的类可以访问。private
:只有同一个类内部可以访问。
2. 继承
概念 :
继承就像是子女继承父母的特征一样,一个类(子类)可以继承另一个类(父类)的属性和方法(一个子类只能直接继承一个父类)。通过继承,子类可以:
- 复用代码:避免重复编写父类已有的功能。
- 扩展功能:在父类基础上添加新的属性或方法。
- 建立层次关系:形成 "is-a" 关系(例如,狗是一种动物)。
为什么需要继承?
假设你正在开发一个游戏,有多种角色(战士、法师、弓箭手),他们都有一些共同的属性(生命值、名字)和方法(移动、攻击)。通过创建一个父类Character
,可以避免在每个子类中重复编写这些共性代码。
实例:
java
// 父类:角色
class Character {
protected String name; // 受保护的属性,子类可以直接访问
protected int health; // 受保护的属性
public Character(String name, int health) {
this.name = name;
this.health = health;
}
public void move() {
System.out.println(name + "正在移动...");
}
public void attack(Character target) {
System.out.println(name + "攻击了" + target.name);
target.health -= 10; // 每次攻击减少10点生命值
System.out.println(target.name + "剩余生命值: " + target.health);
}
public String getName() {
return name;
}
public int getHealth() {
return health;
}
}
// 子类:战士
class Warrior extends Character {
private int strength; // 战士特有的属性:力量
public Warrior(String name, int strength) {
super(name, 100); // 调用父类的构造方法,初始生命值为100
this.strength = strength;
}
// 战士特有的方法:挥舞宝剑
public void swingSword() {
System.out.println(name + "挥舞宝剑,造成" + (strength * 2) + "点伤害!");
}
// 重写父类的攻击方法
@Override
public void attack(Character target) {
System.out.println(name + "用宝剑砍向" + target.name);
target.health -= strength * 2; // 战士的伤害是力量的2倍
System.out.println(target.name + "剩余生命值: " + target.health);
}
}
// 子类:法师
class Mage extends Character {
private int mana; // 法师特有的属性:魔法值
public Mage(String name, int mana) {
super(name, 80); // 调用父类的构造方法,初始生命值为80
this.mana = mana;
}
// 法师特有的方法:施放魔法
public void castSpell() {
if (mana >= 20) {
System.out.println(name + "施放火球术!");
mana -= 20;
} else {
System.out.println(name + "魔法值不足!");
}
}
}
如何使用:
java
Warrior warrior = new Warrior("张三", 15);
Mage mage = new Mage("李四", 100);
warrior.move(); // 输出:张三正在移动...
warrior.swingSword(); // 输出:张三挥舞宝剑,造成30点伤害!
warrior.attack(mage); // 输出:张三用宝剑砍向李四,李四剩余生命值: 50
mage.castSpell(); // 输出:李四射放火球术!
mage.attack(warrior); // 输出:李四攻击了张三,张三剩余生命值: 90
关键点:
- extends 关键字 :
Warrior extends Character
表示Warrior
继承自Character
。 - super 关键字:在子类构造方法中调用父类的构造方法。
- 方法重写 (Override):子类可以修改父类方法的实现(如
Warrior
的attack()
方法)。 - protected 访问修饰符 :父类的
protected
属性可以被子类直接访问。
3. 多态
概念 :
多态意味着 "多种形态",即同一个方法调用可以表现出不同的行为。多态通过两个机制实现:
- 方法重写(Override):子类重写父类的方法。
- 父类引用指向子类对象:可以通过父类类型的变量调用子类的重写方法。
为什么需要多态?
假设你正在开发一个图形绘制程序,有圆形、矩形、三角形等多种图形。通过多态,你可以用一个通用的 "绘制" 方法处理所有图形,而不需要为每个图形单独编写处理代码。
实例:
java
// 父类:图形
abstract class Shape {
// 抽象方法:计算面积(没有方法体,子类必须实现)
public abstract double calculateArea();
// 具体方法:显示图形信息
public void displayInfo() {
System.out.println("这个图形的面积是: " + calculateArea());
}
}
// 子类:圆形
class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
// 子类:矩形
class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
}
如何使用:
java
// 创建不同的图形对象
Shape circle = new Circle(5.0);
Shape rectangle = new Rectangle(4.0, 6.0);
// 调用相同的方法,但表现不同的行为
circle.displayInfo(); // 输出:这个图形的面积是: 78.53981633974483
rectangle.displayInfo(); // 输出:这个图形的面积是: 24.0
// 可以将多个图形放入数组中统一处理
Shape[] shapes = {circle, rectangle};
for (Shape shape : shapes) {
System.out.println("面积: " + shape.calculateArea());
}
关键点:
- 抽象类 (
abstract class
):不能实例化,用于定义通用接口(如Shape
)。 - 抽象方法 (
abstract method
):没有方法体,子类必须实现。 - 父类引用 :
Shape circle = new Circle(5.0)
,通过父类类型调用子类方法。 - 动态绑定:运行时根据实际对象类型决定调用哪个方法(而不是编译时的引用类型)。
三大特征的联系
- 封装为继承和多态提供基础:通过封装数据和方法,子类可以安全地继承和扩展父类的功能。
- 继承是多态的前提:只有通过继承建立类的层次关系,才能实现父类引用指向子类对象。
- 多态是封装和继承的高级应用:通过统一接口处理不同对象,提高代码的灵活性和可维护性。