封装 vs 数据隐藏 - Java

61

面试官: 什么是封装,如何在Java中实现封装?

我: 封装是一种机制,用于隐藏信息不让客户端直接访问。该信息可以是数据、实现或算法。我们使用访问修饰符来实现这个目的。

面试官: 这是数据隐藏。除了使用访问修饰符,还有哪些方法可用于在Java中实现封装?

我: 嗯...

具体问题: 除了“访问修饰符”之外,在Java中实现封装的方法有哪些?


1
@ChanderShivdasani 这更具体地涉及到Java! - Sandeep Jindal
1
信息可以是数据、实现或算法。隐藏数据就是数据隐藏,但隐藏实现或算法不属于数据隐藏的定义。封装是一个目标,而数据隐藏是实现它的一种方式。但访问修饰符并不是实现封装的真正方法。 - Rajesh J Advani
12
总的来说,又是一个无聊的面试问题。这会对你的编程技能产生什么影响呢?我从不用这种吹毛求疵的问题来打扰我的面试者。尤其是因为没有一个 RFC 或 IEEE 标准确切地定义了封装的定义。不同的作者会使用不同的定义。 - Marko Topolnik
我想给这个+100分 :) - Sandeep Jindal
我认为这就是面试官有时试图进行“聪明”的面试时会发生的情况。这些概念(数据隐藏、封装、抽象)高度相关,几乎总是一起使用,过于拘泥于它们之间的细微差别并不能帮助区分好的候选人。你更想看到的是某人如何设计一个系统。 - forumulator
显示剩余2条评论
20个回答

47
更普遍地说,封装指的是将数据(例如对象的数据)与对该数据进行操作的方法捆绑在一起。因此,您可以使用一个类来封装数据-字段,以及用于操作数据的方法
但是封装有时也被用于与您的答案相同的方式,并且捆绑数据和方法的其中一个目的是隐藏实现。
我想一个更好的答案不仅仅是“使用方法并使所有字段私有”,而是:使用接口。这样,对象上的操作完全基于接口契约,并且与用于在内部实现契约的字段或辅助方法无关。

2
具体问题:除了访问修饰符,Java中实现封装的方式还有哪些? - Sandeep Jindal
1
答案非常简单:公开操作数据的方法。 - pb2q
谢谢。但是面试官希望得到更多的回答,我想确认这是否就是全部还是还有其他内容? - Sandeep Jindal
3
你怎么知道面试官期望的不仅仅是这个解释?这不是你的答案吗? - Chris Dargis
答案是:“封装是一种机制,用于隐藏某些信息不让客户端访问。这些信息可能是数据、实现或算法。我们如何使用访问修饰符来实现。”这个回答不完整吗?我认为这已经(隐含地)涵盖了使用方法暴露信息(数据、算法、实现等)。这不是完整的吗? - Sandeep Jindal

30

封装

一般来说,封装 就是将类似的项目绑定在一起。

例如,我们有一个 Student 类,其中我们将学生实例变量和对这些实例变量进行操作的行为/方法放置在一个地方。

为什么这很重要呢?因为我们不想让我们的代码分散在整个代码库中。

如果我们需要做出更改,那么我们需要在所有位置找到这些更改的变体。通过捆绑类似的项目,我们只是避免了这种情况,并且它还有助于使我们捆绑的代码更加聚焦。

数据隐藏

它提供了一种保护您的数据免受外部世界侵害的方式。这意味着,假设我将我的实例变量设置为公共属性,那么任何人都可以更改其状态。但是如果我们将我们的实例变量设置为私有/受保护的,则实际上我们正在限制外部实体对其进行更改。

比较/讨论

现在问题是,在什么方面我们正在保护我们的变量?

再次说明,封装 只是我们需要将类似的项目放置的容器。

它就像一个 黑匣子 对外部世界。外部世界(我指使用我们的 Student 类的客户端/消费者)不知道 Student 类的内部详细信息/实现详细信息,实际上他们不应该关心类的内部详细信息/实现详细信息。他们只想要一些方法/API,以便他们可以在他们的客户端应用程序中使用它们。

因此,我的观点是,所有与学生有关的行为/变量都放置在一个我们称之为类的 黑匣子 中。现在由类的设计者决定对于外部世界而言哪些元素应该隐藏,哪些应该对外公开。

现在回到问题,在Java中:我们将变量私有化,这意味着它们是类受保护的。如果我们希望我们的实例变量在整个包中都可以访问,则它们是包受保护的。在整个项目中它们是公共的。所以我想说的是,要隐藏数据,您需要一些容器来放置您的数据并相对于容器进行隐藏。

因此,我认为数据隐藏没有封装就不可能实现。您不能隐藏数据而不将其放入某种容器中。再次提醒您,我正在将其放在面向对象语言的上下文中。

但是,是的,封装可以在不隐藏数据的情况下实现。将所有内容设为公共,并可以看到影响。


