死磕Java基础–Java中的I/O流,看这个就够了!

>>2020,微服务装逼指南

死磕Java基础--Java中的I/O流,看这个就够了!

网上的技术文章有很多,但是写作手法都大同小异,在这里我想以一种新颖的方式带给大家不一样的技术文章学习体验,我将采用一问一答的方式进行创作,在这里会有两个角色,即庆哥和小白!

一、什么是IO

庆哥:小白,你知道什么是IO吗?

小白:我英文不是太好,但是在这里这个I指的应该是input,而这个O指的则是output吧!对于IO我的认识就是输入输出了,不知道这样理解是否正确!

庆哥:对于IO我们理解成输入输出很正确,但是我们一定要注意一个非常重要的点那就是“流”,什么意思呢?在这里我们更准确的来说其实是输入输出流,因为我们在讨论IO的时候,流则是关键中的关键,我也听过很多人对IO的描述就是输入输出流,这其实是IO的一种分类,在这里想给你说下对于IO我们可以如下分类

  • 按数据流向分类 输入流和输出流

  • 按处理的数据类型分类 字节流和字符流

不知道看到这里你有什么发现没?

小白:到这里我发现了两个比较重要的关键词,那就是数据

庆哥:非常正确,我们之前说过我们在说IO的时候一定逃不过“流”的概念,而另外一个同样重要的就是数据了,那到底什么是流呢?

揭秘输入输出流中的“流”

小白:我们常说的IO流和输入输出流中的这个流应该是一样的吧!

庆哥:没错,这确实是一样的,那么该怎么理解这个流呢?这里我们完全可以类比这水流去理解,只不过我们这里的流指的则是数据了,结合两个概念“流”和“数据”也就得到我们的重点数据流,就如同我们的水流一样,对于水流,那流入流出的都是水,而我们的数据流流入流出的自然就是数据了,所以记住非常重要的一句话

流的本质就是数据传输

那么现在我问你,IO是用来干嘛的?你该怎么回答

小白:都讲到这地步了,我必须明白啊,IO就是用来进行数据传输的

庆哥:很正确,接下来我们还需要弄清一样东西,那就是输入输出流,这对我们后续对IO的学习很重要,那你知道什么是输入输出流吗?

小白:既然流是用来传输数据的,那么输入输出流肯定是针对具体的数据而言。。。算了,不是很理解,不知道该怎么表述!

何为输入输出流

庆哥:首先明白一点,我们这里说的IO指的是Java中的IO,那么对与Java而言我们该如何去理解这个输入输出流呢?接下来我们看一张图你会清晰很多

死磕Java基础--Java中的I/O流,看这个就够了!

有的时候很多人会搞不明白这个输入和输出,总是混淆怎样才是输入,怎样又是输出,我们看这个图也许就能明白了,在Java中输入流就是从某地方进行数据读取然后输入到我们的Java程序,然后有我们的Java程序再输出到某个地方,这就是一个数据传输的过程。

那么我们学习IO到底在学习什么呢?其实这里的Java程序就是我们的额JavaIO来实现的,我们就是要学习IO中哪些东西可以将数据输入进来,哪些又可以将数据输入出去,不知道这样讲你是否明白呢?

小白:嗯嗯,经你这么一说,顿时感觉清晰了好多啊。那么解析来我们是不是就要具体学习IO中那些可以操作数据流向的知识了?

庆哥:是的,接下来我们就从第一个知识点开始,那就是我们的File类。

认真对待File类

庆哥:可以说我们学习Java的IO第一个要学的就是File类,那么为什么要学这个类呢?因为这个类提供了很详细的对文件操作的功能,想一下我们的输入输出流,就是对数据进行操作啊,比如我们想在某个盘符下读取一个文件,将这个文件中的内容读取到然后输出到另一个盘符下的某个文件,在这个过程中我们需要做哪些操作呢?

小白:如果是这样的话闹我们就必须拿到这些文件,然后还得有特定的方法供我们使用。

庆哥:对的,而这个File就可以帮我们完成,我们一步步来,先来看一段程序,首先我在我的电脑F盘下新建一个a.txt的文件,然后我们对这个文件进行操作

死磕Java基础--Java中的I/O流,看这个就够了!

我们看这个代码,应该很容易看懂吧,我们通过创建一个File对象来拿到我们要操作的对象,只有拿到了我们想要操作的对象之后我们才能对该对象进行相应的操作。

小白:这里File的参数是不是就代表我们要操作对象的路径啊,比如这里我们要操作a.txt这个文件,那么这个F:/a.txt就是该文件在我们电脑中的位置?

