暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

精通IO流:文件处理的必备技能!

追梦Java 2024-07-17
57

我们在掌握了 File 类、字节流、字符流,学会了 IO 操作的套路之后,IO 操作基本上就能处理日常工作中80%的常用问题了。
今天再给大家介绍一下处理流,学会处理流之后,日常工作中的文件操作就都可以应对了,掌握了下面的处理流,你将如虎添翼。


我们知道从 IO 流的功能来划分,IO 流分为:节点流和处理流。其中,节点流是用来包装数据源(File)的,它直接和数据源连接,表示从一个节点读取数据或者把数据写入到一个节点;处理流是用来包装节点流的,它是对一个已经存在的节点流进行连接,处理流通过增加缓存的方式来提高输入输出操作的性能。


处理流按照功能划分,可以分为:缓冲流、转换流、数据处理流、对象处理流。缓冲流是为了提高处理性能的,转换流是字节流转换为字符流用于处理乱码的(解码与编码的字符集问题),数据处理流就是对 8个基本类型和字符串类型数据的直接处理,对象数据处理流就是经常说的序列化与反序列化操作。


1

缓冲流

1、认识字节缓冲流


字节缓冲流就是用缓冲流包裹字节流,也就是说在创建缓冲流对象的时候,需要传入一个字节流的对象,同时会创建一个默认 8KB 的字节数组的缓冲区,通过这个缓冲区进行读写操作,以减少 IO 的次数,从而提高字节流的处理性能。


