Chapter1 介绍I/O

输入和输出,简称I/O,是任何计算机操作系统或编程语言的基础。只有理论家才觉得编写不需要输入或产生输出的程序很有意思。
与此同时,I/O几乎没有资格成为计算机科学中更“惊心动魄”的主题之一。它是偏向底层的技术,你每天都在使用它——但对于大多数开发者来说,它不是一个很具有吸引力的话题。

但实际上,Java程序员应该发现I/O很有趣。Java在核心API中包含了一组特别丰富的I/O类,主要是在java.io和java.nio包中。这些包支持几种不同类型的I/O。一个区别是面向字节的I/O(由输入和输出流处理)和字符I/O(由读写器处理)。另一个区别是旧式的基于流的I/O和新式的基于通道和缓冲的I/O。这些都有它们的位置,适用于不同的需求和用例。它们中的任何一个都不应该忽视。

JavaI/O库以抽象的方式设计,使你能够从外部数据源读取并写入外部目标,无论你正在写入或读取的内容是什么类型。你使用相同的方法从控制台或网络连接中读取文件。你使用相同的方法通过编写字节数组或串行端口设备写入文件。

读取和写入而不关心数据的来源或去处是一个非常强大的抽象。在其他方面,这使你能够定义自动压缩,加密和过滤从一种数据格式到另一种数据格式的I/O流。一旦你有这些工具,程序可以发送加密数据或编写zip文件,几乎不知道他们正在做什么。密码学或压缩可以在几行代码中隔离,这些代码说:“哦,是的,将其作为压缩的加密输出流。”

在这本书中,我将深入研究Java的I/O设备的所有部分。这包括你可以使用的所有不同类型的流,以及在服务器上提供高性能、高吞吐量、无阻塞操作的通道和缓冲区。我们还将研究Java对Unicode的支持。我们将研究Java的I/O格式的强大功能。最后,我们将研究各种设备,包括串行端口、并行端口、USB、蓝牙和其他硬件,这些设备不一定看起来像传统的台式计算机或服务器。

我不会这么说,“如果你总是觉得I/O无聊,这是给你的书!”我要说的是,如果你确实觉得I/O没意思,你可能就不太了解它了。I/O是软件与外部世界进行通信的手段。Java提供了强大而灵活的工具来完成这项工作的关键部分。说了完了这句话,让我们从基础开始吧。

流是什么
流是一个不确定长度的有序字节序列。输入流将数据字节从一些外部源移动到Java程序中。输出流将数据字节从Java程序中移动到某些通常外部的目标中。(在特殊情况下,流还可以将字节从Java程序的一部分移动到另一部分。)

stream这个词来源于序列和水流之间的类比。输入流就像一个吸水的虹吸管;输出流就像喷出水的软管。 虹吸管可以连接到软管上,将水从一个地方移到另一个地方。 如果它从有限的水源(如水桶)中抽取,有时虹吸管可能会耗尽水。 另一方面,如果虹吸管从河流中抽水,它可能会无限期地运行。 因此,输入流也可以从有限的字节源读取(例如文件)或无限的字节源读取(例如System.in)。 类似地,输出流可以要输出具有确定数量的字节或不确定数量的字节。

对Java程序的输入可以来自不同的源。输出可以到许多不同类型的目的地。流隐喻的力量在于,这些源和目的地之间的差异是抽象的方式。所有的输入和输出操作都被简单地视为使用相同类和相同方法的流。你不需要为每种不同的设备学习新的API。读取文件的相同API可以读取网络套接字、串行端口、蓝牙传输等。

流的来源
大多数程序员遇到的第一个输入源是System.in。这与C中的STDIN一样,通常是某种控制台窗口,可能是Java程序启动的那个窗口。如果重定向输入以便程序从文件读取,则System.in也会更改。例如,在UNIX上,以下命令重定向stdin,以便当MessageServer程序读取system.in时,实际数据来自文件data.txt而不是控制台:

% java MessageServer < data.txt

控制台也可以通过java.lang.system类中的static字段out(即system.out)进行输出。这相当于C语言中的stdout,可以用类似的方式重定向。最后,stderr作为system.err提供。这最常用于从catch子句中调试和打印错误消息。例如:

try{// ... do something that might throw an exception
}catch(Exception ex){System.err.println(ex);
}

System.out和System.err都是打印流——即java.io.PrintStram的实例。这些将在第7章中详细讨论。

文件是另一个常见的输入源和输出目标。文件输入流提供的数据流以文件中的第一个字节开始,以文件中的最后一个字节结束。文件输出流将数据写入文件,方法是擦除文件的内容并从开始处开始,或者将数据追加到文件中。这些将在第4章中介绍。

网络连接也提供流。当你连接到Web服务器、FTP服务器或其他类型的服务器时,你将从从该服务器连接的输入流读取它发送的数据,并将数据写入连接到该服务器的输出流。这些数据流将在第5章中介绍。

Java程序本身会产生流。 字节数组输入流,字节数组输出流,管道输入流和管道输出流都从Java程序的一部分移动到另一部分。 其中大部分都在第9章中介绍。

也许有点令人惊讶的是,像TextArea和JTextArea这样的GUI组件不会产生流。这里的问题是订阅。 作为流的数据提供的一组字节必须具有固定顺序。 但是,用户可以在任何点(不仅仅是在末尾)更改文本区域或文本字段的内容。 此外,当其他的线程正在读取数据时,它们可以从流中间删除文本。 因此,流不是从GUI组件读取数据的好隐喻。 但是,你可以使用它们生成的字符串来创建字节数组输入流或字符串阅读器。