庆哥:完全正确,你想啊,既然我们要操作某一文件对象,你起码要告诉我这个文件在哪吧!

小白:那这个file是不是应该还有其他的一些方法吧!

庆哥:这是必须的啊,它有这么多方法呢?

死磕Java基础--Java中的I/O流,看这个就够了!

我们可以看到通过这个file我们可以调用很多方法,而对这些方法我们基本上看看名字就能知道方法的作用,而这里有一个最好的学习方法是什么呢?这里有这么多方法,我不可能一一去演示每一个方法的作用,那么该怎么办?当你需要用到哪个方法的时候尽管去尝试就行了。

小白:嗯嗯,确实,在学习的过程中我们还是要多动手才行啊,对了,咱们操作的这个文件也就是a.txt是不是咱们自己手动创建的啊?

庆哥:是的,这个文件实我事先在F盘中创建的,其实我们还可以通过程序来帮我们创建文件,比如我们在F盘下再创建一个b.txt文件,我们可以这么做

死磕Java基础--Java中的I/O流,看这个就够了!

这时候你就会发现你的F盘下多了一个b.txt文件,我们可以看到,我们这里依然是使用了file的一个方法,而这个方法我们从方法名就可以知道就是一个创建新文件的方法,这也提示我们平常再编码中命名一定要有所含义,最好是其功能的说明,不要随随便便就起一个名字。

但是这里应该会有一个问题,你能想出来吗?

小白:问题嘛,我想想,这里是新创建一个文件,那么,如果我本地已经存在这个文件,那这是将原来的覆盖还是新创建一个副本啊

庆哥:思考的非常好,其实如果本地已经有文件的话,这个方法就不会再创建新文件了,也就是只有当文件不存在才会创建一个新的文件,当然也就不存在覆盖之说了,所以正确的写法应该是这样滴。

死磕Java基础--Java中的I/O流,看这个就够了!

这样写是否明白呢?

小白:必须明白啊,也没有那么难嘛,哈哈

庆哥:那是,难了不会,会了不难,咱们继续,我们看我们以上的例子都是在创建文件,那我们通过这个File类是不是也可以创建文件夹呢?答案是肯定的,我们可以这样操作

死磕Java基础--Java中的I/O流,看这个就够了!

其实对于这段代码我觉得都不用怎么说,相信你都能看懂,因为这个跟创建文件的代码几乎一样唯一不同的就是调用方法不同。

但是在这里对于创建文件夹我们有一个需要说明的,那就是这两个方法的区别

死磕Java基础--Java中的I/O流,看这个就够了!

你能猜出来他们的区别嘛?

小白:这个应该都是创建文件夹,那么这个带s的是不是就意味着可以创建多级文件夹?

庆哥:对的,这个带S的可以帮你创建多个嵌套的文件夹,比如新创建一个新的文件夹a并且在a中再创建一个文件夹b,具体的我不多说,你试一下就知道了。

好了到了这里我们学习了IO中的第一个基础的东西也就是文件,关于这个主要是对File类中相关方法的运用,可以多去尝试下,接下来我们就开始学习一些流相关的东西吧!

流的操作

庆哥:我们发现我们之前说的都是在操作文件或者或文件家,比如我们创建一个文件a.txt,我们把这个文件创建成功了,但是这个文件是一个空的,里面并没有内容,那么我们如何去在这个文件中写上一些内容的,或者一些其他的文件内容操作

小白:我觉得肯定也是有一个相应的类吧

庆哥:要学习这里我们必须明白几个概念,我们在之前说过对于IO如果我们按数据流向也就是分为输入流和输出流,这是一个大的方向,如果我们按小的方向也就是数据类型划分的话是分为字符流和字节流的,所以这里我们要清楚什么是字节流什么是字符流,但是要说这个之前我们最好还有明白什么是二进制文件什么是文本文件?

小白:二进制文件和文本文件?我觉得文本文件应该就是那些内容是纯文本的吧,比如txt文件或者我们写的Java文件,至于二进制文件还真说不清楚!

庆哥:是的,你对文本文件理解的很正确,对于二进制文件就是我们平常见到的图片,音视频等,明白了什么是二进制文件和文本文件,那我们来看看什么是字节流和字符流

  1. 字节流 对于字节流,它是用来操作我们的二进制文件的,为什么呢?因为字节流可以操作的数据是8位,也就是一字节,我们知道1 byte = 8bit,而像一些数字和字母等都是占一个字节,这就可以使用字节流来操作,但是对于中文的话就不能使用字节流了

  2. 字符流 因为一个汉字是占两个字节,那么就是16位,字节流是操作不了的,而字符流则可以操作16位,所以对于文本文件则常用字符流操作了

