简述 Java 中的 IO 流

本文是关于IO 流中自己学到的一些基本知识,以及遇到的问题,希望能够帮助到大家。

1.IO 流的引入

我们可以利用 File 类将 java 程序跟硬盘上的文件联系起来。我们可以获取其中某些属性。但是,文件的内容我们不能操作,读取。这时候就引入了 对文件进行操作的 IO 流。

IO 流示意图.png

我们可以生动形象的把 IO 流当作一根根管子,管子的一端怼到目标文件,另一端怼到程序中。我们写的程序在这里相当于一个数据传输的中转站。

2. IO 流分类

2.1 按照读取单位划分:

字节流 字符流
输入流 InputStream Reader
输出流 OutputStream Writer

2.2 按照功能划分

  • 节点流:直接从源文件读取数据到程序中 —— 一根管。
  • 处理流:需要多个流结合使用 —— 多根管。

在我目前所看到的所有程序中,想要利用 IO 流,必须先用输入输出字节流,或者输入输出字符流连接到目标文件!!!

下面主要写一下各个流的用法。如有疑问以及不妥的地方欢迎指正,感激不尽!

3. 字节流–FileInputStream,FileOutputStream

3.1 文件 —> 程序(以程序为主体,对于程序来说属于对内输入,所以要用输入流)

3.1.1 利用单个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
public static void main(String[] args) throws IOException {
//1.确定文件:
File f=new File("i:/test/haha.txt");
//2.创建一根管,然后连接文件:
FileInputStream fis=new FileInputStream(f);
//3.进行动作: 吸 (流:读取)
int n=fis.read();
while(n!=-1){//读到文件末尾就是-1
System.out.print(n+"\t");
n=fis.read();
}
//4.关闭流(无论什么时候,关闭流是必须的):
fis.close();
}
}

首先要确定被读取文件的地址(代码第 4 行),然后创建一根管(就是 IO 流),一端怼到该文件上去,另一端怼到程序中,进行吸的动作(也就是程序读取文件)。(怎么样,理解的还可以不,哈哈)

从上面的代码第 8 行和第 11 行可以看出 这种方法是一个字节一个字节的将文件中的信息读入到程序中

缺点:

  • 运行结果会出现乱码;
  • 一个字节一个字节读取 ,效率太低;

3.1.2 利用数组缓冲区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test02 {
public static void main(String[] args) throws IOException {
//创建文件:
File f=new File("i:/test/haha.txt");
//创建一根管,然后连接文件:
FileInputStream fis=new FileInputStream(f);
//进行动作: 吸 (流:读取)
byte[] b=new byte[8];// 随便创建了一个byte数组,长度为8
int len=fis.read(b);// len是数组中被占用的长度
while(len!=-1){// 读到文件末尾就是-1
System.out.print(len+"\t");
len=fis.read(b);
}
//关闭流:
fis.close();
}
}

这种方法是定义了一个数组(第 8 行)8个字节,通俗一点说,就是每 8 个字节为一组进行读取,用数组将文件中的信息读入到程序中,效率比比上一种方法高。

3.2 程序 —> 文件(以程序为主体,对于程序来说属于对外输出的,所以要用输出流)

3.2.1 利用单个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test03 {
public static void main(String[] args) throws IOException {
//创建文件:
File f=new File("i:/test/haha.txt");
//创建一根管,然后连接文件:
FileOutputStream fos=new FileOutputStream(f);
//进行动作: 吐 (流:写入)
String str="abc123你好";
//将字符串转化成字节:
byte[] bytes=str.getBytes();
for (int i = 0; i < b.length; i++) {
fos.write(b[i]);//一个字节一个字节的往外读
};
//关闭流:
fos.close();
}
}

首先要确定要把文件读取到哪儿(代码第 4 行),然后创建一根管(就是 IO 输出流),一端怼到该文件上去,另一端怼到程序中,进行吐的动作(也就是把内容从程序中写出去)。因为字节输出流只能一个字节一个字节的向外读取,所以要调用 String 的 getBytes() 方法将 String 类型数据准换成 byte 类型,而该方法返回值为一个 byte 类型数组。

3.2.2 利用缓冲数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test04 {
public static void main(String[] args) throws IOException {
//创建文件:
File f = new File("i:/test/haha.txt");
//创建一根管,然后连接文件:
FileOutputStream fos = new FileOutputStream(f);
//进行动作: 吐 (流:写入)
String str = "abc123你好";
byte[] bytes = str.getBytes();//将字符串转化成字节:因为fos只能一个字节一个字节的写
for(byte b : bytes){
fos.write(b);
}
//关闭流:
fos.close();
}
}

