22

我们总是说,如果我们简单地定义变量private并定义 getter setter 来访问这些变量,那么数据将被封装。我的问题是,如果我们可以通过 getter 和 setter 访问变量(数据),为什么数据是隐藏的或安全的?

我用谷歌搜索了很多解释,但我什么也没找到。每个人都在他们的博客和帖子中说这是一种数据隐藏技术,但尚未解释/详细说明。

4

9 回答 9

60

封装不仅仅是为类定义访问器和修改器方法。它是面向对象编程的一个更广泛的概念,包括最小化类之间的相互依赖关系,它通常通过信息隐藏来实现。

封装的美妙之处在于改变事物而不影响其用户的力量

在像 Java 这样的面向对象编程语言中,您可以通过使用可访问性修饰符(public、protected、private,加上没有暗示包私有的修饰符)隐藏细节来实现封装。通过这些级别的可访问性,您可以控制封装级别,级别限制越少,更改发生时的成本就越高,并且类与其他依赖类(即用户类和子类)的耦合度越高。

因此,目标不是隐藏数据本身,而是如何操作这些数据的实现细节。

这个想法是提供一个公共接口,您可以通过该接口访问这些数据。您可以稍后更改数据的内部表示,而不会损害类的公共接口。相反,通过暴露数据本身,您损害了封装性,因此损害了在不影响其用户的情况下改变您处理数据的方式的能力。您创建对数据本身的依赖,而不是对类的公共接口。当“改变”最终找到你时,你会为麻烦调制一杯完美的鸡尾酒。

您可能想要封装对字段的访问有几个原因。Joshua Bloch 在他的《Effective Java 》一书中的第 14 条:最小化类和成员的可访问性中提到了几个令人信服的原因,我在此引用:

  • 您可以限制可以存储在字段中的值(即性别必须是 F 或 M)。
  • 您可以在修改字段时执行操作(触发事件、验证等)。
  • 您可以通过同步方法来提供线程安全。
  • 您可以切换到新的数据表示(即计算字段、不同的数据类型)

然而,封装不仅仅是隐藏字段。在 Java 中,您可以隐藏整个类,从而隐藏整个 API 的实现细节。例如,在方法中思考Arrays.asList()。它返回一个List实现,但你不关心哪个实现,只要它满足List接口,对吧?将来可以更改实现而不影响该方法的用户。

封装之美

现在,在我看来,要理解封装,首先必须理解抽象。

例如,考虑汽车概念的抽象级别。汽车的内部实现很复杂。它们有几个子系统,如传动系统、制动系统、燃油系统等。

然而,我们已经简化了它的抽象,我们通过它们抽象的公共接口与世界上所有的汽车进行交互。我们知道所有的汽车都有一个方向盘,通过它我们可以控制方向,它们有一个踏板,当你按下它时,你可以加速汽车并控制速度,还有一个当你按下它时,它就会停下来,你有一个档位可以让您控制前进或后退的棍子。这些特性构成了汽车抽象的公共接口。早上你可以开一辆轿车,然后下车,下午开一辆 SUV,就好像它是同一件事一样。

然而,我们中很少有人知道所有这些功能是如何在后台实现的。想想汽车没有液压方向系统的时代。有一天,汽车制造商发明了它,他们决定从那时起将其放入汽车中。尽管如此,这并没有改变用户与他们交互的方式。最多,用户体验到使用定向系统的改进。像这样的改变是可能的,因为汽车的内部实现是封装的。可以在不影响其公共接口的情况下安全地进行更改。

现在,想想汽车制造商决定把油箱盖放在汽车下面,而不是放在汽车的一侧。你去买一辆这样的新车,当你用完油时,你去加油站,却找不到油箱盖。突然你意识到它在汽车下方,但你无法用气泵软管到达它。现在,我们已经破坏了公共接口契约,因此,整个世界都崩溃了,它分崩离析,因为事情没有按照预期的方式工作。像这样的改变将花费数百万美元。我们需要更换世界上所有的气泵。当我们打破封装时,我们必须付出代价。

因此,如您所见,封装的目标是最大限度地减少相互依赖并促进变化。您可以通过最小化实现细节的暴露来最大化封装。类的状态只能通过其公共接口访问。

