0
点赞
收藏
分享

微信扫一扫

深入浅出IO流知识——高级流

东言肆语 2022-03-12 阅读 63

😋I/O高级流

😎1. 缓冲流

🐏1.1 缓冲流的分类

🐑1.2 缓冲流的基本原理

在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。

🐐1.3 字节缓冲流

🐪1.3.1 构造方法

  • public BufferedInputStream(InputStream in):创建一个 新的缓冲输入流,需要传入一个InputStream类型参数。
  • public BufferedOutputStream(OutputStream out): 创建一个新的缓冲输出流,需要传入一个OutputStream类型参数。

    演示如下:

    public class BufferedTest {
        public static void main(String[] args) throws FileNotFoundException {
            //构造方式一: 创建字节缓冲输入流【但是开发中一般常用下面的格式申明】
            FileInputStream fps = new FileInputStream("e:\\demo\\b.txt");
            BufferedInputStream bis = new BufferedInputStream(fps);
    
            //构造方式一: 创建字节缓冲输入流
            BufferedInputStream bis2 = new BufferedInputStream(new FileInputStream("e:\\demo\\b.txt"));
    
            //构造方式二: 创建字节缓冲输出流
            FileOutputStream fos = new FileOutputStream("e:\\demo\\b.txt");
            BufferedOutputStream bos = new BufferedOutputStream(fos);
        
            //构造方式二: 创建字节缓冲输出流
            BufferedOutputStream bos2 = new BufferedOutputStream(new FileOutputStream("e:\\demo\\b.txt"));
        }
    }
    

🐫1.3.2 感受高效

既然称缓冲流为高效流,那我们不得测试下看看,是骡子是马得拉出来溜溜啊。

先来感受下:

public class BufferedTest2 {
    public static void main(String[] args) {
        //使用多线程的方式来同时测试两者复制的速度。
        new Thread(() -> oldCopy()).start();//这个是JDK8新特性中的Lambda表达式
        new Thread(() -> bufferedCopy()).start();
        
        /*
           原样相当于:
           new Thread(new Runnable() {
                @Override
                public void run() {
                    oldCopy();
                }
            }).start();
         */
    }