Stream 类
大多数直接使用流的类都是java.io包的一部分。 两个主要类是java.io.InputStream和java.io.OutputStream。 这些是具有更多专业能力的许多不同子类的抽象基类。

子类包括:

BufferedInputStream BufferedOutputStream
ByteArrayInputStream ByteArrayOutputStream
DataInputStream DataOutputStream
FileInputStream FileOutputStream
FilterInputStream FilterInputStream
ObjectInputStream ObjectOutputStream
PipedInputStream PipedOutputStream
PrintStream PushbackInputStream
SequenceInputStream

java.util.zip包中包含四个输入流类,它们以压缩格式读取数据并以非压缩格式返回数据,输出流类以非压缩格式读取数据并以压缩格式写入。 这些将在第10章中讨论。

CheckInputStream CheckOutputStream
DeflaterOutputStream GZIPInputStream
GZIPOutputStream InflaterInputStream
ZipInputStream ZipOutputStream

java.util.jar包中包含两个用于从JAR归档文件中读取文件的流类。这些将在第11章中讨论。

JarInputStream
JarOutputStream

java.security包中包含一些用于计算消息摘要的流类:

DigestInputStream
DigestOutputStream

Java加密扩展(JCE)添加了两个用于加密和解密的类:

CipherInputStream
CipherOutputStream

这四个流将在第12章中讨论。

最后,一些随机流类隐藏在sun包中——例如,sun.net.TelnetInputStream和sun.net.TelnetOutputStream。 但是,这些是故意隐藏的,通常只作为java.io.InputStream或java.io.OutputStream的实例提供。

Numeric 数据
输入流读取字节和输出流写入字节。Reader阅读字符和Writer写入字符。因此,要了解输入和输出,首先需要充分了解Java如何处理字节,整数和其他原始数据类型,以及何时,以及为何将其转换为另一个。 在许多情况下,Java的转化行为并不明显。

整型数据
Java中的基本整数数据类型是int、4字节、大端(big-endian)、2的补码整数。int可以接受-2,147,483,648和-2,147,483,647之间的所有值。在Java源代码中键入文字整数(如7,-8,345或3,000,000,000)时,编译器会将该文字视为int。 在3,000,000,000或类似数字太大而无法放入int的情况下,编译器会发出一条错误消息,引用“数字溢出”(Numeric overflow)。

long是8字节,大端(big-endian),2的补码整数,范围从-9,223,372,036,854,775,808到9,223,372,036,854,775,807。 用小写或大写字母L后缀数字来表示Long型字面量。大写字母L是最好的选择,因为在大多数字体中小写字母L太容易与数字1混淆。 例如,7L,-8345L和3,000,000,000L都是64位Long型字面量。

Java中提供了另外两种整数数据类型,short和byte。short是2字节,大端(big-endian),2的补码整数,范围从-32,768到32,767。它们在Java中很少使用,主要包括与C的兼容性。

但是,字节在Java中非常常用。特别是,它们用于I/O. 字节是一个8位,二进制补码整数,范围从-128到127。请注意,与Java中的所有数值数据类型一样,一个字节是有符号的。 最大字节值为127。128,129,依此类推,255都不是字节的合法值。

Java没有short或byte字面量。当你编写字面量42或24,000时,编译器总是将其作为int读取,而不是作为byte或short读取,即使在赋值语句的右侧将其用作byte或short,如下所示:

byte b = 42;
short s = 24000;

但是,在这些行中,编译器执行特殊的赋值转换,有效地将int字面量转换为较窄的类型。 因为int字面量是在编译时已知的常量,所以这是允许的。但是,从int变量到short和bytes的赋值不是—— 至少没有显式强制转换。例如,请考虑以下几行:

int i = 42;
byte b = i;

编译这些行会产生以下错误:
Error: Incompatible type for declaration.
Explicit cast needed to convert int to short.
ByteTest.java line6

即使编译在理论上能够确定赋值不会丢失信息,也会发生这种情况。 要更正此问题,你必须使用显式强制转换,如下所示:

int i = 42;
byte b = (byte)i;

即使两个字节变量相加产生整数结果,也不能在没有强制转换的情况下分配给字节变量。 以下代码产生相同的错误:

byte b1 = 22;
byte b2 = 23;
byte b3 = b1+ b2;

由于这些原因,直接使用字节变量是非常不方便的。流类中的许多方法都记录为读取或写入字节。但是,它们真正返回或接受的参数是无符号字节(0-255)范围内的整数。这与任何Java原始数据类型都不匹配。然后在内部将这些整数转换为字节。

例如,根据Java类库文档,java.io.InputStream的read()方法返回“数据的下一个字节,如果到达流的末尾则返回-1”。 经过思考,这听起来很奇怪。如何将-1作为流数据的一部分与表示流的结束的-1区分开? 实际上,read()方法并不返回字节; 它的签名显示它返回的类型为int:

public abstract int read() throws IOException

此int不是Java中其值介于-128和127之间的byte,而是更一般的无符号字节,其值介于0和255之间。因此,-1可以很容易地与从流中读取的有效数据值区分开来。

java.io.OutputStream类中的write()方法存在类似的问题。它返回void但是将int作为参数:

public abstract void write(int b) throws IOException

这个int是一个0到255之间的无符号字节值。但是,没有什么可以阻止粗心的程序员传入该范围之外的int值。 在这种情况下,写入8个低位,并忽略前24个高位:

b = b & 0x000000FF;

