编译软件:IntelliJ IDEA 2019.2.4 x64
运行环境:win10 家庭中文版
jdk版本:1.8.0_361
前言
当我们浏览技术文章或查阅专业技术书籍时,经常可以看到“IO”这个词语,很多英语不好的朋友常常抓耳挠腮,不知其所云,在进行Java程序开发时痛苦不堪,常常需要查阅英语词典以究其意。莫急,今天本文让你彻底搞懂IO以及IO流。
提示:以下是本篇文章正文内容,下面案例可供参考
一、什么是IO?
“IO”,“IO”,将这“I"与”O"两个字母分别拆开出来看,“I"就是英语单词”Input"的简写,意为输入,例如键盘输入,从文件中读取,从网络中接收等场景;“O”便是英语单词Output的简写,意为输出,又如Java程序的运行结果输出到控制台,写入文件,通过网络发送等场景。
代码如下(示例):
@Test
public void test08(){
Scanner input=new Scanner(System.in);//键盘输入
System.out.print("输入一个数字:");
int n=input.nextInt();
System.out.println("输出数字:"+n);//输出到控制台
}
而在上述案例场景中数据的传输,亦可看作是数据的流动,基于此,人们便形象的称其为IO流。在Java中,按照流动的方向,以内存为基准,分为输入input
和输出output
,即流向内存是输入流,流出内存的输出流。
Java中I/O操作主要是指使用java.io
包下的内容,进行输入、输出操作。输入也叫做读取数据,输出也叫做作写出数据。
二、IO流的分类
2.1 按数据的流向划分
- 输入流:把数据从
其他设备
上读取到内存
中的流。
🔔以InputStream,Reader结尾
- 输出流 :把数据从
内存
中写出到其他设备
上的流。
🔔 以OutputStream、Writer结尾
2.2 按数据的类型划分
- 字节流 :以字节为单位,读写数据的流。
🔔以InputStream和OutputStream结尾
- 字符流 :以字符为单位,读写数据的流。
🔔 以Reader和Writer结尾
2.3 按IO流的角色划分
- 节点流:可以从 / 向一个特定的地方(节点)读 / 写数据。如FileReader.就是专门向文件读写数据。
- 处理流/装饰流/包装流:它是在其他IO流基础上,增加功能用的。
🔔 举例:
BufferedInputStream、Buffered0utputStream、BufferedReader、BufferedWriter,给其他IO流增加缓冲功能
InputStreamReader、Outputstreamlriter,给其他IO流转换类型用的,或者给其他IO编码、解码用的
💡小tips:
区分上述三者的依据是看创建它们的对象的方式。
如果是处理流,创建调用它们的构造器创建对象时,必须传入另一个IO流对象。
FileInputstream(String name) : 参数不是IO流类型BufferedInputStream(InputStream in): 参数是IO流类型
2.4 小结
可以说,所有的IO流类都有四个基类,四个超类、四个父类,其他的IO流都是从他们演变过来。
如下所示:
字节输入流: Inputstream
字节输出流: Outputstream
字符输入流: Reader
字符输出流: Writer
三、字节流
众所皆知,一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,而这些二进制数字就是字节,那么传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。
3.1 字节输出流【OutputStream】
java.io.OutputStream 抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了如下字节输出流的基本共性功能方法。
public void write(int b)
:将指定的字节输出流。虽然参数为int类型四个字节,但是只会保留一个字节的信息写出。public void write(byte[] b)
:将 b.length字节从指定的字节数组写入此输出流。public void write(byte[] b, int off, int len)
:从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
💡小tips:
偏移量就是指字节数组中的位序,比如偏移量off为2,就是从位序2开始。
public void flush()
:刷新此输出流并强制任何缓冲的输出字节被写出。public void close()
:关闭此输出流并释放与此流相关联的任何系统资源。
💡小tips:当完成流的操作时,必须调用close方法,以释放系统资源。
3.2 FileOutputStream类
java.io.FileOutputStream 类是文件输出流,用于将数据写入文件中。
public FileOutputStream(File file)
:创建文件输出流以写入由指定的 File对象表示的文件。public FileOutputStream(String name)
: 创建文件输出流以指定的名称写入文件。
代码如下(示例):
@Test
public void test05() throws IOException{
FileOutputStream fos=new FileOutputStream("1.txt");
fos.write("生当作人杰".getBytes());
fos.close();
}
经过以上的演示,每次程序运行,创建输出流对象,都会清空目标文件中的数据。
但是这样有一个问题,我如果还想在1.txt中续写内容,而不是重新创建文件
来写内容。该怎么办?
其实为了应对以上的场景需求,Java撰写了上面构造方法的重载形式。
请看JDK API 文档是这样描述的,如下:
public FileOutputStream(File file, boolean append)
: 创建文件输出流以写入由指定的 File对象表示的文件。public FileOutputStream(String name, boolean append)
: 创建文件输出流以指定的名称写入文件。
这两个构造方法,参数中都需要传入一个boolean类型的值,true
表示追加数据,false
表示清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了。
代码使用演示:
@Test
public void test06() throws IOException{
//追加
FileOutputStream fos=new FileOutputStream("1.txt",true);
fos.write("死亦为鬼雄".getBytes());
fos.close();
}
🔔 注意:
当你创建一个流对象时,必须传入一个文件路径。如果该文件不存在,会创建该文件。如果有这个文件,会清空这个文件的数据。如果传入的是一个目录,则会报IOException异常。
3.3 字节输入流【InputStream】
java.io.InputStream 抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。
public int read()
: 从输入流读取一个字节。返回读取的字节值。虽然读取了一个字节,但是会自动提升为int类型。如果已经到达流末尾,没有数据可读,则返回-1。public int read(byte[] b)
: 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。每次最多读取b.length个字节。返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回-1。public int read(byte[] b,int off,int len)
:从输入流中读取一些字节数,并将它们存储到字节数组 b中,从b[off]开始存储,每次最多读取len个字节 。返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回-1。public void close()
:关闭此输入流并释放与此流相关联的任何系统资源。
💡小tips:当完成流的操作时,必须调用close方法,以释放系统资源。
3.4 FileInputStream类
java.io.FileInputStream 类是文件输入流,从文件中读取字节。
FileInputStream(File file)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名。FileInputStream(String name)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。
代码如下(示例):
@Test
public void test09()throws IOException{
// 使用文件名称创建流对象
FileInputStream fis = new FileInputStream("1.txt");
// 定义变量,保存数据
int b;
// 循环读取
while ((b = fis.read())!=-1) {
System.out.println((char)b);
}
// 关闭资源
fis.close();
}
3.5 复制文件
🌟原理: 从已有的文件读取字节数据,然后写入【字节数据】到另一个文件中
如图所示:
案例:使用一个文件字节流来复制一个视频文件
代码使用演示:
@Test
public void test07() throws IOException{
//案例需求:使用一个文件字节流来复制一个视频文件
//源文件
FileInputStream fis=new FileInputStream("D:\\BaiduNetdiskDownload\\尚硅谷2022Java\\01-Java基础【完结】\\day0217_JavaSE_第22天资料\\day0217_22video\\day0217_09IO流扩展练习3:复制文件.avi");
//目标文件
FileOutputStream fos=new FileOutputStream("day0217_09IO流扩展练习3:复制文件.avi");
byte[] data=new byte[1024]; //一次读取1KB
while (true){
int len=fis.read(data); //从源文件中一次性读取len个字节的数据
if (len==-1){
break;
}
fos.write(data,0,len);//把本次读到的len个字节写入目标文件中
}
fis.close();
fos.close();
}
四、字符流
当使用字节流读取文本文件时,可能会有一个小问题。就是遇到中文字符时,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储。所以Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。
💡小tips:
字符流,只能操作文本文件,不能操作图片,视频等非文本文件。
当我们单纯读或者写文本文件时 ,使用字符流; 其他情况使用字节流。
4.1 字符输入流【Reader】
java.io.Reader 抽象类是表示用于读取字符流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。
public int read()
: 从输入流读取一个字符。 虽然读取了一个字符,但是会自动提升为int类型。返回该字符的Unicode编码值。如果已经到达流末尾了,则返回-1。public int read(char[] cbuf)
: 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中 。每次最多读取cbuf.length个字符。返回实际读取的字符个数。如果已经到达流末尾,没有数据可读,则返回-1。public int read(char[] cbuf,int off,int len)
:从输入流中读取一些字符,并将它们存储到字符数组 cbuf中,从cbuf[off]开始的位置存储。每次最多读取len个字符。返回实际读取的字符个数。如果已经到达流末尾,没有数据可读,则返回-1。public void close()
:关闭该流并释放与此流相关联的任何系统资源。
💡小tips:当完成流的操作时,必须调用close方法,以释放系统资源。
4.2 FileReader类
java.io.FileReader
类实现java.io.Reader抽象类,是用来读取字符文件。构造时默认使用系统默认的字符编码和默认字节缓冲区,若另外指定字符编码规则,则另说。
FileReader(File file)
: 创建一个新的 FileReader ,给定要读取的File对象。FileReader(String fileName)
: 创建一个新的 FileReader ,给定要读取的文件的名称。
💡小tips:
当你创建一个流对象时,必须传入一个文件路径。类似于FileInputStream 。如果该文件不存在,则报FileNotFoundException。如果传入的是一个目录,则会报IOException异常。
代码使用演示:
@Test
public void test01() throws IOException {
FileReader fr=new FileReader("2.txt");
char[] data=new char[12];
int len;
while ((len=fr.read(data))!=-1){
System.out.println(new String(data,0,len));
}
}
4.3 字符输出流【Writer】
java.io.Writer 抽象类是超类,它是所有表示用于写出字符流的类的超类,将指定的字符信息写入到目的文件中。它定义了字节输出流的基本共性功能方法。
public void write(int c)
:写入单个字符。public void write(char[] cbuf)
:写入字符数组。public void write(char[] cbuf, int off, int len)
:写入字符数组的某一部分,off数组的开始索引,len写的字符个数。public void write(String str)
:写入字符串。public void write(String str, int off, int len)
: 写入字符串的某一部分,off字符串的开始索引,len写的字符个数。public void flush()
:刷新该流的缓冲。public void close()
:关闭此流,但要先刷新它。
4.4 FileWriter类
java.io.FileWriter 类实现了java.io.Writer 抽象类,它是用来写入字符到文件中。构造时使用系统默认的字符编码和默认字节缓冲区。
FileWriter(File file)
: 创建一个新的 FileWriter,给定要读取的File对象。FileWriter(String fileName)
: 创建一个新的 FileWriter,给定要读取的文件的名称。
💡小tips:
当你创建一个流对象时,必须传入一个文件路径。类似于FileInputStream 。如果该文件不存在,则报FileNotFoundException。如果传入的是一个目录,则会报IOException异常。
代码使用演示:
@Test
public void test02() throws IOException{
FileWriter fw=new FileWriter("2.txt",true);
fw.write("。大善");
fw.close();
}
🔔注意:
FileWriter类也可以像FileOutputStream类一样,在文中续写追加内容,而不用再创建文件写入。
用法和FileOutputStream类一样,这里不做赘述。
4.5 换行问题
代码使用演示:
@Test
public void test10() throws IOException {
// 使用文件名称创建流对象,可以续写数据
FileWriter fw = new FileWriter("f5.txt");
// 写出字符串
fw.write("你");
// 写出换行
fw.write("\r\n");
// 写出字符串
fw.write("好");
// 关闭资源
fw.close();
}
五、IO流问题
5.1 关于直接基于“文件夹/目录”创建IO流对象是错误会报java.io.FileNotFoundException: d: download (拒绝访问。)
代码使用演示:
@Test
public void test11() throws IOException{
FileInputStream fis = new FileInputStream("d:\\download");
//java.io.FileNotFoundException: d: download (拒绝访问)文件IO必须基于文件路径创建对象
// "d:lldownload"这个是一个文件夹的路径,不是一个文件的路径
FileOutputStream fos = new FileOutputStream( "d:\\download");
//java.io.FileNotFoundException: d: download (拒绝访问)
//就算是让 FiLeOutputStream帮你自动创建文件,也必须在IO流的构造器中,指明文件的名称。
}
5.2 输出流如果不能及时关闭(因为可能后面还要用),但是又想要数据及时输出,可以调用flush方法。
但是如果后面不用了,一定要close。因为close不仅仅是刷数据的问题,还涉及内存回收问题。
代码使用演示:
@Test
public void test03() throws IOException{
FileWriter fw=new FileWriter("1.txt");
fw.write("天地有正道");
}
🔔注意:
像Filewriter等很多的输出流的内部有自己的一个小小缓冲区。它在调用write方法时,会先将数据写到缓冲区。当缓冲区满的时候,会自动“溢出”到文件。当缓冲区没满的时候,调用close 方法执行时,会把缓冲区的数据“清空”输出。
如果希望数据及时写出,但是暂时还不close,可以使用flush()方法刷新缓存区
六、缓冲流
6.1 分类
缓冲流又称高效流,按照数据类型分类:
- 字节缓冲流:
BufferedInputStream
,BufferedOutputStream
- 字符缓冲流:
BufferedReader
,BufferedWriter
🤖缓冲流的基本原理:
是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。
6.2 构造方法
public BufferedInputStream(InputStream in)
:创建一个 新的缓冲输入流。public BufferedOutputStream(OutputStream out)
: 创建一个新的缓冲输出流。
🔔注意:
缓冲流构造方法中必须包含字节输入流【InputStream】或字节输出流【OutputStream 】对象,才行
代码使用演示:
// 创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("ab.txt"));
// 创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("ac.txt"));
public BufferedReader(Reader in)
:创建一个 新的缓冲输入流。public BufferedWriter(Writer out)
: 创建一个新的缓冲输出流。
代码使用演示:
// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("12.txt"));
// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("13.txt"));
6.3 使用缓冲流前后耗时对比
代码使用演示:
@Test
//不使用缓冲流
public void test04() throws IOException{
long start=System.currentTimeMillis();
FileInputStream fis=new FileInputStream("D:\\BaiduNetdiskDownload\\尚硅谷2022Java\\01-Java基础【完结】\\尚硅谷_JavaEE_01_JavaSE课程资料\\01_sofeware\\IDE\\ideaIU-Ultimate-2019.2.3.zip");
FileOutputStream fos=new FileOutputStream("D:\\ideaIU-Ultimate-2019.2.3-副本.zip");
byte[] data=new byte[1024];
int len;
while ((len=fis.read(data))!=-1){
fos.write(data,0,len);
}
fis.close();
fos.close();
long end=System.currentTimeMillis();
System.out.println("耗时:"+(end-start)+"毫秒");//耗时:6200毫秒
}
@Test
//使用缓冲流
public void test05() throws IOException{
long start=System.currentTimeMillis();
FileInputStream fis=new FileInputStream("D:\\BaiduNetdiskDownload\\尚硅谷2022Java\\01-Java基础【完结】\\尚硅谷_JavaEE_01_JavaSE课程资料\\01_sofeware\\IDE\\ideaIU-Ultimate-2019.2.3.zip");
FileOutputStream fos=new FileOutputStream("D:\\ideaIU-Ultimate-2019.2.3-副本1.zip");
BufferedInputStream bis=new BufferedInputStream(fis);
BufferedOutputStream bos=new BufferedOutputStream(fos);
byte[] data=new byte[1024];
int len;
while ((len=bis.read(data))!=-1){
bos.write(data,0,len);
}
fis.close();
fos.close();
long end=System.currentTimeMillis();
System.out.println("耗时:"+(end-start)+"毫秒");//耗时:1778毫秒
}
🤮OMG!
不使用缓冲流耗时:6200毫秒;
使用缓冲流耗时:1778毫秒
我们可以很明确的看到,在复制文件时,尤其时很大的文件时,缓冲流真的
非常高效!
why?
原理剖析如下:
6.4 字符缓冲流特有方法
字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,让我们来看它们具备的特有方法。
- BufferedReader:
public String readLine()
: 读一行文字。 - BufferedWriter:
public void newLine()
: 写一行行分隔符,由系统属性定义符号。
代码使用演示:
@Test
public void test12() throws IOException{
// 创建流对象
BufferedReader br = new BufferedReader(new FileReader("in.txt"));
// 定义字符串,保存读取的一行文字
String line;
// 循环读取,读取到最后返回null
while ((line = br.readLine())!=null) {
System.out.println(line);
}
// 释放资源
br.close();
}
@Test
public void testNewLine()throws IOException{
// 创建流对象
BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt"));
// 写出数据
bw.write("你");
// 写出换行
bw.newLine();
bw.write("好");
bw.newLine();
bw.write("吗");
bw.newLine();
// 释放资源
bw.close();
}
七、IO流的关闭问题
IO流的关闭顺序有什么问题?
莫急,请看下面的代码:
@Test
public void test06() throws IOException{
long start=System.currentTimeMillis();
FileInputStream fis=new FileInputStream("D:\\BaiduNetdiskDownload\\尚硅谷2022Java\\01-Java基础【完结】\\尚硅谷_JavaEE_01_JavaSE课程资料\\01_sofeware\\IDE\\ideaIU-Ultimate-2019.2.3.zip");
FileOutputStream fos=new FileOutputStream("D:\\ideaIU-Ultimate-2019.2.3-副本2.zip");
BufferedInputStream bis=new BufferedInputStream(fis);
BufferedOutputStream bos=new BufferedOutputStream(fos);
byte[] data=new byte[1024];
int len;
while ((len=bis.read(data))!=-1){
bos.write(data,0,len);
}
fis.close();
fos.close();
bis.close();
bos.close(); //java.io.IOException: Stream Closed
long end=System.currentTimeMillis();
System.out.println("耗时:"+(end-start)+"毫秒");//耗时:1778毫秒
}
why?
为社么会出现“stream closed”?
🤮原因:
IO流有依赖关系。外层的包装流依赖于内存的被包装流。 比如: BufferedInputStream 依赖于 FileInputStream BufferedOutputstream 依赖于 FileOutputstream
如果把内层的IO先关闭了,外层IO流就失去了依赖,就会报错。 比喻:我坐在登子上,我依赖于凳子,如果把登子先撤了,我就会摔倒。
先让我站起来,然后撤登子。
关闭的顺序是: 先关闭外层的包装流,再关闭内层的被包装流
🔔本质:
这段代码数据的流向: 源文件 ->fis ->bis ->data数组 ->bos ->fos
->目标文件BufferedOutputStream和FileOutputStream,如果把fos先关了,bos中的缓冲的数据没有办法完全写出。因为close时,会清空缓冲区
好记: fis被包装在里面的,比喻成内衣,先穿内衣,再穿外衣bis相当于外套。 fos是内衣,bos是外套 关闭好比是脱衣服。
先脱外套,再脱内衣。
八、转换流
8.1 编码与解码
计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。基于某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。
比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。
乱码问题举例如下:
如何解决乱码问题?
请看下面的类,就是解决乱码问题
8.2 InputStreamReader类
转换流java.io.InputStreamReader,是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。
InputStreamReader(InputStream in)
: 创建一个使用默认字符集的字符流。InputStreamReader(InputStream in, String charsetName)
: 创建一个指定字符集的字符流。
解决方案代码如下:
@Test
public void test08() throws IOException{
FileInputStream fr=new FileInputStream("1.txt");
InputStreamReader isr=new InputStreamReader(fr,"GBK");//文件的编码格式是GBK,用GBK来解文件的编码
char[] data=new char[10];
int len;
while ((len=isr.read(data))!=-1){
System.out.println(new String(data,0,len));
}
fr.close();
}
8.3 OutputStreamWriter类
转换流java.io.OutputStreamWriter ,是Writer的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。
OutputStreamWriter(OutputStream in)
: 创建一个使用默认字符集的字符流。OutputStreamWriter(OutputStream in, String charsetName)
: 创建一个指定字符集的字符流。
8.4 案例
8.4.1 案例①
🔔输出”你太棒了“在GBk格式的1.txt文件中
代码演示如下:
@Test
public void test09() throws IOException{
//案例:输出”你太棒了“在GBk格式的1.txt文件中
FileOutputStream fos=new FileOutputStream("1.txt",true);
OutputStreamWriter isr=new OutputStreamWriter(fos,"GBK");//把字符数据用GBk的格式转换为字节数据写入1.txt文件中
isr.write("你太棒了");
isr.close();
fos.close();
}
8.4.2 案例②
🔔案例需求不变,把缓冲流加入进去
代码演示如下:
@Test
public void test010() throws IOException{
//案例:输出”你太棒了“在GBk格式的1.txt文件中
//将缓冲流加入进去
FileOutputStream fos=new FileOutputStream("1.txt",true); //字节流
BufferedOutputStream bos=new BufferedOutputStream(fos);//缓冲字节流包含字节流
OutputStreamWriter isr=new OutputStreamWriter(fos,"GBK");//字符流 把字符数据转换为GBk格式的数据写入1.txt文件中
BufferedWriter bw=new BufferedWriter(isr);//缓冲字符流包含字符流
isr.write("你太棒了1");
bw.close();
bos.close();
isr.close();
fos.close();
}
8.4.3 案例③
🔔 将编码为GBK的”file_gbk.txt"转换为编码为utf-8的文件”file_utf8.txt"
代码演示如下:
@Test
public void test11() throws IOException{
//案例:将编码为GBK的”file_gbk.txt"转换为编码为utf-8的文件”file_utf8.txt"
FileInputStream fis=new FileInputStream("file_gbk.txt"); //字节输入流
InputStreamReader isr=new InputStreamReader(fis,"GBK");//字符输入流
BufferedReader br=new BufferedReader(isr);//缓冲字符输入流
String readLine;
FileOutputStream fos=new FileOutputStream("file_utf8.txt"); //字节输出流
OutputStreamWriter osw=new OutputStreamWriter(fos,"utf-8"); //字符输出流
BufferedWriter bw=new BufferedWriter(osw); //缓冲字符输出流
while ((readLine=br.readLine())!=null){
bw.write(readLine);
bw.newLine();
}
bw.close();
osw.close();
fos.close();
br.close();
isr.close();
fis.close();
}
8.5 转换流原理
原理图像如下: