最近写代码遇到读取文件和加载文件的问题,搞了半天还是不咋懂,感觉还是自己Java的IO基础太薄弱了,现在重新回顾下,整理笔记如下:
第一节
所谓输入输出流就是把数据从文件中读取到程序中的某个对象中去,然后经过我们的程序处理后再写入到某个文件中去的过程。当然数据的来源和去处并不一定是文件,可能是网络,可能是屏幕等等。但过程就是这么个过程。那就产生了两个问题:
1. 怎么把数据读取到程序中来,读取进来后放在哪?
2. 怎么把存放在程序中的数据写入到目标位置去?
带着这两个问题就进入今天的主题:IO(in & out)。
概念扫盲
- 字节流:以字节为传输的基本单位
- 字符流:以字符为传输的基本单位
- 节点流:数据流的处理方式(不处理)
- 处理流:数据流的处理方式
注意: 输入流并不是真正的数据内容,而是一种将文件中的数据(或其他方式输入的数据)读取到内存中某个对象中的工具。其本质上是工具,而不是数据本身。输出流同理。
核心类
字节流:
InputStream–FileInputStream
OutputStream–FileOutputStream
字符流:
Reader–FileReader
Writer–FileWriter
完整继承图:
一个典型的输入流例子:
class Test{
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("d:/test/from.txt");
byte[] buffer = new byte[100];
//read()第二个参数为偏移量,指前面空出多少位
//read()第三个参数是每次读取的数据长度
//最大值为数组的长度减去偏移量,否则越界
fis.read(buffer,5,10);
for(int i = 0;i<buffer.length;i++){
System.out.println(buffer[i]);
}
}catch(Exception e) {
System.out.println(e);
}
}
}
看完这个例子应该就明白了,数据存在数组中,由工具类读取。工具类首先生成一个绑定特定文件的对象,然后调用读方法,将数据读取到内存中的数组对象中去。
注意:以上例子有个问题,如果文件只有4个字节,而buffer有100个字节,这将导致buffer其余96个字节全部为0,这是一种空间上的浪费。如果能提前知道文件大小就好了(这个下面再讲)。
写入数据和上面的过程差不多,核心代码如下:
FileOutputStream fos = new FileOutputStream("d:/test/to.txt");
fos.write(buffer,0,fileLenght);
理解read和write的参数
- offset是针对buffer而言的。
对read来讲,offset = 5 等于将buffer的前5位写为0,然后从第六位(数组下表的第五位)开始写真正的数据。
对write来讲,offset = 5 等于将buffer的前5位不写进文件,从第六位(数组下标的第五位)开始对文件注入数据。 - 第三个参数length
对read来讲,该参数代表要从文件中读取多少个字节到buffer中去,该长度不能超过buffer的长度。而buffer中未写入数据的部分元素将自动为0。但是该长度必须大于等于文件长度,否则文件的读取就不完整,而超出的部分自动为0. 但是这个0不是从文件中读取到的,而是从文件中读取不到,那么在内存中编译器自动为其分配0.
对write来讲,该参数代表一共要写出多少字节。 - read返回值
该返回值是buffer中非空数据的长度。如果read有offset,数据为0000123000。。。
则read返回值为3.
另外要注意,写入数据的时候不管目标文件现在存的是什么都将被写入操作覆盖
tips:
- 使用这种方法读取文件最好不要设置offset
- 使用这种字节流读取文件中如果含中文会产生乱码
第二节
对于上面的写法,有一个bug就是必须得事先知道要读取的文件有多长,否则不知道buffer设置多大。现在改用这种写法:
class Test{
public static void main(String[] args) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream("d:/test/from.txt");
fos = new FileOutputStream("d:/test/to.txt");
byte[] buffer = new byte[1024];//每次读取1024byte
while(true){
int a = fis.read(buffer,0,1024);
if (a == -1) {
break;
}
fos.write(buffer,0,a);
}
}catch(Exception e) {
System.out.println(e);
}finally{
try{
fis.close();
fos.close();
} catch (Exception e){
System.out.println(e);
}
}
}
}
注意:write第三个参数一定要写a,否则最后一次读取会出问题:
如果最后一次只读取到了500个字节,则read方法将会改写buffer的前500个元素,后524个元素还是上一次读取的数据,并没有被覆盖,此时若还写入1024个数据,则会将上一次已经写入过的后524个数据重写写一遍,这不是我们想要看到的。
所以读和写的配合原则一定要是读多少,写多少。
上面介绍的都是字节流的读取方法,字符流的读取方法几乎一样,在此不赘述
到此,基本的读取文件和写入文件的过程就比较明了了。
第三节
处理流
处理流使用了设计模式中的装饰者模式,就是对现有的东西进行加工。作用和继承是一样的,但是装饰者模式能节省大量代码,具体请看另一篇文章。
下面介绍一种处理流(BufferedReader):
常用方法:public String readLine() throws IOException
实例:
class Test2{
public static void main(String[] args) {
FileReader fr = null;
FileWriter fw = null;
BufferedReader br = null;
BufferedWriter bw = null;
try {
fr = new FileReader("d:/test/from.txt");
br = new BufferedReader(fr);
fw = new FileWriter("d:/test/to.txt");
bw = new BufferedWriter(fw);
String line = null;
while(true){
line = br.readLine();
if (line == null) {
break;
}
System.out.println(line);
//flush一下,否则最后一次无法写入数据
bw.flush();
bw.write(line,0,line.length());
}
} catch(Exception e){
System.out.println(e);
} finally{
try{
//fr.close();
//fw.close();
br.close();
bw.close();
} catch(Exception e){
System.out.println(e);
}
}
}
}
对于flush()方法的解释:
- IO就好比将A桶的水通过输入流抽取到程序中来,加入糖,变成糖水,再通过输出流输出到B桶中去的过程。我们上面说了,输入输出是有基本单位的,不可能一个分子一个分子的取,最有可能的是一勺一勺的取。这样的情况下,输入是没问题的,输出就有问题了:当最后剩下的一点不够一勺时,write方法不会执行,因为缓冲区(勺子)还没满,这个时候就需要使用flush()方法强行将这剩下的一点写入到B桶中去。
关于上面最后的关闭方法说一点:
1. 先关外部,再关内部–没问题
2. 先关内部,再关外部–抛出异常
3. 只关外部–没问题
4. 只关内部–没问题