    public static void oldCopy() {
        // 记录开始时间
        long start = System.currentTimeMillis();
        // 创建流对象
        try (
                FileInputStream fis = new FileInputStream("e:\\demo\\科比演讲.mp4");
                FileOutputStream fos = new FileOutputStream("e:\\demoCopy\\oldCopy.mp4")
        ){
            // 读写数据
            int b;
            while ((b = fis.read()) != -1) {
                fos.write(b);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 记录结束时间
        long end = System.currentTimeMillis();
        System.out.println("普通流复制时间:"+(end - start)+" 毫秒");
    }

    public static void bufferedCopy() {
        // 记录开始时间
        long start = System.currentTimeMillis();
        // 创建流对象
        try (
                BufferedInputStream bis = new BufferedInputStream(new FileInputStream("e:\\demo\\科比演讲.mp4"));
                BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("e:\\demoCopy\\newCopy.mp4"));
        ){
            // 读写数据
            int b;
            while ((b = bis.read()) != -1) {
                bos.write(b);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 记录结束时间
        long end = System.currentTimeMillis();
        System.out.println("缓冲流复制时间:"+(end - start)+" 毫秒");
    }
}

运行结果:
缓冲流复制时间:3498 毫秒
普通流复制时间:319839 毫秒 

程序运行后,可以深刻的体验到缓冲流的快速,缓冲流已经复制成功了,但是普通流还没成功。不止没成功,而且耗时超级久。可以感受到缓冲流是普通流的几十倍了吧。我这个视频仅仅是46.2M大小而已,如果是更大的文件,几百M更甚至是几G的文件呢?那我岂不是一天都没办法传输完成了。这岂不是很百度网盘?

接下来使用数组的方式进行体验:

public class BufferedTest3 {
    public static void main(String[] args) {
    //使用多线程的方式来同时测试两者复制的速度。
        new Thread(() -> oldCopy()).start();
        new Thread(() -> bufferedCopy()).start();

    }

    public static void oldCopy() {
        //开始时间
        long start = System.currentTimeMillis();

        try(
                FileInputStream fis = new FileInputStream("e:\\demo\\科比演讲.mp4");
                FileOutputStream fos = new FileOutputStream("e:\\demoCopy\\oldCopy2.mp4");
                ) {
            int len;
            byte[] b = new byte[1024];
            while((len = fis.read(b)) != -1) {
                fos.write(b, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        //结束时间
        long end = System.currentTimeMillis();
        System.out.println("普通流数组复制时间:" + (end - start) + "毫秒");
    }

    public static void bufferedCopy() {
        //开始时间
        long start = System.currentTimeMillis();
        // 创建流对象
        try (
                BufferedInputStream bis = new BufferedInputStream(new FileInputStream("e:\\demo\\科比演讲.mp4"));
                BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("e:\\demoCopy\\newCopy2.mp4"));
        ){
            // 读写数据
            int len;
            byte[] bytes = new byte[1024];
            while ((len = bis.read(bytes)) != -1) {
                bos.write(bytes, 0 , len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 结束时间
        long end = System.currentTimeMillis();
        System.out.println("缓冲流使用数组复制时间:"+(end - start)+" 毫秒");
    }
}

程序执行结果:
缓冲流使用数组复制时间:370 毫秒
普通流数组复制时间:1016毫秒

是不是这个车速更快了,如果还不够快,你可以把byte[] bytes = new byte[1024]改为byte[] bytes = new byte[1024*8]。可以让你体验到更高效。你就会感觉这速度飞上天了。

🐇1.4 字符缓冲流

🐀1.4.1 构造方法

  • public BufferedReader(Reader in) :创建一个 新的缓冲输入流,传入参数的类型为Reader

  • public BufferedWriter(Writer out): 创建一个新的缓冲输出流,传入参数的类型为Writer

    演示如下:

    // 创建字符缓冲输入流
    BufferedReader br = new BufferedReader(new FileReader("e:\\demo\\b.txt"));
    // 创建字符缓冲输出流
    BufferedWriter bw = new BufferedWriter(new FileWriter("e:\\demo\\b.txt"));
    

🐿️1.4.2 特有方法

不用于字节缓冲流和字符普通流来说,他有别人所没有的东西,那是什么呢,我们现在来看看:

  • 字符缓冲输入流BufferedReaderpublic String readLine(): 读一行文字,如果读到最后,则会返回NULL

  • 字符缓冲输出流BufferedWriterpublic void newLine(): 写一行行分隔符即换行,由系统属性定义符号。

    一步一步来演示下:

    1. public void newLine()

      public class BufferedTest4 {
          public static void main(String[] args) throws IOException {
              BufferedWriter bw = new BufferedWriter(new FileWriter("e:\\demo\\hello.txt"));
      
              //写出数据
              bw.write("Hello");
              //换行
              //public void newLine()
              bw.newLine();
              bw.write("world");
              bw.newLine();
              bw.write("Java");
              bw.newLine();
              bw.write("newLine");
              bw.flush();
              bw.close();
          }
      }
      
      程序执行后,查看hello.txt信息为:
      Hello
      world
      Java
      newLine
      
    2. public String readLine():

      public class BufferedTest5 {
          public static void main(String[] args) throws IOException {
              BufferedReader br = new BufferedReader(new FileReader("e:\\demo\\hello.txt"));
      
              //定义获取字符串
              String line = null;
              while((line = br.readLine()) != null) {
                  System.out.println(line);
              }
              br.close();
          }
      }
      
      程序执行后,查看hello.txt信息为:
      Hello
      world
      Java
      newLine
      

🐓1.5. 文本排序案例

文本排序练习什么呢,下面我分享我比较喜欢的一首,然后按一定的顺序乱排。当然不要担心,肯定有办法让你可以重新排序的。

8.既然目标是地平线
7.我不去想身后会不会袭来寒风冷雨
3.便只顾风雨兼程
9.留给世界的只能是背影
4.我不去想能否赢得爱情
5.既然钟情于玫瑰
6.就勇敢地吐露真诚
10.我不去想未来是平坦还是泥泞
11.只要热爱生命
12.一切,都在意料之中
5.既然钟情于玫瑰
6.就勇敢地吐露真诚
1.我不去想是否能够成功
2.既然选择了远方

<热爱生命>来自汪国真的诗集里头,说实话,这篇我在高中的作文中,一直被我拿来引用。不止这一篇吧,还有汪国真的<感谢>等。虽然作文至今也没有拿到特别高的分,但也不能妨碍我对汪国真的作品的热爱。正如我原想收获一缕春风,你却给了我整个夏天。你要相信你的才华不会永远被埋没。所以当我们跨越了一座高山,也就是跨越了一个真实的自己。

好了,Stop,说回重点吧:我们要如何把这篇诗排序呢。

分析:

  • 我们不是刚学了一个字符缓冲流中读取一行的吗,那我们就可以先读取每一行文字。

  • 然后不得去解析文字吗,我们用前一两位排?但是如果我文本一多,有几百行呢,不就是三位数了吗,显示这种方法不可取的。

  • 我们应该可以看到在序号的后面有个字符".",我们可以用这个缺陷来入手。然后将拆分后的序号和文字存储到集合中,这种有CP组合的,我们就可以采用Map集合啦。

  • 然后再将排好序的文字,重新写入到文件中把。

  • OK,既然知道了思路,我们就一起大干一场吧:

    public class BufferedTest6 {
        public static void main(String[] args) throws IOException {
            //创建字符输入流对象
            BufferedReader br = new BufferedReader(new FileReader("e:\\demo\\热爱生命.txt"));
    
            //创建字符输出流对象
            BufferedWriter bw = new BufferedWriter(new FileWriter("e:\\demoCopy\\热爱生命.txt"));
    
            //创建Map集合,用于保存将来得到的数据,键为序号,值为文本。
            HashMap<String, String> map = new HashMap<>();
    
            //读取数据
            String line = null;
            while((line = br.readLine()) != null) {
                //通过.来分割文本和序号
                String[] split = line.split("\\.");
                //将得到的数据存入集合中
                map.put(split[0], split[1]);
            }
    
            //接下来不就是可以按序号即键找值的方式来得到文本吗
            for (int i = 1; i <= map.size(); i++) {
                String key = String.valueOf(i);
                //键找值
                String value = map.get(key);
    
                //得到文本,这里我序号还是写进去,怕你觉得我懵你,如果相信我的同学,可以不用写序号了。
                bw.write(key + "." + value);
                if(i == 7){
                    System.out.println(value);
                }
                //bw.write(value);
                bw.newLine();
                bw.flush();
            }
            bw.flush();
            //释放资源
            bw.close();
            br.close();
        }
    }
    

    程序运行后,正如一切都在意料之中,查看文件可以看到:

    1.我不去想是否能够成功
    2.既然选择了远方
    3.便只顾风雨兼程
    4.我不去想能否赢得爱情
    5.既然钟情于玫瑰
    6.就勇敢地吐露真诚
    7.我不去想身后会不会袭来寒风冷雨
    8.既然目标是地平线
    9.留给世界的只能是背影
    10.我不去想未来是平坦还是泥泞
    11.只要热爱生命
    12.一切,都在意料之中
    

    如果不想有序号了,可以选择不写入序号。

😗2. 转换流

可以看图知道字节和字符的转换就是通过一定编码和解码的操作完成的。为什么会出现乱码呢?具体一起来看看吧。

🍉2.1 字符编码和解码

可以用我们所学习的Java解释为:

String(byte[] bytes, String charsetName): 通过指定的字符集解码字节数组
byte[] getBytes(String charsetName): 使用指定的字符集合把字符串编码为字节数组

通俗易懂的来说,不知道大家有没有看过孙红雷演过的<潜伏>这部电视剧,就算没看过,大家也都是知道谍战片吧。假如,你和我都是间谍,潜伏在敌营中,然后要互相通信。你会直接说一段话,或者寄信然后里面写着:今晚在天台见面给我吗? 我保证,如果有你这样的队友,潜伏行动不出一天,就直接Over,大家一起玩完。所以我们不能这样嘛,我们要用的一定格式规则来进行转换对吧,这样就算是真的有敌人拿到了这封信,也会懵逼,想着这。。。是什么?然后也会不了了之。大不了今晚不见面,至少保证了我们的存活,安全问题。

  • 那这其中,我们不得先有一定的规则,可以让你我进行转换后都能看懂得的表格数据,我们称这规则称为字符编码:就是一套自然语言的字符与二进制数之间的对应规则。

    而这表格可以相当于我们参照的转换的规则,称之为字符集(编码表):生活中文字和计算机中二进制的对应规则。

  • 当你写信的时候,这过程就是把你我看得懂的东西,按我们知道的规则,进行转换为谁都看不懂的东西,这过程就是编码。

  • 当我得到你的信的时候,这过程,我不就把这段看不懂的文字进行规则解析,这过程就是解码。

  • 这其中要是你喝酒醉写信给我,不按套路出牌,年轻人不讲武德,按另一种规则来编写,然后我按我们的规则来解析,解析完后,都看不懂就一脸问号,这就可以称之为乱码了。

所以我们现在先来了解下规则即字符集:

  • 字符集Charset:也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。

    计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有ASCII字符集、GBK字符集、Unicode字符集等。当我们知道了编码格式后,它所对应的字符集自然就指定了,所以编码才是我们最终要关心的。

    以下字符集是我网上找到相对比较全的,如果还想再多了解的话,可以自行百度。毕竟我们是面向百度编程。嘿嘿。

    • ASCII字符集
      • ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字和西文符号)。
      • 基本的ASCII字符集,使用7位(bits)表示一个字符,共128字符。ASCII的扩展字符集使用8位(bits)表示一个字符,共256字符,方便支持欧洲常用字符。
    • ISO-8859-1字符集
      • 拉丁码表,别名Latin-1,用于显示欧洲使用的语言,包括荷兰、丹麦、德语、意大利语、西班牙语等。
      • ISO-5559-1使用单字节编码,兼容ASCII编码。
    • GBxxx字符集
      • GB就是国标的意思,是为了显示中文而设计的一套字符集。
      • GB2312:简体中文码表。一个小于127的字符的意义与原来相同。但两个大于127的字符连在一起时,就表示一个汉字,这样大约可以组合了包含7000多个简体汉字,此外数学符号、罗马希腊的字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。
      • GBK:最常用的中文码表。是在GB2312标准基础上的扩展规范,使用了双字节编码方案,共收录了21003个汉字,完全兼容GB2312标准,同时支持繁体汉字以及日韩汉字等。
      • GB18030:最新的中文码表。收录汉字70244个,采用多字节编码,每个字可以由1个、2个或4个字节组成。支持中国国内少数民族的文字,同时支持繁体汉字以及日韩汉字等。
    • Unicode字符集
      • Unicode编码系统为表达任意语言的任意字符而设计,是业界的一种标准,也称为统一码、标准万国码。
      • 它最多使用4个字节的数字来表达每个字母、符号,或者文字。有三种编码方案,UTF-8UTF-16UTF-32。最为常用的UTF-8编码。
      • UTF-8编码,可以用来表示Unicode标准中任何字符,它是电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。所以,我们开发Web应用,也要使用UTF-8编码。它使用一至四个字节为每个字符编码,编码规则:
        1. 128个US-ASCII字符,只需一个字节编码。
        2. 拉丁文等字符,需要二个字节编码。
        3. 大部分常用字(含中文),使用三个字节编码。
        4. 其他极少使用的Unicode辅助字符,使用四字节编码。

🍊2.2 乱码问题

为什么我们读取文件会出现乱码呢,因为我们的编辑器IDEA默认的编码格式是UTF-8,而如果我们文件格式不是UTF-8格式的话就会读错,一般来说,IDEA创建的文件一般也是UTF-8格式,读取和写入都不会有任何问题,但是呢,如果我们是在Windows下创建文件的话,其默认是ASCII,会跟随系统默认的编码格式,实际就是GBK格式。所以我们文件是GBK格式,而读取的是UTF-8格式,自然就乱码了。


代码演示下乱码:

public class ReaderTest {
    public static void main(String[] args) throws IOException {
        FileReader fr = new FileReader("E:\\demo\\China.txt");

        int ch;
        while((ch = fr.read()) != -1) {
            System.out.println((char) ch);
        }

        fr.close();
    }
}

程序执行结果:
�й�

是不是完全看不出什么东西呢,你说你能看出,我就算你厉害。

那我们要如何解决乱码问题呢,也就是解决编码问题呢?是时候祭出转换流了。让你觉得乱码不是啥问题。

🥭2.3 InputStreamReader

InputStreamReader:将字节流以字符流输入,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以是默认字符集即你的编辑器是什么字符集就是什么字符集。

🍌2.3.1 构造方法

  • public InputStreamReader(InputStream in):创建一个使用默认字符集的字符流。

  • public InputStreamReader(InputStream in, String charsetName):创建一个指定字符集的字符流。

    演示如下:

    public class IpsrTest {
        public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException {
            InputStreamReader isr = new InputStreamReader(new FileInputStream("e:\\demo\\China.txt"));
    
            InputStreamReader isr2 = new InputStreamReader(new FileInputStream("e:\\demo\\China.txt"), "GBK");
        }
    }
    

🍍2.3.2 解决乱码问题

public class ReadTest2 {
    public static void main(String[] args) throws IOException {
        String fileName = "E:\\demo\\China.txt";

        //创建转换流,默认字符集
        InputStreamReader isr = new InputStreamReader(new FileInputStream(fileName));

        //创建转换流,指定字符集
        InputStreamReader isr2 = new InputStreamReader(new FileInputStream(fileName), "GBK");

        int ch;
        //默认字符集读取
        while((ch = isr.read()) != -1) {
            System.out.print((char) ch);
        }

        isr.close();

        //指定字符集读取
        while((ch = isr2.read()) != -1) {
            System.out.print((char) ch);
        }
        isr2.close();
    }
}

程序执行结果:
�й�
中国

是不是很好的解决乱码问题呢,妈妈再也不担心我看不懂文件啦。有读取的转换流,当然还有写出的转换流啦,一起来看看吧。

🍑2.4 OutputStreamWriter

OutputStreamWriter:将字节流以字符流输入,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以是默认字符集即你的编辑器是什么字符集就是什么字符集。

🍏2.4.1构造方法

  • public OutputStreamWriter(OutputStream in):创建一个使用默认字符集的字符流。

  • public OutputStreamWriter(OutputStream in, String charsetName):创建一个指定字符集的字符流。

    演示如下:

    public class WriterTest {
        public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException {
            OutputStreamWriter osr = new OutputStreamWriter(new FileOutputStream("e:\\demo\\ChinaOut.txt"));
            OutputStreamWriter osr2 = new OutputStreamWriter(new FileOutputStream("e:\\demo\\ChinaOut.txt") , "GBK");
        }
    }
    

🍐2.4.2 以指定编码写出数据

public class WriterTest2 {
    public static void main(String[] args) throws IOException {
        // 定义文件路径
        String fileName = "E:\\demo\\ChinaOut.txt";
        // 创建流对象,默认UTF8编码
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(fileName));
        // 写出数据
        osw.write("北极星");
        osw.close();


        String fileName2 = "E:\\demo\\ChinaOut2.txt";
        // 创建流对象,指定GBK编码
        OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream(fileName2),"GBK");
        // 写出数据
        osw2.write("叫我了");
        osw2.close();
    }
}

程序执行后结果:
北极星
叫我啦

再看记事本格式,可以发现如果存储的是UTF-8,记事本的格式也更改为了UTF-8编码格式了,而指定了GBK字符集,则记事本的格式为ASCII编码格式了。

😚3. 打印流

🧗3.1 分类

打印流只有输出流,分为:

  • 字节打印流:printStream

  • 字符打印流:printWriter

    两者具体使用方法中,基本类似。

🏌️3.2 打印流的特点

  • 只操作目的地,不操作数据源。
  • 可以操作任意类型的数据。
  • 如果启用了自动刷新,在调用println()方法的时候,能够换行并刷新。
  • 可以直接操作文本文件。

🚣3.3 PrintStream

🚣‍♂️3.3.1 构造方法

  • public PrintStream(String fileName): 使用指定的文件名创建一个新的打印流。

    构造举例,代码如下:

    PrintStream ps = new PrintStream("e:\\demo\\ps.txt");
    

🚣‍♀️3.3.2 打印到文件

我们也经常看见System.out.println()这个打印到控制台就是printStream类型的,只不过它的流向是系统规定的,打印在控制台上。不过,既然是流对象,我们就可以玩另一个功能,将数据输出到指定文本文件中。

public class PrintStreamDemo {
    public static void main(String[] args) throws FileNotFoundException {
        // 调用系统的打印流,控制台直接输出97
        System.out.println(97);

        //创建打印流
        PrintStream ps = new PrintStream("e:\\demo\\ps.txt");

        // 设置系统的打印流流向,输出到ps.txt
        System.setOut(ps);
        // 调用系统的打印流,ps.txt中输出97
        System.out.println(97);
    }
}

程序运行结果可以看到控制台只有打印一个97,那剩下一个97打印到哪里了呢,查看ps.txt文件,可以看到97打印到了文件中。

🏊3.3.3 复制文件案例

public class PrintStreamDemo2 {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader("e:\\demo\\ps.txt"));
        PrintStream ps = new PrintStream("e:\\democopy\\psCopy.txt");
        String line;
        while ((line = br.readLine()) != null) {
            ps.println(line);
        }
        br.close();
        ps.close();
    }
}

程序执行结果:

🚴3.4 PrintWriter

🚴‍♀️3.4.1 构造方法

  • public PrintWriter(String fileName): 使用指定的文件名创建一个新的打印流。

    构造举例,代码如下:

    PrintWriter pw = new PrintWriter("e:\\demo\\pw.txt");
    

🚵3.4.2 复制文件案例

public class PrintWriterDemo {
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new FileReader("e:\\demo\\ps.txt"));
        PrintWriter pw = new PrintWriter("e:\\democopy\\pwCopy.txt");
        String line;
        while ((line = br.readLine()) != null) {
            pw.println(line);
        }
        br.close();
        pw.close();
    }
}

程序执行结果:

🤸3.5 标准输出流的本质

