让对象明白什么是面向对象

27次阅读

共计 4472 个字符,预计需要花费 12 分钟才能阅读完成。

前言

之所以写下这篇文章,是因为女朋友这学期要修 Java,但在这之前只接触过 C 语言,对于面向对象的概念一时难以理解,于是这里写一篇文章来讲一讲。我之前并没有接触过 Java,原本只是打算讲讲 OOP 的一些概念的,不过后来还是打算开始学习一下 Java,并且整理一个笔记系列,这篇文章就做个开头吧。有关 Java 的内容基本上是基于对 《On Java 8》《Java 核心技术 卷一》这两本书。
把这些内容发布在公众号上,希望也能对有相同需求的人提供帮助,但是需要注意,在我后续的文章里,都假定读者有一定 C 语言基础。

抽象

抽象 是计算机科学中一个非常重要的概念,它和 具体 正好相反。抽象是为了降低复杂度。在文学作品中常常以具体事物来比喻抽象的东西:

一往情深深几许?深山夕照深秋雨。

但是具体的事物往往是繁琐的复杂的,在计算机科学中,通过抽象,能够更好的解决问题。
首先我们在课堂上应该都听过老师讲解二进制时讲过二进制的 10与计算机内部的电路开关状态对应,CPU 中有非常非常多的电路。让我们来看一下我们的计算机程序,不管是一个简单的计算 1+1 还是一个类似 Windows 这样的操作系统,它们在运行的时候,其实都是这些电路中电子在跑来跑去。CPU 内部有数以亿计的晶体管。
在一堆晶体管的基础上,我们抽象出门电路,与门、或门、非门等等,不用知道在电路中电子怎么运动的,也不需要知道输入多少电压对应二进制的哪一位,只要知道有什么样的输入能得到什么样的输出就行了。
在这之上,又可以抽象出更复杂的电路,触发器、寄存器,我们只要知道怎么操作会得到什么结果,而不必知道怎么得到的结果。
最初的程序,我们使用 1 和 0 组成的机器语言编写,它能直接被计算机 CPU 识别运行。但是对于人类来说还是难以理解。
在机器语言的基础上,产生了汇编语言。汇编语言是对机器语言的一种抽象,如下是一个实现两数相加的汇编程序:

datas segment
      x dw 1234h
      y dw 5678h
      z dw ?
datas ends
codes segment
    assume cs:codes,ds:datas
start:
    mov ax,datas
    mov ds,ax
    mov ax,x
    add ax,y
    mov z,ax
    mov ah,4ch
    int 21h
codes ends
    end start

在汇编中,使用例如 movadd 这样的指令,我们不用操心具体写什么样的机器码,汇编器会最终帮我们将代码翻译成机器语言。但在汇编中,我们还要操心例如将数据移动到某个 CPU 的寄存器中这样的问题。
再对汇编进行抽象,我们发展出了高级语言,例如在 Python 中:

print(2+3)

这样不仅能计算加法的结果还能显示在屏幕上,我们编写的代码更解决人类的自然语言,不用去关心数据是不是要从内存放进 CPU,怎么实现了数据的相加,怎么将数据显示在屏幕中。
好了,现在我们有了一个高级语言,现在我们要写一些程序大致是这样:

函数
函数 1
函数 2
函数 3
函数 4
...

数据
数据 1
数据 2
数据 3
数据 4
...

这时候是一种 面向过程 的编程模式,随着时代的发展,当我们的需求越来越多,一个程序可能会写出非常非常多的函数,有一些函数要实现的功能类似,但又不得不多写很多行代码。
以一个工厂举例,一家工厂,需要工人,生产线,原材料采购,市场销售,技术研发,如果用面向过程的思想去解决,那么这个程序员要考虑一家工厂从设计产品到购买材料到投入生产到销售这一系列环节中所有的功能,所有的数据。
这个时候就需要 面向对象 的思想了。

面向对象

面向对象编程(Object-Oriented Programming OOP)是一种编程思维方式和编码架构,面向对象是一种对现实世界理解和抽象的方法。

在工厂的例子中,使用面向对象的思路来解决,那么我们会将问题分而治之,让工人来负责生产,技术员来负责研发,销售负责贩卖产品,采购负责买材料。

以下摘录自《On Java 8》:

  1. 万物皆对象。你可以将对象想象成一种特殊的变量。它存储数据,但可以在你对其“发出请求”时执行本身的操作。理论上讲,你总是可以从要解决的问题身上抽象出概念性的组件,然后在程序中将其表示为一个对象。
  2. 程序是一组对象,通过消息传递来告知彼此该做什么。要请求调用一个对象的方法,你需要向该对象发送消息。
  3. 每个对象都有自己的存储空间,可容纳其他对象。或者说,通过封装现有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。
  4. 每个对象都有一种类型。根据语法,每个对象都是某个“类”的一个“实例”。其中,“类”(Class)是“类型”(Type)的同义词。一个类最重要的特征就是“能将什么消息发给它?”。
  5. 同一类所有对象都能接收相同的消息。这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆”(Circle)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收发送给 ” 形状”的消息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括“圆”。这一特性称为对象的“可替换性”,是 OOP 最重要的概念之一。

Grady Booch 提供了对对象更简洁的描述:一个对象具有自己的状态,行为和标识。这意味着对象有自己的内部数据(提供状态)、方法 (产生行为),并彼此区分(每个对象在内存中都有唯一的地址)。

