1. 抽象的概念
简而言之:抽象是把复杂问题简单化的艺术。
在编程中,抽象是从一组对象中提取共同的特征与行为的过程,同时忽略具体细节(例如某些东西“底层”究竟如何运作)。想象你在画城市地图:图上标出道路、建筑、河流——但不会标出每扇窗帘的颜色。地图就是城市的抽象。
在 OOP 中,抽象指创建只反映对当前任务重要的属性与行为的类和接口,同时隐藏不必要的细节。
生活中的例子
- 交通工具是一种抽象。无论是公交车、自行车还是宇宙飞船——共同点在于它们都能移动,并且有乘客和驾驶者。
- 动物同样是一种抽象。所有动物都会呼吸、进食、移动——但它们具体如何实现,取决于具体物种。
- 支付是银行软件中的一种抽象。你并不总是关心它是刷卡、通过 PayPal,还是用比特币——关键是它能够被执行并得到结果。
为什么抽象很重要?
- 减少为当前任务无关的细节所分心。
- 设计更易扩展、易维护的系统。
- 通过统一接口与对象交互,而无需关心具体实现。
2. Java 中的抽象
在 Java 中,抽象主要通过两种工具实现:
- 抽象类(abstract class)
- 接口(interface)
本讲我们先聚焦抽象类。(很快就会讲到接口!)
抽象类是不能直接实例化的类。它为其他类提供共同的基础(模板)。在抽象类中既可以有已实现的方法(带方法体),也可以有抽象方法(无方法体)——后者必须由子类实现。
抽象方法是没有实现(即没有方法体)的方法。它表明:“该类的所有子类都必须以各自方式实现此方法”。
示例:抽象“图形”
public abstract class Shape {
public abstract void draw(); // 抽象方法——没有方法体!
}
这里我们在表达:“所有图形都能被绘制,但具体怎么绘制——由各个子类自己决定”。
为什么并不总需要了解实现细节?
使用抽象时,你是通过对象的“外在接口”——它必须提供的一组方法——来与之交互。至于这些方法内部如何工作,并不重要。
例如,你调用对象的 payment.process() 来处理一次支付。它如何实现并不重要——只要能工作即可。这带来:
- 可以在不改动调用方代码的情况下切换实现。
- 简化测试(可以用“桩/假对象”替换实现)。
- 让代码更灵活、更能适应变化。
3. 抽象的优势
简化设计与维护
抽象让你不必为不相干的细节分心。驾驶汽车不需要知道发动机的内部结构——有方向盘、踏板和“向前行驶”的说明就够了。代码亦然:当你面向抽象类或接口编程时,你只关心所需的部分。
易于扩展
当系统建立在抽象之上时,添加新类型会很容易。例如,若你已有抽象类 Shape,就可以添加一个新的图形类型——Triangle,而无需修改旧代码。
降低组件耦合
如果系统各部分仅通过抽象交互,它们就能彼此独立地演进。这就像插头与插座:只要标准一致,任何设备都能接入。
4. 实用示例
我们来看看抽象在实践中的样子。我们将使用可以嵌入你的学习应用的示例,便于你亲手尝试。
示例 1:类“Shape”(图形)
public abstract class Shape {
public abstract void draw();
}
现在创建几个具体的图形:
public class Circle extends Shape {
@Override
public void draw() {
System.out.println("绘制圆");
}
}
public class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("绘制矩形");
}
}
使用抽象:
Shape s1 = new Circle();
Shape s2 = new Rectangle();
s1.draw(); // 绘制圆
s2.draw(); // 绘制矩形
这里我们以 Shape 类型的变量进行操作——至于内部究竟是哪种图形并不重要。这就是抽象的力量!
示例 2:类“Payment”
public abstract class Payment {
public abstract void process();
}
具体实现:
public class CreditCardPayment extends Payment {
@Override
public void process() {
System.out.println("处理信用卡支付");
}
}
public class PaypalPayment extends Payment {
@Override
public void process() {
System.out.println("通过 PayPal 处理支付");
}
}
使用:
Payment[] payments = {
new CreditCardPayment(),
new PaypalPayment()
};
for (Payment p : payments) {
p.process();
}
结果是,每笔支付都会以各自的方式被处理,而调用它们的代码无需为此操心。
5. 抽象与实现细节:如何不被绕晕
抽象关注“做什么”,而不是“怎么做”
设计抽象时,你回答的是:“对象应该能做什么?”
而实现细节回答的是:“它是如何做到的?”。
例如:
- 抽象:“任何图形都应该能被绘制(draw())。”
- 实现细节:“圆用圆弧绘制,矩形用四条线绘制。”
生活类比
想想电视遥控器。你不关心它如何传输信号——关键是有“开机”、“换台”和“调大音量”按钮。这就是抽象——你可以操作的一组按钮。至于实现细节,则是遥控器工程师需要考虑的。
6. 如何在程序中提炼抽象
步骤 1:找出共性
审视你领域内的对象,它们有哪些共同点?比如所有交通工具都会行驶,所有动物都会进食,所有支付都可以被执行.
步骤 2:定义抽象类
创建一个只包含所有对象共同部分的抽象类。
public abstract class Transport {
public abstract void move();
}
步骤 3:在子类中实现细节
public class Car extends Transport {
@Override
public void move() {
System.out.println("汽车在公路上行驶");
}
}
public class Bicycle extends Transport {
@Override
public void move() {
System.out.println("自行车靠踩踏前进");
}
}
步骤 4:在代码中使用抽象
Transport[] transports = {
new Car(),
new Bicycle()
};
for (Transport t : transports) {
t.move();
}
你的代码面向抽象编程——而不依赖具体实现细节。
7. 抽象与细化的平衡
新手的典型错误——要么把抽象做得过于具体,要么反过来过于宽泛。
- 如果抽象过于宽泛(例如只有一个 “doSomething” 方法的 “Object” 类),它几乎没有任何价值。
- 如果抽象过于具体(例如“带红色描边、半径为 5 的圆”),就失去抽象的意义——不如直接写具体类。
金科玉律:抽象只应涵盖对你的任务而言真正通用且重要的部分。
8. 使用抽象时的常见错误
错误 1:尝试实例化抽象类。
抽象类并不用于直接创建对象。如果你写 Shape s = new Shape();,编译器会报错:Cannot instantiate the type Shape。这就像去商店买一个“交通工具”——行不通;你必须选择具体的自行车、汽车或公交车。
错误 2:在子类中忘记实现抽象方法。
如果一个类继承了抽象类却没有实现其所有抽象方法,那么它本身也会变为抽象类——将无法被实例化。请确保所有必需的方法都已实现。
错误 3:混淆抽象与实现细节。
如果你在抽象类中加入只属于某个子类的细节,那通常意味着抽象设计不佳。比如在抽象类 Payment 中加入字段 cardNumber,而并非所有支付都使用银行卡。
错误 4:过度抽象。
不要为抽象而抽象。如果系统中只有一种对象类型,且不会被扩展,引入抽象只会让代码更复杂。
GO TO FULL VERSION