TIJ阅读笔记(第十二章)

80酷酷网    80kuku.com

  笔记12: Java I/O 系统
对编程语言的设计者来说,创建一套好的输入输出(I/O)系统,是一项难度极高的任务。
File 类
在介绍直接从流里读写数据的类之前,我们先介绍一下处理文件和目录的类。

你会认为这是一个关于文件的类,但它不是。你可以用它来表示某个文件的名字,也可以用它来表示目录里一组文件的名字。如果它表示的是一组文件,那么你还可以用list( )方法来进行查询,让它会返回String数组。由于元素数量是固定的,因此数组会比容器更好一些。如果你想要获取另一个目录的清单,再建一个File对象就是了。
目录列表器
假设你想看看这个目录。有两个办法。一是不带参数调用list( )。它返回的是File对象所含内容的完整清单。但是,如果你要的是一个"限制性列表(restricted list)"的话 —— 比方说,你想看看所有扩展名为.java的文件 —— 那么你就得使用"目录过滤器"了。这是一个专门负责挑选显示File对象的内容的类。

FilenameFilter接口的声明:
public interface FilenameFilter { boolean accept(File dir, String name);}


accept( )方法需要两个参数,一个是File对象,表示这个文件是在哪个目录里面的;另一个是String,表示文件名。虽然你可以忽略它们中的一个,甚至两个都不管,但是你大概总得用一下文件名吧。记住,list( )会对目录里的每个文件调用accept( ),并以此判断是不是把它包括到返回值里;这个判断依据就是accept( )的返回值。

切记,文件名里不能有路径信息。为此你只要用一个String对象来创建File对象,然后再调用这个File对象的getName( )就可以了。它会帮你剥离路径信息(以一种平台无关的方式)。然后再在accept( )里面用正则表达式(regular expression)的matcher对象判断,regex是否与文件名相匹配。兜完这个圈子,list( )方法返回了一个数组。
匿名内部类
注意,filter( )的参数必须是final的。要想在匿名内部类里使用其作用域之外的对象,只能这么做。

可以用匿名内部类来创建专门供特定问题用的,一次性的类。这种做法的好处是,它能把解决某个问题的代码全都集中到一个地方。但是从另一角度来说,这样做会使代码的可读性变差,所以要慎重。
查看与创建目录
File类的功能不仅限于显示文件或目录。它还能帮你创建新的目录甚至是目录路径(directory path),如果目录不存在的话。此外它还能用来检查文件的属性(大小,上次修改的日期,读写权限等),判断File对象表示的是文件还是目录,以及删除文件。

renameTo( )这个方法会把文件重命名成(或者说移动到)新的目录,也就是参数所给出的目录。而参数本身就是一个File对象。这个方法也适用于目录。
输入与输出
I/O类库常使用"流(stream)"这种抽象。所谓"流"是一种能生成或接受数据的,代表数据的源和目标的对象。流把I/O设备内部的具体操作给隐藏起来了。

Java的I/O类库分成输入和输出两大部分。所有InputStream和Reader的派生类都有一个基本的,继承下来的,能读取单个或byte数组的read( )方法。同理,所有OutputStream和Writer的派生类都有一个基本的,能写入单个或byte数组的write( )方法。但通常情况下,你是不会去用这些方法的;它们是给其它类用的 —— 而后者会提供一些更实用的接口。因此,你很少会碰到只用一个类就能创建一个流的情形,实际上你得把多个对象叠起来,并以此来获取所需的功能。Java的流类库之所以会那么让人犯晕,最主要的原因就是"你必须为创建一个流而动用多个对象"。
InputStream的种类
InputStream的任务就是代表那些能从各种输入源获取数据的类。这些源包括:
byte数组String对象文件类似流水线的"管道(pipe)"。把东西从一头放进去,让它从另一头出来。一个"流的序列(A sequence of other streams)",可以将它们组装成一个单独的流。其它源,比如Internet的连接。(这部分内容在Thinking in Enterprise Java中讨论。)
这些数据源各自都有与之相对应的InputStream的子类。此外,FilterInputStream也是InputStream的子类,其作用是为基类提供"decorator(修饰)"类,而decorator又是为InputStream配置属性和接口的。

