读Java编程思想随笔のIO流
输入和输出
编程语言的I/O类库中常使用流这个抽象概念,它代表任何有能力产出数据的数据源对象或者是有能力接受数据的接收端对象。“流”屏蔽了实际I/O设备中处理数据的细节。
java类库中的I/O类分成输入和输出两部分,可以在JDK文档里的类层次结构中看到。通过继承,任何自InputStream或Reader派生而来的类都含有名为read()的基本方法,用于读取单个字节或字节数组。同样,任何自OutputStream或Writer派生而来的类都含有名为write()的基本方法,用于写单个字节或字节数组。但是,我们通常不会用到这些方法,它们之所以存在是因为别的类可以使用它们,以便提供更有用的接口。因此,我们很少使用单一的类来创建流对象,而是通过叠合多个对象来提供所期望的功能。实际上,Java中“流”类库让人迷惑的主要原因就在于:创建单一的结果流,却需要创建多个对象。
InputStream
InputStream的作用是用来表示那些从不同数据源产生输入的类。这些数据源包括:
1、字节数组
2、String对象
3、文件
4、“管道”,工作方式与实际管道相似
5、一个由其他种类的流组成的序列,以便我们可以将它们收集合并到一个流内
6、其他数据源,如Internet连接。
每一种数据源都有相应的InputStream子类。例外,FilterInputStream也属于一种InputStream,为装饰器类提供基类,其中,装饰器类可以把属性或有用的接口与输入流连接在一起。
OutputStream
该类别的类决定了输出所要去往的目标:字节数组(但不是String,不过你当然可以用字节数组自己创建)、文件或管道。
另外,FilterOutputStream为装饰器类提供了一个基类,装饰器类把属性或者有用的接口与输出流连接起来。
添加属性和有用的接口
Java I/O类库需要多种不同功能的组合,这正是使用装饰器模式的理由所在。这也是Java I/O类库里存在filter类的原因所在抽象类filter是所有装饰器类的基类。装饰器必须具有和它所装饰的对象相同的接口,但它也可以扩展接口,而这种情况只发生在个别filter类中。
但是,装饰器模式有一个缺点:在编写程序时,它给我们提供了相当多的灵活性,但是它同时也增加了代码的复杂性。Java I/O类库操作不便的原因在于:我们必须创建许多类——核心 I/O类型加上所有的装饰器,才能得到我们所希望的单个I/O对象。
FilterInputStream和FilterOutputStream是用来提供装饰器类接口以控制特定输入流(InputStream)和输出流(OutputStream)的两个类,FilterInputStream和FilterOutputStream分别自I/O类库中的基类InputStream和OutputStream派生而来,这两个类是装饰器的必要条件。
通过FilterInputStream从InputStream读取数据
FilterInputStream类能够完成两件完全不同的事情。其中,DataInputStream允许我们读取不同的基本类型数据以及String对象。搭配相应的DataInputStream,我们就可以通过数据流将基本类型的数据从一个地方迁移到另一个地方。
其他FilterInputStream类则在内部修改InputStream的行为方式:是否缓冲,是否保留它所读过的行(允许我们查询行数或设置行数),以及是否把单一字符推回输入流等等。最后两个类看起来更像是为了创建一个编译器,因此我们在一般编程中不会用到它。
FilterInputStream的类型及功能
通过FilterOutputStream向OutputStream写入
与DataInputStream相对应的是DataOutputStream,它可以将各种基本数据类型以及String对象格式化输出到“流”中;这样一来,任何机器上的任何DataInputStream都能够读取它们。
PrintStream最初的目的便是为了以可视化格式打印所有的基本数据类型以及String对象。这和DataOutputStream不同,后者的目的是将数据元素置于流中,使DataOutputStream能够可移植地重构它们。
BufferedOutputStream是一个修改过的OutputStream,它对数据流使用缓冲技术;因此当每次向流写入时,不必每次都进行实际的物理写动作。所以在进行输出时,我们可能更经常的是使用它。
FilterOutputStream的类型及功能。
尽管一些原始的流类库不再被使用(如果使用它们,则会收到编译器的警告信息),但是InputStream和OutputStream在以面向字节形式的I/O中可以提供极有价值的功能,Reader和Writer则提供兼容Unicode与面向字符的I/O功能。
Java1.1向InputStream和OutputStream继承层次结构中添加了一些新类,所以显然这两个类是不会被取代。
有时候我们必须把来自“字节”层次结构中的类与来自“字符”层次结构中的类结合起来使用。为了实现这个目的,要用到适配器 类:InputStreamReader可以把InputStream转换成Reader,而OutputStreamWriter可以把 OutputStream转换成Writer。
设计InputStream和OutputStreamReader 和Writer继承层次结构主要是为了国际化。老的I/O流继承层次结构仅支持8位的字节流,并且不能很好的处理16位的Unicode字符。由于 Unicode用于字符国际化,所以添加Reader和Writer继承层次结构就是为了在所有的I/O操作中都支持Unicode。
数据的来源和去除
几乎所有的原始的Java I/O流类都有相应的Reader和Writer类来提供天然的Unicode操作。然而在某些场合,面向字节的InputStream和 OutputStream才是正确的解决方案;特别是,java.util.zip类库就是面向字节的而不是字符的。因此,最明智的做法是尽量尝试使用 Reader和Writer,一旦程序代码无法编译成功,我们就会发现自己不得不使用面向字节的类库。
下面展示在两个继承层次结构中,信息的来源和去处。
更改流的行为
对 于InputStream和OutputStream来说,我们会使用FilterInputStream和FilterOutputStream的装饰 器子类来修改流以满足特殊需要。Reader和Writer的类继承层次结构继续沿用相同的思想,但是并不完全相同。尽管 BufferedOutputStream是FilterOutputStream的子类,但是BufferedWriter并不是 FilterWriter的子类(尽管FilterWriter是抽象类,没有任何子类,把它放在那里只是把它当作一个占位符,或仅仅让我们不会对它所在 的地方产生疑惑)。然而,这些类的接口却十分相似。
对象序列化
Java的对象序列化将那些实现了Serializable接口的对象转换成字节序列,并能够在以后将这个字节序列完全恢复到原来的对象。这一过程甚至可以通过网络进行;这意味着序列化机制能弥补不同操作系统之间的差异。也就是说,可以在运行windows系统的计算机上创建一个对象,将其序列化,通过网络将其发送到一台运行Unix系统的计算机,然后在那里准确的进行组装,而却不必担心在不同的计算机上表示会不同,也不必关心字节的顺序和其他任何细节。
就其本身而言,对象的序列化是非常有趣的,因为利用它可以实现轻量级持久性。持久性意味着对象的生命周期并不取决于程序是否在运行,它可以生存于程序的调用之间。通过将一个序列化对象写入磁盘,然后在重新调用程序时恢复该对象,就能够实现持久性效果。之所以称其为轻量级,是因为不能用某种持久化关键字来简单定义一个对象,并让系统自动维护其他细节问题。相反,对象必须在程序中显式地序列化和反序列化还原。
对象的序列化加入语言主要是为了支持两大特性。一是Java的远程方法调用(Remote Method Invocation)RMI,它使存于其他计算机上的对象就像存活于本机上一样。当向远程对象发送消息时,需要通过对象序列化来传输参数和返回值。再者,对于javabean来说,对象的序列化也是必须的,使用一个bean时,一般情况下在设计阶段对它的状态信息进行配置。这种状态信息必须保存下来,并在程序启动时进行后期恢复;这种具体工作就由对象的序列化来完成。
只要对象实现了Serializable接口(该接口仅是一个标记接口,不包括任何方法),对象的序列化处理就会非常简单。当序列化的概念被加入到语言中时,许多标准库类都发生了改变,以便具备序列化特性--其中包括所有基本数据类型的包装器,所有容器类以及其他东西。甚至Class对象也可以被序列化。
要序列化一个对象,首先要创建某些OutputStream对象,然后将其封装在一个ObjectOutputStream对象内。这时,只需调用writeObject()即可对象序列化,然后将其发送给OutputStream(对象化序列是基于字节的,因要使用InputStream和OutputStream继承层次结构)。要反向进行该过程,需要将一个InputStream对象,封装到ObjectInputStream对象内,然后调用readObject()。和往常一样,我们最后获得的是一个引用,它指向一个向上转型的Object,所以必须向下转型才能直接设置它们。
对象序列化特别聪明的一个地方是,它不仅保存了对象的全景图,而且能追踪到对象内所有包含的引用,并保存那些对象;接着又能够对对象内包含的每个这样的引用进行追踪;以此类推。这种情况有时被称为对象网,单个对象与之建立连接,而且它还包含了对象的引用数组和成员对象。如果必须保持自己的一套对象序列化机制,那么维护那些可追踪到所有链接的代码可能会显得非常麻烦。然而,由于Java的对象序列化似乎找不出什么缺点,所以请不要自己动手,让它用优化的算法自动维护整个对象网。
1 class Data implements Serializable{ 2 private int n; 3 public Data(int n){ 4 this.n=n; 5 } 6 @Override 7 public String toString() { 8 return Integer.toString(n); 9 } 10 } 11 public class Worm implements Serializable{ 12 private static Random rand = new Random(47); 13 private Data [] d = {new Data(rand.nextInt(10)),new Data(rand.nextInt(10)),new Data(rand.nextInt(10))}; 14 private Worm next; 15 private char c; 16 public Worm(int i,char x){ 17 System.out.println("Worm constructor:"+i); 18 c = x; 19 if (--i>0){ 20 next = new Worm(i,(char)(x+1)); 21 } 22 } 23 public Worm(){ 24 System.out.println("Default constructor"); 25 } 26 @Override 27 public String toString() { 28 StringBuilder result = new StringBuilder(":"); 29 result.append(c); 30 result.append("("); 31 for (Data dat :d){ 32 result.append(dat); 33 } 34 result.append(")"); 35 if (next!=null){ 36 result.append(next); 37 } 38 return result.toString(); 39 } 40 41 public static void main(String[] args) throws IOException,ClassNotFoundException{ 42 Worm worm = new Worm(6,'a'); 43 System.out.println("w="+worm); 44 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("worm.out")); 45 out.writeObject("Worm storage\n"); 46 out.writeObject(worm); 47 out.close(); 48 ObjectInputStream in = new ObjectInputStream(new FileInputStream("worm.out")); 49 String s = (String) in.readObject(); 50 Worm w = (Worm) in.readObject(); 51 System.out.println(s+" w="+w); 52 ByteArrayOutputStream bout = new ByteArrayOutputStream(); 53 ObjectOutputStream out2 = new ObjectOutputStream(bout); 54 out2.writeObject("Worm storage\n"); 55 out2.writeObject(w); 56 out2.flush(); 57 58 ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray())); 59 s = (String) in2.readObject(); 60 Worm w2 = (Worm) in2.readObject(); 61 System.out.println(s+" w2="+w2); 62 } 63 } 64 // Worm constructor:6 65 // Worm constructor:5 66 // Worm constructor:4 67 // Worm constructor:3 68 // Worm constructor:2 69 // Worm constructor:1 70 // w=:a(853):b(119):c(802):d(788):e(199):f(881) 71 // Worm storage 72 // w=:a(853):b(119):c(802):d(788):e(199):f(881) 73 // Worm storage 74 // w2=:a(853):b(119):c(802):d(788):e(199):f(881)
自我独立的类
RandomAccessFile适用于由大小已知的记录组成的文件,所以我们可以使用seek()将记录从一处转移到另一处,然后读取或者修改记录。文件中记录的大小不一定相同,只要我们能够确定那些记录有多大以及它们在文件中的位置即可。
最初,我们可能难以相信RandomAccessFile不是InputStream和OutputStream继承层次结构中的一部分。除了实现了DataInput和DataOutput(DataInputStream和DataOutputStream也实现了这两个接口)外,它和这两个继承层次结构没有任何关联。它甚至不使用InputStream和OutputStream中的任何功能。它是一个完全独立的类。从头开始编写所有方法。这么做是因为RandomAccessFile拥有与别的I/O类型本质不同的行为,因为我们可以在一个文件内向前或向后移动。在任何情况下,它都是自我独立地,直接从Object派生而来。
从本质上讲,RandomAccessFile的工作方式类似于把DataInputStream和DataOutputStream结合起来使用,还添加了一些方法。其中方法getFilePointer()用于查找当前所处的文件位置,seek()用于在文件内移动至新的位置,length()用于判断文件的最大尺寸。另外,其构造器还需要第二个参数,用来指示我们是随机读(r)还是既读又写(rw)。它并不支持只写文件。
只有RandomAccessFile支持搜寻方法,并且只适用于文件。
1 static int size=1;//主要是为了控制循环的次数,因为是定时刷,每次刷的文件行数可能不一样 2 static long chars=0;//chars指的是字符数 3 public Map readANDwrite(String fileName) { 4 Map<String,Map> bigMap = new HashMap(); 5 File file = new File(fileName); 6 //java提供的一个可以分页读取文件的类,此类的实例支持对随机访问文件的读取和写入 7 RandomAccessFile rf = null; 8 String tempString = null; 9 try { 10 //初始化RandomAccessFile,参数一个为文件路径,一个为权限设置,这点与Linux类似,r为读,w为写 11 rf = new RandomAccessFile(fileName, "rw"); 12 //设置到此文件开头测量到的文件指针偏移量,在该位置发生下一个读取或写入操作 13 rf.seek(chars); 14 //获取文件的行数 15 int fileSize = getTotalLines(file); 16 for (int i = size-1; i < fileSize; i++) {//从上一次读取结束时的文件行数到本次读取文件时的总行数中间的这个差数就是循环次数 17 //一行一行读取 18 tempString = rf.readLine(); 19 //文件中文乱码处理 20 tempString = tempString.replaceAll("%(?![0-9a-fA-F]{2})", "%25"); 21 tempString = tempString.replaceAll("\\+", "%2B"); 22 tempString = java.net.URLDecoder.decode(tempString, "GB2312"); 23 //将字符串JSON转换为实体JSON,以便能通过key取value 24 JSONObject json = JSONObject.fromObject(tempString); 25 String refPage = json.get("refPage").toString(); 26 System.out.println(refPage); 27 Map tmap = new HashMap(); 28 if (bigMap.containsKey(refPage)) 29 tmap = (Map) bigMap.get(refPage); 30 else { 31 tmap = new HashMap(); 32 } 33 // 计数 34 String tCount = "count"; 35 int pvCount = 1; 36 if (tmap.containsKey(tCount)) { 37 pvCount = (Integer) tmap.get(tCount); 38 } 39 pvCount++; 40 tmap.put(tCount, pvCount); 41 bigMap.put(refPage, tmap); 42 } 43 //返回此文件中的当前偏移量。 44 chars = rf.getFilePointer(); 45 size=fileSize; 46 } catch (IOException e) { 47 e.printStackTrace(); 48 }catch(JSONException j){ 49 50 } finally { 51 if (rf != null) { 52 try { 53 rf.close(); 54 } catch (IOException e1) { 55 } 56 } 57 } 58 return bigMap; 59 } 60 //获取文件的行数 61 static int getTotalLines(File file) throws IOException { 62 FileReader in = new FileReader(file); 63 LineNumberReader reader = new LineNumberReader(in); 64 String s = reader.readLine(); 65 int lines = 0; 66 while (s != null) { 67 lines++; 68 s = reader.readLine(); 69 } 70 reader.close(); 71 in.close(); 72 return lines; 73 }
缓冲输入文件
如果想要打开一个文件用于字符输入,可以使用以String或File对象作为文件名的FileInputReader。为了提高速度,我们希望对那个文件进行缓冲,那么我们将产生的引用传给一个BufferedReader构造器。由于BufferedReader也提供了readLine()方法,所以这是我们的最终对象和进行读取的接口。当readLine()将返回null时,你就达到了文件的末尾。
1 public class BufferedInputFile { 2 public static String read(String filename) throws IOException{ 3 BufferedReader in = new BufferedReader(new FileReader(filename)); 4 String s; 5 StringBuilder sb = new StringBuilder(); 6 while((s=in.readLine())!=null){ 7 sb.append(s+"\n"); 8 } 9 in.close(); 10 return sb.toString(); 11 } 12 13 public static void main(String[] args) throws IOException{ 14 System.out.println(read("d:BufferedInputFile.java")); 15 } 16 } 17 // package com.qiqi.io; 18 // import java.io.BufferedReader; 19 // import java.io.FileReader; 20 // import java.io.IOException; 21 // 22 // /** 23 // * @ClassName: BufferedInputFile 24 // * @Description: 缓冲输入文件 25 // * @Author huhailang 26 // * @Date 2015/12/23 16:36 27 // * @Company 中国奇奇科技有限公司 28 // */ 29 // public class BufferedInputFile { 30 // public static String read(String filename) throws IOException{ 31 // BufferedReader in = new BufferedReader(new FileReader(filename)); 32 // String s; 33 // StringBuilder sb = new StringBuilder(); 34 // while((s=in.readLine())!=null){ 35 // sb.append(s+"\n"); 36 // } 37 // in.close(); 38 // return sb.toString(); 39 // } 40 // 41 // public static void main(String[] args) throws IOException{ 42 // System.out.println(read("d:BufferedInputFile.java")); 43 // } 44 // }
从内存输入
1 public class MemoryInput { 2 public static void main(String[] args) throws IOException{ 3 StringReader in = new StringReader(BufferedInputFile.read("d:MemoryInput.java")); 4 int c; 5 while((c=in.read())!=-1){ 6 System.out.println((char)c); 7 } 8 } 9 } 10 /**字节输入*/
注意read()是以int形式返回下一字节,因此必须类型转换为char才能正确打印。
格式化的内存输入
要读取格式化数据,可以使用DataInputStream,它是一个面向字节的I/O类。因此我们必须使用InputStream类而不是Reader类。
1 public class FormattedMemoryInput { 2 public static void main(String[] args) throws IOException{ 3 try { 4 DataInputStream in = new DataInputStream(new ByteArrayInputStream(BufferedInputFile.read("d:FormattedMemoryInput.java").getBytes())); 5 while (true) { 6 System.out.println((char) in.readByte()); 7 } 8 }catch (EOFException e){ 9 /**当输入过程中意外到达文件或流的末尾时,抛出此异常。此异常主要被数据输入流用来表明到达流的末尾。*/ 10 System.err.println("End of stream"); 11 } 12 } 13 }
必须为ByteArrayInputStream提供字节数组,为了产生该数组String包含了一个可以实现此项工作的getBytes()方法。所产生的ByteArrayInputStream是一个适合传递给DataInputStream的InputStream。
如果我们从DataInputStream用readByte()一次一个字节地读取字符,那么任何字节地值都是合法的结果,因此返回值不能用来检测输入是否结束。。相反,我们可以使用available()方法查看还有多少可供存取的字符。
1 public class TestEOF { 2 public static void main(String[] args) throws IOException{ 3 DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("d:TestEOF.java"))); 4 while(in.available()!=0){ 5 System.out.println((char)in.readByte()); 6 } 7 } 8 }
注意,available()的工作方式会随着所读取的媒介类型不同而有所不同;字面意思就是“在没有阻塞的情况下可供读取的字节数”。对于文件,这意味着整个文件;但是对于不同类型的流,可能就不是这样的,因此要谨慎使用。
基本的文件输出
FileWrite对象可以向文件写入数据。首先,创建一个与指定文件连接的FileWrite对象。实际上,我们通常会用BufferedWriter将其包装起来用以缓冲输出。
1 public class BasicFileOutput { 2 static String file = "d:BasicFileOutput.out"; 3 public static void main(String[] args) throws IOException{ 4 BufferedReader in = new BufferedReader(new StringReader(BufferedInputFile.read("d:BasicFileOutput.java"))); 5 PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file))); 6 int lineCount = 1; 7 String s; 8 while((s=in.readLine())!=null){ 9 out.println(lineCount++ +":"+s); 10 } 11 out.close(); 12 System.out.println(BufferedInputFile.read(file)); 13 } 14 } 15 // 1:package com.qiqi.io; 16 // 2: 17 // 3:import java.io.*; 18 // 4: 19 // 5:/**6:* @ClassName: BasicFileOutput 20 // 7:* @Description: 基本的文件输出 21 // 8:* @Author huhailang 22 // 9:* @Date 2015/12/23 19:13 23 // 10:* @Company 中国奇奇科技有限公司 24 // 11:*/ 25 // 12:public class BasicFileOutput { 26 // 13: static String file = "d:BasicFileOutput.out"; 27 // 14: public static void main(String[] args) throws IOException{ 28 // 15: BufferedReader in = new BufferedReader(new StringReader(BufferedInputFile.read("d:BasicFileOutput.java"))); 29 // 16: PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file))); 30 // 17: int lineCount = 1; 31 // 18: String s; 32 // 19: while((s=in.readLine())!=null){ 33 // 20: out.println(lineCount+":"+s); 34 // 21: } 35 // 22: out.close(); 36 // 23: System.out.println(BufferedInputFile.read(file)); 37 // 24: } 38 // 25:}
一旦读完输入数据流,readLine()会返回null。我们可以看到要为out显式调用close()。如果我们不为所有的输出文件调用close(),就会发现缓冲区内容不会被刷新清空,那么它们也就不完整。
文本文件输出的快捷方式
Java SE5在PrintWriter中添加了一个辅助构造器,使得你不必在每次希望创建文本文件并向其中写入时,都去执行所有的装饰工作。下面是利用这种快捷方式重写BasicFileOutput.java。
1 public class FileOutputShortcut { 2 static String file = "d:BasicFileOutput.out"; 3 4 public static void main(String[] args) throws IOException{ 5 BufferedReader in = new BufferedReader(new StringReader(BufferedInputFile.read("d:FileOutputShortcut.java"))); 6 PrintWriter out = new PrintWriter(file); 7 int lineCount = 1; 8 String s; 9 while((s=in.readLine())!=null){ 10 System.out.println(lineCount++ +":"+s); 11 } 12 out.close(); 13 System.out.println(BufferedInputFile.read(file)); 14 } 15 }
存储和恢复数据
PrintWriter可以对数据进行格式化,以便人们阅读。但是为了输出可供另一个流恢复数据,我们需要用DataOutputStream写入数据,并用DataInputStream恢复数据。当然,这些流可以是任何形式的。
1 public class StoringAndRecoveringData { 2 public static void main(String[] args) throws IOException{ 3 DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("d:data.txt"))); 4 out.writeDouble(3.14159); 5 out.writeUTF("That was pi"); 6 out.close(); 7 DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("d:data.txt"))); 8 System.out.println(in.readDouble()); 9 System.out.println(in.readUTF()); 10 System.out.println(in.readDouble()); 11 System.out.println(in.readUTF()); 12 } 13 }
如果我们使用DataOutputStream写入数据,Java保证我们可以使用DataInputStream读取数据--无论读和写数据的平台多么不同。这一点很有价值,因为我们知道,人们曾经花费大量的时间去处理特定平台的数据问题。只要两个平台上都有Java这个问题就不会发生。
读写随机访问文件
使用RandomAccessFile,类似于组合使用了DataInputStream和DataOutputStream(因为它实现了相同的接口DataInput和DataOutput)。另外,我们可以看到,利用seek()可以在文件中到处移动,并修改文件中的某个值。
在使用RandomAccessFile,你需要知道如何排版,你才能正确的操作它。RandomAccessFile拥有读取基本类型和UTF-8字符串的各种具体方法。
1 public class UsingRandomAccessFile { 2 static String file = "d:rtest.dat"; 3 static void display() throws IOException{ 4 RandomAccessFile rf = new RandomAccessFile(file,"r"); 5 for (int i=0;i<7;i++){ 6 System.out.println("Value "+i+":"+rf.readDouble()); 7 } 8 rf.close(); 9 } 10 11 public static void main(String[] args) throws IOException{ 12 RandomAccessFile rf = new RandomAccessFile(file,"rw"); 13 for (int i=0; i<7;i++){ 14 rf.writeDouble(i*1.414); 15 } 16 rf.writeUTF("The end of the file"); 17 rf.close(); 18 display(); 19 rf = new RandomAccessFile(file ,"rw"); 20 rf.seek(5*8); 21 rf.writeDouble(47.0001); 22 rf.close(); 23 display(); 24 25 } 26 } 27 // Value 0:0.0 28 // Value 1:1.414 29 // Value 2:2.828 30 // Value 3:4.242 31 // Value 4:5.656 32 // Value 5:7.069999999999999 33 // Value 6:8.484 34 // Value 0:0.0 35 // Value 1:1.414 36 // Value 2:2.828 37 // Value 3:4.242 38 // Value 4:5.656 39 // Value 5:47.0001 40 // Value 6:8.484
文件读写的实用工具
一个很常见的程序化任务就是读取文件到内存,修改,然后再写出。Java I/O类库的问题之一就是:它需要编写相当多的代码去执行这些常用操作,没有任何基本的帮助功能可以为我们做这一切。更糟糕的是,装饰器会使得要记住如何打开文件变得一件很困难的事。因此,我们的类库中添加帮助类就显得非常有意义了,这样就可以很容易地为我们完成这些基本操作。Java SE5的PrintWriter中添加了方便的构造器,因此你可以很方便的打开一个文本文件进行写入操作。但是,还有许多其他常见操作是需要你反复执行的,这就使得消除这些与任务有关的重复代码变得非常有意义。
1 public class TextFile extends ArrayList<String>{ 2 public static String read(String filename){ 3 StringBuilder sb = new StringBuilder(); 4 try { 5 BufferedReader in = new BufferedReader(new FileReader(new File(filename).getAbsoluteFile())); 6 try{ 7 String s; 8 while((s=in.readLine())!=null){ 9 sb.append(s); 10 sb.append("\n"); 11 } 12 }catch(IOException e){ 13 throw new RuntimeException(); 14 } 15 } catch (FileNotFoundException e) { 16 e.printStackTrace(); 17 } 18 return sb.toString(); 19 } 20 public static void write(String filename,String text){ 21 try { 22 PrintWriter out = new PrintWriter(new File(filename).getAbsoluteFile()); 23 try{ 24 out.print(text); 25 }finally{ 26 out.close(); 27 } 28 } catch (FileNotFoundException e) { 29 throw new RuntimeException(); 30 } 31 } 32 //Read a file,split by any regular expression 33 public TextFile(String filename,String splitter){ 34 super(Arrays.asList(read(filename).split(splitter))); 35 if (get(0).equals("")){ 36 remove(0); 37 } 38 } 39 public TextFile(String filename){ 40 this(filename,"\n"); 41 } 42 public void write(String filename){ 43 try { 44 PrintWriter out = new PrintWriter(new File(filename).getAbsoluteFile()); 45 try{ 46 for (String item:this){ 47 System.out.println(item); 48 } 49 }finally{ 50 out.close(); 51 } 52 } catch (FileNotFoundException e) { 53 throw new RuntimeException(); 54 } 55 } 56 public static void main(String[] args) { 57 String file = read("d:TextFile.java"); 58 write("d:test.txt",file); 59 TextFile text = new TextFile("d:test.txt"); 60 write("d:test2.txt",file); 61 TreeSet<String> words = new TreeSet<String>(new TextFile("d:TextFile.java","\\w+")); 62 System.out.println(words.headSet("a")); 63 } 64 }
read()将每行添加到StringBuffer,并且为每行加上换行符,因为在读的过程中换行符会被除掉。任何打开文件的代码在finally子句中,作为防卫措施都添加了对文件的close()的调用,以确保文件会被正常关闭。PrintWriter 向文本输出流打印对象的格式化表示形式。此类实现在 PrintStream 中的所有 print 方法。它不包含用于写入原始字节的方法,对于这些字节,程序应该使用未编码的字节流进行写入。
读取二进制文件
1 public class BinaryFile { 2 public static byte[] read(String filename)throws IOException{ 3 BufferedInputStream in = new BufferedInputStream(new FileInputStream(new File(filename).getAbsoluteFile())); 4 try{ 5 byte [] data = new byte[in.available()]; 6 in.read(data); 7 return data; 8 } 9 finally{ 10 in.close(); 11 } 12 } 13 }
new File(filename).getAbsoluteFile() 返回此抽象路径名的绝对路径名形式,等同于 new File(this.getAbsolutePath())。in.available() 返回可以不受阻塞地从此输入流读取(或跳过)的估计字节数。
标准I/O
标准I/O术语参考的是Unix中“程序所使用的单一信息流”这个概念。程序的所有输入都可以来自于标准输入,它的所有输出也都可以发送到标准输出,以及所有的错误信息也都可以发送标准错误。标准的I/O意义在于:我们可以很容易的把程序串联起来,一个程序的标准输出可以成为另一个程序的标准输入。
从标准输入中读取
按照标准I/O模型,Java 提供了标准的输出和输入,System.out,System.err以及System.in,System.out,System.err已经事先被包装成了PrintStream对象。System.in是一个未加工的InputStream。
1 public class Echo { 2 public static void main(String[] args) { 3 BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 4 String s; 5 try { 6 while((s=in.readLine())!=null && s.length()!=0){ 7 System.out.println(s); 8 } 9 } catch (IOException e) { 10 e.printStackTrace(); 11 } 12 } 13 }
将System.out转换成Printwriter
1 public class ChangeSystemOut { 2 public static void main(String[] args) { 3 PrintWriter out = new PrintWriter(System.out,true); 4 out.print("hello world"); 5 } 6 }
标准I/O重定向
Java的System类提供了一些简单的静态方法调用,以允许我们对标准输入、输出和错误I/0流进行重定向:
SetIn(InputStream)
SetOut(PrintStream)
SetErr(PrintStream)
如果我们突然开始在显示器上创建大量输出,而这些输出滚动得太快以至于无法阅读,重定向输出就显得极为有用。
1 public class Redirecting { 2 public static void main(String[] args) throws IOException { 3 PrintStream console = System.out; 4 try { 5 BufferedInputStream in = new BufferedInputStream(new FileInputStream("d:Redirecting.java")); 6 PrintStream out = new PrintStream(new BufferedOutputStream(new FileOutputStream("d:Redirecting.txt"))); 7 System.setIn(in); 8 System.setOut(out); 9 System.setErr(out); 10 11 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); 12 String s; 13 while((s=br.readLine())!=null){ 14 System.out.println(s); 15 } 16 out.close(); 17 System.setOut(console); 18 } catch (FileNotFoundException e) { 19 e.printStackTrace(); 20 } 21 } 22 }
I/O重定向操纵的是字节流而不是字符流。
进程控制
你经常会需要在Java内部执行其他操作系统的程序。并且要控制这些程序的输入与输出。Java类提供了执行这些操作的类。
一项常见的任务是运行程序,并将产生的输出发送到控制台。下面示例包含了可以简化这一操作的实用工具。在使用这个实用工具时,可能会产生两种类型的错误:普通的导致异常错误,对这些错误我们只需要重新抛出一个运行时异常;以及从进程自身的执行过程中产生的错误,我们希望用单独的异常来报告这些错误:
要想运行一个程序,你需要向OSExecute.command()传递一个command字符串,它与你程序在控制台运行键入的命令相同。这个命令被传递给ProcessBuilder构造器(它要求这个命令作为一个String对象序列被传递),然后产生的ProcessBuilder对象被启动:
1 public class OSExecute { 2 public static void command(String command) { 3 boolean err = false; 4 try { 5 Process process = 6 new ProcessBuilder(command.split(" ")).start(); 7 BufferedReader results = new BufferedReader( 8 new InputStreamReader(process.getInputStream())); 9 String s; 10 while((s = results.readLine())!= null) 11 System.out.println(s); 12 BufferedReader errors = new BufferedReader( 13 new InputStreamReader(process.getErrorStream())); 14 // Report errors and return nonzero value 15 // to calling process if there are problems: 16 while((s = errors.readLine())!= null) { 17 System.err.println(s); 18 err = true; 19 } 20 } catch(Exception e) { 21 // Compensate for Windows 2000, which throws an 22 // exception for the default command line: 23 if(!command.startsWith("CMD /C")) 24 command("CMD /C " + command); 25 else 26 throw new RuntimeException(e); 27 } 28 if(err) 29 throw new OSExecuteException("Errors executing " + 30 command); 31 } 32 } ///:~
为了捕获程序执行时产生的标准输出流,你需要调用getInputStream(),这是因为InputStream是我们可以从中读取的信息的流。从程序中产生的结果每次输出一行,因此要使用readLine()来读取。这里这些行只是直接打印出来,但是你还希望从command()中捕获和返回它们。该程序的错误被发送到了标准的错误流,并且通过调用getErrorStream()得以捕获。如果存在任何错误,它们都会被打印并且被抛出OSExecuteException,因此调用程序需要处理这个问题。
新I/O
Java1.4的java.nio.*包中引入了新的Java I/O类库,其目的在于提高速度。实际上,旧的I/O包中已经使用nio重新实现过,以便充分利用这种速度提高,因此,即使我们不显式地用nio编写代码,也能从中受益。速度的提高来自于所使用的结构更接近于操作系统执行I/O的方式:通道和缓冲器。我们并没有直接和通道交互,我们只是和缓冲器交互,并把缓冲器派送到通道。通道要么从缓冲器获得数据,要么向缓冲器发送数据。
唯一直接与通道交互的缓冲器是ByteBuffer,可以用存储未加工字节的缓冲器。旧的I/O类库中有三个类被修改了,用以产生FileChannel。这三个被修改的类是FileInputStream\FileOutputStream\RandomAccessFile.这些是字节操纵流,与底层的nio性质一样。Reader和Writer这种字符模式类不能用于产生通道;但是java.nio.channels.Channels类提供了实用方法,用以在通道中产生Reader和Writer。
1 public class GetChannel { 2 private static final int BSIZE = 1024; 3 4 public static void main(String[] args) throws Exception{ 5 FileChannel fc = new FileOutputStream("d:data.txt").getChannel();//返回与此文件输出流有关的唯一 FileChannel 对象。 6 fc.write(ByteBuffer.wrap("Some txt".getBytes()));//将 byte 数组包装到缓冲区中 7 fc.close(); 8 fc = new RandomAccessFile("d:data.txt","rw").getChannel(); 9 //所返回通道的初始 java.nio.channels.FileChannel#position()position 将等于到目前为止从文件中读取的字节数, 10 //除非此流处于挂起模式,在这种情况下,它将等于文件的大小。将字节写入此流中会使通道的位置相应地增加。显式地或通过写入来更改通道的位置会更改此流的文件位置。 11 fc.position(fc.size()); 12 fc.write(ByteBuffer.wrap("Some more".getBytes())); 13 fc.close(); 14 fc = new FileInputStream("d:data.txt").getChannel(); 15 ByteBuffer buff = ByteBuffer.allocate(BSIZE); 16 fc.read(buff); 17 buff.flip(); 18 while(buff.hasRemaining()){ 19 System.out.println((char)buff.get()); 20 } 21 } 22 23 }
对于这里所展示的任何流类,.getChannel()将会产生一个FileChannel。通道是一种相当基础的东西,可以向它传送用于读写的。
ByteBuffer,并且可以锁定文件的某些区域用于独占式访问。
将字节存放于ByteBuffer的方法之一是:使用一种“put”方法直接对它们进行填充,填入一个或多个字节,或基本数据类型的值。不过,正如所见,也可以使用srap()方法将已存在的字节数组包装到ByteBuffer中。一旦如此,就不再复制底层的数组,而是把它作为所产生的ByteBuffer的存储器,我们称之为数组支持的ByteBuffer。
data.txt文件用RandomAccessFile被再次打开。注意我们可以在文件内随处移动FileChannel;在这里,我们把它移动到最后,以便附加其他的写操作。
对于只读访问,我们必须显示地使用静态的allocate()方法来分配ByteBuffer。nio的目标就是快速移动大量数据,因此ByteBuffer的大小就显得尤为重要-实际上,这里使用的1K可能比我们通常要使用的小一点。
甚至达到更高的速度也有可能,方法就是使用allocateDirect()而不是allocate(),以产生一个与操作系统有更高耦合性的直接缓冲器。但是,这个分配的开支会更大,并且具体实现也随操作系统的不同而不同,因此必须再次实际运行应用程序来查看直接缓冲是否可以使我们获得速度上的优势。
一旦调用read()来告知FileChannel向ByteBuffer存储字节,就必须调用缓冲器上的flip(),让它做好让别人读取字节的准备。如果我们打算使用缓冲器执行进一步的read()操作,我们也必须得调用clear()来为每个read()做好准备。
1 public class ChannelCopy { 2 private static final int BSIZE = 1024; 3 4 public static void main(String[] args) throws Exception{ 5 if (args.length!=2){ 6 System.out.println("arguments:sourcefile destfile"); 7 System.exit(1); 8 } 9 FileChannel in = new FileInputStream(args[0]).getChannel(), 10 out = new FileOutputStream(args[0]).getChannel(); 11 ByteBuffer buffer = ByteBuffer.allocate(BSIZE); 12 while(in.read(buffer)!=-1){ 13 buffer.flip(); 14 out.write(buffer); 15 buffer.clear(); 16 } 17 } 18 } 19 // arguments:sourcefile destfile
可以看到,打开一个FileChannel以用于读,而打开另一个以用于写。ByteBuffer被分配了空间,当FileChannel.read()返回-1时,表示我们已经到达了输入的末尾。每次read()操作之后,将会将数据输入到缓冲器中,flip()则是准备缓冲器以便它的信息可以由write()提取。write()操作之后,信息仍在缓冲器中,接着clear()操作则对所有的内部指针重新安排,以便缓冲器在另一个read()操作期间能够做好接受数据的准备。
然而,上面那个程序并不是处理此类操作的理想方式。特殊方法transerTo()和transerFrom()则允许我们将一个通道与另一个通道直接相连。
1 public class TransferTo { 2 public static void main(String[] args) throws Exception { 3 if(args.length != 2) { 4 System.out.println("arguments: sourcefile destfile"); 5 System.exit(1); 6 } 7 FileChannel 8 in = new FileInputStream(args[0]).getChannel(), 9 out = new FileOutputStream(args[1]).getChannel(); 10 in.transferTo(0, in.size(), out); 11 // Or: 12 // out.transferFrom(in, 0, in.size()); 13 } 14 } ///:~