TIP
虽然这是Java语言规范中描述的行为,但由于write()方法是抽象的,因此该方案的实际实现留给了子类,粗心的程序员可以做一些不同的事情。

另一方面,Java中用于读取或写入字节数组的方法。 例如,考虑java.io.InputStream中的这两个read()方法:

public int read(byte[] data) throws IOException
public int read(byte[] data, int offset, int length) throws IOException

虽然8位字节和32位int之间的差异对于单个数字来说是微不足道的,但是当读取几千到几百万个数字时,它可能非常重要。事实上,在Java虚拟机内部,一个字节仍然占用四字节的空间,但是字节数组只占用它实际需要的空间量。虚拟机包括在字节数组上操作的特殊指令,但不包括在单个字节上操作的任何指令。它们只是升级为int。

虽然数据被存储在数组中,值为128到127之间的符号Java字节,但这些有符号值和I/O中通常使用的无符号字节之间存在简单的一一对应关系,这种对应关系由以下公式给出:

int unsignedByte = signedByte >= 0? signedByte:256 + signedByte

转换和转型
由于字节具有如此小的范围,因此它们通常在计算和方法调用中转换为int。通常,他们需要转换回来,通常是通过一个转换。因此,很好地掌握转换是如何发生的是很有用的。

从int转换为byte——就此而言,从任何更宽的整数类型转换为更窄的类型——通过截断高位字节来进行。 这意味着只要较宽类型的值可以用较窄的类型表示,该值就不会改变。转换为字节的int 127仍然保留值127。

另一方面,如果int值对于一个字节来说太大,则会发生奇怪的事情。转换为字节的int 128不是127,即最接近的字节值。相反,它是-128。这是通过二进制补码算法的奇迹发生的。用十六进制表示,128是0x00000080。当该int被强制转换为一个字节时,前导零被截断,留下0x80。在二进制中,这可以写成10000000.如果这是一个无符号数,10000000将是128,一切都会好,但这不是一个无符号数。相反,前导位是符号位,1不表示2^7而是负号。通过取补码(将所有1位改为0位,反之亦然)并加1来求出负数的绝对值。1000000的补码为01111111。加1,你有01111111 + 1 = 10000000 = 128(十进制)。因此,字节0x80实际上代表-128。类似的计算表明int 129被强制转换为字节-127,int 130被强制转换为字节-126,int 131被强制转换为字节-125,依此类推。
这继续通过int 255,它被转换为字节-1。

TIP
在本书中,与Java源代码一样,前面带有0x的所有数字都被读作十六进制。

当达到256时,int的低位字节用零填充。换句话说,256是0x00000100。因此,将其转换为一个byte将产生0,并且循环重新开始。使用此公式可以通过算法再现此行为,尽管强制转换显然更简单。

int byteValue;
int temp = intValue % 256;
if (intValue < 0) {byteValue = temp < -128 ? 256 + temp : temp;
} else {byteValue = temp > 127 ? temp - 256 : temp;
}

Character Data
数字只是典型Java程序读取和写入时所需数据的一部分。许多程序还处理由字符组成的文本。 由于计算机只能真正理解数字,因此通过为给定脚本中的每个字符分配一个数字来编码字符。 例如,在常见的ASCII编码中,字符A被映射到数字65;字符B映射到数字66;字符C映射到数字67;等等。 不同的编码可以编码不同的脚本,或者可以以不同的方式编码相同或相似的脚本。

Java了解各种语言的几十种不同的字符集,从ASCII到Shift日语输入系统(SJIS)再到Unicode。 在内部,Java使用Unicode字符集。 Unicode是1字节Latin-1字符集的超集,而后者又是7位ASCII字符集的8位超集。

ASCII
ASCII,美国信息交换标准码,是一个7位字符集。 因此,它定义了2 ^ 7或128个不同的字符,其数值范围从0到127.这些字符足以处理大多数美国英语。 它是不同计算机常用的最小公分母格式。 如果要从流中读取0到127之间的字节值,然后将其强制转换为char,则结果将是相应的ASCII字符。

ASCII字符0-31和字符127是非打印控制字符。字符32-47是各种标点符号和空格字符。 字符48-57是数字0-9。字符58-64是另一组标点字符。字符65-90是大写字母A-Z。 字符91-96是一些标点符号。字符97-122是小写字母a-z。最后,字符123-126是一些剩余的标点符号。 完整的ASCII字符集如附录中的表A-1所示。

Latin-1
ISO 8859-1,Latin-1是一个8位字符集,是ASCII的严格超集。 它定义了2^8或256个不同的字符,其数值范围从0到256。前128个字符——即那些高位为等于0的数字—— 完全对应于ASCII字符集。 因此65是ASCII A和Latin-1 A; 66是ASCII B和Latin-1 B; 等等。 Latin-1和ASCII分开的字符数介于在128到255之间(高位比特等于1的字符)。ASCII不定义这些字符。 Latin-1将它们用于各种重音字母,例如用罗马字母书写的非英语语言所需的ü,额外的标点符号和符号(如©)以及其他控制字符。Latin-1字符集的上半部分非ASCII半部分显示在附录中的表A-2中。 如果你要从流中读取无符号字节值,然后将其强制转换为char,则结果将是相应的Latin-1字符。