有格式问题,现在已经修复。请查看如何使用Markdown或HTML格式化帖子?获取更多信息。 - Sufian
它只是对外部世界起到黑盒子的作用,这实际上是指抽象化,而不是封装。 - raviraja

12

封装:以胶囊为例。如果你打开它,里面包含很多成分。面向对象编程的封装也是如此。正如其名称所示,“封装”意味着将所有数据成员、属性和相应方法封装在一个单一的胶囊内。

如何实现:假设你创建了一个名为“Car”的类。现在汽车有颜色、价格和型号,这些都是属性,它还有一个“run”方法。因此,你已经将命名为“Car”的车辆的所有这些属性和方法封装在一起。当你创建车的实例时,就像这样:

Car myCar = new Car();
你可以使用myCar变量访问Car的所有属性。
“数据隐藏”:在Java中,数据隐藏是由访问修饰符控制的。要访问数据成员,您使用ACCESSORS,而要修改数据,您使用“Mutators”。Java本身不提供访问者和修改器(getter和setter)。而C#提供了属性来完成此操作。

8
我不喜欢这样做,但是根据维基百科的解释,在编程语言中,封装用于指代两个相关但不同的概念,有时也指它们的组合:
1. 一种限制对象某些组件访问的语言机制。 2. 一种语言结构,方便将数据与操作该数据的方法(或其他函数)捆绑在一起。
你的解释更接近第一个概念,面试官想要了解的是第二个概念。

3

数据隐藏意味着我们在类内提供对数据的安全保护。

抽象化是通过定义成员函数来隐藏代码。

封装是抽象化和数据隐藏的结合,意味着我们将数据及其相关代码包装起来。例如,bean类。

    class student {
private int age;  // Data Hiding 
public setAge(){} // Abstraction 
public getAge(){} // Abstraction 
}

学生类被封装。

封装 = 数据隐藏 + 抽象化


3
问题似乎有误导性,我怀疑除了面试官以外没有人能够回答这个问题,而且他/她的答案很可能是错误的。最重要的是,不清楚这个问题应该评估或发现什么。
不过,我仍然思考了一下,以下是我认为可能是我的回答尝试。
使用访问修饰符
在Java中,封装是通过使用可访问性修饰符(即public、protected、private等)来隐藏细节实现的。使用这些可访问性级别,您可以控制信息隐藏的级别。级别越不限制,改变发生时成本越高,并且类与其他依赖类(即用户类、子类)的耦合度越高。
显然,封装不仅仅是隐藏状态。在Java中,您可以通过隐藏整个类和接口及其状态和行为来隐藏整个API的实现细节。
例如,方法Arrays.asList()返回一个List实现,但我们并不关心哪个实现,只要它满足List公共接口就可以了,对吧?实现可以在将来更改而不影响我们,方法的用户,但实际实现对我们是隐藏的。
到目前为止,似乎封装完全取决于编程语言隐藏细节的能力,因此,如果没有访问修饰符,就无法实现,对吧?
不使用访问修饰符
然而,当没有访问修饰符的语言如Python如何实现封装?在Python中什么都是公开的吗?那意味着封装是不可能的吗?
如果按照惯例,我们定义组件的公共接口,然后仅通过其公共接口访问对象的状态和行为怎么样?显然,为此,我们需要清楚理解问题域中的抽象概念以及这些概念应该如何被用户使用。
对我来说,似乎面试问题旨在评估封装作为更广泛概念,这一概念取决于非常明确的抽象定义,而不仅仅是像访问修饰符这样的语言特性的副产品。
抽象的角色
这就是为什么,在我看来,要真正理解封装,必须首先理解抽象。例如,考虑汽车概念中的抽象级别。汽车在其内部实现方面很复杂。它们有几个子系统,如传动系统、刹车系统、燃油系统等。
然而,我们已经简化了它的抽象,通过它们的抽象的公共接口与全世界所有汽车进行交互。我们知道所有汽车都有一个方向盘,通过它我们控制方向,它们有一个踏板,当你按下它时加速汽车并控制速度,还有另一个踏板,当你按下它时让它停下来,你还有一个变速杆,可以控制前进或后退。这些功能构成了汽车抽象的公共接口。早上你可以开一辆轿车,然后下午再开一辆SUV,好像是同一件事情。
并不是说你不能打开引擎盖看看它是如何运作的。但是,我们中很少有人知道所有这些基本功能的详细信息,事实上,我们不需要知道这些细节就能开车。所有这些东西都在汽车抽象下封装起来。我们只需要知道抽象的公共接口。
想一想汽车没有液压方向系统的时候。有一天,汽车制造商发明了它,并决定从那时起将其放入汽车中。然而,这并没有改变用户与其交互的方式。最多,用户体验到了方向系统使用的改进。这样的改变是可能的,因为汽车的内部实现被封装起来了。
这清楚地证明了通过隐藏方向系统实现细节,他们可以安全地更改它,而不影响汽车的公共接口,因此也不会影响用户与其交互的方式。
现在,想象一下汽车制造商决定将加油口放在汽车下面,而不是在其中一个侧面。你去买一辆这样的新车,当你用完油后去加油站时,你找不到加油口。突然间,你意识到它在汽车底部,但你无法用加油泵软管够到它。现在,我们打破了公共接口契约,因此,整个世界都崩溃了,因为事情的运作方式并不像预期的那样。这样的变化将花费数百万美元。我们需要改变全世界所有加油站的加油泵。当我们打破封装性时,我们必须付出代价。
因此,正如您所看到的,封装的目标是最小化相互依赖并促进变化。通过最小化实现细节的暴露,您可以最大化封装。类的状态应仅通过其公共接口访问。
封装的美妙之处在于“在不影响其用户的情况下更改事物的能力”。
我们可以说,这个最终目标取决于仔细的规划和设计。在Java中,访问修饰符是实现这些想法的一种方式,但在没有此功能的语言中,同样可以实现封装。