定义了一个数组(第 9 行),调用 String 类型的 getBytes() 方法,将字符串转化为字节,用一个数组接住该方法的返回值。然后再用循环遍历将数组中的信息读取到文件中,效率比比上一种方法高。

一定要记得关流!!!

3.3 文件的复制

3.3.1 利用单个字节进行复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestCopyDoc {
public static void main(String[] args) throws IOException {
//先确定文件:
File f1=new File("i:/test/haha.txt");
File f2=new File("i:/test/demo.txt");
//创建两个输入输出流(两个管):
FileInputStream fis=new FileInputStream(f1);//输入流
FileOutputStream fos=new FileOutputStream(f2);//输出流
//开始动作: 吸-----吐
int n=fis.read();//吸;
while(n!=-1){
fos.write(n);//吐;
n=fis.read();//吸;
}
//关闭流:
fis.close();
fos.close();
}
}

功能:就是利用输入输出字节流,一个字节一个字节的将 i:/test/haha.txt 文件中的内容复制到 i:/test/demo.txt 中去。

程序相当于一个中转站(开篇的 IO 流示意图),利用字节输入流(FileInputStream)将 haha.txt 中的内容读取到程序中,然后利用字节输出流(FileOutputStream)从程序中写出到目标文件 demo.txt.

3.3.2 利用数组缓冲区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TestCopyDoc02 {
public static void main(String[] args) throws IOException {
//先确定文件:
File f1=new File("i:/test/haha.txt");
File f2=new File("i:/test/demo.txt");
//创建两个输入输出流(两个管):
FileInputStream fis=new FileInputStream(f1);//输入流
FileOutputStream fos=new FileOutputStream(f2);//输出流
//开始动作: 吸-----吐
byte[] b=new byte[8];
int len=fis.read(b);//len---是这个数组中被占用的数量
while(len!=-1){
fos.write(b,0,len);
len=fis.read(b);
}
//关闭流:
fis.close();
fos.close();
}
}

功能:定义了一个数组(第 10 行)8个字节,每 8 个字节为一组进行读取,FileInputStream 利用数组将 haha.txt 中的信息读入到程序中(第 11 行),其中 len 表示这个数组中被占用的数量,当读取到文件结尾的时候 len = -1(这是规定,我也不知道为啥是 -1 ,不是其他的数字,这个记住就好。。。);然后 FileOutputStream 把程序中读取到的信息写入到目标文件中(第 14 行)。

4. 字符流–FileReader,FileWriter

  • 和字节流一样,用字符流进行文件的复制也分为两种方法,一种是利用单个字符进行读取和写出,另一种是利用数组进行读取和写出

4.1 利用单个字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestFileCopy001 {
public static void main(String[] args) throws IOException {
//1.创建目标文件,源文件
File f1=new File("i:/test/haha.txt");
File f2=new File("i:/test/demo.txt");
//2.创建字符流
FileReader fr=new FileReader(f1);
FileWriter fw=new FileWriter(f2);
//3.读取
int n=fr.read();
while(n!=-1){
fw.write(n);
n=fr.read();
}
//4.关闭流
fw.close;
fr.close;
}
}
  • 功能:和 3.3.1 中一样,只不过是利用字符流来进行操作。

4.2 利用数组缓冲区—char[]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TestFileCopy001 {
public static void main(String[] args) throws IOException {
//1.创建目标文件,源文件
File f1=new File("i:/test/haha.txt");
File f2=new File("i:/test/demo.txt");
//2.创建字符流
FileReader fr=new FileReader(f1);
FileWriter fw=new FileWriter(f2);
//3.读取
char[] ch=new char[8];
int len=fr.read(ch); //len---是这个数组中被占用的数量
while(len!=-1){
fw.write(ch,0,len);
len=fr.read(ch);
}
//4.关闭流
fw.close;
fr.close;
}
}
  • 功能:和 3.3.2 类似,只不过是定义了一个字符数组(代码第 10 行)进行读取和写出操作。

另外要说明的是:用字符流复制非纯文本的文件都是不行的,都是耍流氓!因为用字符流复制的时候,它会按照系统的字符码表进行查找和替换,把二进制数据全部按照码表替换了,但是图片的一些代码能在码表中找到相对应的编码,就转换成编码,另外还有一些找不到,JVM就会用类似的编码代替,那么你再打开的时候就肯定不是图片了。

总之,记住千万不要耍流氓啊!!

5. 缓冲字节流–BufferedInputStream,BufferedOutputStream