表12-1. InputStream的种类类功能构造函数的参数用法ByteArrayInputStream以缓冲区内存为InputStream要从中提取byte的那个缓冲区一种数据源:要把它连到FilterInputStream对象,由后者提供接口。StringBufferInputStream以String为InputStream需要一个String对象。实际上程序内部用的是StringBuffer。一种数据源:要把它连到FilterInputStream对象,由后者提供接口。FileInputStream专门用来读文件的一个表示文件名的String对象,也可以是File或 FileDescriptor对象。一种数据源:要把它连到FilterInputStream对象,由后者提供接口。PipedInputStream从PipedOutputStream提取数据。实现"管道"功能。PipedOutputStream一种多线程环境下的数据源,把它连到FilterInputStream对象,由后者提供的接口。SequenceInputStream将两个或更多的InputStream合并成一个InputStream。两个InputStream对象,或一个InputSteam对象容器的Enumerator一种数据源:要把它连到FilterInputStream对象,由后者提供接口。FilterInputStream一个为decorator定义接口用的抽象类。而decorator的作用是为InputStream实现具体的功能。详见表12-3。见表 12-3见表 12-3
OutputStream的种类
这部分都是些决定往哪里输出的类:是byte的数组(不能是String;不过你可以根据byte数组创建字符串)还是文件,或者是"管道"。

此外,FilterOutputStream还是decorator类的基类。它会为OutputStream安装属性和适用的接口。

表12-2. OutputStream的种类类功能构造函数的参数用法ByteArrayOutputStream在内存里创建一个缓冲区。数据送到流里就是写入这个缓冲区。缓冲区初始大小,可选。要想为数据指定目标,可以用FilterOutputStream对其进行包装,并提供接口。FileOutputStream将数据写入文件。一个表示文件名的字符串,也可以是File或FileDescriptor对象。要想为数据指定目标,可以用FilterOutputStream对其进行包装,并提供接口。PipedOutputStream写入这个流的数据,最终都会成为与之相关联的PipedInputStream的数据源。否则就不成其为"管道"了。PipedInputStream要想在多线程环境下为数据指定目标,可以用FilterOutputStream对其进行包装,并提供接口。FilterOutputStream一个给decorator提供接口用的抽象类。而decorator的作用是为OutputStream实现具体的功能。详见表12-4见表12-4见表12-4
添加属性与适用的接口
使用"分层对象(layered objects)",为单个对象动态地,透明地添加功能的做法,被称为Decorator Pattern。(模式是Thinking in Patterns (with Java)的主题。)Decorator模式要求所有包覆在原始对象之外的对象,都必须具有与之完全相同的接口。这使得decorator的用法变得非常的透明--无论对象是否被decorate过,传给它的消息总是相同的。这也是Java I/O类库要有"filter(过滤器)"类的原因:抽象的"filter"类是所有decorator的基类。(decorator必须具有与它要包装的对象的全部接口,但是decorator可以扩展这个接口,由此就衍生出了很多"filter"类)。

Decorator模式常用于如下的情形:如果用继承来解决各种需求的话,类的数量会多到不切实际的地步。Java的I/O类库需要提供很多功能的组合,于是decorator模式就有了用武之地。但是decorator有个缺点,在提高编程的灵活性的同时(因为你能很容易地混合和匹配属性),也使代码变得更复杂了。Java的I/O类库之所以会这么怪,就是因为它"必须为一个I/O对象创建很多类",也就是为一个"核心"I/O类加上很多decorator。

为InputStream和OutputStream定义decorator类接口的类,分别是FilterInputStream和FilterOutputStream。这两个名字都起得不怎么样。FilterInputStream和FilterOutputStream都继承自I/O类库的基类InputStream和OutputStream,这是decorator模式的关键(惟有这样decorator类的接口才能与它要服务的对象的完全相同)。
用FilterInputStream读取InputStream
FilterInputStream及其派生类有两项重要任务。DataInputStream可以读取各种primitive及String。(所有的方法都以"read"打头,比如readByte( ), readFloat( ))。它,以及它的搭档DataOutputStream,能让你通过流将primitive数据从一个地方导到另一个地方。这些"地方"都列在表12-1里。

