概念

  • IO:输入/输出(Input/Output),视角为内存视角。
  • 流:是一种抽象概念,是对数据传输的总称。也就是说数据在设备间的传输称为流,流的本质是数据传输
  • IO流就是用来处理设备间数据传输问题的。常见的应用场景: 文件复制; 文件上传; 文件下载、文件查看

IO流的分类

  • 按照数据的流向

    • 输入流:读数据
    • 输出流:写数据
  • 按照数据类型来分

    • 字节流(万能流)

      • 字节输入流(InputStream)
      • 字节输出流(OutputStream)
    • 字符流

      • 字符输入流(Reader)
      • 字符输出流(Writer)
  • 字符流指的是传递字符的流,建议只用于读写纯文本文件;

    • 纯文本文件:如果某个文件用系统自带的记事本打开并且能够轻松看懂,就是纯文本文件。
  • 字节流指的是传递字节的流,可以读写任意文件。又称万能流。

3. 字节流

字节流就是以字节为单位操作数据的流。

3.1 字节输出流:写

3.1.1 基本使用:写一个字节

  • 相关方法说明

| 方法签名 | 说明 |
| ------------------------------------------------------ | ---------------------------------------------------------------- |
| FileOutputStream FileOutputStream(File file) | 基于一个File封装一个FileOutputStream对象 |
| FileOutputStream FileOutputStream(String pathName) | 基于一个字符串格式的文件路径封装一个FileOutputStream对象 |
| void write(int b) | 把指定的内容写入文件 |
| void close() | 关闭流释放资源 |
| | |

  • 三个步骤

    1. 创建字节输出流(FileOutputStream)对象
    2. 写出数据
    3. 关闭流释放资源
  • 注释

    // FileOutputStream FileOutputStream(String pathName)  基于一个字符串格式的文件路径封装一个FileOutputStream对象
    // FileOutputStream FileOutputStream(File file)        基于一个File封装一个FileOutputStream对象
    // void write(int b)  把指定的内容写入文件
    // void close()  关闭流释放资源
  • 演示代码

    
    /**
     * 字节输出流入门案例:写一个字节
     */
    public class ByteStreamDemo01 {
        public static void main(String[] args) throws IOException {
    
            // FileOutputStream FileOutputStream(String pathName)  基于一个字符串格式的文件路径封装一个FileOutputStream对象
            // FileOutputStream FileOutputStream(File file)        基于一个File封装一个FileOutputStream对象
            // void write(int b)  把指定的内容写入文件
            // void close()  关闭流释放资源
    
    
    
            // 创建自己输入流对象
            // 创建输出流对象时,会判断关联的文件是否存在;
            // 如果不存在,会自动创建一个空白的文件
            // 如果存在,则清空文件内容
            FileOutputStream fos = new FileOutputStream("d:\\file\\a.txt");
    
            // 写出数据
            // 字节输入流,操作的是字节;写97其实写的不是97这两个字符,而是一个码点/码值;
            // 程序会把他当做一个字节写入到文件。我们查看,把这个字节的数字作为码点,查询字符集中对应的字符,后展示。
            fos.write(98);
    
            // 关闭流释放资源
            // 创建输入流对象时,会自动关联对应的文件;这个时候,可以理解为当前这个Java程序独占了该文件
            // 其他的程序想要操作相同文件时,会有问题。
            // 所以使用完之后,要关闭流,释放对文件的独占
            fos.close();
    
        }
    }

3.1.2 写入一个字节数组

  • 相关方法说明