先上图(以下图都是按照个人理解画出来的,如有错误还请指正):

字节流复制文件原理

我们上面写的代码都是利用字节流进行文件的复制。以上图为例,每次读取或写出都会对硬盘上的文件访问一次,缺点就是对硬盘的访问次数太多,对硬盘来说这是有害的。这时,就可以利用缓冲字节流。

再上图:

缓冲字节流复制文件原理

根据下面的代码来理解一下这张图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test {
public static void main(String[] args) throws IOException {
//1.源文件,目标文件
File f1=new File("i:/test/haha.txt");
File f2=new File("i:/test/demo.txt");
//2.创建流:4个
FileInputStream fis =new FileInputStream(f1);
FileOutputStream fos=new FileOutputStream(f2);
BufferedInputStream bis=new BufferedInputStream(fis);//创建缓冲流bis
BufferedOutputStream bos=new BufferedOutputStream(fos);//创建缓冲流bos
//3.读取
byte[] b=new byte[8];
int len=bis.read(b);//bis 读取数据
while(len!=-1){
bos.write(b,0,len);//bos 写出数据
len=bis.read(b);
}
//4.关闭流:
bis.close();
bos.close();
fis.close();
fos.close();
}
}

程序解释:先创建输入字节流 fis (第 7 行)怼到目标文件(i:/test/haha.txt),然后创建缓冲字节流 bis(如图红色),bis 套在 fis 上使用(相当于一根管子上套了另一根管子)。使用缓冲流会有一个缓冲区,fis(字节输入流)会尽可能多的把源文件中的数据读取到缓冲区,然后 bis 再利用缓冲数组从缓冲区中每 8 个字节为一组的读取。写出的过程和读取的过程正好相反就不在此赘述啦。(如果还不理解可以留下评论)

这种方式属于一根流套在另一根流,也就是管套管。上使用这样的话就减少了对硬盘的访问次数。

6. 缓冲字符流–BufferedReader,BufferedWriter

缓冲字符流和上面的缓冲字节流的运行原理一样,只不过一个使用字节流,一个使用字符流而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test {
public static void main(String[] args) throws IOException {
//1.源文件,目标文件
File f1=new File("i:/test/haha.txt");
File f2=new File("i:/test/demo.txt");
//2.创建流:4个
FileReader fis =new FileReader(f1);
FileWriter fos=new FileWriter(f2);
BufferedReader bis=new BufferedReader(fis);//创建缓冲字符输入流 bis
BufferedWriter bos=new BufferedWriter(fos);//创建缓冲字符输出流 bos
//3.读取
char[] b=new char[8];
int len=bis.read(b);//bis 读取数据
while(len!=-1){
bos.write(b,0,len);//bos 写出数据
len=bis.read(b);
}
//4.关闭--先关高级流,再关闭低级流
bis.close();
bos.close();
fis.close();
fos.close();
}
}

这种方式也是属于管套管来操作数据,效率比上面的缓冲字节流要高(因为使用的是字符流)。

之前操作数据,要么是一个字节一个字节的读取,或者是一个数组一个数组的读取。那么下面介绍一种效率更高的方式:一整行一整行的读取数据

1
2
3
4
5
6
7
//3.读取
String str=bis.readLine();//利用缓冲字符输入流 bis 整行整行的读取数据;
while(str!=null){
bos.write(str);
bos.newLine();//在目标文件中换行
str=bis.readLine();//利用缓冲字符输出流 bos 整行整行的写出数据;
}

程序其他部分不变,只是读取的方式不一样。

7. System对 IO 的支持

在这里补充一下,我们写程序的时候经常会用到键盘输入这个语句:

Scanner sc=new Scanner(System.in);

那么我有没有考虑过键盘输入这件事到底是谁来完成的呢,是 Scanner 还是 Sytem.in ?

其实,键盘录入这个功能是由 System.in 来完成的,Scanner 只是起到一个扫描器的作用。System.in 会返回一个 InputStream 类型的变量,也就是返回一个流。那么Scanner sc=new Scanner(System.in);这条语句可以通俗的理解为有一个扫描器 Scanner,一个键盘,它们俩之间是用一根管子(流)连接起来,键盘录入的数据通过这根管子传进扫描器中。

还有一个比较坑的地方,写出来给大家做个提醒:

1
2
3
4
5
6
7
8
public class TestIO {
public static void main(String[] args) throws IOException {
InputStream in = System.in;
byte[] b=new byte[8];
int n = in.read(b);//用数组读取键盘输入的数据,返回结果为 b 被占用的长度
System.out.print(n);
}
}