Unicode
Latin-1足以满足大多数西欧语言的需要(除了希腊语之外),但它没有任何接近表示西里尔语,希腊语,阿拉伯语,希伯来语或梵文所需的字符数,更不用说中文和日文等象形文字了。 仅中国就有超过80,000个不同的汉字。为了处理这些脚本和许多其他脚本,发明了Unicode字符集。 Unicode可容纳超过一百万种不同的字符。实际上只使用了大约100,000个,其余的用于未来的扩展。 Unicode可以处理世界上大多数的现存语言和一些消亡的语言。

Unicode的前256个字符与Latin-1字符集的字符相同。 因此65是ASCII A和Unicode A;66是ASCII B和Unicode B,依此类推。

Unicode只是一个字符集。它不是字符编码。虽然Unicode指定字母A具有字符代码65,但它没有说明数字65是使用一个字节,两个字节还是四个字节写入,或者使用的字节是按高位还是低位顺序写入的。 但是,Unicode有一些标准的字节编码,最常见的是UTF-8,UTF-16和UTF-32。 UTF-32是最原始的编码方式。它只是将每个字符表示为单个4字节(32位)int。

最后,UTF-8是一种相对有效的编码(尤其是的大多数文本是ASCII时),每个ASCII字符使用一个字节,许多其他字母表中的每个字符使用两个字节,亚洲语言中的字符使用三到四个字节。 Java的.class文件在内部使用UTF-8来存储字符串文字。

Other Encodings
ASCII,Lation-1和Unicode几乎不是常用的唯一字符集,尽管它们是由Java直接处理的字符集。 还有许多其他字符集,它们对不同的脚本进行编码,并以不同的方式对相同的脚本进行编码。 例如,IBM大型机长期使用名为EBCDIC的非ASCII字符集。 EBCDIC与ASCII具有大部分相同的字符,但将它们分配给不同的数字。Macintoshes通常使用一个名为MacRoman的8位编码,它匹配低128位的ASCII,并且在128个字符中具有与Latin-1大部分相同的字符,但位于不同的位置。DOS(包括Windows中的DOS shell)使用Cp850等字符集,其中包括L和+等框插图字符。Big-5和SJIS分别是中文和日文的编码,其中包括这些脚本中使用的大多数字符。

每个编码的确切细节都相当复杂,应该由专家来处理。 幸运的是,Java类库包含一组由这些专家编写的读写器类。 Reader和Writer在没有任何额外努力的情况下将特定编码的字节转换为Java字符。 出于类似的原因,你应该使用编写器而不是输出流来编写文本,如第20章所述。

char数据类型
Java中的文本主要由转换基元数据类型,字符数组和字符串组成,它们在内部存储为字符数组。 正如你需要理解字节以真正掌握输入和输出流的工作方式一样,你也需要了解字符以了解读写器的工作方式。

在Java中,char是一个2字节的无符号整数是Java中唯一的无符号类型。 因此,可能的char值范围从0到65,535。每个char表示Unicode字符集中的特定字符。 可以使用此范围内的int文本将字符分配字符; 例如:

char copyright = 169;

也可以通过使用字符文本(即用单引号括起来的字符本身)来分配字符:

char copyright = '©';

Sun的javac编译器可以使用-encoding命令行标志将许多不同的编码转换为Unicode,以指定写入文件的编码。 例如,如果你知道文件是用ISO 8859-1编写的,则可以按如下方式编译它:

javac -encoding 8859_1 CharTest.java

表A-4给出了可用编码列表。

除了Unicode本身之外,Java理解的大多数字符集都没有所有Unicode字符的等价物。要对字符集中不存在的字符进行编码,可以使用Unicode转义符。 Unicode转义序列是无转义的反斜杠,后跟任意数量的u字符,后跟四个十六进制数字,指定要使用的字符。 例如:

char copyright = '\u00A9';

Unicode转义不仅可以用于字符文本,还可以用于字符串、标识符、注释,甚至可以用于关键字、分隔符、运算符和数字文本。编译器在对源代码文件执行任何其他操作之前,编译器会将Unicode转义转换为实际的Unicode字符。

TIP
当大多数文本编辑器可以处理Unicode时,Unicode转义是一种遗留问题。 幸运的是,这种情况已经多年没有出现了。 今天,Java源代码应该用Unicode(最好是UTF-8)和非ASCII字符直接输入。 在2006年,Unicode转义仅用于混淆代码。

Readers and Writers
流主要用于基本上可以作为纯字节读取的数据,字节数据和数字数据被编码为一种或另一种二进制数。 流特别不用于读取和写入文本,包括ASCII文本(如“Hello World”)和格式化为文本的数字(如“3.1415929”)。出于这些目的,你应该使用Reader和Writer。

输入和输出流基本上是基于字节的。Reader和Writer基于字符,根据字符集可以有不同的宽度。 例如,ASCII和Latin-1使用1字节字符。UTF-32使用4字节字符。 UTF-8使用不同宽度的字符(1到4个字节之间)。
由于字符最终由字节组成,因此Reader从流中获取输入。 但是,在传递这些字节之前,它们会根据指定的编码格式将这些字节转换为字符。 类似地,在将字符写入某个底层流之前,编写器会根据指定的编码将字符转换为字节。

java.io.Reader和java.io.Writer类是读取和写入基于字符的数据的类的抽象超类。 子类主要处理不同字符集之间的转换。核心Java API包括九个Reader和八个Writer类,所有这些都在java.io包中:

BufferedReader BufferedWriter
CharArrayReader CharArrayWriter
FileReader FileWriter
FilterReader FilterWriter
InputStreamReader LineNumberReader
OutputStreamWriter PipedReader
PipedWriter PrintWriter
PushbackReader StringReader

StringWriter