在 Java 中使用 class 关键字定义类,使用例如 Worker w1 = new Worker() 来实例化类为一个对象。

/*
这是一个类,它表示一种类型,例如工人,就是一类人,它有 name 这个属性(field),work 这个方法(method)*/
public class Worker {
    String name;
    public void word() {...}
}
/*
w1 是一个对象,对象是类的实例,比如工厂里有 100 个工人,w1 就是工人中的一个实例
w1 这个对象的 name 属性是张三
w1 可以调用 work 方法
定义了类之后,每一个这个类的实例对象,都会有类定义的属性与方法
*/
Worker w1 = new Worker();
w1.name = "张三";
w1.work()

封装

面向对象编程将数据以及对数据的操作打包起来放到对象里,外界无需知道程序内部实现的细节,只要通过类似 xx.xx() 的形式去调用,封装可以隐藏起对象的属性和实现细节。

我们可以把编程的侧重领域划分为研发和应用。应用程序员调用研发程序员构建的基础工具类来做快速开发。研发程序员开发一个工具类,该工具类仅向应用程序员公开必要的内容,并隐藏内部实现的细节。这样可以有效地避免该工具类被错误的使用和更改,从而减少程序出错的可能。彼此职责划分清晰,相互协作。当应用程序员调用研发程序员开发的工具类时,双方建立了关系。应用程序员通过使用现成的工具类组装应用程序或者构建更大的工具库。如果工具类的创建者将类的内部所有信息都公开给调用者,那么有些使用规则就不容易被遵守。因为前者无法保证后者是否会按照正确的规则来使用,甚至是改变该工具类。只有设定访问控制,才能从根本上阻止这种情况的发生。

Java 有三个显式关键字来设置类中的访问权限:public(公开),private(私有)和 protected(受保护)。这些访问修饰符决定了谁能使用它们修饰的方法、变量或类。

  1. public(公开)表示任何人都可以访问和使用该元素;
  2. private(私有)除了类本身和类内部的方法,外界无法直接访问该元素。private 是类和调用者之间的屏障。任何试图访问私有成员的行为都会报编译时错误;
  3. protected(受保护)类似于 private,区别是子类(下一节就会引入继承的概念)可以访问 protected 的成员,但不能访问 private 成员;
  4. default(默认)如果你不使用前面的三者,默认就是 default 访问权限。default 被称为包访问,因为该权限下的资源可以被同一包(库组件)中其他类的成员访问。

使用访问控制,我们让使用我们编写的类的程序员不要接触我们不想让他们访问的部分,同时我们可以修改程序,优化我们的代码而不影响应用程序员的使用。

public class HelloWorld {public static void main(String[] args)
    {System.out.println("Hello World");
    }
}

在上面这个简单的 helloworld 程序中,我们使用了 System.out.println() 来打印文字到屏幕上,但是我们并不知道这个方法的实现细节,哪怕在版本更新中重写了这个方法的实现,只要接口和功能没变,我们的代码就没有问题。

继承

随着工厂不断发展,业务越做越多,出现了各种不同的职位分工,这些员工在很多方面有相似之处,但具体做的事情略有不同,那我们是不是要写很多个员工类呢?岂不是会有很多重复的代码吗?

我们可以这样做:先定义一个 员工类 ,它拥有 名字、年龄、薪水、工作时间 等等公有的属性与方法,然后有各种类型的员工继承这个工人类,员工类可以称为 父类 ,细分的工种称子类。
子类会拥有父类所有的属性与方法。父类做出改变,也会反映在子类上。

子类也可以添加自己独有的属性与方法,例如工人可能会有领取防护用品的方法

多态

在我们的例子中,我们通过员工类派生出各种类型的子类,有工人、销售、研发等等,他们都继承了父类的 work() 方法,可是,不同的职位做的工作会相同吗?
答案显然是否定的。
那么我们工厂调用员工去工作,怎么实现不同类型的员工做不同的事情呢?要写上很多的 类型判断 吗?根据不同类型完成不同行为?
看下面一个例子:

void toWork(Employee employee) {employee.work();
    // ...
}

使用 toWork() 方法,表面看上去只能使用 Employee 类,让我们接着往下看:

Work work = new Work();
Technician tech = new Technician();
Salesman sale = new Salesman();
toWork(work);
toWork(tech);
toWork(sale);

可以看到传入任何类型的员工都能得到执行。
可以看到我们没有做任何的类型判断,由于工人、技术员、销售都是员工的 子类 ,在toWork() 中它们都能被当作 员工 类型,这种特性在 Java 中被称为 向上转型(upcasting)

发送消息给对象时,如果程序不知道接收的具体类型是什么,但最终执行是正确的,这就是对象的“多态性”(Polymorphism)。面向对象的程序设计语言是通过“动态绑定”的方式来实现对象的多态性的。编译器和运行时系统会负责对所有细节的控制;我们只需知道要做什么,以及如何利用多态性来更好地设计程序。

结语

面向对象归根结底是一种程序设计的思想,并不是有 class 关键字就是面向对象。通过这种设计思想,我们在应对复杂庞大的问题时,能写出可读性更高、复用性更强的程序。如果你去阅读 Linux 内核的源代码,会发现虽然其使用 C 语言 编写,但仍然有着 面向对象 的思想在其中。

扫码关注公众号:

正文完
 0