其它的类都是用来修改InputStream的内部行为的:是不是做缓冲,是不是知道它所读取的行信息(允许你读取行号或设定行号),是不是会弹出单个字符。后两个看上去更像是给编译器用的(也就是说,它们大概是为Java编译器设计的),所以通常情况下,你是不大会用到它们的。

不论你用哪种I/O设备,输入的时候,最好都做缓冲。所以对I/O类库来说,比较明智的做法还是把不缓冲当特例(或者去直接调用方法),而不是像现在这样把缓冲当作特例。

表12-3. FilterInputStream的种类类功能构造函数的参数用法DataInputStream与DataOutputStream配合使用,这样你就能以一种"可携带的方式(portable fashion)"从流里读取primitives了(int,char,long等)InputStream包含了一整套读取primitive数据的接口。BufferedInputStream用这个类来解决"每次要用数据的时候都要进行物理读取"的问题。你的意思是"用缓冲区。"InputStream,以及可选的缓冲区的容量它本身并不提供接口,只是提供一个缓冲区。需要连到一个"有接口的对象(interface object)"。LineNumberInputStream跟踪输入流的行号;有getLineNumber( )和setLineNumber(int)方法InputStream只是加一个行号,所以还得连一个"有接口的对象"。PushbackInputStream有一个"弹压单字节"的缓冲区(has a one byte push-back buffer),这样你就能把最后读到的那个字节再压回去了。InputStream主要用于编译器的扫描程序。可能是为支持Java的编译器而设计的。用的机会不多。
用FilterOutputStream往OutputStream里面写东西
DataInputStream的另一半是DataOutputStream。它的任务是把primitive数据和String对象重新组织成流,这样其它机器就能用DataInputStream读取这个流了。DataOutputStream的方法都是以"write"开头的,比如writeByte( ),writeFloat( )等等。

PrintStream的用意是要以一种大家都能看懂的方式把primitive数据和String对象打印出来。这一点同DataOutputStream不同,后者是要将数据装入一个流,然后再交给 DataInputStream处理。

PrintStream的两个最重要的方法是print( )和println( )。这两个方法都已经作了重载,因此可以打印各种数据。print( )和println( )的区别在于,后者会多打印一个换行符。

使用PrintStream的时候会比较麻烦,因为它会捕捉所有的IOException(所以你必须直接调用checkError( )来检查错误条件,因为这个方法会在碰到问题的时候返回true)。再加上,PrintStream的国际化做得也不好,而且还不能以与平台无关的方式处理换行(这些问题都已经在PrintWriter里得到解决,我们接下来再讲)。

BufferedOutputStream 是个decorator,它表示对流作缓冲,这样每次往流里写东西的时候它就不会再每次都作物理操作了。输出的时候大致都要这么做。

表12-4. FilterOutputStream的种类类功能构造函数的参数用法DataOutputStream与DataInputStream配合使用,这样你就可以用一种"可携带的方式(portable fashion)"往流里写primitive了(int, char, long,等)OutputStream包括写入primitive数据的全套接口。PrintStream负责生成带格式的输出(formatted output)。DataOutputStrem负责数据的存储,而PrintStream负责数据的显示。一个OutputStream以及一个可选的boolean值。这个boolean值表示,要不要清空换行符后面的缓冲区。应该是OutputStream对象的最终包覆层。用的机会很多。BufferedOutputStream用 这个类解决"每次往流里写数据,都要进行物理操作"的问题。也就是说"用缓冲区"。用flush( )清空缓冲区。OutputStream, 以及一个可选的缓冲区大小本身并不提供接口,只是加了一个缓冲区。需要链接一个有接口的对象。
Reader 和 Writer类系
Java 1.1对最底层的I/O流类库作了重大修改。第一次看到Reader和Writer的时候,你会觉得"它们大概是用来取代InputStream和OutputStream的" (和我一样)。但事实并非如此。虽然InputStream和OutputStream的某些功能已经淘汰了(如果你继续使用,编译器就会发警告),但它们仍然提供了很多很有价值的,面向byte的I/O功能,而Reader和Writer则提供了Unicode兼容的,面向字符的I/O功能。此外:
Java 1.1还对InputStream和OutputStream作了新的补充,所以很明显这两个类系并没有被完全替代。有时,你还必须同时使用"基于byte的类"和"基于字符的类"。为此,它还提供了两个"适配器(adapter)"类。InputStreamReader负责将InputStream转化成Reader,而OutputStreamWriter则将OutputStream转化成Writer。
Reader和Writer要解决的,最主要的问题就是国际化。原先的I/O类库只支持8位的字节流,因此不可能很好地处理16位的Unicode字符流。Unicode是国际化的字符集(更何况Java内置的char就是16位的Unicode字符),这样加了Reader和Writer之后,所有的I/O就都支持Unicode了。此外新类库的性能也比旧的好。
数据源和目的
几乎所有的Java I/O流都有与之对应的,专门用来处理Unicode的Reader和Writer。但有时,面向byte的InputStream和OutputStream才是正确的选择;特别是java.util.zip;它的类都是面向byte的。所以最明智的做法是,先用Reader和Writer,等到必须要用面向byte的类库时,你自然会知道的,因为程序编译不过去了。