  • 输出语句的原理和如何使用字符流输出数据

    直接看一段代码先:

    public class SystemOutDemo {
        public static void main(String[] args) {
            //在System类下有out对象,可以获取PrintStream输出流对象。
            //public final static PrintStream out = null;
            System.out.println("helloworld");
    
            //输出流对象
            PrintStream ps = System.out;
            ps.println("helloworld");
        }
    }
    
    程序执行结果:
    都打印在了控制台上:
    helloworld
    helloworld
    
  • 本质:

    System类下有一个public final static PrintStream out = null这个静态对象,可以返回一个printStream对象。

    所以输出语句其本质就是IO流操作,把数据输出到控制台上。

😇4. 序列化

在上面的字节流中,我们不是有看到两个流:ObjectOutputStreamObjectInputStream吗,这个流主要用在对象序列化中。那我们来了解下序列化:

像之前的操作都是跟文件有关直接写入的,那我们学习的是面向对象,那可以把对象存入到文件保存起来吗。而接下来要学习的序列化流就是可以将保存在内存中的对象数据转化为二进制数据流进行传输,任何对象都可以序列化。

Java提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据对象的类型对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。 注意一点就是你可能读不懂这个文件。这个文件不是给我们读取的,是保存对象的信息的。而我们就可以用该字节序列从文件中读取回来,重构对象,对它进行反序列化对象的数据对象的类型对象中存储的属性信息,都可以用来在内存中创建对象。

🏘️4.1 序列化流 ObjectOutputStream

🏔️4.1.1 构造方法