访问修饰符在Java中并不控制访问权限,如果需要,您可以随时通过反射访问私有成员。因此,在这方面它与Python没有区别。 - Esailija
@Esailija 让我看看我是否理解了这个“访问”修饰符在Java中“不控制访问”。这相当矛盾。在Python中,您可以完全访问所有对象成员。在Java中,您必须使用反射来规避访问修饰符,这意味着您想故意违反封装,而不是出于错误(如您可以在Python中做的那样)。因此,反射是一种API机制,而不是语言机制。如果您正在使用安全管理器,则不允许您打破修饰符,因为Java反射会给您异常。 - Edwin Dalorzo
所以让我来重新表述一下:Python 的方式已经足够防止任何意外访问内部,正如作者通过下划线等方式所传达的那样。实际上,没有任何合理的情况可以使某人在诚实的意外情况下绕过此系统。如果意图是故意的,那么无论是必须加前缀下划线还是使用反射(这可以通过库方便地实现),都无关紧要。 - Esailija
我实际上读完了你的帖子,它很不错。但是现在我无法将我的踩转为赞,因为它被锁定了 :((需要编辑才能解锁)。当你似乎认为Java强制执行隐私并使Python看起来更像黑客时,我只是不得不停止阅读。 - Esailija
在所有上面发布的答案中,这个答案提出了最清晰的“封装”概念。我长时间未登录,只是为了点赞这个答案而登录。+1。 - Vishal K
显示剩余2条评论

3
数据封装是指将所有相关属性和方法保存在单个实体中的机制。
例如:一辆汽车。它将方向盘、轮胎、发动机和所有相关的部件组合成一个集体实体,称为“汽车”。
在编程语言中,使用类来实现封装。类包含所有属性和相关方法,设计成一个用于执行特定任务的单个实体。
数据隐藏是指隐藏对用户不重要的细节,只显示相关数据。
例如:当我们在汽车上踩刹车或油门时,我们不知道幕后发生了什么(如何增加速度或如何将刹车施加到轮胎上)。我们只知道有刹车和油门可以产生所需的结果。
在编程中,使用访问修饰符来实现此目的。私有成员无法从类外部访问,只有公共成员可供用户访问。私有成员只能从类的成员中访问,从而为私有成员提供直接评估的安全性,以避免从类的外部进行访问。

1
在上述语句中,“数据隐藏是指从用户隐藏不重要的细节,仅向其显示相关数据”,我认为你谈论的是抽象化,而不是数据隐藏。 - Rahul Rastogi

2
我认为,封装是指将数据和方法绑定到一个称为单个单元中的思想。然而,有两个主要标准来绑定数据和方法。其中之一是信息隐藏,但不是唯一的标准。为了简洁明了,我会说这两个主要标准是:
  1. 低耦合(通过信息隐藏来确保)
  2. 高内聚性

+1,这需要更高。这是一种比BundlingAccess modifiers更为替代的封装视角。我会进一步扩展它,认为客户端代码与数据本身之间应具有低耦合性,并且在该胶囊中,数据状态应具有高内聚性或一致性。 - forumulator

1
最简单的封装定义是“将操作数据的数据和代码绑定到一个单元中”,这样数据就不能被外部直接访问。
在结构化语言(如C语言)中,数据在块的开头声明并直接使用,而且可以公开访问(任何人都可以访问数据)。数据隐藏是一种限制数据直接、公开访问的概念。 封装是实现数据隐藏的一种方式。通过将数据和函数组合在一起(封装),可以实现数据隐藏。
在JAVA中如何实现封装,请参考JAVA中任何getter()、setter()函数示例。请查看此链接How do getters and setters work?

1
在类中,属性和行为被放置在一起。封装意味着当你创建一个类时,这本身就意味着你实现了封装原则。因为数据和行为被封装到类中。所以数据隐藏是封装术语的重要部分。这意味着对象的访问点。

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