在大多数情况下,这些类的方法与等效的流类非常相似。 通常唯一的区别是流方法的签名中的字节在匹配的Reader或Writer方法的签名中变为char。 例如,java.io.OutputStream类声明了这三个write()方法:

public abstract void write(int i) throws IOException
public void write(byte[] data) throws IOException
public void write(byte[] data, int offset, int length) throws IOException

因此,java.io.Writer类声明了这三个write()方法:

public abstract void write(int i) throws IOException
public void write(char[] data) throws IOException
public void write(char[] data, int offset, int length) throws IOException

如你所见,除了后两个方法中的字节数组数据已更改为char数组外,签名匹配。还有一个不太明显的区别没有体现在签名上。当int值传递给OutputStream的write()方法进行输出时减小模数256,当int值传递给Writer的write()方法时减少了模数65,536。这反映了字符和字节的不同范围。

java.io.Writer还有两个write()方法,它们从字符串中获取数据:

public void write(String s) throws IOException
public void write(String s, int offset, int length) throws IOException

由于流不知道如何处理基于字符的数据,因此java.io.OutputStream类中没有相应的方法。

Buffers and Channels
只要应用程序必须一次只能读取或写入一个,流就相当快。 实际上,与Java程序本身相比,瓶颈更可能是你正在读取或写入的磁盘或网络。 当程序需要同时读取或写入许多不同的流时,情况稍微复杂一些。 这是Web服务器中的常见情况,例如,单个进程可能同时与数百甚至数千个不同的客户端进行通信。

在任何给定的时间,流都可能阻塞。也就是说,它可能只是暂时停止接受进一步的请求,而等待它正在写入或读取的实际硬件赶上。这可能发生在磁盘上,这是网络连接的一个主要问题。显然,你不想仅仅因为999个客户机中的一个遇到网络拥塞而停止向他们发送数据。在Java 1.4之前对这个问题的传统解决方案是把每个连接放在一个单独的线程中。500个客户端需要500个线程。每个线程都可以独立于其他线程运行,这样一个缓慢的连接不会使每个线程都减速。

但是,线程并非没有自己的开销。 创建和管理线程需要大量工作,并且很少有虚拟机能够处理更多工作,而且很少有虚拟机可以处理超过一千个线程而不会导致严重的性能下降。 即使是最优秀的虚拟机,产生数千个线程也会崩溃。 尽管如此,大型服务器需要能够同时与数千个客户端进行通信。

Java 1.4中发明的解决方案是非阻塞I/O。在非阻塞I/O中,流通常被用为支持角色,而实际工作由通道和缓冲区完成。输入缓冲区用来自通道的数据填充,然后由应用程序输出数据。输出缓冲区反向工作:应用程序用目标随后输出的数据填充缓冲区。设计使得Reader和Writer不必总是同步操作。最重要的是,客户端应用程序可以对每个通道进行读写进行排队。 它不必仅仅因为通道的另一端还没有完全准备就停止处理。 这使一个线程能够同时为多个不同的通道提供服务,从而大大减少了虚拟机的负载。

通道和缓冲区也用于启用内存映射I/O。在内存映射的I/O中,文件被视为大块内存,基本上是大字节数组。 可以使用诸如int x = file.getInt(1067)之类的语句来读取映射文件的特定部分,并使用诸如file.putInt(x,1067)之类的语句来写入。 数据直接存储在磁盘正确的位置上,而无需读取或写入感兴趣部分之前或之后的所有数据。

通道和缓冲区比流和字节稍微复杂一些。但是,对于某些类型的I/O绑定应用程序,性能提升是显着的,值得增加复杂性。

无处不在的IOException
就计算机操作而言,输入和输出是不可靠的。它们完全不受程序员控制的影响。 在读取文件时,磁盘可能会产生坏扇区。建筑工人挖土机不小心挖断了你的WAN线路。 用户意外取消了他们的输入。电话维修人员在试图修理别人的时候会误关闭你的调制解调器线路。(最后一个实际上发生在我写这一章的时候。我的调制解调器一直连接不上,然后拨打拨号音;我不得不在我的大楼地下室找到“修理工”并向他解释他是在修理错了线路。)

由于这些潜在的问题以及更多,几乎每个执行输入或输出的方法都被声明为抛出IOException。 IOException是一个受检查的异常,因此你必须声明你的方法抛出它,或者包含可以将其抛出到try/catch块中的调用。此规则唯一真正的例外是PrintStream和PrintWriter类。 因为围绕每次调用System.out.println()包装一个try/catch块是不方便的,Sun决定让PrintStream(以及后来的PrintWriter)捕获并吃掉print()或println()方法中抛出的任何异常。 如果你确实要检查print()或println()法中的异常,可以调用checkError():

public boolean checkError()

如果在此打印流上发生异常,则checkError()方法返回true,否则返回false。它只告诉你发生了错误。 它不会告诉你发生了什么类型的错误。 如果你需要了解有关错误的更多信息,则必须使用不同的输出流或Writer类。

IOException有许多子类,仅在java.io中有15个,并且方法经常抛出一个更具体的异常,即子类IOException;例如,当你尝试在未知字符集中读取文本时,在文件意外结束时出现EOFException或者出现UnsupportedEncodingException。但是,方法通常只声明它们抛出IOException。

java.io.IOException类声明没有公共方法或重要字段——只是在大多数异常类中找到的通常的两个构造函数:

public IOException()
public IOException(String message)

第一个构造函数使用空消息创建IOException。第二部分提供了有关出错的更多细节。 当然,IOException具有所有异常类(如toString()和printStackTrace())继承的常用方法。