  • public ObjectOutputStream(OutputStream out): 创建一个指定字节输出流OutputStream的序列化流ObjectOutputStream。传入的参数是OutputStream字节输出流。

    构造方法演示:

    public class ObjectStreamDemo {
        public static void main(String[] args) throws IOException {
            //public ObjectOutputStream(OutputStream out)
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("e:\\demo\\oos.txt")); 
        }
    }
    

⛰️4.1.2 序列化操作

我们要如何实现对象的序列化操作呢?

我们要实现对象的序列化操作,不得先创建一个对象出来:

public class Person {
    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public int getAge() {
        return age;
    }
    
    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return "Person{name = " + name + ", age = " + age + "}";
    }
}

还需要知道一个方法如何写入序列化的方法对吧:

public final void writeObject(Object obj) : 将指定的对象写出。

同时一个对象想要实现序列化操作需要满足以下条件:

  1. 这个对象类必须实现java.io.Serializable 接口,Serializable 可以看到里面没有任何方法,那有什么用呢,一般我们称之为一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException

    具体来看看如果不实现,会出现什么状况:

    public class ObjectStreamDemo {
        public static void main(String[] args) throws IOException {
            //public ObjectOutputStream(OutputStream out)
            // 创建序列化流对象
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("e:\\demo\\oos.txt"));
    
            // 创建对象
            Person p = new Person("测试机器人1号", 01);
    
            //public final void writeObject(Object obj)
            //将对象写出
            oos.writeObject(p);
    
            // 释放资源 
            oos.close();
        }
    }
    

    此时对象并没有实现Serializable 接口,所以程序运行后:、

    Exception in thread "main" java.io.NotSerializableException: com.it.test11.Person
    	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
    	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
    	at com.it.test11.ObjectStreamDemo.main(ObjectStreamDemo.java:18)
    

    表示你没有实现Serializable 接口。所以我们现在来实现这个接口看看

    public class Person implements Serializable {
        .....
    }
    

    现在再来执行看看,运行正常,没有报异常,我们再来看看文件信息:

    这…能看的懂吗,看不懂没关系,但是有东西能看懂就行了,我们能把这个文件读取就行了。那我们再来看看怎样读取?

