关于oop:Java Encapsulation

Java Encapsulation

本问题已经有最佳答案,请猛点这里访问。

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

我在谷歌上搜索了很多解释,但一无所获。每个人在他们的博客和帖子中都说这是一种数据隐藏技术,但还没有解释/阐述它。

期待在StackOverflow的论坛上得到适当、令人满意的解释。


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

封装的美妙之处在于能够在不影响用户的情况下改变事物。好的。

在像Java这样的面向对象编程语言中,通过使用可访问性修改器(公共的、受保护的、私有的、加上没有修改意味着包私有)的细节来实现封装。通过这些可访问性级别,您可以控制封装级别,限制级别越少,发生更改的代价越高,并且类与其他依赖类(即用户类、子类)的耦合程度越高。好的。

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

其思想是提供一个公共接口,通过它您可以访问这些数据。稍后您可以更改数据的内部表示,而不影响类的公共接口。相反,通过公开数据本身,会损害封装,从而影响在不影响数据用户的情况下更改数据操作方式的能力。您创建了对数据本身的依赖关系,而不是与类的公共接口的依赖关系。当"改变"最终找到你时,你会为麻烦创造一个完美的鸡尾酒。好的。

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

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

然而,封装不仅仅是隐藏字段。在爪哇中,可以隐藏整个类,由此隐藏整个API的实现细节。例如,用Arrays.asList()的方法思考。它返回一个List实现,但您不关心哪个实现,只要它满足List接口,对吗?。在不影响方法用户的情况下,可以在将来更改实现。好的。封装之美

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

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

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

然而,很少有人知道这些特性是如何在引擎盖下实现的。想想那个时候,汽车没有液压定向系统。有一天,汽车制造商发明了它,他们决定把它装进汽车里。不过,这并没有改变用户与他们交互的方式。最多,用户在使用定向系统方面经历了改进。这样的改变是可能的,因为汽车的内部实现是封装的。可以在不影响公共接口的情况下安全地进行更改。好的。

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

因此,正如您所看到的,封装的目标是最小化相互依赖性并促进更改。您可以通过最小化实现细节的暴露来最大化封装。类的状态只能通过其公共接口访问。好的。

我真的建议你读一篇由艾伦·斯奈德写的论文,叫做面向对象编程语言中的封装和继承。这个链接指向ACM上的原始文件,但我很确定你可以通过谷歌找到一份PDF副本。好的。好啊。


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

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

1
2
3
4
5
6
7
private int x;

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

对二传手也是如此。希望这有帮助!


数据是安全的,因为您可以在getter/setter中执行额外的逻辑,并且不可能更改变量的值。假设您的代码不使用空变量,那么在setter中,您可以检查空值并指定一个默认值,即!= NULL。因此,不管是否有人试图将变量设置为空,您的代码仍在工作。


My question is if we can access the variables( data) though via getters and setters, how come data is hidden or safe?

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

1
2
3
4
5
6
7
public void setAge(int age){
 if(age < 0){
  this.age = 0;
 }else{
  this.age = age;
 }
}


一般来说,getter和setter对字段进行封装会给更改带来更多的灵活性。

如果直接访问字段,您将被困在"愚蠢的字段"中。只能写入和读取字段。访问字段时无法执行任何其他操作。

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

how come data is hidden or safe

使用getter和setter既不隐藏数据,也不安全。它只是给了你安全的可能性。隐藏的是实现,而不是数据。


继续Jigar的回答。有一些东西是封装的。

  • 合同管理:如果你制作ITpublic,你实际上是让任何人把它改成他们想要的任何东西。您不能通过添加约束来保护它。您的setter可以确保以适当的方式修改数据。

  • 易变性:你不必总是有一个setter。如果有一个属性要在对象的生命周期内保持不变。你只是让它成为私人的,没有设置者。它可能通过构造函数设置。然后getter只返回属性(如果它是不可变的)或属性的副本(如果属性是可变的)。


  • 数据验证是关于封装如何在有访问器和/或变异器的情况下提供安全性的问题的主要答案。其他人已经提到了这一点,他们使用了一个具有故障保护功能的例子来在mutator中设置默认值。您回答说,您更喜欢抛出异常,这是很好的,但是在您使用时识别出您有坏数据不会改变您有坏数据的事实。因此,是否最好在修改数据之前捕获异常,也就是说,在mutator中捕获异常?这样,实际数据就永远不会被修改,除非调换器已经验证它是有效的,因此在出现坏数据时,原始数据会被保留。

    我自己还是一个学生,但是我和你一样,当我第一次遇到封装的时候,我也有同样的想法,所以我花了一些时间来搞清楚。


    我喜欢考虑线程时的解释。如果将字段公开,实例如何知道某个线程何时更改了其中一个字段?唯一的方法是使用封装,或者更简单地将getter和setter赋给该字段,因此您总是知道并可以检查/响应字段更新。


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

    方法的名称正确,开头有一个动词。例如:getname()、setname()、isding()。这有助于读取代码!