TIP
Java 6还添加了一个“在发生严重I/O错误时抛出”的IOError类。 薛明神在后门中仅仅隐藏这个类,以避免在新的控制台类抛出IO异常中声明它们应该使用的方法。我不确定这个疣是否会保留在Java 6的最终版本中。在写这篇文章的时候,我正在努力游说,要把它删除,或者至少用一个运行时异常代替一个错误。

The Console:System.out, System.in, and System.err
控制台是写入System.out或System.err的输出的默认目标,也是System.in的默认输入。 在大多数平台上,控制台是最初启动Java程序的命令行环境,可能是xterm或DOS提示符,如图1-1所示。 控制台这个词有点用词不当,因为在Unix系统上,控制台指的是一个非常特殊的命令行shell而不是整个命令行shell。

关于I/O的许多常见误解是因为大多数程序员首次接触I/O都是通过控制台进行的。控制台很方便快速的创建黑客和教科书中常见的玩具示例,我将在本书中使用它,但它确实是一个非常不寻常的输入和输出目的地来源,而优秀的Java程序可以避免它。它的行为几乎,但不完全,不像你想读或写的任何其他东西。虽然玩具示例在像这样的编程文本中做出了方便的例子,但它们是一个糟糕的用户界面,在现代程序中确实没有什么位置。用户可以更好地使用精心设计的GUI。此外,控制台跨平台不可靠。许多较小的设备,如Palm Pilots和手机都没有控制台。运行applet的Web浏览器有时会提供可用于输出的控制台。但是,默认情况下这是隐藏的,通常不能用于输入,并且在所有平台上的所有浏览器中都不可用。

System.out
System.out是大多数程序员遇到的OutputStream类的第一个实例。实际上,在学生知道类或输出流是什么之前经常遇到它。具体来说,System.out是java.lang.System类的静态输出字段。它是java.io.PrintStream的一个实例,它是java.io.OutputStream的子类。System.out对应于Unix或C中的stdout。通常,发送到System.out的输出显示在控制台上。作为一般规则,控制台将System.out发送的数字字节数据转换为ASCII或Latin-1文本。 因此,以下行写入字符串“Hello World!” 在控制台上:

byte[] hello = {72,101,108,108,111,32,87,111,114,108,100,33,10,13};
System.out.write(hello);

System.err
Unix和C程序员熟悉stderr,它通常用于错误消息。stderr是一个来自stdout的独立文件指针,但通常意味着同样的事情。通常,stderr和stdout都会将数据发送到控制台,无论是什么。但是,stdout和stderr可以重定向到不同的地方。例如,输出可以重定向到文件,而错误消息仍然出现在控制台上。

System.err是Java的stderr版本。 与System.out类似,System.err是java.io.PrintStream的实例,java.io.PrintStream是java.io.OutputStream的子类。 System.err最常用于try/catch块的catch子句中,如下所示:

try{// Do something that may throw an exception.
}catch(Exception ex){System.err.println(ex);
}

开发好的的程序对System.err不太需要,但在调试时它很有用。

TIP
库不应在System.err上打印任何内容。一般来说,库根本不应该与用户交谈,除非这是它们的特定目的。相反,库应该通过在某种类型的错误处理程序对象中引发异常或调用回调方法来通知客户端应用程序他们遇到的任何问题。是的,Xerces,我在和你说话。(Xerces XML解析器,现在内置到Java 5中,有一个非常恼人的习惯,即通过在系统上打印它们来报告非致命错误。)

System.in
System.in是连接到控制台的输入流,就像System.out是连接到控制台的输出流一样。在Unix或C术语中,System.in是stdin,可以以相同的方式从shell重定向。System.in是java.lang.System类的静态字段。它是java.io.InputStream的一个实例,至少就记录而言。

过去记录的内容,System.in实际上是一个java.io.BufferedInputStream。BufferedInputStream不声明任何新方法;它只是覆盖已经在java.io.InputStream中声明的那些。缓冲输入流将大块数据读入缓冲区,然后以请求的大小包裹它。这比一次读取一个字符更有效。否则,数据对程序员完全透明。

这一点的主要意义是当用户在system.in上输入字节时,程序无法使用字节。相反,输入一次输入一行程序。这允许用户在控制台中输入退格键并纠正错误。Java不允许你把控制台设置为“原始模式”,其中每一个字符只要键入就可用,包括字符,如退格和删除。

用户使用平台的默认字符集(通常为ASCII或其某些超集)键入控制台。读取时数据将转换为数字字节。 例如,如果用户键入“Hello world!” 并按Enter键,从System.in中按以下顺序读取下面的字节:

72,101,108,108,111,32,87,111,114,108,100,33,10,13

许多从命令行运行并从System.in读取输入的程序要求您输入“end of stream”字符,也称为“文件结束”或EOF字符,以正常终止程序。输入方式与平台有关。 在Unix和Mac上,Ctrl-D通常表示流的结束。在Windows上,Ctrl-Z可以。 在某些情况下,在Java识别流结束之前,可能需要按Enter/Ctrl-Z或Enter/Ctrl-D。

DOS shell中的重定向是相同的
有时可以方便地从正在运行的程序中重定向Syetem.out,System.in和System.err。 java.lang.System类中的以下三个静态方法就是这样做的:

public static void setIn(InputStream in)
public static void setOut(PrintStream out)
public static void setErr(PrintStream err)