🏚️4.2 反序列化流 ObjectInputStream

🏥4.2.1 构造方法

  • public ObjectInputStream(InputStream in): 创建一个指定字节输入流InputStream 的反序列化流ObjectInputStream 。传入的参数是InputStream 字节输入流。

    构造方法演示如下:

    public class ObjectStreamDemo2 {
        public static void main(String[] args) throws IOException {
            //public ObjectInputStream(InputStream in)
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("e:\\demo\\oos.txt"));
        
        }
    }
    

🏦4.2.2 反序列化操作

既然之前我们已经做了把对象序列化到文件中,那我们如何把文件信息反序列化为对象呢?

那我们先得知道什么方法可以读取:

public final Object readObject () : 读取一个对象。

按照我们之前读取文件的操作方式,可以同理得到对象吗,一起来试试呗。说白了,如果不会,就先尝试,在尝试中,不断改错对吧,才能变得更好。

public class ObjectStreamDemo2 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //public ObjectInputStream(InputStream in)
       // 创建反序列化对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("e:\\demo\\oos.txt"));

        // 还原对象
        Object obj = ois.readObject();

        // 释放资源
        ois.close();

        // 输出对象
        System.out.println(obj);
    }
}

程序执行结果:
Person{name = 测试机器人1, age = 1}