| 方法签名 | 说明 |
| ------------------------------------------ | -------------------------------------------------- |
| void write(int b) | 一次写一个字节数据(编码值)到指定位置(文件) |
| void write(byte[] b) | 一次写一个字节数组数据到指定位置(文件) |
| void write(byte[] b,int off,int len) | 一次写一个字节数组的部分到指定位置(文件) |

  • 注释

    // void write(byte[] b)                一次写一个字节数组数据到指定位置(文件)
    // void write(byte[] b,int off,int len)一次写一个字节数组的部分到指定位置(文件) 
  • 演示代码

    
    public class ByteSteamDemo01 {
        public static void main(String[] args) throws IOException {
    
    
            // 1. 创建字节输出流(`FileOutputStream`)对象
            // FileOutputStream FileOutputStream(String pathName)  基于一个字符串格式的文件路径封装一个FileOutputStream对象
            FileOutputStream fos = new FileOutputStream("d:\\file\\java74\\a.txt",true);
    
            // 2.1 写出数据,每次写一个字节
            // void write(int b)  把指定的内容写入文件
            fos.write(97);
            fos.write(98);
            fos.write(99);
            fos.write(48);
    
            // 2.2 写出数据,每次写一个字节数组
            // void write(byte[] b)                一次写一个字节数组数据到指定位置(文件)
            fos.write(new byte[]{97,98,99});
    
            // void write(byte[] b,int off,int len)一次写一个字节数组的部分到指定位置(文件)
            // fos.write(new byte[]{-28, -72, -83, -27, -101, -67},0,3);
            fos.write(new byte[]{-28, -72, -83, -27, -101, -67},3,3);
    
            // 乱码的可能原因之一:使用字节流读写时,没有读写一个字符应该拥有的所有字节
            // fos.write(new byte[]{-28, -72, -83, -27, -101, -67},0,2);
    
            // 3. 关闭流释放资源
            fos.close();
        }
    }

换行

  • 写数据时写入如下内容即可实现换行

    • windows:\r\n(新版本Windows中 也可以正确识别Linuxmac的换行符)
    • linux:\n
    • mac:\r
  • 字符串提供了把字符串对象转换成字节数组的方法

    • Byte[] getBytes()
  • 演示代码

    // 加一个换行
    // 新版本Windows中  也可以正确识别`Linux`和`mac`的换行符
    fos.write("\r".getBytes());
    fos.write("你好,中国!".getBytes());
    // 加一个换行
    fos.write("\n".getBytes());
    fos.write("你好,中国!".getBytes());
    // 加一个换行
    fos.write("\r\n".getBytes());
    fos.write("你好,中国!".getBytes());

追加写入

创建FileOutputStream对象时,除了关联一个文件之外,还可指定是否追加写入。

  • public FileOutputStream(String name,boolean append)

    • 创建文件输出流以指定的名称写入文件。
    • 如果第二个参数为true ,则字节追加将写入文件的末尾;而不是直接写入,覆盖原有内容。
  • 演示代码

    // 第二个参数指定为true,关联文件后写入内容时,会追加写入而非清空原有内容
    FileOutputStream fos = new FileOutputStream("d:\\file\\a.txt",true);

    完整代码

    
    public class ByteStremDemo03 {
        public static void main(String[] args) throws IOException {
    
            // 1. 创建字节输出流(FileOutputStream)对象
            // 第二个参数指定为true,关联文件后写入内容时,会追加写入而非清空原有内容
            FileOutputStream fos = new FileOutputStream("d:\\file\\a.txt",true);
    
            // 2. 写入数据
    
            // 写入换行 windows
            fos.write("\r\n".getBytes());
    
            // 使用字节流,写  “你好,中国!”到文件
            // 使用API把字符串转为字节(而非自己查码表后拼接),写入文件
            String str = "你好,中国2023!";
            byte[] strBytes = str.getBytes();
            fos.write(strBytes);
    
    
            // 3. 关闭流释放资源
            fos.close();
    
        }
    }

注意事项

  1. 创建字节输出流(FileOutputStream)对象,会去关联一个文件并进行一些基础的操作。

    • 如果文件不存在,就创建空白文件;
    • 如果文件存在且有内容,默认清空文件内容;

      • 如果不想文件内容被清空,就在创建对应输出流对象的时候,指定第二个参数为true,表示追加写入。
  2. 写数据

    • 写出的整数,实际写出的是整数作为编码值在码表上对应的字符;
    • 可以写入多个字节(一个字节数组或者一个字节数组的部分内容);
    • 如果需要换行就写入"\r\n".getBytes()
  3. 释放资源

    • Java程序操作文件时会独占关联的文件,每次使用完流必须要释放资源。否则其他程序将不能正确访问该文件;

字节输入流:读

基本读取:读一个字节

字节输入流:读取一个字节

  • 相关方法说明