例如,要指定在System.out上写入的数据发送文件yankees99.out,从System.in读取的数据来自yankees99.tab,您可以编写:

System.setIn(new FileInputStream("yankees99.tab"));
System.setOut(new PrintStream(new FileOutputStream("yankees99.out")))

控制台/Java 6
在使用Java 6时,Sun终于厌倦了Python和Ruby社区所有关于从控制台读取一行输入的难度。 在大多数脚本语言中,这是一个单行程,但传统上它只涉及Java。

TIP
与其他语言相比,从控制台读取一行输入的原因与Java相比,是因为2006年没有人需要在CS 101课程之外这样做。 真正的程序使用GUI或网络来使用接口,而不是控制台,Java一直专注于完成真正的工作而不是启用玩具示例。

Java 6添加了一个新的java.lang.Console类,它为输入和输出提供了一些便利方法。 这个类是一个单例。 它永远不会有多个实例,它始终适用于System.in,System.out和System.err指向的同一个shell。 你可以使用静态System.console()方法检索此类的单个实例,如下所示:

Console theConsole = System.console();

如果你在诸如手机或没有控制台的Web浏览器等环境中运行,则此方法返回null。

你可以通过多种方式使用此类。 最重要的是,它有一个简单的readLine()方法,它从控制台返回一个文本字符串,不包括换行符:

public String readLine() throws IOException

此方法在流结束时返回null。如果遇到任何I/O问题,它会抛出一个IOError。(同样,这是一个设计错误,我试图说服Sun在最终版本发布之前解决这个问题。如果出现问题,这个方法应该像任何常规方法一样抛出IOException。)

你可以选择在阅读该行之前提供格式化的提示:

public String readLine(String prompt, Object... formatting)

提示字符串的解释方式与任何printf()字符串类似,并在其右侧填充参数。 所有这一切都是格式化提示。 这不是scanf()等效项。返回值与no-args readLine()方法的返回值相同。

控制台还有两个readPassword()方法:

public char[] readPassword()
public char[] readPassword(String prompt, Object... formatting)

与readLine()不同,它们不会将键入的字符串显示到屏幕上。 还要注意,它们返回的是字符数组而不是字符串。 使用完密码后,可以用零覆盖数组中的字符,这样密码就不会在内存中保存的时间超过需要的时间。 这限制了由于虚拟内存而将密码暴露给内存扫描仪或存储在磁盘上的可能性。

对于输出,Console有两个方法,printf()和format():

public Console format(String format, Object... arguments)
public Console printf(String format, Object... arguments)

这两种方法没有区别。 他们是同义词。 例如,此代码片段仅使用printf()在控制台上以度,弧度和梯度打印0到360度之间角度的三列表。 每个数字正好是五个字符宽,小数点后面有一个数字。

Console console = System.console();
for (double degrees = 0.0; degrees < 360.0; degrees++){double radians = Math.PI * degrees / 180.0;double grads = 400 * degrees / 360;console.printf("%5.1f %5.1f %5.1f\n", degrees,radians,grads);
}

这是程序运行时的样子:

0.0  0.0  0.0
1.0  0.0  1.1
2.0  0.0  2.2
3.0  0.1  3.3
...

第7章更详细地探讨了printf()及其格式化参数。

控制台通常会缓冲所有输出,直到看到换行符。你可以通过调用flush()方法强制数据在换行之前写入屏幕:

formatter.flush();
formatter.close();

最后,如果这些方法对你来说还不够,你可以直接使用控制台关联的PrintWriter和Reader:

public PrintWriter writer()
public Reader reader()

第20章探讨了这两个类。

示例1-1是一个简单的程序,它使用Console类来回答典型的作业分配:要求用户输入一个整数并打印从1到该整数的数字的平方。为了与这些程序的性质保持一致,我故意在代码中留下至少三个典型的学生错误。 识别和纠正它们是留给读者的功课。

Example 1-1.CS 101 Homework

import java.io.Console;public class Homework {public static void main(String[] args) {Console console = System.console();String input = console.readLine("Please enter a number between 1 and 10:");int max = Integer.parseInt(input);for (int i = 1; i < max; i++) {console.printf("%d\n", i * i);}}
}

这是程序运行时的样子:

c:\> java Homework
Please enter a number between 1 and 10:4
1
4
9

I/O安全检查
关于从互联网下载applet等可执行内容的最初担忧之一就是恶意applet可以擦除你的手盘或读取你的Quicken文件。自从引入Java以来,没有任何改变。这就是为什么Java applet在安全管理器的控制下运行,安全管理器检查applet执行的每个操作以防止潜在的恶意行为。

安全管理器对I/O操作特别小心。在大多数情况下,检查与这些问题有关:

  • 程序可以读取特定文件吗?
  • 程序可以写一个特定的文件吗?
  • 程序可以删除特定文件吗?
  • 程序可以确定特定文件是否存在吗?
  • 程序可以与特定主机建立网络连接吗?
  • 程序是否可以接受来自特定主机的传入连接?

当程序是applet时,对所有问题的简短回答是“不,它不能”。稍微复杂一点的答案会指出一些例外情况。 小程序可以与它们来自的主机建立网络连接;applet可以读取一些包含Java环境信息的非常具体的文件;和受信任的applet有时可能没有这些限制。但对于几乎所有实际目的,答案几乎总是否定的。