🏨4.2.3 反序列化操作可能出现的问题

当我们已经把对象序列化到文件中后,现在我们第一次读取没什么问题对吧,但是如果我再把Person 类进行修改。然后在读取,会发生什么问题?

public class Person implements Serializable {
    private String name;
    private int age;
    private String address;//新增一个属性
   .....
}

再读取结果:

Exception in thread "main" java.io.InvalidClassException: com.it.test11.Person; local class incompatible: stream classdesc serialVersionUID = 1228968110604265735, local class serialVersionUID = 3463310223685915264
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1829)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1713)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1986)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1535)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
	at com.it.test11.ObjectStreamDemo2.main(ObjectStreamDemo2.java:14)

可以看到你的控制台一堆飘红的异常信息了。发生了InvalidClassException异常,为什么会发生这个异常呢。具体原因有可能如下:

  • 该类的序列版本号与从流中读取的类描述符的版本号不匹配
  • 该类包含未知数据类型
  • 该类没有可访问的无参数构造方法

我们再看下异常的具体原因:

com.it.test11.Person; local class incompatible: stream classdesc serialVersionUID = 1228968110604265735, local class serialVersionUID = 3463310223685915264

可以发现使我们对Person 类修改后,造成了序列的版本号不一致了。因为我们修改后的序列号没有再次保存在文件中,所以最新的序列号和文件序列号不一致,导致异常了。那我们如何解决这个问题呢,有两个方法:

  1. 重新在写入文件:在我们修改Person 类后,我们再进行一次写入文件,可以发现这样再次读取后,没有任何问题了。这个方法比较麻烦吧,每次修改都要重新写入文件,才能再次读取,是不是相对比较麻烦,接下来了教你一个一劳永逸的方法。

  2. 写一个固定的版本号:既然每次修改都会造成文件的序列版本号的改变,那我们如果能设置一个固定的序列化版本号就可以解决这一问题了对吧。那如何设置呢?

    //加入序列版本号
    private static final long serialVersionUID = 2071565876962023344L; 
    

    Person 类中第一行加入这个即可,那我们进行重新写入,在读取都没有问题了。

🏠4.3 transient关键字

只需要在变量名前加上一个关键字transient

  • transient :短暂,瞬态的。表明这个变量不想被序列化。

    现在Person 类我只想保存姓名的变量,年龄的变量我不想被序列化。

    private transient int age;
    

    现在我们在重新写入,并读取后:

    Person{name = 测试机器人1, age = 0}
    

    可以看到,年龄没有再序列化到文件了。

🏡4.4 集合序列化案列

案例:将存有多个自定义教师对象的集合序列化操作,保存到arrayList.txt 文件中。反序列化arrayList.txt,并遍历集合,打印对象信息。

我们要怎么实现这个案例呢:

  1. 创建若干教师对象 ,并保存到集合中。
  2. 把集合序列化。
  3. 反序列化读取时,只需要读取一次,转换为集合类型。
  4. 遍历集合,可以打印所有的教师信息。

OK,既然知道这个过程,现在直接来实现吧:

创建Teacher类,并实现 Serializable 接口。

package com.it.test12;

import java.io.Serializable;

public class Teacher implements Serializable {
    private String name;
    private int age;


    public Teacher() {
    }

    public Teacher(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return "Teacher{name = " + name + ", age = " + age + "}";
    }
}

在进行序列化操作测试。

package com.it.test12;

import java.io.*;
import java.util.ArrayList;

public class ObjectStreamTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //创建教师对象
        Teacher t1 = new Teacher("王老师", 40);
        Teacher t2 = new Teacher("李老师", 48);
        Teacher t3 = new Teacher("张老师", 46);

        //添加到集合中。
        ArrayList<Teacher> arrayList = new ArrayList<>();
        arrayList.add(t1);
        arrayList.add(t2);
        arrayList.add(t3);

        //序列化操作
        serializ(arrayList);

        //先进行序列化操作,然后再注释掉序列化操作。再执行反序列化
//        serializRead();
    }

     //反序列化操作
    private static void serializRead() throws IOException, ClassNotFoundException {
        ObjectInputStream ois  = new ObjectInputStream(new FileInputStream("list.txt"));
        // 读取对象,强转为ArrayList类型
        ArrayList<Teacher> list  = (ArrayList<Teacher>)ois.readObject();

        for (int i = 0; i < list.size(); i++ ){
            Teacher s = list.get(i);
            System.out.println(s.getName()+"--"+ s.getAge());
        }
    }

    //序列化操作
    private static void serializ(ArrayList<Teacher> arrayList) throws IOException {
        // 创建 序列化流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.txt"));
        // 写出对象
        oos.writeObject(arrayList);
        // 释放资源
        oos.close();
    }
}

程序运行后结果:

王老师--40
李老师--48
张老师--46

😶5. 操作基本数据的流

像我们之前都是操作引用类型的数据例如字符串String类型的文本,但是我们还没有遇到一个可以操作基本数据类型的流对吧。接下来就慢慢讲述:

🚋5.1 DataOutputStream

🚌5.1.1 构造方法

  • public DataOutputStream(OutputStream out):创建一个新的数据输出流,以将数据写入指定的底层输出流。传入的参数是OutputStream类型。

    构造举例,代码如下:

    DataOutputStream dos = new DataOutputStream(new FileOutputStream("e:\\demo\\dos.txt"));       
    

