您当前的位置:首页 > 计算机 > 编程开发 > Java

Java IO流真的看这一篇就够了

时间:07-05来源:作者:点击数:

一、IO流简介

1)什么是IO流?

I 是 input(输入)的缩写,O 是 output(输出)的缩写,流是指数据。

2)什么是输入,什么是输出?

我们的计算机上,有内存和硬盘两个可以存放数据的地方,内存中的数据的临时的,硬盘中的数据的持久的。当我们从硬盘中“读取”(read)数据到内存中使用时,称为“输入”(input);当我们将内存中的数据“写入”(write)到硬盘中存储时,称为“输出”(output)。【注意:“读取”对应“输入”,“写入”对应“输出”。背好这句,后面贼好用!】

3)IO流有哪些分类?

根据流的方向进行分类:输入流和输出流。

根据数据的读取方式进行分类:字节流和字符流。

4)什么是字节流,什么是字符流?

如果对输入和输出理解了,那么对输入流和输出流这种分类方式应该不难理解。那么什么是字节流和字符流呢?

字节流,是指数据的读取方式是一次读取1个字节byte,这种流能够读取任何类型的文件,比如音频、视频、图片等都可以;字符流,是指数据的读取方式是一次读取一个字符,是只为了读取文本文件(仅仅只对 【.txt】 文件)而存在的,因此不能读取音频、视频等类型的文件。举个例子:

假设有一个文本文件,是a.txt,文件中的内容如下:a张三12李四
在windows系统的文本文件中,一个数字、符号或者是字母,其大小是1个字节;一个汉字的大小则会因为编码格式的
不同而占用不同的字节。

假如用字节流读取:第一次读取'a',第二次读取'张'的一部分,第三次读取'张'的另一部分......
假如用字符流读取:第一次读取'a',第二次读取'张',第三次读取'三'......

5)IO流怎么学?

java中IO流对应的多种类都已经写好了,初学者不需要关心其底层原理,只需要掌握java为我们提供了哪些流,流的特点是什么,常用方法有哪些即可。且要知道java中所有的流都是在 java.io.* 下面。

6)先看一下大概的IO流家族,如下图,有加中文的,本篇博客都会介绍到。

上图中,带“File”的是文件流,带“Buffered”的是缓冲流,带“StreamReader”的是转换流,带“Data”的是数据流,
带“Print”的是打印流,带“Object”的是对象流,我们会在后面一一介绍到。

注意

1)在java中,类名以“Stream”结尾的就都是字节流;以“Reader/Writer”结尾的就都是字符流。

2)InputStream、OutputStream、Reader、Writer都是抽象类。所有的流都实现了Closeable接口,都有close()方法,都是可以关闭的,且流在使用之后是必须关闭的。

3)所有的输出流还实现了Flushable接口,都有flush()方法,每一次使用完输出流之后,先flush()后,再close()。flush()方法可以将没有输出的数据强行输出,防止丢失数据。


二、FileInputStream、FileOutputStream【重要】

1、FileInputStream

FileInputStream,包含“Stream”,说明它是一个字节流;包含“File”,说明它是专门用于读取文件的流。

现在,我们直接用代码来表述,说明这个类的使用。我们创建一个文件 F:\a.txt,文本内容是 1a.中 。注意这个文本内容很具有代表性,1是数字,a是字母,. 是符号,中是汉字。

然后我们尝试将该硬盘文件中的内容读取并打印出来,看看我们能打印出什么。

public class IOTest01 {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
        //1)先创建流对象,构造方法 FileInputStream() 会抛出异常,需要处理
            fis = new FileInputStream("F:\\a.txt");

		//4)reaf()方法是读取一个字节。
		//在ASCII编码表中,字符'1'对应49,字符'a'对应97,字符'.'对应46
		//可以看到,汉字'中'对应3个字节,分别是228、184、173
		//当读取到-1后,表示文本已经读取完了,没有其他内容了
            int readData = fis.read();
            System.out.println(readData); //49

            readData = fis.read();
            System.out.println(readData); //97

            readData = fis.read();
            System.out.println(readData); //46

            readData = fis.read();
            System.out.println(readData); //228

            readData = fis.read();
            System.out.println(readData); //184

            readData = fis.read();
            System.out.println(readData); //173

            readData = fis.read();
            System.out.println(readData); //-1

