Object-oriented Encapsulation

写在前面

今天是中秋节,祝大家中秋快乐。本文并不是非常专业的科普文章。只是笔者自己在学习过程中得到的一些经验和总结。为了方便日后的回顾与再思考,才想到了要用写下来的方式放在公众号上。大家可以在看完之后提出自己的思考与问题,也可以就文章中出现的一些错误向公众号反馈。看到之后我都会一一回复的~非常感谢大家的支持,如果大家也有和我一样的想法想要将自己在日常学习的一些总结记录下来的话,也欢迎大家加入

今天要紧接着上篇文章的话题来和大家聊聊面向对象编程(Object-Oriented Programming)中的封装(encapsulation )。这个概念和思想可以说是面向对象的基础之基础,封装很好的让程序员之间的配合协作变得更加顺畅与有利,也更有利于代码的安全性和可重构性。本篇文章将会分为三个部分加一个小结,如果想要跳过前面的基础简单内容可以选择快速跳过:

  1. 什么是封装?
  2. 封装的特点
  3. Java中的访问控制
  4. 小结

1 什么是封装

要介绍什么是封装,我觉得用下面这个例子来让大家简要的体会比较好:

ENIAC计算机 的图像结果

最早的计算机需要程序员通过记住许多冗长的指令才能够实现对计算机的操作,而如今的计算机有了非常良好的图形用户界面和操作系统,不需要用户再去记冗长的指令和操作,只需要用简单的鼠标点击和键盘输入就可以非常轻松的使用电脑。相比以往计算机的复杂操作,如今的计算机更能够走进各家各户,成为一种大家都可以拥有的“平民产物”。

上面这个了例子其实就非常好的说明了封装的一种意义是什么:计算机的发展过程正是通过将指令封装起来,用操作系统和图形用户界面(GUI)等来掩盖计算机底层的真正实现,让计算机的使用者可以更好地掌握如何使用计算机的方法。

如今通用计算机的使用者绝大部分都不需要去知道计算机是如何实现一些复杂的操作的,比如说鼠标点击为什么这个文件就会打开呢?输入文字为什么就会显示在显示屏上呢?使用者只需要知道用鼠标点击文件就会打开,用键盘输入文字就会显示在显示屏上。这其实就是封装的一种意义之所在。

那么,封装到底是什么呢?我们知道,面向对象的程序是由一个个对象之间互相发送消息而组成的,封装在其中的作用就是构成一个符合我们所设计的对象。简单来说,将某个对象的成员变量成员方法都放在一个**类(class)**中,我们就将该对象所拥有的属性都封装在了该类中。

即:对象的数据和行为要放置在一起且相互匹配,这是封装要达到的基本要求

什么叫数据和行为的放置在一起且相互匹配呢,我们通过如下的一段Java代码来加以说明。

public class Human {
int birthday;

public Gender getGender() {
// ...
}
}

enum Gender {
MALE, FEMALE;
}

上述代码的Human类就不是一个很好的封装,为Human类定义了birthday的成员变量,但是并没有对应的成员方法来使用到该变量,而getGender()方法用来获取Human的性别的,但是Human类中却没有对应的成员变量来存储该属性。

  1. 成员方法就是一个类中的方法,代表了某个对象所能够做的某些行为,在C中称为“函数”

  2. 成员变量是定义在类内但是定义在成员方法外的变量,他们随着一个对象的创建而被创建,是一个对象所具有的某些特征。(区别于局部变量)

那么该如何修改呢?答案其实非常简单:

public class Human {
Gender gender;

public Gender getGender() {
return this.gender;
}
}

封装其实就是如此:数据与行为的在一起和互相匹配。当然它也远远不止于此。

2 封装的特点

这里首先我们先介绍两个概念,分别是Client代码Server代码。大家可以将他们看成提供服务的代码和客户端的代码。简单来说可以用如下两句话来解释:

  1. Server代码提供某些服务(方法)给Client代码
  2. Client代码通过使用Server代码来实更加复杂的一些功能

比如我们在Java中用到的类库和C++中的STL就是Server代码的一种,通过提供某些服务给我们,我们就可以以此为基础来实现更加复杂的东西。