代码第 5 行中,in.read(b) 按理说返回结果为 b 被占用的长度啊,那么,我输入数字 1,结果输出 3,也就是说,占用了 3 个字节!哇,当时整的我很郁闷,不就占用了一个字节吗,应该是 1 才对嘛。
后来才搞清楚,运行这段代码的时候,你输入 1 之后,按下了 Enter 键,也就是输入完数据之后进行了回车、换行,而回车和换行在 ASCII 表中对应的数值分别是 13 和 10,它们俩又各占了一个字节,所以是占用了 3 个字节,输出结果为 3。不知道我有没有表达清楚?

  • 补充一点:字节流转化为字符流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Test {
public static void main(String[] args) throws IOException {
//输入:
InputStream in = System.in;
//转换流--单向转换:字节流-->字符流转换
InputStreamReader isr=new InputStreamReader(in);
BufferedReader br=new BufferedReader(isr);
//输出:
FileWriter fw=new FileWriter("d:/bjsxt/t.txt");
BufferedWriter bw=new BufferedWriter(fw);
//读取:
String str=br.readLine();
while(!str.equals("byebye")){
bw.write(str);
bw.newLine();
str=br.readLine();
}
//关闭流:
bw.close();
br.close();
fw.close();
isr.close();
in.close();
}
}

这段代码中,主要想表达的就是第 6 行,将字节流转化为字符流,其他的就是和之前的代码差不多,都是读取写入。

8. 数据流–对基本数据类型处理–DataInputStream,DataOutputStream

8.1 将基本数据类型的东西输入到目标文件中去:

1
2
3
4
5
6
7
8
9
10
11
12
public class Test001 {
public static void main(String[] args) throws IOException {
DataOutputStream dos=new DataOutputStream(new 4FileOutputStream(new File("d:/haha/demo001.txt")));//将流套在一起使用
dos.writeInt(12);//int
dos.writeChar('\n');
dos.writeDouble(12.0);//double
dos.writeChar('\n');
dos.writeUTF("hellojava你好");//
//关闭流
dos.close();
}
}

代码第 3 行是将各个需要的流套在一起直接一句代码写出来了,应该能看懂吧。

执行完这段程序之后如果打开目标文件,将会看到…乱码,哈哈。别慌,因为这是给程序看的,咱可能看不懂的。那么要是想看该怎么办呢?有办法,在一个程序中执行下段代码:

1
2
3
4
5
6
7
DataInputStream dis=new DataInputStream(new FileInputStream(new File("d:/bjsxt/demo001.txt")));
System.out.println(dis.readInt());//int
System.out.println(dis.readChar());//char
System.out.println(dis.readDouble());//double
System.out.println(dis.readChar());//char
System.out.println(dis.readUTF());
dis.close();//关闭流

这段代码是为了将文件中的内容写入到程序中,然后在控制台输出。第一句不用解释了吧。而且,重要的是写进文件的顺序和读到程序中的顺序必须一致!就是说,假如你写进文件的第一个是 int 类型的,那么输出的第一个也必须是 int 型的,这样一一对应才能保证不出错。

9. 对象流–对引用数据类型处理–ObjectInputStream,ObjectOutputStream

9.1 放入String 类型数据:

1
2
3
4
5
6
7
public class Test002 {
public static void main(String[] args) throws FileNotFoundException, IOException {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(new File("d:/haha/t.txt")));
oos.writeObject("java");
oos.close();//关闭流
}
}

这段代码就是将 “java” 写到目标文件中去。

9.3 现在要写入 Person 的一个对象:

假如说我自定义了一个 Person 类。,然后我现在要把一个 Person 类型的对象放进目标文件中去,按照上面的放 String 类型数据的方法:

1
2
3
4
5
6
7
public class Test002 {
public static void main(String[] args) throws FileNotFoundException, IOException {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(new File("d:/bjsxt/t.txt")));
oos.writeObject(new Person("lili", 18));
oos.close();
}
}

这段程序运行结果如下:

运行结果

发现出错了,“NotSerializableExeption”,啥意思呢,就是说 Person 类没有序列化!!

那么怎么解决呢?方法:实现 Serializable 方法,加序列号。

调试方法

谨记:以后如果要对类的对象进行网络传输一定要实现序列化!!!


如果您能看到这里,我真的表示万分感谢,这篇文章如果有什么错误的地方,或者您不理解的地方,欢迎留言。另外我这儿有这部分学习的视频,如果需要的话,也可以留下邮箱,我发给您。就这样,谢谢各位!