| 方法签名 | 说明 |
| ------------------------------------------------------ | --------------------------------------------------------------- |
| FileInputStream FileInputStream(String pathName) | 基于一个字符串格式的文件路径封装一个FileInputStream对象 |
| int read() | 从文件中读取一个字节,值为字节的编码值 |
| void close() | 关闭流释放资源 |
| | |

  • 步骤

    1. 创建输入流对象(FileInputStream),关联文件
    2. 读数据
    3. 关闭流释放资源
  • 注释

    // FileInputStream FileInputStream(String pathName)  基于一个字符串格式的文件路径封装一个FileInputStream对象
    // int read()从文件中读取一个字节,值为字节的码表值
    // void close()关闭流释放资源 
  • 演示代码

    
    public class ByteStreamDemo02 {
        public static void main(String[] args) throws IOException {
            // 1. 创建流对象,关联文件
            // FileInputStream FileInputStream(String pathName)  基于一个字符串格式的文件路径封装一个FileInputStream对象
            FileInputStream fis = new FileInputStream("d:\\file\\java74\\a.txt");
    
            // 2. 读:读一个字节
            // int read()   从文件中读取一个字节,返回值为字节的码表值
            int charPoint = fis.read();
            System.out.println("charPoint = " + ((char)charPoint));
    
            // 3. 关闭流,释放资源
            // void close() 关闭流释放资源
            fis.close();
        }
    }
  • 文件内容

    abcdefg
  • 控制台输出

    a

读多个字节

字节输入流:循环读取多个字节

  • 相关方法说明

| 方法签名 | 说明 |
| ---------------- | -------------------------------------------- |
| int read() | 从文件中读取一个字节,值为字节的编码值 |

  • 演示代码

    
    public class ByteStreamDemo03 {
        public static void main(String[] args) throws IOException {
            // 1. 创建流对象,关联文件
            // FileInputStream FileInputStream(String pathName)  基于一个字符串格式的文件路径封装一个FileInputStream对象
            FileInputStream fis = new FileInputStream("d:\\file\\java74\\a.txt");
    
            // 2. 读:读一个字节
            // int read()   从文件中读取一个字节,返回值为字节的码表值
            // read(fis);
            // 定义变量,接收每次读取到的码点
            int charPoint;
            // 循环读取码点,并和-1判断
            while ((charPoint = fis.read()) != -1) {
                // 如果不是-1,说明没有读取到文件末尾,就使用读取到的这个码点(eg:转换成字符使用)
                System.out.println(charPoint + " : " + ((char) charPoint));
            }
    
            // 3. 关闭流,释放资源
            // void close() 关闭流释放资源
            fis.close();
        }
    
        private static void read(FileInputStream fis) throws IOException {
            int charPoint = fis.read();
            System.out.println("charPoint = " + ((char) charPoint));
    
            charPoint = fis.read();
            System.out.println("charPoint = " + ((char) charPoint));
    
            charPoint = fis.read();
            System.out.println("charPoint = " + ((char) charPoint));
    
            charPoint = fis.read();
            System.out.println("charPoint = " + ((char) charPoint));
    
            charPoint = fis.read();
            System.out.println("charPoint = " + ((char) charPoint));
    
            charPoint = fis.read();
            System.out.println("charPoint = " + ((char) charPoint));
    
            charPoint = fis.read();
            System.out.println("charPoint = " + ((char) charPoint));
    
            charPoint = fis.read();
            System.out.println("charPoint = " + (charPoint));
        }
    }
  • 文件内容

    abcdefg
  • 控制台输出

    97 : a
    98 : b
    99 : c
    100 : d
    101 : e
    102 : f
    103 : g

读字节数组

字节输入流:读一个字节数组

  • 相关方法说明

