一、怎么理解”面向对象“和”面向过程“?
我们常说的面向对象和面向过程其实是两种不同的编程范式,在实现相同的功能时分别以对象和过程为中心。具体来说,面向对象编程(OOP)以类(Class)和对象(Object)作为基本构造块,通过封装(Encapsulation)、继承(Inheritance)、多态(Polymorphism)等特性来组织代码,强调将数据和相关操作封装在一起,通过对象间的交互来实现功能。而面向过程编程(POP)则是以函数或过程调用为主线,通过一系列顺序执行的步骤来达成目标,它更关注于算法和步骤的分解,代码组织往往以功能为导向,通过函数间的调用关系来实现功能。
1.面向过程
面向过程编程(Procedural Oriented Programming,POP),C语言应该是很多朋友入门编程的第一语言,它就是一种典型的“面向过程”的编程语言。举个大家都写过的例子:计算圆的面积和周长。
#include <stdio.h>
// 定义计算圆面积的函数
double calculate_circle_area(double radius) {
return 3.14159265359 * radius * radius;
}
// 定义计算圆周长的函数
double calculate_circle_circumference(double radius) {
return 2 * 3.14159265359 * radius;
}
int main() {
double radius = 5.0;
double area, circumference;
// 调用函数计算面积和周长
area = calculate_circle_area(radius);
circumference = calculate_circle_circumference(radius);
// 打印结果
printf("半径为%lf的圆的面积为%lf\n", radius, area);
printf("半径为%lf的圆的周长为%lf\n", radius, circumference);
return 0;
}
如果是用C语言,你第一反应是不是这样写的?
计算圆的面积和周长?写两个计算圆面积周长的函数,在主函数里调用呗,这还不简单。欸,这就是面向过程的编程了:当试图实现一个目标时,我们的思维首先聚焦于整个系统需要执行哪些“步骤”或“动作”。接着,我们将这些复杂的动作逐步拆解为若干个简单且易于管理的小动作。每个小动作都被实现为一个独立的函数或过程。最终,在主函数(或程序的入口点)中,我们通过调用这些已经定义好的小动作(即函数),按照特定的顺序组合起来,从而完成整体的大动作或实现我们的目标。
面向过程:拆解动作 -> 独立实现 -> 顺序组合
2.面向对象
而Python、Java则代表了典型的面向对象编程(Object Oriented Programming, OOP),同样是求圆的周长和面积,你还会写两个方法函数调用吗?
// 定义圆类
public class Circle {
// 成员变量:半径
private double radius;
// 构造方法:初始化半径
public Circle(double radius) {
this.radius = radius;
}
// 成员方法:计算面积
public double calculateArea() {
return Math.PI * radius * radius;
}
// 成员方法:计算周长
public double calculateCircumference() {
return 2 * Math.PI * radius;
}
// Getter方法:获取半径
public double getRadius() {
return radius;
}
// Setter方法:设置半径
public void setRadius(double radius) {
this.radius = radius;
}
// 主方法:测试Circle类
public static void main(String[] args) {
// 创建一个Circle对象
Circle circle = new Circle(5.0);
// 使用对象方法计算面积和周长
double area = circle.calculateArea();
double circumference = circle.calculateCircumference();
// 打印结果
System.out.println("半径为" + circle.getRadius() + "的圆的面积为" + area);
System.out.println("半径为" + circle.getRadius() + "的圆的周长为" + circumference);
}
}
用Java实现同样的功能时,我们的眼光不再像面向过程编程那样仅仅聚焦于拆解小动作,而是更多地关注于识别系统中的各个对象以及它们之间的交互和职责。
在上面这个例子中,我们想要实现的目标是计算圆的面积和周长,于是这个系统中只有一个对象,圆:因此我们定义了一个Circle类;想要算得圆的面积和周长就得知道圆的半径,于是这个类中一定要包含一个成员变量radius;圆这个对象要做什么?它要能求得自己的周长和面积,这两个动作只跟他自己有关系,因此我们还定义了两个成员方法calculateArea()和calculateCircumference();
最后在main方法中,我们创建了一个Circle对象,并通过调用对象的方法来计算面积和周长。
面向对象:识别对象 -> 抽象为类 ->( 封装行为 ->) 对象交互
二、“面向对象”编程的三大特征
在上面的例子中,可以看到我对面向对象的总结中多了一个看似可有可无的环节:封装行为,然而事实上,“封装”正是面向对象编程的三大特征之一,与“继承”、“多态”一起,可以说是面向对象编程的核心,最最最重要了。
1.封装(Encapsulation)
封装是面向对象编程中用于隐藏对象内部细节的过程,确保对象的状态只能通过定义良好的接口进行访问和修改。它强调将对象的属性和行为(即数据和方法)封装在一起,形成一个独立的单元。
比如说,你是一个程序员,为客户实现了功能后,如果直接呈现整个不加修饰的完整代码,客户很可能会感到困惑,因为他们可能并不熟悉编程语言和代码逻辑。此外,如果客户不小心操作不当或改错了代码,还可能会导致整个系统崩溃或功能失效。——这个时候就轮到我们的封装出场了。
通过封装,程序员可以为客户提供一个清晰、简洁且易于使用的接口。这个接口只包含客户需要知道和使用的功能,而不涉及任何内部实现细节。这样,客户就可以通过接口来与系统进行交互,而无需担心会破坏系统的内部状态或逻辑。
这样不仅可以保护对象的状态不被意外修改,还可以减少代码的复杂性,使得外部用户能够更容易地使用和理解对象。
假设我们要创建一个表示“银行账户”的类。为了封装,我们会将账户余额(一个敏感数据)和与之相关的操作(如存款和取款)封装在类内部。外部用户只能通过类提供的方法来访问和修改账户余额,而不能直接访问余额变量。
public class BankAccount {
// 私有属性,表示账户余额
private double balance;
// 私有属性,表示账户密码
private String password;
// 公共方法,取款
public void withdraw(double amount, String inputPassword) {
if (validatePassword(inputPassword)) {
if (amount > 0 && amount <= balance) {
balance -= amount;
System.out.println("取款成功!当前余额:" + balance);
} else if (amount > balance) {
System.out.println("余额不足!");
} else {
System.out.println("取款金额必须大于0!");
}
} else {
System.out.println("密码错误!取款操作被拒绝。");
}
}
// 私有方法,用于验证密码
private boolean validatePassword(String inputPassword) {
return this.password.equals(inputPassword);
}
}
在这个例子中,BankAccount类有一个私有属性balance,表示账户余额。外部用户无法直接访问或修改这个属性,因为它是私有(private)的。用户只能通过类提供的方法如withdraw来与账户余额进行交互,而这些交互可以设定必要的验证逻辑(如上例中的取款操作需要先验证用户输入密码正确且余额足够),对代码的安全非常重要。
因此,封装不仅有助于提高代码的可维护性和可扩展性,还有助于简化用户与系统的交互过程,使得用户能够更容易地使用和理解系统。可以说,封装是提升代码质量的关键。
2.继承(Inheritance)
继承允许一个新的类(称为子类或派生类)基于一个已存在的类(称为父类或基类)进行定义。子类可以继承父类的属性和方法,从而避免代码重复,提高代码的可重用性,并有助于建立一个类的层次结构。
public class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Animal();
Animal myDog = new Dog(); // 向上转型
myAnimal.makeSound(); // 输出:Animal makes a sound
myDog.makeSound(); // 输出:Dog barks
}
}
在这个例子中,我们先创建了一个Animal类,且有一个发出声音的方法makeSound()。而再创建的Dog类毫无疑问也属于Animal类下更细致的一种,因此我们可以通过extends关键字继承Animal类中的所有属性和方法,这时我们新建的Dog对象就已经可以“发出声音了”,但这个叫声没有特色,是动物通用的,所以我们重写了makeSound()方法,这样当我们通过Dog类的对象调用makeSound()方法时,它就会执行Dog类中的实现,而不是Animal类中的实现。
// 父类
public class Employee {
protected String name;
protected double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public void work() {
System.out.println(name + " 正在工作。");
}
public double getSalary() {
return salary;
}
}
// 子类
public class Manager extends Employee {
private int teamSize;
public Manager(String name, double salary, int teamSize) {
super(name, salary); // 调用父类构造方法
this.teamSize = teamSize;
}
@Override
public void work() {
System.out.println(name + " 正在管理团队。");
}
public void manageTeam() {
System.out.println(name + " 正在管理一个 " + teamSize + " 人的团队。");
}
}
// 使用继承
public class Main {
public static void main(String[] args) {
Employee employee = new Employee("Alice", 50000);
Manager manager = new Manager("Bob", 80000, 10);
employee.work();
System.out.println("Alice 的薪水:" + employee.getSalary());
manager.work(); // 覆盖了父类的方法
manager.manageTeam();
System.out.println("Bob 的薪水:" + manager.getSalary());
}
}
同样的,在这个例子中,Manager类继承自Employee类。它继承了name和salary属性以及work()和getSalary()方法。此外,Manager类还添加了一个新的属性teamSize和一个新的方法manageTeam。
继承是代码复用的重要手段,促进了模块化、灵活性和扩展性的提升,同时,也是我们接下来要说的“多态”的基础。
3.多态(Polymorphism)
多态允许同一个类型的对象在不同的情况下表现出不同的行为。多态性是通过继承和接口实现来达到的,子类可以重写父类的方法,以实现特定的行为。多态的核心思想是:一个接口可以有多个不同的实现,一个父类的引用可以指向子类的对象,从而实现对不同对象的统一操作。
所以我们说,继承是多态的基础,上面的Dog继承Animal类的例子中,也已经用到了方法重写这一多态的重要体现。
方法重写(Method Overriding):允许子类提供一个特定的实现,该实现可以覆盖从父类继承来的方法。这意味着,当子类对象调用一个被重写的方法时,它将执行子类中的实现,而不是父类中的实现。
并且在这个例子中,我们将创建的Dog实例,并将其赋值给一个Animal类型的引用变量myDog,这个过程称为向上转型(Upcasting),也是多态性的直接应用。具体体现为:myAnimal和myDog都是Animal类型的引用,但同样调用makeSound方法时,会根据实际的变量类型(Animal或Dog)执行相应的方法实现。
动态绑定(Dynamic Binding):在运行时,Java虚拟机(JVM)会根据对象的实际类型来决定调用哪个方法。这个过程是自动进行的,无需程序员在代码中进行显式的类型检查或方法调用。
动态绑定的存在,就是为什么myDog.makeSound()会输出“Dog barks”,而不是“Animal makes a sound”。
多态允许我们编写更通用的代码,使得同一个方法调用可以以不同实现作用于不同的对象类型。