由于存在这些安全问题,因此在applet中使用本书中的代码片段和示例时需要小心。此处显示的所有内容在应用程序中运行时都有效,但在applet中运行时,在应用程序中运行时可能会失败,但在applet中运行时,它可能会因SecurityException而失败。一个特定的方法或类是否会导致问题并不总是很明显。例如,当最终目标是字节数组时,BufferedOutputStream的write()方法是完全安全的。但是,当目标是文件时,相同的write()方法将引发异常。尝试打开与Web服务器的连接可能会失败,具体取决于你连接的Web服务器是否与applet来自的Web服务器相同。

因此,本书非常关注应用程序。在没有与安全管理器发生冲突的情况下,可以从applet完成很少的I/O. 问题可能并不总是显而易见的——并非所有Web浏览器都能正确报告安全性异常——但它存在。如果applet作为独立应用程序运行时可以使applet工作,并且无法让它在Web浏览器中运行,则问题可能与浏览器的安全管理器发生冲突。

《Java I/O》Chapter 1相关推荐

  1. 《Java I/O》Chapter 5

    Chapter5 网络流 从成立之初,Java就比其他任何通用编程语言都多了网络这块. Java是第一种为网络I/O提供尽可能多的支持的编程语言,甚至对文件I/O也提供了更多的支持(Java的URL, ...

  2. java 密钥工厂 desede_20145212 实验五《Java网络编程》

    20145212 实验五<Java网络编程> 一.实验内容1.运行下载的TCP代码,结对进行,一人服务器,一人客户端: 2.利用加解密代码包,编译运行代码,一人加密,一人解密: 3.集成代 ...

  3. 《JAVA与模式》之简单工厂模式

    在阎宏博士的<JAVA与模式>一书中开头是这样描述简单工厂模式的:简单工厂模式是类的创建模式,又叫做静态工厂方法(Static Factory Method)模式.简单工厂模式是由一个工厂 ...

  4. 美团架构师开源5万字的《Java面试手册》PDF免费下载!

    美团一位架构师利用空余时间在github整理了一份<Java面试手册>,现整理成PDF,初衷也很简单,就是希望在面试的时候能够帮助到大家,减轻大家的负担和节省时间. 前两天,朋友圈分享了这 ...

  5. 5万字的《Java面试手册》V1.0版本,高清PDF免费获取

    利用空余时间整理了一份<Java面试手册>,初衷也很简单,就是希望在面试的时候能够帮助到大家,减轻大家的负担和节省时间. 前两天,朋友圈分享了这份这份面试手册的初稿,再几位同学的提议下,对 ...

  6. 《JAVA与模式》之命令模式

    2019独角兽企业重金招聘Python工程师标准>>> 在阎宏博士的<JAVA与模式>一书中开头是这样描述命令(Command)模式的: 命令模式属于对象的行为模式.命令 ...

  7. 类的包访问权限:《Java编程思想》中一段话的困惑

    类的包访问权限:<Java编程思想>中一段话的困惑 在<java编程思想第三版>(陈昊鹏 饶若楠等译)的第五章隐藏具体实现中,5.4节的最后一段话是: "正如前面所提 ...

  8. 《Java编程思想》学习笔记(三)——初始化与清理

    一.初始化 初始化其实就是为变量分配内存空间,并确定其初始值的过程.想了解Java中初始化的各种情况,首先要了解Java中变量的类型.根据自己的理解,将Java中的变量类型分成以下几种,虽然可能不太准 ...

  9. [转]《JAVA与模式》之责任链模式

    http://www.cnblogs.com/java-my-life/archive/2012/05/28/2516865.html 在阎宏博士的<JAVA与模式>一书中开头是这样描述责 ...

最新文章

  1. 总结一下嵌入式OLED显示屏显示中文汉字的办法
  2. 抢红包的红包生成算法
  3. 智能检测营销是否合规,网易易盾发布广告合规检测解决方案
  4. 人造卫星为什么会绕着地球转而不是停在太空中或者越飞越远.掉进地球的卫星为什么烧不完....
  5. 云原生之上,亚马逊云科技发布多项容器与Serverless服务,持续发力现代化应用
  6. STM32工作笔记004---了解高速版PCB设计Cadence
  7. Moods of Norway扩大RFID系统使用范围,保证库存准确率
  8. 服务治理---限流(令牌桶算法)
  9. origin下载速度慢 解决方法
  10. 一副眼镜一千多贵吗_一副近视眼镜的成本大概多少?
  11. Idea使用起来反应比较慢
  12. WIN10环境下配置hadoop+spark并运行实例的教程
  13. 第十一届蓝桥杯(国赛) 阶乘约数C语言代码
  14. c语言数组读心术,读心术
  15. 等保测评之安全区域边界
  16. svn提交忽略target目录
  17. 基于Python_opencv的车牌识别系统
  18. Java常量池理解与总结(讲的非常浅显易懂)
  19. 字节跳动2019春招研发部分编程题汇总
  20. 超详细!一文讲透机器视觉常用的 3 种“目标识别”方法

热门文章

  1. 计算机设计大赛国赛演讲稿
  2. 使用pyfinance进行证券收益分析!金融界的一大帮手!
  3. 迈特二十能升到鸿蒙系统吗,华为鸿蒙OS 2.0系列Beta 2发布
  4. 学习Linux七(Linux必学60个命令之【系统管理】)
  5. 程序员缓解职业病的秘方
  6. python将pdf转成excel_PDF转EXCEL,python的这个技能知道吗?
  7. HHDESK便捷功能介绍二
  8. ApacheCN Python 译文集(二)20211110 更新
  9. 甲子光年推出中国低代码行业分析报告:本地私有化部署占比超过一半
  10. 一文读懂天翼物联网平台(AIoT)