沉淀再出发:java的文件读写
一、前言
对于java的文件读写是我们必须使用的一项基本技能,因此了解其中的原理,字节流和字符流的本质有着重要的意义。
二、java中的I/O操作
2.1、文件读写的本质
概念框架:
1 方式: 字节流 Byte 和 字符流 Char 2 方向: 输入 Input 和 输出 Output ; 读 Reader 和 写 Writer 3 源: 字符串 String, 数组 Array, 对象 Object, 文件 File, 通道 Channel, 管道 Pipe, 过滤器 Filter,控制台 Console, 网络 Network Link ;
一切可产生/接收数据的 Data 4 性能: 带缓冲和不带缓冲的 5 行为: Readable, Appendable, Closable, Flushable, Serializable/Externalizable
概念组合:
方式、方向、源、行为四个维度可以组合出各种特性的IO流。
JavaIO框架采用装饰器模式,可以在现有IO流的基础上添加新的IO特性,生成更强功能的IO流,体现了读写能力的可扩展性。 比如要构造一个带缓冲的文本文件读取流:
File -> FileInputStream -> InputStreamReader -> FileReader -> BufferedFileReader,
其中文本:字符流, 文件:源, 读:输入方向, 带缓冲的:性能。 读写耗时的字符输入输出流可以使用缓冲来提升性能,比如文件读写、网络流读写等。
这是由于每个待读写的字符都要经过读写字节、字节/字符转换两个操作。
缓冲实际上是将大量单个的读写操作转换为一个大的单个的批量读写操作。
字节流:
1 字节输入流:InputStream, BufferedInputStream, FileInputStream, ByteArrayInputStream, PipedInputStream, FilterInputStream, DataInputStream, ObjectInputStream; 2 字节输出流: OuputStream, BufferedOuputStream, FileOuputStream, ByteArrayOuputStream, PipedOuputStream, FilterOuputStream, DataOuputStream, ObjectOuputStream, PrintStream;
字符流:
1 字符输入流: Reader, BufferedReader, FileReader, CharArrayReader, PipedReader, FilterReader, StringReader, LineNumberReader; 2 字符输出流: Writer, BufferedWriter, FileWriter, CharArrayWriter, PipedWriter, FilterWriter, StringWriter, PrintWriter;
字节流与字符流之间的转化:
1 InputStreamReader:输入字节流转化为输入字符流 2 OutStreamWriter: 输出字符流转化为输出字节流
装饰器模式:
多个类实现同一个接口,并且这些实现类均持有一个该接口的引用,通过构造器传入,从而可以在这些实现类的基础上任意动态地组合叠加,构造出所需特性的实现来。装饰器模式可以实现数学公式/逻辑公式运算函数。
实现:
在IO流的实现类中,通常有一个 Char[],Byte[], CharBuffer, StringBuffer, ByteBuffer的成员数组或成员对象。将成员数组/对象里的数据写到方法里给定的参数数组或返回值,即为读实现;将方法里给定的参数数组写到成员数组或成员对象里,即为写实现。读写数据本质上就是数据在不同源之间的拷贝实现。
IO流通常是会实现读写单个字节/字符、读写字节数组/字符数组、跳过若干字节/字符的功能。成员 (next,pos,mark,count) 用于标识数据读写位置;mark 与 reset 方法组合可实现重复读写。
1 要实现一个自定义的 Reader, 可参考 StringReader 实现;StringReader 可以将字符串作为字符流来读取,内部成员为String;
而 StringWriter 内部成员为线程安全的 StringBuffer。两者均要考虑并发读写的问题,使用一个 Object 锁进行互斥访问。 2 FileReader 是文本文件读取流,通过继承 InputStreamReader, 转化 FileInputStream 而得。
因文件是对磁盘字节数据的抽象,因此要获得人类可读的文本,必然存在从字节流到字符流的转换。 3 InputStreamReader 使用 StreamDecoder 将字节流转换为指定字符编码的字符流,后续读操作直接委托 StreamDecoder 读取;
StreamDecoder 是一个字节解码器, 是 Reader 实现类。
OutputStreamWriter 使用 StreamEncoder 将字符流转换为指定字符编码的字节流,
后续写操作直接委托 StreamEncoder 写入底层 OutputStream;StreamEncoder 是一个 Writer 实现类。 4 FilterReader, FilterWriter, FilterInputStream, FilterOutputStream: IO流的抽象类,分别持有一个[Reader,Writer,InputStream, OutputStream],
自定义 IO 流可以通过继承 FilterXXX 来实现。CommonIO库里 ProxyReader, ProxyWriter 分别提供了 Reader, Writer 的一个示例实现,
java.io.DataInputStream 提供了 FilterInputStream 的一个示例实现,java.io.PrintStream 提供了 FilterOutputStream 的一个示例实现。 5 BufferedReader: 提供了缓冲读及读行的功能。
BufferedReader 主要成员为 [Reader in, char[defaultsize=8192] cb, int nextChar, int nChars, int markedChar];
nextChar 指示将要读取的字符位置, nChars 指示可以读取字符的终止位置,
markedChar 表示被标记的将要读取字符的位置;
BufferedReader 从字符流 in 中读取字符预存入缓冲字符数组 cb 中,然后在需要的时候从 cb 中读取字符。
BufferedReader 的实现由 fill 和 read, readLine 共同完成。
当缓冲字符数组的字符数不足于填充要读取的参数数组时,就会使用 fill 方法从输入流 in 中读取数据再进行填充;6 PipedReader: 管道输入流,必须从 PipedWriter 中读取。
底层实现有两个线程、两个位置索引以及一个字符数组。
两个线程分别是读线程 readSide 和 写线程 writeSide, 两个位置索引分别用于指示字符数组中将要写的位置和将要读的位置。
字符数组是一个循环队列,默认大小为 1024 chars 。初始化时必须将 PipedReader 的输入连接至 PipedWriter 。
读实现方法中,即是将底层字符数组拷贝到方法中的参数数组。PipedWriter 的实现相对简单,其输出连接至 PipedReader;
PipedWriter 的写就是 PipedReader 的读,因此 PipedWriter 的方法实现委托给 PipedReader 引用的方法。
文件:
1 文件相关的类: File, RandomAccessFile, FileDescriptor, FileChannel, FilePermission, FilePermissionCollection, FileFilter, FileSystem, UnixFileSystem ; 2 文件 File: 唯一性路径标识【目录路径/文件名[.文件扩展名]】、文件元信息【描述符、类型、大小、创建时间、最近访问时间、节点信息】、文件访问权限【读/写/执行组合】等;
File 的操作会先使用 SecurityManager 检查访问权限,然后通过 FileSystem 判断和操作。 3 构造可以实际读写的文件输入输出流需要使用 FileInputStream, FileOutputStream, FileReader, FileWriter, BufferedReader, BufferedWriter, BufferedInputStream, BufferedOutputStream 来包装 File ,
比如 new BufferedReader(new FileReader(new File(filename))) 。 4 FileChannel: 操作文件内容的通道;FileChannel 是线程安全的,使用 java.nio 引入的高性能机制来实现。 5 文件描述符 FileDescriptor : 用于操作打开的文件、网络socket、字节流的唯一标识句柄;
其成员是一个整型数 fd 和一个原子计数器 AtomicInteger useCount ;
标准输入流 System.in.fd = 0 , 标准输出流 System.out.fd = 1 , 标准错误流 System.err.fd = 2 6 文件访问权限 FilePermission: 提供了【文件可进行的操作 actions 与位掩码表示 mask 】之间的转化方法。
文件可进行的操作 actions 在类 SecurityConstants 的常量中定义;
mask 是一个整型数,表示文件的执行(0x01)/写(0x02)/读(0x04)/删(0x08)/读链接(0x10)/无(0x00) 等权限组合。 7 FileSystem:所使用的操作系统平台的文件系统。
指明该文件系统所使用的【文件分隔符、路径分隔符、路径标准化、路径解析、文件访问安全控制、文件元信息访问、文件操作等】。
文件分割符是指路径名中分割目录的符号,linux = '/', windows = '\' ;
路径分割符是环境变量中分割多个路径的符号,linux = ':' , windows = ';' ;
UnixFileSystem 提供了 FileSystem 的一个实现示例。
枚举:
存在 BA_EXISTS = 0x01;
普通文件 BA_REGULAR = 0x02;
目录 BA_DIRECTORY = 0x04;
隐藏 BA_HIDDEN = 0x08;
可读 ACCESS_READ = 0x04;
可写 ACCESS_WRITE = 0x02;
可执行 ACCESS_EXECUTE = 0x01;
判断文件属性及访问权限采用与或非位运算实现, 优点是可以灵活组合。
序列化:
序列化是实现内存数据持久化的机制。可以将 Java 对象数据存储到文件中,或者将文件中的数据恢复成Java对象。Java RPC, Dubbo,ORM 等都依赖于序列化机制。 序列化相关的类及方法: Serializable/Externalizable, ObjectInputStream, ObjectOutputStream, readObject, writeObject 。 使用 ObjectInputStream.readObject, ObjectOutputStream.writeObject 方法进行对象序列化:
JDK提供的标准对象序列化方式;对象必须实现 Serializable 接口;
适用于任何 Java 对象图的序列化,通用的序列化方案;输出为可读性差的二进制文件格式,很难为人所读懂,无法进行编辑;
对某些依赖于底层实现的组件,存在版本兼容和可移植性问题;只有基于 Java 的应用程序可以访问序列化数据; 使用 XMLDecoder.readObject, XMLEncoder.writeObject 方法进行对象序列化:
对象是普通的 javaBean,无需实现 Serializable 接口;输出为可读性良好的XML文件,容易编辑和二次处理;
其它平台亦可访问序列化数据;可以修改数据成员的内部结构的实现,只要保留原有bean属性及setter/getter签名,就不影响序列化和反序列化;
可以解决版本兼容的问题,提供长期持久化的能力;每个需要持久化的数据成员都必须有一个 Java bean 属性。 实现 Externalizable 接口来进行对象序列化。
需在对象中实现 readObject, writeObject 方法,提供更高的序列化灵活性,由开发者自行决定实例化哪些字段、何种顺序、输出格式等细节。
NIO:
java.nio 为 javaIO 体系引入了更好的性能改善,主要是 Buffer, Channel 和 file 相关的支撑类。
由于 java.io 类已经引用了 nio 里的类,因此直接使用 java.io 里的类就可以获得 nio 带来的好处了。
2.2、案例分析
首先我们看一个例子:
字节流读写测试,可以发现如果预先规定了Byte对象的大小,就会产生乱码:
1 package com.io.test; 2 3 import java.io.File; 4 import java.io.FileInputStream; 5 import java.io.FileNotFoundException; 6 import java.io.FileOutputStream; 7 import java.io.IOException; 8 import java.io.InputStream; 9 import java.io.OutputStream; 10 11 public class ByteTest { 12 13 public static void main(String[] args) { 14 15 // 第一种方式读文件,因为方法throws了异常,所以在这要捕获异常 16 try { 17 ByteTest.readFromFileByByte(); 18 } catch (FileNotFoundException e) { 19 e.printStackTrace(); 20 System.out.println("找不到文件啊"); 21 } catch (IOException e) { 22 e.printStackTrace(); 23 System.out.println("读不成功啊!"); 24 } 25 26 System.out.println("==========================="); 27 28 // 第二种方式读文件 29 try { 30 ByteTest.readFromFileByteTwo(); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 System.out.println("还是读不成功啊!"); 34 } 35 System.out.println("==========================="); 36 ByteTest.WriteToFile(); 37 } 38 39 /** 40 * 第一种方法读文件 通过字节流读取文件中的数据 41 * 42 * @throws IOException 43 */ 44 public static void readFromFileByByte() throws IOException { 45 File file = new File("abc.txt"); 46 // 如果文件不存在则创建文件 47 if (!file.exists()) { 48 file.createNewFile(); 49 } 50 InputStream inputStream = new FileInputStream(file); 51 // 这里定义了数组的长度是1024个字节,如果文件超出这字节,就会溢出,结果就是读不到1024字节以后的东西 52 byte[] bs = new byte[2048]; 53 // 这里len获得的是文件中内容的长度 54 int len = inputStream.read(bs); 55 inputStream.close(); 56 System.out.println(len+new String(bs)); 57 } 58 59 /** 60 * 第二种方法读文件 通过字节流读取文件中的数据 61 * 62 * @throws IOException 63 */ 64 public static void readFromFileByteTwo() throws IOException { 65 // 注意这里的不同,File.separator是分隔符,这里指明绝对路径,即D盘根目录下的abc.txt文件 66 File file = new File("d:" + File.separator + "abc.txt"); 67 // 如果文件不存在则创建文件 68 if (!file.exists()) { 69 file.createNewFile(); 70 } 71 InputStream inputStream = new FileInputStream(file); 72 // 这里也有不同,可以根据文件的大小来声明byte数组的大小,确保能把文件读完 73 byte[] bs = new byte[(int) file.length()]; 74 // read()方法每次只能读一个byte的内容 75 inputStream.read(bs); 76 inputStream.close(); 77 System.out.println(new String(bs)); 78 } 79 80 public static void WriteToFile() { 81 File file = new File("D:" + File.separator + "write.txt"); 82 OutputStream outputStream = null; 83 if (!file.exists()) { 84 try { 85 // 如果文件找不到,就new一个 86 file.createNewFile(); 87 } catch (IOException e) { 88 e.printStackTrace(); 89 } 90 } 91 try { 92 // 定义输出流,写入文件的流 93 outputStream = new FileOutputStream(file); 94 } catch (FileNotFoundException e) { 95 e.printStackTrace(); 96 } 97 // 定义将要写入文件的数据 98 String string = "Hell Java, Hello World, 你好,世界!"; 99 // 把string转换成byte型的,并存放在数组中 100 byte[] bs = string.getBytes(); 101 try { 102 // 写入bs中的数据到file中 103 outputStream.write(bs); 104 outputStream.close(); 105 } catch (IOException e) { 106 e.printStackTrace(); 107 } 108 109 // =================到此,文件的写入已经完成了! 110 // 如果想在文件后面追加内容的话,用下面的方法 111 OutputStream outToFileEnd = null; 112 try { 113 outToFileEnd = new FileOutputStream(file, true); 114 String string2 = "Here I come!!"; 115 byte[] bs2 = string2.getBytes(); 116 outToFileEnd.write(bs2); 117 outToFileEnd.close(); 118 } catch (FileNotFoundException e) { 119 e.printStackTrace(); 120 } catch (IOException ex) { 121 ex.printStackTrace(); 122 } 123 } 124 }
字符流的读写,同样的对于Byte对象的大小设定非常重要,同时写入的时候并没有识别出换行符:
1 package com.io.test; 2 3 import java.io.BufferedReader; 4 import java.io.BufferedWriter; 5 import java.io.File; 6 import java.io.FileNotFoundException; 7 import java.io.FileReader; 8 import java.io.FileWriter; 9 import java.io.IOException; 10 import java.io.Reader; 11 import java.io.Writer; 12 13 public class CharTest { 14 15 public static void main(String[] args) throws IOException { 16 17 File file1 = new File("D:" + File.separator + "test1.txt"); 18 File file2 = new File("D:" + File.separator + "test2.txt"); 19 Writer writer = new FileWriter(file1); 20 Writer writer1 = new FileWriter(file2, true); 21 String string = "今天是教师节!"; 22 writer.write(string); 23 String string2 = "祝愿所有的老师教师节快乐!"; 24 writer1.write(string); 25 writer1.write(string2); 26 // 在这一定要记得关闭流 27 writer.close(); 28 writer1.close(); 29 30 ReadFromFile(); 31 ReadFromFile2(); 32 } 33 34 public static void ReadFromFile() throws IOException { 35 File file = new File("d:" + File.separator + "test1.txt"); 36 Reader reader = new FileReader(file); 37 char[] cs = new char[1024]; 38 // 上面定义了一个大小为1024的char型数组,如果文件内容过大,程序就会报错,而不是只读到1024的大小 39 reader.read(cs, 0, (int) file.length()); 40 System.out.println(cs); 41 reader.close(); 42 } 43 44 public static void ReadFromFile2() throws IOException { 45 try { 46 // 声明一个可变长的stringBuffer对象 47 StringBuffer sb = new StringBuffer(""); 48 49 Reader reader = new FileReader("d:" + File.separator + "test1.txt"); 50 // 这里我们用到了字符操作的BufferedReader类 51 BufferedReader bufferedReader = new BufferedReader(reader); 52 String string = null; 53 // 按行读取,结束的判断是是否为null,按字节或者字符读取时结束的标志是-1 54 while ((string = bufferedReader.readLine()) != null) { 55 // 这里我们用到了StringBuffer的append方法,这个比string的“+”要高效 56 sb.append(string + "/n"); 57 System.out.println(string); 58 } 59 // 注意这两个关闭的顺序 60 bufferedReader.close(); 61 reader.close(); 62 63 /* 64 * 完整写入文件 65 */ 66 Writer writer = new FileWriter("d:" + File.separator + "test3.txt"); 67 BufferedWriter bw = new BufferedWriter(writer); 68 // 注意这里调用了toString方法 69 bw.write(sb.toString()); 70 // 注意这两个关闭的顺序 71 bw.close(); 72 writer.close(); 73 74 } catch (FileNotFoundException e) { 75 e.printStackTrace(); 76 } catch (IOException e) { 77 e.printStackTrace(); 78 } 79 } 80 81 }