字节缓冲流分为:字节输入缓冲流 BufferedInputStream 和字节输出缓冲流 BufferedOutputStream,以下代码是字符缓冲流的源码:

     // 字节输入缓冲流的构造方法
     private static int DEFAULT_BUFFER_SIZE = 8192;  // 默认8KB
    public BufferedInputStream(InputStream in) {
    this(in, DEFAULT_BUFFER_SIZE);
    }
    public BufferedInputStream(InputStream in, int size) {
    super(in);
    if (size <= 0) {
    throw new IllegalArgumentException("Buffer size <= 0");
    }
            buf = new byte[size];  // 建立缓冲区
    }
     // 节输出缓冲流的构造方法
     public BufferedOutputStream(OutputStream out) {
    this(out, 8192); // 默认8KB
    }
     public BufferedOutputStream(OutputStream outint size) {
    super(out);
    if (size <= 0) {
    throw new IllegalArgumentException("Buffer size <= 0");
    }
    buf = new byte[size]; // 建立缓冲区

    2、用字节缓冲流实现文件拷贝功能

      @Test
      public void testCopy() throws IOException {
      // 1、使用File类与文件建立联系
      File src = new File("D:/file/image/tomcat.png");
      File dest = new File("D:/file/image/tomcat2.jpg");
          // 2、选择对应的输入流或者输出流
      InputStream is = new BufferedInputStream(new FileInputStream(src)); // 用缓冲流包裹节点流
      OutputStream os = new BufferedOutputStream(new FileOutputStream(dest)); // 用缓冲流包裹节点流
      // 3、进行读写操作
      byte[] b = new byte[1024];
      int len = 0;
      while ((len = is.read(b)) != -1) {
      os.write(b, 0, len);
      }
      os.flush();
      // 4、关闭资源
      os.close();
      is.close();
      }

      运行结果:


      3、认识字符缓存流


      字符缓冲流就是用缓冲流包裹字符流,也就是说在创建缓冲流对象的时候,需要传入一个字符流的对象,同时会创建一个默认 8KB 的字符数组的缓冲区,通过这个缓冲区进行读写操作,以减少 IO 的次数,从而提高字符流的处理性能。


      字符缓冲流分为:字符输入缓冲流 BufferedReader 和字符输出缓冲流 BufferedWriter,以下代码是字符缓冲流的源码:

        // 字符输入缓冲流的构造方法
        private static int defaultCharBufferSize = 8192; // 默认8KB
        public BufferedReader(Reader in) {
        this(in, defaultCharBufferSize);
        }
        public BufferedReader(Reader in, int sz) {
        super(in);
        if (sz <= 0)
        throw new IllegalArgumentException("Buffer size <= 0");
        this.in = in;
        cb = new char[sz]; // 建立缓冲区
        nextChar = nChars = 0;
        }
        // 字符输出缓冲流的构造方法
        private static int defaultCharBufferSize = 8192; // 默认8KB
        public BufferedWriter(Writer out) {
        this(out, defaultCharBufferSize);
        }
        public BufferedWriter(Writer out, int sz) {
        super(out);
        if (sz <= 0)
        throw new IllegalArgumentException("Buffer size <= 0");
        this.out = out;
        cb = new char[sz]; // 建立缓冲区
        nChars = sz;
        nextChar = 0;
                lineSeparator = java.security.AccessController.doPrivileged(
        new sun.security.action.GetPropertyAction("line.separator"));
        }

        4、用字符缓冲流实现文件拷贝功能

          /**
          * 字符缓冲流只能处理纯文本的copy
          */
          @Test
          public void testCopy1() throws IOException {
          // 1、使用File类与文件建立联系
          File src = new File("D:/file/txt/output_char.txt");
          File dest = new File("D:/file/image/output_char_coppy.txt");
          // 2、选择对应的输入流或者输出流
          // 想使用新增的readLine方法(不能发生多态)
          BufferedReader reader = new BufferedReader(new FileReader(src));// 用缓冲流包裹节点流
          BufferedWriter writer = new BufferedWriter(new FileWriter(dest, true));// 用缓冲流包裹节点流
          // 3、进行读或写操作
          String line = null;
          while ((line = reader.readLine()) != null) {
          writer.write(line);
          writer.newLine(); // 类似于writer.append("\r\n");
          }
          writer.flush(); // 强制刷出
          // 4、关闭资源
          writer.close();
          reader.close();
          }

          运行结果:


          因为缓冲流可以提高文件操作的性能,所以在以后的开发中,大家尽量要用缓冲流对节点流进行包装,不要直接使用字节流和字符流去操作文件。


          通过以上的代码大家再来体会一下 IO 流操作的套路,是不是套路在手,操作不愁啊!


          2

          转换流

          1、乱码产生的原因


          我们首先来看两个概念,什么是编码,什么是解码?要区分这两个概念的话,也比较好理解:我们从码的角度出发来认识它们,码就是计算机能看懂的东西,也就是“二进制,等同于字节,人类能看懂的语言是“字符或者字符串


          如果是人类能看懂的变为计算机能看懂的就叫编码,也就是说字符或者字符串”变为字节就是编码,反过来,如果是计算机能看懂的变为人类能看懂的就叫解码,也就是说字节变为字符或者字符串就是解码。


          大家可以通过加密和解密来对比理解,人看不懂就是加密,人能看到就是解密。


          乱码产生的原因有两个:

          1、编码与解码的字符集不相同,导致乱码;

          2、字节缺少或者长度丢失,导致乱码;

            /**
            * 乱码的原因
            */
            @Test
            public void test() throws UnsupportedEncodingException {
            // 默认字符集“utf-8”
            System.out.println("默认字符集:" + System.getProperty("file.encoding"));
            String info = "北京欢迎您!"; // 解码
            byte[] data = info.getBytes(); // 编码:char--->byte,字符或者字符串到字节
            // 编码与解码字符集统一,都使用工作空间默认的字符集
            System.out.println(new String(data)); // 解码:byte--->char,字节到字符或者字符串
            // 不统一则出现乱码
            System.out.println(new String(data, "GBK"));


            // 编码与解码的字符集必须相同,否则乱码
            byte[] data2 = "JPM,你好!".getBytes("GBK");// 编码
            String info2 = new String(data2, "GBK");// 解码
            System.out.println(info2);


            // 乱码的原因之二,字节缺少,长度丢失
            String str = "北京";
            byte[] data3 = str.getBytes();
            System.out.println(data3.length); // 6
            System.out.println(new String(data3, 0, 5)); // 字节数不完整导致乱码
            }

            运行结果:

              默认字符集:UTF-8
              北京欢迎您!
              鍖椾含娆㈣繋鎮紒
              JPM,你好!
              6
              北�

              2、认识转换流


              在 Java IO 中除了字节流和字符流外,还有一组字节流转换位字符流的类,用于处理乱码问题。


              字节输入流 InputStreamReader:作用是将输入的字节流变为字符流。

              字节输出流 OutputStreamWriter:作用是将输出的字节流变为字符流。


              转换流只能是把字节流转为字符流,从而完成它的使命,那是因为字符流不能设置字符集,只能是把字符流变为字节流才能进行字符集的设置,因为字节流才有设置字符集的方法。


                // 输入流 InputStreamReader 解码
                public InputStreamReader(InputStream in, String charsetName)
                throws UnsupportedEncodingException
                {
                super(in);
                if (charsetName == null)
                throw new NullPointerException("charsetName");
                sd = StreamDecoder.forInputStreamReader(in, this, charsetName);
                }

                //输出流 OutputStreamWriter 编码
                public OutputStreamWriter(OutputStream out, String charsetName)
                throws UnsupportedEncodingException
                {
                super(out);
                if (charsetName == null)
                throw new NullPointerException("charsetName");
                se = StreamEncoder.forOutputStreamWriter(out, this, charsetName);
                }

                3、转换流的文件拷贝demo,仔细体会注释的文字

                  /**
                  * 转换流:字节转为字符<br>
                  * 1、输入流 InputStreamReader 解码<br>
                  * 2、输出流 OutputStreamWriter 编码<br>
                  * 仔细体会注释的文字
                  */
                  @Test
                  public void test2() throws IOException {
                  String srcPath = "D:/file/txt/output_char.txt";
                  String destPath = "D:/file/txt/output_char_convert.txt";
                  // FileReader(字符流)不能解码,FileInputStream(字节流)才能解码
                  // BufferedReader br = new BufferedReader(new FileReader(new File(srcPath)));
                  // 字符流FileReader要换成字节流FileInputStream,但是字节流与字符流不能直接操作,需要通过转换流InputStreamReader来实现
                  BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(new File(srcPath)), "UTF-8")); // 指定解码字符集
                  // FileWriter(字符流)不能编码,FileOutputStream(字节流)才能编码
                  // BufferedWriter writer = new BufferedWriter(new FileWriter(new File(destPath)));
                  // 字符流FileWriter要换成字节流FileOutputStream,但是字节流与字符流不能直接操作,需要通过转换流OutputStreamWriter来实现
                  BufferedWriter wr = new BufferedWriter(
                  new OutputStreamWriter(new FileOutputStream(new File(destPath)), "UTF-8"));// 指定编码字符集
                  // 读取并写出
                  String line = null;
                  while ((line = br.readLine()) != null) {
                  wr.write(line);
                  wr.newLine();
                  }
                  wr.flush();
                  wr.close();
                  br.close();
                  }

                  运行结果:


                  3

                  数据处理


                  在 Java IO 中,提供了两个数据(基本数据类型+String)操作流 ,分别是数据输入流 DataInputStream 和数据输出流 DataOutputStream


                  下面直接通过一个例子来演示数据处理流的用法:

                    @Test
                    public void test() throws IOException {
                    write("D:/file/txt/data.txt"); // 写到文件
                    read("D:/file/txt/data.txt"); // 从文件读取
                    }
                    /**
                    * 基本数据类型+String类型输出到文件
                    */
                    public static void write(String destPath) throws IOException {
                    int intNum = 100;
                    long longNum = 999L;
                    float floatNum = 3.14f;
                    double doubleNum = 5.50;
                    String str = "基本数据类型+String类型输出到文件";
                    File dest = new File(destPath);
                    DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(dest)));
                    // 操作:注意写出的顺序,读取要和写出的顺序一致
                    dos.writeInt(intNum);
                    dos.writeLong(longNum);
                    dos.writeFloat(floatNum);
                    dos.writeDouble(doubleNum);
                    dos.writeUTF(str);
                    dos.flush();
                    dos.close();
                      }
                      /**
                    * 从文件里读取基本数据类型+String类型
                    */
                    public static void read(String srcPath) throws IOException {
                    File src = new File(srcPath);
                    DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(src)));
                    int intNum = dis.readInt();
                    long longNum = dis.readLong();
                    float floatNum = dis.readFloat();
                    double doubleNum = dis.readDouble();
                    String str = dis.readUTF();
                    dis.close();
                    // 100---->999---->3.14---->5.5---->基本数据类型+String类型输出到文件
                    System.out.println(intNum + "---->" + longNum + "---->" + floatNum + "---->" + doubleNum + "---->" + str);
                    }  

                    运行结果:

                      100---->999---->3.14---->5.5---->基本数据类型+String类型输出到文件


                      4

                      对象处理


                      对象处理流的操作就是我们经常说的序列化与反序列化操作序列化就是把一个对象变为二进制流的一种方法,通过对象序列化可以方便地实现对象的传输和存储,反过来,如果把一个对象读入到程序的过程及时反序列化。序列化操作需要使用输出流 ObjectOutputStream 进行输出,反序列化操作需要使用输出流 ObjectInputStream 进行读取对象数据。


                      如果一个类的对象想被序列化,这个类必须实现 Serializable 接口,同时要注意这个类对象的版本兼容问题,一般我们再要进行序列化的类中设置一个固定的 serialVersionUID 常量,这个值只要不修改,序列化和反序列化操作就不会发生版本兼容问题。


                      为了减少保存对象的使用空间,可以把一个类的某个属性设置为不被序列化,当实现 Serializable 接口实现序列化的时候,可以使用  transient 关键字进行声明。


                      下面直接通过一个例子来演示对象处理流的用法:

                        /**
                        * 对象的序列化以及反序列化操作
                        */
                        @Test
                        public void test() throws FileNotFoundException, IOException, ClassNotFoundException {
                        String filePath = "D:/file/txt/object.txt";
                        serializa(filePath);
                        Object object = UnSerializa(filePath);
                        if (object instanceof User) {
                        object = (User) object;
                        }
                        // User [name=JPM, age=18, address=null],因为address属性被transient修饰,没有被序列化,所以为null
                        System.out.println(object.toString());
                        }


                        /**
                        * 对象序列化:对象变为二进制流的方法
                        */
                        public static void serializa(String destPath) throws FileNotFoundException, IOException {
                        File dest = new File(destPath);
                        ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(dest)));
                        User user = new User("JPM", 18, "中国,北京!");
                        oos.writeObject(user);
                        oos.flush();
                        oos.close();
                        }


                        /**
                        * 对象反序列化:使用对象输入流读取对象数据
                        */
                        public static Object UnSerializa(String srcPath) throws FileNotFoundException, IOException, ClassNotFoundException {
                        Object object = null;
                        File scr = new File(srcPath);
                        ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(scr)));
                        object = ois.readObject();
                        ois.close();
                        return object;
                        }

                        /**
                         * 序列化与反序列化的对象,必须实现Serializable接口
                        */
                        public class User implements Serializable {


                        private static final long serialVersionUID = -6954786920974801199L;


                        private String name;
                        private int age;
                        // transient修饰的属性不会被序列化
                        private transient String address;


                        public User() {
                        super();
                        }


                        public User(String name, int age, String address) {
                        super();
                        this.name = name;
                        this.age = age;
                        this.address = address;
                        }


                        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 getAddress() {
                        return address;
                        }


                        public void setAddress(String address) {
                        this.address = address;
                        }


                        @Override
                        public String toString() {
                        return "User [name=" + name + ", age=" + age + ", address=" + address + "]";
                        }


                        }

                        运行结果:

                          User [name=JPM, age=18, address=null]


                          这是 Java IO 操作的第三篇文章,文章有点长,坚持看下来的小伙伴们也非常不容易,但是我想说,能坚持看完这三篇 IO 文章的同学,你一定掌握了 Java IO 处理的套路,面对未来开发中涉及到的 IO 操作,一定会更加从容自如,如果你能把所有的示例代码手动敲一遍,那你的感觉就会更加美好,不信你试试。


                          回顾一下:

                          IO 第一篇:带你认识 File 类 我们对 File 的使用做了详细介绍;

                          IO 第二篇:IO 流,掌控一切 我们对 IO 流的操作套路有了深刻认识;

                          IO 第三篇:就是本文,我们对不能不懂的 IO 处理流进行了总结;


                          Java IO 系列第四篇:IO 操作大结局,敬请期待。

                          文章转载自追梦Java,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                          评论