写IO相关代码机会挺少的,但却都知道使用BufferedXXXX来读写效率高,没想到里面还有这么多陷阱,这两天突然被其中一个陷阱折腾一下:读一个文件,然后写到另外一个文件,前后两个文件居然不一样?

     解决这个问题之后,总结了几个注意点。

 

注意点一:Reader/Writer读写二进制文件是有问题的


1. public void
2. new File("E://atest//atest.txt");  
3. new File("E://btest//btest.txt");  
4. null;  
5. null;  
6. try
7. new BufferedReader(new
8. new BufferedWriter(new
9.               
10. null;  
11. while((line = in.readLine()) != null) {  
12. "/r/n");  
13.             }  
14. catch
15. // TODO: handle exception
16.             e.printStackTrace();  
17. finally
18. if(in != null) {  
19. try
20.                     in.close();  
21. catch
22. // TODO: handle exception
23.                     e.printStackTrace();  
24.                 }  
25.             }  
26.               
27. if(out != null) {  
28. try
29.                     out.close();  
30. catch
31. // TODO: handle exception
32.                     e.printStackTrace();  
33.                 }  
34.             }  
35.         }

上面代码使用BufferedReader一行一行地读取一个文件,然后使用BufferedWriter把读取到的数据写到另外一个文件中。如果 文件是ASCCII形式的,则内容还是能够正确读取的。但如果文件是二进制的,则读写后的文件与读写前是有很大区别的。当然,把上面的 readLine()换成read(char[])仍然不能正确读写二进制文件的。读写二进制文件请接着看下面注意点。

 

注意点二:read(byte[] b, int offset, int length)中的offset不是指全文件的全文,而是字节数组b的偏移量

现在已经知道使用Reader/Writer不能正确读取二进制文件,这是因为Reader/Writer是字符流,那就改用字节流ufferedInputStream/BufferedOutputStream,网上搜索到的例子大概是这样的:


1. public void
2. new File("E://atest//atest.gif");  
3. new File("E://atest//btest.gif");  
4. in = null;  
5. out = null;          
6. try
7. in = new BufferedInputStream(new
8. out = new BufferedOutputStream(new
9.               
10. byte[] b = new byte[1024];  
11. while(in.read(b) != -1) {  
12. out.write(b);  
13.             }  
14. catch
15. // TODO: handle exception
16.             e.printStackTrace();  
17. finally
18. if(in != null) {  
19. try
20. in.close();  
21. catch
22. // TODO: handle exception
23.                     e.printStackTrace();  
24.                 }  
25.             }  
26. if(out != null) {  
27. try
28. out.close();  
29. catch
30. // TODO: handle exception
31.                     e.printStackTrace();  
32.                 }  
33.             }  
34.         }  
35.     }

每次读1024字节,然后写1024字节。这看似挺正确的,但实际写出来的文件与原文件是不同的。这样就怀疑可能是读写没有接上,因而把代码改成下面的形式:

1. byte[] b = new byte[1024];  
2. int offset = 0;  
3. int length = -1;  
4. while((length = in.read(b, offset, 1024)) != -1) {  
5.                 out.write(b, offset, length);  
6.                 offset += length;  
7.             }



这是误以为:先读一段,写一段,然后改变偏移量,然后使用新的偏移量再读一段、写一段,直到文件读写完毕。但这是错误的,因为使用 BufferedXXX后,里面已经实现了这个过程。而read(byte[] b, int offset, int length)中的offset实际指的是把读到的数据存入到数组b时,从数组的哪个位置(即offset)开始放置数据;同 理,write(byte[] b, int offset, int length)就是把b中的数据,从哪个位置(offset)开始写到文件中。

 

注意点三:使用 length=read (b, 0, 1024)读数据时,应该使用write(b, 0, length)来写

第二个注意点中的第一段代码的做法虽然在网上比较常见,但是有问题的。问题在哪呢?答案是:问题在byte[] b这个数组上。由于二进制文件使用比较工具时,只知道不同、但不能知道哪些不同(是否有更先进的比较工具?)。怎样确定它的不同呢?方法很简单:就把二进 制文件改成文本文件就能看出结果了(Reader/Writer这种字符流虽然不能正确读写二进制文件,但InputStream /OutputStream这些字节流能既能正确读写二进制文件,也能正确读写文本文件)。由于使用了每次读1K(1024字节)的方式,所以会看到的结 果是:写后的文件后面多出一段,这一段的长度与原文件大小以及b数组的大小有关。为了进一步确定是什么关系,把读的文件内容改 为"1234567890123",而把b数组的大小改为10字节,这时结果就出来了:写后的文件内容变 成"12345678901234567890",就是读了两遍。多出的内容的根源在这里:b数组的大小是10字节,而要读的内容长度是13字节,那就要 读两次,第一次读了前10字节,此时b数组内的元素为前10个字符;再读第二次时,由于可读内容只有3个字符,那b数组的内容只有前3个字符被改变了,后 面7个字符仍然保持上一次读取的内容。所以直接采用write(b)的方式,在第二次写文件时,内容就多写了一段不是第二次读取到的内容。

下面是正确的读写(即每次读了多少内容,写入的是多少内容,而不是写入整个数组):


1. public void
2. new File("E://atest//atest.txt");  
3. new File("E://btest//btest.txt");  
4. null;  
5. null;  
6. try
7. new BufferedInputStream(new
8. new BufferedOutputStream(new
9.               
10. int len = -1;  
11. byte[] b = new byte[10];  
12. while((len = in.read(b)) != -1) {  
13. 0, len);  
14.             }  
15. catch
16. // TODO: handle exception
17.             e.printStackTrace();  
18. finally
19. if(in != null) {  
20. try
21.                     in.close();  
22. catch
23. // TODO: handle exception
24.                     e.printStackTrace();  
25.                 }  
26.             }  
27. if(out != null) {  
28. try
29.                     out.close();  
30. catch
31. // TODO: handle exception
32.                     e.printStackTrace();  
33.                 }  
34.             }  
35.         }  
36.     }


 

注意点四:flush()和close()

flush()是把写缓冲区内的内容全部”吐“到文件上,如果没有它,就有可能很多内容还存在于写缓冲区内,而不是在文件中,也就是还有丢失的可能。

close()中会调用flush()。它是文件真正完成的标志,文件内容写完成后不关闭文件流,会导致一些”古怪“的问题。这个在网络中的流更能体现。

所以,写文件完成后注意关闭文件读写流。