下面这张表格列出了这两个类系的数据源和目的之间的关系(也就是说,在这两个类系里,数据是从哪里来的,又是到那里去的)。
数据源和目的Java 1.0的类Java 1.1的类InputStreamReader的适配器:InputStreamReaderOutputStreamWriter的适配器: OutputStreamWriterFileInputStreamFileReaderFileOutputStreamFileWriterStringBufferInputStreamStringReader(没有对应的类)StringWriterByteArrayInputStreamCharArrayReaderByteArrayOutputStreamCharArrayWriterPipedInputStreamPipedReaderPipedOutputStreamPipedWriter
总之,这两个类系即便不是一摸一样,也至少是非常相像。
修改流的行为
不管是InputStream还是OutputStream,用的时候都要先交给FilterInputStream和FilterOutputStrem,并由后者,也就是decorator做一番改造。Reader和Writer继承了这一传统,不过不是完全照搬。

下面这张表的对应关系比前面那张更粗略。这是因为这两个类系的组织结构不同。比方说BufferedOutputStream是FilterOutputStream的子类,但BufferedWriter却不是FilterWriter的子类(后者虽然是一个abstract类,但却没有子类,所以它看上去只是起一个"占位子"的作用,这样你就不会去惦记它在哪里了)。但不管怎么说,它们的接口还是很相似的。
Filter类Java 1.0的类Java 1.1的类FilterInputStreamFilterReaderFilterOutputStreamFilterWriter(这是个无派生类的抽象类)BufferedInputStreamBufferedReader(也有readLine( ))BufferedOutputStreamBufferedWriterDataInputStream尽量用DataInputStream(除非你用BufferedReader的时候要用readLine( ))PrintStreamPrintWriterLineNumberInputStream(过时了)LineNumberReaderStreamTokenizerStreamTokenizer(换一个构造函数,把Reader当参数传给它)PushBackInputStreamPushBackReader
有一条很清楚:别再用DataInputStream的readLine( )(编译时会警告你这个方法已经"过时了(deprecated)"),要用就用BufferedReader的。此外,DataInputStream仍然是I/O类库的"种子选手"。

为了让向PrintWriter的过渡变得更简单,PrintWriter除了有一个拿Writer做参数的构造函数之外,还有一个拿OutputStream做参数的构造函数。但是PrintWriter格式上并不比PrintStream的更好;它们的接口实际上是完全相同的。

PrintWriter的构造函数里还有一个可选的,能自动地进行清空操作的选项。如果你设了这个标记,那么每次println( )之后,它都会自动清空。
没变过的类
Java从1.0升到1.1时,有几个类没有变过:
在Java 1.1 中无相对应的类的 Java 1.0 的类DataOutputStreamFileRandomAccessFileSequenceInputStream
特别是DataOutputStream,用法都一点没变,所以你就可以用InputStream和OutputStream来读写可以传输的数据了。
自成一派: RandomAccessFile
RandomAccessFile是用来访问那些保存数据记录的文件的,这样你就可以用seek( )方法来访问记录,并进行读写了。这些记录的大小不必相同;但是其大小和位置必须是可知的。