小白:哦哦 ,明白了,不过这个字符流和字节流应该也是一个统称吧,具体指的是哪些呢

FileInputStream和FileOutputStrem

庆哥:是的,具体的有好几个呢,但是我们只需要学习几个重点的就行,首先就是 FileInputStream和FileOutputStrem是字节流张比较重要的操作输入和输出的两个类,我们先从它俩开始 我们之前使用File可以创建一个文件,比如我们之前创建的是一个a.txt文件,接下来我们就使用 FileInputStream和FileOutputStrem去操作一下这个txt文件吧

读取文本

小白:我们之前不是说对于文本的操作使用字符流嘛?可是这里我们不是在说字节流嘛?

庆哥:对于到底是使用字节流还是字符流我们要明白重点是什么,重点是你要操作的数据,如果是中文的话我们当然不能使用字节流而要使用字符流,但是如果你的文本内容是一些数字或者字母,这就可以使用我们的字节流啊,对于字节流和字符流是都可以用来操作文本文件的,关键看你的文本内容

小白:哦哦,明白了,要看你的内容到底是个什么玩意

庆哥:对的,接下来我们在a.txt先手动写上abc三个字母,然后我们这样操作

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
26
 //创建一个File对象,拿到我们的数据
        File file = new File("F:/a.txt");        
        if (!file.exists()){     
              try { 
                  file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }else {
            System.out.println("文件已存在");  
              try {
                  //read的返回值为-1说明文件读取完毕
 
                FileInputStream fileInputStream = new FileInputStream(file);                
    //read的读取方式为一个个的读取,也就是说你调用一次read就会读取下一个字节
                int i = fileInputStream.read();               
                while (i > 0){
                    System.out.println((char)i);
                    i = fileInputStream.read();
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

我们知道FileInputStrem是一个输入流,可能说输入流你会犯晕,不理解,我们其实是可以把输入流理解为”拿到“,比如这里我们使用FileInputStrem就是拿到a.txt中文本内容,我的文本中有abc三个字母,然后我们第一次调用read方法会帮你拿到a,我们看返回的是一个int类型,可以知道返回的其实是a的ASCII码,我们需要使用char来进行转换,我们在代码中也说了,当read的返回值是-1的话也就代表文件读取完毕,这里我们要注意的就是read每调用一次就会读取文本内容的下一个字节 那么你对这段代码是否了解呢?尤其循环中的代码

小白:嗯嗯,你这么一说我就明白了,但是我觉得我们在开发中应该不会这样做吧?

庆哥:为什么呢?

小白:这样一个个的读取,效率是不是有点低啊

庆哥:确实,所以我们还有这么一种方式

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
26
27
28
29
30
31
32
//以byte数组方式读取
    public static void main(String[] args) {
        File file = new File("F:/a.txt");
        if (!file.exists()){
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }else {
            System.out.println("文件已存在"); 
           byte[] bytes = new byte[1024];  //1kb
 
            try {
                FileInputStream fileInputStream = new FileInputStream(file);
                //以一个kb的方式去读取,如果文本内容不超过一个kb的话就会全部读取,将读取到的内容放在byte数组中并且返回文本内容的字节数
                int read = fileInputStream.read(bytes); 
                String s = new String(bytes);
 
 
                System.out.println(s);
                System.out.println(read);
 
 
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
 
    }

这种方式我们就是以byte数组方式读取,首先我们定义一个byte数组

byte[] bytes = new byte[1024]; //1kb

为什么要定义这么一个数组,我们看这个byte数组其实就是1kb大小,我们之前是一个字节一个字节读取,我们现在是1kb1kb的去读,然后这样做

1
2
3
FileInputStream fileInputStream = new FileInputStream(file); 
//以一个kb的方式去读取,如果文本内容不超过一个kb的话就会全部读取,将读取到的内容放在byte数组中并且返回文本内容的字节数
int read = fileInputStream.read(bytes);

之前一个字节读取的时候read是没有参数的,现在传入一个byte数组意味着按照byte数组大小读取,然后读取到的文本内容存放在这个byte数组中再返回一共读取的字节数

我们也需要对这个读取到的byte数组进行转换

   String s = new String(bytes);

如此一来我们就得到了我们的abc

小白:嗯嗯,看这种方式觉得才好,那么在开发当中是不是使用的这种饭是钢hi多一点啊

庆哥:确实,其实对于read的字节读取方式我们是很少用的,就像你说的,效率低。

写入文本

小白:我们现在是将文本内容读取到了,那么我们可不可以将它再写入到一个文件

庆哥:这个当然可以,这个我们就要使用到我们的输出流FileOutputStrem了,你看我们可以这样操作

1
2
3
4
//写入
FileOutputStream fileOutputStream = new FileOutputStream("F:/b.txt");
 
fileOutputStream.write(bytes);

我们看就这么两行代码,那是因为我们在操作输入流的时候把一些工作已经做了,我们看第一行代码就代表着你要往哪个文件中写入文本,然后我们执行write就可以开始写入了,而这个byte就是我们之前使用输入流从a.txt中拿到的文本内容,这里有一点需要注意,我们看代码

FileOutputStream fileOutputStream = new FileOutputStream("F:/b.txt",true);

这里增加了一个true,什么意思呢?这里如果你不写的话默认就是false,那么你写入文本的时候就会覆盖之前的内容,但是如果你加上true的话就代表在原有文本之后追加

小白:嗯嗯,记着了,对了其实我一直有一个问题,我们说IO不就是InputStrem和OutputStrem嘛。为什么我们使用的是FileInputStream和FileOutputStrem

庆哥:我看你是迷糊了吧,要知道InputStrem和OutputStrem可都是接口啊,接口是不能被实例化的,我们只能使用它的实现类,明白了吧

小白:好吧,我竟然忘了InputStrem和OutputStrem都是接口了,咱这个跳过去哈

字符流

庆哥:好的,那咱们就来看看字符流吧,对了,我们之前讲了字节流,那你知道字节流的读取单位是什么嘛?

小白:字节流是按字节读取那就是byte了

庆哥:回答完全正确,那你知道我峨嵋你学习字符流主要学习哪几个类嘛

小白:这个嘛,我还真不清楚,还是你来说吧

庆哥:对于字符流,像字节流中的InputStrem和OutputStrem一样,也有两个接口那就是Reader和Writer,当然我们也是需要去学习它们的实现类,同样,它们的实现类有好几个,我们不可能每一个都学,我们会说几个比较重要的。

小白:晓得了,接下来是不是先从Reader的实现类开始啊

庆哥:是的,接下来我们就来看几个比较重要的

Reader

庆哥:对于Reader,我们需要首先熟悉并且会使用它的这几个实现类,对了,这里说的可都是输入流

  1. BufferedReader
  2. FileReader
  3. InputStremReader

InputStremReader

我们首先说一下这个InputStremReader,为什么呢?因为这个我们在平常见到的比较多,而且它和FileInputStrem也很想,我们知道对于FileInputStrem,它是一个字节流,读取的单位是byte,我们看它的参数中是可以传入byte类型数据的,而对于InputStremReader,它所不同的就是传入的参数从byte编程char了,这就是两者的区别,由此我们使用InputStremReader就可以读取中文了,因为一个汉字占两个字节,而char就是两个字节,下面我们看一个例子,首先在我们之前创建的a.txt文件中写上”一个自学的程序员“

接下来我们读取一下试试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
File file = new File("F:/a.txt");
        if (!file.exists()){
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }else {
            System.out.println("文件已存在");
            try{
                FileInputStream fileInputStream = new FileInputStream(file);
                //获取字符输入流
                InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream,"gbk");                char[] c = new char[64];               
                int read = inputStreamReader.read(c);
 
                System.out.println( new String(c));
 
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

我们来看这段代码,使用到InputStremReader的地方在这

1
2
//获取字符输入流
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream,"gbk");

我们在这里创建一个InputStremReader对象,可以在这里传入了两个参数,第一个是一个字节流,也就是说这个InputStremReader是需要传入一个字节流的,InputStremReader是比较特殊的,它可以传入编码格式,比如这里如果我不传入这个gbk,我得到的文本内容则是乱码,加了这个就可以了,要知道其他字符流是不可以的,然后我们看这段代码

1
2
3
char[] c = new char[64];                
int read = inputStreamReader.read(c);                
System.out.println( new String(c));<code class="hljs" style="display: inline; overflow: visible; color: #333333; background: transparent; font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Courier, monospace, sans-serif; border-radius: 3px; line-height: inherit; word-wrap: normal; border: 0px initial initial;"></code><code class="hljs" style="display: inline; overflow: visible; color: #333333; background: transparent; font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Courier, monospace, sans-serif; border-radius: 3px; line-height: inherit; word-wrap: normal; border: 0px initial initial;"></code>

这段代码应该是比较熟悉的吧,因为这个跟我们在讲FileInputStrem读取的时候几乎是一样的,只不过这里变成了char,其他的基本上和FileInputStrem是一样的。

小白:那么这两个学习的时候就可以互相参照对比着学习了吧

庆哥:是的,接下来我们再来说说其他的字符流

FileReader

庆哥:接下来我们看一下另一个字符输入流,那就是FileReader,那么你能知道这个FileReader怎么使用嘛?

小白:这个我觉得既然都是字符输入流,那么在使用方法上应该也很相似吧

庆哥:这分析的倒是没错,我这里贴出FileReader的使用方式代码,你看下就知道区别了

死磕Java基础--Java中的I/O流,看这个就够了!

你能看出来区别吗?

小白:我知道了,对于FileReader它不需要像InputStremReader一样传入一个字节流,同样好像也不能直接传入编码格式吧,至于其他的操作几乎就是一样的了

庆哥:对的,其实就是这样的,相同的地方很多,不同的其实就那么一点,那么接下来我们就再来说一下这个BufferedReader。

BufferedReader

庆哥:这个BufferedReader我们可以好好说一下,因为这个在平常使用的挺多的,相信你应该也有印象,我们使用BufferedReader读取文本内容可以保证文本内容的格式不被打乱,反正吧,关于BufferedReader的用法一定要熟练,BufferedReader我们叫做缓冲流,当然是对Reader做缓冲的,所以使用BufferedReader一定会用到Reader,接下来我们看看代码就知道了

1
2
3
4
5
6
7
8
9
//  使用InputStreamReader
FileInputStream fileInputStream = new FileInputStream(file);
 //获取字符输入流
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream);
                BufferedReader bufferedReader = new BufferedReader(inputStreamReader); 
               String line = null; 
               while ((line=bufferedReader.readLine())!= null){
                    System.out.println(line);
                }

这段代码一定要记住了,烂熟于心,我们看代码知道这个BufferedReader需要接收一个字符流 InputStreamReader,然后就可以直接调取readLine方法进行整行读取,会得到一个字符串,可以直接输出,因为是整行读取可以保持格式不乱,真的很好用,所以要记住了

小白:确实,这段代码我好像也写过,当时还不太明白呢

庆哥:其实IO这块一些实现类很多,我们不可能每一个都掌握,我们只需要熟悉常见的并且会使用即可,至于其他的一些我们用到的时候查一下即可,下面一张图,给你感受下

死磕Java基础--Java中的I/O流,看这个就够了!

这就是IO中的字节流和字符流了,而且每一种又分为相应的输入流和输出流

小白:我的个天,这是不是有点多啊

庆哥:是啊,所以全部学完有点不太现实,我峨嵋你只需要记住常使用的即可

Writer

庆哥:我们知道有写入肯定有输出,我们之前都是将a.txt中的文本内容写入,也就是得到那些文本内容,接下来我们看看看如何将这些文本内容输出到另一个文件也就是我们的b.txt

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.io.*;public class ReaderDeno {
    public static void main(String[] args) {
        File file = new File("F:/a.txt");
        File file1 = new File("F:/b.txt");
        if (!file.exists()){
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }else {
            System.out.println("文件已存在");
            try{
                //使用InputStreamReader
                FileInputStream fileInputStream = new FileInputStream(file);
                //获取字符输入流
                InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream);
 
                BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
 
 
                FileOutputStream fileOutputStream = new FileOutputStream(file1);
                OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream);
                BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
                String line = null;
                while ((line=bufferedReader.readLine())!= null){
                    System.out.println(line);
                    bufferedWriter.write(line);
                    bufferedWriter.newLine();
                }
 
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

我这里将所有的代码都贴出来了,我们要注意输出到b.txt的代码,也就是这些

死磕Java基础--Java中的I/O流,看这个就够了!

这段代码是没有错误的,但是你却发现你始终是无法完成对b.txt文件的输出,这是为什么呢?

小白:我看不出来,这是怎么回事啊

庆哥:因为自始自终我们一直忽略了一个大问题,那就是IO流的关闭

记得IO流的关闭吗

庆哥:因为一个流如果没有关闭,则处于被占用状态,那么一些操作我们是完成不了的,在jdk1.6及之前我们都需要手动的去关闭流,那是相当的麻烦,但是在之后的版本中就可以让Java自动帮我们关闭流了,我们只需要这样做就可以了

死磕Java基础--Java中的I/O流,看这个就够了!

看明白了吗?也就是只需要在try之后加一个括号,将出现异常的代码放到这里,也就是各种流,然后Java就可以自动帮我们关闭流了,这个时候再执行程序就可以看到,b.txt已经有复制的内容了。

今天就先讲到这,后续会补充一些知识!

死磕Java基础--Java中的I/O流,看这个就够了!