第07部分:数据隐藏和封装

类由一些数据和方法组成。目前,我们尚未说明的最重要的面向对象技术之一是,把数据隐藏在类中,只能通过方法获取。这种技术叫作封装(encapsulation),因为它把数据(和内部方法)安全地密封在类这个“容器”中,只能由可信的用户(即这个类中的方法)访问。

为什么要这么做呢?最重要的原因是,隐藏类的内部实现细节。如果避免让程序员依赖这些细节,你就可以放心地修改实现,而无需担心会破坏使用这个类的现有代码。

你应该始终封装自己的代码。如果没有封装好,那么几乎无法推知并最终确认代码是否正确,尤其是在多线程环境中(而基本上所有 Java 程序都运行在多线程环境中)。

使用封装的另一个原因是保护类,避免有意或无意做了糊涂事。类中经常包含一些相互依赖的字段,而且这些字段的状态必须始终如一。如果允许程序员(包括你自己)直接操作这些字段,修改某个字段后可能不会修改重要的相关字段,那么类的状态就前后不一致了。然而,如果必须调用方法才能修改字段,那么这个方法可以做一切所需的措施,确保状态一致。类似地,如果类中定义的某些方法仅供内部使用,隐藏这些方法能避免这个类的用户调用这些方法。

封装还可以这样理解:把类的数据都隐藏后,方法就是在这个类的对象上能执行的唯一一种可能的操作。

只要小心测试和调试方法,就可以认为类能按预期的方式运行。然而,如果类的所有字段都可以直接操作,那么要测试的可能性根本数不完。

隐藏类的字段和方法还有一些次要的原因:

• 如果内部字段和方法在外部可见,会弄乱类的 API。让可见的字段尽量少,可以保持类的整洁,从而更易于使用和理解。

• 如果方法对类的使用者可见,就必须为其编写文档。把方法隐藏起来,可以节省时间和精力。




访问控制

Java 定义了一些访问控制规则,可以禁止类的成员在类外部使用。在一些示例中,你已经见过字段和方法声明中使用的 public 修饰符。这个 public 关键字,连同 protected和 private(还有一个特殊的),是访问控制修饰符,为字段或方法指定访问规则。

1. 访问包

Java 语言不直接支持包的访问控制。访问控制一般在类和类的成员这些层级完成。

已经加载的包始终可以被同一个包中的代码访问。一个包在其他包中是否能访问,取决于这个包在宿主系统中的部署方式。例如,如果组成包的类文件存储在一个目录中,那么用户必须能访问这个目录和其中的文件才能访问包。

2. 访问类

默认情况下,顶层类在定义它的包中可以访问。不过,如果顶层类声明为 public,那么在任何地方都能访问。

嵌套类是定义为其他类的成员的类。因为这种内部类是某个类的成员,因此也遵守成员的访问控制规则。

3. 访问成员

类的成员在类的主体里始终可以访问。默认情况下,在定义这个类的包中也可以访问成员。这种默认的访问等级一般叫作包访问。这只是四个可用的访问等级中的一个。其他三个等级使用 public、protected 和 private 修饰符定义。下面是使用这三个修饰符的示例代码:

下述访问规则适用于类的成员:

1    类中的所有字段和方法在类的主体里始终可以使用。

2    如果类的成员使用 public 修饰符声明,那么可以在能访问这个类的任何地方访问这个成员。这是限制最松的访问控制类型。

3    如果类的成员声明为 private,那么除了在类内部之外,其他地方都不能访问这个成员。这是限制最严的访问控制类型。

4    如果类的成员声明为 protected,那么包里的所有类都能访问这个成员(等同于默认的包访问规则),而且在这个类的任何子类的主体中也能访问这个成员,而不管子类在哪个包中定义。

5    如果声明类的成员时没使用任何修饰符,那么使用默认的访问规则(有时叫包访问),包中的所有类都能访问这个成员,但在包外部不能访问。

默认的访问规则比 protected 严格,因为默认规则不允许在包外部的子类中访问成员。

使用 protected 修饰的成员时要格外小心。假设 A 类使用 protected 声明了一个字段 x,而且在另一个包中定义的 B 类继承 A 类(重点是 B 类在另一包中定义)。因此,B 类继承了这个 protected 声明的字段 x,那么,在 B 类的代码中可以访问当前实例的这个字段,而且引用 B 类实例的代码也能访问这个字段。但是,这并不意味着在 B 类的代码中能读取任何一个 A 类实例的受保护字段。

下面通过代码讲解这个语言细节。A 类的定义如下:

B 类的定义如下:

Java 的包不能“嵌套”,所以 javanut6.ch03.different 和 javanut6.ch03是 不 同 的 包。javanut6.ch03.different 不 以 任 何 方 式 包 含 在 javanut6.ch03 中,也和 javanut6.ch03 没有任何关系。

可是,如果我们试图把下面这个新方法添加到 B 类中,会导致编译出错,因为 B 类的实例无法访问任何一个 A 类的实例:

