ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

《On Java 中文版 基础卷》阅读笔记 实现隐藏 11

《On Java 中文版 基础卷》阅读笔记 实现隐藏 11

《On Java 中文版 基础卷》阅读笔记 实现隐藏 11

7.2 Java 访问权限修饰符

Java 访问权限修饰符包括 public、protected 和 private,它们放在类中成员定义的前面,包括字段和方法。
每个访问权限修饰符仅控制对该特定定义的访问。
java 规定,一个 java 文件可以有多个类,但只能有一个 public 修饰的类。

如果不提供访问权限修饰符,则表示 “包访问权限”​。
因此,无论如何,任何事物都有某些形式的访问控制。
在接下来的几节里,你将了解到各种访问权限。

7.2.1 包访问权限

本章之前的所有示例都只使用了 public 修饰符,或者没有使用修饰符(即默认访问权限)​。
默认访问权限没有关键字,通常称为包访问权限(有时称为 “友好访问权限”​)​。
这意味着当前包中的所有其他类都可以访问该成员。
对于此包之外的所有类,该成员显示为 private。
一个编译单元(即一个 java 文件)只能属于一个包,所以一个编译单元中的所有类都可以通过包访问权限来相互访问。
一个 java 文件是一个编译单元,一个 java 文件中可以包含多个类 class,只能有一个 public 修饰的类 class。

包访问权限将相关的类分组到一个包中,以便它们可以轻松地交互。
包中的类可以访问该包里具有包访问权限的成员,因此你 “拥有” 了该包中的代码。
只有你拥有的代码才能对你拥有的其他代码进行包访问,这是合理的。
包访问权限是将类分组到一个包中的原因之一。
在许多语言里,你可以任意组织文件中的定义方式,
但在 Java 中,你必须以合理的方式组织它们。
另外,如果有的类不应该访问当前包中定义的类的话,你可能需要排除它。

类控制着哪些代码可以访问其成员。
来自另一个包的代码不能只是因为说了一句 “嗨,我是 Bob 的朋友”​,
就能看到 Bob 的 protected、包访问权限和 private 成员。
授予成员访问权限的唯一方法如下所示。

1.将成员设为 public,这样任何地方的任何人都可以访问它。

2.通过去掉访问权限修饰符来授予成员包访问权限,并将其他类放在同一个包中,这样该包中的其他类就可以访问该成员了。

3.正如你在第 8 章会看到的,当引入继承时,**通过继承而来的类(子类)可以访问父类的 protected 成员以及 public 成员(但不能访问 private 成员)​。只有当两个类在同一个包中时,它才可以访问父类的包访问权限成员。**但现在不必担心继承和 protected。

4.提供可以读取和更改值的访问器 (accessor) 和修改器 (mutator) 方法(也称为 “get/set” 方法)​。

7.2.2 public:接口访问权限

当使用 public 关键字时,就意味着紧跟在 public 后面的成员声明对于所有人都是可用的,尤其是对于使用该库的客户程序员。
假设你定义了一个包含以下编译单元的 dessert 包:

// hiding/dessert/Cookie.java// (c)2021 MindView LLC: see Copyright.txt// We make no guarantees that this code is fit for any purpose.// Visit http://OnJava8.com for more book information.// Creates a librarypackagehiding.dessert;publicclassCookie{publicCookie(){System.out.println("Cookie constructor");}voidbite(){System.out.println("bite");}}

记住,Cookie.java 生成的类文件必须置于名为 dessert 的子目录中,该子目录位于 hiding 目录下(表示本书的第 7 章)​,
而 hiding 目录必须位于 CLASSPATH 指定的路径之一里。
不要错误地认为 Java 会始终将当前目录视为搜索的起点之一。
如果 CLASSPATH 中没有指定 “.” 作为路径之一,Java 就不会在当前目录里查找。

现在可以创建一个程序来使用 Cookie 了:

packagehiding;// hiding/Dinner.java// (c)2021 MindView LLC: see Copyright.txt// We make no guarantees that this code is fit for any purpose.// Visit http://OnJava8.com for more book information.// Uses the libraryimporthiding.dessert.*;publicclassDinner{publicstaticvoidmain(String[]args){Cookiex=newCookie();//- x.bite(); // Can't access}}/* Output: Cookie constructor */

