From 1b1d1738c53f68001023d3484d0855e08455654b Mon Sep 17 00:00:00 2001 From: oxygenkun Date: Sat, 8 Feb 2020 11:45:05 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=20Fix:=E4=BF=AE=E6=AD=A3=20Chap.6=20?= =?UTF-8?q?=E8=AF=AD=E5=8F=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/book/07-Implementation-Hiding.md | 40 +++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/book/07-Implementation-Hiding.md b/docs/book/07-Implementation-Hiding.md index a331bd5b..143c483d 100644 --- a/docs/book/07-Implementation-Hiding.md +++ b/docs/book/07-Implementation-Hiding.md @@ -5,23 +5,23 @@ 访问控制(或者隐藏实现)与"最初的实现不恰当"有关。 -所有优秀的作者——包括这些编写软件的人——都知道一件好的作品都是经过反复打磨才变得优秀的。如果你把一段代码置于某个位置一段时间,过一会重新来看,你可能发现更好的实现方式。这是重构的原动力之一,重构就是重写可工作的代码,使之更加可读,易懂,因而更易维护。 +所有优秀的作者——包括那些编写软件的人——都知道一件好的作品都是经过反复打磨才变得优秀的。如果你把一段代码置于某个位置一段时间,过一会重新来看,你可能发现更好的实现方式。这是重构(refactoring)的原动力之一,重构就是重写可工作的代码,使之更加可读,易懂,因而更易维护。 -但是,在修改和完善代码的愿望下,也存在巨大的压力。通常,客户端程序员希望你的代码在某些方面保持不变。所以你想修改代码,但他们希望代码保持不变。由此引出了面向对象设计中的一个基本问题:"如何区分变动的事物和不变的事物"。 +但是,在修改和完善代码的愿望下,也存在巨大的压力。通常,一些用户(客户端程序员)依赖你的代码在某些方面保持不变。所以你想修改代码,但他们希望代码保持不变。由此引出了面向对象设计中的一个基本问题:"如何区分变动的事物和不变的事物"。 -这个问题对于类库而言尤其重要。类库的使用者必须依赖他们所使用的那部分类库,并且知道如果使用了类库的新版本,不需要改写代码。另一方面,类库的开发者必须有修改和改进类库的自由,并保证客户代码不会受这些改动影响。 +这个问题对于类库(library)而言尤其重要。类库的使用者必须依赖他们所使用的那部分类库,并且知道如果使用了类库的新版本,不需要改写代码。另一方面,类库的开发者必须有修改和改进类库的自由,并保证客户代码不会受这些改动影响。 这可以通过约定解决。例如,类库开发者必须同意在修改类库中的一个类时,不会移除已有的方法,因为那样将会破坏客户端程序员的代码。与之相反的情况更加复杂。在有成员属性的情况下,类库开发者如何知道哪些属性被客户端程序员使用?这同样会发生在那些只为实现类库类而创建的方法上,它们也不是设计成可供客户端程序员调用的。如果类库开发者想删除旧的实现,添加新的实现,结果会怎样呢?任何这些成员的改动都可能破环客户端程序员的代码。因此类库开发者会被束缚,不能修改任何事物。 -为了解决这一问题,Java 提供了访问修饰符供类库开发者指明哪些对于客户端程序员是可用的,哪些是不可用的。访问控制权限的等级,从"最大权限"到"最小权限"依次是:**public**,**protected**,包访问权限(没有关键字)和 **private**。根据上一段的内容,你可能会想,作为一名类库设计者,你会尽可能将一切都设为 **private**,仅向客户端程序员暴露你愿意他们使用的方法。这就是你通常所做的,尽管这与使用其他语言(尤其是 C)编程和访问不受任何限制的人们的直觉相违背。 +为了解决这一问题,Java 提供了"访问修饰符"(access specifier)供类库开发者指明哪些对于客户端程序员是可用的,哪些是不可用的。访问控制权限的等级,从"最大权限"到"最小权限"依次是:**public**,**protected**,包访问权限(没有关键字)和 **private**。根据上一段的内容,你可能会想,作为一名类库设计者,你会尽可能将一切都设为 **private**,仅向客户端程序员暴露你愿意他们使用的方法。这就是你通常所做的,尽管这与那些使用其他语言(尤其是 C)编程以及习惯了不受限制地访问任何东西的人们的直觉相违背。 -然而,构建类库的概念和对类库组件的访问控制仍然不完善。其中仍然存在问题就是如何将类库组件捆绑到一个内聚到类库单元中。Java 中通过 package 关键字加以控制,类是在相同包下还是不同包下会影响访问修饰符。所以在这章开始,你将会学习如何将类库组件置于同一个包下,之后你就能明白访问修饰符的全部含义。 +然而,类库组件的概念和对类库组件访问的控制仍然不完善。其中仍然存在问题就是如何将类库组件捆绑到一个内聚的类库单元中。Java 中通过 **package** 关键字加以控制,类在相同包下还是在不同包下,会影响访问修饰符。所以在这章开始,你将会学习如何将类库组件置于同一个包下,之后你就能明白访问修饰符的全部含义。 ## 包的概念 -包内包含一组类,它们被组织在一个单独的命名空间下。 +包内包含一组类,它们被组织在一个单独的"命名空间"(namespace)下。 例如,标准 Java 发布中有一个工具库,它被组织在 **java.util** 命名空间下。**java.util** 中含有一个类,叫做 **ArrayList**。使用 **ArrayList** 的一种方式是用其全名 **java.util.ArrayList**。 @@ -55,15 +55,15 @@ import java.util.* 之所以使用导入,是为了提供一种管理命名空间的机制。所有类名之间都是相互隔离的。类 **A** 中的方法 `f()` 不会与类 **B** 中具有相同签名的方法 `f()` 冲突。但是如果类名冲突呢?假设你创建了一个 **Stack** 类,打算安装在一台已经有别人所写的 **Stack** 类的机器上,该怎么办呢?这种类名的潜在冲突,正是我们需要在 Java 中对命名空间进行完全控制的原因。为了解决冲突,我们为每个类创建一个唯一标识符组合。 -到目前为止的大部分示例都只存在单个文件,并为本地使用的,所以尚未受到包名的干扰。但是,这些示例其实已经位于包中了,叫做"未命名"包或默认包。这当然是一种选择,为了简单起见,本书其余部分会尽可能采用这种方式。但是,如果你打算为相同机器上的其他 Java 程序创建友好的类库或程序时,就必须仔细考虑以防类名冲突。 +到目前为止的大部分示例都只存在单个文件,并为本地使用的,所以尚未受到包名的干扰。但是,这些示例其实已经位于包中了,叫做"未命名"包或默认包(default package)。这当然是一种选择,为了简单起见,本书其余部分会尽可能采用这种方式。但是,如果你打算为相同机器上的其他 Java 程序创建友好的类库或程序时,就必须仔细考虑以防类名冲突。 -一个 Java 源代码文件称为一个*编译单元*(有时也称*翻译单元*)。每个编译单元的文件名后缀必须是 **.java**。在编译单元中可以有一个 **public** 类,它的类名必须与文件名相同(包括大小写,但不包括后缀名 **.java**)。每个编译单元中只能有一个 **public** 类,否则编译器不接受。如果这个编译单元中还有其他类,那么在包之外是无法访问到这些类的,因为它们不是 **public** 类,此时它们支持主 **public** 类。 +一个 Java 源代码文件称为一个编译单元(compilation unit)(有时也称翻译单元(translation unit))。每个编译单元的文件名后缀必须是 **.java**。在编译单元中可以有一个 **public** 类,它的类名必须与文件名相同(包括大小写,但不包括后缀名 **.java**)。每个编译单元中只能有*一个* **public** 类,否则编译器不接受。如果这个编译单元中还有其他类,那么在包之外是无法访问到这些类的,因为它们*不是* **public** 类,此时它们为主 **public**类提供“支持”类 。 ### 代码组织 当编译一个 **.java** 文件时,**.java** 文件的每个类都会有一个输出文件。每个输出的文件名和 **.java** 文件中每个类的类名相同,只是后缀名是 **.class**。因此,在编译少量的 **.java** 文件后,会得到大量的 **.class** 文件。如果你使用过编译型语言,那么你可能习惯编译后产生一个中间文件(通常称为"obj"文件),然后与使用链接器(创建可执行文件)或类库生成器(创建类库)产生的其他同类文件打包到一起的情况。这不是 Java 工作的方式。在 Java 中,可运行程序是一组 **.class** 文件,它们可以打包压缩成一个 Java 文档文件(JAR,使用 **jar** 文档生成器)。Java 解释器负责查找、加载和解释这些文件。 -类库是一组类文件。每个源文件通常都含有一个 **public** 类和任意数量的非 **public** 类,因此每个文件都有一个构件。如果把这些组件集中在一起,就需要使用关键字 **package**。 +类库是一组类文件。每个源文件通常都含有一个 **public** 类和任意数量的非 **public** 类,因此每个文件都有一个**public**组件。如果把这些组件集中在一起,就需要使用关键字 **package**。 如果你使用了 **package** 语句,它必须是文件中除了注释之外的第一行代码。当你如下这样写: @@ -73,7 +73,7 @@ package hiding; 意味着这个编译单元是一个名为 **hiding** 类库的一部分。换句话说,你正在声明的编译单元中的 **public** 类名称位于名为 **hiding** 的保护伞下。任何人想要使用该名称,必须指明完整的类名或者使用 **import** 关键字导入 **hiding**。(注意,Java 包名按惯例一律小写,即使中间的单词也需要小写,与驼峰命名不同) -例如,假设文件名是 **MyClass.java**,这意味着文件中只能有一个 **public** 类,且类名必须是 MyClass(大小写也与文件名相同): +例如,假设文件名是 **MyClass.java**,这意味着文件中只能有一个 **public** 类,且类名必须是 **MyClass**(大小写也与文件名相同): ```java // hiding/mypackage/MyClass.java @@ -117,7 +117,7 @@ public class ImportedMyClass { 将所有的文件放在一个子目录还解决了其他的两个问题:创建独一无二的包名和查找可能隐藏于目录结构某处的类。这是通过将 **.class** 文件所在的路径位置编码成 **package** 名称来实现的。按照惯例,**package** 名称是类的创建者的反顺序的 Internet 域名。如果你遵循惯例,因为 Internet 域名是独一无二的,所以你的 **package** 名称也应该是独一无二的,不会发生名称冲突。如果你没有自己的域名,你就得构造一组不大可能与他人重复的组合(比如你的姓名),来创建独一无二的 package 名称。如果你打算发布 Java 程序代码,那么花些力气去获取一个域名是值得的。 -此技巧的第二部分是把 **package** 名称分解成你机器上的一个目录,所以当 Java 解释器必须要加载一个 .class 文件时,它能定位到 **.class** 文件所在的位置。首先,它找出环境变量 **CLASSPATH**(通过操作系统设置,有时也能通过 Java 的安装程序或基于 Java 的工具设置)。**CLASSPATH** 包含一个或多个目录,用作查找 .**class** 文件的根目录。从根目录开始,Java 解释器获取包名并将每个句点替换成反斜杠,生成一个基于根目录的路径名(包名 foo.bar.baz 变成 foo\bar\baz 或 foo/bar/baz 或其它,取决于你的操作系统)。然后这个路径与 **CLASSPATH** 的不同项连接,解释器就在这些目录中查找与你所创建的类名称相关的 **.class** 文件(解释器还会查找某些涉及 Java 解释器所在位置的标准目录)。 +此技巧的第二部分是把 **package** 名称分解成你机器上的一个目录,所以当 Java 解释器必须要加载一个 .class 文件时,它能定位到 **.class** 文件所在的位置。首先,它找出环境变量 **CLASSPATH**(通过操作系统设置,有时也能通过 Java 的安装程序或基于 Java 的工具设置)。**CLASSPATH** 包含一个或多个目录,用作查找 .**class** 文件的根目录。从根目录开始,Java 解释器获取包名并将每个句点替换成反斜杠,生成一个基于根目录的路径名(取决于你的操作系统,包名 **foo.bar.baz** 变成 **foo\bar\baz** 或 **foo/bar/baz** 或其它)。然后这个路径与 **CLASSPATH** 的不同项连接,解释器就在这些目录中查找与你所创建的类名称相关的 **.class** 文件(解释器还会查找某些涉及 Java 解释器所在位置的标准目录)。 为了理解这点,比如说我的域名 **MindviewInc.com**,将之反转并全部改为小写后就是 **com.mindviewinc**,这将作为我创建的类的独一无二的全局名称。(com、edu、org等扩展名之前在 Java 包中都是大写,但是 Java 2 之后都统一用小写。)我决定再创建一个名为 **simple** 的类库,从而细分名称: @@ -197,7 +197,7 @@ com.mindviewinc.simple.List 当编译器遇到导入 **simple** 库的 **import** 语句时,它首先会在 CLASSPATH 指定的目录中查找子目录 **com/mindviewinc/simple**,然后从已编译的文件中找出名称相符者(对 **Vector** 而言是 **Vector.class**,对 **List** 而言是 **List.class**)。注意,这两个类和其中要访问的方法都必须是 **public** 修饰的。 -对于 Java 新手而言,设置 CLASSPATH 是一件麻烦的事(我最初使用时这么觉得),后面版本的 JDK 更加智能。你会发现当你安装好 JDK 时,即使不设置 CLASSPATH,也能够编译和运行基本的 Java 程序。但是,为了编译和运行本书的代码示例(从[https://github.com/BruceEckel/OnJava8-examples](https://github.com/BruceEckel/OnJava8-examples) 取得),你必须将本书程序代码树的基本目录加入到 CLASSPATH 中( gradlew 命令管理自身的 CLASSPATH,所以如果你想直接使用 javac 和 java,不用 Gradle 的话,就需要设置 CLASSPATH)。 +对于 Java 新手而言,设置 CLASSPATH 是一件麻烦的事(我最初使用时是这么觉得的),后面版本的 JDK 更加智能。你会发现当你安装好 JDK 时,即使不设置 CLASSPATH,也能够编译和运行基本的 Java 程序。但是,为了编译和运行本书的代码示例(从[https://github.com/BruceEckel/OnJava8-examples](https://github.com/BruceEckel/OnJava8-examples) 取得),你必须将本书程序代码树的基本目录加入到 CLASSPATH 中( gradlew 命令管理自身的 CLASSPATH,所以如果你想直接使用 javac 和 java,不用 Gradle 的话,就需要设置 CLASSPATH)。 ### 冲突 @@ -276,7 +276,7 @@ public class Range { ### 使用 import 改变行为 -Java 没有 C 的*条件编译*功能,该功能使你不必更改任何程序代码而能够切换开关产生不同的行为。Java 之所以去掉此功能,可能是因为 C 在绝大多数情况下使用该功能解决跨平台问题:程序代码的不同部分要根据不同的平台来编译。而 Java 自身就是跨平台设计的,这个功能就没有必要了。 +Java 没有 C 的*条件编译*(conditional compilation)功能,该功能使你不必更改任何程序代码而能够切换开关产生不同的行为。Java 之所以去掉此功能,可能是因为 C 在绝大多数情况下使用该功能解决跨平台问题:程序代码的不同部分要根据不同的平台来编译。而 Java 自身就是跨平台设计的,这个功能就没有必要了。 但是,条件编译还有其他的用途。调试是一个很常见的用途,调试功能在开发过程中是开启的,在发布的产品中是禁用的。可以通过改变导入的 **package** 来实现这一目的,修改的方法是将程序中的代码从调试版改为发布版。这个技术可用于任何种类的条件代码。 @@ -296,13 +296,13 @@ Java 访问权限修饰符 **public**,**protected** 和 **private** 位于定 ### 包访问权限 -本章之前的所有示例要么使用 **public** 访问修饰符,要么就没使用修饰符(默认访问)。默认访问权限没有关键字,通常被称为包访问权限(有时也称为 friendly)。这意味着当前包中的所有其他类都可以访问那个成员。对于这个包之外的类,这个成员看上去是 **private** 的。由于一个编译单元(即一个文件)只能隶属于一个包,所以通过包访问权限,位于同一编译单元中的所有类彼此之间都是可访问的。 +本章之前的所有示例要么使用 **public** 访问修饰符,要么就没使用修饰符(*默认访问权限*(default access))。默认访问权限没有关键字,通常被称为*包访问权限*(package access)(有时也称为 **friendly**)。这意味着当前包中的所有其他类都可以访问那个成员。对于这个包之外的类,这个成员看上去是 **private** 的。由于一个编译单元(即一个文件)只能隶属于一个包,所以通过包访问权限,位于同一编译单元中的所有类彼此之间都是可访问的。 -包访问权限可以把相关类聚到一个包下,以便它们能轻易地相互访问。包里的类给它们的包访问权限的成员赋予了相互访问的权限,所以你"拥有”了包内的程序代码。只能通过你所拥有的代码去访问你所拥有的其他代码,这样规定很有意义。构建包访问权限机制是将类聚集在包中的重要原因之一。在许多语言中,在文件中组织定义的方式是任意的,但是在 Java 中你被强制以一种合理的方式组织它们。另外,你可能会将不应该对当前包中的类具有访问权限的类排除在包外。 +包访问权限可以把相关类聚到一个包下,以便它们能轻易地相互访问。包里的类赋予了它们包访问权限的成员相互访问的权限,所以你"拥有”了包内的程序代码。只能通过你所拥有的代码去访问你所拥有的其他代码,这样规定很有意义。构建包访问权限机制是将类聚集在包中的重要原因之一。在许多语言中,在文件中组织定义的方式是任意的,但是在 Java 中你被强制以一种合理的方式组织它们。另外,你可能会将不应该对当前包中的类具有访问权限的类排除在包外。 -类控制着哪些代码有权访问自己的成员。其他包中的代码不能一上来就说"嗨,我是 **Bob** 的朋友!",然后想看到 **Bob** 的 **protected**,包访问权限和 **private** 成员。取得对成员的访问权的唯一方式是: +类控制着哪些代码有权访问自己的成员。其他包中的代码不能一上来就说"嗨,我是 **Bob** 的朋友!",然后想看到 **Bob** 的 **protected**、包访问权限和 **private** 成员。取得对成员的访问权的唯一方式是: -1. 使成员成为 public。那么无论是谁,无论在哪,都可以访问它。 +1. 使成员成为 **public**。那么无论是谁,无论在哪,都可以访问它。 2. 赋予成员默认包访问权限,不用加任何访问修饰符,然后将其他类放在相同的包内。这样,其他类就可以访问该成员。 3. 在"复用"这一章你将看到,继承的类既可以访问 **public** 成员,也可以访问 **protected** 成员(但不能访问 **private** 成员)。只有当两个类处于同一个包内,它才可以访问包访问权限的成员。但现在不用担心继承和 **protected**。 4. 提供访问器(accessor)和修改器(mutator)方法(有时也称为"get/set" 方法),从而读取和改变值。 @@ -556,7 +556,7 @@ new PublicConstructor(); ## 接口和实现 -访问控制通常被称为实现的隐藏。将数据和方法包装进类中并把具体实现隐藏被称作是封装。其结果就是一个同时带有特征和行为的数据类型。 +访问控制通常被称为*隐藏实现*(implementation hiding)。将数据和方法包装进类中并把具体实现隐藏被称作是*封装*(encapsulation)。其结果就是一个同时带有特征和行为的数据类型。 出于两个重要的原因,访问控制在数据类型内部划定了边界。第一个原因是确立客户端程序员可以使用和不能使用的边界。可以在结构中建立自己的内部机制而不必担心客户端程序员偶尔将内部实现作为他们可以使用的接口的一部分。 @@ -613,7 +613,7 @@ import hiding.*; 如果获取了一个在 **hiding** 包中的类,只用来完成 **Widget** 或 **hiding** 包下一些其他 **public** 类所要执行的任务,怎么办呢? 你不想自找麻烦为客户端程序员创建说明文档,并且你认为不久后会完全改变原有方案并将旧版本删除,替换成新版本。为了保留此灵活性,需要确保客户端程序员不依赖隐藏在 **hiding** 中的任何特定细节,那么把 **public** 关键字从类中去掉,给予它包访问权限,就可以了。 -当你创建了一个包访问权限的类,把类中的属性声明为 **private** 仍然是有意义的——应该尽可能将所有属性都声明为 **private**,但是通常把方法声明成与类(包访问权限)相同的访问权限也是合理的。由于一个包访问权限的类只能被用于包内,除非你被强制将某些方法声明为 **public**,这种情况下,编译器会告诉你。 +当你创建了一个包访问权限的类,把类中的属性声明为 **private** 仍然是有意义的——应该尽可能将所有属性都声明为 **private**,但是通常把方法声明成与类(包访问权限)相同的访问权限也是合理的。一个包访问权限的类只能被用于包内,除非强制将某些方法声明为 **public**,这种情况下,编译器会告诉你。 注意,类既不能是 **private** 的(这样除了该类自身,任何类都不能访问它),也不能是 **protected** 的。所以对于类的访问权限只有两种选择:包访问权限或者 **public**。为了防止类被外界访问,可以将所有的构造器声明为 **private**,这样只有你自己能创建对象(在类的 static 成员中): @@ -664,7 +664,7 @@ public class Lunch { **Soup1** 和 **Soup2** 展示了如何通过将你所有的构造器声明为 **private** 的方式防止直接创建某个类的对象。记住,如果你不显式地创建构造器,编译器会自动为你创建一个无参构造器(没有参数的构造器)。如果我们编写了无参构造器,那么编译器就不会自动创建构造器了。将构造器声明为 **private**,那么谁也无法创建该类的对象了。但是现在别人该怎么使用这个类呢?上述例子给出了两个选择。在 **Soup1** 中,有一个 **static** 方法,它的作用是创建一个新的 **Soup1** 对象并返回对象的引用。如果想要在返回引用之前在 **Soup1** 上做一些额外操作,或是记录创建了多少个 **Soup1** 对象(可以用来限制数量),这种做法是有用的。 -**Soup2** 用到了所谓的*设计模式*。这种模式叫做*单例模式*,因为它只允许创建类的一个对象。**Soup2** 类的对象是作为 **Soup2** 的 **static** **private** 成员而创建的,所以有且只有一个,你只能通过 **public** 修饰的 `access()` 方法访问到这个对象。 +**Soup2** 用到了所谓的*设计模式*(design pattern)。这种模式叫做*单例模式*(singleton),因为它只允许创建类的一个对象。**Soup2** 类的对象是作为 **Soup2** 的 **static** **private** 成员而创建的,所以有且只有一个,你只能通过 **public** 修饰的 `access()` 方法访问到这个对象。 From 0e8d1977f512c0888335c2dd704bdb08cc7443ab Mon Sep 17 00:00:00 2001 From: oxygenkun Date: Sun, 9 Feb 2020 11:21:04 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/book/07-Implementation-Hiding.md | 90 +++++++++++++-------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/docs/book/07-Implementation-Hiding.md b/docs/book/07-Implementation-Hiding.md index 143c483d..eb8ba49a 100644 --- a/docs/book/07-Implementation-Hiding.md +++ b/docs/book/07-Implementation-Hiding.md @@ -3,25 +3,25 @@ # 第七章 封装 -访问控制(或者隐藏实现)与"最初的实现不恰当"有关。 +> *访问控制(Access control)*(或者*隐藏实现(Access control)*)与“最初的实现不恰当”有关。 -所有优秀的作者——包括那些编写软件的人——都知道一件好的作品都是经过反复打磨才变得优秀的。如果你把一段代码置于某个位置一段时间,过一会重新来看,你可能发现更好的实现方式。这是重构(refactoring)的原动力之一,重构就是重写可工作的代码,使之更加可读,易懂,因而更易维护。 +所有优秀的作者——包括那些编写软件的人——都知道一件好的作品都是经过反复打磨才变得优秀的。如果你把一段代码置于某个位置一段时间,过一会重新来看,你可能发现更好的实现方式。这是*重构*(refactoring)的原动力之一,重构就是重写可工作的代码,使之更加可读,易懂,因而更易维护。 -但是,在修改和完善代码的愿望下,也存在巨大的压力。通常,一些用户(客户端程序员)依赖你的代码在某些方面保持不变。所以你想修改代码,但他们希望代码保持不变。由此引出了面向对象设计中的一个基本问题:"如何区分变动的事物和不变的事物"。 +但是,在修改和完善代码的愿望下,也存在巨大的压力。通常,一些用户(*客户端程序员(client programmers)*)希望你的代码在某些方面保持不变。所以你想修改代码,但他们希望代码保持不变。由此引出了面向对象设计中的一个基本问题:“如何区分变动的事物和不变的事物”。 这个问题对于类库(library)而言尤其重要。类库的使用者必须依赖他们所使用的那部分类库,并且知道如果使用了类库的新版本,不需要改写代码。另一方面,类库的开发者必须有修改和改进类库的自由,并保证客户代码不会受这些改动影响。 这可以通过约定解决。例如,类库开发者必须同意在修改类库中的一个类时,不会移除已有的方法,因为那样将会破坏客户端程序员的代码。与之相反的情况更加复杂。在有成员属性的情况下,类库开发者如何知道哪些属性被客户端程序员使用?这同样会发生在那些只为实现类库类而创建的方法上,它们也不是设计成可供客户端程序员调用的。如果类库开发者想删除旧的实现,添加新的实现,结果会怎样呢?任何这些成员的改动都可能破环客户端程序员的代码。因此类库开发者会被束缚,不能修改任何事物。 -为了解决这一问题,Java 提供了"访问修饰符"(access specifier)供类库开发者指明哪些对于客户端程序员是可用的,哪些是不可用的。访问控制权限的等级,从"最大权限"到"最小权限"依次是:**public**,**protected**,包访问权限(没有关键字)和 **private**。根据上一段的内容,你可能会想,作为一名类库设计者,你会尽可能将一切都设为 **private**,仅向客户端程序员暴露你愿意他们使用的方法。这就是你通常所做的,尽管这与那些使用其他语言(尤其是 C)编程以及习惯了不受限制地访问任何东西的人们的直觉相违背。 +为了解决这一问题,Java 提供了*访问修饰符*(access specifier)供类库开发者指明哪些对于客户端程序员是可用的,哪些是不可用的。访问控制权限的等级,从“最大权限”到“最小权限”依次是:**public**,**protected**,*包访问权限(package access)*(没有关键字)和 **private**。根据上一段的内容,你可能会想,作为一名类库设计者,你会尽可能将一切都设为 **private**,仅向客户端程序员暴露你愿意他们使用的方法。这就是你通常所做的,尽管这与那些使用其他语言(尤其是 C)编程以及习惯了不受限制地访问任何东西的人们的直觉相违背。 -然而,类库组件的概念和对类库组件访问的控制仍然不完善。其中仍然存在问题就是如何将类库组件捆绑到一个内聚的类库单元中。Java 中通过 **package** 关键字加以控制,类在相同包下还是在不同包下,会影响访问修饰符。所以在这章开始,你将会学习如何将类库组件置于同一个包下,之后你就能明白访问修饰符的全部含义。 +然而,类库组件的概念和对类库组件访问的控制仍然不完善。其中仍然存在问题就是如何将类库组件捆绑到一个内聚的类库单元中。Java 中通过 `package` 关键字加以控制,类在相同包下还是在不同包下,会影响访问修饰符。所以在这章开始,你将会学习如何将类库组件置于同一个包下,之后你就能明白访问修饰符的全部含义。 ## 包的概念 -包内包含一组类,它们被组织在一个单独的"命名空间"(namespace)下。 +包内包含一组类,它们被组织在一个单独的*命名空间*(namespace)下。 例如,标准 Java 发布中有一个工具库,它被组织在 **java.util** 命名空间下。**java.util** 中含有一个类,叫做 **ArrayList**。使用 **ArrayList** 的一种方式是用其全名 **java.util.ArrayList**。 @@ -35,7 +35,7 @@ public class FullQualification { } ``` -这种方式使得程序冗长乏味,因此你可以换一种方式,使用 **import** 关键字。如果需要导入某个类,就需要在 **import** 语句中声明: +这种方式使得程序冗长乏味,因此你可以换一种方式,使用 `import` 关键字。如果需要导入某个类,就需要在 **import** 语句中声明: ```java // hiding/SingleImport.java @@ -55,15 +55,15 @@ import java.util.* 之所以使用导入,是为了提供一种管理命名空间的机制。所有类名之间都是相互隔离的。类 **A** 中的方法 `f()` 不会与类 **B** 中具有相同签名的方法 `f()` 冲突。但是如果类名冲突呢?假设你创建了一个 **Stack** 类,打算安装在一台已经有别人所写的 **Stack** 类的机器上,该怎么办呢?这种类名的潜在冲突,正是我们需要在 Java 中对命名空间进行完全控制的原因。为了解决冲突,我们为每个类创建一个唯一标识符组合。 -到目前为止的大部分示例都只存在单个文件,并为本地使用的,所以尚未受到包名的干扰。但是,这些示例其实已经位于包中了,叫做"未命名"包或默认包(default package)。这当然是一种选择,为了简单起见,本书其余部分会尽可能采用这种方式。但是,如果你打算为相同机器上的其他 Java 程序创建友好的类库或程序时,就必须仔细考虑以防类名冲突。 +到目前为止的大部分示例都只存在单个文件,并为本地使用的,所以尚未受到包名的干扰。但是,这些示例其实已经位于包中了,叫做“未命名”包或*默认包*(default package)。这当然是一种选择,为了简单起见,本书其余部分会尽可能采用这种方式。但是,如果你打算为相同机器上的其他 Java 程序创建友好的类库或程序时,就必须仔细考虑以防类名冲突。 -一个 Java 源代码文件称为一个编译单元(compilation unit)(有时也称翻译单元(translation unit))。每个编译单元的文件名后缀必须是 **.java**。在编译单元中可以有一个 **public** 类,它的类名必须与文件名相同(包括大小写,但不包括后缀名 **.java**)。每个编译单元中只能有*一个* **public** 类,否则编译器不接受。如果这个编译单元中还有其他类,那么在包之外是无法访问到这些类的,因为它们*不是* **public** 类,此时它们为主 **public**类提供“支持”类 。 +一个 Java 源代码文件称为一个*编译单元(compilation unit)*(有时也称*翻译单元(translation unit)*)。每个编译单元的文件名后缀必须是 **.java**。在编译单元中可以有一个 **public** 类,它的类名必须与文件名相同(包括大小写,但不包括后缀名 **.java**)。每个编译单元中只能有*一个* **public** 类,否则编译器不接受。如果这个编译单元中还有其他类,那么在包之外是无法访问到这些类的,因为它们*不是* **public** 类,此时它们为主 **public** 类提供“支持”类 。 ### 代码组织 -当编译一个 **.java** 文件时,**.java** 文件的每个类都会有一个输出文件。每个输出的文件名和 **.java** 文件中每个类的类名相同,只是后缀名是 **.class**。因此,在编译少量的 **.java** 文件后,会得到大量的 **.class** 文件。如果你使用过编译型语言,那么你可能习惯编译后产生一个中间文件(通常称为"obj"文件),然后与使用链接器(创建可执行文件)或类库生成器(创建类库)产生的其他同类文件打包到一起的情况。这不是 Java 工作的方式。在 Java 中,可运行程序是一组 **.class** 文件,它们可以打包压缩成一个 Java 文档文件(JAR,使用 **jar** 文档生成器)。Java 解释器负责查找、加载和解释这些文件。 +当编译一个 **.java** 文件时,**.java** 文件的每个类都会有一个输出文件。每个输出的文件名和 **.java** 文件中每个类的类名相同,只是后缀名是 **.class**。因此,在编译少量的 **.java** 文件后,会得到大量的 **.class** 文件。如果你使用过编译型语言,那么你可能习惯编译后产生一个中间文件(通常称为“obj”文件),然后与使用链接器(创建可执行文件)或类库生成器(创建类库)产生的其他同类文件打包到一起的情况。这不是 Java 工作的方式。在 Java 中,可运行程序是一组 **.class** 文件,它们可以打包压缩成一个 Java 文档文件(JAR,使用 **jar** 文档生成器)。Java 解释器负责查找、加载和解释这些文件。 -类库是一组类文件。每个源文件通常都含有一个 **public** 类和任意数量的非 **public** 类,因此每个文件都有一个**public**组件。如果把这些组件集中在一起,就需要使用关键字 **package**。 +类库是一组类文件。每个源文件通常都含有一个 **public** 类和任意数量的非 **public** 类,因此每个文件都有一个 **public** 组件。如果把这些组件集中在一起,就需要使用关键字 `package`。 如果你使用了 **package** 语句,它必须是文件中除了注释之外的第一行代码。当你如下这样写: @@ -71,9 +71,9 @@ import java.util.* package hiding; ``` -意味着这个编译单元是一个名为 **hiding** 类库的一部分。换句话说,你正在声明的编译单元中的 **public** 类名称位于名为 **hiding** 的保护伞下。任何人想要使用该名称,必须指明完整的类名或者使用 **import** 关键字导入 **hiding**。(注意,Java 包名按惯例一律小写,即使中间的单词也需要小写,与驼峰命名不同) +意味着这个编译单元是一个名为 **hiding** 类库的一部分。换句话说,你正在声明的编译单元中的 **public** 类名称位于名为 **hiding** 的保护伞下。任何人想要使用该名称,必须指明完整的类名或者使用 `import` 关键字导入 **hiding** 。(注意,Java 包名按惯例一律小写,即使中间的单词也需要小写,与驼峰命名不同) -例如,假设文件名是 **MyClass.java**,这意味着文件中只能有一个 **public** 类,且类名必须是 **MyClass**(大小写也与文件名相同): +例如,假设文件名是 **MyClass.java** ,这意味着文件中只能有一个 **public** 类,且类名必须是 **MyClass**(大小写也与文件名相同): ```java // hiding/mypackage/MyClass.java @@ -84,7 +84,7 @@ public class MyClass { } ``` -现在,如果有人想使用 **MyClass** 或 **hiding.mypackage** 中的其他 **public** 类,就必须使用关键字 **import** 来使 **hiding.mypackage** 中的名称可用。还有一种选择是使用完整的名称: +现在,如果有人想使用 **MyClass** 或 **hiding.mypackage** 中的其他 **public** 类,就必须使用关键字 `import` 来使 **hiding.mypackage** 中的名称可用。还有一种选择是使用完整的名称: ```java // hiding/QualifiedMyClass.java @@ -96,7 +96,7 @@ public class QualifiedMyClass { } ``` -关键字 **import** 使之更简洁: +关键字 `import` 使之更简洁: ```java // hiding/ImportedMyClass.java @@ -109,7 +109,7 @@ public class ImportedMyClass { } ``` -**package** 和 **import** 这两个关键字将单一的全局命名空间分隔开,从而避免名称冲突。 +`package` 和 `import` 这两个关键字将单一的全局命名空间分隔开,从而避免名称冲突。 ### 创建独一无二的包名 @@ -117,9 +117,9 @@ public class ImportedMyClass { 将所有的文件放在一个子目录还解决了其他的两个问题:创建独一无二的包名和查找可能隐藏于目录结构某处的类。这是通过将 **.class** 文件所在的路径位置编码成 **package** 名称来实现的。按照惯例,**package** 名称是类的创建者的反顺序的 Internet 域名。如果你遵循惯例,因为 Internet 域名是独一无二的,所以你的 **package** 名称也应该是独一无二的,不会发生名称冲突。如果你没有自己的域名,你就得构造一组不大可能与他人重复的组合(比如你的姓名),来创建独一无二的 package 名称。如果你打算发布 Java 程序代码,那么花些力气去获取一个域名是值得的。 -此技巧的第二部分是把 **package** 名称分解成你机器上的一个目录,所以当 Java 解释器必须要加载一个 .class 文件时,它能定位到 **.class** 文件所在的位置。首先,它找出环境变量 **CLASSPATH**(通过操作系统设置,有时也能通过 Java 的安装程序或基于 Java 的工具设置)。**CLASSPATH** 包含一个或多个目录,用作查找 .**class** 文件的根目录。从根目录开始,Java 解释器获取包名并将每个句点替换成反斜杠,生成一个基于根目录的路径名(取决于你的操作系统,包名 **foo.bar.baz** 变成 **foo\bar\baz** 或 **foo/bar/baz** 或其它)。然后这个路径与 **CLASSPATH** 的不同项连接,解释器就在这些目录中查找与你所创建的类名称相关的 **.class** 文件(解释器还会查找某些涉及 Java 解释器所在位置的标准目录)。 +此技巧的第二部分是把 **package** 名称分解成你机器上的一个目录,所以当 Java 解释器必须要加载一个 .class 文件时,它能定位到 **.class** 文件所在的位置。首先,它找出环境变量 **CLASSPATH**(通过操作系统设置,有时也能通过 Java 的安装程序或基于 Java 的工具设置)。**CLASSPATH** 包含一个或多个目录,用作查找 .**class** 文件的根目录。从根目录开始,Java 解释器获取包名并将每个句点替换成反斜杠,生成一个基于根目录的路径名(取决于你的操作系统,包名 `foo.bar.baz` 变成 `foo\bar\baz` 或 `foo/bar/baz` 或其它)。然后这个路径与 **CLASSPATH** 的不同项连接,解释器就在这些目录中查找与你所创建的类名称相关的 **.class** 文件(解释器还会查找某些涉及 Java 解释器所在位置的标准目录)。 -为了理解这点,比如说我的域名 **MindviewInc.com**,将之反转并全部改为小写后就是 **com.mindviewinc**,这将作为我创建的类的独一无二的全局名称。(com、edu、org等扩展名之前在 Java 包中都是大写,但是 Java 2 之后都统一用小写。)我决定再创建一个名为 **simple** 的类库,从而细分名称: +为了理解这点,比如说我的域名 **MindviewInc.com**,将之反转并全部改为小写后就是 `com.mindviewinc`,这将作为我创建的类的独一无二的全局名称。(com、edu、org等扩展名之前在 Java 包中都是大写,但是 Java 2 之后都统一用小写。)我决定再创建一个名为 **simple** 的类库,从而细分名称: ```java package com.mindviewinc.simple; @@ -159,7 +159,7 @@ C:\DOC\Java\com\mindviewinc\simple (注意,本书的每个文件的第一行注释都指明了文件在源代码目录树中的位置——供本书的自动代码提取工具使用。) -如果你回头看这个路径,会看到包名 **com.mindviewinc.simple**,但是路径的第一部分呢?CLASSPATH 环境变量会处理它。我机器上的环境变量部分如下: +如果你回头看这个路径,会看到包名 `com.mindviewinc.simple`,但是路径的第一部分呢?CLASSPATH 环境变量会处理它。我机器上的环境变量部分如下: ``` CLASSPATH=.;D:\JAVA\LIB;C:\DOC\Java @@ -195,7 +195,7 @@ com.mindviewinc.simple.Vector com.mindviewinc.simple.List ``` -当编译器遇到导入 **simple** 库的 **import** 语句时,它首先会在 CLASSPATH 指定的目录中查找子目录 **com/mindviewinc/simple**,然后从已编译的文件中找出名称相符者(对 **Vector** 而言是 **Vector.class**,对 **List** 而言是 **List.class**)。注意,这两个类和其中要访问的方法都必须是 **public** 修饰的。 +当编译器遇到导入 **simple** 库的 **import** 语句时,它首先会在 CLASSPATH 指定的目录中查找子目录 `com/mindviewinc/simple`,然后从已编译的文件中找出名称相符者(对 **Vector** 而言是 **Vector.class**,对 **List** 而言是 **List.class**)。注意,这两个类和其中要访问的方法都必须是 `public` 修饰的。 对于 Java 新手而言,设置 CLASSPATH 是一件麻烦的事(我最初使用时是这么觉得的),后面版本的 JDK 更加智能。你会发现当你安装好 JDK 时,即使不设置 CLASSPATH,也能够编译和运行基本的 Java 程序。但是,为了编译和运行本书的代码示例(从[https://github.com/BruceEckel/OnJava8-examples](https://github.com/BruceEckel/OnJava8-examples) 取得),你必须将本书程序代码树的基本目录加入到 CLASSPATH 中( gradlew 命令管理自身的 CLASSPATH,所以如果你想直接使用 javac 和 java,不用 Gradle 的话,就需要设置 CLASSPATH)。 @@ -230,9 +230,9 @@ java.util.Vector v = new java.util.Vector(); 具备了以上知识,现在就可以创建自己的工具库来减少重复的程序代码了。 -一般来说,我会使用反转后的域名来命名要创建的工具包,比如 **com.mindviewinc.util**,但为了简化,这里我把工具包命名为 **onjava**。 +一般来说,我会使用反转后的域名来命名要创建的工具包,比如 `com.mindviewinc.util` ,但为了简化,这里我把工具包命名为 **onjava**。 -比如,下面是"控制流"一章中使用到的 `range()` 方法,采用了 for-in 语法进行简单的遍历: +比如,下面是“控制流”一章中使用到的 `range()` 方法,采用了 for-in 语法进行简单的遍历: ```java // onjava/Range.java @@ -270,7 +270,7 @@ public class Range { } ``` -这个文件的位置一定是在某个以一个 CLASSPATH 位置开始,然后接着是 **onjava** 的目录下。编译完之后,就可以在系统的任何地方使用 **import static** 语句来使用这些方法了。 +这个文件的位置一定是在某个以一个 CLASSPATH 位置开始,然后接着是 **onjava** 的目录下。编译完之后,就可以在系统的任何地方使用 `import static` 语句来使用这些方法了。 从现在开始,无论何时你创建了有用的新工具,都可以把它加入到自己的类库中。在本书中,你将会看到更多的组件加入到 **onjava** 库。 @@ -282,7 +282,7 @@ Java 没有 C 的*条件编译*(conditional compilation)功能,该功能 ### 使用包的忠告 -当创建一个包时,包名就隐含了目录结构。这个包必须位于包名指定的目录中,该目录必须在以 CLASSPATH 开始的目录中可以查询到。 最初使用关键字 **package** 可能会有点不顺,因为除非遵守"包名对应目录路径"的规则,否则会收到很多意外的运行时错误信息如找不到特定的类,即使这个类就位于同一目录中。如果你收到类似信息,尝试把 **package** 语句注释掉,如果程序能运行的话,你就知道问题出现在哪里了。 +当创建一个包时,包名就隐含了目录结构。这个包必须位于包名指定的目录中,该目录必须在以 CLASSPATH 开始的目录中可以查询到。 最初使用关键字 `package` 可能会有点不顺,因为除非遵守“包名对应目录路径”的规则,否则会收到很多意外的运行时错误信息如找不到特定的类,即使这个类就位于同一目录中。如果你收到类似信息,尝试把 **package** 语句注释掉,如果程序能运行的话,你就知道问题出现在哪里了。 注意,编译过的代码通常位于与源代码的不同目录中。这是很多工程的标准,而且集成开发环境(IDE)通常会自动为我们做这些。必须保证 JVM 通过 CLASSPATH 能找到编译后的代码。 @@ -290,13 +290,13 @@ Java 没有 C 的*条件编译*(conditional compilation)功能,该功能 ## 访问权限修饰符 -Java 访问权限修饰符 **public**,**protected** 和 **private** 位于定义的类名,属性名和方法名之前。每个访问权限修饰符只能控制它所修饰的对象。 +Java 访问权限修饰符 `public`,`protected` 和 `private` 位于定义的类名,属性名和方法名之前。每个访问权限修饰符只能控制它所修饰的对象。 如果不提供访问修饰符,就意味着"包访问权限"。所以无论如何,万物都有某种形式的访问控制权。接下来的几节中,你将学习各种类型的访问权限。 ### 包访问权限 -本章之前的所有示例要么使用 **public** 访问修饰符,要么就没使用修饰符(*默认访问权限*(default access))。默认访问权限没有关键字,通常被称为*包访问权限*(package access)(有时也称为 **friendly**)。这意味着当前包中的所有其他类都可以访问那个成员。对于这个包之外的类,这个成员看上去是 **private** 的。由于一个编译单元(即一个文件)只能隶属于一个包,所以通过包访问权限,位于同一编译单元中的所有类彼此之间都是可访问的。 +本章之前的所有示例要么使用 `public` 访问修饰符,要么就没使用修饰符(*默认访问权限(default access)*)。默认访问权限没有关键字,通常被称为*包访问权限(package access)*(有时也称为 **friendly**)。这意味着当前包中的所有其他类都可以访问那个成员。对于这个包之外的类,这个成员看上去是 **private** 的。由于一个编译单元(即一个文件)只能隶属于一个包,所以通过包访问权限,位于同一编译单元中的所有类彼此之间都是可访问的。 包访问权限可以把相关类聚到一个包下,以便它们能轻易地相互访问。包里的类赋予了它们包访问权限的成员相互访问的权限,所以你"拥有”了包内的程序代码。只能通过你所拥有的代码去访问你所拥有的其他代码,这样规定很有意义。构建包访问权限机制是将类聚集在包中的重要原因之一。在许多语言中,在文件中组织定义的方式是任意的,但是在 Java 中你被强制以一种合理的方式组织它们。另外,你可能会将不应该对当前包中的类具有访问权限的类排除在包外。 @@ -309,7 +309,7 @@ Java 访问权限修饰符 **public**,**protected** 和 **private** 位于定 ### public: 接口访问权限 -当你使用关键字 **public**,就意味着紧随 public 后声明的成员对于每个人都是可用的,尤其是使用类库的客户端程序员更是如此。假设定义了一个包含下面编译单元的 **dessert** 包: +当你使用关键字 `public`,就意味着紧随 public 后声明的成员对于每个人都是可用的,尤其是使用类库的客户端程序员更是如此。假设定义了一个包含下面编译单元的 **dessert** 包: ```java // hiding/dessert/Cookie.java @@ -388,11 +388,11 @@ class Pie { ### private: 你无法访问 -关键字 **private** 意味着除了包含该成员的类,其他任何类都无法访问这个成员。同一包中的其他类无法访问 **private** 成员,因此这等于说是自己隔离自己。另一方面,让许多人合作创建一个包也是有可能的。使用 **private**,你可以自由地修改那个被修饰的成员,无需担心会影响同一包下的其他类。 +关键字 `private` 意味着除了包含该成员的类,其他任何类都无法访问这个成员。同一包中的其他类无法访问 **private** 成员,因此这等于说是自己隔离自己。另一方面,让许多人合作创建一个包也是有可能的。使用 `private`,你可以自由地修改那个被修饰的成员,无需担心会影响同一包下的其他类。 -默认的包访问权限通常提供了足够的隐藏措施;记住,使用类的客户端程序员无法访问包访问权限成员。这样做很好,因为默认访问权限是一种我们常用的权限(同时也是一种在忘记添加任何访问权限时自动得到的权限)。因此,通常考虑的是把哪些成员声明成 **public** 供客户端程序员使用。所以,最初不常使用关键字 **private**,因为程序没有它也可以照常工作。然而,使用 **private** 是非常重要的,尤其是在多线程环境中。(在"并发编程"一章中将看到)。 +默认的包访问权限通常提供了足够的隐藏措施;记住,使用类的客户端程序员无法访问包访问权限成员。这样做很好,因为默认访问权限是一种我们常用的权限(同时也是一种在忘记添加任何访问权限时自动得到的权限)。因此,通常考虑的是把哪些成员声明成 `public` 供客户端程序员使用。所以,最初不常使用关键字 `private`,因为程序没有它也可以照常工作。然而,使用 `private` 是非常重要的,尤其是在多线程环境中。(在"并发编程"一章中将看到)。 -以下是一个使用 **private** 的例子: +以下是一个使用 `private` 的例子: ```java // hiding/IceCream.java @@ -413,17 +413,17 @@ public class IceCream { } ``` -以上展示了 **private** 的用武之地:控制如何创建对象,防止别人直接访问某个特定的构造器(或全部构造器)。例子中,你无法通过构造器创建一个 **Sundae** 对象,而必须调用 `makeASundae()` 方法创建对象。 +以上展示了 `private` 的用武之地:控制如何创建对象,防止别人直接访问某个特定的构造器(或全部构造器)。例子中,你无法通过构造器创建一个 **Sundae** 对象,而必须调用 `makeASundae()` 方法创建对象。 -任何可以肯定只是该类的"助手"方法,都可以声明为 **private**,以确保不会在包中的其他地方误用它,也防止了你会去改变或删除它。将方法声明为 **private** 确保了你拥有这种选择权。 +任何可以肯定只是该类的"助手"方法,都可以声明为 `private`,以确保不会在包中的其他地方误用它,也防止了你会去改变或删除它。将方法声明为 `private` 确保了你拥有这种选择权。 -对于类中的 **private** 属性也是一样。除非必须公开底层实现(这种情况很少见),否则就将属性声明为 **private**。然而,不能因为类中某个对象的引用是 **private**,就认为其他对象也无法拥有该对象的 **public** 引用(参见附录:对象传递和返回)。 +对于类中的 **private** 属性也是一样。除非必须公开底层实现(这种情况很少见),否则就将属性声明为 `private`。然而,不能因为类中某个对象的引用是 **private**,就认为其他对象也无法拥有该对象的 **public** 引用(参见附录:对象传递和返回)。 ### protected: 继承访问权限 要理解 **protected** 的访问权限,我们在内容上需要作一点跳跃。首先,在介绍本书"复用"章节前,你不必真正理解本节的内容。但为了内容的完整性,这里作了简要介绍,举了个使用 **protected** 的例子。 -关键字 **protected** 处理的是继承的概念,通过继承可以利用一个现有的类——我们称之为基类,然后添加新成员到现有类中而不必碰现有类。我们还可以改变类的现有成员的行为。为了从一个类中继承,需要声明新类 extends 一个现有类,像这样: +关键字 `protected` 处理的是继承的概念,通过继承可以利用一个现有的类——我们称之为基类,然后添加新成员到现有类中而不必碰现有类。我们还可以改变类的现有成员的行为。为了从一个类中继承,需要声明新类 extends 一个现有类,像这样: ```java class Foo extends Bar {} @@ -431,7 +431,7 @@ class Foo extends Bar {} 类定义的其他部分看起来是一样的。 -如果你创建了一个新包,并从另一个包继承类,那么唯一能访问的就是被继承类的 **public** 成员。(如果在同一个包中继承,就可以操作所有的包访问权限的成员。)有时,基类的创建者会希望某个特定成员能被继承类访问,但不能被其他类访问。这时就需要使用 **protected**。**protected** 也提供包访问权限,也就是说,相同包内的其他类可以访问 **protected** 元素。 +如果你创建了一个新包,并从另一个包继承类,那么唯一能访问的就是被继承类的 **public** 成员。(如果在同一个包中继承,就可以操作所有的包访问权限的成员。)有时,基类的创建者会希望某个特定成员能被继承类访问,但不能被其他类访问。这时就需要使用 `protected`。**protected** 也提供包访问权限,也就是说,相同包内的其他类可以访问 **protected** 元素。 回顾下先前的文件 **Cookie.java**,下面的类不能调用包访问权限的方法 `bite()`: @@ -463,7 +463,7 @@ Cookie constructor ChocolateChip constructor ``` -如果类 **Cookie** 中存在一个方法 `bite()`,那么它的任何子类中都存在 `bite()` 方法。但是因为 `bite()` 具有包访问权限并且位于另一个包中,所以我们在这个包中无法使用它。你可以把它声明为 **public**,但这样一来每个人都能访问它,这可能也不是你想要的。如果你将 **Cookie** 改成如下这样: +如果类 **Cookie** 中存在一个方法 `bite()`,那么它的任何子类中都存在 `bite()` 方法。但是因为 `bite()` 具有包访问权限并且位于另一个包中,所以我们在这个包中无法使用它。你可以把它声明为 `public`,但这样一来每个人都能访问它,这可能也不是你想要的。如果你将 **Cookie** 改成如下这样: ```java // hiding/cookie2/Cookie.java @@ -585,7 +585,7 @@ public class OrganizedByAccess { ## 类访问权限 -访问权限修饰符也可以用于确定类库中的哪些类对于类库的使用者是可用的。如果希望某个类可以被客户端程序员使用,就把关键字 **public** 作用于整个类的定义。这甚至控制着客户端程序员能否创建类的对象。 +访问权限修饰符也可以用于确定类库中的哪些类对于类库的使用者是可用的。如果希望某个类可以被客户端程序员使用,就把关键字 `public` 作用于整个类的定义。这甚至控制着客户端程序员能否创建类的对象。 为了控制一个类的访问权限,修饰符必须出现在关键字 **class** 之前: @@ -611,11 +611,11 @@ import hiding.*; 2. **public** 类的名称必须与含有该编译单元的文件名相同,包括大小写。所以对于 **Widget** 来说,文件名必须是 **Widget.java**,不能是 **widget.java** 或 **WIDGET.java**。再次强调,如果名字不匹配,编译器会报错。 3. 虽然不是很常见,但是编译单元内没有 **public** 类也是可能的。这时可以随意命名文件(尽管随意命名会让代码的阅读者和维护者感到困惑)。 -如果获取了一个在 **hiding** 包中的类,只用来完成 **Widget** 或 **hiding** 包下一些其他 **public** 类所要执行的任务,怎么办呢? 你不想自找麻烦为客户端程序员创建说明文档,并且你认为不久后会完全改变原有方案并将旧版本删除,替换成新版本。为了保留此灵活性,需要确保客户端程序员不依赖隐藏在 **hiding** 中的任何特定细节,那么把 **public** 关键字从类中去掉,给予它包访问权限,就可以了。 +如果获取了一个在 **hiding** 包中的类,只用来完成 **Widget** 或 **hiding** 包下一些其他 **public** 类所要执行的任务,怎么办呢? 你不想自找麻烦为客户端程序员创建说明文档,并且你认为不久后会完全改变原有方案并将旧版本删除,替换成新版本。为了保留此灵活性,需要确保客户端程序员不依赖隐藏在 **hiding** 中的任何特定细节,那么把 `public` 关键字从类中去掉,给予它包访问权限,就可以了。 -当你创建了一个包访问权限的类,把类中的属性声明为 **private** 仍然是有意义的——应该尽可能将所有属性都声明为 **private**,但是通常把方法声明成与类(包访问权限)相同的访问权限也是合理的。一个包访问权限的类只能被用于包内,除非强制将某些方法声明为 **public**,这种情况下,编译器会告诉你。 +当你创建了一个包访问权限的类,把类中的属性声明为 `private` 仍然是有意义的——应该尽可能将所有属性都声明为 `private`,但是通常把方法声明成与类(包访问权限)相同的访问权限也是合理的。一个包访问权限的类只能被用于包内,除非强制将某些方法声明为 `public`,这种情况下,编译器会告诉你。 -注意,类既不能是 **private** 的(这样除了该类自身,任何类都不能访问它),也不能是 **protected** 的。所以对于类的访问权限只有两种选择:包访问权限或者 **public**。为了防止类被外界访问,可以将所有的构造器声明为 **private**,这样只有你自己能创建对象(在类的 static 成员中): +注意,类既不能是 **private** 的(这样除了该类自身,任何类都不能访问它),也不能是 **protected** 的。所以对于类的访问权限只有两种选择:包访问权限或者 **public**。为了防止类被外界访问,可以将所有的构造器声明为 `private`,这样只有你自己能创建对象(在类的 static 成员中): ```java // hiding/Lunch.java @@ -662,9 +662,9 @@ public class Lunch { 到目前为止,大部分的方法要么返回 void,要么返回基本类型,所以 [1] 处的定义乍看之下会有点困惑。方法名(**makeSoup**)前面的 **Soup1** 表明了方法返回的类型。到目前为止,这里经常是 **void**,即不返回任何东西。然而也可以返回对象的引用,就像这里一样。这个方法返回了对 **Soup1** 类对象的引用。 -**Soup1** 和 **Soup2** 展示了如何通过将你所有的构造器声明为 **private** 的方式防止直接创建某个类的对象。记住,如果你不显式地创建构造器,编译器会自动为你创建一个无参构造器(没有参数的构造器)。如果我们编写了无参构造器,那么编译器就不会自动创建构造器了。将构造器声明为 **private**,那么谁也无法创建该类的对象了。但是现在别人该怎么使用这个类呢?上述例子给出了两个选择。在 **Soup1** 中,有一个 **static** 方法,它的作用是创建一个新的 **Soup1** 对象并返回对象的引用。如果想要在返回引用之前在 **Soup1** 上做一些额外操作,或是记录创建了多少个 **Soup1** 对象(可以用来限制数量),这种做法是有用的。 +**Soup1** 和 **Soup2** 展示了如何通过将你所有的构造器声明为 `private` 的方式防止直接创建某个类的对象。记住,如果你不显式地创建构造器,编译器会自动为你创建一个无参构造器(没有参数的构造器)。如果我们编写了无参构造器,那么编译器就不会自动创建构造器了。将构造器声明为 `private`,那么谁也无法创建该类的对象了。但是现在别人该怎么使用这个类呢?上述例子给出了两个选择。在 **Soup1** 中,有一个 **static** 方法,它的作用是创建一个新的 **Soup1** 对象并返回对象的引用。如果想要在返回引用之前在 **Soup1** 上做一些额外操作,或是记录创建了多少个 **Soup1** 对象(可以用来限制数量),这种做法是有用的。 -**Soup2** 用到了所谓的*设计模式*(design pattern)。这种模式叫做*单例模式*(singleton),因为它只允许创建类的一个对象。**Soup2** 类的对象是作为 **Soup2** 的 **static** **private** 成员而创建的,所以有且只有一个,你只能通过 **public** 修饰的 `access()` 方法访问到这个对象。 +**Soup2** 用到了所谓的*设计模式*(design pattern)。这种模式叫做*单例模式*(singleton),因为它只允许创建类的一个对象。**Soup2** 类的对象是作为 **Soup2** 的 **static** **private** 成员而创建的,所以有且只有一个,你只能通过 `public` 修饰的 `access()` 方法访问到这个对象。 @@ -676,15 +676,15 @@ public class Lunch { 本章讨论了类库是如何通过类构建的:首先,介绍了将一组类打包到类库的方式,其次介绍了类如何控制对其成员的访问。 -据估计,用 C 语言开发项目,当代码量达到 5 万行和 10 万行时就会出现问题,因为 C 语言只有单一的命名空间,名称开始冲突造成额外的管理开销。在 Java 中,关键字 **package**,包命名模式和关键字 **import** 给了你对于名称的完全控制权,因此可以轻易地避免名称冲突的问题。 +据估计,用 C 语言开发项目,当代码量达到 5 万行和 10 万行时就会出现问题,因为 C 语言只有单一的命名空间,名称开始冲突造成额外的管理开销。在 Java 中,关键字 `package`,包命名模式和关键字 `import` 给了你对于名称的完全控制权,因此可以轻易地避免名称冲突的问题。 -控制成员访问权限有两个原因。第一个原因是使用户不要接触他们不该接触的部分,这部分对于类内部来说是必要的,但是不属于客户端程序员所需接口的一部分。因此将方法和属性声明为 **private** 对于客户端程序员来说是一种服务,可以让他们清楚地看到什么是重要的,什么可以忽略。这可以简化他们对类的理解。 +控制成员访问权限有两个原因。第一个原因是使用户不要接触他们不该接触的部分,这部分对于类内部来说是必要的,但是不属于客户端程序员所需接口的一部分。因此将方法和属性声明为 `private` 对于客户端程序员来说是一种服务,可以让他们清楚地看到什么是重要的,什么可以忽略。这可以简化他们对类的理解。 第二个也是最重要的原因是为了让类库设计者更改类内部的工作方式,而不用担心会影响到客户端程序员。比如最初以某种方式创建一个类,随后发现如果更改代码结构可以极大地提高运行速度。如果接口与实现被明确地隔离和保护,你可以实现这一目的,而不必强制客户端程序员重新编写代码。访问权限控制确保客户端程序员不会依赖某个类的底层实现的任何部分。 当你具备更改底层实现的能力时,不但可以自由地改善设计,还可能会随意地犯错。无论如何细心地计划和设计,都有可能犯错。当了解到犯错是相对安全的时候,你可以更加放心地实验,更快地学会,更快地完成项目。 -类的 **public** 接口是用户真正看到的,所以在分析和设计阶段决定这部分接口是最重要的部分。尽管如此,你仍然有改变的空间。如果最初没有创建出正确的接口,可以添加更多的方法,只要你不删除那些客户端程序员已经在他们的代码中使用的东西。 +类的 `public` 接口是用户真正看到的,所以在分析和设计阶段决定这部分接口是最重要的部分。尽管如此,你仍然有改变的空间。如果最初没有创建出正确的接口,可以添加更多的方法,只要你不删除那些客户端程序员已经在他们的代码中使用的东西。 注意到访问权限控制关注的是类库创建者和外部使用者之间的关系,一种交流方式。很多情况下,事实并非如此。例如,你自己编写了所有的代码,或者在一个小组中工作,所有的东西都放在同一个包下。这些情况下,交流方式则是另外一种,此时严格地遵循访问权限规则也许不是最佳选择,默认(包)访问权限也许就足够好了。 From 1d18011101343ff514cad335be4fca69e92cd141 Mon Sep 17 00:00:00 2001 From: oxygenkun Date: Sun, 9 Feb 2020 14:46:43 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E7=9A=84=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/book/07-Implementation-Hiding.md | 66 +++++++++++++-------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/docs/book/07-Implementation-Hiding.md b/docs/book/07-Implementation-Hiding.md index eb8ba49a..9a6b325f 100644 --- a/docs/book/07-Implementation-Hiding.md +++ b/docs/book/07-Implementation-Hiding.md @@ -3,7 +3,7 @@ # 第七章 封装 -> *访问控制(Access control)*(或者*隐藏实现(Access control)*)与“最初的实现不恰当”有关。 +> *访问控制(Access control)*(或者*隐藏实现(implementation hiding)*)与“最初的实现不恰当”有关。 所有优秀的作者——包括那些编写软件的人——都知道一件好的作品都是经过反复打磨才变得优秀的。如果你把一段代码置于某个位置一段时间,过一会重新来看,你可能发现更好的实现方式。这是*重构*(refactoring)的原动力之一,重构就是重写可工作的代码,使之更加可读,易懂,因而更易维护。 @@ -15,7 +15,7 @@ 为了解决这一问题,Java 提供了*访问修饰符*(access specifier)供类库开发者指明哪些对于客户端程序员是可用的,哪些是不可用的。访问控制权限的等级,从“最大权限”到“最小权限”依次是:**public**,**protected**,*包访问权限(package access)*(没有关键字)和 **private**。根据上一段的内容,你可能会想,作为一名类库设计者,你会尽可能将一切都设为 **private**,仅向客户端程序员暴露你愿意他们使用的方法。这就是你通常所做的,尽管这与那些使用其他语言(尤其是 C)编程以及习惯了不受限制地访问任何东西的人们的直觉相违背。 -然而,类库组件的概念和对类库组件访问的控制仍然不完善。其中仍然存在问题就是如何将类库组件捆绑到一个内聚的类库单元中。Java 中通过 `package` 关键字加以控制,类在相同包下还是在不同包下,会影响访问修饰符。所以在这章开始,你将会学习如何将类库组件置于同一个包下,之后你就能明白访问修饰符的全部含义。 +然而,类库组件的概念和对类库组件访问的控制仍然不完善。其中仍然存在问题就是如何将类库组件捆绑到一个内聚的类库单元中。Java 中通过 **package** 关键字加以控制,类在相同包下还是在不同包下,会影响访问修饰符。所以在这章开始,你将会学习如何将类库组件置于同一个包下,之后你就能明白访问修饰符的全部含义。 @@ -35,7 +35,7 @@ public class FullQualification { } ``` -这种方式使得程序冗长乏味,因此你可以换一种方式,使用 `import` 关键字。如果需要导入某个类,就需要在 **import** 语句中声明: +这种方式使得程序冗长乏味,因此你可以换一种方式,使用 **import** 关键字。如果需要导入某个类,就需要在 **import** 语句中声明: ```java // hiding/SingleImport.java @@ -57,13 +57,13 @@ import java.util.* 到目前为止的大部分示例都只存在单个文件,并为本地使用的,所以尚未受到包名的干扰。但是,这些示例其实已经位于包中了,叫做“未命名”包或*默认包*(default package)。这当然是一种选择,为了简单起见,本书其余部分会尽可能采用这种方式。但是,如果你打算为相同机器上的其他 Java 程序创建友好的类库或程序时,就必须仔细考虑以防类名冲突。 -一个 Java 源代码文件称为一个*编译单元(compilation unit)*(有时也称*翻译单元(translation unit)*)。每个编译单元的文件名后缀必须是 **.java**。在编译单元中可以有一个 **public** 类,它的类名必须与文件名相同(包括大小写,但不包括后缀名 **.java**)。每个编译单元中只能有*一个* **public** 类,否则编译器不接受。如果这个编译单元中还有其他类,那么在包之外是无法访问到这些类的,因为它们*不是* **public** 类,此时它们为主 **public** 类提供“支持”类 。 +一个 Java 源代码文件称为一个*编译单元(compilation unit)*(有时也称*翻译单元(translation unit)*)。每个编译单元的文件名后缀必须是 **.java**。在编译单元中可以有一个 **public** 类,它的类名必须与文件名相同(包括大小写,但不包括后缀名 **.java**)。每个编译单元中只能有一个 **public** 类,否则编译器不接受。如果这个编译单元中还有其他类,那么在包之外是无法访问到这些类的,因为它们不是 **public** 类,此时它们为主 **public** 类提供“支持”类 。 ### 代码组织 当编译一个 **.java** 文件时,**.java** 文件的每个类都会有一个输出文件。每个输出的文件名和 **.java** 文件中每个类的类名相同,只是后缀名是 **.class**。因此,在编译少量的 **.java** 文件后,会得到大量的 **.class** 文件。如果你使用过编译型语言,那么你可能习惯编译后产生一个中间文件(通常称为“obj”文件),然后与使用链接器(创建可执行文件)或类库生成器(创建类库)产生的其他同类文件打包到一起的情况。这不是 Java 工作的方式。在 Java 中,可运行程序是一组 **.class** 文件,它们可以打包压缩成一个 Java 文档文件(JAR,使用 **jar** 文档生成器)。Java 解释器负责查找、加载和解释这些文件。 -类库是一组类文件。每个源文件通常都含有一个 **public** 类和任意数量的非 **public** 类,因此每个文件都有一个 **public** 组件。如果把这些组件集中在一起,就需要使用关键字 `package`。 +类库是一组类文件。每个源文件通常都含有一个 **public** 类和任意数量的非 **public** 类,因此每个文件都有一个 **public** 组件。如果把这些组件集中在一起,就需要使用关键字 **package**。 如果你使用了 **package** 语句,它必须是文件中除了注释之外的第一行代码。当你如下这样写: @@ -71,7 +71,7 @@ import java.util.* package hiding; ``` -意味着这个编译单元是一个名为 **hiding** 类库的一部分。换句话说,你正在声明的编译单元中的 **public** 类名称位于名为 **hiding** 的保护伞下。任何人想要使用该名称,必须指明完整的类名或者使用 `import` 关键字导入 **hiding** 。(注意,Java 包名按惯例一律小写,即使中间的单词也需要小写,与驼峰命名不同) +意味着这个编译单元是一个名为 **hiding** 类库的一部分。换句话说,你正在声明的编译单元中的 **public** 类名称位于名为 **hiding** 的保护伞下。任何人想要使用该名称,必须指明完整的类名或者使用 **import** 关键字导入 **hiding** 。(注意,Java 包名按惯例一律小写,即使中间的单词也需要小写,与驼峰命名不同) 例如,假设文件名是 **MyClass.java** ,这意味着文件中只能有一个 **public** 类,且类名必须是 **MyClass**(大小写也与文件名相同): @@ -84,7 +84,7 @@ public class MyClass { } ``` -现在,如果有人想使用 **MyClass** 或 **hiding.mypackage** 中的其他 **public** 类,就必须使用关键字 `import` 来使 **hiding.mypackage** 中的名称可用。还有一种选择是使用完整的名称: +现在,如果有人想使用 **MyClass** 或 **hiding.mypackage** 中的其他 **public** 类,就必须使用关键字 **import** 来使 **hiding.mypackage** 中的名称可用。还有一种选择是使用完整的名称: ```java // hiding/QualifiedMyClass.java @@ -96,7 +96,7 @@ public class QualifiedMyClass { } ``` -关键字 `import` 使之更简洁: +关键字 **import** 使之更简洁: ```java // hiding/ImportedMyClass.java @@ -109,7 +109,7 @@ public class ImportedMyClass { } ``` -`package` 和 `import` 这两个关键字将单一的全局命名空间分隔开,从而避免名称冲突。 +**package** 和 **import** 这两个关键字将单一的全局命名空间分隔开,从而避免名称冲突。 ### 创建独一无二的包名 @@ -195,7 +195,7 @@ com.mindviewinc.simple.Vector com.mindviewinc.simple.List ``` -当编译器遇到导入 **simple** 库的 **import** 语句时,它首先会在 CLASSPATH 指定的目录中查找子目录 `com/mindviewinc/simple`,然后从已编译的文件中找出名称相符者(对 **Vector** 而言是 **Vector.class**,对 **List** 而言是 **List.class**)。注意,这两个类和其中要访问的方法都必须是 `public` 修饰的。 +当编译器遇到导入 **simple** 库的 **import** 语句时,它首先会在 CLASSPATH 指定的目录中查找子目录 `com/mindviewinc/simple`,然后从已编译的文件中找出名称相符者(对 **Vector** 而言是 **Vector.class**,对 **List** 而言是 **List.class**)。注意,这两个类和其中要访问的方法都必须是 **public** 修饰的。 对于 Java 新手而言,设置 CLASSPATH 是一件麻烦的事(我最初使用时是这么觉得的),后面版本的 JDK 更加智能。你会发现当你安装好 JDK 时,即使不设置 CLASSPATH,也能够编译和运行基本的 Java 程序。但是,为了编译和运行本书的代码示例(从[https://github.com/BruceEckel/OnJava8-examples](https://github.com/BruceEckel/OnJava8-examples) 取得),你必须将本书程序代码树的基本目录加入到 CLASSPATH 中( gradlew 命令管理自身的 CLASSPATH,所以如果你想直接使用 javac 和 java,不用 Gradle 的话,就需要设置 CLASSPATH)。 @@ -270,7 +270,7 @@ public class Range { } ``` -这个文件的位置一定是在某个以一个 CLASSPATH 位置开始,然后接着是 **onjava** 的目录下。编译完之后,就可以在系统的任何地方使用 `import static` 语句来使用这些方法了。 +这个文件的位置一定是在某个以一个 CLASSPATH 位置开始,然后接着是 **onjava** 的目录下。编译完之后,就可以在系统的任何地方使用 **import static** 语句来使用这些方法了。 从现在开始,无论何时你创建了有用的新工具,都可以把它加入到自己的类库中。在本书中,你将会看到更多的组件加入到 **onjava** 库。 @@ -282,7 +282,7 @@ Java 没有 C 的*条件编译*(conditional compilation)功能,该功能 ### 使用包的忠告 -当创建一个包时,包名就隐含了目录结构。这个包必须位于包名指定的目录中,该目录必须在以 CLASSPATH 开始的目录中可以查询到。 最初使用关键字 `package` 可能会有点不顺,因为除非遵守“包名对应目录路径”的规则,否则会收到很多意外的运行时错误信息如找不到特定的类,即使这个类就位于同一目录中。如果你收到类似信息,尝试把 **package** 语句注释掉,如果程序能运行的话,你就知道问题出现在哪里了。 +当创建一个包时,包名就隐含了目录结构。这个包必须位于包名指定的目录中,该目录必须在以 CLASSPATH 开始的目录中可以查询到。 最初使用关键字 **package** 可能会有点不顺,因为除非遵守“包名对应目录路径”的规则,否则会收到很多意外的运行时错误信息如找不到特定的类,即使这个类就位于同一目录中。如果你收到类似信息,尝试把 **package** 语句注释掉,如果程序能运行的话,你就知道问题出现在哪里了。 注意,编译过的代码通常位于与源代码的不同目录中。这是很多工程的标准,而且集成开发环境(IDE)通常会自动为我们做这些。必须保证 JVM 通过 CLASSPATH 能找到编译后的代码。 @@ -290,13 +290,13 @@ Java 没有 C 的*条件编译*(conditional compilation)功能,该功能 ## 访问权限修饰符 -Java 访问权限修饰符 `public`,`protected` 和 `private` 位于定义的类名,属性名和方法名之前。每个访问权限修饰符只能控制它所修饰的对象。 +Java 访问权限修饰符 **public**,**protected** 和 **private** 位于定义的类名,属性名和方法名之前。每个访问权限修饰符只能控制它所修饰的对象。 如果不提供访问修饰符,就意味着"包访问权限"。所以无论如何,万物都有某种形式的访问控制权。接下来的几节中,你将学习各种类型的访问权限。 ### 包访问权限 -本章之前的所有示例要么使用 `public` 访问修饰符,要么就没使用修饰符(*默认访问权限(default access)*)。默认访问权限没有关键字,通常被称为*包访问权限(package access)*(有时也称为 **friendly**)。这意味着当前包中的所有其他类都可以访问那个成员。对于这个包之外的类,这个成员看上去是 **private** 的。由于一个编译单元(即一个文件)只能隶属于一个包,所以通过包访问权限,位于同一编译单元中的所有类彼此之间都是可访问的。 +本章之前的所有示例要么使用 **public** 访问修饰符,要么就没使用修饰符(*默认访问权限(default access)*)。默认访问权限没有关键字,通常被称为*包访问权限(package access)*(有时也称为 **friendly**)。这意味着当前包中的所有其他类都可以访问那个成员。对于这个包之外的类,这个成员看上去是 **private** 的。由于一个编译单元(即一个文件)只能隶属于一个包,所以通过包访问权限,位于同一编译单元中的所有类彼此之间都是可访问的。 包访问权限可以把相关类聚到一个包下,以便它们能轻易地相互访问。包里的类赋予了它们包访问权限的成员相互访问的权限,所以你"拥有”了包内的程序代码。只能通过你所拥有的代码去访问你所拥有的其他代码,这样规定很有意义。构建包访问权限机制是将类聚集在包中的重要原因之一。在许多语言中,在文件中组织定义的方式是任意的,但是在 Java 中你被强制以一种合理的方式组织它们。另外,你可能会将不应该对当前包中的类具有访问权限的类排除在包外。 @@ -309,7 +309,7 @@ Java 访问权限修饰符 `public`,`protected` 和 `private` 位于定义的 ### public: 接口访问权限 -当你使用关键字 `public`,就意味着紧随 public 后声明的成员对于每个人都是可用的,尤其是使用类库的客户端程序员更是如此。假设定义了一个包含下面编译单元的 **dessert** 包: +当你使用关键字 **public**,就意味着紧随 public 后声明的成员对于每个人都是可用的,尤其是使用类库的客户端程序员更是如此。假设定义了一个包含下面编译单元的 **dessert** 包: ```java // hiding/dessert/Cookie.java @@ -388,11 +388,11 @@ class Pie { ### private: 你无法访问 -关键字 `private` 意味着除了包含该成员的类,其他任何类都无法访问这个成员。同一包中的其他类无法访问 **private** 成员,因此这等于说是自己隔离自己。另一方面,让许多人合作创建一个包也是有可能的。使用 `private`,你可以自由地修改那个被修饰的成员,无需担心会影响同一包下的其他类。 +关键字 **private** 意味着除了包含该成员的类,其他任何类都无法访问这个成员。同一包中的其他类无法访问 **private** 成员,因此这等于说是自己隔离自己。另一方面,让许多人合作创建一个包也是有可能的。使用 **private**,你可以自由地修改那个被修饰的成员,无需担心会影响同一包下的其他类。 -默认的包访问权限通常提供了足够的隐藏措施;记住,使用类的客户端程序员无法访问包访问权限成员。这样做很好,因为默认访问权限是一种我们常用的权限(同时也是一种在忘记添加任何访问权限时自动得到的权限)。因此,通常考虑的是把哪些成员声明成 `public` 供客户端程序员使用。所以,最初不常使用关键字 `private`,因为程序没有它也可以照常工作。然而,使用 `private` 是非常重要的,尤其是在多线程环境中。(在"并发编程"一章中将看到)。 +默认的包访问权限通常提供了足够的隐藏措施;记住,使用类的客户端程序员无法访问包访问权限成员。这样做很好,因为默认访问权限是一种我们常用的权限(同时也是一种在忘记添加任何访问权限时自动得到的权限)。因此,通常考虑的是把哪些成员声明成 **public** 供客户端程序员使用。所以,最初不常使用关键字 **private**,因为程序没有它也可以照常工作。然而,使用 **private** 是非常重要的,尤其是在多线程环境中。(在"并发编程"一章中将看到)。 -以下是一个使用 `private` 的例子: +以下是一个使用 **private** 的例子: ```java // hiding/IceCream.java @@ -413,17 +413,17 @@ public class IceCream { } ``` -以上展示了 `private` 的用武之地:控制如何创建对象,防止别人直接访问某个特定的构造器(或全部构造器)。例子中,你无法通过构造器创建一个 **Sundae** 对象,而必须调用 `makeASundae()` 方法创建对象。 +以上展示了 **private** 的用武之地:控制如何创建对象,防止别人直接访问某个特定的构造器(或全部构造器)。例子中,你无法通过构造器创建一个 **Sundae** 对象,而必须调用 `makeASundae()` 方法创建对象。 -任何可以肯定只是该类的"助手"方法,都可以声明为 `private`,以确保不会在包中的其他地方误用它,也防止了你会去改变或删除它。将方法声明为 `private` 确保了你拥有这种选择权。 +任何可以肯定只是该类的"助手"方法,都可以声明为 **private**,以确保不会在包中的其他地方误用它,也防止了你会去改变或删除它。将方法声明为 **private** 确保了你拥有这种选择权。 -对于类中的 **private** 属性也是一样。除非必须公开底层实现(这种情况很少见),否则就将属性声明为 `private`。然而,不能因为类中某个对象的引用是 **private**,就认为其他对象也无法拥有该对象的 **public** 引用(参见附录:对象传递和返回)。 +对于类中的 **private** 属性也是一样。除非必须公开底层实现(这种情况很少见),否则就将属性声明为 **private**。然而,不能因为类中某个对象的引用是 **private**,就认为其他对象也无法拥有该对象的 **public** 引用(参见附录:对象传递和返回)。 ### protected: 继承访问权限 要理解 **protected** 的访问权限,我们在内容上需要作一点跳跃。首先,在介绍本书"复用"章节前,你不必真正理解本节的内容。但为了内容的完整性,这里作了简要介绍,举了个使用 **protected** 的例子。 -关键字 `protected` 处理的是继承的概念,通过继承可以利用一个现有的类——我们称之为基类,然后添加新成员到现有类中而不必碰现有类。我们还可以改变类的现有成员的行为。为了从一个类中继承,需要声明新类 extends 一个现有类,像这样: +关键字 **protected** 处理的是继承的概念,通过继承可以利用一个现有的类——我们称之为基类,然后添加新成员到现有类中而不必碰现有类。我们还可以改变类的现有成员的行为。为了从一个类中继承,需要声明新类 extends 一个现有类,像这样: ```java class Foo extends Bar {} @@ -431,7 +431,7 @@ class Foo extends Bar {} 类定义的其他部分看起来是一样的。 -如果你创建了一个新包,并从另一个包继承类,那么唯一能访问的就是被继承类的 **public** 成员。(如果在同一个包中继承,就可以操作所有的包访问权限的成员。)有时,基类的创建者会希望某个特定成员能被继承类访问,但不能被其他类访问。这时就需要使用 `protected`。**protected** 也提供包访问权限,也就是说,相同包内的其他类可以访问 **protected** 元素。 +如果你创建了一个新包,并从另一个包继承类,那么唯一能访问的就是被继承类的 **public** 成员。(如果在同一个包中继承,就可以操作所有的包访问权限的成员。)有时,基类的创建者会希望某个特定成员能被继承类访问,但不能被其他类访问。这时就需要使用 **protected**。**protected** 也提供包访问权限,也就是说,相同包内的其他类可以访问 **protected** 元素。 回顾下先前的文件 **Cookie.java**,下面的类不能调用包访问权限的方法 `bite()`: @@ -463,7 +463,7 @@ Cookie constructor ChocolateChip constructor ``` -如果类 **Cookie** 中存在一个方法 `bite()`,那么它的任何子类中都存在 `bite()` 方法。但是因为 `bite()` 具有包访问权限并且位于另一个包中,所以我们在这个包中无法使用它。你可以把它声明为 `public`,但这样一来每个人都能访问它,这可能也不是你想要的。如果你将 **Cookie** 改成如下这样: +如果类 **Cookie** 中存在一个方法 `bite()`,那么它的任何子类中都存在 `bite()` 方法。但是因为 `bite()` 具有包访问权限并且位于另一个包中,所以我们在这个包中无法使用它。你可以把它声明为 **public**,但这样一来每个人都能访问它,这可能也不是你想要的。如果你将 **Cookie** 改成如下这样: ```java // hiding/cookie2/Cookie.java @@ -585,7 +585,7 @@ public class OrganizedByAccess { ## 类访问权限 -访问权限修饰符也可以用于确定类库中的哪些类对于类库的使用者是可用的。如果希望某个类可以被客户端程序员使用,就把关键字 `public` 作用于整个类的定义。这甚至控制着客户端程序员能否创建类的对象。 +访问权限修饰符也可以用于确定类库中的哪些类对于类库的使用者是可用的。如果希望某个类可以被客户端程序员使用,就把关键字 **public** 作用于整个类的定义。这甚至控制着客户端程序员能否创建类的对象。 为了控制一个类的访问权限,修饰符必须出现在关键字 **class** 之前: @@ -611,11 +611,11 @@ import hiding.*; 2. **public** 类的名称必须与含有该编译单元的文件名相同,包括大小写。所以对于 **Widget** 来说,文件名必须是 **Widget.java**,不能是 **widget.java** 或 **WIDGET.java**。再次强调,如果名字不匹配,编译器会报错。 3. 虽然不是很常见,但是编译单元内没有 **public** 类也是可能的。这时可以随意命名文件(尽管随意命名会让代码的阅读者和维护者感到困惑)。 -如果获取了一个在 **hiding** 包中的类,只用来完成 **Widget** 或 **hiding** 包下一些其他 **public** 类所要执行的任务,怎么办呢? 你不想自找麻烦为客户端程序员创建说明文档,并且你认为不久后会完全改变原有方案并将旧版本删除,替换成新版本。为了保留此灵活性,需要确保客户端程序员不依赖隐藏在 **hiding** 中的任何特定细节,那么把 `public` 关键字从类中去掉,给予它包访问权限,就可以了。 +如果获取了一个在 **hiding** 包中的类,只用来完成 **Widget** 或 **hiding** 包下一些其他 **public** 类所要执行的任务,怎么办呢? 你不想自找麻烦为客户端程序员创建说明文档,并且你认为不久后会完全改变原有方案并将旧版本删除,替换成新版本。为了保留此灵活性,需要确保客户端程序员不依赖隐藏在 **hiding** 中的任何特定细节,那么把 **public** 关键字从类中去掉,给予它包访问权限,就可以了。 -当你创建了一个包访问权限的类,把类中的属性声明为 `private` 仍然是有意义的——应该尽可能将所有属性都声明为 `private`,但是通常把方法声明成与类(包访问权限)相同的访问权限也是合理的。一个包访问权限的类只能被用于包内,除非强制将某些方法声明为 `public`,这种情况下,编译器会告诉你。 +当你创建了一个包访问权限的类,把类中的属性声明为 **private** 仍然是有意义的——应该尽可能将所有属性都声明为 **private**,但是通常把方法声明成与类(包访问权限)相同的访问权限也是合理的。一个包访问权限的类只能被用于包内,除非强制将某些方法声明为 **public**,这种情况下,编译器会告诉你。 -注意,类既不能是 **private** 的(这样除了该类自身,任何类都不能访问它),也不能是 **protected** 的。所以对于类的访问权限只有两种选择:包访问权限或者 **public**。为了防止类被外界访问,可以将所有的构造器声明为 `private`,这样只有你自己能创建对象(在类的 static 成员中): +注意,类既不能是 **private** 的(这样除了该类自身,任何类都不能访问它),也不能是 **protected** 的。所以对于类的访问权限只有两种选择:包访问权限或者 **public**。为了防止类被外界访问,可以将所有的构造器声明为 **private**,这样只有你自己能创建对象(在类的 static 成员中): ```java // hiding/Lunch.java @@ -662,9 +662,9 @@ public class Lunch { 到目前为止,大部分的方法要么返回 void,要么返回基本类型,所以 [1] 处的定义乍看之下会有点困惑。方法名(**makeSoup**)前面的 **Soup1** 表明了方法返回的类型。到目前为止,这里经常是 **void**,即不返回任何东西。然而也可以返回对象的引用,就像这里一样。这个方法返回了对 **Soup1** 类对象的引用。 -**Soup1** 和 **Soup2** 展示了如何通过将你所有的构造器声明为 `private` 的方式防止直接创建某个类的对象。记住,如果你不显式地创建构造器,编译器会自动为你创建一个无参构造器(没有参数的构造器)。如果我们编写了无参构造器,那么编译器就不会自动创建构造器了。将构造器声明为 `private`,那么谁也无法创建该类的对象了。但是现在别人该怎么使用这个类呢?上述例子给出了两个选择。在 **Soup1** 中,有一个 **static** 方法,它的作用是创建一个新的 **Soup1** 对象并返回对象的引用。如果想要在返回引用之前在 **Soup1** 上做一些额外操作,或是记录创建了多少个 **Soup1** 对象(可以用来限制数量),这种做法是有用的。 +**Soup1** 和 **Soup2** 展示了如何通过将你所有的构造器声明为 **private** 的方式防止直接创建某个类的对象。记住,如果你不显式地创建构造器,编译器会自动为你创建一个无参构造器(没有参数的构造器)。如果我们编写了无参构造器,那么编译器就不会自动创建构造器了。将构造器声明为 **private**,那么谁也无法创建该类的对象了。但是现在别人该怎么使用这个类呢?上述例子给出了两个选择。在 **Soup1** 中,有一个 **static** 方法,它的作用是创建一个新的 **Soup1** 对象并返回对象的引用。如果想要在返回引用之前在 **Soup1** 上做一些额外操作,或是记录创建了多少个 **Soup1** 对象(可以用来限制数量),这种做法是有用的。 -**Soup2** 用到了所谓的*设计模式*(design pattern)。这种模式叫做*单例模式*(singleton),因为它只允许创建类的一个对象。**Soup2** 类的对象是作为 **Soup2** 的 **static** **private** 成员而创建的,所以有且只有一个,你只能通过 `public` 修饰的 `access()` 方法访问到这个对象。 +**Soup2** 用到了所谓的*设计模式*(design pattern)。这种模式叫做*单例模式*(singleton),因为它只允许创建类的一个对象。**Soup2** 类的对象是作为 **Soup2** 的 **static** **private** 成员而创建的,所以有且只有一个,你只能通过 **public** 修饰的 `access()` 方法访问到这个对象。 @@ -676,15 +676,15 @@ public class Lunch { 本章讨论了类库是如何通过类构建的:首先,介绍了将一组类打包到类库的方式,其次介绍了类如何控制对其成员的访问。 -据估计,用 C 语言开发项目,当代码量达到 5 万行和 10 万行时就会出现问题,因为 C 语言只有单一的命名空间,名称开始冲突造成额外的管理开销。在 Java 中,关键字 `package`,包命名模式和关键字 `import` 给了你对于名称的完全控制权,因此可以轻易地避免名称冲突的问题。 +据估计,用 C 语言开发项目,当代码量达到 5 万行和 10 万行时就会出现问题,因为 C 语言只有单一的命名空间,名称开始冲突造成额外的管理开销。在 Java 中,关键字 **package**,包命名模式和关键字 **import** 给了你对于名称的完全控制权,因此可以轻易地避免名称冲突的问题。 -控制成员访问权限有两个原因。第一个原因是使用户不要接触他们不该接触的部分,这部分对于类内部来说是必要的,但是不属于客户端程序员所需接口的一部分。因此将方法和属性声明为 `private` 对于客户端程序员来说是一种服务,可以让他们清楚地看到什么是重要的,什么可以忽略。这可以简化他们对类的理解。 +控制成员访问权限有两个原因。第一个原因是使用户不要接触他们不该接触的部分,这部分对于类内部来说是必要的,但是不属于客户端程序员所需接口的一部分。因此将方法和属性声明为 **private** 对于客户端程序员来说是一种服务,可以让他们清楚地看到什么是重要的,什么可以忽略。这可以简化他们对类的理解。 第二个也是最重要的原因是为了让类库设计者更改类内部的工作方式,而不用担心会影响到客户端程序员。比如最初以某种方式创建一个类,随后发现如果更改代码结构可以极大地提高运行速度。如果接口与实现被明确地隔离和保护,你可以实现这一目的,而不必强制客户端程序员重新编写代码。访问权限控制确保客户端程序员不会依赖某个类的底层实现的任何部分。 当你具备更改底层实现的能力时,不但可以自由地改善设计,还可能会随意地犯错。无论如何细心地计划和设计,都有可能犯错。当了解到犯错是相对安全的时候,你可以更加放心地实验,更快地学会,更快地完成项目。 -类的 `public` 接口是用户真正看到的,所以在分析和设计阶段决定这部分接口是最重要的部分。尽管如此,你仍然有改变的空间。如果最初没有创建出正确的接口,可以添加更多的方法,只要你不删除那些客户端程序员已经在他们的代码中使用的东西。 +类的 **public** 接口是用户真正看到的,所以在分析和设计阶段决定这部分接口是最重要的部分。尽管如此,你仍然有改变的空间。如果最初没有创建出正确的接口,可以添加更多的方法,只要你不删除那些客户端程序员已经在他们的代码中使用的东西。 注意到访问权限控制关注的是类库创建者和外部使用者之间的关系,一种交流方式。很多情况下,事实并非如此。例如,你自己编写了所有的代码,或者在一个小组中工作,所有的东西都放在同一个包下。这些情况下,交流方式则是另外一种,此时严格地遵循访问权限规则也许不是最佳选择,默认(包)访问权限也许就足够好了。 From 1ceff4e98f4eb08c15fffc7db752872eb342009e Mon Sep 17 00:00:00 2001 From: oxygenkun Date: Sun, 9 Feb 2020 15:00:34 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E7=9A=84=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/book/07-Implementation-Hiding.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/book/07-Implementation-Hiding.md b/docs/book/07-Implementation-Hiding.md index 9a6b325f..884466c1 100644 --- a/docs/book/07-Implementation-Hiding.md +++ b/docs/book/07-Implementation-Hiding.md @@ -117,9 +117,9 @@ public class ImportedMyClass { 将所有的文件放在一个子目录还解决了其他的两个问题:创建独一无二的包名和查找可能隐藏于目录结构某处的类。这是通过将 **.class** 文件所在的路径位置编码成 **package** 名称来实现的。按照惯例,**package** 名称是类的创建者的反顺序的 Internet 域名。如果你遵循惯例,因为 Internet 域名是独一无二的,所以你的 **package** 名称也应该是独一无二的,不会发生名称冲突。如果你没有自己的域名,你就得构造一组不大可能与他人重复的组合(比如你的姓名),来创建独一无二的 package 名称。如果你打算发布 Java 程序代码,那么花些力气去获取一个域名是值得的。 -此技巧的第二部分是把 **package** 名称分解成你机器上的一个目录,所以当 Java 解释器必须要加载一个 .class 文件时,它能定位到 **.class** 文件所在的位置。首先,它找出环境变量 **CLASSPATH**(通过操作系统设置,有时也能通过 Java 的安装程序或基于 Java 的工具设置)。**CLASSPATH** 包含一个或多个目录,用作查找 .**class** 文件的根目录。从根目录开始,Java 解释器获取包名并将每个句点替换成反斜杠,生成一个基于根目录的路径名(取决于你的操作系统,包名 `foo.bar.baz` 变成 `foo\bar\baz` 或 `foo/bar/baz` 或其它)。然后这个路径与 **CLASSPATH** 的不同项连接,解释器就在这些目录中查找与你所创建的类名称相关的 **.class** 文件(解释器还会查找某些涉及 Java 解释器所在位置的标准目录)。 +此技巧的第二部分是把 **package** 名称分解成你机器上的一个目录,所以当 Java 解释器必须要加载一个 .class 文件时,它能定位到 **.class** 文件所在的位置。首先,它找出环境变量 **CLASSPATH**(通过操作系统设置,有时也能通过 Java 的安装程序或基于 Java 的工具设置)。**CLASSPATH** 包含一个或多个目录,用作查找 .**class** 文件的根目录。从根目录开始,Java 解释器获取包名并将每个句点替换成反斜杠,生成一个基于根目录的路径名(取决于你的操作系统,包名 **foo.bar.baz** 变成 **foo\bar\baz** 或 **foo/bar/baz** 或其它)。然后这个路径与 **CLASSPATH** 的不同项连接,解释器就在这些目录中查找与你所创建的类名称相关的 **.class** 文件(解释器还会查找某些涉及 Java 解释器所在位置的标准目录)。 -为了理解这点,比如说我的域名 **MindviewInc.com**,将之反转并全部改为小写后就是 `com.mindviewinc`,这将作为我创建的类的独一无二的全局名称。(com、edu、org等扩展名之前在 Java 包中都是大写,但是 Java 2 之后都统一用小写。)我决定再创建一个名为 **simple** 的类库,从而细分名称: +为了理解这点,比如说我的域名 **MindviewInc.com**,将之反转并全部改为小写后就是 **com.mindviewinc**,这将作为我创建的类的独一无二的全局名称。(com、edu、org等扩展名之前在 Java 包中都是大写,但是 Java 2 之后都统一用小写。)我决定再创建一个名为 **simple** 的类库,从而细分名称: ```java package com.mindviewinc.simple; @@ -159,7 +159,7 @@ C:\DOC\Java\com\mindviewinc\simple (注意,本书的每个文件的第一行注释都指明了文件在源代码目录树中的位置——供本书的自动代码提取工具使用。) -如果你回头看这个路径,会看到包名 `com.mindviewinc.simple`,但是路径的第一部分呢?CLASSPATH 环境变量会处理它。我机器上的环境变量部分如下: +如果你回头看这个路径,会看到包名 **com.mindviewinc.simple**,但是路径的第一部分呢?CLASSPATH 环境变量会处理它。我机器上的环境变量部分如下: ``` CLASSPATH=.;D:\JAVA\LIB;C:\DOC\Java @@ -195,7 +195,7 @@ com.mindviewinc.simple.Vector com.mindviewinc.simple.List ``` -当编译器遇到导入 **simple** 库的 **import** 语句时,它首先会在 CLASSPATH 指定的目录中查找子目录 `com/mindviewinc/simple`,然后从已编译的文件中找出名称相符者(对 **Vector** 而言是 **Vector.class**,对 **List** 而言是 **List.class**)。注意,这两个类和其中要访问的方法都必须是 **public** 修饰的。 +当编译器遇到导入 **simple** 库的 **import** 语句时,它首先会在 CLASSPATH 指定的目录中查找子目录 **com/mindviewinc/simple**,然后从已编译的文件中找出名称相符者(对 **Vector** 而言是 **Vector.class**,对 **List** 而言是 **List.class**)。注意,这两个类和其中要访问的方法都必须是 **public** 修饰的。 对于 Java 新手而言,设置 CLASSPATH 是一件麻烦的事(我最初使用时是这么觉得的),后面版本的 JDK 更加智能。你会发现当你安装好 JDK 时,即使不设置 CLASSPATH,也能够编译和运行基本的 Java 程序。但是,为了编译和运行本书的代码示例(从[https://github.com/BruceEckel/OnJava8-examples](https://github.com/BruceEckel/OnJava8-examples) 取得),你必须将本书程序代码树的基本目录加入到 CLASSPATH 中( gradlew 命令管理自身的 CLASSPATH,所以如果你想直接使用 javac 和 java,不用 Gradle 的话,就需要设置 CLASSPATH)。 @@ -230,7 +230,7 @@ java.util.Vector v = new java.util.Vector(); 具备了以上知识,现在就可以创建自己的工具库来减少重复的程序代码了。 -一般来说,我会使用反转后的域名来命名要创建的工具包,比如 `com.mindviewinc.util` ,但为了简化,这里我把工具包命名为 **onjava**。 +一般来说,我会使用反转后的域名来命名要创建的工具包,比如 **com.mindviewinc.util** ,但为了简化,这里我把工具包命名为 **onjava**。 比如,下面是“控制流”一章中使用到的 `range()` 方法,采用了 for-in 语法进行简单的遍历: @@ -327,7 +327,7 @@ public class Cookie { } ``` -记住,**Cookie.java** 文件产生的类文件必须位于名为 **dessert** 的子目录中,该子目录在 **hiding** (表明本书的"封装"章节)下,它必须在 CLASSPATH 的几个目录之下。不要错误地认为 Java 总是会将当前目录视作查找行为的起点之一。如果你的 CLASSPATH 中没有 `.`,Java 就不会查找单独当前目录。 +记住,**Cookie.java** 文件产生的类文件必须位于名为 **dessert** 的子目录中,该子目录在 **hiding** (表明本书的"封装"章节)下,它必须在 CLASSPATH 的几个目录之下。不要错误地认为 Java 总是会将当前目录视作查找行为的起点之一。如果你的 CLASSPATH 中没有 **.**,Java 就不会查找单独当前目录。 现在,使用 **Cookie** 创建一个程序: ```java @@ -384,7 +384,7 @@ class Pie { } ``` -最初看上去这两个文件毫不相关,但在 **Cake** 中可以创建一个 **Pie** 对象并调用它的 `f()` 方法。(注意,你的 CLASSPATH 中一定得有 `.`,这样文件才能编译)通常会认为 **Pie** 和 `f()` 具有包访问权限,因此不能被 **Cake** 访问。它们的确具有包访问权限,这是部分正确。**Cake.java** 可以访问它们是因为它们在相同的目录中且没有给自己设定明确的包名。Java 把这样的文件看作是隶属于该目录的默认包中,因此它们为该目录中所有的其他文件都提供了包访问权限。 +最初看上去这两个文件毫不相关,但在 **Cake** 中可以创建一个 **Pie** 对象并调用它的 `f()` 方法。(注意,你的 CLASSPATH 中一定得有 **.**,这样文件才能编译)通常会认为 **Pie** 和 `f()` 具有包访问权限,因此不能被 **Cake** 访问。它们的确具有包访问权限,这是部分正确。**Cake.java** 可以访问它们是因为它们在相同的目录中且没有给自己设定明确的包名。Java 把这样的文件看作是隶属于该目录的默认包中,因此它们为该目录中所有的其他文件都提供了包访问权限。 ### private: 你无法访问