🚍5.1.2 写入数据

可以看到他的源码方法里有很多可以对数据类型做操作的方法:

public class DataStreamDemo {
    public static void main(String[] args) throws IOException {
        DataOutputStream dos = new DataOutputStream(new FileOutputStream("e:\\demo\\dos.txt"));

        // 写数据了
        dos.writeByte(10);
        dos.writeShort(100);
        dos.writeInt(1000);
        dos.writeLong(10000);
        dos.writeFloat(12.34F);
        dos.writeDouble(12.56);
        dos.writeChar('a');
        dos.writeBoolean(true);

        // 释放资源
        dos.close();
    }
}

程序执行后,写入到文件,你可以发现文件显示,你看不懂,这里不是乱码,是因为这里存入不是给我们读取的,而是给机器读取的。所以不要慌,下面就来讲解读取。

🚎5.2 DataInputStream

🚑5.2.1 构造方法

  • public DataInputStream(InputStream in):创建使用指定的InputStreamDataInputStream

    构造举例,代码如下:

    DataInputStream dis = new DataInputStream(new FileInputStream("e:\\demo\\dos.txt")); 
    

🚒5.2.2 读取数据

可以看到他的源码方法里有很多可以读取数据类型方法:

public class DataStreamDemo2 {
    public static void main(String[] args) throws IOException {
        DataInputStream dis = new DataInputStream(new FileInputStream("e:\\demo\\dos.txt"));

        // 读数据
        byte b = dis.readByte();
        short s = dis.readShort();
        int i = dis.readInt();
        long l = dis.readLong();
        float f = dis.readFloat();
        double d = dis.readDouble();
        char c = dis.readChar();
        boolean bb = dis.readBoolean();

        // 释放资源
        dis.close();

        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(c);
        System.out.println(bb);
    }
}

程序执行结果:
10
100
1000
10000
12.34
12.56
a
true

😏6. 随机访问流

RandomAccessFileIO流体系中比较特殊的流。即可以读取文件内容,也可以向文件中写入内容,和其他输入输出流不同的是可以直接跳到文件的任意位置来读写数据。所以如果我们希望只访问文件的部分内容,那就可以使用RandomAccessFile类。

因为RandomAccessFile类包含了一个记录指针,用以标识当前读写处的位置,当程序新创建一个RandomAccessFile对象时,该对象的文件记录指针位于文件头(也就是0处),当读/写了n个字节后,文件记录指针将会向后移动n个字节。除此之外,RandomAccessFile可以自由的移动记录指针,即可以向前移动,也可以向后移动。RandomAccessFile包含了以下两个方法来操作文件的记录指针.

  • long getFilePointer(); 返回文件记录指针的当前位置
  • void seek(long pos); 将文件记录指针定位到pos位置

RandomAccessFile类不属于流,是Object类的子类。但它融合了InputStreamOutputStream的功能。支持对文件的随机访问读取和写入。

🛰️6.1 构造方法

  • public RandomAccessFile(File file, String mode):创建随机流,传入的参数是File参数。
  • public RandomAccessFile(String name, String mode):创建随机流,传入String参数来指定文件名。

两个构造方法都需要传入一个指定的mode参数,该参数指定RandomAccessFile的访问模式。

分别解释下:

  • r:以只读方式来打开指定文件夹。
  • rw: 以读,写方式打开指定文件。
  • rws:需要更新要写入的文件的内容及其元数据。

  • rwd:只需要更新要写入存储的文件内容。

    这其中比较常用的就是rw模式,既可以写数据,也可以读取数据 。

🚀6.2 测试读写

读写方法基本和之前的字节输入输出流一样,我们主要测试下指定位置读取的方法吧:

public class RandomAccessFileDemo {
    public static void main(String[] args) throws IOException {
        RandomAccessFile raf = new RandomAccessFile("e:\\demo\\raf.txt", "r");
        //写入文件
        write();//helloworld

        //第一次读取
        int ch = raf.read();
        System.out.println((char) ch);
        // 该文件指针可以通过 getFilePointer方法读取
        System.out.println("当前文件的指针位置是:" + raf.getFilePointer());

        //第二次读取
        ch = raf.read();
        System.out.println((char) ch);
        // 该文件指针可以通过 getFilePointer方法读取
        System.out.println("当前文件的指针位置是:" + raf.getFilePointer());


        //那我想直接读取最后的d怎么办
        //通过 seek 方法设置。
        raf.seek(9);
        ch = raf.read();
        System.out.println((char) ch);
        // 该文件指针可以通过 getFilePointer方法读取
        System.out.println("当前文件的指针位置是:" + raf.getFilePointer());

        raf.close();
    }


    private static void write() throws IOException {
        // 创建随机访问流对象
        RandomAccessFile raf = new RandomAccessFile("e:\\demo\\raf.txt", "rw");

       raf.write("helloworld".getBytes());

       raf.close();
    }
}

程序执行结果:
h
当前文件的指针位置是:1
e
当前文件的指针位置是:2
d
当前文件的指针位置是:10

存储到文件的信息:

😯7. 属性集

🌲7.1 Properties

既然它既是一个集合,也是可以和IO流相结合使用的集合类。

具体使用方法下面讲解。

🌳7.2 构造方法

  • public Properties() :创建一个空的属性列表。

    构造演示如下:

    Properties prop = new Properties();
    

🌴7.3 以Map集合来测试

