Java-Day-25( 字节输入流 + FileInputStream 和 FileOutputStream + FileReader 和 FileWriter + 节点流和处理流 )

发布时间 2023-05-29 20:12:23作者: 朱呀朱~

Java-Day-25

InputStream ( 字节输入流 )

image-20230523165426532

  • InputStream 抽象类是所有类字节输入流的超类
  • InputStream 常用的子类
    • FileInputStream:文件输入流
    • BufferedInputStream:缓冲字节输入流
    • ObjectInputStream:对象字节输入流

FileInputStream 和 FileOutputStream

FileInputStream ( 文件输入流 )

  • 读取 hello.txt 文件,并将文件内容显示到控制台
    • read():从该输入流读取一个字节的数据,如果没有输入可用,此方法将阻止
      • 返回值是数据的下一个字节,如果到文件末尾就输出 -1
public class test {
    public static void main(String[] args) {  }

    @Test
    public void readFile01() {
        String filePath = "e:\\hello.txt";
        int readData = 0; // *1
//        字节数组(优化)
        byte[] buf = new byte[8]; // 一次读取八个字节 ~1
        int readLen = 0; // ~1 实际读取的字节数

//        写在外面,扩大范围,便于释放时获取
        FileInputStream fileInputStream = null;

        try {
//            创建对象,用于读取文件
             fileInputStream = new FileInputStream(filePath);
//            存在编译异常,必须处理

//            所有想要全输出就需要while循环
//            while ((readData =  fileInputStream.read()) != -1) {  // *1 返回值是数据的下一个字节,如果到文件末尾就输出 -1
            while ((readLen = fileInputStream.read(buf)) != -1) { // ~1 读取正常,返回的是实际读取到的字节数,如果到文件末尾就输出 -1
//                System.out.print((char) readData); // 因为用的int接收,所有输出原内容需要char转 *1
                System.out.print(new String(buf, 0, readLen)); // 需要构建出字符串加以显示
//                在第一次读到了8个字节并存到buf数组里后,再读第二次,第二次若不满足8个就只替代读取到的数量,剩下没替代的就保存着上次剩下的,
//                buf实行的是指定位置替代
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
//            流是一种资源,所以每次接收需要:
//            关闭文件流,释放资源(否则越来越多的流会造成资源的浪费)
//            此异常或try或抛
            try {
                fileInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 因为若是 utf-8 编码,汉字就是三个字节组成,所以字节读取时只能读到这个汉字的第一个字节 ( 但三个字节一起才是一个字 ),所以会乱码 ( 中文字符也会乱码 )
    • 所以优化用 read(byte[] b),一次性存储多个字节存进 btye 数组中
    • 但是中文符号依旧出现个别乱码

FileOutputStream ( 文件输出流 )

  • 演示如何使用 FileOutputStream 将数据写到文件中,如果该文件不存在,则创建该文件
    • 两种对象创建方法,三种写入方法
@Test
public void writeFile() {
    //        创建 FileOutputStream 对象
    String filePath = "e:\\hello.txt"; // 若无此文件,就加以创建先
    FileOutputStream fileOutputStream = null;
    try {
        //            得到 FileOutputStream 对象,两个方法

        //            F1:   new FileOutputStream(filePath)方式,当写入内容时,会覆盖原来的内容
        //            fileOutputStream = new FileOutputStream(filePath);
        //            F2:   new FileOutputStream(filePath, true)方式,当写入内容时是追加到文件的后面(保留原来)
        fileOutputStream = new FileOutputStream(filePath, true);

        //            1. 写入一个字节: 'H'
        //            fileOutputStream.write('H');

        //            2. 想加入多个,就写入字符串
        String str = "hello, world!";
        //            2.1 getBytes() 可以把字符串 ——> 字节数组
        //            fileOutputStream.write(str.getBytes());

        //            3. write(byte[] b, int off, int len) 将 len 个字节从位于偏移量off的指定字节数组写入此文件输出流
        fileOutputStream.write(str.getBytes(), 0, 3); // 只写前三个字符,
        //            若是想全部输出,也可以选择在off处写:str.length()

    } catch (IOException e) {
        e.printStackTrace();
    }
    try {
        fileOutputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
  • 演示编程完成图片 / 音乐的拷贝,将 e:\\student.jpg 拷贝到 d:\\
    • 文件 1 —( 输入流 )—> java 程序 —( 输出流 )—> 文件 2
    • 如果文件 1 很大,那就是读了一部分数据就输出到了 2,即读取部分数据就写入到指定文件 ( 使用循环操作 )
public class test {
    public static void main(String[] args) {
//        完成文件拷贝,将 e:\\student.jpg 拷贝 d:\\

        String srcFilePath = "e:\\student.jpg"; // 获取
        String destFilePath = "d:\\student.jpg"; // 复制到
        FileInputStream fileInputStream = null; // 创建输入流
        FileOutputStream fileOutputStream = null; // 创建输出流

        try {
            fileInputStream = new FileInputStream(srcFilePath);
            fileOutputStream = new FileOutputStream(destFilePath);
//            定义一个字节数组,提高读取效果
            byte[] buf = new byte[1024];
            int readLen = 0;
            while ((readLen = fileInputStream.read(buf)) != -1) {
//                读取到后就写入文件,注意是:一边读一边写
                fileOutputStream.write(buf, 0 , readLen); // 一定要用含有三个参数的方法
//                write(buf) 的话,第一次读1024,若是第二次只剩了888,还是写进1024就会文件受损
            }
            System.out.println("拷贝成功~~");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
//                关闭输入流和输出流,及时释放
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

FileReader 和 FileWriter

  • FileReader 和 FileWriter 是字符流,即按照字符来操作 IO

FileReader 常用方法

image-20230523234325914

  • new FileReader(File/String):通过字符串或文件对象来构建对象

  • read:每次读取单个字符 ( 汉字就不会乱码了,仍是三个字节 ),返回该字符,到末尾返回 -1

  • read(char[]):批量读取多个字符到数组,返回读取到的字符数,如果到文件末尾返回 -1

    • new String(char[]):将 char[] 转换成 String

    • new String(char[], off, len):将 char[] 的指定部分转换成 String

    • 例:输入时 byte 汉字乱码部分时

  • 莫忘 close,如果有多个 try / catch,就用 IOException

  • 练习:

    /**
     * 单个字符读取
     */
    @Test
    public void readFile01() {
        String filePath = "e:\\hello.txt";
        FileReader fileReader = null;
        int data = 0;
//        1. 创建 FileReader 对象
        try {
            fileReader = new FileReader(filePath);
//            循环读取 使用 read(字符一个个读取)单个字符读取
            while ((data = fileReader.read()) != -1) {
                System.out.print((char) data); // char转
            }
        } catch (IOException e) { // 有多个需要抛出catch的话,就要用IO总
            e.printStackTrace();
        } finally {
            if (fileReader != null) {
                try {
                    fileReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 使用字符数组读取文件
     */
    @Test
    public void readFile02() {
        String filePath = "e:\\Internet.txt";
        FileReader fileReader = null;
        int readLen = 0;
        char[] buf = new char[8]; // 数组大小自行设定
//        1. 创建 FileReader 对象
        try {
            fileReader = new FileReader(filePath);
//            循环读取 使用 read(字符数组)返回的是实际读取到的字符数 ——> 汉字不会乱码
            while ((readLen = fileReader.read(buf)) != -1) {
                System.out.print(new String(buf, 0, readLen)); // new String
            }
        } catch (IOException e) { // 有多个需要抛出catch的话,就要用IO总
            e.printStackTrace();
        } finally {
            if (fileReader != null) {
                try {
                    fileReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

FileWriter 常用方法

image-20230523235351316

  • new FileWriter(File/String):覆盖模式,相当于流的指针在首端

  • new FileWriter(File/String, true):追加模式,相当于流的指针在尾端

  • writer(int):写入单个字符

  • writer(char[]):写入指定数组

  • writer(char[], off, len):写入指定数组的指定部分

  • writer(String):写入整个字符串

  • writer(String, off, len):写入字符串的指定部分

    • String 类:toCharArray 方法,将 String 转换成 char[]
    • 注意指定部分时,后面的是 len 指长度,不是截止序号
  • FileWriter 使用后,必须要关闭 ( close ) 或刷新 ( flush ),否则写入不到指定的文件

  • 练习:

public class test {
    public static void main(String[] args) {
//        创建 FileWriter 对象
        FileWriter fileWriter = null;
        String filePath = "e:\\note.txt";
        char[] chars = {'z', 'y', 'z'};

        try {
            fileWriter = new FileWriter(filePath); // 覆盖型
//            - writer(int):写入单个字符
            fileWriter.write('Z');
//            - writer(char[]):写入指定数组
            fileWriter.write(chars);
//            - writer(char[], off, len):写入指定数组的指定部分
            fileWriter.write("哇zyz·duang".toCharArray(), 0, 3);
//            - writer(String):写入整个字符串
            fileWriter.write("  hunter来了...");
//            - writer(String, off, len):写入字符串的指定部分
            fileWriter.write("哇zyz·duang", 5, 5);
//            在数据量大的情况下,可以使用循环操作

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
//                对应 FileWriter,一定要close关闭流或是flush才能真正的把数据写入到文件
//                否则就会看到只创建了文件(没有此文件名的话),却没有写入
//                原因源码可见

//                fileWriter.flush();
//                close 关闭文件流,其实就等价于 flush() + 关闭
                fileWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  • close() —> se.close() —> this.implClose() —> this.writeBytes( 真正处理 ) —> this.out.write(this.bb.array(), this.bb.arrayOffset() + var2, var3);

节点流和处理流

  • 节点流可以从一个特定的数据源读写数据 ( 比较底层,直接操作数据源 ),如:

    • 针对某种具体的数据源来操作的,如访问文件:
      • FileReader、FileWriter ( 使用字符方式来处理 )
      • FileInputStream、FileOutputStream ( 使用字节来处理 )
    • 针对一个字符串 / 数组来操作的,如访问数组:
      • ByteArrayInputStream、ByteArrayOutputStream ( 字节 )
      • CharArrayReader、CharArrayWriter ( 字符 )
    • 访问管道、访问字符串等
    • 缺点:节点流固定,灵活性不足,所以提出了处理流,如下
  • 处理流 ( 也叫包装流 ),是 “ 连接 ” 在已存在的流 ( 节点流或处理流 ) 之上,为程序提供更为强大的读写功能,也更加灵活,如

    • 缓冲流,不再局限于数组或是其他什么数据源了,用字符的方式:

      • BufferedReader ( class 类内有属性 Reader,即可以封装一个节点流 —> 节点流可以任意,只要是 Reader 的一个子类就行 )
      • BufferedWriter ( 同理,也有 Writer 属性,可以封装任一 Writer 的子类 )

      image-20230526171329856

      • 部分源码:

        public class BufferedWriter extends Writer {
            private Writer out;
        // ......  
        // 构造器其一:
            public BufferedWriter(Writer out)
        

        image-20230526171742545

      • 上图构造器可见,能放入 CharArrayWriter、StringWriter 等对象,这便是包装流 ( 一种修饰器模式的设计模式 )

  • 节点流和处理流都可分为字节和字符,字节和字符又分为输入流或是输出流

区别和联系

  • 节点流是底层流 / 低级流,直接跟数据源相接

  • 处理流 ( 包装流 ) 包装节点流,既可以消除不同节点流的实现差异 ( 有些对文件操作,有些对数组操作 ),也可以提供更方便的方法来完成输入和输出 ( 想对谁操作就传入哪个相关的实现子类 )

  • 处理流 ( 包装流 ) 对节点流进行包装,使用了修饰器设计模式,不会直接与数据源相连 ( 只是调用 )

    • Reader_ 抽象类:

      public abstract class Reader_ { 
          public void readFile() {
      //        FileReader_ 实现
          }
          public void readString() {
      //        StringReader_ 实现
          }
      }
      
    • FileReader_ 实现类

      public class FileReader_ extends Reader_ { // 节点流
          public void readFile() {
              System.out.println("对文件进行读取...");
          }
      }
      
    • StringReader_ 实现类

      public class StringReader_ extends Reader_ { // 节点流
          public void readString() {
              System.out.println("读取字符串...");
          }
      }
      
      
    • BufferedReader_

      public class BufferedReader_ extends Reader_ { // 包装流
           private Reader_ reader_; // 属性就是 Reader_ 类型
      
          public Reader_ getReader_() { // F1:为了在Test中也能够调用传入的对象的方法
              return reader_;
          }
      
      //    构造器接收Reader_子类对象
          public BufferedReader_(Reader_ reader_) {
              this.reader_ = reader_;
          }
      
      //    在此类中让方法更加灵活,如:
      //    扩展为多次读取文件
          public void readFiles(int num) {
              for (int i = 0; i < num; i++) {
                  reader_.readFile();
              }
          }
      
      //    F2:可以选择放入原封不动的实现的方法
          public void readString() { // 相当封装了一层
              reader_.readString();
          }
      
      //    再扩展 readString(),使之批量处理字符串数据
          public void readStrings(int num) {
              for (int i = 0; i < num; i++) {
                  reader_.readString();
              }
          }
      //    在一定程度上扩展了原先的FileReader_和StringReader_
      }
      
    • Test.java:

      public class Test {
          public static void main(String[] args) {
              System.out.println("对文件操作~~~~~~~~~~~~~");
              BufferedReader_ bufferedReader_ = new BufferedReader_(new FileReader_());
              bufferedReader_.readFile(); // F1:bufferedReader_里没有此方法,所以调用了父类的readFile()空方法
      //        应该拿到bufferedReader_的放入的实现类对象(私有属性要get),才能调用实现类中实现了的方法
              System.out.println("既可以调用FileReader_实现类读只一次:");
              bufferedReader_.getReader_().readFile();
      //        此方法是在BufferedReader_类里,借用实现类的实现方法进行增强,可直接调用
              System.out.println("也可以用bufferedReader_进行增强的方法,从而读多次:");
              bufferedReader_.readFiles(2);
      
              System.out.println("对字符串操作~~~~~~~~~~~~~~~");
              BufferedReader_ bufferedReader_2 = new BufferedReader_(new StringReader_());
      //        F2:或者是更简单的,既然想能够直接调用传入实现类的方法,那就在BufferedReader_类里接收、封装下就好
              bufferedReader_2.readString(); // 这样就可以直接用来
              bufferedReader_2.readStrings(2); // 增强后
          }
      }
      
    • 以上是为了区分明显,实则也可以:

      // Reader_:
      public abstract class Reader_ { // 抽象类 Reader
      //    在 Reader_ 抽象类,使用read抽象方法统一管理
      //    后面调用时,利用对象动态绑定机制,绑定到对应的实现子类即可
          public abstract void read();
      }
      
      //   FileReader_:
      public class FileReader_ extends Reader_ {
          public void read() {
      //        这样用多态的动态绑定机制,绑定到对应的实现子类FileReader_即可
              System.out.println("对文件进行读取...");
          }
      }
      
      // BufferedReader_:
      public class BufferedReader_ extends Reader_ {
          private Reader_ reader_; // 属性就是 Reader_ 类型
      
      //    构造器接收Reader_子类对象
          public BufferedReader_(Reader_ reader_) {
              this.reader_ = reader_;
          }
      
          @Override
          public void read() {
              reader_.read();
          }
          public void reads(int num) {
              for (int i = 0; i < num; i++) {
                  reader_.read();
              }
          }
      }
      
      // Test_:
      BufferedReader_ bufferedReader_ = new BufferedReader_(new FileReader_());
      bufferedReader_.read();
      bufferedReader_.reads(2);
      

处理流的功能主要体现:

  • 性能的提高:主要以增加缓冲的方式来提高输入和输出的效率
  • 操作的便捷:处理流可能提供了一系列便捷的方法来一次输入输出大批量的数据,使用更加灵活方便

处理流 BufferedReader 和 BufferedWriter

  • 两者属于字符流,是按照字符来读取数据的

    • 尽量是文本文件会比较高效
    • 因为二进制文件 ( 图片、视频等 ) 是按照字节组织的,可能会造成损毁
  • 关闭处理流时,只需要关闭外层流即可

    • 即,BufferedReader_ bufferedReader_ = new BufferedReader_(new FileReader_()); 时,只需要关闭 bufferedReader_,内部 FileReader ( 包装的节点流 ) 会在底层自动关闭

练习

  • 使用 BufferedReader 读取文件,并显示在控制台

    public class Test {
        public static void main(String[] args) throws Exception {
            String filePath = "e:\\index.jsp";
    //        创建
            BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath));
    //        读取
            String line; // 按行读取:
    //        1. bufferedReader.readLine() 是按行来读取文件
    //        2. 此方法当文件读取完毕到达流末尾时,就返回null
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
    //        关闭流,只需要关闭BufferedReader即可,因为底层会自动关节点流
            /* private Reader in;
             * ...
             * if (in == null)
                    return;
                try {
                    in.close(); // 节点流关闭
                } finally {
                    in = null;
                    cb = null;
                }
             */
            bufferedReader.close();
        }
    }
    
  • 使用 BufferedWriter 将 “ 哈罗呀,朱呀朱 ”,写入到文件中

    public class Test {
        public static void main(String[] args) throws Exception {
            String filePath = "e:\\zyz.txt";
    //        创建
            BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(filePath, true));
    //        要写入多句的话就插入一个换行 newLine
            bufferedWriter.write("哈罗呀,朱呀朱No1");
            bufferedWriter.newLine(); // 插入一个和系统相关的换行
            bufferedWriter.write("哈罗呀,朱呀朱No2");
            bufferedWriter.newLine();
            bufferedWriter.write("哈罗呀,朱呀朱No3");
            bufferedWriter.newLine();
    
    //        关闭包装流
            bufferedWriter.close();
        }
    }
    
    • 想要不被覆盖还得在节点流中操作 ( 如 FileWriter 加上 true 成追加方式 ),BufferedWriter 构造器没有防覆盖操作
  • 综合使用 BufferedReader 和 BufferedWriter 完成文本文件的拷贝

    • 切记不要忘了是字符操作,不要操作二进制文件 ( 声音、视频、doc、pdf 等 )
    public class Test {
        public static void main(String[] args) {
            String srcFilePath = "e:\\index.jsp";
            String desFilePath = "e:\\zyzWa.txt";
    //        创建
            BufferedReader br = null;
            BufferedWriter bw = null;
            String line;
    
            try {
                br = new BufferedReader(new FileReader(srcFilePath));
                bw = new BufferedWriter(new FileWriter(desFilePath, true));
    //            readLine 读取了一行内容,但没用换行,所以还要自行插入换行
                while ((line = br.readLine()) != null) {
    //                每读取一行,就写入
                    bw.write(line);
    //                插入一个换行 
                    bw.newLine();
                }
                System.out.println("拷贝完毕~");
    
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (br != null) {
                        br.close();
                    }
                    if (bw != null) {
                        bw.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    • try-catch 是直接处理,而 throw 是将异常抛给了上级

处理流 BufferedInputStream 和 BufferedOutputStream

  • BufferedInputStream 是字节流,在创建BufferedInputStream 时,会创建一个内部缓冲区数组
    • InputStream 实现类 in 在 FilterInputStream 里获取,在 BufferedInputStream 里操作
    • buf 是缓冲区数组

image-20230528000946639

  • BufferedOutputStream 是字节流,实现缓冲的输出流,可以将多个字节写入底层输出流中,而不必对每次字节写入调用底层系统

image-20230528000858182

练习

public class Test {
    public static void main(String[] args) {
        String srcFilePath = "e:\\student.jpg";
//        String filePath = "e:\\student.jpg";
        String destFilePath = "e:\\zyz.jpg";
//        创建对象
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;

        try {
//            FileInputStream 是 InputStream 的子类
            bis = new BufferedInputStream(new FileInputStream(srcFilePath));
            bos = new BufferedOutputStream(new FileOutputStream(destFilePath));

//            循环地读取文件,并写入到 des文件路径下
            byte[] buff = new byte[1024];
            int readLen;
//            返回-1 表示读取完毕
            while ((readLen = bis.read(buff)) != -1) {
                bos.write(buff, 0, readLen);
            }

            System.out.println("文件拷贝完成~~");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
//            关闭处理流,节点流在底层会自行关闭
            try {
                if (bis != null) {
                    bis.close();
                }
                if (bos != null) {
                    bos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 因为是字节流,所以图片、音频 ( 二进制文件)、文本都可以操作的