首先,你可能会不太相信,RandomAccessFile竟然会是不属于InputStream和OutputStream类系的。实际上,除了实现DataInput和DataOutput接口之外(DataInputStream和DataOutputStream也实现了这两个接口),它和这两个类系毫不相干,甚至都没有用InputStream和OutputStream已经准备好的功能;它是一个完全独立的类,所有方法(绝大多数都只属于它自己)都是从零开始写的。这可能是因为RandomAccessFile能在文件里面前后移动,所以它的行为与其它的I/O类有些根本性的不同。总而言之,它是一个直接继承Object的,独立的类。

基本上,RandomAccessFile的工作方式是,把DataInputStream和DataOutputStream粘起来,再加上它自己的一些方法,比如定位用的getFilePointer( ),在文件里移动用的seek( ),以及判断文件大小的length( )。此外,它的构造函数还要一个表示以只读方式("r"),还是以读写方式("rw")打开文件的参数 (和C的fopen( )一模一样)。它不支持只写文件,从这一点上看,假如RandomAccessFile继承了DataInputStream,它也许会干得更好。

只有RandomAccessFile才有seek方法,而这个方法也只适用于文件。BufferedInputStream有一个mark( )方法,你可以用它来设定标记(把结果保存在一个内部变量里),然后再调用reset( )返回这个位置,但是它的功能太弱了,而且也不怎么实用。

RandomAccessFile的绝大多数功能,如果不是全部的话,已经被JDK 1.4的nio的"内存映射文件(memory-mapped files)"给取代了。下面我们会讲到这部分内容的。
常见的I/O流的使用方法
虽然I/O流的组合方式有很多种,但最常用的也就那么几种。下
输入流
第一到第四部分演示了如何创建和使用InputStream。第四部分还简单地演示了一下OutputStream的用法。
1. 对输入文件作缓冲
要想打开打开文件读取字符,你得先用String或File对象创建一个FileInputReader。为了提高速度,你应该对这个文件作缓冲,因此你得把FileInputReader的reference交给BufferedReader。由于BufferedReader也提供了readLine( )方法,因此它就成为你最终要使用的那个对象,而它的接口也成为你使用的接口了。当你读到了文件的末尾时,readLine( )会返回一个null,于是就退出while循环了。

最后,用close( )来关闭文件。单从技术角度上说,程序退出的时候(不管有没有垃圾要回收)都应该调用finalize( ),而finalize( )又会调用close( )。不过各种JVM的实现并不一致,所以最好还是明确地调用close( )。

System.in是一个InputStream,而BufferedReader需要一个Reader作参数,所以要先通过InputStreamReader来转转手。
2. 读取内存
(StringReader的)read( )方法会把读出来的byte当作int,所以要想正常打印的话,你得先把它们转换成char。
3. 读取格式化的内存
要想读取"格式化"的数据,你就得用DataInputStream了,它是一个面向byte的I/O类 (不是面向char的),因此你只能从头到底一直用InputStream了。当然你可以把所有东西(比方说文件) 都当成byte,然后用InputStream读出来,但这里是String。要想把String变成成byte数组,可以用String的getBytes( )方法,而ByteArrayInputStream是可以处理byte数组的。到了这一步,你就不用担心没有合适的InputStream来创建DataInputStream了。

如果你是用readByte( )逐字节地读取DataInputStream的话,那么无论byte的值是多少,都是合法的,所以你无法根据返回值来判断输入是否已经结束了。你只能用available( )来判断还有多少字符。

注意,available( )的工作方式会随读取介质的不同而不同;严格地讲,它的意思是"可以不被阻塞地读取的字节的数目。"对文件来说,它就是整个文件,但如果是其它流,情况就不一定了,所以用之前要多留一个心眼。

你也可以像这样,用异常来检查输入是不是完了。但不管怎么说,把异常当成控制流程来用总是对这种功能的滥用。
4. 读取文件
(试试把BufferedWriter去掉,你就能看到它对性能的影响了—— 缓冲能大幅提高I/O的性能)。

LineNumberInputStream这是一个傻乎乎的,没什么用的类