public class PropertiesDemo {
    public static void main(String[] args) {
        //以Map集合来测试
        Properties prop = new Properties();

        prop.put("001", "hello");
        prop.put("002", "world");
        prop.put("003", "Java");

        System.out.println("prop = " + prop);

        for (Object key : prop.keySet()) {
            Object value = prop.get(key);
            System.out.println(key + "::" + value);
        }
    }
}

程序执行结果:
prop = {003=Java, 002=world, 001=hello}
003::Java
002::world
001::hello

🌵7.4 特殊存储方法

  • public Object setProperty(String key, String value) : 保存一对属性。(添加元素)

  • public String getProperty(String key) :使用此属性列表中指定的键搜索属性值。(获取元素)

  • public Set<String> stringPropertyNames() :所有键的名称的集合。

    代码演示下:

    public class PropertiesDemo2 {
        public static void main(String[] args) {
            //测试特殊存储功能
            Properties prop = new Properties();
    
            //添加元素
            prop.setProperty("001", "hello");
            prop.setProperty("002", "world");
            prop.setProperty("003", "Java");
            // 打印属性集对象
            System.out.println("prop = " + prop);
            // 通过键,获取属性值
            System.out.println(prop.getProperty("001"));
            System.out.println(prop.getProperty("002"));
            System.out.println(prop.getProperty("003"));
    
            // 遍历属性集,获取所有键的集合
            Set<String> strings = prop.stringPropertyNames();
            // 打印键值对
            for (String key : strings ) {
                System.out.println(key+" -- "+prop.getProperty(key));
            }
        }
    }
    
    程序运行结果:
    prop = {003=Java, 002=world, 001=hello}
    hello
    world
    Java
    003 -- Java
    002 -- world
    001 -- hello
    

🌾7.5 与流相关的方法

  • public void load(Reader reader) :把文件中的数据读取到集合中。

  • public void store(Writer writer,String comments) :把集合中的数据存储到文件。

    代码演示下存入和读取的功能:

    public class PropertiesDemo3 {
        public static void main(String[] args) throws IOException {
            storeTest();
    
            loadTest();
        }
    
        //public void store(Writer writer,String comments):把集合中的数据存储到文件
        private static void storeTest() throws IOException {
            Properties prop = new Properties();
    
            prop.setProperty("刘方", "27");
            prop.setProperty("刘洋", "45");
            prop.setProperty("刘慈", "37");
            prop.setProperty("刘念", "18");
    
            prop.store(new FileWriter("e:\\demo\\prop.txt"), "");
        }
    
        //public void load(Reader reader):把文件中的数据读取到集合中
        private static void loadTest() throws IOException {
            Properties prop = new Properties();
            prop.load(new FileReader("e:\\demo\\prop.txt"));
    
            System.out.println("prop:" + prop);
            Set<String> strings = prop.stringPropertyNames();
            for (String key : strings) {
                System.out.println(key + " -- " + prop.getProperty(key));
            }
        }
    }
    
    程序执行结果:
    prop:{刘慈=37, 刘洋=45, 刘方=27, 刘念=18}
    刘慈 -- 37
    刘洋 -- 45
    刘方 -- 27
    刘念 -- 18
    

    可以查看文件:

🌿7.6 小游戏案例

/**
 * 猜数字小游戏,只能玩5次。
 */
public class PlayGameDemo {
    public static void main(String[] args) throws IOException {
        // 把数据加载到集合中
        Properties prop = new Properties();
        prop.load(new FileReader("e:\\demo\\count.txt"));// count=0

        String value = prop.getProperty("count");
        int number = Integer.parseInt(value);

        if (number > 5) {
            System.out.println("游戏试玩已结束,请您进行付费充值。");
            System.exit(0);
        } else {
            number++;
            prop.setProperty("count", String.valueOf(number));
            Writer w = new FileWriter("e:\\demo\\count.txt");
            prop.store(w, null);
            w.close();

            GuessNumber.start();
        }
    }
}

class GuessNumber {
    private GuessNumber() {
    }

    public static void start() {
        // 产生一个随机数
        int number = (int) (Math.random() * 100) + 1;

        // 定义一个统计变量
        int count = 0;

        while (true) {
            // 键盘录入一个数据
            Scanner sc = new Scanner(System.in);
            System.out.println("请输入数据(1-100):");
            int guessNumber = sc.nextInt();

            count++;

            // 判断
            if (guessNumber > number) {
                System.out.println("你猜的数据" + guessNumber + "大了");
            } else if (guessNumber < number) {
                System.out.println("你猜的数据" + guessNumber + "小了");
            } else {
                System.out.println("恭喜你," + count + "次就猜中了");
                break;
            }
        }
    }
}

当我玩到其中count>5 及count= 6时,在运行就会显示
游戏试玩已结束,请您进行付费充值。
    

可以看到当我玩了5次猜数字小游戏后,就不能再玩了,而且看文件后,发现count.txt中的count值也达到了6了。

🌸完结散花

相信各位看官都对整个IO流体系有了详细的了解,在实际应用中,IO流用到的地方其实还是很多滴,只是你没发现而已!比如我们的上传下载,这是不是得用到IO流,比如传输文件等等,所以IO流的学习也是必不可少滴!基础打牢,底盘坚固,才能向上发展。

让我们也一起加油吧!本人不才,如有什么缺漏、错误的地方,也欢迎各位人才大佬评论中批评指正!当然如果这篇文章确定对你有点小小帮助的话,也请亲切可爱的人才大佬们给个点赞、收藏下吧,一键三连,非常感谢!

举报

相关推荐

0 条评论