Java封装

22

我们总是说,如果我们仅仅将变量定义为private 并定义 getters 和 setters 来访问这些变量,那么数据会被封装起来。我的问题是,如果我们可以通过 getters 和 setters 访问变量(数据),那么数据如何被隐藏或保护?

我在网上搜了很多解释,但什么都没找到。每个人在他们的博客和文章中都只说这是一种数据隐藏技术,但并没有解释或详细说明。


在getter和setter本身中排除检查? - geometrian
你也可以看一下这个:封装 - John Alexander Betts
9个回答

62

封装不仅仅是为类定义存取器和修改器方法。它是面向对象编程的更广泛概念,旨在最小化类之间的相互依赖,通常通过信息隐藏实现。

封装的美妙之处在于可以在不影响用户的情况下进行更改

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

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

关键是提供公共接口来访问这些数据。稍后,您可以更改数据的内部表示,而不会影响类的公共接口。相反,通过暴露数据本身,您会破坏封装性,从而无法更改操作数据的方式,而不影响其用户。您会创造出对数据本身的依赖,而不是对类的公共接口的依赖。当“更改”最终找到您时,您将创造出一个完美的麻烦混合物。

有几个原因可能会促使您封装对字段的访问。约书亚·布洛赫在他的书中Effective Java第14项:最小化类和成员的可访问性中提到了几个令人信服的原因,我在这里引用:

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

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

封装的美

现在,我认为要理解封装,首先必须理解抽象。例如,在汽车的概念中,抽象化的级别是什么。汽车在其内部实现上非常复杂。它们有几个子系统,如传动系统、制动系统、燃油系统等等。
然而,我们已经简化了它的抽象,并通过其抽象的公共接口与世界上所有汽车进行交互。我们知道所有的汽车都有一个方向盘,通过它我们控制方向;当你踩下踏板时,汽车就会加速并控制速度;另外还有一个踏板可以使它停下来,还有一个变速杆可以让你控制前进或后退。这些特征构成了汽车抽象的公共接口。早上你可以开一辆轿车,下午再开一辆SUV,就好像它们是同样的东西。
然而,我们中的很少人知道所有这些功能是如何在幕后实现的细节。想想汽车没有液压定向系统的时候。有一天,汽车制造商发明了它,并决定从那时起将其放入汽车中。尽管如此,这并没有改变用户与其交互的方式。最多,用户体验到了定向系统使用的改进。像这样的更改是可能的,因为汽车的内部实现是封装的。可以安全地进行更改,而不影响其公共接口。
现在,想象一下汽车制造商决定将燃油加注口放在汽车下方,而不是其中的一侧。你去买一辆这样的新车,当你用完油后去加油站时,你找不到加油口。突然间你意识到它在汽车下方,但你无法用加油枪软管够到它。现在,我们已经破坏了公共接口合同,因此整个世界都崩溃了,因为事情不按预期工作。这样的更改将耗费数百万美元。我们需要更改全球所有的加油泵。当我们打破封装时,我们必须付出代价。
因此,正如您所看到的,封装的目标是最小化相互依赖并促进更改。通过最小化实现细节的公开,可以最大限度地提高封装性。类的状态只能通过其公共接口访问。
我真的建议您阅读Alan Snyder的一篇论文,名为面向对象编程语言中的封装和继承。这个链接指向ACM上的原始论文,但我确信您能够通过谷歌找到PDF副本。

8
很好的回复。特别是当您用这三行总结您的评论时:“所以,正如您所看到的,封装的目标是最小化相互依赖并促进更改。通过最小化实现细节的公开露出来最大化封装。类的状态只能通过其公共接口进行访问。”..做得很好! - Just_another_developer
1
@oblivion 指的是您 API 的用户,那些实际使用它来解决问题的人。 - Edwin Dalorzo
2
@oblivion 你可能也想阅读抽象、封装和信息隐藏 - Edwin Dalorzo
请问您能否详细说明一下javafx.scene.Scene类中的私有实例变量 javafx.scene.Camera?通过将实际实例变量内容放在一个 javafx.beans.property.ObjectProperty 中,会增加一个额外的抽象/封装层。这种方式使得 Setter/Getter 方法看起来像是野蛮行为。除了能够添加监听器之外,是否还有其他原因可以使用它呢?在什么情况下使用它会更有利,而不是使用“简单”的Setter/Getter方法呢? - Lealo
1
@Lealo,仅仅评论这个问题是非常困难的。我的建议是你开一个关于这个问题的提问,并引用这篇文章,这样你就可以得到一个详细的答案。 - Edwin Dalorzo
显示剩余6条评论

