提交 0562c0e7 编写于 作者: W wizardforcel

修改术语

上级 6c8034f3
...@@ -54,7 +54,7 @@ A String representing the file name, or a File or FileDescriptor object. ...@@ -54,7 +54,7 @@ A String representing the file name, or a File or FileDescriptor object.
As a source of data. Connect it to a FilterInputStream object to provide a useful interface. As a source of data. Connect it to a FilterInputStream object to provide a useful interface.
``` ```
类 功能 构器参数/如何使用 类 功能 构器参数/如何使用
ByteArrayInputStream 允许内存中的一个缓冲区作为InputStream使用 从中提取字节的缓冲区/作为一个数据源使用。通过将其同一个FilterInputStream对象连接,可提供一个有用的接口 ByteArrayInputStream 允许内存中的一个缓冲区作为InputStream使用 从中提取字节的缓冲区/作为一个数据源使用。通过将其同一个FilterInputStream对象连接,可提供一个有用的接口
...@@ -148,7 +148,7 @@ See Table 10-4. ...@@ -148,7 +148,7 @@ See Table 10-4.
See Table 10-4. See Table 10-4.
``` ```
类 功能 构器参数/如何使用 类 功能 构器参数/如何使用
ByteArrayOutputStream 在内存中创建一个缓冲区。我们发送给流的所有数据都会置入这个缓冲区。 可选缓冲区的初始大小/ ByteArrayOutputStream 在内存中创建一个缓冲区。我们发送给流的所有数据都会置入这个缓冲区。 可选缓冲区的初始大小/
用于指出数据的目的地。若将其同FilterOutputStream对象连接到一起,可提供一个有用的接口 用于指出数据的目的地。若将其同FilterOutputStream对象连接到一起,可提供一个有用的接口
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
(5) 修改练习2,在文件中查找指定的单词。打印出包含了欲找单词的所有文本行。 (5) 修改练习2,在文件中查找指定的单词。打印出包含了欲找单词的所有文本行。
(6) 在Blips.java中复制文件,将其重命名为BlipCheck.java。然后将类Blip2重命名为BlipCheck(在进程中将其标记为public)。删除文件中的//!记号,并执行程序。接下来,将BlipCheck的默认构器变成注释信息。运行它,并解释为什么仍然能够工作。 (6) 在Blips.java中复制文件,将其重命名为BlipCheck.java。然后将类Blip2重命名为BlipCheck(在进程中将其标记为public)。删除文件中的//!记号,并执行程序。接下来,将BlipCheck的默认构器变成注释信息。运行它,并解释为什么仍然能够工作。
(7) 在Blip3.java中,将接在“You must do this:”字样后的两行变成注释,然后运行程序。解释得到的结果为什么会与执行了那两行代码不同。 (7) 在Blip3.java中,将接在“You must do this:”字样后的两行变成注释,然后运行程序。解释得到的结果为什么会与执行了那两行代码不同。
......
...@@ -59,7 +59,7 @@ InputStream ...@@ -59,7 +59,7 @@ InputStream
Generally used in the scanner for a compiler and probably included because the Java compiler needed it. You probably won’t use this. Generally used in the scanner for a compiler and probably included because the Java compiler needed it. You probably won’t use this.
类 功能 构器参数/如何使用 类 功能 构器参数/如何使用
DataInputStream 与DataOutputStream联合使用,使自己能以机动方式读取一个流中的基本数据类型(int,char,long等等) InputStream/包含了一个完整的接口,以便读取基本数据类型 DataInputStream 与DataOutputStream联合使用,使自己能以机动方式读取一个流中的基本数据类型(int,char,long等等) InputStream/包含了一个完整的接口,以便读取基本数据类型
...@@ -114,7 +114,7 @@ OutputStream, with optional buffer size. ...@@ -114,7 +114,7 @@ OutputStream, with optional buffer size.
This doesn’t provide an interface per se, just a requirement that a buffer is used. Attach an interface object. This doesn’t provide an interface per se, just a requirement that a buffer is used. Attach an interface object.
类 功能 构器参数/如何使用 类 功能 构器参数/如何使用
DataOutputStream 与DataInputStream配合使用,以便采用方便的形式将基本数据类型(int,char,long等)写入一个数据流 OutputStream/包含了完整接口,以便我们写入基本数据类型 DataOutputStream 与DataInputStream配合使用,以便采用方便的形式将基本数据类型(int,char,long等)写入一个数据流 OutputStream/包含了完整接口,以便我们写入基本数据类型
......
...@@ -4,6 +4,6 @@ RandomAccessFile用于包含了已知长度记录的文件,以便我们能用s ...@@ -4,6 +4,6 @@ RandomAccessFile用于包含了已知长度记录的文件,以便我们能用s
首先,我们有点难以相信RandomAccessFile不属于InputStream或者OutputStream分层结构的一部分。除了恰巧实现了DataInput以及DataOutput(这两者亦由DataInputStream和DataOutputStream实现)接口之外,它们与那些分层结构并无什么关系。它甚至没有用到现有InputStream或OutputStream类的功能——采用的是一个完全不相干的类。该类属于全新的设计,含有自己的全部(大多数为固有)方法。之所以要这样做,是因为RandomAccessFile拥有与其他IO类型完全不同的行为,因为我们可在一个文件里向前或向后移动。不管在哪种情况下,它都是独立运作的,作为Object的一个“直接继承人”使用。 首先,我们有点难以相信RandomAccessFile不属于InputStream或者OutputStream分层结构的一部分。除了恰巧实现了DataInput以及DataOutput(这两者亦由DataInputStream和DataOutputStream实现)接口之外,它们与那些分层结构并无什么关系。它甚至没有用到现有InputStream或OutputStream类的功能——采用的是一个完全不相干的类。该类属于全新的设计,含有自己的全部(大多数为固有)方法。之所以要这样做,是因为RandomAccessFile拥有与其他IO类型完全不同的行为,因为我们可在一个文件里向前或向后移动。不管在哪种情况下,它都是独立运作的,作为Object的一个“直接继承人”使用。
从根本上说,RandomAccessFile类似DataInputStream和DataOutputStream的联合使用。其中,getFilePointer()用于了解当前在文件的什么地方,seek()用于移至文件内的一个新地点,而length()用于判断文件的最大长度。此外,构器要求使用另一个参数(与C的fopen()完全一样),指出自己只是随机读("r"),还是读写兼施("rw")。这里没有提供对“只写文件”的支持。也就是说,假如是从DataInputStream继承的,那么RandomAccessFile也有可能能很好地工作。 从根本上说,RandomAccessFile类似DataInputStream和DataOutputStream的联合使用。其中,getFilePointer()用于了解当前在文件的什么地方,seek()用于移至文件内的一个新地点,而length()用于判断文件的最大长度。此外,构器要求使用另一个参数(与C的fopen()完全一样),指出自己只是随机读("r"),还是读写兼施("rw")。这里没有提供对“只写文件”的支持。也就是说,假如是从DataInputStream继承的,那么RandomAccessFile也有可能能很好地工作。
还有更难对付的。很容易想象我们有时要在其他类型的数据流中搜索,比如一个ByteArrayInputStream,但搜索方法只有RandomAccessFile才会提供。而后者只能针对文件才能操作,不能针对数据流操作。此时,BufferedInputStream确实允许我们标记一个位置(使用mark(),它的值容纳于单个内部变量中),并用reset()重设那个位置。但这些做法都存在限制,并不是特别有用。 还有更难对付的。很容易想象我们有时要在其他类型的数据流中搜索,比如一个ByteArrayInputStream,但搜索方法只有RandomAccessFile才会提供。而后者只能针对文件才能操作,不能针对数据流操作。此时,BufferedInputStream确实允许我们标记一个位置(使用mark(),它的值容纳于单个内部变量中),并用reset()重设那个位置。但这些做法都存在限制,并不是特别有用。
...@@ -53,7 +53,7 @@ boolean accept(文件目录, 字串名); ...@@ -53,7 +53,7 @@ boolean accept(文件目录, 字串名);
它指出这种类型的所有对象都提供了一个名为accept()的方法。之所以要创建这样的一个类,背后的全部原因就是把accept()方法提供给list()方法,使list()能够“回调”accept(),从而判断应将哪些文件名包括到列表中。因此,通常将这种技术称为“回调”,有时也称为“算子”(也就是说,DirFilter是一个算子,因为它唯一的作用就是容纳一个方法)。由于list()采用一个FilenameFilter对象作为自己的参数使用,所以我们能传递实现了FilenameFilter的任何类的一个对象,用它决定(甚至在运行期)list()方法的行为方式。回调的目的是在代码的行为上提供更大的灵活性。 它指出这种类型的所有对象都提供了一个名为accept()的方法。之所以要创建这样的一个类,背后的全部原因就是把accept()方法提供给list()方法,使list()能够“回调”accept(),从而判断应将哪些文件名包括到列表中。因此,通常将这种技术称为“回调”,有时也称为“算子”(也就是说,DirFilter是一个算子,因为它唯一的作用就是容纳一个方法)。由于list()采用一个FilenameFilter对象作为自己的参数使用,所以我们能传递实现了FilenameFilter的任何类的一个对象,用它决定(甚至在运行期)list()方法的行为方式。回调的目的是在代码的行为上提供更大的灵活性。
通过DirFilter,我们看出尽管一个“接口”只包含了一系列方法,但并不局限于只能写那些方法(但是,至少必须提供一个接口内所有方法的定义。在这种情况下,DirFilter构器也会创建)。 通过DirFilter,我们看出尽管一个“接口”只包含了一系列方法,但并不局限于只能写那些方法(但是,至少必须提供一个接口内所有方法的定义。在这种情况下,DirFilter构器也会创建)。
accept()方法必须接纳一个File对象,用它指示用于寻找一个特定文件的目录;并接纳一个String,其中包含了要寻找之文件的名字。可决定使用或忽略这两个参数之一,但有时至少要使用文件名。记住list()方法准备为目录对象中的每个文件名调用 accept()方法必须接纳一个File对象,用它指示用于寻找一个特定文件的目录;并接纳一个String,其中包含了要寻找之文件的名字。可决定使用或忽略这两个参数之一,但有时至少要使用文件名。记住list()方法准备为目录对象中的每个文件名调用
...@@ -195,7 +195,7 @@ public class SortedDirList { ...@@ -195,7 +195,7 @@ public class SortedDirList {
} ///:~ } ///:~
``` ```
这里进行了另外少许改进。不再是将path(路径)和list(列表)创建为main()的本地变量,它们变成了类的成员,使它们的值能在对象“生存”期间方便地访问。事实上,main()现在只是对类进行测试的一种方式。大家可以看到,一旦列表创建完毕,类的构器就会自动开始对列表进行排序。 这里进行了另外少许改进。不再是将path(路径)和list(列表)创建为main()的本地变量,它们变成了类的成员,使它们的值能在对象“生存”期间方便地访问。事实上,main()现在只是对类进行测试的一种方式。大家可以看到,一旦列表创建完毕,类的构器就会自动开始对列表进行排序。
这种排序不要求区分大小写,所以最终不会得到一组全部单词都以大写字母开头的列表,跟着是全部以小写字母开头的列表。然而,我们注意到在以相同字母开头的一组文件名中,大写字母是排在前面的——这对标准的排序来说仍是一种不合格的行为。Java 1.2已成功解决了这个问题。 这种排序不要求区分大小写,所以最终不会得到一组全部单词都以大写字母开头的列表,跟着是全部以小写字母开头的列表。然而,我们注意到在以相同字母开头的一组文件名中,大写字母是排在前面的——这对标准的排序来说仍是一种不合格的行为。Java 1.2已成功解决了这个问题。
......
...@@ -139,7 +139,7 @@ public class IOStreamDemo { ...@@ -139,7 +139,7 @@ public class IOStreamDemo {
1. 缓冲的输入文件 1. 缓冲的输入文件
为打开一个文件以便输入,需要使用一个FileInputStream,同时将一个String或File对象作为文件名使用。为提高速度,最好先对文件进行缓冲处理,从而获得用于一个BufferedInputStream的构建器的结果引用。为了以格式化的形式读取输入数据,我们将那个结果引用赋给用于一个DataInputStream的构建器。DataInputStream是我们的最终(final)对象,并是我们进行读取操作的接口。 为打开一个文件以便输入,需要使用一个FileInputStream,同时将一个String或File对象作为文件名使用。为提高速度,最好先对文件进行缓冲处理,从而获得用于一个BufferedInputStream的构造器的结果引用。为了以格式化的形式读取输入数据,我们将那个结果引用赋给用于一个DataInputStream的构造器。DataInputStream是我们的最终(final)对象,并是我们进行读取操作的接口。
在这个例子中,只用到了readLine()方法,但理所当然任何DataInputStream方法都可以采用。一旦抵达文件末尾,readLine()就会返回一个null(空),以便中止并退出while循环。 在这个例子中,只用到了readLine()方法,但理所当然任何DataInputStream方法都可以采用。一旦抵达文件末尾,readLine()就会返回一个null(空),以便中止并退出while循环。
...@@ -147,7 +147,7 @@ public class IOStreamDemo { ...@@ -147,7 +147,7 @@ public class IOStreamDemo {
2. 从内存输入 2. 从内存输入
这一部分采用已经包含了完整文件内容的String s2,并用它创建一个StringBufferInputStream(字串缓冲输入流)——作为构器的参数,要求使用一个String,而非一个StringBuffer)。随后,我们用read()依次读取每个字符,并将其发送至控制台。注意read()将下一个字节返回为int,所以必须将其转换为一个char,以便正确地打印。 这一部分采用已经包含了完整文件内容的String s2,并用它创建一个StringBufferInputStream(字串缓冲输入流)——作为构器的参数,要求使用一个String,而非一个StringBuffer)。随后,我们用read()依次读取每个字符,并将其发送至控制台。注意read()将下一个字节返回为int,所以必须将其转换为一个char,以便正确地打印。
3. 格式化内存输入 3. 格式化内存输入
...@@ -181,7 +181,7 @@ public class TestEOF { ...@@ -181,7 +181,7 @@ public class TestEOF {
4. 行的编号与文件输出 4. 行的编号与文件输出
这个例子展示了如何LineNumberInputStream来跟踪输入行的编号。在这里,不可简单地将所有构器都组合起来,因为必须保持LineNumberInputStream的一个引用(注意这并非一种继承环境,所以不能简单地将in4转换到一个LineNumberInputStream)。因此,li容纳了指向LineNumberInputStream的引用,然后在它的基础上创建一个DataInputStream,以便读入数据。 这个例子展示了如何LineNumberInputStream来跟踪输入行的编号。在这里,不可简单地将所有构器都组合起来,因为必须保持LineNumberInputStream的一个引用(注意这并非一种继承环境,所以不能简单地将in4转换到一个LineNumberInputStream)。因此,li容纳了指向LineNumberInputStream的引用,然后在它的基础上创建一个DataInputStream,以便读入数据。
这个例子也展示了如何将格式化数据写入一个文件。首先创建了一个FileOutputStream,用它同一个文件连接。考虑到效率方面的原因,它生成了一个BufferedOutputStream。这几乎肯定是我们一般的做法,但却必须明确地这样做。随后为了进行格式化,它转换成一个PrintStream。用这种方式创建的数据文件可作为一个原始的文本文件读取。 这个例子也展示了如何将格式化数据写入一个文件。首先创建了一个FileOutputStream,用它同一个文件连接。考虑到效率方面的原因,它生成了一个BufferedOutputStream。这几乎肯定是我们一般的做法,但却必须明确地这样做。随后为了进行格式化,它转换成一个PrintStream。用这种方式创建的数据文件可作为一个原始的文本文件读取。
标志DataInputStream何时结束的一个方法是readLine()。一旦没有更多的字串可以读取,它就会返回null。每个行都会伴随自己的行号打印到文件里。该行号可通过li查询。 标志DataInputStream何时结束的一个方法是readLine()。一旦没有更多的字串可以读取,它就会返回null。每个行都会伴随自己的行号打印到文件里。该行号可通过li查询。
...@@ -204,7 +204,7 @@ writeDouble()将double数字保存到数据流中,并用补充的readDouble() ...@@ -204,7 +204,7 @@ writeDouble()将double数字保存到数据流中,并用补充的readDouble()
正如早先指出的那样,RandomAccessFile与IO层次结构的剩余部分几乎是完全隔离的,尽管它也实现了DataInput和DataOutput接口。所以不可将其与InputStream及OutputStream子类的任何部分关联起来。尽管也许能将一个ByteArrayInputStream当作一个随机访问元素对待,但只能用RandomAccessFile打开一个文件。必须假定RandomAccessFile已得到了正确的缓冲,因为我们不能自行选择。 正如早先指出的那样,RandomAccessFile与IO层次结构的剩余部分几乎是完全隔离的,尽管它也实现了DataInput和DataOutput接口。所以不可将其与InputStream及OutputStream子类的任何部分关联起来。尽管也许能将一个ByteArrayInputStream当作一个随机访问元素对待,但只能用RandomAccessFile打开一个文件。必须假定RandomAccessFile已得到了正确的缓冲,因为我们不能自行选择。
可以自行选择的是第二个构器参数:可决定以“只读”(r)方式或“读写”(rw)方式打开一个RandomAccessFile文件。 可以自行选择的是第二个构器参数:可决定以“只读”(r)方式或“读写”(rw)方式打开一个RandomAccessFile文件。
使用RandomAccessFile的时候,类似于组合使用DataInputStream和DataOutputStream(因为它实现了等同的接口)。除此以外,还可看到程序中使用了seek(),以便在文件中到处移动,对某个值作出修改。 使用RandomAccessFile的时候,类似于组合使用DataInputStream和DataOutputStream(因为它实现了等同的接口)。除此以外,还可看到程序中使用了seek(),以便在文件中到处移动,对某个值作出修改。
...@@ -236,7 +236,7 @@ public class InFile extends DataInputStream { ...@@ -236,7 +236,7 @@ public class InFile extends DataInputStream {
} ///:~ } ///:~
``` ```
无论构器的String版本还是File版本都包括在内,用于共同创建一个FileInputStream。 无论构器的String版本还是File版本都包括在内,用于共同创建一个FileInputStream。
就象这个例子展示的那样,现在可以有效减少创建文件时由于重复强调造成的问题。 就象这个例子展示的那样,现在可以有效减少创建文件时由于重复强调造成的问题。
...@@ -265,7 +265,7 @@ public class PrintFile extends PrintStream { ...@@ -265,7 +265,7 @@ public class PrintFile extends PrintStream {
} ///:~ } ///:~
``` ```
注意构建器不可能捕获一个由基础类构建器“抛”出的异常。 注意构造器不可能捕获一个由基础类构造器“抛”出的异常。
9. 快速输出数据文件 9. 快速输出数据文件
......
...@@ -112,7 +112,7 @@ public class SortedWordCount { ...@@ -112,7 +112,7 @@ public class SortedWordCount {
在countWords()中,每次从数据流中取出一个记号,而ttype信息的作用是判断对每个记号采取什么操作——因为记号可能代表一个行尾、一个数字、一个字串或者一个字符。 在countWords()中,每次从数据流中取出一个记号,而ttype信息的作用是判断对每个记号采取什么操作——因为记号可能代表一个行尾、一个数字、一个字串或者一个字符。
找到一个记号后,会查询Hashtable counts,核实其中是否已经以“键”(Key)的形式包含了一个记号。若答案是肯定的,对应的Counter(计数器)对象就会增值,指出已找到该单词的另一个实例。若答案为否,则新建一个Counter——因为Counter构器会将它的值初始化为1,正是我们计算单词数量时的要求。 找到一个记号后,会查询Hashtable counts,核实其中是否已经以“键”(Key)的形式包含了一个记号。若答案是肯定的,对应的Counter(计数器)对象就会增值,指出已找到该单词的另一个实例。若答案为否,则新建一个Counter——因为Counter构器会将它的值初始化为1,正是我们计算单词数量时的要求。
SortedWordCount并不属于Hashtable(散列表)的一种类型,所以它不会继承。它执行的一种特定类型的操作,所以尽管keys()和values()方法都必须重新揭示出来,但仍不表示应使用那个继承,因为大量Hashtable方法在这里都是不适当的。除此以外,对于另一些方法来说(比如getCounter()——用于获得一个特定字串的计数器;又如sortedKeys()——用于产生一个枚举),它们最终都改变了SortedWordCount接口的形式。 SortedWordCount并不属于Hashtable(散列表)的一种类型,所以它不会继承。它执行的一种特定类型的操作,所以尽管keys()和values()方法都必须重新揭示出来,但仍不表示应使用那个继承,因为大量Hashtable方法在这里都是不适当的。除此以外,对于另一些方法来说(比如getCounter()——用于获得一个特定字串的计数器;又如sortedKeys()——用于产生一个枚举),它们最终都改变了SortedWordCount接口的形式。
...@@ -123,7 +123,7 @@ SortedWordCount并不属于Hashtable(散列表)的一种类型,所以它 ...@@ -123,7 +123,7 @@ SortedWordCount并不属于Hashtable(散列表)的一种类型,所以它
尽管并不必要IO库的一部分,但StringTokenizer提供了与StreamTokenizer极相似的功能,所以在这里一并讲述。 尽管并不必要IO库的一部分,但StringTokenizer提供了与StreamTokenizer极相似的功能,所以在这里一并讲述。
StringTokenizer的作用是每次返回字串内的一个记号。这些记号是一些由制表站、空格以及新行分隔的连续字符。因此,字串“Where is my cat?”的记号分别是“Where”、“is”、“my”和“cat?”。与StreamTokenizer类似,我们可以指示StringTokenizer按照我们的愿望分割输入。但对于StringTokenizer,却需要向构器传递另一个参数,即我们想使用的分隔字串。通常,如果想进行更复杂的操作,应使用StreamTokenizer。 StringTokenizer的作用是每次返回字串内的一个记号。这些记号是一些由制表站、空格以及新行分隔的连续字符。因此,字串“Where is my cat?”的记号分别是“Where”、“is”、“my”和“cat?”。与StreamTokenizer类似,我们可以指示StringTokenizer按照我们的愿望分割输入。但对于StringTokenizer,却需要向构器传递另一个参数,即我们想使用的分隔字串。通常,如果想进行更复杂的操作,应使用StreamTokenizer。
可用nextToken()向StringTokenizer对象请求字串内的下一个记号。该方法要么返回一个记号,要么返回一个空字串(表示没有记号剩下)。 可用nextToken()向StringTokenizer对象请求字串内的下一个记号。该方法要么返回一个记号,要么返回一个空字串(表示没有记号剩下)。
......
...@@ -134,13 +134,13 @@ BufferedOutputStream BufferedWriter ...@@ -134,13 +134,13 @@ BufferedOutputStream BufferedWriter
DataInputStream 使用DataInputStream(除非要使用readLine(),那时需要使用一个BufferedReader) DataInputStream 使用DataInputStream(除非要使用readLine(),那时需要使用一个BufferedReader)
PrintStream PrintWriter PrintStream PrintWriter
LineNumberInputStream LineNumberReader LineNumberInputStream LineNumberReader
StreamTokenizer StreamTokenizer(用构器取代Reader) StreamTokenizer StreamTokenizer(用构器取代Reader)
PushBackInputStream PushBackReader PushBackInputStream PushBackReader
``` ```
有一条规律是显然的:若想使用readLine(),就不要再用一个DataInputStream来实现(否则会在编译期得到一条出错消息),而应使用一个BufferedReader。但除这种情况以外,DataInputStream仍是Java 1.1 IO库的“首选”成员。 有一条规律是显然的:若想使用readLine(),就不要再用一个DataInputStream来实现(否则会在编译期得到一条出错消息),而应使用一个BufferedReader。但除这种情况以外,DataInputStream仍是Java 1.1 IO库的“首选”成员。
为了将向PrintWriter的过渡变得更加自然,它提供了能采用任何OutputStream对象的构器。PrintWriter提供的格式化支持没有PrintStream那么多;但接口几乎是相同的。 为了将向PrintWriter的过渡变得更加自然,它提供了能采用任何OutputStream对象的构器。PrintWriter提供的格式化支持没有PrintStream那么多;但接口几乎是相同的。
10.7.3 未改变的类 10.7.3 未改变的类
...@@ -271,7 +271,7 @@ StringReader替换StringBufferInputStream,剩下的代码是完全相同的。 ...@@ -271,7 +271,7 @@ StringReader替换StringBufferInputStream,剩下的代码是完全相同的。
②:到你现在正式使用的时候,这个错误可能已经修正。 ②:到你现在正式使用的时候,这个错误可能已经修正。
第4节明显是从老式数据流到新数据流的一个直接转换,没有需要特别指出的。在第5节中,我们被强迫使用所有的老式数据流,因为DataOutputStream和DataInputStream要求用到它们,而且没有可供替换的东西。然而,编译期间不会产生任何“反对”信息。若不赞成一种数据流,通常是由于它的构器产生了一条反对消息,禁止我们使用整个类。但在DataInputStream的情况下,只有readLine()是不赞成使用的,因为我们最好为readLine()使用一个BufferedReader(但为其他所有格式化输入都使用一个DataInputStream)。 第4节明显是从老式数据流到新数据流的一个直接转换,没有需要特别指出的。在第5节中,我们被强迫使用所有的老式数据流,因为DataOutputStream和DataInputStream要求用到它们,而且没有可供替换的东西。然而,编译期间不会产生任何“反对”信息。若不赞成一种数据流,通常是由于它的构器产生了一条反对消息,禁止我们使用整个类。但在DataInputStream的情况下,只有readLine()是不赞成使用的,因为我们最好为readLine()使用一个BufferedReader(但为其他所有格式化输入都使用一个DataInputStream)。
若比较第5节和IOStreamDemo.java中的那一小节,会注意到在这个版本中,数据是在文本之前写入的。那是由于Java 1.1本身存在一个错误,如下述代码所示: 若比较第5节和IOStreamDemo.java中的那一小节,会注意到在这个版本中,数据是在文本之前写入的。那是由于Java 1.1本身存在一个错误,如下述代码所示:
...@@ -370,6 +370,6 @@ class Redirecting { ...@@ -370,6 +370,6 @@ class Redirecting {
> Note:The constructor java.io.PrintStream(java.io.OutputStream) has been deprecated. > Note:The constructor java.io.PrintStream(java.io.OutputStream) has been deprecated.
注意:不推荐使用构器java.io.PrintStream(java.io.OutputStream)。 注意:不推荐使用构器java.io.PrintStream(java.io.OutputStream)。
然而,无论System.setOut()还是System.setErr()都要求用一个PrintStream作为参数使用,所以必须调用PrintStream构建器。所以大家可能会觉得奇怪,既然Java 1.1通过反对构建器而反对了整个PrintStream,为什么库的设计人员在添加这个反对的同时,依然为System添加了新方法,且指明要求用PrintStream,而不是用PrintWriter呢?毕竟,后者是一个崭新和首选的替换措施呀?这真令人费解。 然而,无论System.setOut()还是System.setErr()都要求用一个PrintStream作为参数使用,所以必须调用PrintStream构造器。所以大家可能会觉得奇怪,既然Java 1.1通过反对构造器而反对了整个PrintStream,为什么库的设计人员在添加这个反对的同时,依然为System添加了新方法,且指明要求用PrintStream,而不是用PrintWriter呢?毕竟,后者是一个崭新和首选的替换措施呀?这真令人费解。
...@@ -63,7 +63,7 @@ public class GZIPcompress { ...@@ -63,7 +63,7 @@ public class GZIPcompress {
} ///:~ } ///:~
``` ```
压缩类的用法非常直观——只需将输出流封装到一个GZIPOutputStream或者ZipOutputStream内,并将输入流封装到GZIPInputStream或者ZipInputStream内即可。剩余的全部操作就是标准的IO读写。然而,这是一个很典型的例子,我们不得不混合使用新旧IO流:数据的输入使用Reader类,而GZIPOutputStream的构器只能接收一个OutputStream对象,不能接收Writer对象。 压缩类的用法非常直观——只需将输出流封装到一个GZIPOutputStream或者ZipOutputStream内,并将输入流封装到GZIPInputStream或者ZipInputStream内即可。剩余的全部操作就是标准的IO读写。然而,这是一个很典型的例子,我们不得不混合使用新旧IO流:数据的输入使用Reader类,而GZIPOutputStream的构器只能接收一个OutputStream对象,不能接收Writer对象。
10.8.2 用Zip进行多文件保存 10.8.2 用Zip进行多文件保存
......
...@@ -97,7 +97,7 @@ public class Worm implements Serializable { ...@@ -97,7 +97,7 @@ public class Worm implements Serializable {
} ///:~ } ///:~
``` ```
更有趣的是,Worm内的Data对象数组是用随机数字初始化的(这样便不用怀疑编译器保留了某种原始信息)。每个Worm段都用一个Char标记。这个Char是在重复生成链接的Worm列表时自动产生的。创建一个Worm时,需告诉构建器希望它有多长。为产生下一个引用(next),它总是用减去1的长度来调用Worm构建器。最后一个next引用则保持为null(空),表示已抵达Worm的尾部。 更有趣的是,Worm内的Data对象数组是用随机数字初始化的(这样便不用怀疑编译器保留了某种原始信息)。每个Worm段都用一个Char标记。这个Char是在重复生成链接的Worm列表时自动产生的。创建一个Worm时,需告诉构造器希望它有多长。为产生下一个引用(next),它总是用减去1的长度来调用Worm构造器。最后一个next引用则保持为null(空),表示已抵达Worm的尾部。
上面的所有操作都是为了加深事情的复杂程度,加大对象序列化的难度。然而,真正的序列化过程却是非常简单的。一旦从另外某个流里创建了ObjectOutputStream,writeObject()就会序列化对象。注意也可以为一个String调用writeObject()。亦可使用与DataOutputStream相同的方法写入所有基本数据类型(它们有相同的接口)。 上面的所有操作都是为了加深事情的复杂程度,加大对象序列化的难度。然而,真正的序列化过程却是非常简单的。一旦从另外某个流里创建了ObjectOutputStream,writeObject()就会序列化对象。注意也可以为一个String调用writeObject()。亦可使用与DataOutputStream相同的方法写入所有基本数据类型(它们有相同的接口)。
有两个单独的try块看起来是类似的。第一个读写的是文件,而另一个读写的是一个ByteArray(字节数组)。可利用对任何DataInputStream或者DataOutputStream的序列化来读写特定的对象;正如在关于连网的那一章会讲到的那样,这些对象甚至包括网络。一次循环后的输出结果如下: 有两个单独的try块看起来是类似的。第一个读写的是文件,而另一个读写的是一个ByteArray(字节数组)。可利用对任何DataInputStream或者DataOutputStream的序列化来读写特定的对象;正如在关于连网的那一章会讲到的那样,这些对象甚至包括网络。一次循环后的输出结果如下:
...@@ -116,7 +116,7 @@ Worm storage, w3 = :a(262):b(100):c(396):d(480):e(316):f(398) ...@@ -116,7 +116,7 @@ Worm storage, w3 = :a(262):b(100):c(396):d(480):e(316):f(398)
可以看出,装配回原状的对象确实包含了原来那个对象里包含的所有链接。 可以看出,装配回原状的对象确实包含了原来那个对象里包含的所有链接。
注意在对一个Serializable(可序列化)对象进行重新装配的过程中,不会调用任何构建器(甚至默认构建器)。整个对象都是通过从InputStream中取得数据恢复的。 注意在对一个Serializable(可序列化)对象进行重新装配的过程中,不会调用任何构造器(甚至默认构造器)。整个对象都是通过从InputStream中取得数据恢复的。
作为Java 1.1特性的一种,我们注意到对象的序列化并不属于新的Reader和Writer层次结构的一部分,而是沿用老式的InputStream和OutputStream结构。所以在一些特殊的场合下,不得不混合使用两种类型的层次结构。 作为Java 1.1特性的一种,我们注意到对象的序列化并不属于新的Reader和Writer层次结构的一部分,而是沿用老式的InputStream和OutputStream结构。所以在一些特殊的场合下,不得不混合使用两种类型的层次结构。
...@@ -266,9 +266,9 @@ Blip1 Constructor ...@@ -266,9 +266,9 @@ Blip1 Constructor
Blip1.readExternal Blip1.readExternal
``` ```
未恢复Blip2对象的原因是那样做会导致一个异常。你找出了Blip1和Blip2之间的区别吗?Blip1的构建器是“公共的”(public),Blip2的构建器则不然,这样便会在恢复时造成异常。试试将Blip2的构建器属性变成“public”,然后删除//!注释标记,看看是否能得到正确的结果。 未恢复Blip2对象的原因是那样做会导致一个异常。你找出了Blip1和Blip2之间的区别吗?Blip1的构造器是“公共的”(public),Blip2的构造器则不然,这样便会在恢复时造成异常。试试将Blip2的构造器属性变成“public”,然后删除//!注释标记,看看是否能得到正确的结果。
恢复b1后,会调用Blip1默认构建器。这与恢复一个Serializable(可序列化)对象不同。在后者的情况下,对象完全以它保存下来的二进制位为基础恢复,不存在构建器调用。而对一个Externalizable对象,所有普通的默认构建行为都会发生(包括在字段定义时的初始化),而且会调用readExternal()。必须注意这一事实——特别注意所有默认的构建行为都会进行——否则很难在自己的Externalizable对象中产生正确的行为。 恢复b1后,会调用Blip1默认构造器。这与恢复一个Serializable(可序列化)对象不同。在后者的情况下,对象完全以它保存下来的二进制位为基础恢复,不存在构造器调用。而对一个Externalizable对象,所有普通的默认构建行为都会发生(包括在字段定义时的初始化),而且会调用readExternal()。必须注意这一事实——特别注意所有默认的构建行为都会进行——否则很难在自己的Externalizable对象中产生正确的行为。
下面这个例子揭示了保存和恢复一个Externalizable对象必须做的全部事情: 下面这个例子揭示了保存和恢复一个Externalizable对象必须做的全部事情:
...@@ -331,7 +331,7 @@ class Blip3 implements Externalizable { ...@@ -331,7 +331,7 @@ class Blip3 implements Externalizable {
} ///:~ } ///:~
``` ```
其中,字段s和i只在第二个构建器中初始化,不关默认构建器的事。这意味着假如不在readExternal中初始化s和i,它们就会成为null(因为在对象创建的第一步中已将对象的存储空间清除为1)。若注释掉跟随于“You must do this”后面的两行代码,并运行程序,就会发现当对象恢复以后,s是null,而i是零。 其中,字段s和i只在第二个构造器中初始化,不关默认构造器的事。这意味着假如不在readExternal中初始化s和i,它们就会成为null(因为在对象创建的第一步中已将对象的存储空间清除为1)。若注释掉跟随于“You must do this”后面的两行代码,并运行程序,就会发现当对象恢复以后,s是null,而i是零。
若从一个Externalizable对象继承,通常需要调用writeExternal()和readExternal()的基础类版本,以便正确地保存和恢复基础类组件。 若从一个Externalizable对象继承,通常需要调用writeExternal()和readExternal()的基础类版本,以便正确地保存和恢复基础类组件。
...@@ -496,7 +496,7 @@ public class SerialCtl implements Serializable { ...@@ -496,7 +496,7 @@ public class SerialCtl implements Serializable {
``` ```
在这个例子中,一个String保持原始状态,其他设为transient(临时),以便证明非临时字段会被defaultWriteObject()方法自动保存,而transient字段必须在程序中明确保存和恢复。字段是在构器内部初始化的,而不是在定义的时候,这证明了它们不会在重新装配的时候被某些自动化机制初始化。 在这个例子中,一个String保持原始状态,其他设为transient(临时),以便证明非临时字段会被defaultWriteObject()方法自动保存,而transient字段必须在程序中明确保存和恢复。字段是在构器内部初始化的,而不是在定义的时候,这证明了它们不会在重新装配的时候被某些自动化机制初始化。
若准备通过默认机制写入对象的非transient部分,那么必须调用defaultWriteObject(),令其作为writeObject()中的第一个操作;并调用defaultReadObject(),令其作为readObject()的第一个操作。这些都是不常见的调用方法。举个例子来说,当我们为一个ObjectOutputStream调用defaultWriteObject()的时候,而且没有为其传递参数,就需要采取这种操作,使其知道对象的引用以及如何写入所有非transient的部分。这种做法非常不便。 若准备通过默认机制写入对象的非transient部分,那么必须调用defaultWriteObject(),令其作为writeObject()中的第一个操作;并调用defaultReadObject(),令其作为readObject()的第一个操作。这些都是不常见的调用方法。举个例子来说,当我们为一个ObjectOutputStream调用defaultWriteObject()的时候,而且没有为其传递参数,就需要采取这种操作,使其知道对象的引用以及如何写入所有非transient的部分。这种做法非常不便。
...@@ -749,7 +749,7 @@ public class CADState { ...@@ -749,7 +749,7 @@ public class CADState {
Shape(几何形状)类“实现了可序列化”(implements Serializable),所以从Shape继承的任何东西也都会自动“可序列化”。每个Shape都包含了数据,而且每个衍生的Shape类都包含了一个特殊的static字段,用于决定所有那些类型的Shape的颜色(如将一个static字段置入基础类,结果只会产生一个字段,因为static字段未在衍生类中复制)。可对基础类中的方法进行覆盖处理,以便为不同的类型设置颜色(static方法不会动态绑定,所以这些都是普通的方法)。每次调用randomFactory()方法时,它都会创建一个不同的Shape(Shape值采用随机值)。 Shape(几何形状)类“实现了可序列化”(implements Serializable),所以从Shape继承的任何东西也都会自动“可序列化”。每个Shape都包含了数据,而且每个衍生的Shape类都包含了一个特殊的static字段,用于决定所有那些类型的Shape的颜色(如将一个static字段置入基础类,结果只会产生一个字段,因为static字段未在衍生类中复制)。可对基础类中的方法进行覆盖处理,以便为不同的类型设置颜色(static方法不会动态绑定,所以这些都是普通的方法)。每次调用randomFactory()方法时,它都会创建一个不同的Shape(Shape值采用随机值)。
Circle(圆)和Square(矩形)属于对Shape的直接扩展;唯一的差别是Circle在定义时会初始化颜色,而Square在构器中初始化。Line(直线)的问题将留到以后讨论。 Circle(圆)和Square(矩形)属于对Shape的直接扩展;唯一的差别是Circle在定义时会初始化颜色,而Square在构器中初始化。Line(直线)的问题将留到以后讨论。
在main()中,一个Vector用于容纳Class对象,而另一个用于容纳形状。若不提供相应的命令行参数,就会创建shapeTypes Vector,并添加Class对象。然后创建shapes Vector,并添加Shape对象。接下来,所有static color值都会设成GREEN,而且所有东西都会序列化到文件CADState.out。 在main()中,一个Vector用于容纳Class对象,而另一个用于容纳形状。若不提供相应的命令行参数,就会创建shapeTypes Vector,并添加Class对象。然后创建shapes Vector,并添加Shape对象。接下来,所有static color值都会设成GREEN,而且所有东西都会序列化到文件CADState.out。
...@@ -783,7 +783,7 @@ Circle(圆)和Square(矩形)属于对Shape的直接扩展;唯一的差 ...@@ -783,7 +783,7 @@ Circle(圆)和Square(矩形)属于对Shape的直接扩展;唯一的差
] ]
``` ```
从中可以看出,xPos,yPos以及dim的值都已成功保存和恢复出来。但在获取static信息时却出现了问题。所有“3”都已进入,但没有正常地出来。Circle有一个1值(定义为RED),而Square有一个0值(记住,它们是在构器里初始化的)。看上去似乎static根本没有得到初始化!实情正是如此——尽管类Class是“可以序列化的”,但却不能按我们希望的工作。所以假如想序列化static值,必须亲自动手。 从中可以看出,xPos,yPos以及dim的值都已成功保存和恢复出来。但在获取static信息时却出现了问题。所有“3”都已进入,但没有正常地出来。Circle有一个1值(定义为RED),而Square有一个0值(记住,它们是在构器里初始化的)。看上去似乎static根本没有得到初始化!实情正是如此——尽管类Class是“可以序列化的”,但却不能按我们希望的工作。所以假如想序列化static值,必须亲自动手。
这正是Line中的serializeStaticState()和deserializeStaticState()两个static方法的用途。可以看到,这两个方法都是作为存储和恢复进程的一部分明确调用的(注意写入序列化文件和从中读回的顺序不能改变)。所以为了使CADState.java正确运行起来,必须采用下述三种方法之一: 这正是Line中的serializeStaticState()和deserializeStaticState()两个static方法的用途。可以看到,这两个方法都是作为存储和恢复进程的一部分明确调用的(注意写入序列化文件和从中读回的顺序不能改变)。所以为了使CADState.java正确运行起来,必须采用下述三种方法之一:
......
...@@ -293,7 +293,7 @@ public class PetCount { ...@@ -293,7 +293,7 @@ public class PetCount {
在Java 1.0中,对instanceof有一个比较小的限制:只可将其与一个已命名的类型比较,不能同Class对象作对比。在上述例子中,大家可能觉得将所有那些instanceof表达式写出来是件很麻烦的事情。实际情况正是这样。但在Java 1.0中,没有办法让这一工作自动进行——不能创建Class的一个Vector,再将其与之比较。大家最终会意识到,如编写了数量众多的instanceof表达式,整个设计都可能出现问题。 在Java 1.0中,对instanceof有一个比较小的限制:只可将其与一个已命名的类型比较,不能同Class对象作对比。在上述例子中,大家可能觉得将所有那些instanceof表达式写出来是件很麻烦的事情。实际情况正是这样。但在Java 1.0中,没有办法让这一工作自动进行——不能创建Class的一个Vector,再将其与之比较。大家最终会意识到,如编写了数量众多的instanceof表达式,整个设计都可能出现问题。
当然,这个例子只是一个构想——最好在每个类型里添加一个static数据成员,然后在构器中令其增值,以便跟踪计数。编写程序时,大家可能想象自己拥有类的源码控制权,能够自由改动它。但由于实际情况并非总是这样,所以RTTI显得特别方便。 当然,这个例子只是一个构想——最好在每个类型里添加一个static数据成员,然后在构器中令其增值,以便跟踪计数。编写程序时,大家可能想象自己拥有类的源码控制权,能够自由改动它。但由于实际情况并非总是这样,所以RTTI显得特别方便。
1. 使用类标记 1. 使用类标记
......
...@@ -60,7 +60,7 @@ Class.getInterfaces方法会返回Class对象的一个数组,用于表示包 ...@@ -60,7 +60,7 @@ Class.getInterfaces方法会返回Class对象的一个数组,用于表示包
若有一个Class对象,也可以用getSuperclass()查询该对象的直接基础类是什么。当然,这种做会返回一个Class引用,可用它作进一步的查询。这意味着在运行期的时候,完全有机会调查到对象的完整层次结构。 若有一个Class对象,也可以用getSuperclass()查询该对象的直接基础类是什么。当然,这种做会返回一个Class引用,可用它作进一步的查询。这意味着在运行期的时候,完全有机会调查到对象的完整层次结构。
若从表面看,Class的newInstance()方法似乎是克隆(clone())一个对象的另一种手段。但两者是有区别的。利用newInstance(),我们可在没有现成对象供“克隆”的情况下新建一个对象。就象上面的程序演示的那样,当时没有Toy对象,只有cy——即y的Class对象的一个引用。利用它可以实现“虚拟构建器”。换言之,我们表达:“尽管我不知道你的准确类型是什么,但请你无论如何都正确地创建自己。”在上述例子中,cy只是一个Class引用,编译期间并不知道进一步的类型信息。一旦新建了一个实例后,可以得到Object引用。但那个引用指向一个Toy对象。当然,如果要将除Object能够接收的其他任何消息发出去,首先必须进行一些调查研究,再进行转换。除此以外,用newInstance()创建的类必须有一个默认构建器。没有办法用newInstance()创建拥有非默认构建器的对象,所以在Java 1.0中可能存在一些限制。然而,Java 1.1的“反射”API(下一节讨论)却允许我们动态地使用类里的任何构建器。 若从表面看,Class的newInstance()方法似乎是克隆(clone())一个对象的另一种手段。但两者是有区别的。利用newInstance(),我们可在没有现成对象供“克隆”的情况下新建一个对象。就象上面的程序演示的那样,当时没有Toy对象,只有cy——即y的Class对象的一个引用。利用它可以实现“虚拟构造器”。换言之,我们表达:“尽管我不知道你的准确类型是什么,但请你无论如何都正确地创建自己。”在上述例子中,cy只是一个Class引用,编译期间并不知道进一步的类型信息。一旦新建了一个实例后,可以得到Object引用。但那个引用指向一个Toy对象。当然,如果要将除Object能够接收的其他任何消息发出去,首先必须进行一些调查研究,再进行转换。除此以外,用newInstance()创建的类必须有一个默认构造器。没有办法用newInstance()创建拥有非默认构造器的对象,所以在Java 1.0中可能存在一些限制。然而,Java 1.1的“反射”API(下一节讨论)却允许我们动态地使用类里的任何构造器。
程序中的最后一个方法是printInfo(),它取得一个Class引用,通过getName()获得它的名字,并用interface()调查它是不是一个接口。 程序中的最后一个方法是printInfo(),它取得一个Class引用,通过getName()获得它的名字,并用interface()调查它是不是一个接口。
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
在运行期查询类信息的另一个原动力是通过网络创建与执行位于远程系统上的对象。这就叫作“远程方法调用”(RMI),它允许Java程序(版本1.1以上)使用由多台机器发布或分布的对象。这种对象的分布可能是由多方面的原因引起的:可能要做一件计算密集型的工作,想对它进行分割,让处于空闲状态的其他机器分担部分工作,从而加快处理进度。某些情况下,可能需要将用于控制特定类型任务(比如多层客户/服务器架构中的“运作规则”)的代码放置在一台特殊的机器上,使这台机器成为对那些行动进行描述的一个通用储藏所。而且可以方便地修改这个场所,使其对系统内的所有方面产生影响(这是一种特别有用的设计思路,因为机器是独立存在的,所以能轻易修改软件!)。分布式计算也能更充分地发挥某些专用硬件的作用,它们特别擅长执行一些特定的任务——例如矩阵逆转——但对常规编程来说却显得太夸张或者太昂贵了。 在运行期查询类信息的另一个原动力是通过网络创建与执行位于远程系统上的对象。这就叫作“远程方法调用”(RMI),它允许Java程序(版本1.1以上)使用由多台机器发布或分布的对象。这种对象的分布可能是由多方面的原因引起的:可能要做一件计算密集型的工作,想对它进行分割,让处于空闲状态的其他机器分担部分工作,从而加快处理进度。某些情况下,可能需要将用于控制特定类型任务(比如多层客户/服务器架构中的“运作规则”)的代码放置在一台特殊的机器上,使这台机器成为对那些行动进行描述的一个通用储藏所。而且可以方便地修改这个场所,使其对系统内的所有方面产生影响(这是一种特别有用的设计思路,因为机器是独立存在的,所以能轻易修改软件!)。分布式计算也能更充分地发挥某些专用硬件的作用,它们特别擅长执行一些特定的任务——例如矩阵逆转——但对常规编程来说却显得太夸张或者太昂贵了。
在Java 1.1中,Class类(本章前面已有详细论述)得到了扩展,可以支持“反射”的概念。针对Field,Method以及Constructor类(每个都实现了Memberinterface——成员接口),它们都新增了一个库:java.lang.reflect。这些类型的对象都是JVM在运行期创建的,用于代表未知类里对应的成员。这样便可用构建器创建新对象,用get()和set()方法读取和修改与Field对象关联的字段,以及用invoke()方法调用与Method对象关联的方法。此外,我们可调用方法getFields(),getMethods(),getConstructors(),分别返回用于表示字段、方法以及构建器的对象数组(在联机文档中,还可找到与Class类有关的更多的资料)。因此,匿名对象的类信息可在运行期被完整的揭露出来,而在编译期间不需要知道任何东西。 在Java 1.1中,Class类(本章前面已有详细论述)得到了扩展,可以支持“反射”的概念。针对Field,Method以及Constructor类(每个都实现了Memberinterface——成员接口),它们都新增了一个库:java.lang.reflect。这些类型的对象都是JVM在运行期创建的,用于代表未知类里对应的成员。这样便可用构造器创建新对象,用get()和set()方法读取和修改与Field对象关联的字段,以及用invoke()方法调用与Method对象关联的方法。此外,我们可调用方法getFields(),getMethods(),getConstructors(),分别返回用于表示字段、方法以及构造器的对象数组(在联机文档中,还可找到与Class类有关的更多的资料)。因此,匿名对象的类信息可在运行期被完整的揭露出来,而在编译期间不需要知道任何东西。
大家要认识的很重要的一点是“反射”并没有什么神奇的地方。通过“反射”同一个未知类型的对象打交道时,JVM只是简单地检查那个对象,并调查它从属于哪个特定的类(就象以前的RTTI那样)。但在这之后,在我们做其他任何事情之前,Class对象必须载入。因此,用于那种特定类型的.class文件必须能由JVM调用(要么在本地机器内,要么可以通过网络取得)。所以RTTI和“反射”之间唯一的区别就是对RTTI来说,编译器会在编译期打开和检查.class文件。换句话说,我们可以用“普通”方式调用一个对象的所有方法;但对“反射”来说,.class文件在编译期间是不可使用的,而是由运行期环境打开和检查。 大家要认识的很重要的一点是“反射”并没有什么神奇的地方。通过“反射”同一个未知类型的对象打交道时,JVM只是简单地检查那个对象,并调查它从属于哪个特定的类(就象以前的RTTI那样)。但在这之后,在我们做其他任何事情之前,Class对象必须载入。因此,用于那种特定类型的.class文件必须能由JVM调用(要么在本地机器内,要么可以通过网络取得)。所以RTTI和“反射”之间唯一的区别就是对RTTI来说,编译器会在编译期打开和检查.class文件。换句话说,我们可以用“普通”方式调用一个对象的所有方法;但对“反射”来说,.class文件在编译期间是不可使用的,而是由运行期环境打开和检查。
11.3.1 一个类方法提取器 11.3.1 一个类方法提取器
...@@ -66,7 +66,7 @@ Class方法getMethods()和getConstructors()可以分别返回Method和Constructo ...@@ -66,7 +66,7 @@ Class方法getMethods()和getConstructors()可以分别返回Method和Constructo
这里便用到了“反射”技术,因为由Class.forName()产生的结果不能在编译期间获知,所以所有方法签名信息都会在运行期间提取。若研究一下联机文档中关于“反射”(Reflection)的那部分文字,就会发现它已提供了足够多的支持,可对一个编译期完全未知的对象进行实际的设置以及发出方法调用。同样地,这也属于几乎完全不用我们操心的一个步骤——Java自己会利用这种支持,所以程序设计环境能够控制Java Beans——但它无论如何都是非常有趣的。 这里便用到了“反射”技术,因为由Class.forName()产生的结果不能在编译期间获知,所以所有方法签名信息都会在运行期间提取。若研究一下联机文档中关于“反射”(Reflection)的那部分文字,就会发现它已提供了足够多的支持,可对一个编译期完全未知的对象进行实际的设置以及发出方法调用。同样地,这也属于几乎完全不用我们操心的一个步骤——Java自己会利用这种支持,所以程序设计环境能够控制Java Beans——但它无论如何都是非常有趣的。
一个有趣的试验是运行java ShowMehods ShowMethods。这样做可得到一个列表,其中包括一个public默认构建器,尽管我们在代码中看见并没有定义一个构建器。我们看到的是由编译器自动合成的那一个构建器。如果随之将ShowMethods设为一个非public类(即换成“友好”类),合成的默认构建器便不会在输出结果中出现。合成的默认构建器会自动获得与类一样的访问权限。 一个有趣的试验是运行java ShowMehods ShowMethods。这样做可得到一个列表,其中包括一个public默认构造器,尽管我们在代码中看见并没有定义一个构造器。我们看到的是由编译器自动合成的那一个构造器。如果随之将ShowMethods设为一个非public类(即换成“友好”类),合成的默认构造器便不会在输出结果中出现。合成的默认构造器会自动获得与类一样的访问权限。
ShowMethods的输出仍然有些“不爽”。例如,下面是通过调用java ShowMethods java.lang.String得到的输出结果的一部分: ShowMethods的输出仍然有些“不爽”。例如,下面是通过调用java ShowMethods java.lang.String得到的输出结果的一部分:
``` ```
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
(1) 写一个方法,向它传递一个对象,循环打印出对象层次结构中的所有类。 (1) 写一个方法,向它传递一个对象,循环打印出对象层次结构中的所有类。
(2) 在ToyTest.java中,将Toy的默认构器标记成注释信息,解释随之发生的事情。 (2) 在ToyTest.java中,将Toy的默认构器标记成注释信息,解释随之发生的事情。
(3) 新建一种类型的集合,令其使用一个Vector。捕获置入其中的第一个对象的类型,然后从那时起只允许用户插入那种类型的对 (3) 新建一种类型的集合,令其使用一个Vector。捕获置入其中的第一个对象的类型,然后从那时起只允许用户插入那种类型的对
象。 象。
......
...@@ -195,14 +195,14 @@ public class Snake implements Cloneable { ...@@ -195,14 +195,14 @@ public class Snake implements Cloneable {
} }
} ///:~ } ///:~
一条Snake(蛇)由数段构成,每一段的类型都是Snake。所以,这是一个一段段链接起来的列表。所有段都是以循环方式创建的,每做好一段,都会使第一个构建器参数的值递减,直至最终为零。而为给每段赋予一个独一无二的标记,第二个参数(一个Char)的值在每次循环构建器调用时都会递增。 一条Snake(蛇)由数段构成,每一段的类型都是Snake。所以,这是一个一段段链接起来的列表。所有段都是以循环方式创建的,每做好一段,都会使第一个构造器参数的值递减,直至最终为零。而为给每段赋予一个独一无二的标记,第二个参数(一个Char)的值在每次循环构造器调用时都会递增。
increment()方法的作用是循环递增每个标记,使我们能看到发生的变化;而toString则循环打印出每个标记。输出如下: increment()方法的作用是循环递增每个标记,使我们能看到发生的变化;而toString则循环打印出每个标记。输出如下:
s = :a:b:c:d:e s = :a:b:c:d:e
s2 = :a:b:c:d:e s2 = :a:b:c:d:e
after s.increment, s2 = :a:c:d:e:f after s.increment, s2 = :a:c:d:e:f
这意味着只有第一段才是由Object.clone()复制的,所以此时进行的是一种“浅层复制”。若希望复制整条蛇——即进行“深层复制”——必须在被覆盖的clone()里采取附加的操作。 这意味着只有第一段才是由Object.clone()复制的,所以此时进行的是一种“浅层复制”。若希望复制整条蛇——即进行“深层复制”——必须在被覆盖的clone()里采取附加的操作。
通常可在从一个能克隆的类里调用super.clone(),以确保所有基础类行动(包括Object.clone())能够进行。随着是为对象内每个引用都明确调用一个clone();否则那些引用会别名变成原始对象的引用。构建器的调用也大致相同——首先构造基础类,然后是下一个衍生的构建器……以此类推,直到位于最深层的衍生构建器。区别在于clone()并不是个构建器,所以没有办法实现自动克隆。为了克隆,必须由自己明确进行。 通常可在从一个能克隆的类里调用super.clone(),以确保所有基础类行动(包括Object.clone())能够进行。随着是为对象内每个引用都明确调用一个clone();否则那些引用会别名变成原始对象的引用。构造器的调用也大致相同——首先构造基础类,然后是下一个衍生的构造器……以此类推,直到位于最深层的衍生构造器。区别在于clone()并不是个构造器,所以没有办法实现自动克隆。为了克隆,必须由自己明确进行。
12.2.6 克隆合成对象 12.2.6 克隆合成对象
试图深层复制合成对象时会遇到一个问题。必须假定成员对象中的clone()方法也能依次对自己的引用进行深层复制,以此类推。这使我们的操作变得复杂。为了能正常实现深层复制,必须对所有类中的代码进行控制,或者至少全面掌握深层复制中需要涉及的类,确保它们自己的深层复制能正确进行。 试图深层复制合成对象时会遇到一个问题。必须假定成员对象中的clone()方法也能依次对自己的引用进行深层复制,以此类推。这使我们的操作变得复杂。为了能正常实现深层复制,必须对所有类中的代码进行控制,或者至少全面掌握深层复制中需要涉及的类,确保它们自己的深层复制能正确进行。
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
(3) 有条件地支持克隆。若类容纳了其他对象的引用,而那些对象也许能够克隆(集合类便是这样的一个例子),就可试着克隆拥有对方引用的所有对象;如果它们“抛”出了异常,只需让这些异常通过即可。举个例子来说,假设有一个特殊的Vector,它试图克隆自己容纳的所有对象。编写这样的一个Vector时,并不知道客户程序员会把什么形式的对象置入这个Vector中,所以并不知道它们是否真的能够克隆。 (3) 有条件地支持克隆。若类容纳了其他对象的引用,而那些对象也许能够克隆(集合类便是这样的一个例子),就可试着克隆拥有对方引用的所有对象;如果它们“抛”出了异常,只需让这些异常通过即可。举个例子来说,假设有一个特殊的Vector,它试图克隆自己容纳的所有对象。编写这样的一个Vector时,并不知道客户程序员会把什么形式的对象置入这个Vector中,所以并不知道它们是否真的能够克隆。
(4) 不实现Cloneable(),但是将clone()覆盖成protected,使任何字段都具有正确的复制行为。这样一来,从这个类继承的所有东西都能覆盖clone(),并调用super.clone()来产生正确的复制行为。注意在我们实现方案里,可以而且应该调用super.clone()——即使那个方法本来预期的是一个Cloneable对象(否则会抛出一个异常),因为没有人会在我们这种类型的对象上直接调用它。它只有通过一个衍生类调用;对那个衍生类来说,如果要保证它正常工作,需实现Cloneable。 (4) 不实现Cloneable(),但是将clone()覆盖成protected,使任何字段都具有正确的复制行为。这样一来,从这个类继承的所有东西都能覆盖clone(),并调用super.clone()来产生正确的复制行为。注意在我们实现方案里,可以而且应该调用super.clone()——即使那个方法本来预期的是一个Cloneable对象(否则会抛出一个异常),因为没有人会在我们这种类型的对象上直接调用它。它只有通过一个衍生类调用;对那个衍生类来说,如果要保证它正常工作,需实现Cloneable。
(5) 不实现Cloneable来试着防止克隆,并覆盖clone(),以产生一个异常。为使这一设想顺利实现,只有令从它衍生出来的任何类都调用重新定义后的clone()里的suepr.clone()。 (5) 不实现Cloneable来试着防止克隆,并覆盖clone(),以产生一个异常。为使这一设想顺利实现,只有令从它衍生出来的任何类都调用重新定义后的clone()里的suepr.clone()。
(6) 将类设为final,从而防止克隆。若clone()尚未被我们的任何一个上级类覆盖,这一设想便不会成功。若已被覆盖,那么再一次覆盖它,并“抛”出一个CloneNotSupportedException(克隆不支持)异常。为担保克隆被禁止,将类设为final是唯一的办法。除此以外,一旦涉及保密对象或者遇到想对创建的对象数量进行控制的其他情况,应该将所有构器都设为private,并提供一个或更多的特殊方法来创建对象。采用这种方式,这些方法就可以限制创建的对象数量以及它们的创建条件——一种特殊情况是第16章要介绍的singleton(独子)方案。 (6) 将类设为final,从而防止克隆。若clone()尚未被我们的任何一个上级类覆盖,这一设想便不会成功。若已被覆盖,那么再一次覆盖它,并“抛”出一个CloneNotSupportedException(克隆不支持)异常。为担保克隆被禁止,将类设为final是唯一的办法。除此以外,一旦涉及保密对象或者遇到想对创建的对象数量进行控制的其他情况,应该将所有构器都设为private,并提供一个或更多的特殊方法来创建对象。采用这种方式,这些方法就可以限制创建的对象数量以及它们的创建条件——一种特殊情况是第16章要介绍的singleton(独子)方案。
下面这个例子总结了克隆的各种实现方法,然后在层次结构中将其“关闭”: 下面这个例子总结了克隆的各种实现方法,然后在层次结构中将其“关闭”:
//: CheckCloneable.java //: CheckCloneable.java
...@@ -131,8 +131,8 @@ Could not clone ReallyNoMore ...@@ -131,8 +131,8 @@ Could not clone ReallyNoMore
(4) 在自己的clone()中捕获异常 (4) 在自己的clone()中捕获异常
这一系列步骤能达到最理想的效果。 这一系列步骤能达到最理想的效果。
12.3.1 副本构 12.3.1 副本构
克隆看起来要求进行非常复杂的设置,似乎还该有另一种替代方案。一个办法是制作特殊的构建器,令其负责复制一个对象。在C++中,这叫作“副本构建器”。刚开始的时候,这好象是一种非常显然的解决方案(如果你是C++程序员,这个方法就更显亲切)。下面是一个实际的例子: 克隆看起来要求进行非常复杂的设置,似乎还该有另一种替代方案。一个办法是制作特殊的构造器,令其负责复制一个对象。在C++中,这叫作“副本构造器”。刚开始的时候,这好象是一种非常显然的解决方案(如果你是C++程序员,这个方法就更显亲切)。下面是一个实际的例子:
//: CopyConstructor.java //: CopyConstructor.java
// A constructor for copying an object // A constructor for copying an object
// of the same type, as an attempt to create // of the same type, as an attempt to create
...@@ -260,10 +260,10 @@ public class CopyConstructor { ...@@ -260,10 +260,10 @@ public class CopyConstructor {
} }
} ///:~ } ///:~
这个例子第一眼看上去显得有点奇怪。不同水果的质量肯定有所区别,但为什么只是把代表那些质量的数据成员直接置入Fruit(水果)类?有两方面可能的原因。第一个是我们可能想简便地插入或修改质量。注意Fruit有一个protected(受到保护的)addQualities()方法,它允许衍生类来进行这些插入或修改操作(大家或许会认为最合乎逻辑的做法是在Fruit中使用一个protected构建器,用它获取FruitQualities参数,但构建器不能继承,所以不可在第二级或级数更深的类中使用它)。通过将水果的质量置入一个独立的类,可以得到更大的灵活性,其中包括可以在特定Fruit对象的存在期间中途更改质量。 这个例子第一眼看上去显得有点奇怪。不同水果的质量肯定有所区别,但为什么只是把代表那些质量的数据成员直接置入Fruit(水果)类?有两方面可能的原因。第一个是我们可能想简便地插入或修改质量。注意Fruit有一个protected(受到保护的)addQualities()方法,它允许衍生类来进行这些插入或修改操作(大家或许会认为最合乎逻辑的做法是在Fruit中使用一个protected构造器,用它获取FruitQualities参数,但构造器不能继承,所以不可在第二级或级数更深的类中使用它)。通过将水果的质量置入一个独立的类,可以得到更大的灵活性,其中包括可以在特定Fruit对象的存在期间中途更改质量。
之所以将FruitQualities设为一个独立的对象,另一个原因是考虑到我们有时希望添加新的质量,或者通过继承与多态性改变行为。注意对GreenZebra来说(这实际是西红柿的一类——我已栽种成功,它们简直令人难以置信),构器会调用addQualities(),并为其传递一个ZebraQualities对象。该对象是从FruitQualities衍生出来的,所以能与基础类中的FruitQualities引用联系在一起。当然,一旦GreenZebra使用FruitQualities,就必须将其向下转换成为正确的类型(就象evaluate()中展示的那样),但它肯定知道类型是ZebraQualities。 之所以将FruitQualities设为一个独立的对象,另一个原因是考虑到我们有时希望添加新的质量,或者通过继承与多态性改变行为。注意对GreenZebra来说(这实际是西红柿的一类——我已栽种成功,它们简直令人难以置信),构器会调用addQualities(),并为其传递一个ZebraQualities对象。该对象是从FruitQualities衍生出来的,所以能与基础类中的FruitQualities引用联系在一起。当然,一旦GreenZebra使用FruitQualities,就必须将其向下转换成为正确的类型(就象evaluate()中展示的那样),但它肯定知道类型是ZebraQualities。
大家也看到有一个Seed(种子)类,Fruit(大家都知道,水果含有自己的种子)包含了一个Seed数组。 大家也看到有一个Seed(种子)类,Fruit(大家都知道,水果含有自己的种子)包含了一个Seed数组。
最后,注意每个类都有一个副本构建器,而且每个副本构建器都必须关心为基础类和成员对象调用副本构建器的问题,从而获得“深层复制”的效果。对副本构建器的测试是在CopyConstructor类内进行的。方法ripen()需要获取一个Tomato参数,并对其执行副本构建工作,以便复制对象: 最后,注意每个类都有一个副本构造器,而且每个副本构造器都必须关心为基础类和成员对象调用副本构造器的问题,从而获得“深层复制”的效果。对副本构造器的测试是在CopyConstructor类内进行的。方法ripen()需要获取一个Tomato参数,并对其执行副本构建工作,以便复制对象:
t = new Tomato(t); t = new Tomato(t);
而slice()需要获取一个更常规的Fruit对象,而且对它进行复制: 而slice()需要获取一个更常规的Fruit对象,而且对它进行复制:
f = new Fruit(f); f = new Fruit(f);
...@@ -273,7 +273,7 @@ In slice, f is a Fruit ...@@ -273,7 +273,7 @@ In slice, f is a Fruit
In ripen, t is a Tomato In ripen, t is a Tomato
In slice, f is a Fruit In slice, f is a Fruit
从中可以看出一个问题。在slice()内部对Tomato进行了副本构建工作以后,结果便不再是一个Tomato对象,而只是一个Fruit。它已丢失了作为一个Tomato(西红柿)的所有特征。此外,如果采用一个GreenZebra,ripen()和slice()会把它分别转换成一个Tomato和一个Fruit。所以非常不幸,假如想制作对象的一个本地副本,Java中的副本构器便不是特别适合我们。 从中可以看出一个问题。在slice()内部对Tomato进行了副本构建工作以后,结果便不再是一个Tomato对象,而只是一个Fruit。它已丢失了作为一个Tomato(西红柿)的所有特征。此外,如果采用一个GreenZebra,ripen()和slice()会把它分别转换成一个Tomato和一个Fruit。所以非常不幸,假如想制作对象的一个本地副本,Java中的副本构器便不是特别适合我们。
1. 为什么在C++的作用比在Java中大? 1. 为什么在C++的作用比在Java中大?
副本构建器是C++的一个基本构成部分,因为它能自动产生对象的一个本地副本。但前面的例子确实证明了它不适合在Java中使用,为什么呢?在Java中,我们操控的一切东西都是引用,而在C++中,却可以使用类似于引用的东西,也能直接传递对象。这时便要用到C++的副本构建器:只要想获得一个对象,并按值传递它,就可以复制对象。所以它在C++里能很好地工作,但应注意这套机制在Java里是很不通的,所以不要用它。 副本构造器是C++的一个基本构成部分,因为它能自动产生对象的一个本地副本。但前面的例子确实证明了它不适合在Java中使用,为什么呢?在Java中,我们操控的一切东西都是引用,而在C++中,却可以使用类似于引用的东西,也能直接传递对象。这时便要用到C++的副本构造器:只要想获得一个对象,并按值传递它,就可以复制对象。所以它在C++里能很好地工作,但应注意这套机制在Java里是很不通的,所以不要用它。
...@@ -46,7 +46,7 @@ public class MutableInteger { ...@@ -46,7 +46,7 @@ public class MutableInteger {
} ///:~ } ///:~
注意n在这里简化了我们的编码。 注意n在这里简化了我们的编码。
若默认的初始化为零已经足够(便不需要构器),而且不用考虑把它打印出来(便不需要toString),那么IntValue甚至还能更加简单。如下所示: 若默认的初始化为零已经足够(便不需要构器),而且不用考虑把它打印出来(便不需要toString),那么IntValue甚至还能更加简单。如下所示:
class IntValue { int n; } class IntValue { int n; }
将元素取出来,再对其进行转换,这多少显得有些笨拙,但那是Vector的问题,不是IntValue的错。 将元素取出来,再对其进行转换,这多少显得有些笨拙,但那是Vector的问题,不是IntValue的错。
...@@ -222,7 +222,7 @@ public class ImmutableStrings { ...@@ -222,7 +222,7 @@ public class ImmutableStrings {
方法 参数,覆盖 用途 方法 参数,覆盖 用途
器 已被覆盖:默认,String,StringBuffer,char数组,byte数组 创建String对象 器 已被覆盖:默认,String,StringBuffer,char数组,byte数组 创建String对象
length() 无 String中的字符数量 length() 无 String中的字符数量
charAt() int Index 位于String内某个位置的char charAt() int Index 位于String内某个位置的char
getChars(),getBytes 开始复制的起点和终点,要向其中复制内容的数组,对目标数组的一个索引 将char或byte复制到外部数组内部 getChars(),getBytes 开始复制的起点和终点,要向其中复制内容的数组,对目标数组的一个索引 将char或byte复制到外部数组内部
...@@ -246,7 +246,7 @@ Intern() 无 为每个独一无二的字符顺序都产生一个(而且只有 ...@@ -246,7 +246,7 @@ Intern() 无 为每个独一无二的字符顺序都产生一个(而且只有
方法 参数,覆盖 用途 方法 参数,覆盖 用途
器 已覆盖:默认,要创建的缓冲区长度,要根据它创建的String 新建一个StringBuffer对象 器 已覆盖:默认,要创建的缓冲区长度,要根据它创建的String 新建一个StringBuffer对象
toString() 无 根据这个StringBuffer创建一个String toString() 无 根据这个StringBuffer创建一个String
length() 无 StringBuffer中的字符数量 length() 无 StringBuffer中的字符数量
capacity() 无 返回目前分配的空间大小 capacity() 无 返回目前分配的空间大小
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
12.6 练习 12.6 练习
(1) 创建一个myString类,在其中包含了一个String对象,以便用在构建器中用构建器的参数对其进行初始化。添加一个toString()方法以及一个concatenate()方法,令其将一个String对象追加到我们的内部字串。在myString中实现clone()。创建两个static方法,每个都取得一个myString x引用作为自己的参数,并调用x.concatenate("test")。但在第二个方法中,请首先调用clone()。测试这两个方法,观察它们不同的结果。 (1) 创建一个myString类,在其中包含了一个String对象,以便用在构造器中用构造器的参数对其进行初始化。添加一个toString()方法以及一个concatenate()方法,令其将一个String对象追加到我们的内部字串。在myString中实现clone()。创建两个static方法,每个都取得一个myString x引用作为自己的参数,并调用x.concatenate("test")。但在第二个方法中,请首先调用clone()。测试这两个方法,观察它们不同的结果。
(2) 创建一个名为Battery(电池)的类,在其中包含一个int,用它表示电池的编号(采用独一无二的标识符的形式)。接下来,创建一个名为Toy的类,其中包含了一个Battery数组以及一个toString,用于打印出所有电池。为Toy写一个clone()方法,令其自动关闭所有Battery对象。克隆Toy并打印出结果,完成对它的测试。 (2) 创建一个名为Battery(电池)的类,在其中包含一个int,用它表示电池的编号(采用独一无二的标识符的形式)。接下来,创建一个名为Toy的类,其中包含了一个Battery数组以及一个toString,用于打印出所有电池。为Toy写一个clone()方法,令其自动关闭所有Battery对象。克隆Toy并打印出结果,完成对它的测试。
(3) 修改CheckCloneable.java,使所有clone()方法都能捕获CloneNotSupportException异常,而不是把它直接传递给调用者。 (3) 修改CheckCloneable.java,使所有clone()方法都能捕获CloneNotSupportException异常,而不是把它直接传递给调用者。
(4) 修改Compete.java,为Thing2和Thing4类添加更多的成员对象,看看自己是否能判断计时随复杂性变化的规律——是一种简单的线性关系,还是看起来更加复杂。 (4) 修改Compete.java,为Thing2和Thing4类添加更多的成员对象,看看自己是否能判断计时随复杂性变化的规律——是一种简单的线性关系,还是看起来更加复杂。
......
...@@ -108,7 +108,7 @@ public class SimpleThread extends Thread { ...@@ -108,7 +108,7 @@ public class SimpleThread extends Thread {
run()方法几乎肯定含有某种形式的循环——它们会一直持续到线程不再需要为止。因此,我们必须规定特定的条件,以便中断并退出这个循环(或者在上述的例子中,简单地从run()返回即可)。run()通常采用一种无限循环的形式。也就是说,通过阻止外部发出对线程的stop()或者destroy()调用,它会永远运行下去(直到程序完成)。 run()方法几乎肯定含有某种形式的循环——它们会一直持续到线程不再需要为止。因此,我们必须规定特定的条件,以便中断并退出这个循环(或者在上述的例子中,简单地从run()返回即可)。run()通常采用一种无限循环的形式。也就是说,通过阻止外部发出对线程的stop()或者destroy()调用,它会永远运行下去(直到程序完成)。
在main()中,可看到创建并运行了大量线程。Thread包含了一个特殊的方法,叫作start(),它的作用是对线程进行特殊的初始化,然后调用run()。所以整个步骤包括:调用构建器来构建对象,然后用start()配置线程,再调用run()。如果不调用start()——如果适当的话,可在构建器那样做——线程便永远不会启动。 在main()中,可看到创建并运行了大量线程。Thread包含了一个特殊的方法,叫作start(),它的作用是对线程进行特殊的初始化,然后调用run()。所以整个步骤包括:调用构造器来构建对象,然后用start()配置线程,再调用run()。如果不调用start()——如果适当的话,可在构造器那样做——线程便永远不会启动。
下面是该程序某一次运行的输出(注意每次运行都会不同): 下面是该程序某一次运行的输出(注意每次运行都会不同):
...@@ -228,7 +228,7 @@ public class Counter2 extends Applet { ...@@ -228,7 +228,7 @@ public class Counter2 extends Applet {
现在,Counter2变成了一个相当直接的程序,它的唯一任务就是设置并管理用户界面。但假若用户现在按下Start按钮,却不会真正调用一个方法。此时不是创建类的一个线程,而是创建SeparateSubTask,然后继续Counter2事件循环。注意此时会保存SeparateSubTask的引用,以便我们按下onOff按钮的时候,能正常地切换位于SeparateSubTask内部的runFlag(运行标志)。随后那个线程便可启动(当它看到标志的时候),然后将自己中止(亦可将SeparateSubTask设为一个内部类来达到这一目的)。 现在,Counter2变成了一个相当直接的程序,它的唯一任务就是设置并管理用户界面。但假若用户现在按下Start按钮,却不会真正调用一个方法。此时不是创建类的一个线程,而是创建SeparateSubTask,然后继续Counter2事件循环。注意此时会保存SeparateSubTask的引用,以便我们按下onOff按钮的时候,能正常地切换位于SeparateSubTask内部的runFlag(运行标志)。随后那个线程便可启动(当它看到标志的时候),然后将自己中止(亦可将SeparateSubTask设为一个内部类来达到这一目的)。
SeparateSubTask类是对Thread的一个简单扩展,它带有一个构器(其中保存了Counter2引用,然后通过调用start()来运行线程)以及一个run()——本质上包含了Counter1.java的go()内的代码。由于SeparateSubTask知道自己容纳了指向一个Counter2的引用,所以能够在需要的时候介入,并访问Counter2的TestField(文本字段)。 SeparateSubTask类是对Thread的一个简单扩展,它带有一个构器(其中保存了Counter2引用,然后通过调用start()来运行线程)以及一个run()——本质上包含了Counter1.java的go()内的代码。由于SeparateSubTask知道自己容纳了指向一个Counter2的引用,所以能够在需要的时候介入,并访问Counter2的TestField(文本字段)。
按下onOff按钮,几乎立即能得到正确的响应。当然,这个响应其实并不是“立即”发生的,它毕竟和那种由“中断”驱动的系统不同。只有线程拥有CPU的执行时间,并注意到标记已发生改变,计数器才会停止。 按下onOff按钮,几乎立即能得到正确的响应。当然,这个响应其实并不是“立即”发生的,它毕竟和那种由“中断”驱动的系统不同。只有线程拥有CPU的执行时间,并注意到标记已发生改变,计数器才会停止。
...@@ -302,7 +302,7 @@ public class Counter2i extends Applet { ...@@ -302,7 +302,7 @@ public class Counter2i extends Applet {
这个SeparateSubTask名字不会与前例中的SeparateSubTask冲突——即使它们都在相同的目录里——因为它已作为一个内部类隐藏起来。大家亦可看到内部类被设为private(私有)属性,这意味着它的字段和方法都可获得默认的访问权限(run()除外,它必须设为public,因为它在基础类中是公开的)。除Counter2i之外,其他任何方面都不可访问private内部类。而且由于两个类紧密结合在一起,所以很容易放宽它们之间的访问限制。在SeparateSubTask中,我们可看到invertFlag()方法已被删去,因为Counter2i现在可以直接访问runFlag。 这个SeparateSubTask名字不会与前例中的SeparateSubTask冲突——即使它们都在相同的目录里——因为它已作为一个内部类隐藏起来。大家亦可看到内部类被设为private(私有)属性,这意味着它的字段和方法都可获得默认的访问权限(run()除外,它必须设为public,因为它在基础类中是公开的)。除Counter2i之外,其他任何方面都不可访问private内部类。而且由于两个类紧密结合在一起,所以很容易放宽它们之间的访问限制。在SeparateSubTask中,我们可看到invertFlag()方法已被删去,因为Counter2i现在可以直接访问runFlag。
此外,注意SeparateSubTask的构器已得到了简化——它现在唯一的用外就是启动线程。Counter2i对象的引用仍象以前那样得以捕获,但不再是通过人工传递和引用外部对象来达到这一目的,此时的内部类机制可以自动照料它。在run()中,可看到对t的访问是直接进行的,似乎它是SeparateSubTask的一个字段。父类中的t字段现在可以变成private,因为SeparateSubTask能在未获任何特殊许可的前提下自由地访问它——而且无论如何都该尽可能地把字段变成“私有”属性,以防来自类外的某种力量不慎地改变它们。 此外,注意SeparateSubTask的构器已得到了简化——它现在唯一的用外就是启动线程。Counter2i对象的引用仍象以前那样得以捕获,但不再是通过人工传递和引用外部对象来达到这一目的,此时的内部类机制可以自动照料它。在run()中,可看到对t的访问是直接进行的,似乎它是SeparateSubTask的一个字段。父类中的t字段现在可以变成private,因为SeparateSubTask能在未获任何特殊许可的前提下自由地访问它——而且无论如何都该尽可能地把字段变成“私有”属性,以防来自类外的某种力量不慎地改变它们。
无论在什么时候,只要注意到类相互之间结合得比较紧密,就可考虑利用内部类来改善代码的编写与维护。 无论在什么时候,只要注意到类相互之间结合得比较紧密,就可考虑利用内部类来改善代码的编写与维护。
...@@ -382,7 +382,7 @@ public class Counter3 ...@@ -382,7 +382,7 @@ public class Counter3
new Thread(Counter3.this); new Thread(Counter3.this);
``` ```
若某样东西有一个Runnable接口,实际只是意味着它有一个run()方法,但不存在与之相关的任何特殊东西——它不具有任何天生的线程处理能力,这与那些从Thread继承的类是不同的。所以为了从一个Runnable对象产生线程,必须单独创建一个线程,并为其传递Runnable对象;可为其使用一个特殊的构器,并令其采用一个Runnable作为自己的参数使用。随后便可为那个线程调用start(),如下所示: 若某样东西有一个Runnable接口,实际只是意味着它有一个run()方法,但不存在与之相关的任何特殊东西——它不具有任何天生的线程处理能力,这与那些从Thread继承的类是不同的。所以为了从一个Runnable对象产生线程,必须单独创建一个线程,并为其传递Runnable对象;可为其使用一个特殊的构器,并令其采用一个Runnable作为自己的参数使用。随后便可为那个线程调用start(),如下所示:
``` ```
selfThread.start(); selfThread.start();
...@@ -398,7 +398,7 @@ Runnable接口最大的一个优点是所有东西都从属于相同的类。若 ...@@ -398,7 +398,7 @@ Runnable接口最大的一个优点是所有东西都从属于相同的类。若
现在考虑一下创建多个不同的线程的问题。我们不可用前面的例子来做到这一点,所以必须倒退回去,利用从Thread继承的多个独立类来封装run()。但这是一种更常规的方案,而且更易理解,所以尽管前例揭示了我们经常都能看到的编码样式,但并不推荐在大多数情况下都那样做,因为它只是稍微复杂一些,而且灵活性稍低一些。 现在考虑一下创建多个不同的线程的问题。我们不可用前面的例子来做到这一点,所以必须倒退回去,利用从Thread继承的多个独立类来封装run()。但这是一种更常规的方案,而且更易理解,所以尽管前例揭示了我们经常都能看到的编码样式,但并不推荐在大多数情况下都那样做,因为它只是稍微复杂一些,而且灵活性稍低一些。
下面这个例子用计数器和切换按钮再现了前面的编码样式。但这一次,一个特定计数器的所有信息(按钮和文本字段)都位于它自己的、从Thread继承的对象内。Ticker中的所有字段都具有private(私有)属性,这意味着Ticker的具体实现方案可根据实际情况任意修改,其中包括修改用于获取和显示信息的数据组件的数量及类型。创建好一个Ticker对象以后,构器便请求一个AWT容器(Container)的引用——Ticker用自己的可视组件填充那个容器。采用这种方式,以后一旦改变了可视组件,使用Ticker的代码便不需要另行修改一道。 下面这个例子用计数器和切换按钮再现了前面的编码样式。但这一次,一个特定计数器的所有信息(按钮和文本字段)都位于它自己的、从Thread继承的对象内。Ticker中的所有字段都具有private(私有)属性,这意味着Ticker的具体实现方案可根据实际情况任意修改,其中包括修改用于获取和显示信息的数据组件的数量及类型。创建好一个Ticker对象以后,构器便请求一个AWT容器(Container)的引用——Ticker用自己的可视组件填充那个容器。采用这种方式,以后一旦改变了可视组件,使用Ticker的代码便不需要另行修改一道。
``` ```
//: Counter4.java //: Counter4.java
...@@ -510,7 +510,7 @@ Ticker[] s = new Ticker[size] ...@@ -510,7 +510,7 @@ Ticker[] s = new Ticker[size]
此外,上述代码被同时设置成一个程序片和一个应用(程序)。在它是应用程序的情况下,size参数可从命令行里提取出来(否则就提供一个默认的值)。 此外,上述代码被同时设置成一个程序片和一个应用(程序)。在它是应用程序的情况下,size参数可从命令行里提取出来(否则就提供一个默认的值)。
数组的长度建好以后,就可以创建新的Ticker对象;作为Ticker构器的一部分,用于每个Ticker的按钮和文本字段就会加入程序片。 数组的长度建好以后,就可以创建新的Ticker对象;作为Ticker构器的一部分,用于每个Ticker的按钮和文本字段就会加入程序片。
按下Start按钮后,会在整个Ticker数组里遍历,并为每个Ticker调用start()。记住,start()会进行必要的线程初始化工作,然后为那个线程调用run()。 按下Start按钮后,会在整个Ticker数组里遍历,并为每个Ticker调用start()。记住,start()会进行必要的线程初始化工作,然后为那个线程调用run()。
......
...@@ -146,7 +146,7 @@ public class Sharing1 extends Applet { ...@@ -146,7 +146,7 @@ public class Sharing1 extends Applet {
} ///:~ } ///:~
``` ```
和往常一样,每个计数器都包含了自己的显示组件:两个文本字段以及一个标签。根据它们的初始值,可知道计数是相同的。这些组件在TwoCounter构器加入Container。由于这个线程是通过用户的一个“按下按钮”操作启动的,所以start()可能被多次调用。但对一个线程来说,对Thread.start()的多次调用是非法的(会产生异常)。在started标记和重载的start()方法中,大家可看到针对这一情况采取的防范措施。 和往常一样,每个计数器都包含了自己的显示组件:两个文本字段以及一个标签。根据它们的初始值,可知道计数是相同的。这些组件在TwoCounter构器加入Container。由于这个线程是通过用户的一个“按下按钮”操作启动的,所以start()可能被多次调用。但对一个线程来说,对Thread.start()的多次调用是非法的(会产生异常)。在started标记和重载的start()方法中,大家可看到针对这一情况采取的防范措施。
在run()中,count1和count2的增值与显示方式表面上似乎能保持它们完全一致。随后会调用sleep();若没有这个调用,程序便会出错,因为那会造成CPU难于交换任务。 在run()中,count1和count2的增值与显示方式表面上似乎能保持它们完全一致。随后会调用sleep();若没有这个调用,程序便会出错,因为那会造成CPU难于交换任务。
......
...@@ -317,7 +317,7 @@ class Receiver extends Blockable { ...@@ -317,7 +317,7 @@ class Receiver extends Blockable {
令人惊讶的是,主要的程序片(Applet)类非常简单,这是大多数工作都已置入Blockable框架的缘故。大概地说,我们创建了一个由Blockable对象构成的数组。而且由于每个对象都是一个线程,所以在按下“start”按钮后,它们会采取自己的行动。还有另一个按钮和actionPerformed()从句,用于中止所有Peeker对象。由于Java 1.2“反对”使用Thread的stop()方法,所以可考虑采用这种折衷形式的中止方式。 令人惊讶的是,主要的程序片(Applet)类非常简单,这是大多数工作都已置入Blockable框架的缘故。大概地说,我们创建了一个由Blockable对象构成的数组。而且由于每个对象都是一个线程,所以在按下“start”按钮后,它们会采取自己的行动。还有另一个按钮和actionPerformed()从句,用于中止所有Peeker对象。由于Java 1.2“反对”使用Thread的stop()方法,所以可考虑采用这种折衷形式的中止方式。
为了在Sender和Receiver之间建立一个连接,我们创建了一个PipedWriter和一个PipedReader。注意PipedReader in必须通过一个构建器参数同PipedWriterout连接起来。在那以后,我们在out内放进去的所有东西都可从in中提取出来——似乎那些东西是通过一个“管道”传输过去的。随后将in和out对象分别传递给Receiver和Sender构建器;后者将它们当作任意类型的Reader和Writer看待(也就是说,它们被“上溯”转换了)。 为了在Sender和Receiver之间建立一个连接,我们创建了一个PipedWriter和一个PipedReader。注意PipedReader in必须通过一个构造器参数同PipedWriterout连接起来。在那以后,我们在out内放进去的所有东西都可从in中提取出来——似乎那些东西是通过一个“管道”传输过去的。随后将in和out对象分别传递给Receiver和Sender构造器;后者将它们当作任意类型的Reader和Writer看待(也就是说,它们被“上溯”转换了)。
Blockable引用b的数组在定义之初并未得到初始化,因为管道化的数据流是不可在定义前设置好的(对try块的需要将成为障碍): Blockable引用b的数组在定义之初并未得到初始化,因为管道化的数据流是不可在定义前设置好的(对try块的需要将成为障碍):
......
...@@ -158,7 +158,7 @@ Ticker采用本章前面构造好的形式,但有一个额外的TextField( ...@@ -158,7 +158,7 @@ Ticker采用本章前面构造好的形式,但有一个额外的TextField(
也要注意yield()的用法,它将控制权自动返回给调试程序(机制)。若不进行这样的处理,多线程机制仍会工作,但我们会发现它的运行速度慢了下来(试试删去对yield()的调用)。亦可调用sleep(),但假若那样做,计数频率就会改由sleep()的持续时间控制,而不是优先级。 也要注意yield()的用法,它将控制权自动返回给调试程序(机制)。若不进行这样的处理,多线程机制仍会工作,但我们会发现它的运行速度慢了下来(试试删去对yield()的调用)。亦可调用sleep(),但假若那样做,计数频率就会改由sleep()的持续时间控制,而不是优先级。
Counter5中的init()创建了由10个Ticker2构成的一个数组;它们的按钮以及输入字段(文本字段)由Ticker2构器置入窗体。Counter5增加了新的按钮,用于启动一切,以及用于提高和降低线程组的最大优先级。除此以外,还有一些标签用于显示一个线程可以采用的最大及最小优先级;以及一个特殊的文本字段,用于显示线程组的最大优先级(在下一节里,我们将全面讨论线程组的问题)。最后,父线程组的优先级也作为标签显示出来。 Counter5中的init()创建了由10个Ticker2构成的一个数组;它们的按钮以及输入字段(文本字段)由Ticker2构器置入窗体。Counter5增加了新的按钮,用于启动一切,以及用于提高和降低线程组的最大优先级。除此以外,还有一些标签用于显示一个线程可以采用的最大及最小优先级;以及一个特殊的文本字段,用于显示线程组的最大优先级(在下一节里,我们将全面讨论线程组的问题)。最后,父线程组的优先级也作为标签显示出来。
按下“up”(上)或“down”(下)按钮的时候,会先取得Ticker2当前的优先级,然后相应地提高或者降低。 按下“up”(上)或“down”(下)按钮的时候,会先取得Ticker2当前的优先级,然后相应地提高或者降低。
运行该程序时,我们可注意到几件事情。首先,线程组的默认优先级是5。即使在启动线程之前(或者在创建线程之前,这要求对代码进行适当的修改)将最大优先级降到5以下,每个线程都会有一个5的默认优先级。 运行该程序时,我们可注意到几件事情。首先,线程组的默认优先级是5。即使在启动线程之前(或者在创建线程之前,这要求对代码进行适当的修改)将最大优先级降到5以下,每个线程都会有一个5的默认优先级。
...@@ -171,7 +171,7 @@ Counter5中的init()创建了由10个Ticker2构成的一个数组;它们的按 ...@@ -171,7 +171,7 @@ Counter5中的init()创建了由10个Ticker2构成的一个数组;它们的按
所有线程都隶属于一个线程组。那可以是一个默认线程组,亦可是一个创建线程时明确指定的组。在创建之初,线程被限制到一个组里,而且不能改变到一个不同的组。每个应用都至少有一个线程从属于系统线程组。若创建多个线程而不指定一个组,它们就会自动归属于系统线程组。 所有线程都隶属于一个线程组。那可以是一个默认线程组,亦可是一个创建线程时明确指定的组。在创建之初,线程被限制到一个组里,而且不能改变到一个不同的组。每个应用都至少有一个线程从属于系统线程组。若创建多个线程而不指定一个组,它们就会自动归属于系统线程组。
线程组也必须从属于其他线程组。必须在构器里指定新线程组从属于哪个线程组。若在创建一个线程组的时候没有指定它的归属,则同样会自动成为系统线程组的一名属下。因此,一个应用程序中的所有线程组最终都会将系统线程组作为自己的“父”。 线程组也必须从属于其他线程组。必须在构器里指定新线程组从属于哪个线程组。若在创建一个线程组的时候没有指定它的归属,则同样会自动成为系统线程组的一名属下。因此,一个应用程序中的所有线程组最终都会将系统线程组作为自己的“父”。
之所以要提出“线程组”的概念,很难从字面上找到原因。这多少为我们讨论的主题带来了一些混乱。一般地说,我们认为是由于“安全”或者“保密”方面的理由才使用线程组的。根据Arnold和Gosling的说法:“线程组中的线程可以修改组内的其他线程,包括那些位于分层结构最深处的。一个线程不能修改位于自己所在组或者下属组之外的任何线程”(注释①)。然而,我们很难判断“修改”在这儿的具体含义是什么。下面这个例子展示了位于一个“叶子组”内的线程能修改它所在线程组树的所有线程的优先级,同时还能为这个“树”内的所有线程都调用一个方法。 之所以要提出“线程组”的概念,很难从字面上找到原因。这多少为我们讨论的主题带来了一些混乱。一般地说,我们认为是由于“安全”或者“保密”方面的理由才使用线程组的。根据Arnold和Gosling的说法:“线程组中的线程可以修改组内的其他线程,包括那些位于分层结构最深处的。一个线程不能修改位于自己所在组或者下属组之外的任何线程”(注释①)。然而,我们很难判断“修改”在这儿的具体含义是什么。下面这个例子展示了位于一个“叶子组”内的线程能修改它所在线程组树的所有线程的优先级,同时还能为这个“树”内的所有线程都调用一个方法。
``` ```
......
...@@ -76,8 +76,8 @@ public class ColorBoxes extends Frame { ...@@ -76,8 +76,8 @@ public class ColorBoxes extends Frame {
} ///:~ } ///:~
``` ```
ColorBoxes是一个典型的应用(程序),有一个构建器用于设置GUI。这个构建器采用int grid的一个参数,用它设置GridLayout(网格布局),使每一维里都有一个grid单元。随后,它添加适当数量的CBox对象,用它们填充网格,并为每一个都传递pause值。在main()中,我们可看到如何对pause和grid的默认值进行修改(如果用命令行参数传递)。 ColorBoxes是一个典型的应用(程序),有一个构造器用于设置GUI。这个构造器采用int grid的一个参数,用它设置GridLayout(网格布局),使每一维里都有一个grid单元。随后,它添加适当数量的CBox对象,用它们填充网格,并为每一个都传递pause值。在main()中,我们可看到如何对pause和grid的默认值进行修改(如果用命令行参数传递)。
CBox是进行正式工作的地方。它是从Canvas继承的,并实现了Runnable接口,使每个Canvas也能是一个Thread。记住在实现Runnable的时候,并没有实际产生一个Thread对象,只是一个拥有run()方法的类。因此,我们必须明确地创建一个Thread对象,并将Runnable对象传递给构建器,随后调用start()(在构建器里进行)。在CBox里,这个线程的名字叫作t。 CBox是进行正式工作的地方。它是从Canvas继承的,并实现了Runnable接口,使每个Canvas也能是一个Thread。记住在实现Runnable的时候,并没有实际产生一个Thread对象,只是一个拥有run()方法的类。因此,我们必须明确地创建一个Thread对象,并将Runnable对象传递给构造器,随后调用start()(在构造器里进行)。在CBox里,这个线程的名字叫作t。
请留意数组colors,它对Color类中的所有颜色进行了列举(枚举)。它在newColor()中用于产生一种随机选择的颜色。当前的单元(格)颜色是cColor。 请留意数组colors,它对Color类中的所有颜色进行了列举(枚举)。它在newColor()中用于产生一种随机选择的颜色。当前的单元(格)颜色是cColor。
paint()则相当简单——只是将颜色设为cColor,然后用那种颜色填充整张画布(Canvas)。 paint()则相当简单——只是将颜色设为cColor,然后用那种颜色填充整张画布(Canvas)。
...@@ -185,7 +185,7 @@ CBox2类似CBox——能用一种随机选择的颜色描绘自己。但那就 ...@@ -185,7 +185,7 @@ CBox2类似CBox——能用一种随机选择的颜色描绘自己。但那就
CBoxVector也可以拥有继承的Thread,并有一个类型为Vector的成员对象。这样设计的好处就是addElement()和elementAt()方法可以获得特定的参数以及返回值类型,而不是只能获得常规Object(它们的名字也可以变得更短)。然而,这里采用的设计表面上看需要较少的代码。除此以外,它会自动保留一个Vector的其他所有行为。由于elementAt()需要大量进行“封闭”工作,用到许多括号,所以随着代码主体的扩充,最终仍有可能需要大量代码。 CBoxVector也可以拥有继承的Thread,并有一个类型为Vector的成员对象。这样设计的好处就是addElement()和elementAt()方法可以获得特定的参数以及返回值类型,而不是只能获得常规Object(它们的名字也可以变得更短)。然而,这里采用的设计表面上看需要较少的代码。除此以外,它会自动保留一个Vector的其他所有行为。由于elementAt()需要大量进行“封闭”工作,用到许多括号,所以随着代码主体的扩充,最终仍有可能需要大量代码。
和以前一样,在我们实现Runnable的时候,并没有获得与Thread配套提供的所有功能,所以必须创建一个新的Thread,并将自己传递给它的构建器,以便正式“启动”——start()——一些东西。大家在CBoxVector构建器和go()里都可以体会到这一点。run()方法简单地选择Vector里的一个随机元素编号,并为那个元素调用nextColor(),令其挑选一种新的随机颜色。 和以前一样,在我们实现Runnable的时候,并没有获得与Thread配套提供的所有功能,所以必须创建一个新的Thread,并将自己传递给它的构造器,以便正式“启动”——start()——一些东西。大家在CBoxVector构造器和go()里都可以体会到这一点。run()方法简单地选择Vector里的一个随机元素编号,并为那个元素调用nextColor(),令其挑选一种新的随机颜色。
运行这个程序时,大家会发现它确实变得更快,响应也更迅速(比如在中断它的时候,它能更快地停下来)。而且随着网格尺寸的壮 运行这个程序时,大家会发现它确实变得更快,响应也更迅速(比如在中断它的时候,它能更快地停下来)。而且随着网格尺寸的壮
大,它也不会经常性地陷于“停顿”状态。因此,线程的处理又多了一项新的考虑因素:必须随时检查自己有没有“太多的线程”(无论对什么程序和运行平台)。若线程太多,必须试着使用上面介绍的技术,对程序中的线程数量进行“平衡”。如果在一个多线程的程序中遇到了性能上的问题,那么现在有许多因素需要检查: 大,它也不会经常性地陷于“停顿”状态。因此,线程的处理又多了一项新的考虑因素:必须随时检查自己有没有“太多的线程”(无论对什么程序和运行平台)。若线程太多,必须试着使用上面介绍的技术,对程序中的线程数量进行“平衡”。如果在一个多线程的程序中遇到了性能上的问题,那么现在有许多因素需要检查:
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
14.7 练习 14.7 练习
(1) 从Thread继承一个类,并(重载)覆盖run()方法。在run()内,打印出一条消息,然后调用sleep()。重复三遍这些操作,然后从run()返回。在构器中放置一条启动消息,并覆盖finalize(),打印一条关闭消息。创建一个独立的线程类,使它在run()内调用System.gc()和System.runFinalization(),并打印一条消息,表明调用成功。创建这两种类型的几个线程,然后运行它们,看看会发生什么。 (1) 从Thread继承一个类,并(重载)覆盖run()方法。在run()内,打印出一条消息,然后调用sleep()。重复三遍这些操作,然后从run()返回。在构器中放置一条启动消息,并覆盖finalize(),打印一条关闭消息。创建一个独立的线程类,使它在run()内调用System.gc()和System.runFinalization(),并打印一条消息,表明调用成功。创建这两种类型的几个线程,然后运行它们,看看会发生什么。
(2) 修改Counter2.java,使线程成为一个内部类,而且不需要明确保存指向Counter2的一个。 (2) 修改Counter2.java,使线程成为一个内部类,而且不需要明确保存指向Counter2的一个。
......
...@@ -71,7 +71,7 @@ public class JabberServer { ...@@ -71,7 +71,7 @@ public class JabberServer {
可以看到,ServerSocket需要的只是一个端口编号,不需要IP地址(因为它就在这台机器上运行)。调用accept()时,方法会暂时陷入停顿状态(堵塞),直到某个客户尝试同它建立连接。换言之,尽管它在那里等候连接,但其他进程仍能正常运行(参考第14章)。建好一个连接以后,accept()就会返回一个Socket对象,它是那个连接的代表。 可以看到,ServerSocket需要的只是一个端口编号,不需要IP地址(因为它就在这台机器上运行)。调用accept()时,方法会暂时陷入停顿状态(堵塞),直到某个客户尝试同它建立连接。换言之,尽管它在那里等候连接,但其他进程仍能正常运行(参考第14章)。建好一个连接以后,accept()就会返回一个Socket对象,它是那个连接的代表。
清除套接字的责任在这里得到了很艺术的处理。假如ServerSocket构建器失败,则程序简单地退出(注意必须保证ServerSocket的构建器在失败之后不会留下任何打开的网络套接字)。针对这种情况,main()会“抛”出一个IOException异常,所以不必使用一个try块。若ServerSocket构建器成功执行,则其他所有方法调用都必须到一个try-finally代码块里寻求保护,以确保无论块以什么方式留下,ServerSocket都能正确地关闭。 清除套接字的责任在这里得到了很艺术的处理。假如ServerSocket构造器失败,则程序简单地退出(注意必须保证ServerSocket的构造器在失败之后不会留下任何打开的网络套接字)。针对这种情况,main()会“抛”出一个IOException异常,所以不必使用一个try块。若ServerSocket构造器成功执行,则其他所有方法调用都必须到一个try-finally代码块里寻求保护,以确保无论块以什么方式留下,ServerSocket都能正确地关闭。
同样的道理也适用于由accept()返回的Socket。若accept()失败,那么我们必须保证Socket不再存在或者含有任何资源,以便不必清除它们。但假若执行成功,则后续的语句必须进入一个try-finally块内,以保障在它们失败的情况下,Socket仍能得到正确的清除。由于套接字使用了重要的非内存资源,所以在这里必须特别谨慎,必须自己动手将它们清除(Java中没有提供“破坏器”来帮助我们做这件事情)。 同样的道理也适用于由accept()返回的Socket。若accept()失败,那么我们必须保证Socket不再存在或者含有任何资源,以便不必清除它们。但假若执行成功,则后续的语句必须进入一个try-finally块内,以保障在它们失败的情况下,Socket仍能得到正确的清除。由于套接字使用了重要的非内存资源,所以在这里必须特别谨慎,必须自己动手将它们清除(Java中没有提供“破坏器”来帮助我们做这件事情)。
...@@ -84,7 +84,7 @@ Socket[addr=127.0.0.1,PORT=1077,localport=8080] ...@@ -84,7 +84,7 @@ Socket[addr=127.0.0.1,PORT=1077,localport=8080]
大家不久就会看到它们如何与客户程序做的事情配合。 大家不久就会看到它们如何与客户程序做的事情配合。
程序的下一部分看来似乎仅仅是打开文件,以便读取和写入,只是InputStream和OutputStream是从Socket对象创建的。利用两个“转换器”类InputStreamReader和OutputStreamWriter,InputStream和OutputStream对象已经分别转换成为Java 1.1的Reader和Writer对象。也可以直接使用Java1.0的InputStream和OutputStream类,但对输出来说,使用Writer方式具有明显的优势。这一优势是通过PrintWriter表现出来的,它有一个重载的构器,能获取第二个参数——一个布尔值标志,指向是否在每一次println()结束的时候自动刷新输出(但不适用于print()语句)。每次写入了输出内容后(写进out),它的缓冲区必须刷新,使信息能正式通过网络传递出去。对目前这个例子来说,刷新显得尤为重要,因为客户和服务器在采取下一步操作之前都要等待一行文本内容的到达。若刷新没有发生,那么信息不会进入网络,除非缓冲区满(溢出),这会为本例带来许多问题。 程序的下一部分看来似乎仅仅是打开文件,以便读取和写入,只是InputStream和OutputStream是从Socket对象创建的。利用两个“转换器”类InputStreamReader和OutputStreamWriter,InputStream和OutputStream对象已经分别转换成为Java 1.1的Reader和Writer对象。也可以直接使用Java1.0的InputStream和OutputStream类,但对输出来说,使用Writer方式具有明显的优势。这一优势是通过PrintWriter表现出来的,它有一个重载的构器,能获取第二个参数——一个布尔值标志,指向是否在每一次println()结束的时候自动刷新输出(但不适用于print()语句)。每次写入了输出内容后(写进out),它的缓冲区必须刷新,使信息能正式通过网络传递出去。对目前这个例子来说,刷新显得尤为重要,因为客户和服务器在采取下一步操作之前都要等待一行文本内容的到达。若刷新没有发生,那么信息不会进入网络,除非缓冲区满(溢出),这会为本例带来许多问题。
编写网络应用程序时,需要特别注意自动刷新机制的使用。每次刷新缓冲区时,必须创建和发出一个数据包(数据封)。就目前的情况来说,这正是我们所希望的,因为假如包内包含了还没有发出的文本行,服务器和客户机之间的相互“握手”就会停止。换句话说,一行的末尾就是一条消息的末尾。但在其他许多情况下,消息并不是用行分隔的,所以不如不用自动刷新机制,而用内建的缓冲区判决机制来决定何时发送一个数据包。这样一来,我们可以发出较大的数据包,而且处理进程也能加快。 编写网络应用程序时,需要特别注意自动刷新机制的使用。每次刷新缓冲区时,必须创建和发出一个数据包(数据封)。就目前的情况来说,这正是我们所希望的,因为假如包内包含了还没有发出的文本行,服务器和客户机之间的相互“握手”就会停止。换句话说,一行的末尾就是一条消息的末尾。但在其他许多情况下,消息并不是用行分隔的,所以不如不用自动刷新机制,而用内建的缓冲区判决机制来决定何时发送一个数据包。这样一来,我们可以发出较大的数据包,而且处理进程也能加快。
注意和我们打开的几乎所有数据流一样,它们都要进行缓冲处理。本章末尾有一个练习,清楚展现了假如我们不对数据流进行缓冲,那么会得到什么样的后果(速度会变慢)。 注意和我们打开的几乎所有数据流一样,它们都要进行缓冲处理。本章末尾有一个练习,清楚展现了假如我们不对数据流进行缓冲,那么会得到什么样的后果(速度会变慢)。
...@@ -171,7 +171,7 @@ Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077] ...@@ -171,7 +171,7 @@ Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]
大家会注意到每次重新启动客户程序的时候,本地端口的编号都会增加。这个编号从1025(刚好在系统保留的1-1024之外)开始,并会一直增加下去,除非我们重启机器。若重新启动机器,端口号仍然会从1025开始增值(在Unix机器中,一旦超过保留的套按字范围,数字就会再次从最小的可用数字开始)。 大家会注意到每次重新启动客户程序的时候,本地端口的编号都会增加。这个编号从1025(刚好在系统保留的1-1024之外)开始,并会一直增加下去,除非我们重启机器。若重新启动机器,端口号仍然会从1025开始增值(在Unix机器中,一旦超过保留的套按字范围,数字就会再次从最小的可用数字开始)。
创建好Socket对象后,将其转换成BufferedReader和PrintWriter的过程便与在服务器中相同(同样地,两种情况下都要从一个Socket开始)。在这里,客户通过发出字串"howdy",并在后面跟随一个数字,从而初始化通信。注意缓冲区必须再次刷新(这是自动发生的,通过传递给PrintWriter构器的第二个参数)。若缓冲区没有刷新,那么整个会话(通信)都会被挂起,因为用于初始化的“howdy”永远不会发送出去(缓冲区不够满,不足以造成发送动作的自动进行)。从服务器返回的每一行都会写入System.out,以验证一切都在正常运转。为中止会话,需要发出一个"END"。若客户程序简单地挂起,那么服务器会“抛”出一个异常。 创建好Socket对象后,将其转换成BufferedReader和PrintWriter的过程便与在服务器中相同(同样地,两种情况下都要从一个Socket开始)。在这里,客户通过发出字串"howdy",并在后面跟随一个数字,从而初始化通信。注意缓冲区必须再次刷新(这是自动发生的,通过传递给PrintWriter构器的第二个参数)。若缓冲区没有刷新,那么整个会话(通信)都会被挂起,因为用于初始化的“howdy”永远不会发送出去(缓冲区不够满,不足以造成发送动作的自动进行)。从服务器返回的每一行都会写入System.out,以验证一切都在正常运转。为中止会话,需要发出一个"END"。若客户程序简单地挂起,那么服务器会“抛”出一个异常。
大家在这里可以看到我们采用了同样的措施来确保由Socket代表的网络资源得到正确的清除,这是用一个try-finally块实现的。 大家在这里可以看到我们采用了同样的措施来确保由Socket代表的网络资源得到正确的清除,这是用一个try-finally块实现的。
......
...@@ -82,11 +82,11 @@ public class MultiJabberServer { ...@@ -82,11 +82,11 @@ public class MultiJabberServer {
每次有新客户请求建立一个连接时,ServeOneJabber线程都会取得由accept()在main()中生成的Socket对象。然后和往常一样,它创建一个BufferedReader,并用Socket自动刷新PrintWriter对象。最后,它调用Thread的特殊方法start(),令其进行线程的初始化,然后调用run()。这里采取的操作与前例是一样的:从套扫字读入某些东西,然后把它原样反馈回去,直到遇到一个特殊的"END"结束标志为止。 每次有新客户请求建立一个连接时,ServeOneJabber线程都会取得由accept()在main()中生成的Socket对象。然后和往常一样,它创建一个BufferedReader,并用Socket自动刷新PrintWriter对象。最后,它调用Thread的特殊方法start(),令其进行线程的初始化,然后调用run()。这里采取的操作与前例是一样的:从套扫字读入某些东西,然后把它原样反馈回去,直到遇到一个特殊的"END"结束标志为止。
同样地,套接字的清除必须进行谨慎的设计。就目前这种情况来说,套接字是在ServeOneJabber外部创建的,所以清除工作可以“共享”。若ServeOneJabber构建器失败,那么只需向调用者“抛”出一个异常即可,然后由调用者负责线程的清除。但假如构建器成功,那么必须由ServeOneJabber对象负责线程的清除,这是在它的run()里进行的。 同样地,套接字的清除必须进行谨慎的设计。就目前这种情况来说,套接字是在ServeOneJabber外部创建的,所以清除工作可以“共享”。若ServeOneJabber构造器失败,那么只需向调用者“抛”出一个异常即可,然后由调用者负责线程的清除。但假如构造器成功,那么必须由ServeOneJabber对象负责线程的清除,这是在它的run()里进行的。
请注意MultiJabberServer有多么简单。和以前一样,我们创建一个ServerSocket,并调用accept()允许一个新连接的建立。但这一次,accept()的返回值(一个套接字)将传递给用于ServeOneJabber的构器,由它创建一个新线程,并对那个连接进行控制。连接中断后,线程便可简单地消失。 请注意MultiJabberServer有多么简单。和以前一样,我们创建一个ServerSocket,并调用accept()允许一个新连接的建立。但这一次,accept()的返回值(一个套接字)将传递给用于ServeOneJabber的构器,由它创建一个新线程,并对那个连接进行控制。连接中断后,线程便可简单地消失。
如果ServerSocket创建失败,则再一次通过main()抛出异常。如果成功,则位于外层的try-finally代码块可以担保正确的清除。位于内层的try-catch块只负责防范ServeOneJabber构建器的失败;若构建器成功,则ServeOneJabber线程会将对应的套接字关掉。 如果ServerSocket创建失败,则再一次通过main()抛出异常。如果成功,则位于外层的try-finally代码块可以担保正确的清除。位于内层的try-catch块只负责防范ServeOneJabber构造器的失败;若构造器成功,则ServeOneJabber线程会将对应的套接字关掉。
为了证实服务器代码确实能为多名客户提供服务,下面这个程序将创建许多客户(使用线程),并同相同的服务器建立连接。每个线程的“存在时间”都是有限的。一旦到期,就留出空间以便创建一个新线程。允许创建的线程的最大数量是由final int maxthreads决定的。大家会注意到这个值非常关键,因为假如把它设得很大,线程便有可能耗尽资源,并产生不可预知的程序错误。 为了证实服务器代码确实能为多名客户提供服务,下面这个程序将创建许多客户(使用线程),并同相同的服务器建立连接。每个线程的“存在时间”都是有限的。一旦到期,就留出空间以便创建一个新线程。允许创建的线程的最大数量是由final int maxthreads决定的。大家会注意到这个值非常关键,因为假如把它设得很大,线程便有可能耗尽资源,并产生不可预知的程序错误。
...@@ -175,6 +175,6 @@ public class MultiJabberClient { ...@@ -175,6 +175,6 @@ public class MultiJabberClient {
} ///:~ } ///:~
``` ```
JabberClientThread构建器获取一个InetAddress,并用它打开一个套接字。大家可能已看出了这样的一个套路:Socket肯定用于创建某种Reader以及/或者Writer(或者InputStream和/或OutputStream)对象,这是运用Socket的唯一方式(当然,我们可考虑编写一、两个类,令其自动完成这些操作,避免大量重复的代码编写工作)。同样地,start()执行线程的初始化,并调用run()。在这里,消息发送给服务器,而来自服务器的信息则在屏幕上回显出来。然而,线程的“存在时间”是有限的,最终都会结束。注意在套接字创建好以后,但在构建器完成之前,假若构建器失败,套接字会被清除。否则,为套接字调用close()的责任便落到了run()方法的头上。 JabberClientThread构造器获取一个InetAddress,并用它打开一个套接字。大家可能已看出了这样的一个套路:Socket肯定用于创建某种Reader以及/或者Writer(或者InputStream和/或OutputStream)对象,这是运用Socket的唯一方式(当然,我们可考虑编写一、两个类,令其自动完成这些操作,避免大量重复的代码编写工作)。同样地,start()执行线程的初始化,并调用run()。在这里,消息发送给服务器,而来自服务器的信息则在屏幕上回显出来。然而,线程的“存在时间”是有限的,最终都会结束。注意在套接字创建好以后,但在构造器完成之前,假若构造器失败,套接字会被清除。否则,为套接字调用close()的责任便落到了run()方法的头上。
threadcount跟踪计算目前存在的JabberClientThread对象的数量。它将作为构器的一部分增值,并在run()退出时减值(run()退出意味着线程中止)。在MultiJabberClient.main()中,大家可以看到线程的数量会得到检查。若数量太多,则多余的暂时不创建。方法随后进入“休眠”状态。这样一来,一旦部分线程最后被中止,多作的那些线程就可以创建了。大家可试验一下逐渐增大MAX_THREADS,看看对于你使用的系统来说,建立多少线程(连接)才会使您的系统资源降低到危险程度。 threadcount跟踪计算目前存在的JabberClientThread对象的数量。它将作为构器的一部分增值,并在run()退出时减值(run()退出意味着线程中止)。在MultiJabberClient.main()中,大家可以看到线程的数量会得到检查。若数量太多,则多余的暂时不创建。方法随后进入“休眠”状态。这样一来,一旦部分线程最后被中止,多作的那些线程就可以创建了。大家可试验一下逐渐增大MAX_THREADS,看看对于你使用的系统来说,建立多少线程(连接)才会使您的系统资源降低到危险程度。
...@@ -5,18 +5,18 @@ ...@@ -5,18 +5,18 @@
大家迄今看到的例子使用的都是“传输控制协议”(TCP),亦称作“基于数据流的套接字”。根据该协议的设计宗旨,它具有高度的可靠性,而且能保证数据顺利抵达目的地。换言之,它允许重传那些由于各种原因半路“走失”的数据。而且收到字节的顺序与它们发出来时是一样的。当然,这种控制与可靠性需要我们付出一些代价:TCP具有非常高的开销。 大家迄今看到的例子使用的都是“传输控制协议”(TCP),亦称作“基于数据流的套接字”。根据该协议的设计宗旨,它具有高度的可靠性,而且能保证数据顺利抵达目的地。换言之,它允许重传那些由于各种原因半路“走失”的数据。而且收到字节的顺序与它们发出来时是一样的。当然,这种控制与可靠性需要我们付出一些代价:TCP具有非常高的开销。
还有另一种协议,名为“用户数据报协议”(UDP),它并不刻意追求数据包会完全发送出去,也不能担保它们抵达的顺序与它们发出时一样。我们认为这是一种“不可靠协议”(TCP当然是“可靠协议”)。听起来似乎很糟,但由于它的速度快得多,所以经常还是有用武之地的。对某些应用来说,比如声音信号的传输,如果少量数据包在半路上丢失了,那么用不着太在意,因为传输的速度显得更重要一些。大多数互联网游戏,如Diablo,采用的也是UDP协议通信,因为网络通信的快慢是游戏是否流畅的决定性因素。也可以想想一台报时服务器,如果某条消息丢失了,那么也真的不必过份紧张。另外,有些应用也许能向服务器传回一条UDP消息,以便以后能够恢复。如果在适当的时间里没有响应,消息就会丢失。 还有另一种协议,名为“用户数据报协议”(UDP),它并不刻意追求数据包会完全发送出去,也不能担保它们抵达的顺序与它们发出时一样。我们认为这是一种“不可靠协议”(TCP当然是“可靠协议”)。听起来似乎很糟,但由于它的速度快得多,所以经常还是有用武之地的。对某些应用来说,比如声音信号的传输,如果少量数据包在半路上丢失了,那么用不着太在意,因为传输的速度显得更重要一些。大多数互联网游戏,如Diablo,采用的也是UDP协议通信,因为网络通信的快慢是游戏是否流畅的决定性因素。也可以想想一台报时服务器,如果某条消息丢失了,那么也真的不必过份紧张。另外,有些应用也许能向服务器传回一条UDP消息,以便以后能够恢复。如果在适当的时间里没有响应,消息就会丢失。
Java对数据报的支持与它对TCP套接字的支持大致相同,但也存在一个明显的区别。对数据报来说,我们在客户和服务器程序都可以放置一个DatagramSocket(数据报套接字),但与ServerSocket不同,前者不会干巴巴地等待建立一个连接的请求。这是由于不再存在“连接”,取而代之的是一个数据报陈列出来。另一项本质的区别的是对TCP套接字来说,一旦我们建好了连接,便不再需要关心谁向谁“说话”——只需通过会话流来回传送数据即可。但对数据报来说,它的数据包必须知道自己来自何处,以及打算去哪里。这意味着我们必须知道每个数据报包的这些信息,否则信息就不能正常地传递。 Java对数据报的支持与它对TCP套接字的支持大致相同,但也存在一个明显的区别。对数据报来说,我们在客户和服务器程序都可以放置一个DatagramSocket(数据报套接字),但与ServerSocket不同,前者不会干巴巴地等待建立一个连接的请求。这是由于不再存在“连接”,取而代之的是一个数据报陈列出来。另一项本质的区别的是对TCP套接字来说,一旦我们建好了连接,便不再需要关心谁向谁“说话”——只需通过会话流来回传送数据即可。但对数据报来说,它的数据包必须知道自己来自何处,以及打算去哪里。这意味着我们必须知道每个数据报包的这些信息,否则信息就不能正常地传递。
DatagramSocket用于收发数据包,而DatagramPacket包含了具体的信息。准备接收一个数据报时,只需提供一个缓冲区,以便安置接收到的数据。数据包抵达时,通过DatagramSocket,作为信息起源地的因特网地址以及端口编号会自动得到初化。所以一个用于接收数据报的DatagramPacket构器是: DatagramSocket用于收发数据包,而DatagramPacket包含了具体的信息。准备接收一个数据报时,只需提供一个缓冲区,以便安置接收到的数据。数据包抵达时,通过DatagramSocket,作为信息起源地的因特网地址以及端口编号会自动得到初化。所以一个用于接收数据报的DatagramPacket构器是:
DatagramPacket(buf, buf.length) DatagramPacket(buf, buf.length)
其中,buf是一个字节数组。既然buf是个数组,大家可能会奇怪为什么构器自己不能调查出数组的长度呢?实际上我也有同感,唯一能猜到的原因就是C风格的编程使然,那里的数组不能自己告诉我们它有多大。 其中,buf是一个字节数组。既然buf是个数组,大家可能会奇怪为什么构器自己不能调查出数组的长度呢?实际上我也有同感,唯一能猜到的原因就是C风格的编程使然,那里的数组不能自己告诉我们它有多大。
可以重复使用数据报的接收代码,不必每次都建一个新的。每次用它的时候(再生),缓冲区内的数据都会被覆盖。 可以重复使用数据报的接收代码,不必每次都建一个新的。每次用它的时候(再生),缓冲区内的数据都会被覆盖。
缓冲区的最大容量仅受限于允许的数据报包大小,这个限制位于比64KB稍小的地方。但在许多应用程序中,我们都宁愿它变得还要小一些,特别是在发送数据的时候。具体选择的数据包大小取决于应用程序的特定要求。 缓冲区的最大容量仅受限于允许的数据报包大小,这个限制位于比64KB稍小的地方。但在许多应用程序中,我们都宁愿它变得还要小一些,特别是在发送数据的时候。具体选择的数据包大小取决于应用程序的特定要求。
发出一个数据报时,DatagramPacket不仅需要包含正式的数据,也要包含因特网地址以及端口号,以决定它的目的地。所以用于输出DatagramPacket的构器是: 发出一个数据报时,DatagramPacket不仅需要包含正式的数据,也要包含因特网地址以及端口号,以决定它的目的地。所以用于输出DatagramPacket的构器是:
DatagramPacket(buf, length, inetAddress, port) DatagramPacket(buf, length, inetAddress, port)
这一次,buf(一个字节数组)已经包含了我们想发出的数据。length可以是buf的长度,但也可以更短一些,意味着我们只想发出那么多的字节。另两个参数分别代表数据包要到达的因特网地址以及目标机器的一个目标端口(注释②)。 这一次,buf(一个字节数组)已经包含了我们想发出的数据。length可以是buf的长度,但也可以更短一些,意味着我们只想发出那么多的字节。另两个参数分别代表数据包要到达的因特网地址以及目标机器的一个目标端口(注释②)。
②:我们认为TCP和UDP端口是相互独立的。也就是说,可以在端口8080同时运行一个TCP和UDP服务程序,两者之间不会产生冲突。 ②:我们认为TCP和UDP端口是相互独立的。也就是说,可以在端口8080同时运行一个TCP和UDP服务程序,两者之间不会产生冲突。
大家也许认为两个构器创建了两个不同的对象:一个用于接收数据报,另一个用于发送它们。如果是好的面向对象的设计方案,会建议把它们创建成两个不同的类,而不是具有不同的行为的一个类(具体行为取决于我们如何构建对象)。这也许会成为一个严重的问题,但幸运的是,DatagramPacket的使用相当简单,我们不需要在这个问题上纠缠不清。这一点在下例里将有很明确的说明。该例类似于前面针对TCP套接字的MultiJabberServer和MultiJabberClient例子。多个客户都会将数据报发给服务器,后者会将其反馈回最初发出消息的同样的客户。 大家也许认为两个构器创建了两个不同的对象:一个用于接收数据报,另一个用于发送它们。如果是好的面向对象的设计方案,会建议把它们创建成两个不同的类,而不是具有不同的行为的一个类(具体行为取决于我们如何构建对象)。这也许会成为一个严重的问题,但幸运的是,DatagramPacket的使用相当简单,我们不需要在这个问题上纠缠不清。这一点在下例里将有很明确的说明。该例类似于前面针对TCP套接字的MultiJabberServer和MultiJabberClient例子。多个客户都会将数据报发给服务器,后者会将其反馈回最初发出消息的同样的客户。
为简化从一个String里创建DatagramPacket的工作(或者从DatagramPacket里创建String),这个例子首先用到了一个工具类,名为Dgram: 为简化从一个String里创建DatagramPacket的工作(或者从DatagramPacket里创建String),这个例子首先用到了一个工具类,名为Dgram:
//: Dgram.java //: Dgram.java
...@@ -46,8 +46,8 @@ public class Dgram { ...@@ -46,8 +46,8 @@ public class Dgram {
} }
} ///:~ } ///:~
Dgram的第一个方法采用一个String、一个InetAddress以及一个端口号作为自己的参数,将String的内容复制到一个字节缓冲区,再将缓冲区传递进入DatagramPacket构器,从而构建一个DatagramPacket。注意缓冲区分配时的"+1"——这对防止截尾现象是非常重要的。String的getByte()方法属于一种特殊操作,能将一个字串包含的char复制进入一个字节缓冲。该方法现在已被“反对”使用;Java 1.1有一个“更好”的办法来做这个工作,但在这里却被当作注释屏蔽掉了,因为它会截掉String的部分内容。所以尽管我们在Java 1.1下编译该程序时会得到一条“反对”消息,但它的行为仍然是正确无误的(这个错误应该在你读到这里的时候修正了)。 Dgram的第一个方法采用一个String、一个InetAddress以及一个端口号作为自己的参数,将String的内容复制到一个字节缓冲区,再将缓冲区传递进入DatagramPacket构器,从而构建一个DatagramPacket。注意缓冲区分配时的"+1"——这对防止截尾现象是非常重要的。String的getByte()方法属于一种特殊操作,能将一个字串包含的char复制进入一个字节缓冲。该方法现在已被“反对”使用;Java 1.1有一个“更好”的办法来做这个工作,但在这里却被当作注释屏蔽掉了,因为它会截掉String的部分内容。所以尽管我们在Java 1.1下编译该程序时会得到一条“反对”消息,但它的行为仍然是正确无误的(这个错误应该在你读到这里的时候修正了)。
Dgram.toString()方法同时展示了Java 1.0的方法和Java 1.1的方法(两者是不同的,因为有一种新类型的String构器)。 Dgram.toString()方法同时展示了Java 1.0的方法和Java 1.1的方法(两者是不同的,因为有一种新类型的String构器)。
下面是用于数据报演示的服务器代码: 下面是用于数据报演示的服务器代码:
//: ChatterServer.java //: ChatterServer.java
...@@ -150,7 +150,7 @@ public class ChatterServer { ...@@ -150,7 +150,7 @@ public class ChatterServer {
} }
} ///:~ } ///:~
ChatterClient被创建成一个线程(Thread),所以可以用多个客户来“骚扰”服务器。从中可以看到,用于接收的DatagramPacket和用于ChatterServer的那个是相似的。在构器中,创建DatagramPacket时没有附带任何参数,因为它不需要明确指出自己位于哪个特定编号的端口里。用于这个套接字的因特网地址将成为“这台机器”(比如localhost),而且会自动分配端口编号,这从输出结果即可看出。同用于服务器的那个一样,这个DatagramPacket将同时用于发送和接收。 ChatterClient被创建成一个线程(Thread),所以可以用多个客户来“骚扰”服务器。从中可以看到,用于接收的DatagramPacket和用于ChatterServer的那个是相似的。在构器中,创建DatagramPacket时没有附带任何参数,因为它不需要明确指出自己位于哪个特定编号的端口里。用于这个套接字的因特网地址将成为“这台机器”(比如localhost),而且会自动分配端口编号,这从输出结果即可看出。同用于服务器的那个一样,这个DatagramPacket将同时用于发送和接收。
hostAddress是我们想与之通信的那台机器的因特网地址。在程序中,如果需要创建一个准备传出去的DatagramPacket,那么必须知道一个准确的因特网地址和端口号。可以肯定的是,主机必须位于一个已知的地址和端口号上,使客户能启动与主机的“会话”。 hostAddress是我们想与之通信的那台机器的因特网地址。在程序中,如果需要创建一个准备传出去的DatagramPacket,那么必须知道一个准确的因特网地址和端口号。可以肯定的是,主机必须位于一个已知的地址和端口号上,使客户能启动与主机的“会话”。
每个线程都有自己独一无二的标识号(尽管自动分配给线程的端口号是也会提供一个唯一的标识符)。在run()中,我们创建了一个String消息,其中包含了线程的标识编号以及该线程准备发送的消息编号。我们用这个字串创建一个数据报,发到主机上的指定地址;端口编号则直接从ChatterServer内的一个常数取得。一旦消息发出,receive()就会暂时被“堵塞”起来,直到服务器回复了这条消息。与消息附在一起的所有信息使我们知道回到这个特定线程的东西正是从始发消息中投递出去的。在这个例子中,尽管是一种“不可靠”协议,但仍然能够检查数据报是否到去过了它们该去的地方(这在localhost和LAN环境中是成立的,但在非本地连接中却可能出现一些错误)。 每个线程都有自己独一无二的标识号(尽管自动分配给线程的端口号是也会提供一个唯一的标识符)。在run()中,我们创建了一个String消息,其中包含了线程的标识编号以及该线程准备发送的消息编号。我们用这个字串创建一个数据报,发到主机上的指定地址;端口编号则直接从ChatterServer内的一个常数取得。一旦消息发出,receive()就会暂时被“堵塞”起来,直到服务器回复了这条消息。与消息附在一起的所有信息使我们知道回到这个特定线程的东西正是从始发消息中投递出去的。在这个例子中,尽管是一种“不可靠”协议,但仍然能够检查数据报是否到去过了它们该去的地方(这在localhost和LAN环境中是成立的,但在非本地连接中却可能出现一些错误)。
运行该程序时,大家会发现每个线程都会结束。这意味着发送到服务器的每个数据报包都会回转,并反馈回正确的接收者。如果不是这样,一个或更多的线程就会挂起并进入“堵塞”状态,直到它们的输入被显露出来。 运行该程序时,大家会发现每个线程都会结束。这意味着发送到服务器的每个数据报包都会回转,并反馈回正确的接收者。如果不是这样,一个或更多的线程就会挂起并进入“堵塞”状态,直到它们的输入被显露出来。
......
...@@ -178,7 +178,7 @@ public class NameCollector { ...@@ -178,7 +178,7 @@ public class NameCollector {
NameCollector中的第一个定义应该是大家所熟悉的:选定端口,创建一个数据报包,然后创建指向一个DatagramSocket的引用。接下来的三个定义负责与C程序的连接:一个Process对象是C程序由Java程序启动之后返回的,而且那个Process对象产生了InputStream和OutputStream,分别代表C程序的标准输出和标准输入。和Java IO一样,它们理所当然地需要“封装”起来,所以我们最后得到的是一个PrintStream和DataInputStream。 NameCollector中的第一个定义应该是大家所熟悉的:选定端口,创建一个数据报包,然后创建指向一个DatagramSocket的引用。接下来的三个定义负责与C程序的连接:一个Process对象是C程序由Java程序启动之后返回的,而且那个Process对象产生了InputStream和OutputStream,分别代表C程序的标准输出和标准输入。和Java IO一样,它们理所当然地需要“封装”起来,所以我们最后得到的是一个PrintStream和DataInputStream。
这个程序的所有工作都是在构器内进行的。为启动C程序,需要取得当前的Runtime对象。我们用它调用exec(),再由后者返回Process对象。在Process对象中,大家可看到通过一简单的调用即可生成数据流:getOutputStream()和getInputStream()。从这个时候开始,我们需要考虑的全部事情就是将数据传给数据流nameList,并从addResult中取得结果。 这个程序的所有工作都是在构器内进行的。为启动C程序,需要取得当前的Runtime对象。我们用它调用exec(),再由后者返回Process对象。在Process对象中,大家可看到通过一简单的调用即可生成数据流:getOutputStream()和getInputStream()。从这个时候开始,我们需要考虑的全部事情就是将数据传给数据流nameList,并从addResult中取得结果。
和往常一样,我们将DatagramSocket同一个端口连接到一起。在无限while循环中,程序会调用receive()——除非一个数据报到来,否则receive()会一起处于“堵塞”状态。数据报出现以后,它的内容会提取到String rcvd里。我们首先将该字串两头的空格剔除(trim),再将其发给C程序。如下所示: 和往常一样,我们将DatagramSocket同一个端口连接到一起。在无限while循环中,程序会调用receive()——除非一个数据报到来,否则receive()会一起处于“堵塞”状态。数据报出现以后,它的内容会提取到String rcvd里。我们首先将该字串两头的空格剔除(trim),再将其发给C程序。如下所示:
......
...@@ -212,7 +212,7 @@ CGI程序(不久即可看到)的名字是Listmgr2.exe。许多Web服务器 ...@@ -212,7 +212,7 @@ CGI程序(不久即可看到)的名字是Listmgr2.exe。许多Web服务器
name和email数据都是它们对应的文字框里提取出来,而且两端多余的空格都用trim()剔去了。为了进入列表,email名字被强制换成小写形式,以便能够准确地对比(防止基于大小写形式的错误判断)。来自每个字段的数据都编码为URL形式,随后采用与HTML页中一样的方式汇编GET字串(这样一来,我们可将Java程序片与现有的任何CGI程序结合使用,以满足常规的HTML GET请求)。 name和email数据都是它们对应的文字框里提取出来,而且两端多余的空格都用trim()剔去了。为了进入列表,email名字被强制换成小写形式,以便能够准确地对比(防止基于大小写形式的错误判断)。来自每个字段的数据都编码为URL形式,随后采用与HTML页中一样的方式汇编GET字串(这样一来,我们可将Java程序片与现有的任何CGI程序结合使用,以满足常规的HTML GET请求)。
到这时,一些Java的魔力已经开始发挥作用了:如果想同任何URL连接,只需创建一个URL对象,并将地址传递给构建器即可。构建器会负责建立同服务器的连接(对Web服务器来说,所有连接行动都是根据作为URL使用的字串来判断的)。就目前这种情况来说,URL指向的是当前Web站点的cgi-bin目录(当前Web站点的基础地址是用getDocumentBase()设定的)。一旦Web服务器在URL中看到了一个“cgi-bin”,会接着希望在它后面跟随了cgi-bin目录内的某个程序的名字,那是我们要运行的目标程序。程序名后面是一个问号以及CGI程序会在QUERY_STRING环境变量中查找的一个参数字串(马上就要学到)。 到这时,一些Java的魔力已经开始发挥作用了:如果想同任何URL连接,只需创建一个URL对象,并将地址传递给构造器即可。构造器会负责建立同服务器的连接(对Web服务器来说,所有连接行动都是根据作为URL使用的字串来判断的)。就目前这种情况来说,URL指向的是当前Web站点的cgi-bin目录(当前Web站点的基础地址是用getDocumentBase()设定的)。一旦Web服务器在URL中看到了一个“cgi-bin”,会接着希望在它后面跟随了cgi-bin目录内的某个程序的名字,那是我们要运行的目标程序。程序名后面是一个问号以及CGI程序会在QUERY_STRING环境变量中查找的一个参数字串(马上就要学到)。
我们发出任何形式的请求后,一般都会得到一个回应的HTML页。但若使用Java的URL对象,我们可以拦截自CGI程序传回的任何东西,只需从URL对象里取得一个InputStream(输入数据流)即可。这是用URL对象的openStream()方法实现,它要封装到一个DataInputStream里。随后就可以读取数据行,若readLine()返回一个null(空值),就表明CGI程序已结束了它的输出。 我们发出任何形式的请求后,一般都会得到一个回应的HTML页。但若使用Java的URL对象,我们可以拦截自CGI程序传回的任何东西,只需从URL对象里取得一个InputStream(输入数据流)即可。这是用URL对象的openStream()方法实现,它要封装到一个DataInputStream里。随后就可以读取数据行,若readLine()返回一个null(空值),就表明CGI程序已结束了它的输出。
我们即将看到的CGI程序返回的仅仅是一行,它是用于标志成功与否(以及失败的具体原因)的一个字串。这一行会被捕获并置放第二个Label字段里,使用户看到具体发生了什么事情。 我们即将看到的CGI程序返回的仅仅是一行,它是用于标志成功与否(以及失败的具体原因)的一个字串。这一行会被捕获并置放第二个Label字段里,使用户看到具体发生了什么事情。
...@@ -279,7 +279,7 @@ URL类的最大的特点就是有效地保护了我们的安全。可以同一 ...@@ -279,7 +279,7 @@ URL类的最大的特点就是有效地保护了我们的安全。可以同一
在这儿使用C++的一个原因是要利用C++“标准模板库”(STL)提供的便利。至少,STL包含了一个vector类。这是一个C++模板,可在编译期间进行配置,令其只容纳一种特定类型的对象(这里是Pair对象)。和Java的Vector不同,如果我们试图将除Pair对象之外的任何东西置入vector,C++的vector模板都会造成一个编译期错误;而Java的Vector能够照单全收。而且从vector里取出什么东西的时候,它会自动成为一个Pair对象,毋需进行转换处理。所以检查在编译期进行,这使程序显得更为“健壮”。此外,程序的运行速度也可以加快,因为没有必要进行运行期间的转换。vector也会重载operator[],所以可以利用非常方便的语法来提取Pair对象。vector模板将在CGI_vector创建时使用;在那时,大家就可以体会到如此简短的一个定义居然蕴藏有那么巨大的能量。 在这儿使用C++的一个原因是要利用C++“标准模板库”(STL)提供的便利。至少,STL包含了一个vector类。这是一个C++模板,可在编译期间进行配置,令其只容纳一种特定类型的对象(这里是Pair对象)。和Java的Vector不同,如果我们试图将除Pair对象之外的任何东西置入vector,C++的vector模板都会造成一个编译期错误;而Java的Vector能够照单全收。而且从vector里取出什么东西的时候,它会自动成为一个Pair对象,毋需进行转换处理。所以检查在编译期进行,这使程序显得更为“健壮”。此外,程序的运行速度也可以加快,因为没有必要进行运行期间的转换。vector也会重载operator[],所以可以利用非常方便的语法来提取Pair对象。vector模板将在CGI_vector创建时使用;在那时,大家就可以体会到如此简短的一个定义居然蕴藏有那么巨大的能量。
若提到缺点,就一定不要忘记Pair在下列代码中定义时的复杂程度。与我们在Java代码中看到的相比,Pair的方法定义要多得多。这是由于C++的程序员必须提前知道如何用副本构器控制复制过程,而且要用重载的operator=完成赋值。正如第12章解释的那样,我们有时也要在Java中考虑同样的事情。但在C++中,几乎一刻都不能放松对这些问题的关注。 若提到缺点,就一定不要忘记Pair在下列代码中定义时的复杂程度。与我们在Java代码中看到的相比,Pair的方法定义要多得多。这是由于C++的程序员必须提前知道如何用副本构器控制复制过程,而且要用重载的operator=完成赋值。正如第12章解释的那样,我们有时也要在Java中考虑同样的事情。但在C++中,几乎一刻都不能放松对这些问题的关注。
这个项目首先创建一个可以重复使用的部分,由C++头文件中的Pair和CGI_vector构成。从技术角度看,确实不应把这些东西都塞到一个头文件里。但就目前的例子来说,这样做不会造成任何方面的损害,而且更具有Java风格,所以大家阅读理解代码时要显得轻松一些: 这个项目首先创建一个可以重复使用的部分,由C++头文件中的Pair和CGI_vector构成。从技术角度看,确实不应把这些东西都塞到一个头文件里。但就目前的例子来说,这样做不会造成任何方面的损害,而且更具有Java风格,所以大家阅读理解代码时要显得轻松一些:
``` ```
...@@ -435,13 +435,13 @@ using namespace std; ...@@ -435,13 +435,13 @@ using namespace std;
C++中的“命名空间”(Namespace)解决了由Java的package负责的一个问题:将库名隐藏起来。std命名空间引用的是标准C++库,而vector就在这个库中,所以这一行是必需的。 C++中的“命名空间”(Namespace)解决了由Java的package负责的一个问题:将库名隐藏起来。std命名空间引用的是标准C++库,而vector就在这个库中,所以这一行是必需的。
Pair类表面看异常简单,只是容纳了两个(private)字符指针而已——一个用于名字,另一个用于值。默认构建器将这两个指针简单地设为零。这是由于在C++中,对象的内存不会自动置零。第二个构建器调用方法decodeURLString(),在新分配的堆内存中生成一个解码过后的字串。这个内存区域必须由对象负责管理及清除,这与“破坏器”中见到的相同。name()和value()方法为相关的字段产生只读指针。利用empty()方法,我们查询Pair对象它的某个字段是否为空;返回的结果是一个bool——C++内建的基本布尔数据类型。operator bool()使用的是C++“运算符重载”的一种特殊形式。它允许我们控制自动类型转换。如果有一个名为p的Pair对象,而且在一个本来希望是布尔结果的表达式中使用,比如if(p){//...,那么编译器能辨别出它有一个Pair,而且需要的是个布尔值,所以自动调用operator bool(),进行必要的转换。 Pair类表面看异常简单,只是容纳了两个(private)字符指针而已——一个用于名字,另一个用于值。默认构造器将这两个指针简单地设为零。这是由于在C++中,对象的内存不会自动置零。第二个构造器调用方法decodeURLString(),在新分配的堆内存中生成一个解码过后的字串。这个内存区域必须由对象负责管理及清除,这与“破坏器”中见到的相同。name()和value()方法为相关的字段产生只读指针。利用empty()方法,我们查询Pair对象它的某个字段是否为空;返回的结果是一个bool——C++内建的基本布尔数据类型。operator bool()使用的是C++“运算符重载”的一种特殊形式。它允许我们控制自动类型转换。如果有一个名为p的Pair对象,而且在一个本来希望是布尔结果的表达式中使用,比如if(p){//...,那么编译器能辨别出它有一个Pair,而且需要的是个布尔值,所以自动调用operator bool(),进行必要的转换。
接下来的三个方法属于常规编码,在C++中创建类时必须用到它们。根据C++类采用的所谓“经典形式”,我们必须定义必要的“原始”构建器,以及一个副本构建器和赋值运算符——operator=(以及破坏器,用于清除内存)。之所以要作这样的定义,是由于编译器会“默默”地调用它们。在对象传入、传出一个函数的时候,需要调用副本构建器;而在分配对象时,需要调用赋值运算符。只有真正掌握了副本构建器和赋值运算符的工作原理,才能在C++里写出真正“健壮”的类,但这需要需要一个比较艰苦的过程(注释⑤)。 接下来的三个方法属于常规编码,在C++中创建类时必须用到它们。根据C++类采用的所谓“经典形式”,我们必须定义必要的“原始”构造器,以及一个副本构造器和赋值运算符——operator=(以及破坏器,用于清除内存)。之所以要作这样的定义,是由于编译器会“默默”地调用它们。在对象传入、传出一个函数的时候,需要调用副本构造器;而在分配对象时,需要调用赋值运算符。只有真正掌握了副本构造器和赋值运算符的工作原理,才能在C++里写出真正“健壮”的类,但这需要需要一个比较艰苦的过程(注释⑤)。
⑤:我的《Thinking in C++》(Prentice-Hall,1995)用了一整章的地方来讨论这个主题。若需更多的帮助,请务必看看那一章。 ⑤:我的《Thinking in C++》(Prentice-Hall,1995)用了一整章的地方来讨论这个主题。若需更多的帮助,请务必看看那一章。
只要将一个对象按值传入或传出函数,就会自动调用副本构建器Pair(const Pair&)。也就是说,对于准备为其制作一个完整副本的那个对象,我们不准备在函数框架中传递它的地址。这并不是Java提供的一个选项,由于我们只能传递引用,所以在Java里没有所谓的副本构建器(如果想制作一个本地副本,可以“克隆”那个对象——使用clone(),参见第12章)。类似地,如果在Java里分配一个引用,它会简单地复制。但C++中的赋值意味着整个对象都会复制。在副本构建器中,我们创建新的存储空间,并复制原始数据。但对于赋值运算符,我们必须在分配新存储空间之前释放老存储空间。我们要见到的也许是C++类最复杂的一种情况,但那正是Java的支持者们论证Java比C++简单得多的有力证据。在Java中,我们可以自由传递引用,善后工作则由垃圾收集器负责,所以可以轻松许多。 只要将一个对象按值传入或传出函数,就会自动调用副本构造器Pair(const Pair&)。也就是说,对于准备为其制作一个完整副本的那个对象,我们不准备在函数框架中传递它的地址。这并不是Java提供的一个选项,由于我们只能传递引用,所以在Java里没有所谓的副本构造器(如果想制作一个本地副本,可以“克隆”那个对象——使用clone(),参见第12章)。类似地,如果在Java里分配一个引用,它会简单地复制。但C++中的赋值意味着整个对象都会复制。在副本构造器中,我们创建新的存储空间,并复制原始数据。但对于赋值运算符,我们必须在分配新存储空间之前释放老存储空间。我们要见到的也许是C++类最复杂的一种情况,但那正是Java的支持者们论证Java比C++简单得多的有力证据。在Java中,我们可以自由传递引用,善后工作则由垃圾收集器负责,所以可以轻松许多。
但事情并没有完。Pair类为nm和val使用的是char*,最复杂的情况主要是围绕指针展开的。如果用较时髦的C++ string类来代替 `char*` ,事情就要变得简单得多(当然,并不是所有编译器都提供了对string的支持)。那么,Pair的第一部分看起来就象下面这样: 但事情并没有完。Pair类为nm和val使用的是char*,最复杂的情况主要是围绕指针展开的。如果用较时髦的C++ string类来代替 `char*` ,事情就要变得简单得多(当然,并不是所有编译器都提供了对string的支持)。那么,Pair的第一部分看起来就象下面这样:
...@@ -471,13 +471,13 @@ public: ...@@ -471,13 +471,13 @@ public:
} }
``` ```
(此外,对这个类decodeURLString()会返回一个string,而不是一个char*)。我们不必定义副本构器、operator=或者破坏器,因为编译器已帮我们做了,而且做得非常好。但即使有些事情是自动进行的,C++程序员也必须了解副本构建以及赋值的细节。 (此外,对这个类decodeURLString()会返回一个string,而不是一个char*)。我们不必定义副本构器、operator=或者破坏器,因为编译器已帮我们做了,而且做得非常好。但即使有些事情是自动进行的,C++程序员也必须了解副本构建以及赋值的细节。
Pair类剩下的部分由两个方法构成:decodeURLString()以及一个“帮助器”方法translateHex()——将由decodeURLString()使用。注意translateHex()并不能防范用户的恶意输入,比如“%1H”。分配好足够的存储空间后(必须由破坏器释放),decodeURLString()就会其中遍历,将所有“+”都换成一个空格;将所有十六进制代码(以一个“%”打头)换成对应的字符。 Pair类剩下的部分由两个方法构成:decodeURLString()以及一个“帮助器”方法translateHex()——将由decodeURLString()使用。注意translateHex()并不能防范用户的恶意输入,比如“%1H”。分配好足够的存储空间后(必须由破坏器释放),decodeURLString()就会其中遍历,将所有“+”都换成一个空格;将所有十六进制代码(以一个“%”打头)换成对应的字符。
CGI_vector用于解析和容纳整个CGI GET命令。它是从STL vector里继承的,后者例示为容纳Pair。C++中的继承是用一个冒号表示,在Java中则要用extends。此外,继承默认为private属性,所以几乎肯定需要用到public关键字,就象这样做的那样。大家也会发现CGI_vector有一个副本构器以及一个operator=,但它们都声明成private。这样做是为了防止编译器同步两个函数(如果不自己声明它们,两者就会同步)。但这同时也禁止了客户程序员按值或者通过赋值传递一个CGI_vector。 CGI_vector用于解析和容纳整个CGI GET命令。它是从STL vector里继承的,后者例示为容纳Pair。C++中的继承是用一个冒号表示,在Java中则要用extends。此外,继承默认为private属性,所以几乎肯定需要用到public关键字,就象这样做的那样。大家也会发现CGI_vector有一个副本构器以及一个operator=,但它们都声明成private。这样做是为了防止编译器同步两个函数(如果不自己声明它们,两者就会同步)。但这同时也禁止了客户程序员按值或者通过赋值传递一个CGI_vector。
CGI_vector的工作是获取QUERY_STRING,并把它解析成“名称/值”对,这需要在Pair的帮助下完成。它首先将字串复制到本地分配的内存,并用常数指针start跟踪起始地址(稍后会在破坏器中用于释放内存)。随后,它用自己的nextPair()方法将字串解析成原始的“名称/值”对,各个对之间用一个“=”和“&”符号分隔。这些对由nextPair()传递给Pair构器,所以nextPair()返回的是一个Pair对象。随后用push_back()将该对象加入vector。nextPair()遍历完整个QUERY_STRING后,会返回一个零值。 CGI_vector的工作是获取QUERY_STRING,并把它解析成“名称/值”对,这需要在Pair的帮助下完成。它首先将字串复制到本地分配的内存,并用常数指针start跟踪起始地址(稍后会在破坏器中用于释放内存)。随后,它用自己的nextPair()方法将字串解析成原始的“名称/值”对,各个对之间用一个“=”和“&”符号分隔。这些对由nextPair()传递给Pair构器,所以nextPair()返回的是一个Pair对象。随后用push_back()将该对象加入vector。nextPair()遍历完整个QUERY_STRING后,会返回一个零值。
现在基本工具已定义好,它们可以简单地在一个CGI程序中使用,就象下面这样: 现在基本工具已定义好,它们可以简单地在一个CGI程序中使用,就象下面这样:
...@@ -560,7 +560,7 @@ void main() { ...@@ -560,7 +560,7 @@ void main() {
``` ```
alreadyInList()函数与前一个版本几乎是完全相同的,只是它假定所有电子函件地址都在一个“<>”内。 alreadyInList()函数与前一个版本几乎是完全相同的,只是它假定所有电子函件地址都在一个“<>”内。
在使用GET方法时(通过在FORM引导命令的METHOD标记内部设置,但这在这里由数据发送的方式控制),Web服务器会收集位于“?”后面的所有信息,并把它们置入环境变量QUERY_STRING(查询字串)里。所以为了读取那些信息,必须获得QUERY_STRING的值,这是用标准的C库函数getnv()完成的。在main()中,注意对QUERY_STRING的解析有多么容易:只需把它传递给用于CGI_vector对象的构器(名为query),剩下的所有工作都会自动进行。从这时开始,我们就可以从query中取出名称和值,把它们当作数组看待(这是由于operator[]在vector里已经重载了)。在调试代码中,大家可看到这一切是如何运作的;调试代码封装在预处理器引导命令#if defined(DEBUG)和#endif(DEBUG)之间。 在使用GET方法时(通过在FORM引导命令的METHOD标记内部设置,但这在这里由数据发送的方式控制),Web服务器会收集位于“?”后面的所有信息,并把它们置入环境变量QUERY_STRING(查询字串)里。所以为了读取那些信息,必须获得QUERY_STRING的值,这是用标准的C库函数getnv()完成的。在main()中,注意对QUERY_STRING的解析有多么容易:只需把它传递给用于CGI_vector对象的构器(名为query),剩下的所有工作都会自动进行。从这时开始,我们就可以从query中取出名称和值,把它们当作数组看待(这是由于operator[]在vector里已经重载了)。在调试代码中,大家可看到这一切是如何运作的;调试代码封装在预处理器引导命令#if defined(DEBUG)和#endif(DEBUG)之间。
现在,我们迫切需要掌握一些与CGI有关的东西。CGI程序用两个方式之一传递它们的输入:在GET执行期间通过QUERY_STRING传递(目前用的这种方式),或者在POST期间通过标准输入。但CGI程序通过标准输出发送自己的输出,这通常是用C程序的printf()命令实现的。那么这个输出到哪里去了呢?它回到了Web服务器,由服务器决定该如何处理它。服务器作出决定的依据是content-type(内容类型)头数据。这意味着假如content-type头不是它看到的第一件东西,就不知道该如何处理收到的数据。因此,我们无论如何也要使所有CGI程序都从content-type头开始输出。 现在,我们迫切需要掌握一些与CGI有关的东西。CGI程序用两个方式之一传递它们的输入:在GET执行期间通过QUERY_STRING传递(目前用的这种方式),或者在POST期间通过标准输入。但CGI程序通过标准输出发送自己的输出,这通常是用C程序的printf()命令实现的。那么这个输出到哪里去了呢?它回到了Web服务器,由服务器决定该如何处理它。服务器作出决定的依据是content-type(内容类型)头数据。这意味着假如content-type头不是它看到的第一件东西,就不知道该如何处理收到的数据。因此,我们无论如何也要使所有CGI程序都从content-type头开始输出。
...@@ -609,7 +609,7 @@ void main() { ...@@ -609,7 +609,7 @@ void main() {
getenv()函数返回指向一个字串的指针,那个字串指示着内容的长度。若指针为零,表明CONTENT_LENGTH环境变量尚未设置,所以肯定某个地方出了问题。否则就必须用ANSI C库函数atoi()将字串转换成一个整数。这个长度将与new一起运用,分配足够的存储空间,以便容纳查询字串(另加它的空中止符)。随后为cin()调用read()。read()函数需要取得指向目标缓冲区的一个指针以及要读入的字节数。随后用空字符(null)中止query_str,指出已经抵达字串的末尾,这就叫作“空中止”。 getenv()函数返回指向一个字串的指针,那个字串指示着内容的长度。若指针为零,表明CONTENT_LENGTH环境变量尚未设置,所以肯定某个地方出了问题。否则就必须用ANSI C库函数atoi()将字串转换成一个整数。这个长度将与new一起运用,分配足够的存储空间,以便容纳查询字串(另加它的空中止符)。随后为cin()调用read()。read()函数需要取得指向目标缓冲区的一个指针以及要读入的字节数。随后用空字符(null)中止query_str,指出已经抵达字串的末尾,这就叫作“空中止”。
到这个时候,我们得到的查询字串与GET查询字串已经没有什么区别,所以把它传递给用于CGI_vector的构器。随后便和前例一样,我们可以自由vector内不同的字段。 到这个时候,我们得到的查询字串与GET查询字串已经没有什么区别,所以把它传递给用于CGI_vector的构器。随后便和前例一样,我们可以自由vector内不同的字段。
为测试这个程序,必须把它编译到主机Web服务器的cgi-bin目录下。然后就可以写一个简单的HTML页进行测试,就象下面这样: 为测试这个程序,必须把它编译到主机Web服务器的cgi-bin目录下。然后就可以写一个简单的HTML页进行测试,就象下面这样:
......
...@@ -35,7 +35,7 @@ interface PerfectTimeI extends Remote { ...@@ -35,7 +35,7 @@ interface PerfectTimeI extends Remote {
服务器必须包含一个扩展了UnicastRemoteObject的类,并实现远程接口。这个类也可以含有附加的方法,但客户只能使用远程接口中的方法。这是显然的,因为客户得到的只是指向接口的一个引用,而非实现它的那个类。 服务器必须包含一个扩展了UnicastRemoteObject的类,并实现远程接口。这个类也可以含有附加的方法,但客户只能使用远程接口中的方法。这是显然的,因为客户得到的只是指向接口的一个引用,而非实现它的那个类。
必须为远程对象明确定义构建器,即使只准备定义一个默认构建器,用它调用基础类构建器。必须把它明确地编写出来,因为它必须“抛”出RemoteException异常。 必须为远程对象明确定义构造器,即使只准备定义一个默认构造器,用它调用基础类构造器。必须把它明确地编写出来,因为它必须“抛”出RemoteException异常。
下面列出远程接口PerfectTime的实施过程: 下面列出远程接口PerfectTime的实施过程:
......
...@@ -52,7 +52,7 @@ public class SingletonPattern { ...@@ -52,7 +52,7 @@ public class SingletonPattern {
} ///:~ } ///:~
``` ```
创建单子的关键就是防止客户程序员采用除由我们提供的之外的任何一种方式来创建一个对象。必须将所有构建器都设为private(私有),而且至少要创建一个构建器,以防止编译器帮我们自动同步一个默认构建器(它会自做聪明地创建成为“友好的”——friendly,而非private)。 创建单子的关键就是防止客户程序员采用除由我们提供的之外的任何一种方式来创建一个对象。必须将所有构造器都设为private(私有),而且至少要创建一个构造器,以防止编译器帮我们自动同步一个默认构造器(它会自做聪明地创建成为“友好的”——friendly,而非private)。
此时应决定如何创建自己的对象。在这儿,我们选择了静态创建的方式。但亦可选择等候客户程序员发出一个创建请求,然后根据他们的要求动态创建。不管在哪种情况下,对象都应该保存为“私有”属性。我们通过公用方法提供访问途径。在这里,getHandle()会产生指向Singleton的一个引用。剩下的接口(getValue()和setValue())属于普通的类接口。 此时应决定如何创建自己的对象。在这儿,我们选择了静态创建的方式。但亦可选择等候客户程序员发出一个创建请求,然后根据他们的要求动态创建。不管在哪种情况下,对象都应该保存为“私有”属性。我们通过公用方法提供访问途径。在这里,getHandle()会产生指向Singleton的一个引用。剩下的接口(getValue()和setValue())属于普通的类接口。
......
...@@ -46,7 +46,7 @@ class Info { ...@@ -46,7 +46,7 @@ class Info {
} }
``` ```
Info对象唯一的任务就是容纳用于factory()方法的信息。现在,假如出现了一种特殊情况,factory()需要更多或者不同的信息来新建一种类型的Trash对象,那么再也不需要改动factory()了。通过添加新的数据和构器,我们可以修改Info类,或者采用子类处理更典型的面向对象形式。 Info对象唯一的任务就是容纳用于factory()方法的信息。现在,假如出现了一种特殊情况,factory()需要更多或者不同的信息来新建一种类型的Trash对象,那么再也不需要改动factory()了。通过添加新的数据和构器,我们可以修改Info类,或者采用子类处理更典型的面向对象形式。
用于这个简单示例的factory()方法如下: 用于这个简单示例的factory()方法如下:
...@@ -80,7 +80,7 @@ Info对象唯一的任务就是容纳用于factory()方法的信息。现在, ...@@ -80,7 +80,7 @@ Info对象唯一的任务就是容纳用于factory()方法的信息。现在,
Math.random() * 100))); Math.random() * 100)));
``` ```
我们在这里创建了一个Info对象,用于将数据传入factory();后者在内存堆中创建某种Trash对象,并返回添加到Vector bin内的引用。当然,如果改变了参数的数量及类型,仍然需要修改这个语句。但假如Info对象的创建是自动进行的,也可以避免那个麻烦。例如,可将参数的一个Vector传递到Info对象的构器中(或直接传入一个factory()调用)。这要求在运行期间对参数进行分析与检查,但确实提供了非常高的灵活程度。 我们在这里创建了一个Info对象,用于将数据传入factory();后者在内存堆中创建某种Trash对象,并返回添加到Vector bin内的引用。当然,如果改变了参数的数量及类型,仍然需要修改这个语句。但假如Info对象的创建是自动进行的,也可以避免那个麻烦。例如,可将参数的一个Vector传递到Info对象的构器中(或直接传入一个factory()调用)。这要求在运行期间对参数进行分析与检查,但确实提供了非常高的灵活程度。
大家从这个代码可看出Factory要负责解决的“领头变化”问题:如果向系统添加了新类型(发生了变化),唯一需要修改的代码在Factory内部,所以Factory将那种变化的影响隔离出来了。 大家从这个代码可看出Factory要负责解决的“领头变化”问题:如果向系统添加了新类型(发生了变化),唯一需要修改的代码在Factory内部,所以Factory将那种变化的影响隔离出来了。
...@@ -92,7 +92,7 @@ Info对象唯一的任务就是容纳用于factory()方法的信息。现在, ...@@ -92,7 +92,7 @@ Info对象唯一的任务就是容纳用于factory()方法的信息。现在,
采用这种方案,我们不必用硬编码的方式植入任何创建信息。每个对象都知道如何揭示出适当的信息,以及如何对自身进行克隆。所以一种新类型加入系统的时候,factory()方法不需要任何改变。 采用这种方案,我们不必用硬编码的方式植入任何创建信息。每个对象都知道如何揭示出适当的信息,以及如何对自身进行克隆。所以一种新类型加入系统的时候,factory()方法不需要任何改变。
为解决原型的创建问题,一个方法是添加大量方法,用它们支持新对象的创建。但在Java 1.1中,如果拥有指向Class对象的一个引用,那么它已经提供了对创建新对象的支持。利用Java 1.1的“反射”(已在第11章介绍)技术,即便我们只有指向Class对象的一个引用,亦可正常地调用一个构器。这对原型问题的解决无疑是个完美的方案。 为解决原型的创建问题,一个方法是添加大量方法,用它们支持新对象的创建。但在Java 1.1中,如果拥有指向Class对象的一个引用,那么它已经提供了对创建新对象的支持。利用Java 1.1的“反射”(已在第11章介绍)技术,即便我们只有指向Class对象的一个引用,亦可正常地调用一个构器。这对原型问题的解决无疑是个完美的方案。
原型列表将由指向所有想创建的Class对象的一个引用列表间接地表示。除此之外,假如原型处理失败,则factory()方法会认为由于一个特定的Class对象不在列表中,所以会尝试装载它。通过以这种方式动态装载原型,Trash类根本不需要知道自己要操纵的是什么类型。因此,在我们添加新类型时不需要作出任何形式的修改。于是,我们可在本章剩余的部分方便地重复利用它。 原型列表将由指向所有想创建的Class对象的一个引用列表间接地表示。除此之外,假如原型处理失败,则factory()方法会认为由于一个特定的Class对象不在列表中,所以会尝试装载它。通过以这种方式动态装载原型,Trash类根本不需要知道自己要操纵的是什么类型。因此,在我们添加新类型时不需要作出任何形式的修改。于是,我们可在本章剩余的部分方便地重复利用它。
...@@ -189,14 +189,14 @@ public abstract class Trash { ...@@ -189,14 +189,14 @@ public abstract class Trash {
在Trash.factory()中,Info对象id(Info类的另一个版本,与前面讨论的不同)内部的String包含了要创建的那种Trash的类型名称。这个String会与列表中的Class名比较。若存在相符的,那便是要创建的对象。当然,还有很多方法可以决定我们想创建的对象。之所以要采用这种方法,是因为从一个文件读入的信息可以转换成对象。 在Trash.factory()中,Info对象id(Info类的另一个版本,与前面讨论的不同)内部的String包含了要创建的那种Trash的类型名称。这个String会与列表中的Class名比较。若存在相符的,那便是要创建的对象。当然,还有很多方法可以决定我们想创建的对象。之所以要采用这种方法,是因为从一个文件读入的信息可以转换成对象。
发现自己要创建的Trash(垃圾)种类后,接下来就轮到“反射”方法大显身手了。getConstructor()方法需要取得自己的参数——由Class引用构成的一个数组。这个数组代表着不同的参数,并按它们正确的顺序排列,以便我们查找的构器使用。在这儿,该数组是用Java 1.1的数组创建语法动态创建的: 发现自己要创建的Trash(垃圾)种类后,接下来就轮到“反射”方法大显身手了。getConstructor()方法需要取得自己的参数——由Class引用构成的一个数组。这个数组代表着不同的参数,并按它们正确的顺序排列,以便我们查找的构器使用。在这儿,该数组是用Java 1.1的数组创建语法动态创建的:
``` ```
new Class[] {double.class} new Class[] {double.class}
``` ```
这个代码假定所有Trash类型都有一个需要double数值的构建器(注意double.class与Double.class是不同的)。若考虑一种更灵活的方案,亦可调用getConstructors(),令其返回可用构建器的一个数组。 这个代码假定所有Trash类型都有一个需要double数值的构造器(注意double.class与Double.class是不同的)。若考虑一种更灵活的方案,亦可调用getConstructors(),令其返回可用构造器的一个数组。
从getConstructors()返回的是指向一个Constructor对象的引用(该对象是java.lang.reflect的一部分)。我们用方法newInstance()动态地调用构器。该方法需要获取包含了实际参数的一个Object数组。这个数组同样是按Java 1.1的语法创建的: 从getConstructors()返回的是指向一个Constructor对象的引用(该对象是java.lang.reflect的一部分)。我们用方法newInstance()动态地调用构器。该方法需要获取包含了实际参数的一个Object数组。这个数组同样是按Java 1.1的语法创建的:
``` ```
new Object[] {new Double(info.data)} new Object[] {new Double(info.data)}
...@@ -206,11 +206,11 @@ new Object[] {new Double(info.data)} ...@@ -206,11 +206,11 @@ new Object[] {new Double(info.data)}
理解了具体的过程后,再来创建一个新对象,并且只为它提供一个Class引用,事情就变得非常简单了。就目前的情况来说,内部循环中的return永远不会执行,我们在终点就会退出。在这儿,程序动态装载Class对象,并把它加入trashTypes(垃圾类型)列表,从而试图纠正这个问题。若仍然找不到真正有问题的地方,同时装载又是成功的,那么就重复调用factory方法,重新试一遍。 理解了具体的过程后,再来创建一个新对象,并且只为它提供一个Class引用,事情就变得非常简单了。就目前的情况来说,内部循环中的return永远不会执行,我们在终点就会退出。在这儿,程序动态装载Class对象,并把它加入trashTypes(垃圾类型)列表,从而试图纠正这个问题。若仍然找不到真正有问题的地方,同时装载又是成功的,那么就重复调用factory方法,重新试一遍。
正如大家会看到的那样,这种设计方案最大的优点就是不需要改动代码。无论在什么情况下,它都能正常地使用(假定所有Trash子类都包含了一个构器,用以获取单个double参数)。 正如大家会看到的那样,这种设计方案最大的优点就是不需要改动代码。无论在什么情况下,它都能正常地使用(假定所有Trash子类都包含了一个构器,用以获取单个double参数)。
1. Trash子类 1. Trash子类
为了与原型机制相适应,对Trash每个新子类唯一的要求就是在其中包含了一个构器,指示它获取一个double参数。Java 1.1的“反射”机制可负责剩下的所有工作。 为了与原型机制相适应,对Trash每个新子类唯一的要求就是在其中包含了一个构器,指示它获取一个double参数。Java 1.1的“反射”机制可负责剩下的所有工作。
下面是不同类型的Trash,每种类型都有它们自己的文件里,但都属于Trash包的一部分(同样地,为了方便在本章内重复使用): 下面是不同类型的Trash,每种类型都有它们自己的文件里,但都属于Trash包的一部分(同样地,为了方便在本章内重复使用):
``` ```
...@@ -245,7 +245,7 @@ public class Cardboard extends Trash { ...@@ -245,7 +245,7 @@ public class Cardboard extends Trash {
} ///:~ } ///:~
``` ```
可以看出,除构器以外,这些类根本没有什么特别的地方。 可以看出,除构器以外,这些类根本没有什么特别的地方。
2. 从外部文件中解析出Trash 2. 从外部文件中解析出Trash
......
...@@ -27,7 +27,7 @@ ts.addElement(new Vector()); ...@@ -27,7 +27,7 @@ ts.addElement(new Vector());
OOP设计一条基本的准则是“为状态的变化使用数据成员,为行为的变化使用多性形”。对于容纳Paper(纸张)的Vector,以及容纳Glass(玻璃)的Vector,大家最开始或许会认为分别用于它们的grab()方法肯定会产生不同的行为。但具体如何却完全取决于类型,而不是其他什么东西。可将其解释成一种不同的状态,而且由于Java有一个类可表示类型(Class),所以可用它判断特定的Tbin要容纳什么类型的Trash。 OOP设计一条基本的准则是“为状态的变化使用数据成员,为行为的变化使用多性形”。对于容纳Paper(纸张)的Vector,以及容纳Glass(玻璃)的Vector,大家最开始或许会认为分别用于它们的grab()方法肯定会产生不同的行为。但具体如何却完全取决于类型,而不是其他什么东西。可将其解释成一种不同的状态,而且由于Java有一个类可表示类型(Class),所以可用它判断特定的Tbin要容纳什么类型的Trash。
用于Tbin的构器要求我们为其传递自己选择的一个Class。这样做可告诉Vector它希望容纳的是什么类型。随后,grab()方法用Class BinType和RTTI来检查我们传递给它的Trash对象是否与它希望收集的类型相符。 用于Tbin的构器要求我们为其传递自己选择的一个Class。这样做可告诉Vector它希望容纳的是什么类型。随后,grab()方法用Class BinType和RTTI来检查我们传递给它的Trash对象是否与它希望收集的类型相符。
下面列出完整的解决方案。设定为注释的编号(如*1*)便于大家对照程序后面列出的说明。 下面列出完整的解决方案。设定为注释的编号(如*1*)便于大家对照程序后面列出的说明。
``` ```
......
...@@ -471,22 +471,22 @@ public class CodePackager { ...@@ -471,22 +471,22 @@ public class CodePackager {
1. 构建一个打包文件 1. 构建一个打包文件
第一个构器用于从本书的ASCII文本版里提取出一个文件。发出调用的代码(在列表里较深的地方)会读入并检查每一行,直到找到与一个列表的开头相符的为止。在这个时候,它就会新建一个SourceCodeFile对象,将第一行的内容(已经由调用代码读入了)传递给它,同时还要传递BufferedReader对象,以便在这个缓冲区中提取源码列表剩余的内容。 第一个构器用于从本书的ASCII文本版里提取出一个文件。发出调用的代码(在列表里较深的地方)会读入并检查每一行,直到找到与一个列表的开头相符的为止。在这个时候,它就会新建一个SourceCodeFile对象,将第一行的内容(已经由调用代码读入了)传递给它,同时还要传递BufferedReader对象,以便在这个缓冲区中提取源码列表剩余的内容。
从这时起,大家会发现String方法被频繁运用。为提取出文件名,需调用substring()的重载版本,令其从一个起始偏移开始,一直读到字串的末尾,从而形成一个“子串”。为算出这个起始索引,先要用length()得出startMarker的总长,再用trim()删除字串头尾多余的空格。第一行在文件名后也可能有一些字符;它们是用indexOf()侦测出来的。若没有发现找到我们想寻找的字符,就返回-1;若找到那些字符,就返回它们第一次出现的位置。注意这也是indexOf()的一个重载版本,采用一个字串作为参数,而非一个字符。 从这时起,大家会发现String方法被频繁运用。为提取出文件名,需调用substring()的重载版本,令其从一个起始偏移开始,一直读到字串的末尾,从而形成一个“子串”。为算出这个起始索引,先要用length()得出startMarker的总长,再用trim()删除字串头尾多余的空格。第一行在文件名后也可能有一些字符;它们是用indexOf()侦测出来的。若没有发现找到我们想寻找的字符,就返回-1;若找到那些字符,就返回它们第一次出现的位置。注意这也是indexOf()的一个重载版本,采用一个字串作为参数,而非一个字符。
解析出并保存好文件名后,第一行会被置入字串contents中(该字串用于保存源码清单的完整正文)。随后,将剩余的代码行读入,并合并进入contents字串。当然事情并没有想象的那么简单,因为特定的情况需加以特别的控制。一种情况是错误检查:若直接遇到一个startMarker(起始标记),表明当前操作的这个代码列表没有设置一个结束标记。这属于一个出错条件,需要退出程序。 解析出并保存好文件名后,第一行会被置入字串contents中(该字串用于保存源码清单的完整正文)。随后,将剩余的代码行读入,并合并进入contents字串。当然事情并没有想象的那么简单,因为特定的情况需加以特别的控制。一种情况是错误检查:若直接遇到一个startMarker(起始标记),表明当前操作的这个代码列表没有设置一个结束标记。这属于一个出错条件,需要退出程序。
另一种特殊情况与package关键字有关。尽管Java是一种自由形式的语言,但这个程序要求package关键字必须位于行首。若发现package关键字,就通过检查位于开头的空格以及位于末尾的分号,从而提取出包名(注意亦可一次单独的操作实现,方法是使用重载的substring(),令其同时检查起始和结束索引位置)。随后,将包名中的点号替换成特定的文件分隔符——当然,这里要假设文件分隔符仅有一个字符的长度。尽管这个假设可能对目前的所有系统都是适用的,但一旦遇到问题,一定不要忘了检查一下这里。 另一种特殊情况与package关键字有关。尽管Java是一种自由形式的语言,但这个程序要求package关键字必须位于行首。若发现package关键字,就通过检查位于开头的空格以及位于末尾的分号,从而提取出包名(注意亦可一次单独的操作实现,方法是使用重载的substring(),令其同时检查起始和结束索引位置)。随后,将包名中的点号替换成特定的文件分隔符——当然,这里要假设文件分隔符仅有一个字符的长度。尽管这个假设可能对目前的所有系统都是适用的,但一旦遇到问题,一定不要忘了检查一下这里。
默认操作是将每一行都连接到contents里,同时还有换行字符,直到遇到一个endMarker(结束标记)为止。该标记指出构器应当停止了。若在endMarker之前遇到了文件结尾,就认为存在一个错误。 默认操作是将每一行都连接到contents里,同时还有换行字符,直到遇到一个endMarker(结束标记)为止。该标记指出构器应当停止了。若在endMarker之前遇到了文件结尾,就认为存在一个错误。
2. 从打包文件中提取 2. 从打包文件中提取
第二个构建器用于将源码文件从打包文件中恢复(提取)出来。在这儿,作为调用者的方法不必担心会跳过一些中间文本。打包文件包含了所有源码文件,它们相互间紧密地靠在一起。需要传递给该构建器的仅仅是一个BufferedReader,它代表着“信息源”。构建器会从中提取出自己需要的信息。但在每个代码列表开始的地方还有一些配置信息,它们的身份是用packMarker(打包标记)指出的。若packMarker不存在,意味着调用者试图用错误的方法来使用这个构建器。 第二个构造器用于将源码文件从打包文件中恢复(提取)出来。在这儿,作为调用者的方法不必担心会跳过一些中间文本。打包文件包含了所有源码文件,它们相互间紧密地靠在一起。需要传递给该构造器的仅仅是一个BufferedReader,它代表着“信息源”。构造器会从中提取出自己需要的信息。但在每个代码列表开始的地方还有一些配置信息,它们的身份是用packMarker(打包标记)指出的。若packMarker不存在,意味着调用者试图用错误的方法来使用这个构造器。
一旦发现packMarker,就会将其剥离出来,并提取出目录名(用一个'#'结尾)以及文件名(直到行末)。不管在哪种情况下,旧分隔符都会被替换成本地适用的一个分隔符,这是用String replace()方法实现的。老的分隔符被置于打包文件的开头,在代码列表稍靠后的一部分即可看到是如何把它提取出来的。 一旦发现packMarker,就会将其剥离出来,并提取出目录名(用一个'#'结尾)以及文件名(直到行末)。不管在哪种情况下,旧分隔符都会被替换成本地适用的一个分隔符,这是用String replace()方法实现的。老的分隔符被置于打包文件的开头,在代码列表稍靠后的一部分即可看到是如何把它提取出来的。
器剩下的部分就非常简单了。它读入每一行,把它合并到contents里,直到遇见endMarker为止。 器剩下的部分就非常简单了。它读入每一行,把它合并到contents里,直到遇见endMarker为止。
3. 程序列表的存取 3. 程序列表的存取
...@@ -502,7 +502,7 @@ public class CodePackager { ...@@ -502,7 +502,7 @@ public class CodePackager {
DirMap类可帮助我们实现这一效果,并有效地演示了一个“多重映射”的概述。这是通过一个散列表(Hashtable)实现的,它的“键”是准备创建的子目录,而“值”是包含了那个特定目录中的SourceCodeFile对象的Vector对象。所以,我们在这儿并不是将一个“键”映射(或对应)到一个值,而是通过对应的Vector,将一个键“多重映射”到一系列值。尽管这听起来似乎很复杂,但具体实现时却是非常简单和直接的。大家可以看到,DirMap类的大多数代码都与向文件中的写入有关,而非与“多重映射”有关。与它有关的代码仅极少数而已。 DirMap类可帮助我们实现这一效果,并有效地演示了一个“多重映射”的概述。这是通过一个散列表(Hashtable)实现的,它的“键”是准备创建的子目录,而“值”是包含了那个特定目录中的SourceCodeFile对象的Vector对象。所以,我们在这儿并不是将一个“键”映射(或对应)到一个值,而是通过对应的Vector,将一个键“多重映射”到一系列值。尽管这听起来似乎很复杂,但具体实现时却是非常简单和直接的。大家可以看到,DirMap类的大多数代码都与向文件中的写入有关,而非与“多重映射”有关。与它有关的代码仅极少数而已。
可通过两种方式建立一个DirMap(目录映射或对应)关系:默认构建器假定我们希望目录从当前位置向下展开,而另一个构建器让我们为起始目录指定一个备用的“绝对”路径。 可通过两种方式建立一个DirMap(目录映射或对应)关系:默认构造器假定我们希望目录从当前位置向下展开,而另一个构造器让我们为起始目录指定一个备用的“绝对”路径。
add()方法是一个采取的行动比较密集的场所。首先将directory()从我们想添加的SourceCodeFile里提取出来,然后检查散列表(Hashtable),看看其中是否已经包含了那个键。如果没有,就向散列表加入一个新的Vector,并将它同那个键关联到一起。到这时,不管采取的是什么途径,Vector都已经就位了,可以将它提取出来,以便添加SourceCodeFile。由于Vector可象这样同散列表方便地合并到一起,所以我们从两方面都能感觉得非常方便。 add()方法是一个采取的行动比较密集的场所。首先将directory()从我们想添加的SourceCodeFile里提取出来,然后检查散列表(Hashtable),看看其中是否已经包含了那个键。如果没有,就向散列表加入一个新的Vector,并将它同那个键关联到一起。到这时,不管采取的是什么途径,Vector都已经就位了,可以将它提取出来,以便添加SourceCodeFile。由于Vector可象这样同散列表方便地合并到一起,所以我们从两方面都能感觉得非常方便。
...@@ -514,11 +514,11 @@ add()方法是一个采取的行动比较密集的场所。首先将directory() ...@@ -514,11 +514,11 @@ add()方法是一个采取的行动比较密集的场所。首先将directory()
前面介绍的那些类都要在CodePackager中用到。大家首先看到的是用法字串。一旦最终用户不正确地调用了程序,就会打印出介绍正确用法的这个字串。调用这个字串的是usage()方法,同时还要退出程序。main()唯一的任务就是判断我们希望创建一个打包文件,还是希望从一个打包文件中提取什么东西。随后,它负责保证使用的是正确的参数,并调用适当的方法。 前面介绍的那些类都要在CodePackager中用到。大家首先看到的是用法字串。一旦最终用户不正确地调用了程序,就会打印出介绍正确用法的这个字串。调用这个字串的是usage()方法,同时还要退出程序。main()唯一的任务就是判断我们希望创建一个打包文件,还是希望从一个打包文件中提取什么东西。随后,它负责保证使用的是正确的参数,并调用适当的方法。
创建一个打包文件时,它默认位于当前目录,所以我们用默认构器创建DirMap。打开文件后,其中的每一行都会读入,并检查是否符合特殊的条件: 创建一个打包文件时,它默认位于当前目录,所以我们用默认构器创建DirMap。打开文件后,其中的每一行都会读入,并检查是否符合特殊的条件:
(1) 若行首是一个用于源码列表的起始标记,就新建一个SourceCodeFile对象。构器会读入源码列表剩下的所有内容。结果产生的引用将直接加入DirMap。 (1) 若行首是一个用于源码列表的起始标记,就新建一个SourceCodeFile对象。构器会读入源码列表剩下的所有内容。结果产生的引用将直接加入DirMap。
(2) 若行首是一个用于源码列表的结束标记,表明某个地方出现错误,因为结束标记应当只能由SourceCodeFile构器发现。 (2) 若行首是一个用于源码列表的结束标记,表明某个地方出现错误,因为结束标记应当只能由SourceCodeFile构器发现。
提取/释放一个打包文件时,提取出来的内容可进入当前目录,亦可进入另一个备用目录。所以需要相应地创建DirMap对象。打开文件,并将第一行读入。老的文件路径分隔符信息将从这一行中提取出来。随后根据输入来创建第一个SourceCodeFile对象,它会加入DirMap。只要包含了一个文件,新的SourceCodeFile对象就会创建并加入(创建的最后一个用光输入内容后,会简单地返回,然后hasFile()会返回一个错误)。 提取/释放一个打包文件时,提取出来的内容可进入当前目录,亦可进入另一个备用目录。所以需要相应地创建DirMap对象。打开文件,并将第一行读入。老的文件路径分隔符信息将从这一行中提取出来。随后根据输入来创建第一个SourceCodeFile对象,它会加入DirMap。只要包含了一个文件,新的SourceCodeFile对象就会创建并加入(创建的最后一个用光输入内容后,会简单地返回,然后hasFile()会返回一个错误)。
...@@ -791,7 +791,7 @@ MultiStringMap类是个特殊的工具,允许我们将一组字串与每个键 ...@@ -791,7 +791,7 @@ MultiStringMap类是个特殊的工具,允许我们将一组字串与每个键
针对特定目录中的文件,为找出相应的类与标识符,我们使用了两个MultiStringMap:classMap以及identMap。此外在程序启动的时候,它会将标准类名仓库装载到名为classes的Properties对象中。一旦在本地目录发现了一个新类名,也会将其加入classes以及classMap。这样一来,classMap就可用于在本地目录的所有类间遍历,而且可用classes检查当前标记是不是一个类名(它标记着对象或方法定义的开始,所以收集接下去的记号——直到碰到一个分号——并将它们都置入identMap)。 针对特定目录中的文件,为找出相应的类与标识符,我们使用了两个MultiStringMap:classMap以及identMap。此外在程序启动的时候,它会将标准类名仓库装载到名为classes的Properties对象中。一旦在本地目录发现了一个新类名,也会将其加入classes以及classMap。这样一来,classMap就可用于在本地目录的所有类间遍历,而且可用classes检查当前标记是不是一个类名(它标记着对象或方法定义的开始,所以收集接下去的记号——直到碰到一个分号——并将它们都置入identMap)。
ClassScanner的默认构器会创建一个由文件名构成的列表(采用FilenameFilter的JavaFilter实现形式,参见第10章)。随后会为每个文件名都调用scanListing()。 ClassScanner的默认构器会创建一个由文件名构成的列表(采用FilenameFilter的JavaFilter实现形式,参见第10章)。随后会为每个文件名都调用scanListing()。
在scanListing()内部,会打开源码文件,并将其转换成一个StreamTokenizer。根据Java帮助文档,将true传递给slashStartComments()和slashSlashComments()的本意应当是剥除那些注释内容,但这样做似乎有些问题(在Java 1.0中几乎无效)。所以相反,那些行被当作注释标记出去,并用另一个方法来提取注释。为达到这个目的,'/'必须作为一个原始字符捕获,而不是让StreamTokeinzer将其当作注释的一部分对待。此时要用ordinaryChar()方法指示StreamTokenizer采取正确的操作。同样的道理也适用于点号('.'),因为我们希望让方法调用分离出单独的标识符。但对下划线来说,它最初是被StreamTokenizer当作一个单独的字符对待的,但此时应把它留作标识符的一部分,因为它在static final值中以TT_EOF等等形式使用。当然,这一点只对目前这个特殊的程序成立。wordChars()方法需要取得我们想添加的一系列字符,把它们留在作为一个单词看待的记号中。最后,在解析单行注释或者放弃一行的时候,我们需要知道一个换行动作什么时候发生。所以通过调用eollsSignificant(true),换行符(EOL)会被显示出来,而不是被StreamTokenizer吸收。 在scanListing()内部,会打开源码文件,并将其转换成一个StreamTokenizer。根据Java帮助文档,将true传递给slashStartComments()和slashSlashComments()的本意应当是剥除那些注释内容,但这样做似乎有些问题(在Java 1.0中几乎无效)。所以相反,那些行被当作注释标记出去,并用另一个方法来提取注释。为达到这个目的,'/'必须作为一个原始字符捕获,而不是让StreamTokeinzer将其当作注释的一部分对待。此时要用ordinaryChar()方法指示StreamTokenizer采取正确的操作。同样的道理也适用于点号('.'),因为我们希望让方法调用分离出单独的标识符。但对下划线来说,它最初是被StreamTokenizer当作一个单独的字符对待的,但此时应把它留作标识符的一部分,因为它在static final值中以TT_EOF等等形式使用。当然,这一点只对目前这个特殊的程序成立。wordChars()方法需要取得我们想添加的一系列字符,把它们留在作为一个单词看待的记号中。最后,在解析单行注释或者放弃一行的时候,我们需要知道一个换行动作什么时候发生。所以通过调用eollsSignificant(true),换行符(EOL)会被显示出来,而不是被StreamTokenizer吸收。
...@@ -815,4 +815,4 @@ discardLine()方法是一个简单的工具,用于查找行末位置。注意 ...@@ -815,4 +815,4 @@ discardLine()方法是一个简单的工具,用于查找行末位置。注意
程序列表剩下的部分由main()构成,它负责控制命令行参数,并判断我们是准备在标准Java库的基础上构建由一系列类名构成的“仓库”,还是想检查已写好的那些代码的正确性。不管在哪种情况下,都会创建一个ClassScanner对象。 程序列表剩下的部分由main()构成,它负责控制命令行参数,并判断我们是准备在标准Java库的基础上构建由一系列类名构成的“仓库”,还是想检查已写好的那些代码的正确性。不管在哪种情况下,都会创建一个ClassScanner对象。
无论准备构建一个“仓库”,还是准备使用一个现成的,都必须尝试打开现有仓库。通过创建一个File对象并测试是否存在,就可决定是否打开文件并在ClassScanner中装载classes这个Properties列表(使用load())。来自仓库的类将追加到由ClassScanner构器发现的类后面,而不是将其覆盖。如果仅提供一个命令行参数,就意味着自己想对类名和标识符名字进行一次检查。但假如提供两个参数(第二个是"-a"),就表明自己想构成一个类名仓库。在这种情况下,需要打开一个输出文件,并用Properties.save()方法将列表写入一个文件,同时用一个字串提供文件头信息。 无论准备构建一个“仓库”,还是准备使用一个现成的,都必须尝试打开现有仓库。通过创建一个File对象并测试是否存在,就可决定是否打开文件并在ClassScanner中装载classes这个Properties列表(使用load())。来自仓库的类将追加到由ClassScanner构器发现的类后面,而不是将其覆盖。如果仅提供一个命令行参数,就意味着自己想对类名和标识符名字进行一次检查。但假如提供两个参数(第二个是"-a"),就表明自己想构成一个类名仓库。在这种情况下,需要打开一个输出文件,并用Properties.save()方法将列表写入一个文件,同时用一个字串提供文件头信息。
...@@ -170,7 +170,7 @@ class StripQualifiers { ...@@ -170,7 +170,7 @@ class StripQualifiers {
GUI包含了一个名为name的“文本字段”(TextField),或在其中输入想查找的类名;还包含了另一个文本字段,名为searchFor,可选择性地在其中输入一定的文字,希望在方法列表中查找那些文字。Checkbox(复选框)允许我们指出最终希望在输出中使用完整的名字,还是将前面的各种限定信息删去。最后,结果显示于一个“文本区域”(TextArea)中。 GUI包含了一个名为name的“文本字段”(TextField),或在其中输入想查找的类名;还包含了另一个文本字段,名为searchFor,可选择性地在其中输入一定的文字,希望在方法列表中查找那些文字。Checkbox(复选框)允许我们指出最终希望在输出中使用完整的名字,还是将前面的各种限定信息删去。最后,结果显示于一个“文本区域”(TextArea)中。
大家会注意到这个程序未使用任何按钮或其他组件,不能用它们开始一次搜索。这是由于无论文本字段还是复选框都会受到它们的“侦听者(Listener)对象的监视。只要作出一项改变,结果列表便会立即更新。若改变了name字段中的文字,新的文字就会在NameL类中捕获。若文字不为空,则在Class.forName()中用于尝试查找类。当然,在文字键入期间,名字可能会变得不完整,而Class.forName()会失败,这意味着它会“抛”出一个异常。该异常会被捕获,TextArea会随之设为“Nomatch”(没有相符)。但只要键入了一个正确的名字(大小写也算在内),Class.forName()就会成功,而getMethods()和getConstructors()会分别返回由Method和Constructor对象构成的一个数组。这些数组中的每个对象都会通过toString()转变成一个字串(这样便产生了完整的方法或构器签名),而且两个列表都会合并到n中——一个独立的字串数组。数组n属于DisplayMethods类的一名成员,并在调用reDisplay()时用于显示的更新。 大家会注意到这个程序未使用任何按钮或其他组件,不能用它们开始一次搜索。这是由于无论文本字段还是复选框都会受到它们的“侦听者(Listener)对象的监视。只要作出一项改变,结果列表便会立即更新。若改变了name字段中的文字,新的文字就会在NameL类中捕获。若文字不为空,则在Class.forName()中用于尝试查找类。当然,在文字键入期间,名字可能会变得不完整,而Class.forName()会失败,这意味着它会“抛”出一个异常。该异常会被捕获,TextArea会随之设为“Nomatch”(没有相符)。但只要键入了一个正确的名字(大小写也算在内),Class.forName()就会成功,而getMethods()和getConstructors()会分别返回由Method和Constructor对象构成的一个数组。这些数组中的每个对象都会通过toString()转变成一个字串(这样便产生了完整的方法或构器签名),而且两个列表都会合并到n中——一个独立的字串数组。数组n属于DisplayMethods类的一名成员,并在调用reDisplay()时用于显示的更新。
若改变了Checkbox或searchFor组件,它们的“侦听者”会简单地调用reDisplay()。reDisplay()会创建一个临时数组,其中包含了名为rs的字串(rs代表“结果集”——Result Set)。结果集要么直接从n复制(没有find关键字),要么选择性地从包含了find关键字的n中的字串复制。最后会检查strip Checkbox,看看用户是不是希望将名字中多余的部分删除(默认为“是”)。若答案是肯定的,则用StripQualifiers.strip()做这件事情;反之,就将列表简单地显示出来。 若改变了Checkbox或searchFor组件,它们的“侦听者”会简单地调用reDisplay()。reDisplay()会创建一个临时数组,其中包含了名为rs的字串(rs代表“结果集”——Result Set)。结果集要么直接从n复制(没有find关键字),要么选择性地从包含了find关键字的n中的字串复制。最后会检查strip Checkbox,看看用户是不是希望将名字中多余的部分删除(默认为“是”)。若答案是肯定的,则用StripQualifiers.strip()做这件事情;反之,就将列表简单地显示出来。
......
# 2.1 用引用操纵对象
每种编程语言都有自己的数据处理方式。有些时候,程序员必须时刻留意准备处理的是什么类型。您曾利用一些特殊语法直接操作过对象,或处理过一些间接表示的对象吗(C或C++里的指针)?
所有这些在Java里都得到了简化,任何东西都可看作对象。因此,我们可采用一种统一的语法,任何地方均可照搬不误。但要注意,尽管将一切都“看作”对象,但操纵的标识符实际是指向一个对象的“引用”(Handle)。在其他Java参考书里,还可看到有的人将其称作一个“引用”,甚至一个“指针”。可将这一情形想象成用遥控板(引用)操纵电视机(对象)。只要握住这个遥控板,就相当于掌握了与电视机连接的通道。但一旦需要“换频道”或者“关小声音”,我们实际操纵的是遥控板(引用),再由遥控板自己操纵电视机(对象)。如果要在房间里四处走走,并想保持对电视机的控制,那么手上拿着的是遥控板,而非电视机。
此外,即使没有电视机,遥控板亦可独立存在。也就是说,只是由于拥有一个引用,并不表示必须有一个对象同它连接。所以如果想容纳一个词或句子,可创建一个String引用:
```
String s;
```
但这里创建的只是引用,并不是对象。若此时向s发送一条消息,就会获得一个错误(运行期)。这是由于s实际并未与任何东西连接(即“没有电视机”)。因此,一种更安全的做法是:创建一个引用时,记住无论如何都进行初始化:
```
String s = "asdf";
```
然而,这里采用的是一种特殊类型:字串可用加引号的文字初始化。通常,必须为对象使用一种更通用的初始化类型。
...@@ -72,7 +72,7 @@ Java 1.1增加了两个类,用于进行高精度的计算:BigInteger和BigDe ...@@ -72,7 +72,7 @@ Java 1.1增加了两个类,用于进行高精度的计算:BigInteger和BigDe
BigInteger支持任意精度的整数。也就是说,我们可精确表示任意大小的整数值,同时在运算过程中不会丢失任何信息。 BigInteger支持任意精度的整数。也就是说,我们可精确表示任意大小的整数值,同时在运算过程中不会丢失任何信息。
BigDecimal支持任意精度的定点数字。例如,可用它进行精确的币值计算。 BigDecimal支持任意精度的定点数字。例如,可用它进行精确的币值计算。
至于调用这两个类时可选用的构器和方法,请自行参考联机帮助文档。 至于调用这两个类时可选用的构器和方法,请自行参考联机帮助文档。
2.2.3 Java的数组 2.2.3 Java的数组
......
...@@ -19,7 +19,7 @@ ATypeName a = new ATypeName(); ...@@ -19,7 +19,7 @@ ATypeName a = new ATypeName();
2.4.1 字段和方法 2.4.1 字段和方法
定义一个类时(我们在Java里的全部工作就是定义类、制作那些类的对象以及将消息发给那些对象),可在自己的类里设置两种类型的元素:数据成员(有时也叫“字段”)以及成员函数(通常叫“方法”)。其中,数据成员是一种对象(通过它的引用与其通信),可以为任何类型。它也可以是基本类型(并不是引用)之一。如果是指向对象的一个引用,则必须初始化那个引用,用一种名为“构器”(第4章会对此详述)的特殊函数将其与一个实际对象连接起来(就象早先看到的那样,使用new关键字)。但若是一种基本类型,则可在类定义位置直接初始化(正如后面会看到的那样,引用亦可在定义位置初始化)。 定义一个类时(我们在Java里的全部工作就是定义类、制作那些类的对象以及将消息发给那些对象),可在自己的类里设置两种类型的元素:数据成员(有时也叫“字段”)以及成员函数(通常叫“方法”)。其中,数据成员是一种对象(通过它的引用与其通信),可以为任何类型。它也可以是基本类型(并不是引用)之一。如果是指向对象的一个引用,则必须初始化那个引用,用一种名为“构器”(第4章会对此详述)的特殊函数将其与一个实际对象连接起来(就象早先看到的那样,使用new关键字)。但若是一种基本类型,则可在类定义位置直接初始化(正如后面会看到的那样,引用亦可在定义位置初始化)。
每个对象都为自己的数据成员保有存储空间;数据成员不会在对象之间共享。下面是定义了一些数据成员的类示例: 每个对象都为自己的数据成员保有存储空间;数据成员不会在对象之间共享。下面是定义了一些数据成员的类示例:
......
# 4.1 用构器自动初始化 # 4.1 用构器自动初始化
对于方法的创建,可将其想象成为自己写的每个类都调用一次initialize()。这个名字提醒我们在使用对象之前,应首先进行这样的调用。但不幸的是,这也意味着用户必须记住调用方法。在Java中,由于提供了名为“构建器”的一种特殊方法,所以类的设计者可担保每个对象都会得到正确的初始化。若某个类有一个构建器,那么在创建对象时,Java会自动调用那个构建器——甚至在用户毫不知觉的情况下。所以说这是可以担保的! 对于方法的创建,可将其想象成为自己写的每个类都调用一次initialize()。这个名字提醒我们在使用对象之前,应首先进行这样的调用。但不幸的是,这也意味着用户必须记住调用方法。在Java中,由于提供了名为“构造器”的一种特殊方法,所以类的设计者可担保每个对象都会得到正确的初始化。若某个类有一个构造器,那么在创建对象时,Java会自动调用那个构造器——甚至在用户毫不知觉的情况下。所以说这是可以担保的!
接着的一个问题是如何命名这个方法。存在两方面的问题。第一个是我们使用的任何名字都可能与打算为某个类成员使用的名字冲突。第二是由于编译器的责任是调用构建器,所以它必须知道要调用是哪个方法。C++采取的方案看来是最简单的,且更有逻辑性,所以也在Java里得到了应用:构建器的名字与类名相同。这样一来,可保证象这样的一个方法会在初始化期间自动调用。 接着的一个问题是如何命名这个方法。存在两方面的问题。第一个是我们使用的任何名字都可能与打算为某个类成员使用的名字冲突。第二是由于编译器的责任是调用构造器,所以它必须知道要调用是哪个方法。C++采取的方案看来是最简单的,且更有逻辑性,所以也在Java里得到了应用:构造器的名字与类名相同。这样一来,可保证象这样的一个方法会在初始化期间自动调用。
下面是带有构器的一个简单的类(若执行这个程序有问题,请参考第3章的“赋值”小节)。 下面是带有构器的一个简单的类(若执行这个程序有问题,请参考第3章的“赋值”小节)。
``` ```
//: SimpleConstructor.java //: SimpleConstructor.java
...@@ -32,9 +32,9 @@ public class SimpleConstructor { ...@@ -32,9 +32,9 @@ public class SimpleConstructor {
new Rock(); new Rock();
``` ```
就会分配相应的存储空间,并调用构器。这样可保证在我们经手之前,对象得到正确的初始化。 就会分配相应的存储空间,并调用构器。这样可保证在我们经手之前,对象得到正确的初始化。
请注意所有方法首字母小写的编码规则并不适用于构建器。这是由于构建器的名字必须与类名完全相同! 请注意所有方法首字母小写的编码规则并不适用于构造器。这是由于构造器的名字必须与类名完全相同!
和其他任何方法一样,构建器也能使用参数,以便我们指定对象的具体创建方式。可非常方便地改动上述例子,以便构建器使用自己的参数。如下所示: 和其他任何方法一样,构造器也能使用参数,以便我们指定对象的具体创建方式。可非常方便地改动上述例子,以便构造器使用自己的参数。如下所示:
``` ```
class Rock { class Rock {
...@@ -53,13 +53,13 @@ public class SimpleConstructor { ...@@ -53,13 +53,13 @@ public class SimpleConstructor {
``` ```
利用构建器的参数,我们可为一个对象的初始化设定相应的参数。举个例子来说,假设类Tree有一个构建器,它用一个整数参数标记树的高度,那么就可以象下面这样创建一个Tree对象: 利用构造器的参数,我们可为一个对象的初始化设定相应的参数。举个例子来说,假设类Tree有一个构造器,它用一个整数参数标记树的高度,那么就可以象下面这样创建一个Tree对象:
``` ```
tree t = new Tree(12); // 12英尺高的树 tree t = new Tree(12); // 12英尺高的树
``` ```
若Tree(int)是我们唯一的构器,那么编译器不会允许我们以其他任何方式创建一个Tree对象。 若Tree(int)是我们唯一的构器,那么编译器不会允许我们以其他任何方式创建一个Tree对象。
器有助于消除大量涉及类的问题,并使代码更易阅读。例如在前述的代码段中,我们并未看到对initialize()方法的明确调用——那些方法在概念上独立于定义内容。在Java中,定义和初始化属于统一的概念——两者缺一不可。 器有助于消除大量涉及类的问题,并使代码更易阅读。例如在前述的代码段中,我们并未看到对initialize()方法的明确调用——那些方法在概念上独立于定义内容。在Java中,定义和初始化属于统一的概念——两者缺一不可。
建器属于一种较特殊的方法类型,因为它没有返回值。这与void返回值存在着明显的区别。对于void返回值,尽管方法本身不会自动返回什么,但仍然可以让它返回另一些东西。构建器则不同,它不仅什么也不会自动返回,而且根本不能有任何选择。若存在一个返回值,而且假设我们可以自行选择返回内容,那么编译器多少要知道如何对那个返回值作什么样的处理。 造器属于一种较特殊的方法类型,因为它没有返回值。这与void返回值存在着明显的区别。对于void返回值,尽管方法本身不会自动返回什么,但仍然可以让它返回另一些东西。构造器则不同,它不仅什么也不会自动返回,而且根本不能有任何选择。若存在一个返回值,而且假设我们可以自行选择返回内容,那么编译器多少要知道如何对那个返回值作什么样的处理。
...@@ -9,9 +9,9 @@ ...@@ -9,9 +9,9 @@
大多数程序设计语言(特别是C)要求我们为每个函数都设定一个独一无二的标识符。所以绝对不能用一个名为print()的函数来显示整数,再用另一个print()显示浮点数——每个函数都要求具备唯一的名字。 大多数程序设计语言(特别是C)要求我们为每个函数都设定一个独一无二的标识符。所以绝对不能用一个名为print()的函数来显示整数,再用另一个print()显示浮点数——每个函数都要求具备唯一的名字。
在Java里,另一项因素强迫方法名出现重载情况:构建器。由于构建器的名字由类名决定,所以只能有一个构建器名称。但假若我们想用多种方式创建一个对象呢?例如,假设我们想创建一个类,令其用标准方式进行初始化,另外从文件里读取信息来初始化。此时,我们需要两个构建器,一个没有参数(默认构建器),另一个将字串作为参数——用于初始化对象的那个文件的名字。由于都是构建器,所以它们必须有相同的名字,亦即类名。所以为了让相同的方法名伴随不同的参数类型使用,“方法重载”是非常关键的一项措施。同时,尽管方法重载是构建器必需的,但它亦可应用于其他任何方法,且用法非常方便。 在Java里,另一项因素强迫方法名出现重载情况:构造器。由于构造器的名字由类名决定,所以只能有一个构造器名称。但假若我们想用多种方式创建一个对象呢?例如,假设我们想创建一个类,令其用标准方式进行初始化,另外从文件里读取信息来初始化。此时,我们需要两个构造器,一个没有参数(默认构造器),另一个将字串作为参数——用于初始化对象的那个文件的名字。由于都是构造器,所以它们必须有相同的名字,亦即类名。所以为了让相同的方法名伴随不同的参数类型使用,“方法重载”是非常关键的一项措施。同时,尽管方法重载是构造器必需的,但它亦可应用于其他任何方法,且用法非常方便。
在下面这个例子里,我们向大家同时展示了重载构器和重载的原始方法: 在下面这个例子里,我们向大家同时展示了重载构器和重载的原始方法:
``` ```
//: Overloading.java //: Overloading.java
...@@ -56,9 +56,9 @@ public class Overloading { ...@@ -56,9 +56,9 @@ public class Overloading {
} ///:~ } ///:~
``` ```
Tree既可创建成一颗种子,不含任何参数;亦可创建成生长在苗圃中的植物。为支持这种创建,共使用了两个构建器,一个没有参数(我们把没有参数的构建器称作“默认构建器”,注释①),另一个采用现成的高度。 Tree既可创建成一颗种子,不含任何参数;亦可创建成生长在苗圃中的植物。为支持这种创建,共使用了两个构造器,一个没有参数(我们把没有参数的构造器称作“默认构造器”,注释①),另一个采用现成的高度。
①:在Sun公司出版的一些Java资料中,用简陋但很说明问题的词语称呼这类构建器——“无参数构建器”(no-arg constructors)。但“默认构建器”这个称呼已使用了许多年,所以我选择了它。 ①:在Sun公司出版的一些Java资料中,用简陋但很说明问题的词语称呼这类构造器——“无参数构造器”(no-arg constructors)。但“默认构造器”这个称呼已使用了许多年,所以我选择了它。
我们也有可能希望通过多种途径调用info()方法。例如,假设我们有一条额外的消息想显示出来,就使用String参数;而假设没有其他话可说,就不使用。由于为显然相同的概念赋予了两个独立的名字,所以看起来可能有些古怪。幸运的是,方法重载允许我们为两者使用相同的名字。 我们也有可能希望通过多种途径调用info()方法。例如,假设我们有一条额外的消息想显示出来,就使用String参数;而假设没有其他话可说,就不使用。由于为显然相同的概念赋予了两个独立的名字,所以看起来可能有些古怪。幸运的是,方法重载允许我们为两者使用相同的名字。
...@@ -277,9 +277,9 @@ f(); ...@@ -277,9 +277,9 @@ f();
Java怎样判断f()的具体调用方式呢?而且别人如何识别并理解代码呢?由于存在这一类的问题,所以不能根据返回值类型来区分重载的方法。 Java怎样判断f()的具体调用方式呢?而且别人如何识别并理解代码呢?由于存在这一类的问题,所以不能根据返回值类型来区分重载的方法。
4.2.4 默认构 4.2.4 默认构
正如早先指出的那样,默认构建器是没有参数的。它们的作用是创建一个“空对象”。若创建一个没有构建器的类,则编译程序会帮我们自动创建一个默认构建器。例如: 正如早先指出的那样,默认构造器是没有参数的。它们的作用是创建一个“空对象”。若创建一个没有构造器的类,则编译程序会帮我们自动创建一个默认构造器。例如:
``` ```
//: DefaultConstructor.java //: DefaultConstructor.java
...@@ -301,7 +301,7 @@ public class DefaultConstructor { ...@@ -301,7 +301,7 @@ public class DefaultConstructor {
new Bird(); new Bird();
``` ```
它的作用是新建一个对象,并调用默认构建器——即使尚未明确定义一个象这样的构建器。若没有它,就没有方法可以调用,无法构建我们的对象。然而,如果已经定义了一个构建器(无论是否有参数),编译程序都不会帮我们自动合成一个: 它的作用是新建一个对象,并调用默认构造器——即使尚未明确定义一个象这样的构造器。若没有它,就没有方法可以调用,无法构建我们的对象。然而,如果已经定义了一个构造器(无论是否有参数),编译程序都不会帮我们自动合成一个:
``` ```
class Bush { class Bush {
...@@ -316,7 +316,7 @@ Bush(double d) {} ...@@ -316,7 +316,7 @@ Bush(double d) {}
new Bush(); new Bush();
``` ```
编译程序就会报告自己找不到一个相符的构建器。就好象我们没有设置任何构建器,编译程序会说:“你看来似乎需要一个构建器,所以让我们给你制造一个吧。”但假如我们写了一个构建器,编译程序就会说:“啊,你已写了一个构建器,所以我知道你想干什么;如果你不放置一个默认的,是由于你打算省略它。” 编译程序就会报告自己找不到一个相符的构造器。就好象我们没有设置任何构造器,编译程序会说:“你看来似乎需要一个构造器,所以让我们给你制造一个吧。”但假如我们写了一个构造器,编译程序就会说:“啊,你已写了一个构造器,所以我知道你想干什么;如果你不放置一个默认的,是由于你打算省略它。”
4.2.5 this关键字 4.2.5 this关键字
...@@ -373,11 +373,11 @@ public class Leaf { ...@@ -373,11 +373,11 @@ public class Leaf {
由于increment()通过this关键字返回当前对象的引用,所以可以方便地对同一个对象执行多项操作。 由于increment()通过this关键字返回当前对象的引用,所以可以方便地对同一个对象执行多项操作。
1. 在构建器里调用构建 1. 在构造器里调用构造
若为一个类写了多个构建器,那么经常都需要在一个构建器里调用另一个构建器,以避免写重复的代码。可用this关键字做到这一点。 若为一个类写了多个构造器,那么经常都需要在一个构造器里调用另一个构造器,以避免写重复的代码。可用this关键字做到这一点。
通常,当我们说this的时候,都是指“这个对象”或者“当前对象”。而且它本身会产生当前对象的一个引用。在一个构建器中,若为其赋予一个参数列表,那么this关键字会具有不同的含义:它会对与那个参数列表相符的构建器进行明确的调用。这样一来,我们就可通过一条直接的途径来调用其他构建器。如下所示: 通常,当我们说this的时候,都是指“这个对象”或者“当前对象”。而且它本身会产生当前对象的一个引用。在一个构造器中,若为其赋予一个参数列表,那么this关键字会具有不同的含义:它会对与那个参数列表相符的构造器进行明确的调用。这样一来,我们就可通过一条直接的途径来调用其他构造器。如下所示:
``` ```
//: Flower.java //: Flower.java
...@@ -420,11 +420,11 @@ public class Flower { ...@@ -420,11 +420,11 @@ public class Flower {
} ///:~ } ///:~
``` ```
其中,构建器Flower(String s,int petals)向我们揭示出这样一个问题:尽管可用this调用一个构建器,但不可调用两个。除此以外,构建器调用必须是我们做的第一件事情,否则会收到编译程序的报错信息。 其中,构造器Flower(String s,int petals)向我们揭示出这样一个问题:尽管可用this调用一个构造器,但不可调用两个。除此以外,构造器调用必须是我们做的第一件事情,否则会收到编译程序的报错信息。
这个例子也向大家展示了this的另一项用途。由于参数s的名字以及成员数据s的名字是相同的,所以会出现混淆。为解决这个问题,可用this.s来引用成员数据。经常都会在Java代码里看到这种形式的应用,本书的大量地方也采用了这种做法。 这个例子也向大家展示了this的另一项用途。由于参数s的名字以及成员数据s的名字是相同的,所以会出现混淆。为解决这个问题,可用this.s来引用成员数据。经常都会在Java代码里看到这种形式的应用,本书的大量地方也采用了这种做法。
在print()中,我们发现编译器不让我们从除了一个构建器之外的其他任何方法内部调用一个构建器。 在print()中,我们发现编译器不让我们从除了一个构造器之外的其他任何方法内部调用一个构造器。
2. static的含义 2. static的含义
......
...@@ -128,9 +128,9 @@ int i = f(); ...@@ -128,9 +128,9 @@ int i = f();
这正是编译器对“向前引用”感到不适应的一个地方,因为它与初始化的顺序有关,而不是与程序的编译方式有关。 这正是编译器对“向前引用”感到不适应的一个地方,因为它与初始化的顺序有关,而不是与程序的编译方式有关。
这种初始化方法非常简单和直观。它的一个限制是类型Measurement的每个对象都会获得相同的初始化值。有时,这正是我们希望的结果,但有时却需要盼望更大的灵活性。 这种初始化方法非常简单和直观。它的一个限制是类型Measurement的每个对象都会获得相同的初始化值。有时,这正是我们希望的结果,但有时却需要盼望更大的灵活性。
4.4.2 构器初始化 4.4.2 构器初始化
可考虑用构建器执行初始化进程。这样便可在编程时获得更大的灵活程度,因为我们可以在运行期调用方法和采取行动,从而“现场”决定初始化值。但要注意这样一件事情:不可妨碍自动初始化的进行,它在构建器进入之前就会发生。因此,假如使用下述代码: 可考虑用构造器执行初始化进程。这样便可在编程时获得更大的灵活程度,因为我们可以在运行期调用方法和采取行动,从而“现场”决定初始化值。但要注意这样一件事情:不可妨碍自动初始化的进行,它在构造器进入之前就会发生。因此,假如使用下述代码:
``` ```
class Counter { class Counter {
...@@ -139,13 +139,13 @@ Counter() { i = 7; } ...@@ -139,13 +139,13 @@ Counter() { i = 7; }
// . . . // . . .
``` ```
那么i首先会初始化成零,然后变成7。对于所有基本类型以及对象引用,这种情况都是成立的,其中包括在定义时已进行了明确初始化的那些一些。考虑到这个原因,编译器不会试着强迫我们在构器任何特定的场所对元素进行初始化,或者在它们使用之前——初始化早已得到了保证(注释⑤)。 那么i首先会初始化成零,然后变成7。对于所有基本类型以及对象引用,这种情况都是成立的,其中包括在定义时已进行了明确初始化的那些一些。考虑到这个原因,编译器不会试着强迫我们在构器任何特定的场所对元素进行初始化,或者在它们使用之前——初始化早已得到了保证(注释⑤)。
⑤:相反,C++有自己的“构建器初始模块列表”,能在进入构建器主体之前进行初始化,而且它对于对象来说是强制进行的。参见《Thinking in C++》。 ⑤:相反,C++有自己的“构造器初始模块列表”,能在进入构造器主体之前进行初始化,而且它对于对象来说是强制进行的。参见《Thinking in C++》。
1. 初始化顺序 1. 初始化顺序
在一个类里,初始化的顺序是由变量在类内的定义顺序决定的。即使变量定义大量遍布于方法定义的中间,那些变量仍会在调用任何方法之前得到初始化——甚至在构器调用之前。例如: 在一个类里,初始化的顺序是由变量在类内的定义顺序决定的。即使变量定义大量遍布于方法定义的中间,那些变量仍会在调用任何方法之前得到初始化——甚至在构器调用之前。例如:
``` ```
//: OrderOfInitialization.java //: OrderOfInitialization.java
...@@ -181,7 +181,7 @@ public class OrderOfInitialization { ...@@ -181,7 +181,7 @@ public class OrderOfInitialization {
} ///:~ } ///:~
``` ```
在Card中,Tag对象的定义故意到处散布,以证明它们全都会在构建器进入或者发生其他任何事情之前得到初始化。除此之外,t3在构建器内部得到了重新初始化。它的输入结果如下: 在Card中,Tag对象的定义故意到处散布,以证明它们全都会在构造器进入或者发生其他任何事情之前得到初始化。除此之外,t3在构造器内部得到了重新初始化。它的输入结果如下:
``` ```
Tag(1) Tag(1)
...@@ -192,7 +192,7 @@ Tag(33) ...@@ -192,7 +192,7 @@ Tag(33)
f() f()
``` ```
因此,t3引用会被初始化两次,一次在构建器调用前,一次在调用期间(第一个对象会被丢弃,所以它后来可被当作垃圾收掉)。从表面看,这样做似乎效率低下,但它能保证正确的初始化——若定义了一个重载的构建器,它没有初始化t3;同时在t3的定义里并没有规定“默认”的初始化方式,那么会产生什么后果呢? 因此,t3引用会被初始化两次,一次在构造器调用前,一次在调用期间(第一个对象会被丢弃,所以它后来可被当作垃圾收掉)。从表面看,这样做似乎效率低下,但它能保证正确的初始化——若定义了一个重载的构造器,它没有初始化t3;同时在t3的定义里并没有规定“默认”的初始化方式,那么会产生什么后果呢?
2. 静态数据的初始化 2. 静态数据的初始化
...@@ -294,7 +294,7 @@ static初始化只有在必要的时候才会进行。如果不创建一个Table ...@@ -294,7 +294,7 @@ static初始化只有在必要的时候才会进行。如果不创建一个Table
(5) 进行字段定义时发生的所有初始化都会执行。 (5) 进行字段定义时发生的所有初始化都会执行。
(6) 执行构器。正如第6章将要讲到的那样,这实际可能要求进行相当多的操作,特别是在涉及继承的时候。 (6) 执行构器。正如第6章将要讲到的那样,这实际可能要求进行相当多的操作,特别是在涉及继承的时候。
3. 明确进行的静态初始化 3. 明确进行的静态初始化
......
# 4.6 总结 # 4.6 总结
作为初始化的一种具体操作形式,构建器应使大家明确感受到在语言中进行初始化的重要性。与C++的程序设计一样,判断一个程序效率如何,关键是看是否由于变量的初始化不正确而造成了严重的编程错误(臭虫)。这些形式的错误很难发现,而且类似的问题也适用于不正确的清除或收尾工作。由于构建器使我们能保证正确的初始化和清除(若没有正确的构建器调用,编译器不允许对象创建),所以能获得完全的控制权和安全性。 作为初始化的一种具体操作形式,构造器应使大家明确感受到在语言中进行初始化的重要性。与C++的程序设计一样,判断一个程序效率如何,关键是看是否由于变量的初始化不正确而造成了严重的编程错误(臭虫)。这些形式的错误很难发现,而且类似的问题也适用于不正确的清除或收尾工作。由于构造器使我们能保证正确的初始化和清除(若没有正确的构造器调用,编译器不允许对象创建),所以能获得完全的控制权和安全性。
在C++中,与“构建”相反的“破坏”(Destruction)工作也是相当重要的,因为用new创建的对象必须明确地清除。在Java中,垃圾收集器会自动为所有对象释放内存,所以Java中等价的清除方法并不是经常都需要用到的。如果不需要类似于构器的行为,Java的垃圾收集器可以极大简化编程工作,而且在内存的管理过程中增加更大的安全性。有些垃圾收集器甚至能清除其他资源,比如图形和文件引用等。然而,垃圾收集器确实也增加了运行期的开销。但这种开销到底造成了多大的影响却是很难看出的,因为到目前为止,Java解释器的总体运行速度仍然是比较慢的。随着这一情况的改观,我们应该能判断出垃圾收集器的开销是否使Java不适合做一些特定的工作(其中一个问题是垃圾收集器不可预测的性质)。 在C++中,与“构建”相反的“破坏”(Destruction)工作也是相当重要的,因为用new创建的对象必须明确地清除。在Java中,垃圾收集器会自动为所有对象释放内存,所以Java中等价的清除方法并不是经常都需要用到的。如果不需要类似于构器的行为,Java的垃圾收集器可以极大简化编程工作,而且在内存的管理过程中增加更大的安全性。有些垃圾收集器甚至能清除其他资源,比如图形和文件引用等。然而,垃圾收集器确实也增加了运行期的开销。但这种开销到底造成了多大的影响却是很难看出的,因为到目前为止,Java解释器的总体运行速度仍然是比较慢的。随着这一情况的改观,我们应该能判断出垃圾收集器的开销是否使Java不适合做一些特定的工作(其中一个问题是垃圾收集器不可预测的性质)。
由于所有对象都肯定能获得正确的构建,所以同这儿讲述的情况相比,构建器实际做的事情还要多得多。特别地,当我们通过“创作”或“继承”生成新类的时候,对构建的保证仍然有效,而且需要一些附加的语法来提供对它的支持。大家将在以后的章节里详细了解创作、继承以及它们对构建器造成的影响。 由于所有对象都肯定能获得正确的构建,所以同这儿讲述的情况相比,构造器实际做的事情还要多得多。特别地,当我们通过“创作”或“继承”生成新类的时候,对构建的保证仍然有效,而且需要一些附加的语法来提供对它的支持。大家将在以后的章节里详细了解创作、继承以及它们对构造器造成的影响。
# 4.7 练习 # 4.7 练习
(1) 用默认构器创建一个类(没有参数),用它打印一条消息。创建属于这个类的一个对象。 (1) 用默认构器创建一个类(没有参数),用它打印一条消息。创建属于这个类的一个对象。
(2) 在练习1的基础上增加一个重载的构器,令其采用一个String参数,并随同自己的消息打印出来。 (2) 在练习1的基础上增加一个重载的构器,令其采用一个String参数,并随同自己的消息打印出来。
(3) 以练习2创建的类为基础上,创建属于它的对象引用的一个数组,但不要实际创建对象并分配到数组里。运行程 (3) 以练习2创建的类为基础上,创建属于它的对象引用的一个数组,但不要实际创建对象并分配到数组里。运行程
序时,注意是否打印出来自构器调用的初始化消息。 序时,注意是否打印出来自构器调用的初始化消息。
(4) 创建同引用数组联系起来的对象,最终完成练习3。 (4) 创建同引用数组联系起来的对象,最终完成练习3。
......
...@@ -57,7 +57,7 @@ public class Dinner { ...@@ -57,7 +57,7 @@ public class Dinner {
} ///:~ } ///:~
``` ```
就可以创建一个Cookie对象,因为它的构器是public的,而且类也是public的(公共类的概念稍后还会进行更详细的讲述)。然而,foo()成员不可在Dinner.java内访问,因为foo()只有在dessert包内才是“友好”的。 就可以创建一个Cookie对象,因为它的构器是public的,而且类也是public的(公共类的概念稍后还会进行更详细的讲述)。然而,foo()成员不可在Dinner.java内访问,因为foo()只有在dessert包内才是“友好”的。
1. 默认包 1. 默认包
...@@ -113,9 +113,9 @@ public class IceCream { ...@@ -113,9 +113,9 @@ public class IceCream {
} ///:~ } ///:~
``` ```
这个例子向我们证明了使用private的方便:有时可能想控制对象的创建方式,并防止有人直接访问一个特定的构建器(或者所有构建器)。在上面的例子中,我们不可通过它的构建器创建一个Sundae对象;相反,必须调用makeASundae()方法来实现(注释③)。 这个例子向我们证明了使用private的方便:有时可能想控制对象的创建方式,并防止有人直接访问一个特定的构造器(或者所有构造器)。在上面的例子中,我们不可通过它的构造器创建一个Sundae对象;相反,必须调用makeASundae()方法来实现(注释③)。
③:此时还会产生另一个影响:由于默认构器是唯一获得定义的,而且它的属性是private,所以可防止对这个类的继承(这是第6章要重点讲述的主题)。 ③:此时还会产生另一个影响:由于默认构器是唯一获得定义的,而且它的属性是private,所以可防止对这个类的继承(这是第6章要重点讲述的主题)。
若确定一个类只有一个“助手”方法,那么对于任何方法来说,都可以把它们设为private,从而保证自己不会误在包内其他地方使用它,防止自己更改或删除方法。将一个方法的属性设为private后,可保证自己一直保持这一选项(然而,若一个引用被设为private,并不表明其他对象不能拥有指向同一个对象的public引用。有关“别名”的问题将在第12章详述)。 若确定一个类只有一个“助手”方法,那么对于任何方法来说,都可以把它们设为private,从而保证自己不会误在包内其他地方使用它,防止自己更改或删除方法。将一个方法的属性设为private后,可保证自己一直保持这一选项(然而,若一个引用被设为private,并不表明其他对象不能拥有指向同一个对象的public引用。有关“别名”的问题将在第12章详述)。
......
...@@ -31,7 +31,7 @@ import mylib.*; ...@@ -31,7 +31,7 @@ import mylib.*;
如果已经获得了mylib内部的一个类,准备用它完成由Widget或者mylib内部的其他某些public类执行的任务,此时又会出现什么情况呢?我们不希望花费力气为客户程序员编制文档,并感觉以后某个时候也许会进行大手笔的修改,并将自己的类一起删掉,换成另一个不同的类。为获得这种灵活处理的能力,需要保证没有客户程序员能够依赖自己隐藏于mylib内部的特定实施细节。为达到这个目的,只需将public关键字从类中剔除即可,这样便把类变成了“友好的”(类仅能在包内使用)。 如果已经获得了mylib内部的一个类,准备用它完成由Widget或者mylib内部的其他某些public类执行的任务,此时又会出现什么情况呢?我们不希望花费力气为客户程序员编制文档,并感觉以后某个时候也许会进行大手笔的修改,并将自己的类一起删掉,换成另一个不同的类。为获得这种灵活处理的能力,需要保证没有客户程序员能够依赖自己隐藏于mylib内部的特定实施细节。为达到这个目的,只需将public关键字从类中剔除即可,这样便把类变成了“友好的”(类仅能在包内使用)。
注意不可将类设成private(那样会使除类之外的其他东西都不能访问它),也不能设成protected(注释④)。因此,我们现在对于类的访问只有两个选择:“友好的”或者public。若不愿其他任何人访问那个类,可将所有构器设为private。这样一来,在类的一个static成员内部,除自己之外的其他所有人都无法创建属于那个类的一个对象(注释⑤)。如下例所示: 注意不可将类设成private(那样会使除类之外的其他东西都不能访问它),也不能设成protected(注释④)。因此,我们现在对于类的访问只有两个选择:“友好的”或者public。若不愿其他任何人访问那个类,可将所有构器设为private。这样一来,在类的一个static成员内部,除自己之外的其他所有人都无法创建属于那个类的一个对象(注释⑤)。如下例所示:
``` ```
//: Lunch.java //: Lunch.java
...@@ -85,7 +85,7 @@ return psl; ...@@ -85,7 +85,7 @@ return psl;
它最开始多少会使人有些迷惑。位于方法名(access)前的单词指出方法到底返回什么。在这之前,我们看到的都是void,它意味着“什么也不返回”(void在英语里是“虚无”的意思。但亦可返回指向一个对象的引用,此时出现的就是这个情况。该方法返回一个引用,它指向类Soup的一个对象。 它最开始多少会使人有些迷惑。位于方法名(access)前的单词指出方法到底返回什么。在这之前,我们看到的都是void,它意味着“什么也不返回”(void在英语里是“虚无”的意思。但亦可返回指向一个对象的引用,此时出现的就是这个情况。该方法返回一个引用,它指向类Soup的一个对象。
Soup类向我们展示出如何通过将所有构建器都设为private,从而防止直接创建一个类。请记住,假若不明确地至少创建一个构建器,就会自动创建默认构建器(没有参数)。若自己编写默认构建器,它就不会自动创建。把它变成private后,就没人能为那个类创建一个对象。但别人怎样使用这个类呢?上面的例子为我们揭示出了两个选择。第一个选择,我们可创建一个static方法,再通过它创建一个新的Soup,然后返回指向它的一个引用。如果想在返回之前对Soup进行一些额外的操作,或者想了解准备创建多少个Soup对象(可能是为了限制它们的个数),这种方案无疑是特别有用的。 Soup类向我们展示出如何通过将所有构造器都设为private,从而防止直接创建一个类。请记住,假若不明确地至少创建一个构造器,就会自动创建默认构造器(没有参数)。若自己编写默认构造器,它就不会自动创建。把它变成private后,就没人能为那个类创建一个对象。但别人怎样使用这个类呢?上面的例子为我们揭示出了两个选择。第一个选择,我们可创建一个static方法,再通过它创建一个新的Soup,然后返回指向它的一个引用。如果想在返回之前对Soup进行一些额外的操作,或者想了解准备创建多少个Soup对象(可能是为了限制它们的个数),这种方案无疑是特别有用的。
第二个选择是采用“设计方案”(Design Pattern)技术,本书后面会对此进行详细介绍。通常方案叫作“独子”,因为它仅允许创建一个对象。类Soup的对象被创建成Soup的一个static private成员,所以有一个而且只能有一个。除非通过public方法access(),否则根本无法访问它。 第二个选择是采用“设计方案”(Design Pattern)技术,本书后面会对此进行详细介绍。通常方案叫作“独子”,因为它仅允许创建一个对象。类Soup的对象被创建成Soup的一个static private成员,所以有一个而且只能有一个。除非通过public方法access(),否则根本无法访问它。
......
...@@ -46,7 +46,7 @@ System.out.println("source = " + source) ; ...@@ -46,7 +46,7 @@ System.out.println("source = " + source) ;
编译器会发现我们试图向一个WaterSource添加一个String对象("source =")。这对它来说是不可接受的,因为我们只能将一个字串“添加”到另一个字串,所以它会说:“我要调用toString(),把source转换成字串!”经这样处理后,它就能编译两个字串,并将结果字串传递给一个System.out.println()。每次随同自己创建的一个类允许这种行为的时候,都只需要写一个toString()方法。 编译器会发现我们试图向一个WaterSource添加一个String对象("source =")。这对它来说是不可接受的,因为我们只能将一个字串“添加”到另一个字串,所以它会说:“我要调用toString(),把source转换成字串!”经这样处理后,它就能编译两个字串,并将结果字串传递给一个System.out.println()。每次随同自己创建的一个类允许这种行为的时候,都只需要写一个toString()方法。
如果不深究,可能会草率地认为编译器会为上述代码中的每个引用都自动构造对象(由于Java的安全和谨慎的形象)。例如,可能以为它会为WaterSource调用默认构器,以便初始化source。打印语句的输出事实上是: 如果不深究,可能会草率地认为编译器会为上述代码中的每个引用都自动构造对象(由于Java的安全和谨慎的形象)。例如,可能以为它会为WaterSource调用默认构器,以便初始化source。打印语句的输出事实上是:
``` ```
valve1 = null valve1 = null
...@@ -62,9 +62,9 @@ source = null ...@@ -62,9 +62,9 @@ source = null
编译器并不只是为每个引用创建一个默认对象,因为那样会在许多情况下招致不必要的开销。如希望引用得到初始化,可在下面这些地方进行: 编译器并不只是为每个引用创建一个默认对象,因为那样会在许多情况下招致不必要的开销。如希望引用得到初始化,可在下面这些地方进行:
(1) 在对象定义的时候。这意味着它们在构器调用之前肯定能得到初始化。 (1) 在对象定义的时候。这意味着它们在构器调用之前肯定能得到初始化。
(2) 在那个类的构器中。 (2) 在那个类的构器中。
(3) 紧靠在要求实际使用那个对象之前。这样做可减少不必要的开销——假如对象并不需要创建的话。 (3) 紧靠在要求实际使用那个对象之前。这样做可减少不必要的开销——假如对象并不需要创建的话。
...@@ -118,7 +118,7 @@ public class Bath { ...@@ -118,7 +118,7 @@ public class Bath {
} ///:~ } ///:~
``` ```
请注意在Bath构器中,在所有初始化开始之前执行了一个语句。如果不在定义时进行初始化,仍然不能保证能在将一条消息发给一个对象引用之前会执行任何初始化——除非出现不可避免的运行期异常。 请注意在Bath构器中,在所有初始化开始之前执行了一个语句。如果不在定义时进行初始化,仍然不能保证能在将一条消息发给一个对象引用之前会执行任何初始化——除非出现不可避免的运行期异常。
下面是该程序的输出: 下面是该程序的输出:
``` ```
......
# 6.11 练习 # 6.11 练习
(1) 用默认构建器(空参数列表)创建两个类:A和B,令它们自己声明自己。从A继承一个名为C的新类,并在C内创建一个成员B。不要为C创建一个构建器。创建类C的一个对象,并观察结果。 (1) 用默认构造器(空参数列表)创建两个类:A和B,令它们自己声明自己。从A继承一个名为C的新类,并在C内创建一个成员B。不要为C创建一个构造器。创建类C的一个对象,并观察结果。
(2) 修改练习1,使A和B都有含有参数的构建器,则不是采用默认构建器。为C写一个构建器,并在C的构建器中执行所有初始化工作。 (2) 修改练习1,使A和B都有含有参数的构造器,则不是采用默认构造器。为C写一个构造器,并在C的构造器中执行所有初始化工作。
(3) 使用文件Cartoon.java,将Cartoon类的构器代码变成注释内容标注出去。解释会发生什么事情。 (3) 使用文件Cartoon.java,将Cartoon类的构器代码变成注释内容标注出去。解释会发生什么事情。
(4) 使用文件Chess.java,将Chess类的构建器代码作为注释标注出去。同样解释会发生什么。 (4) 使用文件Chess.java,将Chess类的构造器代码作为注释标注出去。同样解释会发生什么。
\ No newline at end of file \ No newline at end of file
...@@ -65,7 +65,7 @@ public class Detergent extends Cleanser { ...@@ -65,7 +65,7 @@ public class Detergent extends Cleanser {
由于这儿涉及到两个类——基础类及衍生类,而不再是以前的一个,所以在想象衍生类的结果对象时,可能会产生一些迷惑。从外部看,似乎新类拥有与基础类相同的接口,而且可包含一些额外的方法和字段。但继承并非仅仅简单地复制基础类的接口了事。创建衍生类的一个对象时,它在其中包含了基础类的一个“子对象”。这个子对象就象我们根据基础类本身创建了它的一个对象。从外部看,基础类的子对象已封装到衍生类的对象里了。 由于这儿涉及到两个类——基础类及衍生类,而不再是以前的一个,所以在想象衍生类的结果对象时,可能会产生一些迷惑。从外部看,似乎新类拥有与基础类相同的接口,而且可包含一些额外的方法和字段。但继承并非仅仅简单地复制基础类的接口了事。创建衍生类的一个对象时,它在其中包含了基础类的一个“子对象”。这个子对象就象我们根据基础类本身创建了它的一个对象。从外部看,基础类的子对象已封装到衍生类的对象里了。
当然,基础类子对象应该正确地初始化,而且只有一种方法能保证这一点:在构建器中执行初始化,通过调用基础类构建器,后者有足够的能力和权限来执行对基础类的初始化。在衍生类的构建器中,Java会自动插入对基础类构建器的调用。下面这个例子向大家展示了对这种三级继承的应用: 当然,基础类子对象应该正确地初始化,而且只有一种方法能保证这一点:在构造器中执行初始化,通过调用基础类构造器,后者有足够的能力和权限来执行对基础类的初始化。在衍生类的构造器中,Java会自动插入对基础类构造器的调用。下面这个例子向大家展示了对这种三级继承的应用:
``` ```
//: Cartoon.java //: Cartoon.java
...@@ -102,11 +102,11 @@ Cartoon constructor ...@@ -102,11 +102,11 @@ Cartoon constructor
``` ```
可以看出,构建是在基础类的“外部”进行的,所以基础类会在衍生类访问它之前得到正确的初始化。 可以看出,构建是在基础类的“外部”进行的,所以基础类会在衍生类访问它之前得到正确的初始化。
即使没有为Cartoon()创建一个构建器,编译器也会为我们自动合成一个默认构建器,并发出对基础类构建器的调用。 即使没有为Cartoon()创建一个构造器,编译器也会为我们自动合成一个默认构造器,并发出对基础类构造器的调用。
1. 含有参数的构 1. 含有参数的构
上述例子有自己默认的构建器;也就是说,它们不含任何参数。编译器可以很容易地调用它们,因为不存在具体传递什么参数的问题。如果类没有默认的参数,或者想调用含有一个参数的某个基础类构建器,必须明确地编写对基础类的调用代码。这是用super关键字以及适当的参数列表实现的,如下所示: 上述例子有自己默认的构造器;也就是说,它们不含任何参数。编译器可以很容易地调用它们,因为不存在具体传递什么参数的问题。如果类没有默认的参数,或者想调用含有一个参数的某个基础类构造器,必须明确地编写对基础类的调用代码。这是用super关键字以及适当的参数列表实现的,如下所示:
``` ```
//: Chess.java //: Chess.java
...@@ -136,8 +136,8 @@ public class Chess extends BoardGame { ...@@ -136,8 +136,8 @@ public class Chess extends BoardGame {
} ///:~ } ///:~
``` ```
如果不调用BoardGames()内的基础类构建器,编译器就会报告自己找不到Games()形式的一个构建器。除此以外,在衍生类构建器中,对基础类构建器的调用是必须做的第一件事情(如操作失当,编译器会向我们指出)。 如果不调用BoardGames()内的基础类构造器,编译器就会报告自己找不到Games()形式的一个构造器。除此以外,在衍生类构造器中,对基础类构造器的调用是必须做的第一件事情(如操作失当,编译器会向我们指出)。
2. 捕获基本构器的异常 2. 捕获基本构器的异常
正如刚才指出的那样,编译器会强迫我们在衍生类构建器的主体中首先设置对基础类构建器的调用。这意味着在它之前不能出现任何东西。正如大家在第9章会看到的那样,这同时也会防止衍生类构建器捕获来自一个基础类的任何异常事件。显然,这有时会为我们造成不便。 正如刚才指出的那样,编译器会强迫我们在衍生类构造器的主体中首先设置对基础类构造器的调用。这意味着在它之前不能出现任何东西。正如大家在第9章会看到的那样,这同时也会防止衍生类构造器捕获来自一个基础类的任何异常事件。显然,这有时会为我们造成不便。
# 6.3 合成与继承的结合 # 6.3 合成与继承的结合
许多时候都要求将合成与继承两种技术结合起来使用。下面这个例子展示了如何同时采用继承与合成技术,从而创建一个更复杂的类,同时进行必要的构器初始化工作: 许多时候都要求将合成与继承两种技术结合起来使用。下面这个例子展示了如何同时采用继承与合成技术,从而创建一个更复杂的类,同时进行必要的构器初始化工作:
``` ```
//: PlaceSetting.java //: PlaceSetting.java
...@@ -75,7 +75,7 @@ public class PlaceSetting extends Custom { ...@@ -75,7 +75,7 @@ public class PlaceSetting extends Custom {
} ///:~ } ///:~
``` ```
尽管编译器会强迫我们对基础类进行初始化,并要求我们在构器最开头做这一工作,但它并不会监视我们是否正确初始化了成员对象。所以对此必须特别加以留意。 尽管编译器会强迫我们对基础类进行初始化,并要求我们在构器最开头做这一工作,但它并不会监视我们是否正确初始化了成员对象。所以对此必须特别加以留意。
6.3.1 确保正确的清除 6.3.1 确保正确的清除
...@@ -168,7 +168,7 @@ public class CADSystem extends Shape { ...@@ -168,7 +168,7 @@ public class CADSystem extends Shape {
} ///:~ } ///:~
``` ```
这个系统中的所有东西都属于某种Shape(几何形状)。Shape本身是一种Object(对象),因为它是从根类明确继承的。每个类都重新定义了Shape的cleanup()方法,同时还要用super调用那个方法的基础类版本。尽管对象存在期间调用的所有方法都可负责做一些要求清除的工作,但对于特定的Shape类——Circle(圆)、Triangle(三角形)以及Line(直线),它们都拥有自己的构器,能完成“作图”(draw)任务。每个类都有它们自己的cleanup()方法,用于将非内存的东西恢复回对象存在之前的景象。 这个系统中的所有东西都属于某种Shape(几何形状)。Shape本身是一种Object(对象),因为它是从根类明确继承的。每个类都重新定义了Shape的cleanup()方法,同时还要用super调用那个方法的基础类版本。尽管对象存在期间调用的所有方法都可负责做一些要求清除的工作,但对于特定的Shape类——Circle(圆)、Triangle(三角形)以及Line(直线),它们都拥有自己的构器,能完成“作图”(draw)任务。每个类都有它们自己的cleanup()方法,用于将非内存的东西恢复回对象存在之前的景象。
在main()中,可看到两个新关键字:try和finally。我们要到第9章才会向大家正式引荐它们。其中,try关键字指出后面跟随的块(由花括号定界)是一个“警戒区”。也就是说,它会受到特别的待遇。其中一种待遇就是:该警戒区后面跟随的finally从句的代码肯定会得以执行——不管try块到底存不存在(通过异常控制技术,try块可有多种不寻常的应用)。在这里,finally从句的意思是“总是为x调用cleanup(),无论会发生什么事情”。这些关键字将在第9章进行全面、完整的解释。 在main()中,可看到两个新关键字:try和finally。我们要到第9章才会向大家正式引荐它们。其中,try关键字指出后面跟随的块(由花括号定界)是一个“警戒区”。也就是说,它会受到特别的待遇。其中一种待遇就是:该警戒区后面跟随的finally从句的代码肯定会得以执行——不管try块到底存不存在(通过异常控制技术,try块可有多种不寻常的应用)。在这里,finally从句的意思是“总是为x调用cleanup(),无论会发生什么事情”。这些关键字将在第9章进行全面、完整的解释。
......
...@@ -117,7 +117,7 @@ class BlankFinal { ...@@ -117,7 +117,7 @@ class BlankFinal {
} ///:~ } ///:~
``` ```
现在强行要求我们对final进行赋值处理——要么在定义字段时使用一个表达 式,要么在每个构器中。这样就可以确保final字段在使用前获得正确的初始化。 现在强行要求我们对final进行赋值处理——要么在定义字段时使用一个表达 式,要么在每个构器中。这样就可以确保final字段在使用前获得正确的初始化。
3. final参数 3. final参数
......
...@@ -65,4 +65,4 @@ j = 39 ...@@ -65,4 +65,4 @@ j = 39
若基础类含有另一个基础类,则另一个基础类随即也会载入,以此类推。接下来,会在根基础类(此时是Insect)执行static初始化,再在下一个衍生类执行,以此类推。保证这个顺序是非常关键的,因为衍生类的初始化可能要依赖于对基础类成员的正确初始化。 若基础类含有另一个基础类,则另一个基础类随即也会载入,以此类推。接下来,会在根基础类(此时是Insect)执行static初始化,再在下一个衍生类执行,以此类推。保证这个顺序是非常关键的,因为衍生类的初始化可能要依赖于对基础类成员的正确初始化。
此时,必要的类已全部装载完毕,所以能够创建对象。首先,这个对象中的所有基本数据类型都会设成它们的默认值,而将对象引用设为null。随后会调用基础类构建器。在这种情况下,调用是自动进行的。但也完全可以用super来自行指定构建器调用(就象在Beetle()构建器中的第一个操作一样)。基础类的构建采用与衍生类构建器完全相同的处理过程。基础顺构建器完成以后,实例变量会按本来的顺序得以初始化。最后,执行构建器剩余的主体部分。 此时,必要的类已全部装载完毕,所以能够创建对象。首先,这个对象中的所有基本数据类型都会设成它们的默认值,而将对象引用设为null。随后会调用基础类构造器。在这种情况下,调用是自动进行的。但也完全可以用super来自行指定构造器调用(就象在Beetle()构造器中的第一个操作一样)。基础类的构建采用与衍生类构造器完全相同的处理过程。基础顺构造器完成以后,实例变量会按本来的顺序得以初始化。最后,执行构造器剩余的主体部分。
...@@ -267,7 +267,7 @@ public final class Month2 { ...@@ -267,7 +267,7 @@ public final class Month2 {
①:是Rich Hoffarth的一封E-mail触发了我这样编写程序的灵感。 ①:是Rich Hoffarth的一封E-mail触发了我这样编写程序的灵感。
这个类叫作Month2,因为标准Java库里已经有一个Month。它是一个final类,并含有一个private构器,所以没有人能从它继承,或制作它的一个实例。唯一的实例就是那些final static对象,它们是在类本身内部创建的,包括:JAN,FEB,MAR等等。这些对象也在month数组中使用,后者让我们能够按数字挑选月份,而不是按名字(注意数组中提供了一个多余的JAN,使偏移量增加了1,也使December确实成为12月)。在main()中,我们可注意到类型的安全性:m是一个Month2对象,所以只能将其分配给Month2。在前面的Months.java例子中,只提供了int值,所以本来想用来代表一个月份的int变量可能实际获得一个整数值,那样做可能不十分安全。 这个类叫作Month2,因为标准Java库里已经有一个Month。它是一个final类,并含有一个private构器,所以没有人能从它继承,或制作它的一个实例。唯一的实例就是那些final static对象,它们是在类本身内部创建的,包括:JAN,FEB,MAR等等。这些对象也在month数组中使用,后者让我们能够按数字挑选月份,而不是按名字(注意数组中提供了一个多余的JAN,使偏移量增加了1,也使December确实成为12月)。在main()中,我们可注意到类型的安全性:m是一个Month2对象,所以只能将其分配给Month2。在前面的Months.java例子中,只提供了int值,所以本来想用来代表一个月份的int变量可能实际获得一个整数值,那样做可能不十分安全。
这儿介绍的方法也允许我们交换使用==或者equals(),就象main()尾部展示的那样。 这儿介绍的方法也允许我们交换使用==或者equals(),就象main()尾部展示的那样。
7.5.4 初始化接口中的字段 7.5.4 初始化接口中的字段
......
...@@ -156,11 +156,11 @@ class Test { ...@@ -156,11 +156,11 @@ class Test {
(3) 一个匿名类,用于实现一个接口 (3) 一个匿名类,用于实现一个接口
(4) 一个匿名类,用于扩展拥有非默认构器的一个类 (4) 一个匿名类,用于扩展拥有非默认构器的一个类
(5) 一个匿名类,用于执行字段初始化 (5) 一个匿名类,用于执行字段初始化
(6) 一个匿名类,通过实例初始化进行构建(匿名内部类不可拥有构器) (6) 一个匿名类,通过实例初始化进行构建(匿名内部类不可拥有构器)
所有这些都在innerscopes包内发生。首先,来自前述代码的通用接口会在它们自己的文件里获得定义,使它们能在所有的例子里使用: 所有这些都在innerscopes包内发生。首先,来自前述代码的通用接口会在它们自己的文件里获得定义,使它们能在所有的例子里使用:
...@@ -197,7 +197,7 @@ public class Wrapping { ...@@ -197,7 +197,7 @@ public class Wrapping {
} ///:~ } ///:~
``` ```
在上面的代码中,我们注意到Wrapping有一个要求使用参数的构器,这就使情况变得更加有趣了。 在上面的代码中,我们注意到Wrapping有一个要求使用参数的构器,这就使情况变得更加有趣了。
第一个例子展示了如何在一个方法的作用域(而不是另一个类的作用域)中创建一个完整的类: 第一个例子展示了如何在一个方法的作用域(而不是另一个类的作用域)中创建一个完整的类:
``` ```
...@@ -304,7 +304,7 @@ public int value() { return i; } ...@@ -304,7 +304,7 @@ public int value() { return i; }
return new MyContents(); return new MyContents();
``` ```
在匿名内部类中,Contents是用一个默认构建器创建的。下面这段代码展示了基础类需要含有参数的一个构建器时做的事情: 在匿名内部类中,Contents是用一个默认构造器创建的。下面这段代码展示了基础类需要含有参数的一个构造器时做的事情:
``` ```
//: Parcel7.java //: Parcel7.java
...@@ -328,9 +328,9 @@ public class Parcel7 { ...@@ -328,9 +328,9 @@ public class Parcel7 {
} ///:~ } ///:~
``` ```
也就是说,我们将适当的参数简单地传递给基础类构建器,在这儿表现为在“new Wrapping(x)”中传递x。匿名类不能拥有一个构建器,这和在调用super()时的常规做法不同。 也就是说,我们将适当的参数简单地传递给基础类构造器,在这儿表现为在“new Wrapping(x)”中传递x。匿名类不能拥有一个构造器,这和在调用super()时的常规做法不同。
在前述的两个例子中,分号并不标志着类主体的结束(和C++不同)。相反,它标志着用于包含匿名类的那个表达式的结束。因此,它完全等价于在其他任何地方使用分号。 在前述的两个例子中,分号并不标志着类主体的结束(和C++不同)。相反,它标志着用于包含匿名类的那个表达式的结束。因此,它完全等价于在其他任何地方使用分号。
若想对匿名内部类的一个对象进行某种形式的初始化,此时会出现什么情况呢?由于它是匿名的,没有名字赋给构建器,所以我们不能拥有一个构建器。然而,我们可在定义自己的字段时进行初始化: 若想对匿名内部类的一个对象进行某种形式的初始化,此时会出现什么情况呢?由于它是匿名的,没有名字赋给构造器,所以我们不能拥有一个构造器。然而,我们可在定义自己的字段时进行初始化:
``` ```
//: Parcel8.java //: Parcel8.java
...@@ -356,7 +356,7 @@ public class Parcel8 { ...@@ -356,7 +356,7 @@ public class Parcel8 {
``` ```
若试图定义一个匿名内部类,并想使用在匿名内部类外部定义的一个对象,则编译器要求外部对象为final属性。这正是我们将dest()的参数设为final的原因。如果忘记这样做,就会得到一条编译期出错提示。 若试图定义一个匿名内部类,并想使用在匿名内部类外部定义的一个对象,则编译器要求外部对象为final属性。这正是我们将dest()的参数设为final的原因。如果忘记这样做,就会得到一条编译期出错提示。
只要自己只是想分配一个字段,上述方法就肯定可行。但假如需要采取一些类似于构建器的行动,又应怎样操作呢?通过Java 1.1的实例初始化,我们可以有效地为一个匿名内部类创建一个构建器: 只要自己只是想分配一个字段,上述方法就肯定可行。但假如需要采取一些类似于构造器的行动,又应怎样操作呢?通过Java 1.1的实例初始化,我们可以有效地为一个匿名内部类创建一个构造器:
``` ```
//: Parcel9.java //: Parcel9.java
...@@ -386,7 +386,7 @@ public class Parcel9 { ...@@ -386,7 +386,7 @@ public class Parcel9 {
} ///:~ } ///:~
``` ```
在实例初始化模块中,我们可看到代码不能作为类初始化模块(即if语句)的一部分执行。所以实际上,一个实例初始化模块就是一个匿名内部类的构建器。当然,它的功能是有限的;我们不能对实例初始化模块进行重载处理,所以只能拥有这些构建器的其中一个。 在实例初始化模块中,我们可看到代码不能作为类初始化模块(即if语句)的一部分执行。所以实际上,一个实例初始化模块就是一个匿名内部类的构造器。当然,它的功能是有限的;我们不能对实例初始化模块进行重载处理,所以只能拥有这些构造器的其中一个。
7.6.3 链接到外部类 7.6.3 链接到外部类
...@@ -585,7 +585,7 @@ Parcel11.Contents c = p.new Contents(); ...@@ -585,7 +585,7 @@ Parcel11.Contents c = p.new Contents();
7.6.6 从内部类继承 7.6.6 从内部类继承
由于内部类构器必须同封装类对象的一个引用联系到一起,所以从一个内部类继承的时候,情况会稍微变得有些复杂。这儿的问题是封装类的“秘密”引用必须获得初始化,而且在衍生类中不再有一个默认的对象可以连接。解决这个问题的办法是采用一种特殊的语法,明确建立这种关联: 由于内部类构器必须同封装类对象的一个引用联系到一起,所以从一个内部类继承的时候,情况会稍微变得有些复杂。这儿的问题是封装类的“秘密”引用必须获得初始化,而且在衍生类中不再有一个默认的对象可以连接。解决这个问题的办法是采用一种特殊的语法,明确建立这种关联:
``` ```
//: InheritInner.java //: InheritInner.java
...@@ -608,7 +608,7 @@ public class InheritInner ...@@ -608,7 +608,7 @@ public class InheritInner
} ///:~ } ///:~
``` ```
从中可以看到,InheritInner只对内部类进行了扩展,没有扩展外部类。但在需要创建一个构建器的时候,默认对象已经没有意义,我们不能只是传递封装对象的一个引用。此外,必须在构建器中采用下述语法: 从中可以看到,InheritInner只对内部类进行了扩展,没有扩展外部类。但在需要创建一个构造器的时候,默认对象已经没有意义,我们不能只是传递封装对象的一个引用。此外,必须在构造器中采用下述语法:
``` ```
enclosingClassHandle.super(); enclosingClassHandle.super();
...@@ -650,7 +650,7 @@ public class BigEgg extends Egg { ...@@ -650,7 +650,7 @@ public class BigEgg extends Egg {
} ///:~ } ///:~
``` ```
默认构建器是由编译器自动合成的,而且会调用基础类的默认构建器。大家或许会认为由于准备创建一个BigEgg,所以会使用Yolk的“被覆盖”版本。但实际情况并非如此。输出如下: 默认构造器是由编译器自动合成的,而且会调用基础类的默认构造器。大家或许会认为由于准备创建一个BigEgg,所以会使用Yolk的“被覆盖”版本。但实际情况并非如此。输出如下:
``` ```
New Egg() New Egg()
...@@ -707,7 +707,7 @@ BigEgg2.Yolk() ...@@ -707,7 +707,7 @@ BigEgg2.Yolk()
BigEgg2.Yolk.f() BigEgg2.Yolk.f()
``` ```
对Egg2.Yolk()的第二个调用是BigEgg2.Yolk构建器的基础类构建器调用。调用 对Egg2.Yolk()的第二个调用是BigEgg2.Yolk构造器的基础类构造器调用。调用
g()的时候,可发现使用的是f()的被覆盖版本。 g()的时候,可发现使用的是f()的被覆盖版本。
7.6.8 内部类标识符 7.6.8 内部类标识符
...@@ -752,7 +752,7 @@ abstract public class Event { ...@@ -752,7 +752,7 @@ abstract public class Event {
} ///:~ } ///:~
``` ```
希望Event(事件)运行的时候,构器即简单地捕获时间。同时ready()告诉我们何时该运行它。当然,ready()也可以在一个衍生类中被覆盖,将事件建立在除时间以外的其他东西上。 希望Event(事件)运行的时候,构器即简单地捕获时间。同时ready()告诉我们何时该运行它。当然,ready()也可以在一个衍生类中被覆盖,将事件建立在除时间以外的其他东西上。
action()是事件就绪后需要调用的方法,而description()提供了与事件有关的文字信息。 action()是事件就绪后需要调用的方法,而description()提供了与事件有关的文字信息。
......
# 7.7 构器和多态性 # 7.7 构器和多态性
同往常一样,构建器与其他种类的方法是有区别的。在涉及到多态性的问题后,这种方法依然成立。尽管构建器并不具有多态性(即便可以使用一种“虚拟构建器”——将在第11章介绍),但仍然非常有必要理解构建器如何在复杂的分级结构中以及随同多态性使用。这一理解将有助于大家避免陷入一些令人不快的纠纷。 同往常一样,构造器与其他种类的方法是有区别的。在涉及到多态性的问题后,这种方法依然成立。尽管构造器并不具有多态性(即便可以使用一种“虚拟构造器”——将在第11章介绍),但仍然非常有必要理解构造器如何在复杂的分级结构中以及随同多态性使用。这一理解将有助于大家避免陷入一些令人不快的纠纷。
7.7.1 构器的调用顺序 7.7.1 构器的调用顺序
器调用的顺序已在第4章进行了简要说明,但那是在继承和多态性问题引入之前说的话。 器调用的顺序已在第4章进行了简要说明,但那是在继承和多态性问题引入之前说的话。
用于基础类的构建器肯定在一个衍生类的构建器中调用,而且逐渐向上链接,使每个基础类使用的构建器都能得到调用。之所以要这样做,是由于构建器负有一项特殊任务:检查对象是否得到了正确的构建。一个衍生类只能访问它自己的成员,不能访问基础类的成员(这些成员通常都具有private属性)。只有基础类的构建器在初始化自己的元素时才知道正确的方法以及拥有适当的权限。所以,必须令所有构建器都得到调用,否则整个对象的构建就可能不正确。那正是编译器为什么要强迫对衍生类的每个部分进行构建器调用的原因。在衍生类的构建器主体中,若我们没有明确指定对一个基础类构建器的调用,它就会“默默”地调用默认构建器。如果不存在默认构建器,编译器就会报告一个错误(若某个类没有构建器,编译器会自动组织一个默认构建器)。 用于基础类的构造器肯定在一个衍生类的构造器中调用,而且逐渐向上链接,使每个基础类使用的构造器都能得到调用。之所以要这样做,是由于构造器负有一项特殊任务:检查对象是否得到了正确的构建。一个衍生类只能访问它自己的成员,不能访问基础类的成员(这些成员通常都具有private属性)。只有基础类的构造器在初始化自己的元素时才知道正确的方法以及拥有适当的权限。所以,必须令所有构造器都得到调用,否则整个对象的构建就可能不正确。那正是编译器为什么要强迫对衍生类的每个部分进行构造器调用的原因。在衍生类的构造器主体中,若我们没有明确指定对一个基础类构造器的调用,它就会“默默”地调用默认构造器。如果不存在默认构造器,编译器就会报告一个错误(若某个类没有构造器,编译器会自动组织一个默认构造器)。
下面让我们看看一个例子,它展示了按构建顺序进行合成、继承以及多态性的效果: 下面让我们看看一个例子,它展示了按构建顺序进行合成、继承以及多态性的效果:
...@@ -53,7 +53,7 @@ class Sandwich extends PortableLunch { ...@@ -53,7 +53,7 @@ class Sandwich extends PortableLunch {
} ///:~ } ///:~
``` ```
这个例子在其他类的外部创建了一个复杂的类,而且每个类都有一个构器对自己进行了宣布。其中最重要的类是Sandwich,它反映出了三个级别的继承(若将从Object的默认继承算在内,就是四级)以及三个成员对象。在main()里创建了一个Sandwich对象后,输出结果如下: 这个例子在其他类的外部创建了一个复杂的类,而且每个类都有一个构器对自己进行了宣布。其中最重要的类是Sandwich,它反映出了三个级别的继承(若将从Object的默认继承算在内,就是四级)以及三个成员对象。在main()里创建了一个Sandwich对象后,输出结果如下:
``` ```
Meal() Meal()
...@@ -65,15 +65,15 @@ Lettuce() ...@@ -65,15 +65,15 @@ Lettuce()
Sandwich() Sandwich()
``` ```
这意味着对于一个复杂的对象,构器的调用遵照下面的顺序: 这意味着对于一个复杂的对象,构器的调用遵照下面的顺序:
(1) 调用基础类构器。这个步骤会不断重复下去,首先得到构建的是分级结构的根部,然后是下一个衍生类,等等。直到抵达最深一层的衍生类。 (1) 调用基础类构器。这个步骤会不断重复下去,首先得到构建的是分级结构的根部,然后是下一个衍生类,等等。直到抵达最深一层的衍生类。
(2) 按声明顺序调用成员初始化模块。 (2) 按声明顺序调用成员初始化模块。
(3) 调用衍生构器的主体。 (3) 调用衍生构器的主体。
建器调用的顺序是非常重要的。进行继承时,我们知道关于基础类的一切,并且能访问基础类的任何public和protected成员。这意味着当我们在衍生类的时候,必须能假定基础类的所有成员都是有效的。采用一种标准方法,构建行动已经进行,所以对象所有部分的成员均已得到构建。但在构建器内部,必须保证使用的所有成员都已构建。为达到这个要求,唯一的办法就是首先调用基础类构建器。然后在进入衍生类构建器以后,我们在基础类能够访问的所有成员都已得到初始化。此外,所有成员对象(亦即通过合成方法置于类内的对象)在类内进行定义的时候(比如上例中的b,c和l),由于我们应尽可能地对它们进行初始化,所以也应保证构建器内部的所有成员均为有效。若坚持按这一规则行事,会有助于我们确定所有基础类成员以及当前对象的成员对象均已获得正确的初始化。但不幸的是,这种做法并不适用于所有情况,这将在下一节具体说明。 造器调用的顺序是非常重要的。进行继承时,我们知道关于基础类的一切,并且能访问基础类的任何public和protected成员。这意味着当我们在衍生类的时候,必须能假定基础类的所有成员都是有效的。采用一种标准方法,构建行动已经进行,所以对象所有部分的成员均已得到构建。但在构造器内部,必须保证使用的所有成员都已构建。为达到这个要求,唯一的办法就是首先调用基础类构造器。然后在进入衍生类构造器以后,我们在基础类能够访问的所有成员都已得到初始化。此外,所有成员对象(亦即通过合成方法置于类内的对象)在类内进行定义的时候(比如上例中的b,c和l),由于我们应尽可能地对它们进行初始化,所以也应保证构造器内部的所有成员均为有效。若坚持按这一规则行事,会有助于我们确定所有基础类成员以及当前对象的成员对象均已获得正确的初始化。但不幸的是,这种做法并不适用于所有情况,这将在下一节具体说明。
7.7.2 继承和finalize() 7.7.2 继承和finalize()
...@@ -218,12 +218,12 @@ finalizing Characteristic can live in water ...@@ -218,12 +218,12 @@ finalizing Characteristic can live in water
尽管成员对象按照与它们创建时相同的顺序进行收尾,但从技术角度说,并没有指定对象收尾的顺序。但对于基础类,我们可对收尾的顺序进行控制。采用的最佳顺序正是在这里采用的顺序,它与初始化顺序正好相反。按照与C++中用于“破坏器”相同的形式,我们应该首先执行衍生类的收尾,再是基础类的收尾。这是由于衍生类的收尾可能调用基础类中相同的方法,要求基础类组件仍然处于活动状态。因此,必须提前将它们清除(破坏)。 尽管成员对象按照与它们创建时相同的顺序进行收尾,但从技术角度说,并没有指定对象收尾的顺序。但对于基础类,我们可对收尾的顺序进行控制。采用的最佳顺序正是在这里采用的顺序,它与初始化顺序正好相反。按照与C++中用于“破坏器”相同的形式,我们应该首先执行衍生类的收尾,再是基础类的收尾。这是由于衍生类的收尾可能调用基础类中相同的方法,要求基础类组件仍然处于活动状态。因此,必须提前将它们清除(破坏)。
7.7.3 构器内部的多态性方法的行为 7.7.3 构器内部的多态性方法的行为
建器调用的分级结构(顺序)为我们带来了一个有趣的问题,或者说让我们进入了一种进退两难的局面。若当前位于一个构建器的内部,同时调用准备构建的那个对象的一个动态绑定方法,那么会出现什么情况呢?在原始的方法内部,我们完全可以想象会发生什么——动态绑定的调用会在运行期间进行解析,因为对象不知道它到底从属于方法所在的那个类,还是从属于从它衍生出来的某些类。为保持一致性,大家也许会认为这应该在构建器内部发生。 造器调用的分级结构(顺序)为我们带来了一个有趣的问题,或者说让我们进入了一种进退两难的局面。若当前位于一个构造器的内部,同时调用准备构建的那个对象的一个动态绑定方法,那么会出现什么情况呢?在原始的方法内部,我们完全可以想象会发生什么——动态绑定的调用会在运行期间进行解析,因为对象不知道它到底从属于方法所在的那个类,还是从属于从它衍生出来的某些类。为保持一致性,大家也许会认为这应该在构造器内部发生。
但实际情况并非完全如此。若调用构器内部一个动态绑定的方法,会使用那个方法被覆盖的定义。然而,产生的效果可能并不如我们所愿,而且可能造成一些难于发现的程序错误。 但实际情况并非完全如此。若调用构器内部一个动态绑定的方法,会使用那个方法被覆盖的定义。然而,产生的效果可能并不如我们所愿,而且可能造成一些难于发现的程序错误。
从概念上讲,构建器的职责是让对象实际进入存在状态。在任何构建器内部,整个对象可能只是得到部分组织——我们只知道基础类对象已得到初始化,但却不知道哪些类已经继承。然而,一个动态绑定的方法调用却会在分级结构里“向前”或者“向外”前进。它调用位于衍生类里的一个方法。如果在构建器内部做这件事情,那么对于调用的方法,它要操纵的成员可能尚未得到正确的初始化——这显然不是我们所希望的。 从概念上讲,构造器的职责是让对象实际进入存在状态。在任何构造器内部,整个对象可能只是得到部分组织——我们只知道基础类对象已得到初始化,但却不知道哪些类已经继承。然而,一个动态绑定的方法调用却会在分级结构里“向前”或者“向外”前进。它调用位于衍生类里的一个方法。如果在构造器内部做这件事情,那么对于调用的方法,它要操纵的成员可能尚未得到正确的初始化——这显然不是我们所希望的。
通过观察下面这个例子,这个问题便会昭然若揭: 通过观察下面这个例子,这个问题便会昭然若揭:
``` ```
...@@ -261,7 +261,7 @@ public class PolyConstructors { ...@@ -261,7 +261,7 @@ public class PolyConstructors {
} ///:~ } ///:~
``` ```
在Glyph中,draw()方法是“抽象的”(abstract),所以它可以被其他方法覆盖。事实上,我们在RoundGlyph中不得不对其进行覆盖。但Glyph构器会调用这个方法,而且调用会在RoundGlyph.draw()中止,这看起来似乎是有意的。但请看看输出结果: 在Glyph中,draw()方法是“抽象的”(abstract),所以它可以被其他方法覆盖。事实上,我们在RoundGlyph中不得不对其进行覆盖。但Glyph构器会调用这个方法,而且调用会在RoundGlyph.draw()中止,这看起来似乎是有意的。但请看看输出结果:
``` ```
Glyph() before draw() Glyph() before draw()
...@@ -270,19 +270,19 @@ Glyph() after draw() ...@@ -270,19 +270,19 @@ Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5 RoundGlyph.RoundGlyph(), radius = 5
``` ```
当Glyph的构器调用draw()时,radius的值甚至不是默认的初始值1,而是0。这可能是由于一个点号或者屏幕上根本什么都没有画而造成的。这样就不得不开始查找程序中的错误,试着找出程序不能工作的原因。 当Glyph的构器调用draw()时,radius的值甚至不是默认的初始值1,而是0。这可能是由于一个点号或者屏幕上根本什么都没有画而造成的。这样就不得不开始查找程序中的错误,试着找出程序不能工作的原因。
前一节讲述的初始化顺序并不十分完整,而那是解决问题的关键所在。初始化的实际过程是这样的: 前一节讲述的初始化顺序并不十分完整,而那是解决问题的关键所在。初始化的实际过程是这样的:
(1) 在采取其他任何操作之前,为对象分配的存储空间初始化成二进制零。 (1) 在采取其他任何操作之前,为对象分配的存储空间初始化成二进制零。
(2) 就象前面叙述的那样,调用基础类构建器。此时,被覆盖的draw()方法会得到调用(的确是在RoundGlyph构建器调用之前),此时会发现radius的值为0,这是由于步骤(1)造成的。 (2) 就象前面叙述的那样,调用基础类构造器。此时,被覆盖的draw()方法会得到调用(的确是在RoundGlyph构造器调用之前),此时会发现radius的值为0,这是由于步骤(1)造成的。
(3) 按照原先声明的顺序调用成员初始化代码。 (3) 按照原先声明的顺序调用成员初始化代码。
(4) 调用衍生类构器的主体。 (4) 调用衍生类构器的主体。
采取这些操作要求有一个前提,那就是所有东西都至少要初始化成零(或者某些特殊数据类型与“零”等价的值),而不是仅仅留作垃圾。其中包括通过“合成”技术嵌入一个类内部的对象引用。如果假若忘记初始化那个引用,就会在运行期间出现异常事件。其他所有东西都会变成零,这在观看结果时通常是一个严重的警告信号。 采取这些操作要求有一个前提,那就是所有东西都至少要初始化成零(或者某些特殊数据类型与“零”等价的值),而不是仅仅留作垃圾。其中包括通过“合成”技术嵌入一个类内部的对象引用。如果假若忘记初始化那个引用,就会在运行期间出现异常事件。其他所有东西都会变成零,这在观看结果时通常是一个严重的警告信号。
在另一方面,应对这个程序的结果提高警惕。从逻辑的角度说,我们似乎已进行了无懈可击的设计,所以它的错误行为令人非常不可思议。而且没有从编译器那里收到任何报错信息(C++在这种情况下会表现出更合理的行为)。象这样的错误会很轻易地被人忽略,而且要花很长的时间才能找出。 在另一方面,应对这个程序的结果提高警惕。从逻辑的角度说,我们似乎已进行了无懈可击的设计,所以它的错误行为令人非常不可思议。而且没有从编译器那里收到任何报错信息(C++在这种情况下会表现出更合理的行为)。象这样的错误会很轻易地被人忽略,而且要花很长的时间才能找出。
因此,设计构建器时一个特别有效的规则是:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调用任何方法。在构建器内唯一能够安全调用的是在基础类中具有final属性的那些方法(也适用于private方法,它们自动具有final属性)。这些方法不能被覆盖,所以不会出现上述潜在的问题。 因此,设计构造器时一个特别有效的规则是:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调用任何方法。在构造器内唯一能够安全调用的是在基础类中具有final属性的那些方法(也适用于private方法,它们自动具有final属性)。这些方法不能被覆盖,所以不会出现上述潜在的问题。
...@@ -111,7 +111,7 @@ public class Bits { ...@@ -111,7 +111,7 @@ public class Bits {
} ///:~ } ///:~
``` ```
随机数字生成器用于创建一个随机的byte、short和int。每一个都会转换成BitSet内相应的位模型。此时一切都很正常,因为BitSet是64位的,所以它们都不会造成最终尺寸的增大。但在Java 1.0中,一旦BitSet大于64位,就会出现一些令人迷惑不解的行为。假如我们设置一个只比BitSet当前分配存储空间大出1的一个位,它能够正常地扩展。但一旦试图在更高的位置设置位,同时不先接触边界,就会得到一个恼人的异常。这正是由于BitSet在Java 1.0里不能正确扩展造成的。本例创建了一个512位的BitSet。构器分配的存储空间是位数的两倍。所以假如设置位1024或更高的位,同时没有先设置位1023,就会在Java 1.0里得到一个异常。但幸运的是,这个问题已在Java 1.1得到了改正。所以如果是为Java 1.0写代码,请尽量避免使用BitSet。 随机数字生成器用于创建一个随机的byte、short和int。每一个都会转换成BitSet内相应的位模型。此时一切都很正常,因为BitSet是64位的,所以它们都不会造成最终尺寸的增大。但在Java 1.0中,一旦BitSet大于64位,就会出现一些令人迷惑不解的行为。假如我们设置一个只比BitSet当前分配存储空间大出1的一个位,它能够正常地扩展。但一旦试图在更高的位置设置位,同时不先接触边界,就会得到一个恼人的异常。这正是由于BitSet在Java 1.0里不能正确扩展造成的。本例创建了一个512位的BitSet。构器分配的存储空间是位数的两倍。所以假如设置位1024或更高的位,同时没有先设置位1023,就会在Java 1.0里得到一个异常。但幸运的是,这个问题已在Java 1.1得到了改正。所以如果是为Java 1.0写代码,请尽量避免使用BitSet。
8.4.3 Stack 8.4.3 Stack
...@@ -209,7 +209,7 @@ public class AssocArray extends Dictionary { ...@@ -209,7 +209,7 @@ public class AssocArray extends Dictionary {
} ///:~ } ///:~
``` ```
在对AssocArray的定义中,我们注意到的第一个问题是它“扩展”了字典。这意味着AssocArray属于Dictionary的一种类型,所以可对其发出与Dictionary一样的请求。如果想生成自己的Dictionary,而且就在这里进行,那么要做的全部事情只是填充位于Dictionary内的所有方法(而且必须覆盖所有方法,因为它们——除构器外——都是抽象的)。 在对AssocArray的定义中,我们注意到的第一个问题是它“扩展”了字典。这意味着AssocArray属于Dictionary的一种类型,所以可对其发出与Dictionary一样的请求。如果想生成自己的Dictionary,而且就在这里进行,那么要做的全部事情只是填充位于Dictionary内的所有方法(而且必须覆盖所有方法,因为它们——除构器外——都是抽象的)。
Vector key和value通过一个标准索引编号链接起来。也就是说,如果用“roof”的一个键以及“blue”的一个值调用put()——假定我们准备将一个房子的各部分与它们的油漆颜色关联起来,而且AssocArray里已有100个元素,那么“roof”就会有101个键元素,而“blue”有101个值元素。而且要注意一下get(),假如我们作为键传递“roof”,它就会产生与keys.index.Of()的索引编号,然后用那个索引编号生成相关的值矢量内的值。 Vector key和value通过一个标准索引编号链接起来。也就是说,如果用“roof”的一个键以及“blue”的一个值调用put()——假定我们准备将一个房子的各部分与它们的油漆颜色关联起来,而且AssocArray里已有100个元素,那么“roof”就会有101个键元素,而“blue”有101个值元素。而且要注意一下get(),假如我们作为键传递“roof”,它就会产生与keys.index.Of()的索引编号,然后用那个索引编号生成相关的值矢量内的值。
......
...@@ -575,7 +575,7 @@ Implementation based on a red-black tree. When you view the keys or the pairs, t ...@@ -575,7 +575,7 @@ Implementation based on a red-black tree. When you view the keys or the pairs, t
Map(接口) 维持“键-值”对应关系(对),以便通过一个键查找相应的值 Map(接口) 维持“键-值”对应关系(对),以便通过一个键查找相应的值
`HashMap*` 基于一个散列表实现(用它代替Hashtable)。针对“键-值”对的插入和检索,这种形式具有最稳定的性能。可通过构器对这一性能进行调整,以便设置散列表的“能力”和“装载因子” `HashMap*` 基于一个散列表实现(用它代替Hashtable)。针对“键-值”对的插入和检索,这种形式具有最稳定的性能。可通过构器对这一性能进行调整,以便设置散列表的“能力”和“装载因子”
ArrayMap 由一个ArrayList后推得到的Map。对反复的顺序提供了精确的控制。面向非常小的Map设计,特别是那些需要经常创建和删除的。对于非常小的Map,创建和反复所付出的代价要比HashMap低得多。但在Map变大以后,性能也会相应地大幅度降低 ArrayMap 由一个ArrayList后推得到的Map。对反复的顺序提供了精确的控制。面向非常小的Map设计,特别是那些需要经常创建和删除的。对于非常小的Map,创建和反复所付出的代价要比HashMap低得多。但在Map变大以后,性能也会相应地大幅度降低
...@@ -765,7 +765,7 @@ public class ListPerformance { ...@@ -765,7 +765,7 @@ public class ListPerformance {
} ///:~ } ///:~
``` ```
内部类Tester是一个抽象类,用于为特定的测试提供一个基础类。它包含了一个要在测试开始时打印的字串、一个用于计算测试次数或元素数量的size参数、用于初始化字段的一个构器以及一个抽象方法test()。test()做的是最实际的测试工作。各种类型的测试都集中到一个地方:tests数组。我们用继承于Tester的不同匿名内部类来初始化该数组。为添加或删除一个测试项目,只需在数组里简单地添加或移去一个内部类定义即可,其他所有工作都是自动进行的。 内部类Tester是一个抽象类,用于为特定的测试提供一个基础类。它包含了一个要在测试开始时打印的字串、一个用于计算测试次数或元素数量的size参数、用于初始化字段的一个构器以及一个抽象方法test()。test()做的是最实际的测试工作。各种类型的测试都集中到一个地方:tests数组。我们用继承于Tester的不同匿名内部类来初始化该数组。为添加或删除一个测试项目,只需在数组里简单地添加或移去一个内部类定义即可,其他所有工作都是自动进行的。
首先用元素填充传递给test()的List,然后对tests数组中的测试计时。由于测试用机器的不同,结果当然也会有所区别。这个程序的宗旨是揭示出不同集合类型的相对性能比较。下面是某一次运行得到的结果: 首先用元素填充传递给test()的List,然后对tests数组中的测试计时。由于测试用机器的不同,结果当然也会有所区别。这个程序的宗旨是揭示出不同集合类型的相对性能比较。下面是某一次运行得到的结果:
...@@ -1142,7 +1142,7 @@ public class MapCreation { ...@@ -1142,7 +1142,7 @@ public class MapCreation {
``` ```
在写这个程序期间,TreeMap的创建速度比其他两种类型明显快得多(但你应亲自尝试一下,因为据说新版本可能会改善ArrayMap的性能)。考虑到这方面的原因,同时由于前述TreeMap出色的put()性能,所以如果需要创建大量Map,而且只有在以后才需要涉及大量检索操作,那么最佳的策略就是:创建和填充TreeMap;以后检索量增大的时候,再将重要的TreeMap转换成HashMap——使用HashMap(Map)构器。同样地,只有在事实证明确实存在性能瓶颈后,才应关心这些方面的问题——先用起来,再根据需要加快速度。 在写这个程序期间,TreeMap的创建速度比其他两种类型明显快得多(但你应亲自尝试一下,因为据说新版本可能会改善ArrayMap的性能)。考虑到这方面的原因,同时由于前述TreeMap出色的put()性能,所以如果需要创建大量Map,而且只有在以后才需要涉及大量检索操作,那么最佳的策略就是:创建和填充TreeMap;以后检索量增大的时候,再将重要的TreeMap转换成HashMap——使用HashMap(Map)构器。同样地,只有在事实证明确实存在性能瓶颈后,才应关心这些方面的问题——先用起来,再根据需要加快速度。
8.7.6 未支持的操作 8.7.6 未支持的操作
......
# 8.9 练习 # 8.9 练习
(1) 新建一个名为Gerbil的类,在构器中初始化一个int gerbilNumber(类似本章的Mouse例子)。为其写一个名为hop()的方法,用它打印出符合hop()条件的Gerbil的编号。建一个Vector,并为Vector添加一系列Gerbil对象。现在,用elementAt()方法在Vector中遍历,并为每个Gerbil都调用hop()。 (1) 新建一个名为Gerbil的类,在构器中初始化一个int gerbilNumber(类似本章的Mouse例子)。为其写一个名为hop()的方法,用它打印出符合hop()条件的Gerbil的编号。建一个Vector,并为Vector添加一系列Gerbil对象。现在,用elementAt()方法在Vector中遍历,并为每个Gerbil都调用hop()。
(2) 修改练习1,用Enumeration在调用hop()的同时遍历Vector。 (2) 修改练习1,用Enumeration在调用hop()的同时遍历Vector。
......
...@@ -17,7 +17,7 @@ throw new NullPointerException(); ...@@ -17,7 +17,7 @@ throw new NullPointerException();
9.1.1 异常参数 9.1.1 异常参数
和Java的其他任何对象一样,需要用new在内存堆里创建异常,并需调用一个构建器。在所有标准异常中,存在着两个构建器:第一个是默认构建器,第二个则需使用一个字串参数,使我们能在异常里置入相关信息: 和Java的其他任何对象一样,需要用new在内存堆里创建异常,并需调用一个构造器。在所有标准异常中,存在着两个构造器:第一个是默认构造器,第二个则需使用一个字串参数,使我们能在异常里置入相关信息:
``` ```
if(t == null) if(t == null)
...@@ -26,7 +26,7 @@ throw new NullPointerException("t = null"); ...@@ -26,7 +26,7 @@ throw new NullPointerException("t = null");
稍后,字串可用各种方法提取出来,就象稍后会展示的那样。 稍后,字串可用各种方法提取出来,就象稍后会展示的那样。
在这儿,关键字throw会象变戏法一样做出一系列不可思议的事情。它首先执行new表达式,创建一个不在程序常规执行范围之内的对象。而且理所当然,会为那个对象调用构器。随后,对象实际会从方法中返回——尽管对象的类型通常并不是方法设计为返回的类型。为深入理解异常控制,可将其想象成另一种返回机制——但是不要在这个问题上深究,否则会遇到麻烦。通过“抛”出一个异常,亦可从原来的作用域中退出。但是会先返回一个值,再退出方法或作用域。 在这儿,关键字throw会象变戏法一样做出一系列不可思议的事情。它首先执行new表达式,创建一个不在程序常规执行范围之内的对象。而且理所当然,会为那个对象调用构器。随后,对象实际会从方法中返回——尽管对象的类型通常并不是方法设计为返回的类型。为深入理解异常控制,可将其想象成另一种返回机制——但是不要在这个问题上深究,否则会遇到麻烦。通过“抛”出一个异常,亦可从原来的作用域中退出。但是会先返回一个值,再退出方法或作用域。
但是,与普通方法返回的相似性到此便全部结束了,因为我们返回的地方与从普通方法调用中返回的地方是迥然有异的(我们结束于一个恰当的异常控制器,它距离异常“抛”出的地方可能相当遥远——在调用堆栈中要低上许多级)。 但是,与普通方法返回的相似性到此便全部结束了,因为我们返回的地方与从普通方法调用中返回的地方是迥然有异的(我们结束于一个恰当的异常控制器,它距离异常“抛”出的地方可能相当遥远——在调用堆栈中要低上许多级)。
......
# 9.10 练习 # 9.10 练习
(1) 用main()创建一个类,令其抛出try块内的Exception类的一个对象。为Exception的构器赋予一个字串参数。在catch从句内捕获异常,并打印出字串参数。添加一个finally从句,并打印一条消息,证明自己真正到达那里。 (1) 用main()创建一个类,令其抛出try块内的Exception类的一个对象。为Exception的构器赋予一个字串参数。在catch从句内捕获异常,并打印出字串参数。添加一个finally从句,并打印一条消息,证明自己真正到达那里。
(2) 用extends关键字创建自己的异常类。为这个类写一个构器,令其采用String参数,并随同String引用把它保存到对象内。写一个方法,令其打印出保存下来的String。创建一个try-catch从句,练习实际操作新异常。 (2) 用extends关键字创建自己的异常类。为这个类写一个构器,令其采用String参数,并随同String引用把它保存到对象内。写一个方法,令其打印出保存下来的String。创建一个try-catch从句,练习实际操作新异常。
(3) 写一个类,并令一个方法抛出在练习2中创建的类型的一个异常。试着在没有异常规范的前提下编译它,观察编译器会报告什么。接着添加适当的异常规范。在一个try-catch从句中尝试自己的类以及它的异常。 (3) 写一个类,并令一个方法抛出在练习2中创建的类型的一个异常。试着在没有异常规范的前提下编译它,观察编译器会报告什么。接着添加适当的异常规范。在一个try-catch从句中尝试自己的类以及它的异常。
......
...@@ -52,7 +52,7 @@ class MyException extends Exception { ...@@ -52,7 +52,7 @@ class MyException extends Exception {
} }
``` ```
这里的关键是“extends Exception”,它的意思是:除包括一个Exception的全部含义以外,还有更多的含义。增加的代码数量非常少——实际只添加了两个构建器,对MyException的创建方式进行了定义。请记住,假如我们不明确调用一个基础类构建器,编译器会自动调用基础类默认构建器。在第二个构建器中,通过使用super关键字,明确调用了带有一个String参数的基础类构建器。 这里的关键是“extends Exception”,它的意思是:除包括一个Exception的全部含义以外,还有更多的含义。增加的代码数量非常少——实际只添加了两个构造器,对MyException的创建方式进行了定义。请记住,假如我们不明确调用一个基础类构造器,编译器会自动调用基础类默认构造器。在第二个构造器中,通过使用super关键字,明确调用了带有一个String参数的基础类构造器。
该程序输出结果如下: 该程序输出结果如下:
...@@ -69,7 +69,7 @@ MyException: Originated in g() ...@@ -69,7 +69,7 @@ MyException: Originated in g()
可以看到,在从f()“抛”出的MyException异常中,缺乏详细的消息。 可以看到,在从f()“抛”出的MyException异常中,缺乏详细的消息。
创建自己的异常时,还可以采取更多的操作。我们可添加额外的构器及成员: 创建自己的异常时,还可以采取更多的操作。我们可添加额外的构器及成员:
``` ```
//: Inheriting2.java //: Inheriting2.java
...@@ -126,7 +126,7 @@ public class Inheriting2 { ...@@ -126,7 +126,7 @@ public class Inheriting2 {
} ///:~ } ///:~
``` ```
此时添加了一个数据成员i;同时添加了一个特殊的方法,用它读取那个值;也添加了一个额外的构器,用它设置那个值。输出结果如下: 此时添加了一个数据成员i;同时添加了一个特殊的方法,用它读取那个值;也添加了一个额外的构器,用它设置那个值。输出结果如下:
``` ```
Throwing MyException2 from f() Throwing MyException2 from f()
...@@ -152,4 +152,4 @@ class SimpleException extends Exception { ...@@ -152,4 +152,4 @@ class SimpleException extends Exception {
} ///:~ } ///:~
``` ```
它要依赖编译器来创建默认构建器(会自动调用基础类的默认构建器)。当然,在这种情况下,我们不会得到一个SimpleException(String)构建器,但它实际上也不会经常用到。 它要依赖编译器来创建默认构造器(会自动调用基础类的默认构造器)。当然,在这种情况下,我们不会得到一个SimpleException(String)构造器,但它实际上也不会经常用到。
...@@ -78,11 +78,11 @@ public class StormyInning extends Inning ...@@ -78,11 +78,11 @@ public class StormyInning extends Inning
} ///:~ } ///:~
``` ```
在Inning中,可以看到无论构器还是event()方法都指出自己会“抛”出一个异常,但它们实际上没有那样做。这是合法的,因为它允许我们强迫用户捕获可能在覆盖过的event()版本里添加的任何异常。同样的道理也适用于abstract方法,就象在atBat()里展示的那样。 在Inning中,可以看到无论构器还是event()方法都指出自己会“抛”出一个异常,但它们实际上没有那样做。这是合法的,因为它允许我们强迫用户捕获可能在覆盖过的event()版本里添加的任何异常。同样的道理也适用于abstract方法,就象在atBat()里展示的那样。
“interface Storm”非常有趣,因为它包含了在Incoming中定义的一个方法——event(),以及不是在其中定义的一个方法。这两个方法都会“抛”出一个新的异常类型:RainedOut。当执行到“StormyInning extends”和“implements Storm”的时候,可以看到Storm中的event()方法不能改变Inning中的event()的异常接口。同样地,这种设计是十分合理的;否则的话,当我们操作基础类时,便根本无法知道自己捕获的是否正确的东西。当然,假如interface中定义的一个方法不在基础类里,比如rainHard(),它产生异常时就没什么问题。 “interface Storm”非常有趣,因为它包含了在Incoming中定义的一个方法——event(),以及不是在其中定义的一个方法。这两个方法都会“抛”出一个新的异常类型:RainedOut。当执行到“StormyInning extends”和“implements Storm”的时候,可以看到Storm中的event()方法不能改变Inning中的event()的异常接口。同样地,这种设计是十分合理的;否则的话,当我们操作基础类时,便根本无法知道自己捕获的是否正确的东西。当然,假如interface中定义的一个方法不在基础类里,比如rainHard(),它产生异常时就没什么问题。
对异常的限制并不适用于构建器。在StormyInning中,我们可看到一个构建器能够“抛”出它希望的任何东西,无论基础类构建器“抛”出什么。然而,由于必须坚持按某种方式调用基础类构建器(在这里,会自动调用默认构建器),所以衍生类构建器必须在自己的异常规范中声明所有基础类构建器异常。 对异常的限制并不适用于构造器。在StormyInning中,我们可看到一个构造器能够“抛”出它希望的任何东西,无论基础类构造器“抛”出什么。然而,由于必须坚持按某种方式调用基础类构造器(在这里,会自动调用默认构造器),所以衍生类构造器必须在自己的异常规范中声明所有基础类构造器异常。
StormyInning.walk()不会编译的原因是它“抛”出了一个异常,而Inning.walk()却不会“抛”出。若允许这种情况发生,就可让自己的代码调用Inning.walk(),而且它不必控制任何异常。但在以后替换从Inning衍生的一个类的对象时,异常就会“抛”出,造成代码执行的中断。通过强迫衍生类方法遵守基础类方法的异常规范,对象的替换可保持连贯性。 StormyInning.walk()不会编译的原因是它“抛”出了一个异常,而Inning.walk()却不会“抛”出。若允许这种情况发生,就可让自己的代码调用Inning.walk(),而且它不必控制任何异常。但在以后替换从Inning衍生的一个类的对象时,异常就会“抛”出,造成代码执行的中断。通过强迫衍生类方法遵守基础类方法的异常规范,对象的替换可保持连贯性。
......
...@@ -17,7 +17,7 @@ try { ...@@ -17,7 +17,7 @@ try {
} }
``` ```
④:C++异常控制未提供finally从句,因为它依赖构器来达到这种清除效果。 ④:C++异常控制未提供finally从句,因为它依赖构器来达到这种清除效果。
为演示finally从句,请试验下面这个程序: 为演示finally从句,请试验下面这个程序:
...@@ -60,9 +60,9 @@ in finally clause ...@@ -60,9 +60,9 @@ in finally clause
9.6.1 用finally做什么 9.6.1 用finally做什么
在没有“垃圾收集”以及“自动调用破坏器”机制的一种语言中(注释⑤),finally显得特别重要,因为程序员可用它担保内存的正确释放——无论在try块内部发生了什么状况。但Java提供了垃圾收集机制,所以内存的释放几乎绝对不会成为问题。另外,它也没有构器可供调用。既然如此,Java里何时才会用到finally呢? 在没有“垃圾收集”以及“自动调用破坏器”机制的一种语言中(注释⑤),finally显得特别重要,因为程序员可用它担保内存的正确释放——无论在try块内部发生了什么状况。但Java提供了垃圾收集机制,所以内存的释放几乎绝对不会成为问题。另外,它也没有构器可供调用。既然如此,Java里何时才会用到finally呢?
⑤:“破坏器”(Destructor)是“构器”(Constructor)的反义词。它代表一个特殊的函数,一旦某个对象失去用处,通常就会调用它。我们肯定知道在哪里以及何时调用破坏器。C++提供了自动的破坏器调用机制,但Delphi的Object Pascal版本1及2却不具备这一能力(在这种语言中,破坏器的含义与用法都发生了变化)。 ⑤:“破坏器”(Destructor)是“构器”(Constructor)的反义词。它代表一个特殊的函数,一旦某个对象失去用处,通常就会调用它。我们肯定知道在哪里以及何时调用破坏器。C++提供了自动的破坏器调用机制,但Delphi的Object Pascal版本1及2却不具备这一能力(在这种语言中,破坏器的含义与用法都发生了变化)。
除将内存设回原始状态以外,若要设置另一些东西,finally就是必需的。例如,我们有时需要打开一个文件或者建立一个网络连接,或者在屏幕上画一些东西,甚至设置外部世界的一个开关,等等。如下例所示: 除将内存设回原始状态以外,若要设置另一些东西,finally就是必需的。例如,我们有时需要打开一个文件或者建立一个网络连接,或者在屏幕上画一些东西,甚至设置外部世界的一个开关,等等。如下例所示:
......
# 9.7 构 # 9.7 构
为异常编写代码时,我们经常要解决的一个问题是:“一旦产生异常,会正确地进行清除吗?”大多数时候都会非常安全,但在构建器中却是一个大问题。构建器将对象置于一个安全的起始状态,但它可能执行一些操作——如打开一个文件。除非用户完成对象的使用,并调用一个特殊的清除方法,否则那些操作不会得到正确的清除。若从一个构建器内部“抛”出一个异常,这些清除行为也可能不会正确地发生。所有这些都意味着在编写构建器时,我们必须特别加以留意。 为异常编写代码时,我们经常要解决的一个问题是:“一旦产生异常,会正确地进行清除吗?”大多数时候都会非常安全,但在构造器中却是一个大问题。构造器将对象置于一个安全的起始状态,但它可能执行一些操作——如打开一个文件。除非用户完成对象的使用,并调用一个特殊的清除方法,否则那些操作不会得到正确的清除。若从一个构造器内部“抛”出一个异常,这些清除行为也可能不会正确地发生。所有这些都意味着在编写构造器时,我们必须特别加以留意。
由于前面刚学了finally,所以大家可能认为它是一种合适的方案。但事情并没有这么简单,因为finally每次都会执行清除代码——即使我们在清除方法运行之前不想执行清除代码。因此,假如真的用finally进行清除,必须在构器正常结束时设置某种形式的标志。而且只要设置了标志,就不要执行finally块内的任何东西。由于这种做法并不完美(需要将一个地方的代码同另一个地方的结合起来),所以除非特别需要,否则一般不要尝试在finally中进行这种形式的清除。 由于前面刚学了finally,所以大家可能认为它是一种合适的方案。但事情并没有这么简单,因为finally每次都会执行清除代码——即使我们在清除方法运行之前不想执行清除代码。因此,假如真的用finally进行清除,必须在构器正常结束时设置某种形式的标志。而且只要设置了标志,就不要执行finally块内的任何东西。由于这种做法并不完美(需要将一个地方的代码同另一个地方的结合起来),所以除非特别需要,否则一般不要尝试在finally中进行这种形式的清除。
在下面这个例子里,我们创建了一个名为InputFile的类。它的作用是打开一个文件,然后每次读取它的一行内容(转换为一个字串)。它利用了由Java标准IO库提供的FileReader以及BufferedReader类(将于第10章讨论)。这两个类都非常简单,大家现在可以毫无困难地掌握它们的基本用法: 在下面这个例子里,我们创建了一个名为InputFile的类。它的作用是打开一个文件,然后每次读取它的一行内容(转换为一个字串)。它利用了由Java标准IO库提供的FileReader以及BufferedReader类(将于第10章讨论)。这两个类都非常简单,大家现在可以毫无困难地掌握它们的基本用法:
...@@ -81,11 +81,11 @@ public class Cleanup { ...@@ -81,11 +81,11 @@ public class Cleanup {
该例使用了Java 1.1 IO类。 该例使用了Java 1.1 IO类。
用于InputFile的构器采用了一个String(字串)参数,它代表我们想打开的那个文件的名字。在一个try块内部,它用该文件名创建了一个FileReader。对FileReader来说,除非转移并用它创建一个能够实际与之“交谈”的BufferedReader,否则便没什么用处。注意InputFile的一个好处就是它同时合并了这两种行动。 用于InputFile的构器采用了一个String(字串)参数,它代表我们想打开的那个文件的名字。在一个try块内部,它用该文件名创建了一个FileReader。对FileReader来说,除非转移并用它创建一个能够实际与之“交谈”的BufferedReader,否则便没什么用处。注意InputFile的一个好处就是它同时合并了这两种行动。
若FileReader构建器不成功,就会产生一个FileNotFoundException(文件未找到异常)。必须单独捕获这个异常——这属于我们不想关闭文件的一种特殊情况,因为文件尚未成功打开。其他任何捕获从句(catch)都必须关闭文件,因为文件已在进入那些捕获从句时打开(当然,如果多个方法都能产生一个FileNotFoundException异常,就需要稍微用一些技巧。此时,我们可将不同的情况分隔到数个try块内)。close()方法会抛出一个尝试过的异常。即使它在另一个catch从句的代码块内,该异常也会得以捕获——对Java编译器来说,那个catch从句不过是另一对花括号而已。执行完本地操作后,异常会被重新“抛”出。这样做是必要的,因为这个构建器的执行已经失败,我们不希望调用方法来假设对象已正确创建以及有效。 若FileReader构造器不成功,就会产生一个FileNotFoundException(文件未找到异常)。必须单独捕获这个异常——这属于我们不想关闭文件的一种特殊情况,因为文件尚未成功打开。其他任何捕获从句(catch)都必须关闭文件,因为文件已在进入那些捕获从句时打开(当然,如果多个方法都能产生一个FileNotFoundException异常,就需要稍微用一些技巧。此时,我们可将不同的情况分隔到数个try块内)。close()方法会抛出一个尝试过的异常。即使它在另一个catch从句的代码块内,该异常也会得以捕获——对Java编译器来说,那个catch从句不过是另一对花括号而已。执行完本地操作后,异常会被重新“抛”出。这样做是必要的,因为这个构造器的执行已经失败,我们不希望调用方法来假设对象已正确创建以及有效。
在这个例子中,没有采用前述的标志技术,finally从句显然不是关闭文件的正确地方,因为这可能在每次构器结束的时候关闭它。由于我们希望文件在InputFile对象处于活动状态时一直保持打开状态,所以这样做并不恰当。 在这个例子中,没有采用前述的标志技术,finally从句显然不是关闭文件的正确地方,因为这可能在每次构器结束的时候关闭它。由于我们希望文件在InputFile对象处于活动状态时一直保持打开状态,所以这样做并不恰当。
getLine()方法会返回一个字串,其中包含了文件中下一行的内容。它调用了readLine(),后者可能产生一个异常,但那个异常会被捕获,使getLine()不会再产生任何异常。对异常来说,一项特别的设计问题是决定在这一级完全控制一个异常,还是进行部分控制,并传递相同(或不同)的异常,或者只是简单地传递它。在适当的时候,简单地传递可极大简化我们的编码工作。 getLine()方法会返回一个字串,其中包含了文件中下一行的内容。它调用了readLine(),后者可能产生一个异常,但那个异常会被捕获,使getLine()不会再产生任何异常。对异常来说,一项特别的设计问题是决定在这一级完全控制一个异常,还是进行部分控制,并传递相同(或不同)的异常,或者只是简单地传递它。在适当的时候,简单地传递可极大简化我们的编码工作。
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
* [3.3 总结](3.3 总结.md) * [3.3 总结](3.3 总结.md)
* [3.4 练习](3.4 练习.md) * [3.4 练习](3.4 练习.md)
* [第4章 初始化和清除](第4章 初始化和清除.md) * [第4章 初始化和清除](第4章 初始化和清除.md)
* [4.1 用构建器自动初始化](4.1 用构建器自动初始化.md) * [4.1 用构造器自动初始化](4.1 用构造器自动初始化.md)
* [4.2 方法重载](4.2 方法重载.md) * [4.2 方法重载](4.2 方法重载.md)
* [4.3 清除:收尾和垃圾收集](4.3 清除:收尾和垃圾收集.md) * [4.3 清除:收尾和垃圾收集](4.3 清除:收尾和垃圾收集.md)
* [4.4 成员初始化](4.4 成员初始化.md) * [4.4 成员初始化](4.4 成员初始化.md)
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
* [7.4 抽象类和方法](7.4 抽象类和方法.md) * [7.4 抽象类和方法](7.4 抽象类和方法.md)
* [7.5 接口](7.5 接口.md) * [7.5 接口](7.5 接口.md)
* [7.6 内部类](7.6 内部类.md) * [7.6 内部类](7.6 内部类.md)
* [7.7 构建器和多态性](7.7 构建器和多态性.md) * [7.7 构造器和多态性](7.7 构造器和多态性.md)
* [7.8 通过继承进行设计](7.8 通过继承进行设计.md) * [7.8 通过继承进行设计](7.8 通过继承进行设计.md)
* [7.9 总结](7.9 总结.md) * [7.9 总结](7.9 总结.md)
* [7.10 练习](7.10 练习.md) * [7.10 练习](7.10 练习.md)
...@@ -89,7 +89,7 @@ ...@@ -89,7 +89,7 @@
* [9.4 创建自己的异常](9.4 创建自己的异常.md) * [9.4 创建自己的异常](9.4 创建自己的异常.md)
* [9.5 异常的限制](9.5 异常的限制.md) * [9.5 异常的限制](9.5 异常的限制.md)
* [9.6 用finally清除](9.6 用finally清除.md) * [9.6 用finally清除](9.6 用finally清除.md)
* [9.7 构建器](9.7 构建器.md) * [9.7 构造器](9.7 构造器.md)
* [9.8 异常匹配](9.8 异常匹配.md) * [9.8 异常匹配](9.8 异常匹配.md)
* [9.9 总结](9.9 总结.md) * [9.9 总结](9.9 总结.md)
* [9.10 练习](9.10 练习.md) * [9.10 练习](9.10 练习.md)
......
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
**(1) 第1章:对象入门** **(1) 第1章:对象入门**
这一章是对面向对象的程序设计(OOP)的一个综述,其中包括对“什么是对象”之类的基本问题的回答,并讲述了接口与实现、抽象与封装、消息与函数、继承与合成以及非常重要的多态性的概念。这一章会向大家提出一些对象创建的基本问题,比如构器、对象存在于何处、创建好后把它们置于什么地方以及魔术般的垃圾收集器(能够清除不再需要的对象)。要介绍的另一些问题还包括通过异常实现的错误控制机制、反应灵敏的用户界面的多线程处理以及连网和因特网等等。大家也会从中了解到是什么使得Java如此特别,它为什么取得了这么大的成功,以及与面向对象的分析与设计有关的问题。 这一章是对面向对象的程序设计(OOP)的一个综述,其中包括对“什么是对象”之类的基本问题的回答,并讲述了接口与实现、抽象与封装、消息与函数、继承与合成以及非常重要的多态性的概念。这一章会向大家提出一些对象创建的基本问题,比如构器、对象存在于何处、创建好后把它们置于什么地方以及魔术般的垃圾收集器(能够清除不再需要的对象)。要介绍的另一些问题还包括通过异常实现的错误控制机制、反应灵敏的用户界面的多线程处理以及连网和因特网等等。大家也会从中了解到是什么使得Java如此特别,它为什么取得了这么大的成功,以及与面向对象的分析与设计有关的问题。
**(2) 第2章:一切都是对象** **(2) 第2章:一切都是对象**
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
**(4) 第4章:初始化和清除** **(4) 第4章:初始化和清除**
本章开始介绍构建器,它的作用是担保初始化的正确实现。对构建器的定义要涉及函数重载的概念(因为可能同时有几个构建器)。随后要讨论的是清除过程,它并非肯定如想象的那么简单。用完一个对象后,通常可以不必管它,垃圾收集器会自动介入,释放由它占据的内存。这里详细探讨了垃圾收集器以及它的一些特点。在这一章的最后,我们将更贴近地观察初始化过程:自动成员初始化、指定成员初始化、初始化的顺序、static(静态)初始化以及数组初始化等等。 本章开始介绍构造器,它的作用是担保初始化的正确实现。对构造器的定义要涉及函数重载的概念(因为可能同时有几个构造器)。随后要讨论的是清除过程,它并非肯定如想象的那么简单。用完一个对象后,通常可以不必管它,垃圾收集器会自动介入,释放由它占据的内存。这里详细探讨了垃圾收集器以及它的一些特点。在这一章的最后,我们将更贴近地观察初始化过程:自动成员初始化、指定成员初始化、初始化的顺序、static(静态)初始化以及数组初始化等等。
**(5) 第5章:隐藏实现过程** **(5) 第5章:隐藏实现过程**
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
**(9) 第9章:异常差错控制** **(9) 第9章:异常差错控制**
Java最基本的设计宗旨之一便是组织错误的代码不会真的运行起来。编译器会尽可能捕获问题。但某些情况下,除非进入运行期,否则问题是不会被发现的。这些问题要么属于编程错误,要么则是一些自然的出错状况,它们只有在作为程序正常运行的一部分时才会成立。Java为此提供了“异常控制”机制,用于控制程序运行时产生的一切问题。这一章将解释try、catch、throw、throws以及finally等关键字在Java中的工作原理。并讲述什么时候应当“抛”出异常,以及在捕获到异常后该采取什么操作。此外,大家还会学习Java的一些标准异常,如何构建自己的异常,异常发生在构器中怎么办,以及异常控制器如何定位等等。 Java最基本的设计宗旨之一便是组织错误的代码不会真的运行起来。编译器会尽可能捕获问题。但某些情况下,除非进入运行期,否则问题是不会被发现的。这些问题要么属于编程错误,要么则是一些自然的出错状况,它们只有在作为程序正常运行的一部分时才会成立。Java为此提供了“异常控制”机制,用于控制程序运行时产生的一切问题。这一章将解释try、catch、throw、throws以及finally等关键字在Java中的工作原理。并讲述什么时候应当“抛”出异常,以及在捕获到异常后该采取什么操作。此外,大家还会学习Java的一些标准异常,如何构建自己的异常,异常发生在构器中怎么办,以及异常控制器如何定位等等。
**(10) 第10章:Java IO系统** **(10) 第10章:Java IO系统**
......
...@@ -5,4 +5,4 @@ ...@@ -5,4 +5,4 @@
“初始化”和“清除”是这些安全问题的其中两个。许多C程序的错误都是由于程序员忘记初始化一个变量造成的。对于现成的库,若用户不知道如何初始化库的一个组件,就往往会出现这一类的错误。清除是另一个特殊的问题,因为用完一个元素后,由于不再关心,所以很容易把它忘记。这样一来,那个元素占用的资源会一直保留下去,极易产生资源(主要是内存)用尽的后果。 “初始化”和“清除”是这些安全问题的其中两个。许多C程序的错误都是由于程序员忘记初始化一个变量造成的。对于现成的库,若用户不知道如何初始化库的一个组件,就往往会出现这一类的错误。清除是另一个特殊的问题,因为用完一个元素后,由于不再关心,所以很容易把它忘记。这样一来,那个元素占用的资源会一直保留下去,极易产生资源(主要是内存)用尽的后果。
C++为我们引入了“构器”的概念。这是一种特殊的方法,在一个对象创建之后自动调用。Java也沿用了这个概念,但新增了自己的“垃圾收集器”,能在资源不再需要的时候自动释放它们。本章将讨论初始化和清除的问题,以及Java如何提供它们的支持。 C++为我们引入了“构器”的概念。这是一种特殊的方法,在一个对象创建之后自动调用。Java也沿用了这个概念,但新增了自己的“垃圾收集器”,能在资源不再需要的时候自动释放它们。本章将讨论初始化和清除的问题,以及Java如何提供它们的支持。
...@@ -51,14 +51,14 @@ import java.awt.*; ...@@ -51,14 +51,14 @@ import java.awt.*;
(15) Java用包代替了命名空间。由于将所有东西都置入一个类,而且由于采用了一种名为“封装”的机制,它能针对类名进行类似于命名空间分解的操作,所以命名的问题不再进入我们的考虑之列。数据包也会在单独一个库名下收集库的组件。我们只需简单地“import”(导入)一个包,剩下的工作会由编译器自动完成。 (15) Java用包代替了命名空间。由于将所有东西都置入一个类,而且由于采用了一种名为“封装”的机制,它能针对类名进行类似于命名空间分解的操作,所以命名的问题不再进入我们的考虑之列。数据包也会在单独一个库名下收集库的组件。我们只需简单地“import”(导入)一个包,剩下的工作会由编译器自动完成。
(16) 被定义成类成员的对象引用会自动初始化成null。对基本类数据成员的初始化在Java里得到了可靠的保障。若不明确地进行初始化,它们就会得到一个默认值(零或等价的值)。可对它们进行明确的初始化(显式初始化):要么在类内定义它们,要么在构器中定义。采用的语法比C++的语法更容易理解,而且对于static和非static成员来说都是固定不变的。我们不必从外部定义static成员的存储方式,这和C++是不同的。 (16) 被定义成类成员的对象引用会自动初始化成null。对基本类数据成员的初始化在Java里得到了可靠的保障。若不明确地进行初始化,它们就会得到一个默认值(零或等价的值)。可对它们进行明确的初始化(显式初始化):要么在类内定义它们,要么在构器中定义。采用的语法比C++的语法更容易理解,而且对于static和非static成员来说都是固定不变的。我们不必从外部定义static成员的存储方式,这和C++是不同的。
(17) 在Java里,没有象C和C++那样的指针。用new创建一个对象的时候,会获得一个引用(本书一直将其称作“引用”)。例如: (17) 在Java里,没有象C和C++那样的指针。用new创建一个对象的时候,会获得一个引用(本书一直将其称作“引用”)。例如:
String s = new String("howdy"); String s = new String("howdy");
然而,C++引用在创建时必须进行初始化,而且不可重定义到一个不同的位置。但Java引用并不一定局限于创建时的位置。它们可根据情况任意定义,这便消除了对指针的部分需求。在C和C++里大量采用指针的另一个原因是为了能指向任意一个内存位置(这同时会使它们变得不安全,也是Java不提供这一支持的原因)。指针通常被看作在基本变量数组中四处移动的一种有效手段。Java允许我们以更安全的形式达到相同的目标。解决指针问题的终极方法是“固有方法”(已在附录A讨论)。将指针传递给方法时,通常不会带来太大的问题,因为此时没有全局函数,只有类。而且我们可传递对对象的引用。Java语言最开始声称自己“完全不采用指针!”但随着许多程序员都质问没有指针如何工作?于是后来又声明“采用受到限制的指针”。大家可自行判断它是否“真”的是一个指针。但不管在何种情况下,都不存在指针“算术”。 然而,C++引用在创建时必须进行初始化,而且不可重定义到一个不同的位置。但Java引用并不一定局限于创建时的位置。它们可根据情况任意定义,这便消除了对指针的部分需求。在C和C++里大量采用指针的另一个原因是为了能指向任意一个内存位置(这同时会使它们变得不安全,也是Java不提供这一支持的原因)。指针通常被看作在基本变量数组中四处移动的一种有效手段。Java允许我们以更安全的形式达到相同的目标。解决指针问题的终极方法是“固有方法”(已在附录A讨论)。将指针传递给方法时,通常不会带来太大的问题,因为此时没有全局函数,只有类。而且我们可传递对对象的引用。Java语言最开始声称自己“完全不采用指针!”但随着许多程序员都质问没有指针如何工作?于是后来又声明“采用受到限制的指针”。大家可自行判断它是否“真”的是一个指针。但不管在何种情况下,都不存在指针“算术”。
(18) Java提供了与C++类似的“构建器”(Constructor)。如果不自己定义一个,就会获得一个默认构建器。而如果定义了一个非默认的构建器,就不会为我们自动定义默认构建器。这和C++是一样的。注意没有复制构建器,因为所有参数都是按引用传递的。 (18) Java提供了与C++类似的“构造器”(Constructor)。如果不自己定义一个,就会获得一个默认构造器。而如果定义了一个非默认的构造器,就不会为我们自动定义默认构造器。这和C++是一样的。注意没有复制构造器,因为所有参数都是按引用传递的。
(19) Java中没有“破坏器”(Destructor)。变量不存在“作用域”的问题。一个对象的“存在时间”是由对象的存在时间决定的,并非由垃圾收集器决定。有个finalize()方法是每一个类的成员,它在某种程度上类似于C++的“破坏器”。但finalize()是由垃圾收集器调用的,而且只负责释放“资源”(如打开的文件、套接字、端口、URL等等)。如需在一个特定的地点做某样事情,必须创建一个特殊的方法,并调用它,不能依赖finalize()。而在另一方面,C++中的所有对象都会(或者说“应该”)破坏,但并非Java中的所有对象都会被当作“垃圾”收集掉。由于Java不支持破坏器的概念,所以在必要的时候,必须谨慎地创建一个清除方法。而且针对类内的基础类以及成员对象,需要明确调用所有清除方法。 (19) Java中没有“破坏器”(Destructor)。变量不存在“作用域”的问题。一个对象的“存在时间”是由对象的存在时间决定的,并非由垃圾收集器决定。有个finalize()方法是每一个类的成员,它在某种程度上类似于C++的“破坏器”。但finalize()是由垃圾收集器调用的,而且只负责释放“资源”(如打开的文件、套接字、端口、URL等等)。如需在一个特定的地点做某样事情,必须创建一个特殊的方法,并调用它,不能依赖finalize()。而在另一方面,C++中的所有对象都会(或者说“应该”)破坏,但并非Java中的所有对象都会被当作“垃圾”收集掉。由于Java不支持破坏器的概念,所以在必要的时候,必须谨慎地创建一个清除方法。而且针对类内的基础类以及成员对象,需要明确调用所有清除方法。
...@@ -84,7 +84,7 @@ String s = new String("howdy"); ...@@ -84,7 +84,7 @@ String s = new String("howdy");
(30) Java不存在“嵌入”(inline)方法。Java编译器也许会自行决定嵌入一个方法,但我们对此没有更多的控制权力。在Java中,可为一个方法使用final关键字,从而“建议”进行嵌入操作。然而,嵌入函数对于C++的编译器来说也只是一种建议。 (30) Java不存在“嵌入”(inline)方法。Java编译器也许会自行决定嵌入一个方法,但我们对此没有更多的控制权力。在Java中,可为一个方法使用final关键字,从而“建议”进行嵌入操作。然而,嵌入函数对于C++的编译器来说也只是一种建议。
(31) Java中的继承具有与C++相同的效果,但采用的语法不同。Java用extends关键字标志从一个基础类的继承,并用super关键字指出准备在基础类中调用的方法,它与我们当前所在的方法具有相同的名字(然而,Java中的super关键字只允许我们访问父类的方法——亦即分级结构的上一级)。通过在C++中设定基础类的作用域,我们可访问位于分级结构较深处的方法。亦可用super关键字调用基础类构建器。正如早先指出的那样,所有类最终都会从Object里自动继承。和C++不同,不存在明确的构建器初始化列表。但编译器会强迫我们在构建器主体的开头进行全部的基础类初始化,而且不允许我们在主体的后面部分进行这一工作。通过组合运用自动初始化以及来自未初始化对象引用的异常,成员的初始化可得到有效的保证。 (31) Java中的继承具有与C++相同的效果,但采用的语法不同。Java用extends关键字标志从一个基础类的继承,并用super关键字指出准备在基础类中调用的方法,它与我们当前所在的方法具有相同的名字(然而,Java中的super关键字只允许我们访问父类的方法——亦即分级结构的上一级)。通过在C++中设定基础类的作用域,我们可访问位于分级结构较深处的方法。亦可用super关键字调用基础类构造器。正如早先指出的那样,所有类最终都会从Object里自动继承。和C++不同,不存在明确的构造器初始化列表。但编译器会强迫我们在构造器主体的开头进行全部的基础类初始化,而且不允许我们在主体的后面部分进行这一工作。通过组合运用自动初始化以及来自未初始化对象引用的异常,成员的初始化可得到有效的保证。
``` ```
public class Foo extends Bar { public class Foo extends Bar {
...@@ -133,7 +133,7 @@ derived d = (derived)base; ...@@ -133,7 +133,7 @@ derived d = (derived)base;
这与旧式风格的C转换是一样的。编译器会自动调用动态转换机制,不要求使用额外的语法。尽管它并不象C++的“new casts”那样具有易于定位转换的优点,但Java会检查使用情况,并丢弃那些“异常”,所以它不会象C++那样允许坏转换的存在。 这与旧式风格的C转换是一样的。编译器会自动调用动态转换机制,不要求使用额外的语法。尽管它并不象C++的“new casts”那样具有易于定位转换的优点,但Java会检查使用情况,并丢弃那些“异常”,所以它不会象C++那样允许坏转换的存在。
(37) Java采取了不同的异常控制机制,因为此时已经不存在构器。可添加一个finally从句,强制执行特定的语句,以便进行必要的清除工作。Java中的所有异常都是从基础类Throwable里继承而来的,所以可确保我们得到的是一个通用接口。 (37) Java采取了不同的异常控制机制,因为此时已经不存在构器。可添加一个finally从句,强制执行特定的语句,以便进行必要的清除工作。Java中的所有异常都是从基础类Throwable里继承而来的,所以可确保我们得到的是一个通用接口。
``` ```
public void f(Obj b) throws IOException { public void f(Obj b) throws IOException {
...@@ -154,7 +154,7 @@ public void f(Obj b) throws IOException { ...@@ -154,7 +154,7 @@ public void f(Obj b) throws IOException {
(39) Java具有方法重载的能力,但不允许运算符重载。String类不能用+和+=运算符连接不同的字串,而且String表达式使用自动的类型转换,但那是一种特殊的内建情况。 (39) Java具有方法重载的能力,但不允许运算符重载。String类不能用+和+=运算符连接不同的字串,而且String表达式使用自动的类型转换,但那是一种特殊的内建情况。
(40) 通过事先的约定,C++中经常出现的const问题在Java里已得到了控制。我们只能传递指向对象的引用,本地副本永远不会为我们自动生成。若希望使用类似C++按值传递那样的技术,可调用clone(),生成参数的一个本地副本(尽管clone()的设计依然尚显粗糙——参见第12章)。根本不存在被自动调用的副本构器。为创建一个编译期的常数值,可象下面这样编码: (40) 通过事先的约定,C++中经常出现的const问题在Java里已得到了控制。我们只能传递指向对象的引用,本地副本永远不会为我们自动生成。若希望使用类似C++按值传递那样的技术,可调用clone(),生成参数的一个本地副本(尽管clone()的设计依然尚显粗糙——参见第12章)。根本不存在被自动调用的副本构器。为创建一个编译期的常数值,可象下面这样编码:
``` ```
static final int SIZE = 255 static final int SIZE = 255
......
...@@ -50,7 +50,7 @@ implement Serializable ...@@ -50,7 +50,7 @@ implement Serializable
(12) 避免使用“魔术数字”,这些数字很难与代码很好地配合。如以后需要修改它,无疑会成为一场噩梦,因为根本不知道“100”到底是指“数组大小”还是“其他全然不同的东西”。所以,我们应创建一个常数,并为其使用具有说服力的描述性名称,并在整个程序中都采用常数标识符。这样可使程序更易理解以及更易维护。 (12) 避免使用“魔术数字”,这些数字很难与代码很好地配合。如以后需要修改它,无疑会成为一场噩梦,因为根本不知道“100”到底是指“数组大小”还是“其他全然不同的东西”。所以,我们应创建一个常数,并为其使用具有说服力的描述性名称,并在整个程序中都采用常数标识符。这样可使程序更易理解以及更易维护。
(13) 涉及构建器和异常的时候,通常希望重新丢弃在构建器中捕获的任何异常——如果它造成了那个对象的创建失败。这样一来,调用者就不会以为那个对象已正确地创建,从而盲目地继续。 (13) 涉及构造器和异常的时候,通常希望重新丢弃在构造器中捕获的任何异常——如果它造成了那个对象的创建失败。这样一来,调用者就不会以为那个对象已正确地创建,从而盲目地继续。
(14) 当客户程序员用完对象以后,若你的类要求进行任何清除工作,可考虑将清除代码置于一个良好定义的方法里,采用类似于cleanup()这样的名字,明确表明自己的用途。除此以外,可在类内放置一个boolean(布尔)标记,指出对象是否已被清除。在类的finalize()方法里,请确定对象已被清除,并已丢弃了从RuntimeException继承的一个类(如果还没有的话),从而指出一个编程错误。在采取象这样的方案之前,请确定finalize()能够在自己的系统中工作(可能需要调用System.runFinalizersOnExit(true),从而确保这一行为)。 (14) 当客户程序员用完对象以后,若你的类要求进行任何清除工作,可考虑将清除代码置于一个良好定义的方法里,采用类似于cleanup()这样的名字,明确表明自己的用途。除此以外,可在类内放置一个boolean(布尔)标记,指出对象是否已被清除。在类的finalize()方法里,请确定对象已被清除,并已丢弃了从RuntimeException继承的一个类(如果还没有的话),从而指出一个编程错误。在采取象这样的方案之前,请确定finalize()能够在自己的系统中工作(可能需要调用System.runFinalizersOnExit(true),从而确保这一行为)。
...@@ -62,7 +62,7 @@ implement Serializable ...@@ -62,7 +62,7 @@ implement Serializable
(18) 尽量使用interfaces,不要使用abstract类。若已知某样东西准备成为一个基础类,那么第一个选择应是将其变成一个interface(接口)。只有在不得不使用方法定义或者成员变量的时候,才需要将其变成一个abstract(抽象)类。接口主要描述了客户希望做什么事情,而一个类则致力于(或允许)具体的实施细节。 (18) 尽量使用interfaces,不要使用abstract类。若已知某样东西准备成为一个基础类,那么第一个选择应是将其变成一个interface(接口)。只有在不得不使用方法定义或者成员变量的时候,才需要将其变成一个abstract(抽象)类。接口主要描述了客户希望做什么事情,而一个类则致力于(或允许)具体的实施细节。
(19) 在构器内部,只进行那些将对象设为正确状态所需的工作。尽可能地避免调用其他方法,因为那些方法可能被其他人覆盖或取消,从而在构建过程中产生不可预知的结果(参见第7章的详细说明)。 (19) 在构器内部,只进行那些将对象设为正确状态所需的工作。尽可能地避免调用其他方法,因为那些方法可能被其他人覆盖或取消,从而在构建过程中产生不可预知的结果(参见第7章的详细说明)。
(20) 对象不应只是简单地容纳一些数据;它们的行为也应得到良好的定义。 (20) 对象不应只是简单地容纳一些数据;它们的行为也应得到良好的定义。
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
我之所以想到速度,部分原因是由于C++模型。C++将自己的主要精力放在编译期间“静态”发生的所有事情上,所以程序的运行期版本非常短小和快速。C++也直接建立在C模型的基础上(主要为了向后兼容),但有时仅仅由于它在C中能按特定的方式工作,所以也是C++中最方便的一种方法。最重要的一种情况是C和C++对内存的管理方式,它是某些人觉得Java速度肯定慢的重要依据:在Java中,所有对象都必须在内存“堆”里创建。 我之所以想到速度,部分原因是由于C++模型。C++将自己的主要精力放在编译期间“静态”发生的所有事情上,所以程序的运行期版本非常短小和快速。C++也直接建立在C模型的基础上(主要为了向后兼容),但有时仅仅由于它在C中能按特定的方式工作,所以也是C++中最方便的一种方法。最重要的一种情况是C和C++对内存的管理方式,它是某些人觉得Java速度肯定慢的重要依据:在Java中,所有对象都必须在内存“堆”里创建。
而在C++中,对象是在堆栈中创建的。这样可达到更快的速度,因为当我们进入一个特定的作用域时,堆栈指针会向下移动一个单位,为那个作用域内创建的、以堆栈为基础的所有对象分配存储空间。而当我们离开作用域的时候(调用完毕所有局部构器后),堆栈指针会向上移动一个单位。然而,在C++里创建“内存堆”(Heap)对象通常会慢得多,因为它建立在C的内存堆基础上。这种内存堆实际是一个大的内存池,要求必须进行再循环(再生)。在C++里调用delete以后,释放的内存会在堆里留下一个洞,所以再调用new的时候,存储分配机制必须进行某种形式的搜索,使对象的存储与堆内任何现成的洞相配,否则就会很快用光堆的存储空间。之所以内存堆的分配会在C++里对性能造成如此重大的性能影响,对可用内存的搜索正是一个重要的原因。所以创建基于堆栈的对象要快得多。 而在C++中,对象是在堆栈中创建的。这样可达到更快的速度,因为当我们进入一个特定的作用域时,堆栈指针会向下移动一个单位,为那个作用域内创建的、以堆栈为基础的所有对象分配存储空间。而当我们离开作用域的时候(调用完毕所有局部构器后),堆栈指针会向上移动一个单位。然而,在C++里创建“内存堆”(Heap)对象通常会慢得多,因为它建立在C的内存堆基础上。这种内存堆实际是一个大的内存池,要求必须进行再循环(再生)。在C++里调用delete以后,释放的内存会在堆里留下一个洞,所以再调用new的时候,存储分配机制必须进行某种形式的搜索,使对象的存储与堆内任何现成的洞相配,否则就会很快用光堆的存储空间。之所以内存堆的分配会在C++里对性能造成如此重大的性能影响,对可用内存的搜索正是一个重要的原因。所以创建基于堆栈的对象要快得多。
同样地,由于C++如此多的工作都在编译期间进行,所以必须考虑这方面的因素。但在Java的某些地方,事情的发生却要显得“动态”得多,它会改变模型。创建对象的时候,垃圾收集器的使用对于提高对象创建的速度产生了显著的影响。从表面上看,这种说法似乎有些奇怪——存储空间的释放会对存储空间的分配造成影响,但它正是JVM采取的重要手段之一,这意味着在Java中为堆对象分配存储空间几乎能达到与C++中在堆栈里创建存储空间一样快的速度。 同样地,由于C++如此多的工作都在编译期间进行,所以必须考虑这方面的因素。但在Java的某些地方,事情的发生却要显得“动态”得多,它会改变模型。创建对象的时候,垃圾收集器的使用对于提高对象创建的速度产生了显著的影响。从表面上看,这种说法似乎有些奇怪——存储空间的释放会对存储空间的分配造成影响,但它正是JVM采取的重要手段之一,这意味着在Java中为堆对象分配存储空间几乎能达到与C++中在堆栈里创建存储空间一样快的速度。
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册