【问题标题】:Java encapsulation [duplicate]Java封装[重复]
【发布时间】:2012-08-11 14:21:39
【问题描述】:

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

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

【问题讨论】:

  • 排除 getter 和 setter 本身的检查?
  • 你也可以看看这个:encapsulation

标签: java oop jakarta-ee encapsulation


【解决方案1】:

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

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

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

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

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

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

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

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

封装之美

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

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

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

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

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

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

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

【讨论】:

  • 反响很好。特别是当您在这 3 行中总结您的 cmets 时“所以,正如您所看到的,封装的目标是最小化相互依赖并促进更改。您通过最小化实现细节的暴露来最大化封装。类的状态应该只通过其公共接口访问。” ..干得好!
  • 很好解释。但是,我不明白你说的一件事:"The beauty of encapsulation is the power of changing things without affecting its users." 现在,以java 的上下文来看,这里的"users" 是什么?这是否意味着java应用程序的end users?这是否意味着part of java code or module 将使用公共接口?这是否意味着java programmers 将引用或调用这些公共接口?
  • @oblivion 这意味着你的 API 的用户,实际使用它来解决问题的人。
  • @Lealo,很难对此发表评论。我的建议是你提出一个关于它的问题并参考这篇文章,这样你就可以获得广泛的答案。
【解决方案2】:

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

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

即,

private int x;

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

二传手也是如此。

【讨论】:

  • 是的,它有帮助,这是一个很好的例子……但通常我们不会将值传递给 getter,而是获取返回的值。它不违反约定吗?还是不会篡改封装属性?
  • (没有/我没听说过)这样的约定是不将参数值传递给getter! (它们是参数值而不是显式值)并且就 setter 而言,您可以将多个参数传递给方法。
  • 还有一种使用ObjectProperty<intstancevariabel>的封装方式,我在javafx.scene.Scene类等地方看到过(查看私有实例变量@987654325 @ 在Scene 类中)。我不完全了解它的好处或它的使用方式,但我可以肯定这样做有一些好处。
  • 我不认为这是一个很好的例子。 getter 和 setter 应该获取和设置,仅此而已。而且您的示例无法编译 - 如果它不是真正的密码会发生什么?你返回 0 还是 -1?调用者如何知道没有返回真正的值?
【解决方案3】:

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

【讨论】:

    【解决方案4】:

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

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

    public void setAge(int age) {
        if (age < 0) {
            this.age = 0;
        }
        else {
            this.age = age;
        }
    }
    

    【讨论】:

    • 这是唯一的解释吗?
    • 虽然我在示例中明白了要点,但事实是,在这种情况下,我宁愿抛出 IllegalArgumentException 而不是默默地将年龄设置为 0。任何将年龄设置为负数的尝试value 显然是一个错误,应该尽快显示出来,但有例外。你不同意吗?
    • 这只是一个说明我们如何封装这些东西,它取决于应用程序业务逻辑是引发异常还是忽略传递的值并设置为默认值
    【解决方案5】:

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

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

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

    【讨论】:

      【解决方案6】:

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

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

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

      为什么数据是隐藏的或安全的

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

      【讨论】:

        【解决方案7】:

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

        我自己还是个学生,不过我和你第一次接触封装的时候和你一样,所以我花了一些时间弄清楚。

        【讨论】:

          【解决方案8】:

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

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

          【讨论】:

            【解决方案9】:

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

            方法以动词开头。如:getName()、setName()、isDying()。哪位可以帮助阅读代码!

            【讨论】:

              猜你喜欢
              • 2012-08-11
              • 2018-09-20
              • 2011-03-20
              • 2016-11-03
              • 1970-01-01
              • 2016-05-08
              • 2011-06-14
              • 1970-01-01
              相关资源
              最近更新 更多