章
目
录
面向对象编程(OOP)是指基于对象的编程方法,而不是像函数式编程那样仅基于函数和过程。这些对象可以包含数据(属性)和方法(行为),就像我们在应用程序中建模的现实生活实体一样。
本教程将教我们四个主要特性——抽象、封装、继承和多态性。这些也被称为面向对象编程范式的四大特性。
1. 什么是OOP或面向对象编程?
早期,人们用二进制代码编写程序,并使用机械开关来加载程序。后来,随着硬件功能的发展,专家尝试使用高级语言来简化编程,我们使用编译器从程序生成机器指令。
随着更多的发展,专家们创建了基于小函数的结构化编程。这些函数在很多方面都有帮助,例如代码重用、局部变量、代码调试和代码可维护性。
随着计算的进步和对更复杂应用程序的需求,结构化编程的局限性开始显现出来。复杂的应用程序需要与现实世界和用例更紧密地建模。
后来,专家们开发了面向对象编程。在 OOP 的中心,我们有对象和类。就像现实生活中的实体一样,对象具有两个重要特征:
- 数据– 讲述属性和对象的状态
- 行为– 赋予其改变自身并与其他对象进行通信的能力
1.1. 类和对象
对象是类的实例。每个对象都有自己的状态、行为和身份。类是其对象的蓝图或模板。
对象可以通过调用函数与其他对象进行通信。它有时被称为消息传递。
例如,如果我们正在开发人力资源应用程序,那么它由实体/参与者组成,例如员工、经理、部门、工资单、假期、目标、时间跟踪等。为了在计算机程序中对这些实体进行建模,我们可以创建具有类似属性的类现实生活中的数据属性和行为。
例如,员工实体可以表示为Employee
类:
public class Employee
{
private long id;
private String title;
private String firstName;
private String middleName;
private String lastName;
private Date dateOfBirth;
private Address mailingAddress;
private Address permanentAddress;
// 根据应用程序的要求,可以添加更多类似的属性、getter和setter方法。
}
以上Employee
作为模板。我们可以使用此类在应用程序中创建所需数量的不同员工对象。
Employee e = new Employee(111);
e.setFirstName("Alex");
..
..
int age = e.getAge();
该id
字段有助于存储和检索任何单个员工的详细信息。
对象标识通常由应用程序运行时环境维护,例如,用于Java应用程序的Java虚拟机(JVM)。每次我们创建一个 Java 对象时,JVM 都会为该对象创建一个哈希码并分配它。这样,即使程序员忘记添加id
字段,JVM 也能确保所有对象都是唯一标识的。
1.2. 构造函数
构造函数是没有任何返回值的特殊方法。它们的名称始终与类的名称相同,但它们可以接受参数,这些参数有助于在应用程序开始使用对象之前设置对象的初始状态。
如果我们不提供任何构造函数,JVM 会为该类分配一个默认构造函数。此默认构造函数不接受任何参数,即无参构造。
请记住,如果我们为任何类分配构造函数,那么 JVM 不会为其分配默认构造函数。如果需要,我们需要向类显式指定默认构造函数。
public class Employee
{
// 默认构造
public Employee()
{
}
// 自定义构造
public Employee(int id)
{
this.id = id;
}
}
2. OOP 的 4 个特性
面向对象编程的四大特点是:
- 抽象
- 封装
- 继承
- 多态
2.1. 抽象
当我们将抽象与实时示例联系起来时,它就很容易理解。例如,当我们驾驶汽车时,我们不必关心汽车的确切内部工作原理。我们关心的是通过方向盘、制动踏板、油门踏板等接口与汽车进行交互。在这里,我们对汽车的了解是抽象的。
在计算机科学中,抽象是用形式与其含义(语义)相似的表示来定义数据和程序,同时隐藏实现细节的过程。
简而言之,抽象隐藏了与上下文无关的信息,或者只显示相关信息,并通过将其与现实世界中的类似信息进行比较来简化它。
通常抽象可以通过两种方式来看待:
2.1.1. 数据抽象
数据抽象是从多个较小的数据类型创建复杂数据类型的方法,这更接近现实生活中的实体。例如, Employee
类可以是具有各种小关联的复杂对象。
public class Employee
{
private Department department;
private Address address;
private Education education;
//So on...
}
因此,如果您想获取有关员工的信息,您可以从 Employee 对象中询问 – 就像在现实生活中一样,询问该人本人。
2.1.2. 控制抽象
控制抽象是通过隐藏复杂任务的操作序列(在简单的方法调用内)来实现的,因此执行任务的逻辑可以对客户端隐藏,并且可以在不影响客户端代码的情况下进行更改。
public class EmployeeManager
{
public Address getPrefferedAddress(Employee e)
{
//从数据库获取所有地址
//在这里实现获取所有地址的逻辑
//返回一个包含所有地址的列表
}
}
在上面的例子中,明天如果你想改变逻辑,让每次国内地址始终是首选地址,你就改变getPrefferedAddress()方法内部的逻辑, 客户端将不受影响。
2.2. 封装
将数据和方法包装在类中并与实现隐藏(通过访问控制)相结合通常称为封装。结果是具有特征和行为的数据类型。
“无论发生何种变化,都要将其封装起来” – 一条著名的设计原则。
封装本质上既有信息隐藏,也有实现隐藏。
- 信息隐藏是通过使用访问控制修饰符(public、private、protected)来完成的,实现隐藏是通过为类创建接口来实现的。
- 实现隐藏允许设计者修改对象履行职责的方式。当设计(甚至需求)可能发生变化时,这一点尤其有价值。
让我们举一个例子来更清楚地说明这一点。
2.2.1. 信息隐藏
class InformationHiding
{
//限制直接访问内部数据
private ArrayList items = new ArrayList();
//提供一种访问数据的方式 - 内部逻辑可以在将来安全地进行更改
public ArrayList getItems(){
return items;
}
}
2.2.2. 实施隐藏
interface ImplemenatationHiding {
Integer sumAllItems(ArrayList items);
}
class InformationHiding implements ImplemenatationHiding
{
//限制直接访问内部数据
private ArrayList items = new ArrayList();
//提供一种访问数据的方式 - 内部逻辑可以在将来安全地进行更改
public ArrayList getItems(){
return items;
}
public Integer sumAllItems(ArrayList items) {
// 在这里,您可以按任何顺序执行N项操作
// 这些操作您不希望客户端知道
// 您可以更改顺序甚至整个逻辑
// 而不会影响客户端。
}
}
2.3. 继承
继承是面向对象编程中的另一个重要概念。继承是一个类获取父类的属性和行为的一种机制。它本质上是在类之间创建父子关系。在Java中,我们使用继承主要是为了代码的可重用性和可维护性。
Java 中的关键字“ extends ”用于继承类。“ extends
”关键字表示我们正在创建一个派生自现有类的新类。
在Java术语中,被继承的类称为超类。新类称为子类。
子类从其超类继承所有非私有成员(字段、方法和嵌套类)。构造函数不是成员,因此不能被子类继承,但可以从子类调用超类的构造函数。
2.3.1. 继承示例
public class Employee
{
private Department department;
private Address address;
private Education education;
//So on...
}
public class Manager extends Employee {
private List<Employee> reportees;
}
在上面的代码中,Manager 是 Employee 的特殊版本,重用Employee类中的部门、地址和教育,并定义了自己的reportees
列表。
2.3.2. 继承的类型
单继承——子类派生自一个父类。
class Parent {
//code
}
class Child extends Parent {
//code
}
多重继承——一个孩子可以从多个父母那里继承。在 JDK 1.7 之前,通过使用类在 java 中无法实现多重继承。但从 JDK 1.8 开始,通过使用带有默认方法的接口可以实现多重继承。
interface MyInterface1 {
}
interface MyInterface2 {
}
class MyClass implements MyInterface1, MyInterface2 {
}
多级继承——指三个以上的类之间的继承,其中一个子类将充当另一个子类的父类。
class A {
}
class B extends A {
}
class C extends B {
}
层次继承是指有一个超类和多个扩展超类的子类时的继承。
class A {
}
class B extends A {
}
class C extends A {
}
class D extends A {
}
混合继承——是两种或多种继承类型的组合。因此,当类之间的关系包含两种或多种类型的继承时,我们就说类实现了混合继承。
interface A {
}
interface B extends A {
}
class C implements A {
}
class D extends C impements B {
}
2.4. 多态性
多态性是一种能力,通过它,我们可以创建在不同的编程上下文中表现不同的函数或引用变量。它通常被称为具有多种形式的一个名称。
例如,在大多数编程语言中, '+'
运算符用于添加两个数字和连接两个字符串。根据变量的类型,运算符会更改其行为。这称为运算符重载。
在Java中,多态本质上分为两种:
2.4.1. 编译时多态性
在编译时多态性中,编译器可以在编译时将适当的方法绑定到相应的对象,因为它拥有所有必要的信息并且知道在程序编译期间调用哪个方法。
它通常被称为静态绑定或早期绑定。
在Java中,它是通过使用方法重载来实现的。在方法重载中,方法参数可以随参数的数量、顺序或类型而变化。
class PlusOperator {
int sum(int x, int y) {
return x + y;
}
double sum(double x, double y) {
return x + y;
}
String sum(String s1, String s2) {
return s1.concat(s2);
}
}
2.4.2. 运行时多态性
在运行时多态性中,对重写方法的调用在运行时动态解析。将在其上执行方法的对象是在运行时确定的——通常取决于用户驱动的上下文。
它通常被称为动态绑定或方法重写。我们可能听说过动态方法调度这个名字。
在运行时多态性中,我们通常有一个父类和至少一个子类。在类中,我们编写一条语句来执行父类和子类中存在的方法。
使用父类类型的变量给出方法调用。类的实际实例是在运行时确定的,因为父类类型变量也可以存储对父类以及子类实例的引用。
class Animal {
public void sound() {
System.out.println("Some sound");
}
}
class Lion extends Animal {
public void sound() {
System.out.println("Roar");
}
}
class Main {
public static void main(String[] args) {
//Parent class reference is pointing to a parent object
Animal animal = new Animal();
animal.sound(); //Some sound
//Parent class reference is pointing to a child object
Animal animal = new Lion();
animal.sound(); //Roar
}
}
3.更多面向对象编程概念
除了上述的四个面向对象编程构建基块之外,还有一些其他概念在构建整体理解方面起着重要作用。
在深入探讨之前,我们需要了解术语“模块”。在一般的编程中,模块是执行独特功能的类或子应用程序。在人力资源应用程序中,一个类可以执行诸如发送电子邮件、生成工资单、计算员工年龄等各种功能。
3.1. 耦合度
耦合度是模块之间相互依赖程度的度量。耦合度指的是软件元素与其他元素之间的联系强度。良好的软件将具有低耦合度。
这意味着一个类应该执行唯一的任务,或者只执行与其他任务无关的任务。例如,一个EmailValidator类只会验证电子邮件。同样,EmailSender类只会发送电子邮件。
如果我们将这两个功能都包含在一个名为EmailUtils的单个类中,那么这就是紧密耦合的示例。
3.2. 内聚度
内聚度是保持模块整体性的内在因素。良好的软件设计将具有高内聚度。
这意味着一个类/模块应该包含执行其功能所需的所有信息,而不依赖于其他内容。例如,一个EmailSender类应该能够配置SMTP服务器,并接受发件人的电子邮件、主题和内容。基本上,它应该只关注发送电子邮件。
应用程序不应该将EmailSender用于发送电子邮件以外的任何其他功能。低内聚度会导致庞大的类,难以维护、理解和降低可重用性。
3.3. 关联
关联指的是具有独立生命周期且彼此没有所有权的对象之间的关系。
以教师和学生为例。多个学生可以与单个教师关联,单个学生也可以与多个教师关联,但它们都有自己的生命周期。
两者都可以独立创建和删除,因此当教师离开学校时,我们不需要删除任何学生,当学生离开学校时,我们也不需要删除任何教师。
3.4. 聚合
聚合指的是具有独立生命周期但具有“拥有关系”的对象之间的关系。它发生在子类和父类之间,其中子对象不能属于另一个父对象。
以手机和手机电池为例。一块电池一次只能属于一个手机。如果手机停止工作,我们从数据库中删除它,手机电池将不会被删除,因为它可能仍然可用。因此,在聚合中,虽然存在所有权,但对象有自己的生命周期。
3.5. 组合
组合指的是对象没有独立生命周期的关系。如果删除父对象,所有子对象都将被删除。
例如,问题和答案之间的关系。一个问题可以有多个答案,但答案不能属于多个问题。如果我们删除一个问题,它的所有答案将自动被删除。
4.最佳实践
4.1. 优先考虑组合而不是继承
继承和组合都促进了代码的可重用性。但在继承与组合之间,更倾向于使用组合。
组合的实现通常从创建各种表示系统必须展现的行为的接口开始。接口使得多态行为成为可能。实现已确定接口的类会根据需要构建并添加到业务领域类中。因此,系统行为可以在没有继承的情况下实现。
interface Printable {
print();
}
interface Convertible {
print();
}
class HtmlReport implements Printable, Convertible
{
}
class PdfReport implements Printable
{
}
class XmlReport implements Convertible
{
}
4.2. 面向接口编程,而不是具体实现
这会导致灵活的代码,可以与接口的任何新实现一起使用。我们应该将接口用作变量、方法的返回类型或方法的参数类型。
接口充当超类类型。通过这种方式,我们可以在不修改现有代码的情况下,在将来创建更多接口的特定实现。
4.3. DRY(不要重复自己)
不要编写重复的代码,而是使用抽象将常见的内容抽象到一个地方。
作为一个基本原则,如果在两个地方写了相同的代码 – 考虑将其提取到一个单独的函数中,并在两个地方调用该函数。
4.4. 封装变化
所有软件都会随着时间的推移而发生变化。因此,将您期望或怀疑将来会发生变化的代码封装起来。
在Java中,使用私有方法来隐藏此类实现,以使客户端不必在进行更改时更改其代码。
还建议使用设计模式来实现封装。例如,工厂设计模式封装了对象创建代码,并提供了灵活性,以后可以引入新类型,而不会影响现有客户端。
4.5. 单一职责原则
这是面向对象编程类设计的SOLID原则之一。它强调一个类应该只有一个责任。
换句话说,我们应该为一个目的编写、更改和维护一个类。这将使我们能够在不担心更改对另一个实体产生影响的情况下进行未来更改。
4.6. 开闭原则
它强调软件组件应该对扩展开放,但对修改关闭。
这意味着我们的类应该设计得当,以便其他开发人员在应用程序中的特定条件下更改控制流时,他们只需要扩展我们的类并重写一些函数,就可以了。
如果其他开发人员由于我们的类所施加的约束而无法设计所需的行为,那么我们应该重新考虑更改我们的类。
在整个面向对象编程范 Paradigm 内还有许多其他概念和定义,我们将在其他教程中学习。
5. 总结
这个Java面向对象编程(OOP)教程讨论了Java中的四个主要OOP特性,附有易于理解的程序和片段。您可以在评论部分提出您的问题。