            readData = fis.read();
            System.out.println(readData); //-1
            //2)FileInputStream() 抛出的异常在这里处理
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) { //5)read()方法也存在异常,因此也需要捕捉
            e.printStackTrace();
        } finally {
        //3)流使用之后,需要关闭,在finally中进行关闭
            if(fis!=null){ 
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

对上述代码的解释,按注释给的顺序阅读代码,很容易懂

对 if(fis!=null) 的解释:

这个判断条件很有必要加,加完之后。
假设fis为空,那么就不需要关闭了,就不会执行 if{ } 中关闭流这些代码;如果fis不为空,就关闭流。
这样做的目的是防止空指针异常。
你想想,如果不加的话,fis为空,即fis这个引用没有指向任何对象,那它怎么调用close()方法,这样就会发空指
针异常。

对read()方法的解释:

read()方法就是“读取”,每次读取一个字节大小的数据,返回一个int类型的字面量。
每读取一个,光标就会往下移动一个字节,以便读取下一个字节。
当读取的返回值是-1时,表示文本已经读完了。

对’中’读取了三个字节的解释:

字母,英文符号,数字只占一个字节。但是汉字的字节数是不确定的,编码格式不同,汉字的字节数也可能会不同。

细心的小伙伴应该有发现到,上面读一个字节,打印一个字节,代码大量重复,因此我们优化一下代码,用循环来读取。代码如下:

public class IOTest01 {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("F:\\a.txt");
            //1)我们定义一个变量readData,来存放读取到的字节
            int readData = 0;
            //2)以下代码的意思是,只要读取到的字节不为-1,也就是文本没有读完,就一直循环地读,并把
            //读到的值赋值给readData,同时打印readData。
            while((readData=fis.read())!=-1){
                System.out.println(readData);
            }
        //!!!注意从现在开始,这行以下代码基本都是相同的,我们不需要再关注它们
        //重要事情再说一遍,接下来的重复代码,我们看try{}中的代码即可
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(fis!=null){
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

学会了怎么使用输入流之后,我们反过来,再来看看JDK帮助文档,我们发现,read()方法还有另一个重载方法,如下:

在这里插入图片描述
read()方法:这个方法,是将一个字节从硬盘读取内存,再继续读下一个,依次往返读取。这就好比是送外卖,外卖
小哥一次从饭店拿一个饭盒,送给客户,然后又回到饭店,再拿一个饭盒....显然这样做的效率十分低下。
read(byte[] b)方法:这个方法,是将读取到的多个字节存放到字节数组中,然后一次性搬到内存。这就好比外卖小
哥学聪明了,他用大袋子装了很多饭盒,这就不用往复跑很多次了。这样,效率就变高了。

注意:read()的返回值是一个小于256的int类型的数字;read(byte[] b)的返回值表示读到了几个字节数量,比如我们
定义一个byte[1024],也就是能存1024个字节的数组,然后我们的文件大小是1025个字节,那么使用循环读取,第
一次返回的int是1024,第二次是1,第三次是-1。具体看下面的代码。

我们把 a.txt 文件的内容改成 abcde,把文件放到项目下

我们来看一下代码是怎样的:

public class IOTest01 {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("a.txt");
            //注意这里的变量名从readData变成了readCount,因为使用read(byte[] b)方法,其返回值是
            //字节数量,而不是字节这个数据本身
            int readCount = 0;
            //1)定义一个字节数组,长度为4
            byte[] bytes = new byte[4];
            
            //2)将字节4个4个地读取到bytes中
            readData = fis.read(bytes);
            System.out.println(readData); //4

            readData = fis.read(bytes);
            System.out.println(readData); //1

            readData = fis.read(bytes);
            System.out.println(readData); //-1
            
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(fis!=null){
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

同样的,上述代码也可以用循环进行优化

public class IOTest01 {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("a.txt");
            int readData = 0;
            //1)定义一个字节数组,长度为4
            byte[] bytes = new byte[4];
            
            //2)将字节4个4个地读取到bytes中
            while((readData=fis.read(bytes))!=-1){
            //3)这一步稍作修改,使用String类的构造方法传入一个字节数组,将字节数组中的数据转换成
            //字符串,从下标0读到下标为readData的位置
				System.out.println(new String(bytes,0,readData));
			}
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(fis!=null){
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2、FileInputStream的其他方法

在前面的学习中,我们只学习了FileInputStream的两个方法,read()和close()。现在,我们来了解另外的方法,可能以后会用到。

1)avaliable()方法:该方法返回文件中剩下的字节数量,注意是数量

我们的文本内容依然是 abcde,来看看avaliable()方法有什么妙用

public class IOTest01 {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("a.txt");
            //1)先不读,光标落在文件的初始位置,因此available()会返回文件的总字节数量,将其作为
            //byte[]数组的大小,我们就不需要循环读取了
            byte[] bytes = new byte[fis.available()];
            fis.read(bytes);
            System.out.println(new String(bytes));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(fis!=null){
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2)skip()方法:跳过指定数量的字节,不读

public class IOTest01 {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("a.txt");
            fis.skip(3); //跳过3个,我们推测接下来读到的是d,而不是a
            System.out.println(fis.read()); //100.ASCII编码表中,字符'd'对应100.
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(fis!=null){
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3、相对路径和绝对路径

绝对路径:指从盘符开始的路径

相对路径:指当前项目的路径

这样说简直不是人话,我们用图说话吧。

首先是绝对路径:我们在javase这个项目下面新建一个a.txt文件,那么这个文件的绝对路径就是 F:\software\IDEA\project\javase\a.txt

在这里插入图片描述

然后是相对路径,a.txt 的相对路径是什么呢?上面是说,当前项目的路径。诶,我们的 a.txt 不就是刚好就在 javase 这个项目下吗?所以相对路径就是 a.txt 。如下代码,盘符和前面的各层文件夹都不用写出来了。

FileInputStream fis = new FileInputStream("a.txt");

来,考一考各位,以下将 a.txt 放在src文件夹下,该怎么写相对路径呢?如果会的话,说明你就掌握了。

在这里插入图片描述
//同样的,盘符和前面的各层文件夹都不用写
FileInputStream fis = new FileInputStream("IO流\\src\\a.txt"); 

另外值得一提的是,在java中,以下路径写法都是对的

FileInputStream fis = new FileInputStream("IO流\\src\\a.txt"); 
// 使用两个 \ ,是因为java中的字符串中,\是转义字符,在字符串中两个 \ 才能表示一个 \
FileInputStream fis = new FileInputStream("IO流/src/a.txt");

4、FileOutputStream

学明白FileInputStream之后,FileOutputStream就简单了。我们直接看代码

public class IOTest01 {
    public static void main(String[] args) {
        FileOutputStream fos = null;
        try {
	        //同样的,我们关注try{}中的代码即可
	        //1)假设我们的new.txt文件是不存在。那它会像FileInputStream那样出现找不到文件的异常吗?
	        //答案是不会,如果new.txt文件不存在,由于这是个相对路径,因此会在当前项目创建一个new.txt
	        //文件
            fos = new FileOutputStream("new.txt");
           	//2)我们要想 new.txt 中写入数据,先定义一个有数据的字节数组。97、98、99、100分别对应字
           	//符'a'、'b'、'c'、'd'
            byte[] bytes = {97,98,99,100};
            //3)调用write()方法将数组bytes中的数据写到文件 new.txt 中
            fos.write(bytes);
            //4)前面已经有提到,输出流在关闭之前,需要调用flush()方法清空数据流。防止数据缺漏
            fos.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(fos!=null){
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

执行以上代码之后,我们可以看到在 javase 文件夹下,多了一个新的 new.txt 文件,且文本内容是 abcd 。

关于write()方法

输出流的write()方法和输入流的read()方法其实是对应的。
read()方法有几个重载方法,read(byte[] b),read(byte[],int off,int length)(off是起始位置,length)
是所要读取的字节数。
类比过去,write()方法也是有write(byte[] b),write(byte[],int off,int length),他们的功能也都是类似
的。只不过read()是读取,write()是写入而已。也就是说write()是一个字节一个字节地写入,write(byte[] b)
是将字节数组中的多个字节同时写入。

此时,我们想在 new.txt 中,再加上 efg,使文本内容变成 abcdefg,我们将上面代码修改一下

byte[] bytes = {101,102,103};

继续执行,然后看看我们的文件。发现文本内容竟然是 efg,并没有达到我们想要的结果。我们去看看JDK的帮助文档。

在这里插入图片描述

发现还有另外一个构造方法,第二个参数 boolean append,当为true时,表示在文本内容末尾加上新的内容;当为false时,表示将文本内容覆盖掉,写入新的内容。这里就不再演示了,大家改改代码验证一下就行。

FileOutputStream fos = new FileOutputStream("new.txt",true);

接着,上面的代码中,我们想要向文件中写入 abcd ,这是因为我们知道在ASCII码表中,他们分别对应97、98、99、100,那要是我想写入汉字呢,比如我想写入“我爱中国”,有什么办法将这些汉字转换成字节吗?有的,我们可以使用getBytes()方法。

我直接写try中的内容。

try{
	fos = new FileOutputStream("new.txt",true);
	byte[] bytes = "我爱中国".getBytes();
	fos.write(bytes);
	fos.flush();
}

//当然,还能简化一点
try{
	fos = new FileOutputStream("new.txt",true);
	fos.write("我爱中国".getBytes());
	fos.flush();
}

5、文件复制

已经学习了FileInputStream和FileOutputStream,输入流是将数据从硬盘读取到内存,输出流是将数据从内存写入到硬盘,那这样的话,我们就能实现文件的复制了。这次不操作文本文件,我们来试一下图片的复制。

在这里插入图片描述

如上,将图片 01.jpg 放到项目 javase 中,我们将它复制到 IO流 模块的目录下。

public class IOTest01 {
    public static void main(String[] args) {
    	//1)声明两个对象,要在try{}以外声明,否则不能在finally{}中关闭
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            fis = new FileInputStream("01.jpg");
            fos = new FileOutputStream("IO流\\01.jpg");
            byte[] bytes = new byte[fis.available()];
            fis.read(bytes);
            fos.write(bytes);
            fos.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
        //2)注意这里两个流都要关闭
            if(fis!=null){
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(fos!=null){
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

有了前面的学习,这里的代码就不难了。执行程序,图片就复制成功了。

注意这里前面说过,字节流能操作任何类型的文件,所以这里除了复制图片,复制其他类型的文件也是可以的,只不过如果是比较大的视频,效率可能会比较低。

在这里插入图片描述

三、FileReader、FileWriter

在第一部分 IO简介 中就有指出,类名包含 Reader 和 Writer 的,就是专门用来操作文本文件的。而且学懂前面的字节流,字符流就会相当简单。

字节流的read()和write(),是一个字节一个字节地读取和写入,那么字符流的read()和write(),自然就是一个字符一个字符地读取和写入。比如我们读取一个内容为 我爱abc 的文本文件,会先读 ‘我’ ,然后光标移动一位,下次调用read()方法的时候,就是读 ‘爱’。

这里口水比较多,大家估计都已经懂了,但我还是要啰嗦一下,同样的,字符流也有read(char[] c)和write(char[] c)方法来一次性读取或者写入多个字符。

已经没有多的知识点了,直接贴代码吧。

1、FileReader

public class IOTest01 {
    public static void main(String[] args) {
        FileReader fr = null;
        try {
            fr = new FileReader("a.txt");
            //1)字节流就要定义字节数组,字符流就要定义字符数组。
            //注意字节流中有avaliable()方法得到剩下的字节数,而字符流没有对应的方法
            char[] chars = new char[1024]; 
            //2)与字节流相同。同样读取到数组中。
            fr.read(chars);
            System.out.println(chars);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
        	//3)这里的变量名要修改成fr喽,别忘了
            if(fr!=null){
                try {
                    fr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2、FileWriter

public class IOTest01 {
    public static void main(String[] args) {
        FileWriter fw = null;
        try {
            fw = new FileWriter("a.txt",true);
            char[] chars = {'我','爱','中','国'};
            fw.write(chars);
            fw.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(fw!=null){
                try {
                    fw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

四、缓冲流 BufferedReader和BufferedWriter

在前面那个说要介绍哪些流的图中,有四个缓冲流,分别是 BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream ,和文件流中的字节流和字符流类似,缓冲流中的字节流和字符流之间基本的方法也都是类似的,因此这里只介绍 BufferedReader和BufferedWriter。

所谓缓冲流,就是自带字节数组或者字符数组,我们不需要像之前那样去定义字节数组或者字符数组了,因为缓冲流中是自带的。

另外,很多人死记硬背使用缓冲流的代码,这样子一旦太久没有敲IO流的代码,就会很容易忘记。

所以接下来。我们一起去看帮助文档,理解了,就容易记了。

在这里插入图片描述

我们看到这两个构造方法,第二个参数是 sz,即 size(大小) 的缩写,这个就是用来指定字符数组的大小的;关键是第一个参数 Reader in,我们貌似还学习过这个类,继续查这个类,得到以下重要信息。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

绕了一圈,因为Reader是抽象类不能实例化,因此我们要实例化其子类,查看其子类,最终找到其子类InputStreamReader的子类FileReader是我们学过的,所以,我们应该用FileReader对象来作为参数。

现在,我们来读取一个文本,文本内容如下图:

在这里插入图片描述

看代码。

public class IOTest01 {
    public static void main(String[] args) {
        BufferedReader br = null;
        try {
        //1)用FileReader对象作为参数传入
            br = new BufferedReader(new FileReader("a.txt"));
		//2)使用缓冲流的好处,就是其有一个方法readLine(),可以读取文本的一行。
		//当文本读完时,返回的不是-1,而是null
            String str = br.readLine();
            System.out.println(str); //我爱中国
            str = br.readLine();
            System.out.println(str); //
            str = br.readLine();
            System.out.println(str); //我爱中国
            str = br.readLine();
            System.out.println(str); //null

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(br!=null){
                try {
                //3)注意这里,只有最外层的包装流BufferedReader需要关闭
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

同样的,我们用循环进行优化

        try {
            br = new BufferedReader(new FileReader("a.txt"));
            String str = null;
            //只要读到的字符串不为null,也就是文本没有结束,就一直一行一行地往下读
            while((str=br.readLine())!=null){
				System.out.println(str);
			}
        } 
这里还有两个概念,就是节点流和包装流,很好理解。当我们将一个流对象A作为参数传给另一个流B的构造方法时,
A就是节点流,B就是包装流。
节点流和包装流是相对而言的,比如下面的代码中,InputStreamReader对于FileInputStream来说是包装流,对于
BufferedReader来说是节点流。

现在,假设我们用一个FileInputStream对象作为参数传入会怎样呢?会报错,因为FileInputStream是字节流,而我们所需要的参数是Reader,是字符流,因此,我们需要用到转换流,也就是我们在找Reader的子类时,找到的那个InputStreamReader。

用法如下:

        try {
        //1)套娃行为,就是现将字节流转换成字符流,再传给BufferedReader的构造方法
            br = new BufferedReader(new InputStreamReader(new FileInputStream("a.txt")));
            String str = null;

            while((str=br.readLine())!=null){
                System.out.println(str);
            }

        }

BufferedWriter就不多介绍了,你想想,BufferedReader可以读取一行,且读取到的一行是字符串,那么相对的,BufferedWriter的write()方法,也可以写入一行字符串,直接给代码,试试效果。

        BufferedWriter br = null;
        try {
            br = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("a.txt")));
            br.write("张三\n");
            br.write("李四");
            br.flush();
        } 

五、数据流 DataOutputStream、DataInputStream

到这里内容优点多,我们做个简单回顾,前面已经学习了文件流、转换流、缓冲流,其实最重要的两个流,是FileInputStream、FileOutputStream,以下是杜聚宾老师的原话

我们以前在开发中,最常用的是FileInputStream、FileOutputStream,偶尔会用到缓冲流,转换流基本没用过。

这里你会觉得很坑,那前面不是都白学了吗?并没有,因为这些是基本功,把基础打牢固了,后面接触某些新知识就会容易了。

回到数据流,我们来介绍一下什么是数据流?

数据流在将数据写入到文件的时候,除了数据本身之外,数据的类型也会被一起写入到文件中。因此这个文件是特殊
,不是普通的文本文件,不能被直接打开。

以下是DataOutputStream的构造方法

在这里插入图片描述

同样的,OutputStream是一个抽象类,不能实例化,我们用其子类FileOutputStream

public class IOTest01 {
	//这里的throws抛出异常是不规范写法,应该要用try catch捕捉处理
    public static void main(String[] args) throws IOException {
	    //1)定义数据流,构造方法的参数传一个FileOutputStream对象。
	    //注意这里的文件名没有后缀,不是 .txt 文件
        DataOutputStream dos = new DataOutputStream(new FileOutputStream("b"));
        //2)定义各种数据类型的变量
        byte b = 100;
        short s = 200;
        int i = 300;
        long l = 400L;
        float f = 1.0f;
        double d = 2.0;
        boolean bo = true;
        char c = 'a';
        //3)调用相对应的方法将变量写入到文件中
        dos.writeByte(b);
        dos.writeShort(s);
        dos.writeInt(i);
        dos.writeLong(l);
        dos.writeFloat(f);
        dos.writeDouble(d);
        dos.writeBoolean(bo);
        dos.writeChar(c);
        
        //4)流的刷新和关闭
        dos.flush();
        dos.close();
    }
}

执行以上程序之后,我们来打开,看看这个文件,发现是一堆乱码。

在这里插入图片描述

那如何重新查看这些数据呢?

[ 只能用数据输入流重新读取,且读取的顺序,必须和存入的时候一模一样,才能使读到的数据完全正确 ]

代码如下:

public class IOTest02 {
    public static void main(String[] args) throws IOException {
        DataInputStream dis = new DataInputStream(new FileInputStream("b"));
        byte b = dis.readByte();
        short s = dis.readShort();
        int i = dis.readInt();
        long l = dis.readLong();
        float f = dis.readFloat();
        double d = dis.readDouble();
        boolean bo = dis.readBoolean();
        char c = dis.readChar();
        
        System.out.println(b);
        System.out.println(s);
        System.out.println(i);
        System.out.println(l);
        System.out.println(f);
        System.out.println(d);
        System.out.println(bo);
        System.out.println(c);
    }
}

数据流也用的很少,以下仍是杜聚宾老师的原话

在开发生涯中,只用过一次

六、打印流 PrintStream【重要】、PrintWriter

你还记得我们学习java的第一个程序“Hello World”吗?打印字符串的语句:

System.out.println("Hello world");

这我们都会写。但是你有没有去想过,为什么System.out.println(),就能将字符串打印在控制台上吗?我来解释给你听。

1)首先了解System,它也是一个类,在java.lang包下面,java规定,lang包下的类都是不用导入就能直接使用的。因此我们不导包,也能直接使用System类。

2)其次了解System.out。我们来看System类的部分源代码。System类中有一个引用out,其类型是就是PrintStream,我们还发现,这是一个用static修饰的引用,因此我们能直接用 System.out 进行访问。

这里的 out 是 null ,但往下的代码中会对其进行赋值,这里涉及到的有些方法是用C++或者C代码写的,我们没有必要再深究了。只要知道,其最后会被赋值一个PrintStream对象即可。

在这里插入图片描述

3)接着了解整句,System.out.println()。我们看PrintStream的源码。我们发现,原来println()是PrintStream类的一个方法。关于这个方法为什么能将字符串打印到控制台上,我们依旧没有必要再深究了。

在这里插入图片描述

来,也就是说我们得到以下的代码,两者是完全等价的。

public class IOTest02 {
    public static void main(String[] args) throws IOException {
        System.out.println("张三");

        PrintStream ps = System.out;
        ps.println("张三");
    }
}

理解了System.out.println()之后,先转个脑回路,我们思考一个问题:凭什么只能输出在控制台上,我输出到其他地方不可以吗?我们来看看System类的源码。我们发现一个setOut()方法,根据字面意思,就是设置输出,这应该是就是我们要用到的方法了。

在这里插入图片描述

setOut()方法调用了两个方法,我们来看setOut0()方法。发现它是个本地方法(用关键字 native 修饰的方法称为本地方法,底层是用C++写的),那就不深究了,我们会用setOut()方法就行。

在这里插入图片描述

setOut()方法需要传入一个PrintStream对象,因此我们需要先创建一个PrintStream对象,PrintStream的构造方法又需要传入一个OutputStream,OutputStream是抽象类,因此我们要使用其子类FileOutputStream来创建(利用多态特性)。

在这里插入图片描述

万事俱备了,我们来看代码

public class IOTest01 {
    public static void main(String[] args) throws IOException {
        PrintStream ps = new PrintStream(new FileOutputStream("a.txt"));
        System.setOut(ps);
        //以下语句会打印到当前项目下的a.txt文件中
        System.out.println("张三");
        System.out.println("李四");
        System.out.println("王五");
    }
}

那输出到文件有什么作用呢?作用大着呢,我们可以利用上述代码来写一个日志类,通过日志类来生成日志。(日志文件,就是专门用来记录程序执行情况的文件)

日志类的代码如下:(这个类的代码是垃圾代码,不规范,只是想让大家知道日志是个什么东西)

public class Logger {
    public static void log(String str){
        try {
        	//1)创建一个打印流,打印在log.txt文件上
            PrintStream ps = new PrintStream(new FileOutputStream("log.txt",true));
            //2)通过System类修改输出方向
            System.setOut(ps);
            //3)创建日期对象
            Date date = new Date();
            //4)格式化日期:年-月-日 时-分-秒 毫秒
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
            String time = sdf.format(date);
            //5)将日期和参数str打印到log.txt文件中
            System.out.println(time + ":" + str);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

创建好Logger类之后,我们执行以下程序。日志文件就生成了,打开看文件中的内容,你就了解日志文件是个什么玩意儿了。

public class IOTest01 {
    public static void main(String[] args) throws IOException {
        Logger.log("调用了login()方法,用户开始登陆");
        Logger.log("用户登陆成功");
    }
}
在这里插入图片描述
在这里插入图片描述

七、对象流 ObjectInputStream【重要】、ObjectOutputStream【重要】

在前面的所有所学到的流中,我们向文件中写入的数据,要么是字节,要么是字符,要么是字符串,最后一部分,我们要来学习怎么向文件中写入java对象。

1)为什么我们要向文件中写入java对象呢?

java对象中可以保存数据,假设我们定义了一个User类(用户)来存放用户的一些信息,比如账号、密码,以及个人
信息等,这些在用户注册完账号后,都要先把数据作为参数保存在User对象中,再将User对象写入到文件中,这样就
能将用户信息永久地保存在硬盘中了。
当下一次用户登陆的时候,我们就去文件中找到对应的账号,只有用户输入的密码是正确的,才能成功地登陆。

2)什么是“序列化”和“反序列化”?

这两个流因为能写入和读取文件中的java对象,因此称为“对象流”。其中,ObjectOutputStream又称为“序列化流”,
ObjectInputStream又称为“反序列化流”。
序列化是指将对象从内存写入到硬盘的过程,反序列化是指将对象从硬盘读取到内存的过程。

我们仍然先看看帮助文档,其构造方法仍然是需要传入FileOutputStream和FileInputStream(多态,再次强调!)

在这里插入图片描述
在这里插入图片描述

我们先创建一个User类,等会就来存这个类的对象(直接复制代码即可)

public class User {
    private long account; //账号
    private String password; //密码

    public User(long account, String password) {
        this.account = account;
        this.password = password;
    }

    public long getAccount() {
        return account;
    }

    public void setAccount(long account) {
        this.account = account;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "account=" + account +
                ", password='" + password + '\'' +
                '}';
    }
}

1、序列化流将对象写入文件

现在,我们来实际操作一下,首先是序列化流ObjectOutputStream

public class IOTest01 {
    public static void main(String[] args) throws IOException {
    	//1)创建一个序列化对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user"));
        //2)创建user对象
        User user = new User(123456, "abc");
        //3)调用方法将user对象写入
        oos.writeObject(user);
        oos.flush();
        oos.close();
    }
}

执行以上程序,结果出异常了,如下

在这里插入图片描述

解决方法很简单,我们修改一下User类如下:也就是去实现Serializable这个接口

public class User implements Serializable{
}

我们查看这个Serializable接口,发现这个接口里面竟然什么都没有写!

在这里插入图片描述
这里扩展一下,向Serializable这种什么内容都没有的接口,称为'标识接口'。
实现Serializable这个接口,并不是想要实现它的某些方法,或者遵从它的某些规范,它仅仅只是一个标识的作用,
它仅仅只是要告诉程序员,实现了它的类,就是可以序列化的类。

也就是说,实现了Serializable之后,现在我们的User对象可以序列化了。

继续执行将其序列化的代码,发现成功了,而且生成了一个user文件,且打开之后都是乱码。想要知道里面存的是什么,方法就是使用反序列化流来读取。

在这里插入图片描述

2、反序列化流读取文件中的对象

其实,我们发现,加上序列化流中上述代码,和之前相比只多了两个新的方法,writeObject()和readObject(),所以要掌握的新内容也不多,下面直接给代码,很容易看懂。

接下来,开始反序列化流ObjectInputStream的代码:

public class IOTest02 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
    	//1)定义一个反序列化对象,指向user文件
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user"));
        //2)调用readObject()方法,读取文件中的对象
        Object obj = ois.readObject();
        System.out.println(obj); //打印一个对象,其实是调用该对象的toString()方法,我们已经重写了该方法
        ois.close();
    }
}

运行结果如下,也就是说我们成功读取出了对象。

在这里插入图片描述

3、存入和读取多个对象

向文件中写入多个对象,不能向其他流一样,通过 new FileOutputStream(" ",true) 的方式,也就是在文件末继续写入对象来实现(原因比较复杂,我会重新开坑,写一篇专门介绍这个原因的博客)。

而是采用集合的方式,向集合对象中添加入多个User对象,再将集合对象写入到文件中,来实现一次性存入多个User对象。如果你还没学过集合,就先理解成集合是一个容器,可以存放对象。当然,这不是唯一的实现方式,且这是较差的一种方式,我都会在新坑里做介绍的。

我们先看看代码,注意要先把原来的 user 文件删除掉。

写入集合对象的代码如下:

public class IOTest01 {
    public static void main(String[] args) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user"));
        List<User> list = new ArrayList<>(); //创建一个集合
        list.add(new User(111,"abc")); //add()方法是,向集合中添加对象,我们这里直接传入一个匿名对象
        list.add(new User(222,"abc")); 
        list.add(new User(333,"abc"));
        oos.writeObject(list); //将集合写入文件
        oos.flush();
        oos.close();
    }
}

读取集合对象

public class IOTest02 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user"));
        //将集合读取出来,由于readObject()方法是返回一个Object对象,因此我们这里要将Object强转成List
        List list = (ArrayList) ois.readObject(); 
        User user1 = (User) list.get(0); //集合的get()方法,可以通过传入索引来获取到对象
        User user2 = (User) list.get(1);
        User user3 = (User) list.get(2);
        System.out.println(user1); //打印取到的User对象
        System.out.println(user2);
        System.out.println(user3);
        ois.close();
    }
}

这样,我们就取出了User对象,如下图:

在这里插入图片描述

这一小节的开头我有说到,使用 new FileOutputStream(" ",true) 作为参数传给 ObjectOutputStream 的构造方法,来实现添加对象的方式是行不通的。因此,如果我们还要写入更多的User对象,只能将集合读取出来,往集合中存入新的User对象,再将集合继续写入文件来实现。我们来看下面的代码,稍微修改一下IOTest02即可。

public class IOTest02 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user"));
        //ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user"));
        List list = (ArrayList) ois.readObject();
        System.out.println(list); //这里的打印,是为了确认一下user文件集合中只有3个User对象
        list.add(new User(444,"abc")); //然后我们往读取出来的集合中,再添加2个User对象
        list.add(new User(555,"abc"));
        ois.close(); 
        //注意以下细节,不能在上面那个位置中定义,会报错
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user"));
        oos.writeObject(list);
        oos.flush();
        oos.close();
    }
}

再执行一下以下程序,验证我们的文件中的集合中已经有5个User对象了。

public class IOTest03 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user"));
        List list = (ArrayList) ois.readObject();
        System.out.println(list);
    }
}

这种方式,显然过于繁琐,所以我才说这是一种比较差劲的方式。当然,这里你也可以不用深究,也没有必要深究。如果想深究的话,可以去看看其他博客,或者等我把填坑的博客写完,我会在这里加上链接。

4、transient关键字

很简单,比如你给 User 类中的 password 变量加上 transient,(private transient String password;)那么序列化 User 类对象的时候,变量 password 将不会被写入到文件中。

以下代码可以直接复制粘贴(记得删除 user 文件),没有学习的必要,主要是要知道 transient 关键字的作用即可。

public class IOTest01 {
    public static void main(String[] args) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user"));
        oos.writeObject(new User(111,"abc"));
        oos.flush();
        oos.close();
    }
}
public class IOTest02 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user"));
        User user = (User) ois.readObject();
        System.out.println(user); //输出结果 User{account=111, password='null'}
    }
}

我们取出来的 User 对象,其变量 password 的值是null,说明我们在给 User 类的 password 加上关键字 transient 之后,其就没有被写入到文件了。

5、序列化版本号

了解序列化版本号之前,我们还是先来试试代码。

我们把 User 类中的 transient 去掉,然后运行第4节中的两段代码,此时我们的 user 文件中就有一个对象,User{account=111, password=‘abc’}。现在,我们修改一下 User 类,给它新增一个成员变量 email,如下,请大家直接复制代码试验即可:

public class User implements Serializable {
    private long account; //账号
    private String password; //密码
    private String email;

    public User(long account, String password, String email) {
        this.account = account;
        this.password = password;
        this.email = email;
    }

    public long getAccount() {
        return account;
    }

    public void setAccount(long account) {
        this.account = account;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public String toString() {
        return "User{" +
                "account=" + account +
                ", password='" + password + '\'' +
                ", email='" + email + '\'' +
                '}';
    }
}

改完之后,我们再去执行第4节中的第二段代码,来读取我们的 User 类对象,发现报错了。错误如下:

java.io.InvalidClassException: User; local class incompatible: stream classdesc serialVersionUID = 3094711356857122676, local class serialVersionUID = 4110144175829258817

很长,但是我们不慌,拆开来看就清晰了,如下图:

在这里插入图片描述

原来报错的根本原因是类的序列化版本号不一致。

那么这个序列化版本号究竟是什么呢?

Java虚拟机在看到 User 类实现了 Serializable 这个标识接口之后,就会给这个类自动生成一个序列化版本号。这个版本号的作用,是确保流中的类和文件中的类是同一个类,因为只有同一个类,序列化之后,才能被反序列化。

由于我们修改了 User 类,导致 Java虚拟机 重新为 User 类生成了一个新的序列化版本号,所以导致不一致。

但是,我们自己清楚,这两个类确实都是 User 类。所以,我们不需要 Java虚拟机 为我们自动生成序列化版本号,我们可以自己定义,且当我们自己定义的时候,便不会再自动生成。

自定义序列化版本号非常简单,只需要在类中加上 serialVersionUID 这个常量即可,如下:

public static final long serialVersionUID = 16516516543215L; //可以随便写

【小总结】:我们想要序列化和反序列化的对象,都要实现 Serializable 这个接口,且最好手动给它加上一个自定义的序列化版本号。

另外,我们在 IDEA 中也可以设置自动生成序列化对象的代码,设置方法如下:将√取消掉,然后apply,OK。

在这里插入图片描述

设置完之后,只要实现了 Serializable 接口的类,可以将光标停在该类上,然后按 “Alt + Enter”,就能手动添加序列化版本号了。

在这里插入图片描述

八、IO和Properties【重要】

第八章很重要,也许你现在学完不能理解有什么用,但是你要记住有这么个事,就是后面的重点代码我会强调,你至少要看得懂,因为学完JavaSE之后,这个知识点还会用得到,而且用得很多。

1、Properties

如果你还没有学习过集合,那也没有关系,我简单跟你介绍一下Properties是个什么玩意儿。

我们知道,数组是一种容器,比如一个 char[ ] 数组,那么它可以用来存放多个字符;同样的,集合也是一种容器,不过它是专门用来存放对象(引用数据类型)的容器。

Properties也是集合当中的一种,不过它是专门存字符串的,它可以存一个“键”,和一个“值”,键和值构成“键值对”,将键值对存入Properties对象之后,我们可以再根据“键”,来获取“值”。你先了解这些,也它有知道setProperty()和getProperty()两个方法就行,因为这是我们接下来需要用到的知识。

这么说可能有点抽象,看看代码你就很容易理解了。

public class PropertiesTest01 {
    public static void main(String[] args) {
    	//1)创建一个Properties对象
        Properties pro = new Properties();

		//2)设置键值对。左边的参数就是键,右边的参数就是值。这样,键值对就存入到pro容器中了、
        pro.setProperty("username","zhangsan");
        pro.setProperty("password","123");

		//3)然后我们从容器中,就可以通过键作为参数,来读取到值。
        String username = pro.getProperty("username"); //这里用字符串类型变量来接收读取到的值
        String password = pro.getProperty("password");

		//4)我们将他们打印出来
        System.out.println(username); //zhangsan
        System.out.println(password); //123
    }
}

2、属性配置文件

我们准备一个文件,文件名为 userinfo.properties,userinfo是“用户信息”的意思,但这不是我们关注的重点;我们要关注的是文件后缀.properties,我们称其为配置文件(当然,不一定是这个文件后缀才叫配置文件,配置文件还有很多种)。

我们还注意到,红框中的图标发生了改变,这是因为IDEA识别到你的文件后缀是 .properties,它就知道是个配置文件,为了与其他文件进行区分,它就修改了图标。

在这里插入图片描述

我们可以打开这个文件,然后直接编写以下内容。在这里,等号左边的,我们称为“”,英文为“key”;等号右边的,我们称为“”,英文为“value”。

且如果内容格式为如下格式,也就是:

key1=value1

key2=value2

的配置文件,我们称为属性配置文件

在这里插入图片描述

通过1、2节的学习,我们就能将其结合起来运用。我们运用 IO流 来获得文件的数据,再运用 Properties集合 来将数据存到集合对象中,再通过集合对象获取数据。

你会觉得,这不是多此一举吗?我直接用 IO流 读取数据不就得了吗,为什么还要先将数据存到集合,再读取出来?这是因为,我们编写的文件内容格式是固定的,是 “键=值” 的格式,我们在将数据存入集合后,就能通过集合,来通过键,获取值。

还是不理解?看第三节的内容。

3、结合IO流和Properties集合读取文件内容

public class IOPropertiesTest {
    public static void main(String[] args) throws IOException {
        //1)将文件中的数据放到流当中
        FileReader reader = new FileReader("userinfo.properties");
        //2)创建一个Properties对象
        Properties pro = new Properties();
        //3)将流中的数据加载到pro集合中,其中等号左边称为“key”,等号右边称为“value”
        pro.load(reader); 
        //4)通过getProperty()方法,传入键,获取返回值
        String username = pro.getProperty("username"); //通过传入键username,来获得值zhangsan,然后赋值给变量username
        String password = pro.getProperty("password");
        System.out.println(username); //输出结果是 zhangsan
        System.out.println(password);
    }
}

嗯,我们确实是通过键拿到了值,但是有什么用呢?

4、第八章的重要性

以后的实际开发中,一旦应用程序开发后上线供用户使用,就基本上不能再对定义好的类进行修改了。因为类一旦改变,就会相当麻烦,比如源代码需要重新编译,项目需要重新部署,服务器需要重启,且如果该类关联了其他类,其他类也可能要修改等等…

此时,我们可以在配置文件上修改啊!我们修改了配置文件上的值,就相当于修改了传入方法的字符串,我们就不需要修改代码了。

还是不理解有什么用的话,你学到后面的知识,就会慢慢理解了。

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门