16
我理解你的问题是,尽管我们将变量声明为“private”,但由于可以使用getter和setter访问这些变量,它们并不是私有的。那么这样做的意义是什么呢?
好的,使用getter和setter时,您可以限制对“private”变量的访问。
也就是说,
private int x;

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

对于setter方法同样适用。


1
是的,它很有帮助,这是一个很好的例子...但通常我们不会将值传递给getter,而是获取返回的值。这难道没有破坏约定吗?或者它难道没有伪造封装属性吗? - Just_another_developer
3
没有不允许向getter传递参数值的规定!(这些是参数值而不是显式值)。至于setter,您可以向该方法传递多个参数。 - mithilatw
还有一种封装方式是使用ObjectProperty<intstancevariabel>,我在javafx.scene.Scene类中以及其他地方看到过(查看Scene类中的私有实例变量camera)。我并不完全理解它的好处或者如何使用它,但我肯定能感觉到这样做有一些好处。 - Lealo
我认为这不是一个好的例子。getter和setter应该只用于获取和设置,其他操作不应该在其中进行。而且你的例子无法编译 - 如果它不是真正的密码会发生什么?你会返回0还是-1?调用者如何知道真实值没有被返回? - pecks

13

数据是安全的,因为你可以在getter / setter中执行其他逻辑,并且不可能更改变量的值。想象一下你的代码不能使用空变量,所以在设置器中,你可以检查空值并分配一个默认值,该值!= null。因此,无论是否有人尝试将您的变量设置为null,您的代码仍将正常工作。


5
我的问题是,如果我们可以通过getter和setter访问变量(数据),那么为什么数据会被隐藏或保护?
您可以通过封装getter/setter下的逻辑来实现数据的隐藏和保护。例如:
public void setAge(int age) {
    if (age < 0) {
        this.age = 0;
    }
    else {
        this.age = age;
    }
}

这是唯一的解释吗? - Just_another_developer
尽管我理解示例中的观点,但实际上,在这种情况下,我更愿意抛出一个非法参数异常(IllegalArgumentException),而不是将年龄静默设置为0。任何试图将年龄设为负值的尝试显然都是错误的,并且应该尽早通过异常显示出来。你同意吗? - Edwin Dalorzo
1
这只是一个示例,说明我们如何封装内容。具体是否引发异常或忽略传递的值并设置为默认值,取决于应用程序业务逻辑。 - jmj

4

继续Jigar的回答:封装涉及到几个方面。

  1. 合同管理:如果你将其设为public,那么任何人都可以随意更改它。你无法通过添加约束来保护它。你的setter可以确保数据以适当的方式被修改。

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


3

一般来说,通过getter和setter封装字段可以为更改留下更大的灵活性。

如果直接访问字段,则会陷入“愚蠢的字段”的困境。只能写入和读取字段,不能在访问字段时进行其他操作。

使用方法可以在设置/读取值时做任何您想做的事情。正如Markus和Jigar提到的,验证是可能的。此外,您可能会决定某天该值是由另一个值派生的或者在值发生变化时执行某些操作。

为什么数据被隐藏或安全了?

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


2
数据验证是关于封装如何在访问器和/或修改器存在的情况下提供安全性的主要答案。其他人已经使用在修改器中设置默认值的故障保护示例来提到了这一点。您回复说您更喜欢抛出异常,这很好,但是识别出在使用数据时存在错误数据并不能改变事实。因此,在修改数据之前捕获异常是否最好,也就是在修改器中进行处理?这样,除非修改器已将其验证为有效,否则实际数据永远不会被修改,因此原始数据在出现错误数据的情况下得以保留。
我仍然只是一个学生,但当我第一次接触封装时,我也有完全相同的想法,所以我花了一些时间来弄清楚它。

1

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

唯一的方法是使用封装,或者更简单地说,为该字段提供getter和setter,这样您就可以始终知道并可以检查/响应字段更新,例如。


0

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

方法应以动词开头进行适当命名。例如:getName(),setName(),isDying()。这有助于阅读代码!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接