我真的建议您阅读 Alan Snyder 的一篇名为Encapsulation and Inheritance in Object-Oriented Programming Languages的论文。此链接指向 ACM 上的原始论文,但我很确定您可以通过 Google 找到 PDF 副本。

于 2012-08-15T13:48:21.993 回答
16

我理解您的问题的方式是,尽管我们将变量声明为private,因为可以使用 getter 和 setter 访问这些变量,但它们不是私有的。因此,这样做的意义是什么?

好吧,当使用 getter 和 setter 时,您可以限制对private变量的访问。

IE,

private int x;

public int getInt(String password){
 if(password.equals("RealPassword")){
   return x;
  }
}

对于二传手也是如此。

于 2012-08-15T09:25:42.933 回答
13

数据是安全的,因为您可以在 getter / setter 中执行其他逻辑,并且无法更改变量的值。想象一下,您的代码不适用于 null 变量,因此在您的 setter 中,您可以检查 null 值并分配一个默认值,即 != null。因此,无论是否有人尝试将您的变量设置为 null,您的代码仍然有效。

于 2012-08-15T09:20:16.870 回答
5

我的问题是,如果我们可以通过 getter 和 setter 访问变量(数据),为什么数据是隐藏的或安全的?

您可以将逻辑封装在 getter/setter 下。例如,

public void setAge(int age) {
    if (age < 0) {
        this.age = 0;
    }
    else {
        this.age = age;
    }
}
于 2012-08-15T09:17:52.893 回答
4

继续 Jigar 的回答:有几件事需要封装。

  1. 合同管理:如果你成功了public,你实际上是在让任何人将其更改为他们想要的任何东西。您无法通过添加约束来保护它。您的 setter 可以确保以适当的方式修改数据。

  2. 可变性:你不必总是有一个 setter。如果您想在对象的生命周期内保持不可变的属性。您只需将其设为私有并且没有设置器。它可能会通过构造函数设置。然后你的 getter 将只返回属性(如果它是不可变的)或属性的副本(如果属性是可变的)。

于 2012-08-15T09:24:59.547 回答
3

一般来说,getter 和 setter 对字段的封装为更改留下了更大的灵活性。

如果直接访问字段,您将陷入“愚蠢的字段”。字段只能写入和读取。访问字段时不能做任何其他事情。

使用方法时,您可以在设置/读取值时做任何您想做的事情。正如 markus 和 Jigar 所提到的,验证是可能的。此外,您可以决定某天该值是从另一个值派生的,或者如果该值发生更改,则必须执行某些操作。

数据是如何隐藏或安全的

通过使用 getter 和 setter,数据既不隐藏也不安全。它只是为您提供了使其安全的可能性。隐藏的是实现而不是数据。

于 2012-08-15T09:47:39.747 回答
2

如果存在访问器和/或突变器,数据验证是您关于封装如何提供安全性的问题的主要答案。其他人使用具有故障安全功能以在 mutator 中设置默认值的示例提到了这一点。你回答说你更喜欢抛出异常,这很好,但是当你去使用它时识别你有坏数据并不会改变你有坏数据的事实因此,在修改数据之前捕获异常不是最好的吗,即在 mutator 中进行处理?这样,除非 mutator 验证它是有效的,否则实际数据永远不会被修改,因此在出现错误数据的情况下会保留原始数据。

我自己还只是一个学生,但是我和你第一次遇到封装时的情况一样,所以我花了一些时间弄清楚。

于 2013-06-06T06:11:04.693 回答
1

我喜欢考虑线程时的解释。如果您公开了您的字段,您的实例如何知道某个线程何时更改了它的一个字段?

做到这一点的唯一方法是封装,或者更简单的 put 为该字段提供 getter 和 setter,因此您始终知道并可以检查/响应字段更新,例如。

于 2012-08-15T09:22:04.433 回答
0

封装使代码更容易被其他人重用。使用封装的另一个关键原因是接口不能声明字段,但它们可以声明方法,方法可以引用一个字段!

方法在开头用动词正确命名。如:getName()、setName()、isDying()。哪位能帮忙看代码!

于 2015-07-25T07:05:57.233 回答