你可以创建一个 Cookie 对象,因为它的构造函数是 public 的,而且类也是 public 的**(稍后将进一步介绍 public 类的概念)**​。
然而,bite() 方法在 Dinner.java 中是不可访问的,因为 bite() 只提供了在包 dessert 中的访问,所以编译器会阻止你使用它。

默认包

你可能会惊讶地发现以下代码可以编译,尽管它看起来似乎违反了规则,没有指明 package。

// hiding/Cake.java// (c)2021 MindView LLC: see Copyright.txt// We make no guarantees that this code is fit for any purpose.// Visit http://OnJava8.com for more book information.// Accesses a class in a separate compilation unitclassCake{publicstaticvoidmain(String[]args){Piex=newPie();x.f();}}/* Output: Pie.f() */

下面的代码是处于相同目录中的第二个文件:

// hiding/Pie.java// (c)2021 MindView LLC: see Copyright.txt// We make no guarantees that this code is fit for any purpose.// Visit http://OnJava8.com for more book information.// The other classclassPie{voidf(){System.out.println("Pie.f()");}}


乍一看这两个文件好像完全不相关,但 Cake 能够创建一个 Pie 对象并调用它的 f() 方法(注意 CLASSPATH 中必须有 “.” 才能编译,没有配置 CLASSPATH 也能编译)​。
你通常可能会认为 Pie 和 f() 具有包访问权限,因此不能用于 Cake。它们确实有包访问权——这是正确的。
它们在 Cake.java 中可用是因为它们都在同一目录中,并且没有明确的包名。
Java 将这样的文件看作属于该目录的 “默认包” 的隐含部分,因此它们为该目录中所有其他文件提供了包访问权限。

7.2.3 private:你无法访问它

private 关键字意味着,除了包含该成员的类之外,其他任何类都不能访问,当然在该类的方法内部也能访问。
同一个包中的其他类无法访问 private 成员,因此这就好像你故意将类与自己隔离。
不过从另一方面来说,一个包由几个人合作创建的可能性也并非不存在。
使用 private 的话,就可以自由更改该成员,而不必担心它会影响同一个包中的另一个类。

默认的包访问权限通常已经提供了足够的隐藏。
记住,使用该类的客户程序员是无法访问包访问权限成员的。
这是比较合理的,因为默认的包访问权限是你通常使用的访问权限(如果你忘记添加任何访问控制,则会默认获得该访问权限)​。
因此,你通常会考虑的是,哪些成员需要公开访问权限来给客户程序员调用。
最初你可能认为自己不会经常使用 private 关键字,因为没有它好像也可以正常工作。
但是,坚持使用 private 是十分重要的,尤其在涉及多线程的情况下(正如你将在进阶卷第 5 章中看到的那样)​。

下面是一个使用 private 的例子:

packagehiding;// hiding/IceCream.java// (c)2021 MindView LLC: see Copyright.txt// We make no guarantees that this code is fit for any purpose.// Visit http://OnJava8.com for more book information.// Demonstrates "private" keywordclassSundae{privateSundae(){}staticSundaemakeASundae(){returnnewSundae();}}publicclassIceCream{publicstaticvoidmain(String[]args){//- Sundae x = new Sundae();Sundaex=Sundae.makeASundae();}}

上面的示例显示了一个 private 大显身手的场景:控制对象的创建方式,并防止特定的构造器(或所有构造器)被调用。
在这个例子中,你不能通过它的构造器来创建一个 Sundae 对象;相反,你必须调用 makeASundae() 方法来做这件事。

只要能确定是类的 “辅助” 方法,这个方法就可以设为 private,以确保在包的其他地方不会意外地使用它,从而让自己无法再更改或删除。
将方法指定为 private 让你保留了这些选择。

对于类中的 private 字段也是如此。
除非你必须要公开底层实现(可能性比你以为的要小)​,否则就将字段设为 private。
然而,仅仅因为一个对象的引用在类中是 private 的,并不意味着其他对象不能拥有对同一个对象的public引用。​
(请参阅进阶卷第 2 章了解别名问题。​)

7.2.4 protected:继承访问权限

要理解 protected 访问权限修饰符,就需要提前做些准备。
首先,注意在第 8 章介绍继承之前,你即使不理解这一节的内容也可以继续阅读本书。
但为了完整起见,这里是 protected 的一个简要说明和相关示例。

