1. I/O流前置知识
在讲解IO流之前,需要先说明几个小知识点:
(1)bit 是最小的二进制单位,是计算机的操作部分,取值0或1。
(2)Byte(字节)是计算机操作数据的最小单位由 8 位 bit 组成 取值(-128-127)。
(3)Char(字符)是用户的可读写的最小单位,在 Java 里面由 16 位 bit 组成 取值(0- 65535)。
2. I/O流分类
关于io流,有多种分类方式,这里只举最常用的几种。
方式一:按照流的方向进行分类,我们均以内存为参照物。
往内存中去,叫输入流(Input),或者叫做读(Read);
从内存中出来,叫输出流(Ouput),或者叫做写(Write);
如下图所示:
方式二:按照读取数据方式不同进行分类。
1. 有的流按照字节的方式读取数据,一次读取一个字节byte,等同于一次读取8个二进制位。称为字节流。
例如:假设有文件file1.txt
文件内容为 “a中国bc张三”
第一次读:读取一个字节,读取到‘a’字符(a 字符在windows系统中占用1个字节);
第二次都:读取一个字节,读取到‘中’字符的一半(‘中’汉字字符在windows系统中占用两个字节);
第三次都:读取一个字节,读取到‘中’字符的剩下一半,在显示一半时就会是乱码(‘中’汉字字符在windows系统中占用两个字节);
2. 有的流是按照字符的方式读取数据,一次读取一个字符,,这种流是为了方便读取普通文本文件而存在的,但这种流不能读取:图片,声音,视频,等文件,只能读取纯文本文件,连word文件都无法读取。这种流称为字符流。
例如:假如有文件file1.txt
文件内容为 “a中国bc张三”
第一次读:读取到‘a’字符(a 字符在windows系统中占用1个字节);
第二次都:读取到‘中’字符(‘中’汉字字符在windows系统中占用两个字节);
3. 有的流是万能流,什么类型的文件都可以读取,包括,文本文件,视频文件,音频文件,图片等。
Java 中IO四大家族,四大家族首领如下:
java.io.InputStream 字节输入流;
java.io.OnputStream 字节输出流;
java.io.Reader 字符输入流;
java.io.Writer 字符输出流;
3. I/O流通用知识点
(1)所有流都实现了java.io.Closeable接口,都是可关闭的,都有Close()方法。
流是一个管道,是内存与硬盘之间的通道,用完之后一定一定要关闭,不然会消耗(占用)更多的资源。
(2)所有输出流都实现了java.io.Flushable接口,都是可刷新的,都有flush()方法。所以输出流在最终输出之后,一定要刷新,刷新的作用是为了将管道中的数据全部写入硬盘,清空管道。
如果不刷新,可能会导致数据丢失。
4. Java中常用的IO流方法
Java io包下io流的类有将近40个,这里我列举了常用的16个IO流的类,下面我会挑几个开发中常用的进行用法介绍
文件专属的类:
1. java.io.FileInputStream
2. java.io.FileOutputStream
3. java.io.FileReader
4.java.io.FileWriter
转换流(将字节流转换成字符流)
5. java.io.InputStreamRead
6. java.io.OnputStreamWriter
缓冲流专属(加快文件读写的效率)
7.java.io.BufferedReader
8.java.io.BufferedWriter
9. java.io.BufferedInputStream
10..java.io.BufferedOnputStream
数据流专属
11. java.io.DataInputStream
12. java.io.DataOnputStream
标准输出流
13. java.io.PrintWriter
14. java.io.PrintStream
对象专属流
15. java.io.ObjectInputStream
16. java.io.ObjectOutputStream
5. FileInputStream 的用法
FileInputStream 是一个字节输入流,它是以字节的方式向内存中读取文件内容
下面是源码中的 FileInputStream ,可以看到,它继承了 InputStream 抽象类
5.1 FileInputStream 的构造方法
FileInputStream 有如上图所示三种构造方法,但常用的是第一种和第三种。
第一个构造方法参数需要传递一个文件的对象
第二种构造方法参数需要传入一个 String 类型的字符串,这里的字符串参数其实指代的是文件的路径,例如我们可以写 "D:\\video"
通常情况下我们会采用第二种构造器,因为直接写文件的路径,在编写时比较直观,第一种还需要去创建一个我们要操作的文件的对象,多此一举,所以我个人更推荐采用第二种方式。
5.2 FileInputStream 的常用方法
如上图所示,因为流都是需要关闭的,所以 close()方法是必须要用到的,通常情况下,一个文件中内容都会很多,所以在三个 read 方法中,read(byte[] b) 和 read(byte[] b ,int off,int len)是比较常用的两个方法;
看我如下代码,我这里各行注释都解释得非常详细
public static void main(String[] args) {
/**
* 因为我手动输入的文件路径可能有误,或者程序找不到指定文件,会爆出异常,
* 又因为这是在 main 方法中,所以采用 try catch 捕获异常
* 这里使用 try catch 捕获异常还有一个好处,就是可以在 finally 语句块中关闭流
*/
// 这里提前把 FileInputStream 对象创建为空
InputStream is = null;
try {
// 给FileInputStream 对象赋值
is = new FileInputStream("D:\\study\\IDEA\\workspaces\\cloud-demo\\user-service\\test.txt");
// 定义一个字节数组,一次读取 5 个字节
byte[] buffer = new byte[5];
// 定义一个int类型的变量 length 接收 is 读取到的字节数量,
int length;
// 采用循环的方式读取文件内容,当 length = -1时,说明读取完成,结束循环
while ((length = is.read(buffer)) != -1){
// new 一个字符串输出读取到的字节内容,从0索引开始,读取 length 个字节,防止多读少读等情况的发生
// 这里细节不换行,因为我们要确保读取出来的格式与原文件完全一致
System.out.print(new String(buffer,0,length));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
// 这里做一个判空操作,非空才关闭,如果为空还关闭,会出现空指针异常
if (is != null){
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
这里有一点需要注意,我定义的字节数组大小为5,但实际开发时,我们可以把它定义的很大,或者直接把它定义成要读取的文件的本身大小,这样只读取一次就可以了,减少磁盘IO次数,提高程序的运行效率。
6. FileOnputStream 的用法
FileOutputStream 是一个字节输出流,它是以字节的方式从内存向硬盘写入内容
6.1 FileOutputStream 的构造方法
FileOutputStream 的构造方法如下图所示,这里的 boolean append 是一个可选参数选项,如果在定义时设置为 true ,说明是要在文件末尾追加内容;
如果不设置,默认为false,那么我们在往文件中写入内容的时候,会把原来的内容覆盖。
这个构造器的方法是与 FileInputStream 相似的,参数为 file 是要传入一个文件对象,参数为 String 是要我们传入要操作的文件的路径,这里就不需要我在过多的解释了。
6.2 FileOutputStream 的常用方法
FileOutputStream 与 FileInputStream 的操作方法也和FileInputStream 非常相似,只不过是从读文件变成了写文件,相应方法如下所示
FileOutputStream 输出流可以把内存中的东西写入到磁盘中去长久保存,其实也可以结合 File InputStream 输入流完成文件的复制操作,思路很简单,我们知道,文件是保存在磁盘中的,我们可以先通过输入流将要复制的文件读取到内存中去,然后再通过输出流写入到硬盘中我们制定好的位置,就能完成文件的复制操作。
6.3 FileOutputStream与FileOutputStream结合使用
如下图所示,在我的电脑中C盘下的某个文件夹中,我存放着一个名为 "tupian.png" 屏幕截图,现在要通过IO流的方式将该图片复制到 D盘目录下
具体代码如下所示,我注释的都非常详细,非常好理解,就不需要我做很多的解释了:
public static void main(String[] args) {
/**
* 因为我手动输入的文件路径可能有误,或者程序找不到指定文件,会爆出异常,
* 又因为这是在 main 方法中,所以采用 try catch 捕获异常
* 这里使用 try catch 捕获异常还有一个好处,就是可以在 finally 语句块中关闭流
*/
// 这里提前把 FileInputStream 对象创建为空
InputStream is = null;
// 这里提前把 FileOutputStream 对象创建为空
OutputStream os = null;
try {
// 给FileInputStream 对象赋值
is = new FileInputStream("C:\\Users\\18727\\Pictures\\Screenshots\\tupian.png");
// 给 FileOutputStream 对象赋值
os = new FileOutputStream("D:\\tupian.png");
// 定义一个字节数组,一次读取 1024 * 2 个字节,也就是2kb 字节
byte[] buffer = new byte[1024 * 2];
// 定义一个int类型的变量 length 接收 is 读取到的字节数量,
int length;
// 采用循环的方式读取文件内容,当 length = -1时,说明读取完成,结束循环
while ((length = is.read(buffer)) != -1){
// 循环读取文件,并在读取文件的同时输出文件内容
os.write(buffer,0,length);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
// 这里做一个判空操作,非空才关闭,如果为空还关闭,会出现空指针异常
// 此外还有一点需要注意,如果创建了多个流,关闭时最好谁最晚创建谁先关闭,顺序不要乱,否则后期可能会出问题
if (os != null)
os.close();
if (is != null)
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
然后我们运行上述的 main 方法,得到如下图片中的结果
程序运行完成,这个时候可以发现,在D盘根目录下,已经出现了名为 "tupian.png" 的图片,说明已经被拷贝过来了。
其实我上面所写的程序,可以复制任何文件,如图片,音频,视频,文件夹,文本文件都可以,因为在计算机底层,所有文件都是以字节的方式存储的,而我们所写的方法正是字节输入流与字节输出流。
7. FileReader 的用法
FileReader 是字符输入流,它与 FileInputStream 的区别就是去读取内容的方式不一样,FileReader 是以字符的方式读取数据。我们可以进行类比学习,下面我就简单介绍了, FileReader 的构造方法与读取方法如下图所示
我们还是和刚才一样,写一个 main 方法,读取文件中的内容
public static void main(String[] args) {
// 这里提前把 FileReader 对象创建为空
FileReader fr = null;
//仍然使用 try catch finally 进行异常捕获与流的关闭
try {
// 这里使用相对路径读取文件
fr = new FileReader("user-service/test.txt");
// 定义一个字符数组 长度为 8
char[] buffer = new char[8];
int length;
while ((length = fr.read(buffer)) != -1){
// 输出读取到的字符内容
System.out.print(buffer);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (fr != null){
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
我们在该项目的目录下新建一个 test.txt 文件,并加入一些字符
运行 main 方法,可以看到运行成功,文件中的内容已经被打印出来。
8. FileWriter 的用法
FileWriter 是什么,该怎么理解,其实对比着 FileOutputStream 与 FileInputStream 就很好理解了,FileWriter是字节输出流,它可以以自己的方式将内存中的内容写入到硬盘中长久保存。
我们还是用刚才的例子,把 项目中的 test.txt 文件中的内容写入到D盘根本目录下的 test.txt 文件去,代码如下所示
public static void main(String[] args) {
// 这里提前把 FileReader 对象创建为空
FileReader fr = null;
// 这里提前把 FileWriter 对象创建为空
FileWriter fw = null;
//仍然使用 try catch finally 进行异常捕获与流的关闭
try {
// 这里使用相对路径读取文件
fr = new FileReader("user-service/test.txt");
// 对fw 对象赋值,指定D盘下的 test.txt 文件
fw = new FileWriter("D:/test.txt");
// 定义一个字符数组 长度为 8
char[] buffer = new char[8];
int length;
while ((length = fr.read(buffer)) != -1){
// 将读取到的内容写入到 text.txt 文件中
fw.write(buffer,0,length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if (fw != null){
fw.close();
}
if (fr != null){
fr.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
我们运行此方法,得到如下结果,然后在电脑D盘下,可以找到已经存在的 test.txt 文件,这里程序会帮我们自动创建,打开该文件,可以发现内容也都复制过来了
这里我再对字符输出流多一点补充,我们在使用字符输出流之后,一定要进行刷新,其实字符输出流的底层,它会把要写入到硬盘的内容全部都暂存到内存的缓存中去,当要输入的内容全部输出完毕后,缓存区的内容就会一次性的全部加入到硬盘,如果不刷新,它就不会到达硬盘。
由于在 close() 方法中,已经添加了刷新缓存的操作,所以就不需要我们手动刷新了。
但是,一旦我们调用了 close() 方法,那么内存与硬盘的通道就会断开,无法再次输出,如果我们的程序中还会再次使用到字节输出流,建议手动刷新缓存而不关闭缓存,这样下次使用的时候就不需要再次创建流对象了,可以提高程序的效率。