输入流用完之后,readLine( )会返回null。如果写文件的时候不调用close( ),它是不会去清空缓冲区的,这样就有可能会落下一些东西了。
输出流
根据写数据的方式不同,OutputStream主要分成两类;一类是写给人看的,一类是供DataInputStream用的。虽然RandomAccessFile的数据格式同DataInputStream和DataOutputStream的相同,但它不属于OutputStream的。
5. 存储和恢复数据
PrintWriter会对数据进行格式化,这样人就能读懂了。但是如果数据输出之后,还要恢复出来供其它流用,那你就必须用DataOutputStream来写数据,再用DataInputStream来读数据了。当然,它们可以是任何流,不过我们这里用的是一个经缓冲的文件。DataOutputStream和DataInputStream是面向byte的,因此这些流必须都是InputStream和OutputStream。

如果数据是用DataOutputStream写的,那么不管在哪个平台上,DataInputStream都能准确地把它还原出来。这一点真是太有用了,因为没人知道谁在为平台专属的数据操心。如果你在两个平台上都用Java,那这个问题就根本不存在了 。

用DataOutputStream写String的时候,要想确保将来能用DataInputStream恢复出来,唯一的办法就是使用UTF-8编码,也就是像例程第5部分那样,用writeUTF( )和readUTF( )。UTF-8是Unicode的一种变形。Unicode用两个字节来表示一个字符。但是,如果你处理的全部,或主要是ASCII字符(只有7位),那么无论从存储空间还是从带宽上看,就都显得太浪费了,所以UTF-8 用一个字节表示ASCII字符,用两或三个字节表示非ASCII的字符。此外,字符串的长度信息存在(字符串)的头两个字节里。writeUTF( )和readUTF( )用的是Java自己的UTF-8版本,所以如果你要用一个Java程序读取writeUTF( )写的字符串的话,就必须进行一些特殊处理了。

有了writeUTF( )和readUTF( ),你就能放心地把String和其它数据混在一起交给DataOutputStream了,因为你知道String是以Unicode的形式存储的,而且可以很方便地用DataOutputStream恢复出来。

writeDouble( )会往流里写double,而它"影子"readDouble( )则负责把它恢复出来(其它数据也有类似的读写方法)。但是要想让读取方法能正常工作,你就必须知道流的各个位置上都放了些什么数据。因为你完全可以把double读成byte,char,或其它什么东西。所以要么以固定的格式写文件,要么在文件里提供额外的解释信息,然后一边读数据一边找数据。先提一下,对于复杂数据的存储和恢复,对象的序列化可能会比较简单。
6. 读写随机文件
正如我们前面所讲的,如果不算它实现了DataInput和DataOutput接口,RandomAccessFile几乎是完全独立于其它I/O类库之外的,所以它不能与InputStream和OutputStream合起来用。虽然把ByteArrayInputStream当作"随机存取的元素(random-access element)"是一件很合情合理的事,但你只能用RandomAccessFile来打开文件。而且,你只能假定RandomAccessFile已经做过缓冲了,因为即便没做你也无能为力。

构造函数的第二个参数的意思是:是以只读("r") 还是读写("rw")方式打开RandomAccessFile。

RandomAccessFile的用法就像是DataInputStream和DataOutputStream的结合(因为它们的接口是等效的)。此外,你还能用seek( )在文件里上下移动,并进行修改。

随着JDK 1.4的new I/O的问世,你该考虑一下是不是用"内存映射文件(memory-mapped file)"来代替RandomAccessFile了。
管道流
这一章只会大致地提一下PipedInputStream,PipedOutputStream,PipedReader和PipedWriter。这并不是说它们不重要,只是因为管道流是用于线程间的通信的,所以除非你已经理解了多线程,否则是不会理解它的价值的。我们会在第13章用一个例子来讲解这个问题。
读写文件的实用程序
把文件读进内存,改完,再写文件。这是再普通不过的编程任务了。但是Java的I/O就是有这种问题,即便是做这种常规操作,你也必须写一大串代码——根本就没有辅助函数。更糟的是,那些喧宾夺主的decorator会让你忘了该怎样打开文件。因此比较明智的做法还是自己写一个辅助类。下面就是这样一个类,它包含了一些"能让你将文本文件当作字符串来读写"的static方法。此外,你还可以创建一个"会把文件的内容逐行存入ArrayList的"TextFile类,(这样在处理文件的时候,就能使用ArrayList的功能了):