这其实就是封装的一大特点:**只把客户所需要使用到的部分暴露出来,而将用户所不需要知道的底层实现全部隐藏起来。**也是我们上一小节所说的封装的意义。

由此特点我们就可以解释为什么封装会是面向对象的三大特性之一,而且也是其中的基础部分了。对象之间发送消息的时候,从“问题空间”来看,我们所模拟的就是两个对象之间的互相交流与通信,我们不会关注对象的发送消息背后究竟发生了什么。

这么说可能有些抽象,那么我们就来举个例子吧。

现在有一队身高各不相同的人在军训,教官让他们按身高从高到低排好队,于是给“队列”这个对象发送消息说:“你们该排好队啦!“

队列收到了消息,于是排好了队。但是在这背后,“教官”这个对象并不知道队列究竟是如何排好队的。他只知道,只要他给“队列”这个对象发送消息让他们排好队,他们就会排好队,而不用去关心他们如何排好队。

嗯,我相信这个例子已经很好地解释了上面所说的话,对象之间的发送消息往往不去在乎底层的具体实现是什么,因为在对象一看来,他只要知道对象二接收到消息后所做出来的结果就可以,而不需要去在意这中间是如何实现的。

这种将用户需要的部分暴露给用户,而将用户不需要的底层实现隐藏起来让程序员之间的配合更加顺利,比如如下例子:

专注于写工具类的程序员不需要知道Java本身自带的类库中的底层实现究竟是如何(当然有时我们还是需要知道的,不过大部分我们只需要知道接口就可以了),只需要知道类库中暴露给他的接口是如何使用的。专注于写其他方面的程序员也不需要知道工具类中的某个方法究竟是如何实现的,只需要知道工具类中有哪些方法,该怎么用就可以了。

对于开发人员来说,要追求**“高内聚,低耦合”**,高内聚就是类的内部数据操作及细节自己完成,不需要也不允许外部干涉。低耦合就是仅暴露少量方法给外部使用,能方便外部调用就好。这就是封装的特点和优点所在:

  1. 代码的安全性提高了。使用者不知道底层代码的实现,也触碰不到底层的实现,他们只能够通过暴露给他们的接口来使用该方法,而不会去修改到不属于他们的范围。
  2. 代码的可修改性和复用性提高了。某个方法的底层实现修改时,不需要去告诉这个方法的使用者这个方法的实现会被如何修改,只需要维持原有的接口不变,就可以避免使用者代码的修改。

当然,可能有人会问使用者怎么知道该方法该如何使用呢?这就是“文档”的重要性了,文档会将某个方法的使用场景、参数、返回值、抛出的异常等等方面都十分细致的告诉使用者。

在理解了封装的这种特性之后,我们就该来解决下一个问题了,封装时我们该如何让使用者触碰不到我们不想让他触碰的地方呢?我们又如何规定哪个方法是能够被使用者使用的,哪个方法是使用者不能够使用的呢?这就是访问控制的领域了。下面我们就以Java的访问控制来简单的讨论一下。

3 Java中的访问控制

Java中,访问控制是能够作用于类、成员变量、成员方法上的一种修饰,能够控制该类、成员变量、成员方法能够被谁所访问到,对于Java中的封装具有极大的意义。

Java中的访问控制一共有四种:

  1. public
  2. protected
  3. default(没有任何修饰的缺省)
  4. private

Public:接口访问权限

public,顾名思义,就是所有都可以使用的意思。一个public class能够被其他所有类所调用,当你使用关键字 public,就意味着紧随 public 后声明的成员对于每个人都是可用的,尤其是使用类库的客户端程序员更是如此。需要注意的是,一个.java文件只能够有一个public class,而且该class的名字必须与.java文件的名字相同(大小写也要相同!!)。

下面我们以一个例子来介绍:

public class Human {
...
}

class Home {
Human human = new Human();
}

无论Human类位于何处,Home总是能够访问到该类,前提是要导入Human类所在的包,关于Java中的包结构我们放在以后讲,这是Java中防止类冲突和封装的一种机制。

Protected:继承访问权限

