文章目录

  • 基本使用思路
  • 代码实战
  • 简单的文本文件处理
  • 文件复制
  • 对象的序列化与反序列化
  • 总结


基本使用思路

当很多人学到IO的时候都特别懵,这也难怪,毕竟关于IO有各种流,记都要记糊涂了。其实只要换一个思维角度来看待IO流,还是不难的,甚至是非常容易和方便的,至少平常的应用不难。更深层次、更底层或者更高级的咱暂且不谈,这篇文章只介绍最基本的运用,让新手能熟悉得将IO流用到自己的项目中(其实不讲高级的原因是我不会(●′ω`●))

贴上代码之前咱们先捋一下IO的使用思路,为啥新手懵,因为流对象太多,开发时不知道用哪个对象合适,只要理清思路即可知道该使用哪些对象

IO,分为Input和Output,输入和输出。将硬盘上的文件读取到内存里来,为输入;将内存中的数据存储到硬盘上的文件里去,为输出。(这里能够处理的数据不光只有硬盘,还有其他设备,比如键盘或者网络流,但是咱们最常处理的就是硬盘上的文件,硬盘上的文件会操作之后,其他设备自然就会了)
无论输入还是输出,流程线是完全一致的,只是顺序不同
输入:拿到文件中的数据 >>> 开始读取数据 >>> 将数据“转移”到内存中去 >>> 在内存中操作数据
输出:拿到内存中的数据 >>> 开始读取数据 >>> 将数据“转移”到文件中去 >>> 文件中保存了数据

所以,在使用IO流前,你先得确认第一个问题:明确数据源和目的地,即明确数据流的方向

明确数据流方向之后,咱们就得确认第二个问题:我要处理的是什么数据。 咱们处理的数据可以分为两大类:文本非文本。 文本需要处理的数据为字符,非文本需要处理的数据为字节

要处理非文本数据(字节)就用:
(输入)InputStream,(输出)OutputStream
要处理文本数据(字符)就用:
(输入)Reader,(输出)Writer

OK,这两个问题确认好后,基本上就知道要用哪个对象了。之前也说了,数据不光只有硬盘上的文件,其实还有其他设备,但是为了方便大家理解咱们就以硬盘上的文件来操作。 既然要操作文件,那就肯定要用到File,流也要用处理File设备的流,即:
(输入)FileInputStream,(输出)FileOutputStream
(输入)FileReader,(输出)FileWriter

不管是什么流,其中的方法基本都是一致的,输入数据就用 read(),输出数据就用 write(),一定要记住这一点哦,为啥java这么流行,就是因为不管你操作的是啥数据、啥设备、啥流,处理方式都是一样的:

  1. 明确数据源和目的地
  2. 需要处理的数据类型
  3. 再确认要处理的设备(一般是File,这里也只用File举例)

代码实战

注意哈,为了方便演示,代码中就没有写上 异常的捕捉和声明,正常使用的过程中是需要进行异常处理的!

简单的文本文件处理

好了咱们现在来实战,我要读取一个文本文件里的文字到我的内存中,该怎么操作?
文本文件,那就是FileReader或者FileWriter呗,读到内存里,那就是FileReader呗,看到没,该使用什么对象立马就确定好了

/*假设现在硬盘有一个文件为1.txt
内容为:哈哈哈哈哈哈*/

// 首先咱们开始得创建一个FileReader对象
// 流创建的同时你肯定也要指定操作哪个东西嘛,所以
// 文件流的对象参数里自然就是放的File对象,或者文件路径,这里放的是文件路径
FileReader fr = new FileReader("src/1.txt");

// 还记得之前说的嘛,输入就用read()方法,所以这里咱就要调用read方法来开始读数据了
// 因为是文本文件,所以处理的是字符,read()方法则每次读取都是读的一个字符

// read()方法返回值是字符的字符编码,即int类型,所以声明一个int变量来接受
int len; 
// 既然要读文本,自然就创建一个字符串来接受文件中的文本
String str = "";

// 开始循环读取数据,如果read()的返回值是-1,就代表没有内容可读取了,所以循环的判断条件即不为-1
while((len = fr.read()) != -1) {
	// 每读一次,就将读取到的字符编码转换成字符存到我们刚才的字符串里
	str += (char)len;
}
// 流都操作完了,就不要留着占资源了嘛,记得每次用完关掉流哦
fr.close();
// 循环完毕了,就代表所有文本已经读取完毕了,打印即可
System.out.println(str);

刚才的代码咋一看很复杂,其实内容非常简单,可以回顾一下之前说的流程线:
拿到数据 >>> 读取数据 >>> 操作数据,即
创建流对象 >>> 用read()方法读取数据 >>> 打印字符串

输入流过了一遍,咱再过一下输出流。我要将一个字符串输出到一个文本文件里,该怎么操作?
文本文件,那就是FileReader或者FileWriter呗,输出到文件里,那就是FileWriter呗:

// 老套路,创建一个流对象,流对象参数里放上文件路径
FileWriter fw = new FileWriter("src/1.txt");
// 记得之前说的嘛,输出用write()方法
fw.write("嘿嘿嘿嘿");
/*为啥这里不用循环呢,因为直接将要输出的所有数据都一次性给流了,read()是一个字符一个字符读,自然要用循环*/

// 输出完了,记得关闭流
fw.close();

/*这时候文件里的文本内容就变成了“嘿嘿嘿嘿”,要注意哦,这里输出是会覆盖原文件的文本的*/

看到没,三句话搞定,完全对应了流程线,是不是简单的一批?
拿到数据 >>> 输出数据 >>> 保存数据,即
创建流对象 >>> 用write()方法输出数据 >>> 文件内容已被覆盖

注意哈,上面我演示的是非常简单的输入和输出方法,运行性能也并不是特别好,但是先掌握这个,咱们慢慢来加难度。

文件复制

刚才咱们处理的是文本文件,那么如何处理非文本文件呢? 非文本文件,咱们就从文件的复制来开始入手。复制这个功能,肯定要将文件A的数据,转移到文件B(这个文件B是要自己创建),这代表既要输入又要输出,所以(输入)FileInputStream,(输出)FileOutputStream两个对象都要创建。

// 先创建一个文件读取流,流对象参数里放上需要复制的文件的路径
FileInputStream fis = new FileInputStream("src/1.gif");
// 再创建一个文件输出流,流对象参数里放上目标文件路径
// 文件输出的时候,如果没有该文件,则会自动创建文件(注意,读取的时候可不行,输入的源文件必须存在,否则报错)
FileOutputStream fos = new FileOutputStream("src/2.gif");
// 之前说过,字符流处理的数据是字符,字节流是字节,之前字符流的read()方法读取的是一个字符,那字节流的read()方法自然就是字节了
// 字节流read()方法返回的字节数据,即int类型,所以创建一个变量来接收
int len;
// 开始循环读取数据,操作方式和流程和字符类是一样的
while((len = fis.read()) != -1) {
	// 每读取到一个字节,就写到目标文件中去
	fos.write(len);
}
// 关闭流
fis.close();
fos.close();

就算创建了两个流对象,但是操作流程还是一样地简单:
拿到数据 >>> 输出数据 >>> 保存数据,即
创建流对象 >>> 读数据 >>> 写数据

在这里基本上就能印证之前的思路了:不管IO你要处理啥,怎样处理,本质的操作都是一样的!就算业务复杂的一批,无非就是 先读数据,再写(操作)数据
一定要记住这个基本的思路,思路解决后,咱们再来进行优化和进步!

上面复制文件的代码,虽然功能是可以完成,但是性能太慢太慢了!它是一个字节字节读取然后写入的,大家都知道,内存的读写速度要比硬盘的速度快得多!上面代码操作呢,完全就是在硬盘里进行读写,就算复制一个1MB的文件,只怕也要十几秒。所以上面的方式,只是为了让大家了解基本的操作,但是在实际运用中是不会这么用的。现在就介绍一下比较常用的方法来优化,下面代码要仔细看一下注释:

// 这个肯定是不变的,创建输入和输出流
FileInputStream fis = new FileInputStream("src/1.gif");
FileOutputStream fos = new FileOutputStream("src/2.gif");
// read()的返回值一直是int,所以得创建一个变量来接受,这个也不会变
int len;
// 这里是重点,为啥要创建一个字节数组呢,因为read()方法里面其实可以放参数,就可以放字节数组
// read()参数里放了字节数组后,就代表着将读取的数据全部先存放到数组里
// 说白了就是创建数组用来存读取的数据,也就是经常说的缓存,数组的初始化大小有多大,就代表能存多少数据
byte[] buf = new byte[1024];
// 开始循环读取数据到缓存数组里(这里就和之前有一点不同,多了一个参数)
// 这里返回值是还是int类型,之前返回的是一个字节数据,加了参数后,返回的就是输入的数据长度
while((len = fis.read(buf)) != -1) {
	// 这里也是重点!write也可以放参数,之前放的是字节数据,当然也可以放字节数组
	// 参数第一个代表要写的数据数组,第二个和第三个参数代表要写的长度,从0开始写,写到结尾
	fos.write(buf,0,len);
}
// 关闭流
fis.close();
fos.close();

这种代码是比较常用的,运行速度比之前的代码快了很多很多,最重要的就是加入了一个缓存数组。之前代码是一个字节一个字节往硬盘里写,现在代码就是,先将内存里的缓存存满,然后再将缓存里的数据一次性给存入到硬盘里,说白了,就是读写硬盘的次数变少了,读写内存的次数变多了,自然而然速度就快了。
大家不要懵,一开始我就是在这里挺懵的,为啥好端端加个数组我开始完全弄不明白,在这里我举个例子大家就会清楚为什么了:
就好像在超市里购物,如果你看中一样东西,就立马得把那个东西拿到收银台先放着,然后再继续购物,又看中一个东西,又得跑到收银台放着,循环往复,最后再结账,这样是不是慢的一批。这个收银台就相当于硬盘,超市里的物品就相当于内存中的数据。而缓存是啥呢,就是购物车!有了购物车之后,你就能在超市里购物时,看中一个东西了,先放到购物车里然后再继续选购,直到你选购完毕再推着购物车里去收银台结账,这样效率就高多了!
这就是为什么要用到缓存机制了!在代码里,那个字节数组buf就相当于是购物车,先存够一定的数据,再跑到“硬盘”那里去“结账”。

对象的序列化与反序列化

谈到Java,就肯定要谈到面向对象,那么问题就来了,对象这种东西,又不是文本我该怎样去保存对象数据到文件里呢? Java当然贴心的为你提供了解决的方案:那就是对象的序列化。在这里,我只讲怎样用IO实现序列化,至于序列化的一些细节等我以后单独写一篇文章再说。

首先,咱们弄清楚一下序列化的定义,我看到有些同学在网上查询序列化相关的知识,越查越懵。其实懵是因为 在没有掌握基本的使用方法,却去了解使用原理,这样是绝对会懵的
序列化,说白了就是将对象保存到本地文件上,反序列化,说白了就是将本地文件上的对象数据,读取出来到内存里:
序列化: 对象数据 >>> 本地文件
反序列化:本地文件 >>> 对象数据
是不是和IO的操作没啥区别,事实也确实如此,就是没啥本质的区别,只是因为要处理的是对象数据,所有就要用到序列化相关的流。在介绍序列化流前呢,咱们还是按照之前的思路来走一遍:
现在我要将对象数据存到本地文件里,对象数据是文本数据吗? 那肯定不是,所以就要用FileInputStream或者FileOutputStream呗。这里咱们要的是输出,那就是FileOutputStream嘛

// 老套路,创建一个输出流,设置好文件路径名(序列化不一定要这个文件后缀,其他的也可以,没有特别规定)
FileOutputStream fos = new FileOutputStream("src/obj.data");

假设咱们要存(序列化)的是数组,咋存呢,直接用write()吗?那肯定不行,字节流write()里只能放字节数组或者int类型的字节数据,放不了其他的玩意。这里只要额外加一个东西就好了,就是对象序列化流

// 需要保存(序列化)的数据
int[] array = {1,2,3};
// 老套路,创建一个输出流,设置好文件路径名
FileOutputStream fos = new FileOutputStream("src/obj.data");

/*注意,这里是重点了*/
// 创建一个对象输出流,构造方法里放的是一个输出流对象
// 这就代表着,我要处理的是对象,但是呢,我自己只能处理对象还处理不了文件
// 所以就得连接一个能处理文件的文件输出流
ObjectOutputStream oos = new ObjectOutputStream(fos);
// 调用序列化流对象的方法,参数里面就是你要序列化的对象数据,一句话序列化完毕了!
oos.writeObject(array);
// 关闭流
oos.close();
fos.close();

是不是处理对象的操作流程也是一样的?甚至比操作普通的文件还简单吧!
拿到数据 >>> 输出数据 ,即
创建序列化流对象 >>> 写数据

  1. 创建一个对象序列化流
  2. 连接一个文件输出流
  3. 开始写数据

演示了序列化,那反序列化呢,很简单嘛,将流程线反过来就好了:
获得文件 >>> 拿到数据 >>> 读取数据,即
创建反序列化流对象 >>> 读数据

// 老套路,要读数据嘛,创建一个文件输入流,设置好文件路径名
FileInputStream fis = new FileInputStream("src/obj.data");
// 创建一个反序列化流,就是把Output改成Input就可以了,记得连接输出流
ObjectInputStream ois = new ObjectInputStream(fis);
// 将对象数据读回来,注意哦,反序列化拿到的对象都是Object对象,所以要强制转换类型
int[] arrays = (int[])ois.readObject();
// 正常使用数据
System.out.println(arrays[1]);

是不是也特别简单?

  1. 创建一个对象反序列化流
  2. 连接一个文件输入流
  3. 开始读数据

总结

咱们再次回顾一下思路:

  1. 明确数据源和目的地
  • 源:InputStream 或 Reader
  • 目的地:OutputStream 或 Writer
  1. 需要处理的数据类型
  • 源:是纯文本:Reader
  • 否:InputStream
  • 目的:是纯文本 Writer
  • 否:OutputStream
  1. 再确认要处理的设备(一般是File,这里也只用File举例)
  • 文件:File
  • 对象:Object
  • 键盘:System.in
  • 控制台:System.out

是不是觉得很简单了?为啥很多人学到这就懵了呢,因为 在没有掌握基本的使用方法,却去了解使用原理,其实你只要先掌握基本的使用方法,然后慢慢了解就好了!

如果文章帮到了你,请评论告诉我,如果还有疑问,也请告诉我。当然,如果觉得文章中有哪些地方需要改进,十分欢迎留言探讨!

最后祝大家都称为大神,咱们一起成长一起进步!