| 方法签名 | 说明 |
| ---------------------------- | ------------------------------------------------------------------------------ |
| int read(byte[] bytes) | 从文件中读取一个字节数组的内容,并传入形参,返回读取到的字节数,否则为-1 |
| byte[] readAllBytes() | 读取文件中所有内容并转换为字节数组返回 |

  • 演示代码

    
    public class ByteStreamDemo04 {
        public static void main(String[] args) throws IOException {
            // 1. 创建流对象,关联文件
            // FileInputStream FileInputStream(String pathName)  基于一个字符串格式的文件路径封装一个FileInputStream对象
            FileInputStream fis = new FileInputStream("d:\\file\\java74\\a.txt");
    
            // 2. 读:读一个字节数组
            // read2Array(fis);
    
    
            //  byte[] readAllBytes()   读取文件中所有内容并转换为字节数组返回
            byte[] allBytes = fis.readAllBytes();
    
            String str = new String(allBytes);
            // allBytes.length = 7
            System.out.println("allBytes.length = " + allBytes.length);
            // str = abcdefg
            System.out.println("str = " + str);
    
            // 3. 关闭流,释放资源
            // void close() 关闭流释放资源
            fis.close();
        }
    
        /**
         * 读:读一个字节数组
         * @param fis
         * @throws IOException
         */
        private static void read2Array(FileInputStream fis) throws IOException {
            //  int read(byte[] bytes)  从文件中读取一个字节数组的内容,并传入形参,返回读取到的字节数,否则为-1
            byte[] bytes = new byte[1024];
            int len = fis.read(bytes);
            // 打印字节数组,看到大量的默认值0  bytes = [97, 98, 99, 100, 101, 102, 103, 0, 0, ...]
            // System.out.println("bytes = " + Arrays.toString(bytes));
    
            // 转换成字符串,会出现大量乱码  abcdefg后面有一大堆方块
            // String str = new String(bytes);
    
            // 不想出现乱码,就要控制转换的长度,只转换读取到的有效内容 str = abcdefg
            String str = new String(bytes,0,len);
            System.out.println("str = " + str);
        }
    }
  • 文件内容

    abcdefg
  • 控制台输出内容

注意:
  • read方法调用时,需要传递一个字节数组对象,用于接收读取到的多个字节
  • 使用字节数组中数据时,需要通过读取的长度来控制,否则可能造成数据出错。

循环读取多个字节数组

字节输入流-循环读取多个字节数组

方法和之前一样,配合while循环使用

  • 相关方法说明

| 方法签名 | 说明 |
| ---------------------------- | ------------------------------------------------------------------------------ |
| int read(byte[] bytes) | 从文件中读取一个字节数组的内容,并传入形参,返回读取到的字节数,否则为-1 |
| | |

  • 演示代码

    
    /**
     * 字节输入流:循环读取多个字节数组
     * @Description: 字节输入流:读一个字节
     */
    public class ByteStreamDemo05 {
        public static void main(String[] args) throws IOException {
            // 1. 创建流对象,关联文件
            // FileInputStream FileInputStream(String pathName)  基于一个字符串格式的文件路径封装一个FileInputStream对象
            FileInputStream fis = new FileInputStream("d:\\file\\java74\\a.txt");
    
            // 2. 读:读一个字节数组
    
            // read(fis);
            //  int read(byte[] bytes)  从文件中读取一个字节数组的内容,并传入形参,返回读取到的字节数,否则为-1
            byte[] bytes = new byte[2];
            int len;
            while ((len = fis.read(bytes)) != -1) {
                // 使用读取到的字节数组
                // 最后一次数据中,多了一个f
                // String str = new String(bytes);
                // 读取多少个字节,就转多少个字节,不会出现错误数据
                String str = new String(bytes,0,len);
                System.out.println("str = " + str);
            }
    
            // 3. 关闭流,释放资源
            // void close() 关闭流释放资源
            fis.close();
        }
    
        private static void read(FileInputStream fis) throws IOException {
            //  int read(byte[] bytes)  从文件中读取一个字节数组的内容,并传入形参,返回读取到的字节数,否则为-1
            byte[] bytes = new byte[1024];
            int len = fis.read(bytes);
    
            // 返回的是成功读取的字节数
            System.out.println("len = " + len);
    
    
            len = fis.read(bytes);
            // 返回的是成功读取的字节数
            System.out.println("len = " + len);
        }
    
    }
  • 核心代码

    //  int read(byte[] bytes)  从文件中读取一个字节数组的内容,并传入形参,返回读取到的字节数,否则为-1
    byte[] bytes = new byte[2];
    int len;
    while ((len = fis.read(bytes)) != -1) {
        // 使用读取到的字节数组
        // 最后一次数据中,多了一个f
        // String str = new String(bytes);
        // 读取多少个字节,就转多少个字节,不会出现错误数据
        String str = new String(bytes,0,len);
        System.out.println("str = " + str);
    }
  • 文件内容

    abcdefg
  • 控制台输出内容

    str = ab
    str = cd
    str = ef
    str = g