关键字 protected 处理的是继承的概念,通过继承可以利用一个现有的类——我们称之为基类,然后添加新成员到现有类中而不必碰现有类。我们还可以改变类的现有成员的行为。为了从一个类中继承,需要声明新类 extends 一个现有类,像这样:

class Son extends Father {...}

有时,基类的创建者会希望某个特定成员能被继承类访问,但不能被其他类访问。这时就需要使用 protectedprotected 也提供包访问权限,也就是说,相同包内的其他类可以访问 protected 元素。

Default:包访问权限

如果在你要修饰的对象之前你没有加任何的修饰符,那么就默认它是包可见的。以后的文章中我们会对包结构做一个更好的介绍,在这里就先跳过不讲。可以简单理解为包结构是为了防止相同名字的类出现的冲突而引入的,与此同时他也为封装提供了一定的机制。默认的修饰符就只有在同一个包下的类可以访问,比如如下:(注释声明了他们所处的包)

// package com.zyLearningCode.simple

public class Human {
int birthday;

int getBirthday() {
return this.birthday;
}
}

// package com.zyLearningCode.simple

public class TestSimple {
public static void main(String[] args) {
Human human = new Human();
int humanBirthday = human.getBirthday();
}
}

// package com.zyLearningCode.other

public class TestOther {
public static void main(String[] args) {
Human human = new Human(); // OK
// int humanBirthday = human.getBirthday();
// 上面的方法调用是错误的
}
}

Private:自身访问权限

关键字 private 意味着除了包含该成员的类,其他任何类都无法访问这个成员。同一包中的其他类无法访问 private 成员,因此这等于说是自己隔离自己。使用 private,你可以自由地修改那个被修饰的成员,无需担心会影响同一包下的其他类。

默认的包访问权限通常提供了足够的隐藏措施;记住,使用类的客户端程序员无法访问包访问权限成员。因为默认访问权限是一种我们常用的权限(同时也是一种在忘记添加任何访问权限时自动得到的权限)。因此,通常考虑的是把哪些成员声明成 public 供客户端程序员使用。

使用private在于我们可以隐藏一些对外界使用者来说不是必要而且我们也不愿意让外界使用者修改的东西,这对于封装来说是及其重要的。

控制成员访问权限有两个原因。第一个原因是使用户不要接触他们不该接触的部分,这部分对于类内部来说是必要的,但是不属于客户端程序员所需接口的一部分。因此将方法和属性声明为 private 对于客户端程序员来说是一种服务,可以让他们清楚地看到什么是重要的,什么可以忽略。这可以简化他们对类的理解。

第二个也是最重要的原因是为了让类库设计者更改类内部的工作方式,而不用担心会影响到客户端程序员。比如最初以某种方式创建一个类,随后发现如果更改代码结构可以极大地提高运行速度。如果接口与实现被明确地隔离和保护,你可以实现这一目的,而不必强制客户端程序员重新编写代码。访问权限控制确保客户端程序员不会依赖某个类的底层实现的任何部分。

小结

这篇文章我们简单的介绍了一下什么是封装。封装的一些基本概念与规范相信大家有了一定的认识。

封装就是通过将某个对象的相匹配数据和行为放在同一个类中来构建这个对象,同时在对象发送消息的过程中尽可能的少暴露类中的方法与具体实现,而只给使用者所需要的方法接口和实用规范

Java中,访问权限的合理应用能够使得类的封装性更加的出色,也能够增强代码的安全性和可修改性。

以上就是这篇文章的全部内容啦,之后会跟大家聊聊继承包结构~

感谢大家能够仔细的阅读完这篇文章,希望阅读完你能够有所收获。如果对文章内容有所疑问可以私戳公众号后台。感激不尽!如果也想加入一起分享自己所学的一些所感所得,也可以私戳我~

Reference

  1. 《On Java 8》
  2. java中的面向对象-封装 - 知乎 (zhihu.com)
  3. 南京大学2020软件工程课程–软件工程与计算I
  4. 南京大学2021计算机科学与技术课程–Java高级程序设计
文章作者: ZY
文章链接: https://zyinnju.com/2022/02/14/Object-oriented-Encapsulation/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 ZY in NJU