JavaIO流真的看这一篇就够了

    科技2022-07-14  131

    前言:根据动力节点培训班视频来总结的一篇博客,可以说是取其精华去其糟粕,我将用最通俗易懂的语言,讲明白IO流,最为新手入门应该掌握的知识点。 1)注意,目录中表名【重要】的,就是要掌握的流,其他的流作为了解即可。 2)代码中 main() 方法 throws 异常这种写法是不规范的,应该 try catch,为了省空间,我直接写 throws 3)本文哪里写得不好的,还请各位不吝赐教,我一定第一时间修改,在此谢过。 视频连接:https://www.bilibili.com/video/BV1Rx411876f?p=720

    目录

    一、IO流简介 二、FileInputStream【重要】、FileOutputStream【重要】     - 1)FileInputStream     - 2)FileInputStream的其他方法     - 3)相对路径和绝对路径     - 4)FileOutputStream     - 5)文件复制 三、FileReader、FileWriter     - 1)FileOutputStream     - 2)文件复制 四、缓冲流 BufferedReader和BufferedWriter 五、数据流 DataOutputStream、DataInputStream 六、打印流 PrintStream【重要】、PrintWriter 七、对象流 ObjectInputStream【重要】、ObjectOutputStream【重要】     - 1)序列化流将对象写入文件     - 2)反序列化流读取文件中的对象     - 3)存入和读取多个对象     - 4)transient关键字     - 5)序列化版本号 八、IO和Properties【重要】     - 1)Properties     - 2)属性配置文件     - 3)结合IO流和Properties集合读取文件内容     - 4)第八章的重要性

    一、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、第八章的重要性

    以后的实际开发中,一旦应用程序开发后上线供用户使用,就基本上不能再对定义好的类进行修改了。因为类一旦改变,就会相当麻烦,比如源代码需要重新编译,项目需要重新部署,服务器需要重启,且如果该类关联了其他类,其他类也可能要修改等等… 此时,我们可以在配置文件上修改啊!我们修改了配置文件上的值,就相当于修改了传入方法的字符串,我们就不需要修改代码了。

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

    Processed: 0.016, SQL: 8