注意:
  • read方法调用时,需要传递一个字节数组对象,用于接收读取到的多个字节
  • 使用字节数组中数据时,需要通过读取的长度来控制,否则可能造成数据出错。

注意事项

  1. 创建字节输入流(FileInputStream)对象,会去关联一个文件并进行一些基础的操作。

    • 如果文件不存在,就报错;
  2. 读数据

    • read():读取的内容默认得到的是该数据的编码值,读取到文件末尾则返回-1
    • read(byte[] bytes):读取到多个字节并存入字节数组中,返回成功读取的字节个数;读取到文件末尾返回-1
      在使用返回的数组时,一定要注意使用读取到的有效字节个数,否则数据有误。
  3. 释放资源

    • Java程序操作文件时会独占关联的文件,每次使用完流必须要释放资源。否则其他程序将不能正确访问该文件。

读写案例:复制文件

  • 需求:
    把一个文件复制到另外一个位置:把D:\\file\\猜猜我是啥.mp4复制到其他位置。
  • 概念
    数据源:被读取的文件,复制数据的来源
    目的地:要写入的文件,复制数据的目的地
  • 演示代码

    
    
    /**
     * 使用字节流复制文件
     *
     * @Description: 使用字节流复制文件
     */
    public class ByteStreamDemo06 {
        public static void main(String[] args) throws IOException {
            // 1. 数据源:读,关联输入流。内存视角
            FileInputStream fis = new FileInputStream("D:\\file\\猜猜我是啥.mp4");
    
            // 2. 目的地:写,关联输出流。内存视角
            FileOutputStream fos = new FileOutputStream("D:\\file\\猜猜你是啥.mp4");
    
    
            // 3. 边读编写
            // 3.1 每次读写一个字节
    
            // operateByByte(fis, fos);
    
            // 3.2 每次读写一个字节数组
            operateByByteArray(fis, fos);
    
    
            // 4. 关闭流,释放资源(先开后关)
            fos.close();
            fis.close();
    
    
        }
    
        /**
         * 每次读写一个字节数组
         * @param fis
         * @param fos
         * @throws IOException
         */
        private static void operateByByteArray(FileInputStream fis, FileOutputStream fos) throws IOException {
            // 统计性能,开始时间
            long start = System.currentTimeMillis();
    
    
            // a. 定义一个字节数组,用于存储每次读取到的多个字节
            byte[] bytes = new byte[1024];
            // b. 定义一个int变量,用于记录每次读取到的字节数
            int len;
            // c. 循环读取并出入字节数组,知道返回的有效字节数为-1
            while ((len = fis.read(bytes)) != -1) {
                // d. 循环内部,不停的将读取到的数组写入文件,注意控制长度
                fos.write(bytes,0,len);
            }
    
            // 统计性能,结束时间
            long end = System.currentTimeMillis();
            // 自定义字节数组复制,共耗时:45ms
            System.out.println("自定义字节数组复制,共耗时:" + (end - start) + "ms");
        }
    
        /**
         * 每次读写一个字节
         *
         * @param fis
         * @param fos
         * @throws IOException
         */
        private static void operateByByte(FileInputStream fis, FileOutputStream fos) throws IOException {
            // 统计性能,开始时间
            long start = System.currentTimeMillis();
    
            // a. 定义一个变量,接收每次读取到的字节(的码点)
            int read;
            // b. 循环不断的读,直到读到文件结尾,返回-1
            while ((read = fis.read()) != -1) {
                // c. 每次读取到之后,就写入文件
                fos.write(read);
            }
            // 统计性能,结束时间
            long end = System.currentTimeMillis();
            // 逐个字节复制,共耗时:37946ms
            System.out.println("逐个字节复制,共耗时:" + (end - start) + "ms");
        }
    }

单字节拷贝慢的原因

  • 每次读取一个字节,需要大量频繁的让内存和硬盘交互,性能低下。