protected 关键字处理的是继承的概念,它利用一个现有类——我们叫作基类 (base class) ——并在不修改现有类的情况下向该类添加新成员,还可以改变该类现有成员的行为。
为了继承一个类,你需要声明自己的新类扩展 (extends) 了现有类,如下所示:

classFooextendsBar{

类定义的其余部分看起来都是一样的。

如果你创建了一个新包,并需要继承另一个包中的类,那么唯一可以访问的成员是这个类的 public 成员。
当然如果继承同一个包中的类,就可以操作所有的包访问权限成员了。
有时候基类的创建者想要把特定成员的访问权限赋给子类,而不是所有的类,这时候 protected 就可以发挥作用了。
protected 还提供了包访问权,也就是说,同一包中的其他类也可以访问 protected 元素。

如果你回顾文件 Cookie.java,就会知道下面的类不能调用包访问权限成员 bite():

packagehiding;// hiding/ChocolateChip.java// (c)2021 MindView LLC: see Copyright.txt// We make no guarantees that this code is fit for any purpose.// Visit http://OnJava8.com for more book information.// Can't use package-access member from another packageimporthiding.dessert.*;publicclassChocolateChipextendsCookie{publicChocolateChip(){System.out.println("ChocolateChip constructor");}publicvoidchomp(){//- bite(); // Can't access bite}publicstaticvoidmain(String[]args){ChocolateChipx=newChocolateChip();x.chomp();}}/* Output: Cookie constructor ChocolateChip constructor */


如果类 Cookie 中存在一个方法 bite(),那么这个方法也存在于任何继承 Cookie 的类中。
但是 bite() 只具有包访问权限并且位于另一个包中,因此无法在当前包中使用它。
你可以将其修改为 public,但这样的话每个人就都可以访问它了,这也许不是你想要的。
如果按如下方式更改类 Cookie:

// hiding/cookie2/Cookie.java// (c)2021 MindView LLC: see Copyright.txt// We make no guarantees that this code is fit for any purpose.// Visit http://OnJava8.com for more book information.packagehiding.cookie2;publicclassCookie{publicCookie(){System.out.println("Cookie constructor");}protectedvoidbite(){System.out.println("bite");}}

这样任何继承 Cookie 的类都可以访问 bite():

packagehiding;// hiding/ChocolateChip2.java// (c)2021 MindView LLC: see Copyright.txt// We make no guarantees that this code is fit for any purpose.// Visit http://OnJava8.com for more book information.importhiding.cookie2.*;publicclassChocolateChip2extendsCookie{publicChocolateChip2(){System.out.println("ChocolateChip2 constructor");}publicvoidchomp(){bite();}// Protected methodpublicstaticvoidmain(String[]args){ChocolateChip2x=newChocolateChip2();x.chomp();}}/* Output: Cookie constructor ChocolateChip2 constructor bite */

这时尽管 bite() 也有包访问权限,但它不是 public 的。

7.2.5 包访问权限与公共构造器

当定义一个具有包访问权限的类时,你可以给它一个 public 构造器,此时编译器不会提示任何错误:

// hiding/packageaccess/PublicConstructor.java// (c)2021 MindView LLC: see Copyright.txt// We make no guarantees that this code is fit for any purpose.// Visit http://OnJava8.com for more book information.packagehiding.packageaccess;classPublicConstructor{publicPublicConstructor(){}}

如果运行 Checkstyle 这样的工具,它会指出这是虚假陈述,并且从技术上讲这样定义类是错误的。
你实际上无法从包外部访问这个所谓的 public 构造器:

packagehiding;// hiding/CreatePackageAccessObject.java// (c)2021 MindView LLC: see Copyright.txt// We make no guarantees that this code is fit for any purpose.// Visit http://OnJava8.com for more book information.// {WillNotCompile}importhiding.packageaccess.*;publicclassCreatePackageAccessObject{publicstaticvoidmain(String[]args){newPublicConstructor();}}


如果手动编译此文件,你将看到一个编译器错误消息:

因此,在只有包访问权限的类中声明一个 public 构造器,实际上并不会使这个构造器成为 public 的,编译器应该在声明处将其标记为一个错误。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进