如果把这个方法改成:

就能编译通过,因为同一类型的多个实例可以访问各自的 protected 字段。当然,如果 B类和 A 类在同一包中,那么任何一个 B 类的实例都能访问任何一个 A 类实例的全部受保护字段,因为使用 protected 声明的字段对同一个包中的每个类都可见。

4. 访问控制和继承

Java 规范规定:

1    子类继承超类中所有可以访问的实例字段和实例方法;

2     如果子类和超类在同一个包中定义,那么子类继承所有没使用 private 声明的实例字段和方法;

3    如果子类在其他包中定义,那么它继承所有使用 protected 和 public 声明的实例字段和方法;

4    使用 private 声明的字段和方法绝不会被继承;类字段和类方法也一样;

5    构造方法不会被继承(而是链在一起调用,本章前面已经说过)。

不过,有些程序员会对“子类不继承超类中不可访问的字段和方法”感到困惑。这似乎暗示了,创建子类的实例时不会为超类中使用 private 声明的字段分配内存。然而,这不是上述规定想表述的。

其实,子类的每个实例都包含一个完整的超类实例,其中包括所有不可访问的字段和方法。

某些成员可能无法访问,这似乎和类的成员在类的主体中始终可以访问相矛盾。为了避免误解,我们要使用“继承的成员”表示那些可以访问的超类成员。

那么,关于成员访问性的正确表述应该是:“所有继承的成员和所有在类中定义的成员都是可以访问的。”这句话还可以换种方式说:

1    类继承超类的所有实例字段和实例方法(但不继承构造方法);

2    在类的主体中始终可以访问这个类定义的所有字段和方法,而且还可以访问继承自超类的可访问的字段和方法。


5. 成员访问规则总结

下面是一些使用可见性修饰符的经验法则:

1    只使用 public 声明组成类的公开 API 的方法和常量。使用 public 声明的字段只能是常量和不能修改的对象,而且必须同时使用 final 声明。

2    使用 protected 声明大多数使用这个类的程序员不会用到的字段和方法,但在其他包中定义子类时可能会用到。

严格来说,使用 protected 声明的成员是类公开 API 的一部分,必须为其编写文档,而且不能轻易修改,以防破坏依赖这些成员的代码。

3    如果字段和方法供类的内部实现细节使用,但是同一个包中协作的类也要使用,那么就使用默认的包可见性。

4    使用 private 声明只在类内部使用,在其他地方都要隐藏的字段和方法。

如果不确定该使用 protected、包还是 private 可见性,那么先使用 private。如果太过严格,可以稍微放松访问限制(如果是字段的话,还可以提供访问器方法)。

设计 API 时这么做尤其重要,因为提高访问限制是不向后兼容的改动,可能会破坏依赖成员访问性的代码。



数据访问器方法

在 Circle 类那个示例中,我们使用 public 声明表示圆半径的字段。Circle 类可能有很好的理由让这个字段可以公开访问;这个类很简单,字段之间不相互依赖。但是,当前实现的 Circle 类允许对象的半径为负数,而半径为负数的圆肯定不存在。可是,只要半径存储在声明为 public 的字段中,任何程序员都能把这个字段的值设为任何想要的值,而不管这个值有多么不合理。唯一的办法是限制程序员,不让他们直接访问这个字段,然后定义public 方法,间接访问这个字段。提供 public 方法读写字段和把字段本身声明为 public不是一回事。目前而言,二者的区别是,方法可以检查错误。

例如,我们或许不想让 Circle 对象的半径使用负数——负数显然不合理,但目前的实现没有阻止这么做。示例 3-4 展示了如何修改 Circle 类的定义,避免把半径设为负数。

Circle 类的这个版本使用 protected 声明 r 字段,还定义了访问器方法 getRadius() 和setRadius(),用于读写这个字段的值,而且限制半径不能为负数。r 字段使用 protected声明,所以可以在子类中直接(且高效地)访问。

使用数据隐藏和封装技术定义的 Circle 类:

我们在一个名为 shapes 的包中定义 Circle 类。因为 r 字段使用 protected 声明,所以shapes 包中的任何其他类都能直接访问这个字段,而且能把它设为任何值。这里假设shapes 包中的所有类都由同一个作者或者协作的多个作者编写,而且包中的类相互信任,不会滥用拥有的访问权限影响彼此的实现细节。

最后,限制半径不能使用负数的代码在一个使用 protected 声明的方法中,这个方法是checkRadius()。虽然 Circle 类的用户无法调用这个方法,但这个类的子类可以调用,而且如果想修改对半径的限制,还可以覆盖这个方法。

在 Java 中,数据访问器方法的命名有个通用约定,即以“get”和“set”开头。但是,如果要访问的字段是 boolean 类型,那么读取字段的方法使用的名称可能会以“is”开头。例如,名为 readable 的 boolean 类型字段对应的访问器方法是 isReadable() 而不是 getReadable()。

你可能感兴趣的:(第07部分:数据隐藏和封装)