我们总是说,如果我们简单地定义变量private
并定义 getter setter 来访问这些变量,那么数据将被封装。我的问题是,如果我们可以通过 getter 和 setter 访问变量(数据),为什么数据是隐藏的或安全的?
我用谷歌搜索了很多解释,但我什么也没找到。每个人都在他们的博客和帖子中说这是一种数据隐藏技术,但尚未解释/详细说明。
我们总是说,如果我们简单地定义变量private
并定义 getter setter 来访问这些变量,那么数据将被封装。我的问题是,如果我们可以通过 getter 和 setter 访问变量(数据),为什么数据是隐藏的或安全的?
我用谷歌搜索了很多解释,但我什么也没找到。每个人都在他们的博客和帖子中说这是一种数据隐藏技术,但尚未解释/详细说明。
封装不仅仅是为类定义访问器和修改器方法。它是面向对象编程的一个更广泛的概念,包括最小化类之间的相互依赖关系,它通常通过信息隐藏来实现。
封装的美妙之处在于改变事物而不影响其用户的力量。
在像 Java 这样的面向对象编程语言中,您可以通过使用可访问性修饰符(public、protected、private,加上没有暗示包私有的修饰符)隐藏细节来实现封装。通过这些级别的可访问性,您可以控制封装级别,级别限制越少,更改发生时的成本就越高,并且类与其他依赖类(即用户类和子类)的耦合度越高。
因此,目标不是隐藏数据本身,而是如何操作这些数据的实现细节。
这个想法是提供一个公共接口,您可以通过该接口访问这些数据。您可以稍后更改数据的内部表示,而不会损害类的公共接口。相反,通过暴露数据本身,您损害了封装性,因此损害了在不影响其用户的情况下改变您处理数据的方式的能力。您创建对数据本身的依赖,而不是对类的公共接口。当“改变”最终找到你时,你会为麻烦调制一杯完美的鸡尾酒。
您可能想要封装对字段的访问有几个原因。Joshua Bloch 在他的《Effective Java 》一书中的第 14 条:最小化类和成员的可访问性中提到了几个令人信服的原因,我在此引用:
然而,封装不仅仅是隐藏字段。在 Java 中,您可以隐藏整个类,从而隐藏整个 API 的实现细节。例如,在方法中思考Arrays.asList()
。它返回一个List
实现,但你不关心哪个实现,只要它满足List
接口,对吧?将来可以更改实现而不影响该方法的用户。
现在,在我看来,要理解封装,首先必须理解抽象。
例如,考虑汽车概念的抽象级别。汽车的内部实现很复杂。它们有几个子系统,如传动系统、制动系统、燃油系统等。
然而,我们已经简化了它的抽象,我们通过它们抽象的公共接口与世界上所有的汽车进行交互。我们知道所有的汽车都有一个方向盘,通过它我们可以控制方向,它们有一个踏板,当你按下它时,你可以加速汽车并控制速度,还有一个当你按下它时,它就会停下来,你有一个档位可以让您控制前进或后退的棍子。这些特性构成了汽车抽象的公共接口。早上你可以开一辆轿车,然后下车,下午开一辆 SUV,就好像它是同一件事一样。
然而,我们中很少有人知道所有这些功能是如何在后台实现的。想想汽车没有液压方向系统的时代。有一天,汽车制造商发明了它,他们决定从那时起将其放入汽车中。尽管如此,这并没有改变用户与他们交互的方式。最多,用户体验到使用定向系统的改进。像这样的改变是可能的,因为汽车的内部实现是封装的。可以在不影响其公共接口的情况下安全地进行更改。
现在,想想汽车制造商决定把油箱盖放在汽车下面,而不是放在汽车的一侧。你去买一辆这样的新车,当你用完油时,你去加油站,却找不到油箱盖。突然你意识到它在汽车下方,但你无法用气泵软管到达它。现在,我们已经破坏了公共接口契约,因此,整个世界都崩溃了,它分崩离析,因为事情没有按照预期的方式工作。像这样的改变将花费数百万美元。我们需要更换世界上所有的气泵。当我们打破封装时,我们必须付出代价。
因此,如您所见,封装的目标是最大限度地减少相互依赖并促进变化。您可以通过最小化实现细节的暴露来最大化封装。类的状态只能通过其公共接口访问。
我真的建议您阅读 Alan Snyder 的一篇名为Encapsulation and Inheritance in Object-Oriented Programming Languages的论文。此链接指向 ACM 上的原始论文,但我很确定您可以通过 Google 找到 PDF 副本。
我理解您的问题的方式是,尽管我们将变量声明为private
,因为可以使用 getter 和 setter 访问这些变量,但它们不是私有的。因此,这样做的意义是什么?
好吧,当使用 getter 和 setter 时,您可以限制对private
变量的访问。
IE,
private int x;
public int getInt(String password){
if(password.equals("RealPassword")){
return x;
}
}
对于二传手也是如此。
数据是安全的,因为您可以在 getter / setter 中执行其他逻辑,并且无法更改变量的值。想象一下,您的代码不适用于 null 变量,因此在您的 setter 中,您可以检查 null 值并分配一个默认值,即 != null。因此,无论是否有人尝试将您的变量设置为 null,您的代码仍然有效。
我的问题是,如果我们可以通过 getter 和 setter 访问变量(数据),为什么数据是隐藏的或安全的?
您可以将逻辑封装在 getter/setter 下。例如,
public void setAge(int age) {
if (age < 0) {
this.age = 0;
}
else {
this.age = age;
}
}
继续 Jigar 的回答:有几件事需要封装。
合同管理:如果你成功了public
,你实际上是在让任何人将其更改为他们想要的任何东西。您无法通过添加约束来保护它。您的 setter 可以确保以适当的方式修改数据。
可变性:你不必总是有一个 setter。如果您想在对象的生命周期内保持不可变的属性。您只需将其设为私有并且没有设置器。它可能会通过构造函数设置。然后你的 getter 将只返回属性(如果它是不可变的)或属性的副本(如果属性是可变的)。
一般来说,getter 和 setter 对字段的封装为更改留下了更大的灵活性。
如果直接访问字段,您将陷入“愚蠢的字段”。字段只能写入和读取。访问字段时不能做任何其他事情。
使用方法时,您可以在设置/读取值时做任何您想做的事情。正如 markus 和 Jigar 所提到的,验证是可能的。此外,您可以决定某天该值是从另一个值派生的,或者如果该值发生更改,则必须执行某些操作。
数据是如何隐藏或安全的
通过使用 getter 和 setter,数据既不隐藏也不安全。它只是为您提供了使其安全的可能性。隐藏的是实现而不是数据。
如果存在访问器和/或突变器,数据验证是您关于封装如何提供安全性的问题的主要答案。其他人使用具有故障安全功能以在 mutator 中设置默认值的示例提到了这一点。你回答说你更喜欢抛出异常,这很好,但是当你去使用它时识别你有坏数据并不会改变你有坏数据的事实。因此,在修改数据之前捕获异常不是最好的吗,即在 mutator 中进行处理?这样,除非 mutator 验证它是有效的,否则实际数据永远不会被修改,因此在出现错误数据的情况下会保留原始数据。
我自己还只是一个学生,但是我和你第一次遇到封装时的情况一样,所以我花了一些时间弄清楚。
我喜欢考虑线程时的解释。如果您公开了您的字段,您的实例如何知道某个线程何时更改了它的一个字段?
做到这一点的唯一方法是封装,或者更简单的 put 为该字段提供 getter 和 setter,因此您始终知道并可以检查/响应字段更新,例如。
封装使代码更容易被其他人重用。使用封装的另一个关键原因是接口不能声明字段,但它们可以声明方法,方法可以引用一个字段!
方法在开头用动词正确命名。如:getName()、setName()、isDying()。哪位能帮忙看代码!