多字节拷贝速度提高的原因

  • 减少了硬盘和内存的交互次数,提高了性能。

释放资源

手动处理

思考:如何确保资源被正确释放

输出流操作文件期间如果产生了异常,异常之后的代码就不会继续执行,也就无法释放资源。

这个程序(生产环境程序不会关闭)就会独占关联的文件,造成其他程序无法访问该文件。

我们就要想办法让任何时候,资源都可以释放掉。

所以这个时候就需要用到finally

  • 演示代码

    
    public class ByteStreamDemo07 {
        public static void main(String[] args) {
            FileInputStream fis = null;
            FileOutputStream fos = null;
    
            try {
                System.out.println(1 / 0);
                // 读,输入流 从硬盘到内存
                fis = new FileInputStream("d:\\file\\猜猜我是啥.mp4");
    
    
                // 写,输出流, 从内存到硬盘
                fos = new FileOutputStream("d:\\file\\猜猜我是啥2.mp4");
    
    
                // 定义变量记录每次读取到的字节个数
                int len;
                // 创建数组缓存读取到的数据
                byte[] bytes = new byte[1024];
                // 边读边写
                while ((len = fis.read(bytes)) != -1) {
                    fos.write(bytes, 0, len);
                }
            } catch (IOException e) {
                e.printStackTrace();
                // 处理异常。常见的操作:记录日志,调用第三方接口发邮件、发短信
            } finally {
                // 关闭流,释放资源
                if (fos != null) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
    
                }
                if (fis != null) {
                    try {
                        fis.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("流真的关闭了,舒了一口气!!");
            }
        }
    }
问题:释放资源的代码比核心代码还多还复杂!!!

自动释放

JDK7中提供了语法糖,简化资源的释放。可以理解为对try代码块的增强。

语法糖名称:try-with-resource

格式:

try(开启资源){
    // 可能出现异常的代码
}catch(异常类型 异常变量){
    // 处理异常
}

作用:

  • 在try小括号中开启的资源,会在try代码块结束后自动释放
  • 只要是实现了Closeable接口的类,都可以称之为资源,都可以被Try-With-Resource管理并自动关闭
  • 演示代码

    
    
    /**
     * JDK7新特性演示:自动释放资源
     *
     */
    public class TryWithResource {
    
    
        public static void main(String[] args) {
    
            /*
                JDK7提供的新特性:
                Try-With-Resource:可以理解为对Try{}catch(){}finally{}的简化
    
                格式:
                    try(开启资源){}catch(){处理异常}
    
                效果:
                    在try小括号中开启的资源,会在try代码块结束后自动释放
    
                资源:
                    只要是实现了AutoCloseable接口的类,都可以称之为资源,都可以被Try-With-Resource管理并自动关闭
    
             */
    
            // beforeJdk7();
    
            // jdk7_01();
            // jdk7_02();
    
    
        }
    
        private static void jdk7_02() {
            // 在try小括号中开启的资源,try代码块中可以使用,并且会在try代码块执行完成后自动关闭释放
            // 多个资源
            try (
                    FileInputStream fis = new FileInputStream("d:\\a.mp4");
                    FileOutputStream fos = new FileOutputStream("d:\\a.mp4");
            ) {
    
                int read = fis.read();
                System.out.println("(char)read = " + (char) read);
                fos.write(read);
    
                // Try代码块中,如果出现异常,会被catch字句捕获,并在字句中处理
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        private static void jdk7_01() {
            // 在try小括号中开启的资源,try代码块中可以使用,并且会在try代码块执行完成后自动关闭释放
            // 单个资源
            try (FileInputStream fis = new FileInputStream("d:\\a.mp4")) {
    
                int read = fis.read();
                System.out.println("(char)read = " + (char) read);
    
                // Try代码块中,如果出现异常,会被catch字句捕获,并在字句中处理
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        private static void beforeJdk7() {
            FileInputStream fis = null;
            try {
                fis = new FileInputStream("d:\\a.mp4");
                int read = fis.read();
                System.out.println("read = " + (char) read);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (fis != null) {
                    try {
                        fis.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
最后修改:2023 年 05 月 15 日
如果觉得我的文章对你有用,请随意赞赏