//: com:bruceeckel:util:TextFile.java// Static functions for reading and writing text files as// a single string, and treating a file as an ArrayList.// {Clean: test.txt test2.txt}package com.bruceeckel.util;import java.io.*;import java.util.*;public class TextFile extends ArrayList { // Tools to read and write files as single strings: public static String read(String fileName) throws IOException { StringBuffer sb = new StringBuffer(); BufferedReader in = new BufferedReader(new FileReader(fileName)); String s; while((s = in.readLine()) != null) { sb.append(s); sb.append("\n"); } in.close(); return sb.toString(); } public static void write(String fileName, String text) throws IOException { PrintWriter out = new PrintWriter( new BufferedWriter(new FileWriter(fileName))); out.print(text); out.close(); } public TextFile(String fileName) throws IOException { super(Arrays.asList(read(fileName).split("\n"))); } public void write(String fileName) throws IOException { PrintWriter out = new PrintWriter( new BufferedWriter(new FileWriter(fileName))); for(int i = 0; i < size(); i++) out.println(get(i)); out.close(); } // Simple test: public static void main(String[] args) throws Exception { String file = read("TextFile.java"); write("test.txt", file); TextFile text = new TextFile("test.txt"); text.write("test2.txt"); }} ///:~

所有这些方法都会直接往外面抛IOException。由于一行读出来之后,后面的换行符就没了,因此read( )会在每行的后面再加一个换行符,然后接到StringBuffer的后面(出于效率考虑)。最后它会返回一个"含有整个文件的内容"的String。write( )的任务是打开文件,然后往里面写东西。任务完成之后要记着把文件给close( )了。

(TextFile)的构造函数用read( )方法将文件转化成String,然后用String.split( )和换行符转换成数组(如果你要经常使用这个类,或许应该重写一遍构造函数以提高性能)。此外,由于没有相应的"join"方法,非static的write( )方法只能手动地逐行打印文件。

为了确保它能正常工作,main( )作了一个基本测试。虽然它只是个小程序,但到后面你就会发觉,它却能帮你节省很多时间,同时让生活变得轻松一点。
标准I/O
"标准I/O"是Unix的概念,它的意思是,一个程序只使用一个信息流(这种设计思想也以某种形式体现在Windows及其它很多操作系统上)。所有输入都是从"标准输入"进来的,输出都从"标准输出"出去,错误消息都送到"标准错误"里。标准I/O的优点是,它可以很容易地把程序串连起来,并且把一个程序的输出当作另一个程序的输入。这是一种非常强大的功能。
读取标准输入
Java遵循标准I/O的模型,提供了Syetem.in,System.out,以及System.err。本书一直都在用System.out往标准输出上写,而它(System.out)是一个已经预先处理过的,被包装成PrintStream的对象。和System.out一样,System.err也是一个PrintStream,但是System.in就不对了,它是一个未经处理的InputStream。也就是说,虽然你可以直接往System.out和System.err上写,但是要想读System.in的话,就必须先做处理了。

通常情况下,你会用readLine( )一行一行地读取输入,因此要把System.in包装成BufferedReader。但在这之前还得先用InputSteamReader把System.in转换成Reader。
将System.out转换成PrintWriter
System.out是PrintStream,也就是说它是OutputStream。不过PrintWriter有一个能将OutputStream改造成PrintWriter的构造函数。有了这个构造函数,你就可以随时将System.out转化成PrintWriter了:

为了启动自动清空缓冲区的功能,一定要使用双参数版的构造函数,并且把第二个参数设成true。这点非常重要,否则就有可能会看不到输出了。
标准I/O的重定向
Java的System类还提供了几个能让你重定向标准输入,标准输出和标准错误的静态方法:

setIn(InputStream)setOut(PrintStream)setErr(PrintStream)

如果程序在短时间内输出了大量的信息,使得翻屏的速度非常快,以致于你都没法读了,这时对输出进行重定向就会显得非常有用了 。对于那些要重复测试用户输入的命令行程序来说,对输入进行重定向也是非常重要的。

I/O重定向处理的不是character流,而是byte流,因此不能用Reader和Writer,要用InputStream和OutputStream。



分享到
  • 微信分享
  • 新浪微博
  • QQ好友
  • QQ空间
点击: