JavaSE:I/O 流


前言

  1. 字节流和字符流的操作相同,只是操作的数据类型不同。
  2. 节点流和过滤流的关系,是 的体现。
  3. 转换流,是 的体现。

1、流

内存与存储设备之间传输数据的通道

分类

  1. 按方向(以内存为基准)
    • 输入流:将存储设备中的内容读到内存中。
    • 输出流:将内存中的内容写到存储设备中。
  2. 按单位
    • 字节流:以字节为单位,可以读写所有数据。
    • 字符流:以字符为单位,只能读写文本数据。
  3. 按功能
    • 节点流:具有实际传输数据的读写功能。
    • 过滤流:在节点流的基础之上增强功能()

2、字节流

2.1、抽象类

2.1.1、InputStream

字节输入流

image-20220317235716205
  • read():从输入流读取数据的下一个字节,返回下一个字节
  • read(byte[]):从输入流读取数据的一些字节,存储到缓冲区,返回读取字节数
  • read(byte[], off, len):从输入流读取数据 len 个字节,从偏移 off 开始读取,存储到缓冲区,返回读取字节数
  • close():关闭输入流,释放资源

2.1.1、OutputStream

字节输出流

image-20220317235732262
  • write(int):将指定的字节(ASCII 码)写入输出流。
  • write(byte[]):将指定的字节数组写入输出流。
  • write(byte[], off, len):将指定的字节数组的 len 个字节,从偏移 off 开始写入到输出流。
  • flush():刷新输出流,并将任何缓冲的字节写出。
  • close():先调用 flush(),后关闭输出流,释放资源。

write() 仅将数据写入流中,flush() 才将数据刷新到磁盘文件中

2.2、节点流

直接读取磁盘文件

2.2.1、文件输入流

FileInputStream

  1. 创建流:参数为文件名

  2. 创建缓冲区:减少磁盘 I/O,提高效率

  3. 读取数据

  4. 关闭流

    public void readFile(String fileName) throws IOException {
        // 1、创建流
        FileInputStream fis = new FileInputStream(fileName);
        // 2、缓冲区
        byte[] buf = new byte[1024];
        int len = 0;
        // 3、读取
        while ((len = fis.read(buf)) != -1) {
            System.out.println(new String(buf, 0, len));
        }
        // 4、关闭流
        fis.close();
    }
    

2.2.2、文件输出流

使用

  1. 创建流:参数 1 为文件名,参数 2 表示是否追加(true 表示追加,false 表示覆盖文件)

  2. 写数据

  3. 刷新到磁盘

  4. 关闭流

    public void writeFile(String location) throws IOException {
        // 1、创建流
        FileOutputStream fos = new FileOutputStream(location, true);
        // 2、写
        String str = "Hello, I/O world!";
        fos.write(str.getBytes());
        // 3、刷新
        fos.flush();
        // 4、关闭流
        fos.close();
    }
    

2.2.3、case:文件复制

通过文件字节流实现文件复制

思路:通过输入流读取文件数据,通过输出流写数据。

  1. 创建流

  2. 创建缓冲区:减少磁盘 I/O,提高效率

  3. 读写数据

  4. 关闭流:先开后关

    public void copyFile(String source, String dest) throws IOException {
        // 1、创建流
        FileInputStream fis = new FileInputStream(source);
        FileOutputStream fos = new FileOutputStream(dest);
        // 2、缓冲区
        byte[] buf = new byte[1024];
        int len = 0;
        // 3、读写
        while ((len = fis.read(buf)) != -1) {
            fos.write(buf, 0, len);
        }
        // 4、关闭流:先开后关
        fos.close();
        fis.close();
    }
    

2.3、缓冲流

属于过滤流

在节点流的基础上使用缓冲区。

  • 优点:提高 IO 效率,减少访问磁盘次数。
  • 原理
    1. 使用节点流读(写)数据到过滤流的缓冲区中。
    2. 调用方实际读(写)的是缓冲区中的数据,而不是直接读(写)磁盘。
  • 开启和关闭
    • 创建过滤流时,需要传入节点流。
    • 关闭过滤流时,会自动关闭节点流()

2.3.1、缓冲输入流

BufferedInputStream

public void readFile(String filename) throws IOException {
    // 1、创建流
    FileInputStream fis = new FileInputStream(filename);
    BufferedInputStream bis = new BufferedInputStream(fis);
    // 2、缓冲区
    byte[] buf = new byte[1024];
    int len = 0;
    // 3、读取
    while ((len = bis.read(buf)) != -1) {
        System.out.print(new String(buf, 0, len));
    }
    // 3、关闭流
    bis.close();
}

说明:上面使用的缓冲区(称为 b,1K),与过滤流内置的缓冲区(称为 B,默认 8K)不是同一个概念。

在调用 bis.read(buf) 时,实际情况如下

  1. 过滤流内部使用节点流,将一定的字节数读到 8K 缓冲区 B 中。
  2. 读取缓冲区中一定数量的字节数,存储到 1K 缓冲区 b 中。

2.3.2、缓冲输出流

BufferedOutputStream

public void writeFile(String location) throws IOException {
    // 1、创建流
    FileOutputStream fos = new FileOutputStream(location);
    BufferedOutputStream bos = new BufferedOutputStream(fos);
    // 2、写
    String str = "Hello, buffer I/O";
    for (int i = 0; i < 10; i++) {
        bos.write(str.getBytes());
        // 3、刷新
        bos.flush();
    }
    // 4、关闭流
    bos.close();
}

注意

  1. bos.write(buf):过滤流内部使用节点流,将一定的字节数写到 8K 缓冲区 B 中。

    public void write(byte b[]) throws IOException {
        write(b, 0, b.length);
    }
    
    public void write(byte b[], int off, int len) throws IOException {
        // 判断下标越界
        for (int i = 0 ; i < len ; i++) {
            write(b[off + i]);
        }
    }
    
    public void write(int b) throws IOException {
        out.write(b);
    }
    
  2. bos.flush():过滤流内部使用节点流,将缓冲区中的数据写出

    public synchronized void flush() throws IOException {
        flushBuffer();
        out.flush();
    }
    
    private void flushBuffer() throws IOException {
        if (count > 0) {
            out.write(buf, 0, count);
            count = 0;
        }
    }
    
  3. bos.close():关闭连接之前,会调用 flush() 刷新输出流。

    public void close() throws IOException {
        try (OutputStream ostream = out) {
            flush();
        }
    }
    

2.3.3、case:文件复制

  1. 创建流:节点流、过滤流

  2. 缓冲区

  3. 读写数据

  4. 关闭连接

    public void copyFile(String source, String dest) throws IOException {
        // 1、 创建流
        FileInputStream fis = new FileInputStream(source);
        FileOutputStream fos = new FileOutputStream(dest);
        BufferedInputStream bis = new BufferedInputStream(fis);
        BufferedOutputStream bos = new BufferedOutputStream(fos);
        // 2、缓冲区
        byte[] buf = new byte[1024];
        int len = 0;
        // 3、读写
        while ((len = bis.read(buf)) != -1) {
            bos.write(buf, 0, len);
            bos.flush();
        }
        // 4、关闭连接
        bos.close();
        bis.close();
    }
    

2.4、对象流(!)

对象流是字节流的子类之一

属于过滤流,增强了以下功能

  1. 缓冲区
  2. 读写基本类型和字符串
  3. 读写对象
  • 序列化:向流中写入一个对象
  • 反序列化:从流中读取一个对象

2.4.1、Serializable 接口

对象序列化,必须实现 Serializable 接口。

  1. Serializable 接口:标记接口。

  2. 序列化版本 ID:反序列化时使用,比较二进制文件的 ID 与 Class 文件得到 ID,判断是否为同一个类。

  3. 属性

    • 序列化类的属性页必须实现 Serializable 接口。
    • transient 关键字:标明该属性不被序列化。
    • 静态属性不被序列化:因为静态属性在类加载时分配在方法区中,而非堆中。
  4. 序列化多个对象,可以借助集合来实现

    // 示例
    public class Person implements Serializable {
        private static final long serialVersionUID = 4620885805607038048L;
    
        private String name;
        private transient int age;
    
       	// 构造器、toString()等
    }
    

2.4.1、序列化

ObjectOutputStream

public void write(String location) throws IOException {
    // 1、创建流
    FileOutputStream fos = new FileOutputStream(location);
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    // 2、序列化对象
    Person p1 = new Person("Jaywee", 17);
    oos.writeObject(p1);
    oos.flush();
    // 3、关闭流
    oos.close();
}

2.4.2、反序列化

ObjectInputStream

public Person read(String name) throws IOException, ClassNotFoundException {
    // 1、创建流
    FileInputStream fis = new FileInputStream(name);
    ObjectInputStream ois = new ObjectInputStream(fis);
    // 2、读
    Person p = (Person) ois.readObject();
    // 3、关闭
    ois.close();

    return p;
}

3、字符流

注意:操作和字节流基本相同,但是操作的数据对象不同。

3.0、编码问题

字符编码方式

当编码格式与解码格式不一致时,会出现乱码。

编码字节 说明
UTF-8 可变长度编码 根据字符类型,单、双、三、四字节编码
ASCII 单字节编码 用 1 个字节表示英文字符,共 128 个
ISO-8859-1 单字节编码 共 256 个字符
GBK 单、双字节编码 能表示 21003 个汉字

字节流是以字节为单位读取数据,而有些字符是采用多字节编码的方式。

如果使用字节流,对数据的逐个字节进行读取,就会导致乱码。

  • :逐个读取字节

    public static void read(String name) throws IOException {
        // 1、创建流
        FileInputStream fis = new FileInputStream(name);
        // 2、逐个字节读取
        int data = 0;
        // fis.read()返回数据的下一个字节
        while ((data = fis.read()) != -1) {
            System.out.print((char) data);
        }
        // 3、关闭
        fis.close();
    }
    
  • 测试:输出乱码

    image-20220318161957150

解决方案

  1. 调用 read(byte[]) 重载方法,将数据批量读取到缓冲区中。
  2. 使用字符流

3.1、抽象类

3.1.1、Reader

字符输入流

image-20220318163024746
  • read():从输入流读取数据的下一个字符,返回下一个字符
  • read(char[]):从输入流读取数据的一些字符,存储到缓冲区,返回读取字符数
  • read(char[], off, len):从输入流读取数据 len 个字符,从偏移 off 开始读取,存储到缓冲区,返回读取字符数
  • close():关闭输入流,释放资源

3.1.2、Writer

字符输出流

  • write(int):将指定字符写入输出流。
  • write(byte[]):将指定的字符数组写入输出流。
  • write(byte[], off, len):将指定的字符数组的 len 个字节,从偏移 off 开始写入到输出流。
  • flush():刷新输出流,并将任何缓冲的字符写出。
  • close():先调用 flush(),后关闭输出流,释放资源。

write() 仅将数据写入流中,flush() 才将数据刷新到磁盘文件中

3.2、节点流

3.2.1、读文件

FileReader

  • 逐个字符读取并打印,不会报错。

  • 也可以调用 read(char[]) 重载方法,通过缓冲区读取。

    public void readFile(String fileName) throws IOException {
        // 1、创建流
        FileReader fr = new FileReader(fileName);
        // 2、逐个字符读取
        int data = 0;
        // fr.read()返回数据的下一个字符
        while ((data = fr.read()) != -1) {
            System.out.print((char) data);
        }
        // 3、关闭
        fr.close();
    }
    

3.2.2、写文件

FileWriter

public void writeFile(String location) throws IOException {
    // 1、创建流
    FileWriter fw = new FileWriter(location);
    // 2、写
    String str = "Hello, I/O world!";
    fw.write(str);
    // 3、刷新
    fw.flush();
    // 4、关闭流
    fw.close();
}

3.2.3、case:文件复制

与使用字节流进行文件复制的区别

  1. 使用的流不同(字节流、字符流)

  2. 缓冲区类型不同(byte、char)

  3. 文件类型不同

    • 字节流可以复制任意类型。

    • 字符流只能复制文本文件,不能复制图片等二进制文件。

      public void copyFile(String source, String dest) throws IOException {
          // 1、创建流
          FileReader fr = new FileReader(source);
          FileWriter fw = new FileWriter(dest);
          // 2、缓冲区
          char[] buf = new char[1024];
          int len = 0;
          // 3、读取
          while ((len = fr.read(buf)) != -1) {
              fw.write(buf, 0, len);
          }
          // 4、关闭流:先开后关
          fw.close();
          fr.close();
      }
      

3.3、缓冲流

缓冲流属于过滤流

相比字节缓冲流的不同点

  1. 支持输入换行符
    • 使用字节流需要手动输入换行符:Linux 是 \n,Windows 是 \r\n\
    • 字符缓冲流提供 newLine() 方法,根据操作系统自动写入换行符。
  2. 支持读一行:提供 readLine() 方法。

3.3.1、缓冲读

BufferedReader

演示逐行读取的方式

public void read(String name) throws IOException {
    // 1、创建流
    FileReader fr = new FileReader(name);
    BufferedReader br = new BufferedReader(fr);
    // 2、逐行读取
    String line = null;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
    // 3、关闭
    br.close();
}

3.3.1、缓冲写

BufferedWriter

public void write(String location) throws IOException {
    // 1、创建流
    FileWriter fw = new FileWriter(location);
    BufferedWriter bw = new BufferedWriter(fw);
    // 2、写
    String str = "Hello, buffer I/O";
    for (int i = 0; i < 10; i++) {
        bw.write(str);
        // 换行符
        bw.newLine();
        bw.flush();
    }
    // 3、关闭
    bw.close();
}

3.4、打印流

3.4.1、说明

打印流是 Writer 的子类之一

属于过滤流,增强了以下功能

  1. 封装 print()println(),支持写入后换行

    • print() 实际上还是调用 write()
    • println() 相当于 print() + 换行
  2. 数据原样打印:如不会将数字转换为字符。

    // print()部分重载方法
    public void print(boolean b) { write(b ? "true" : "false"); }
    public void print(char c) { write(c); }
    public void print(int i) { write(String.valueOf(i)); }
    
    // println(String)
    public void println(String x) {
        synchronized (lock) {
            print(x);
            println();
        }
    }
    

3.4.2、使用

public void write(String location) throws IOException {
    // 1、创建流
    FileWriter fw = new FileWriter(location);
    PrintWriter pw = new PrintWriter(fw);
    // 2、打印
    pw.print(97);
    pw.print(7.2);
    pw.print('c');
    pw.println("Jaywee");
    pw.print(true);
    // 3、刷新
    pw.flush();
    // 4、关闭
    pw.close();
}

3.5、转换流

作用:将字节流转换为字符流,可显式设置或使用默认字符编码(解码)方式。

的体现

  • 适配器类:InputStreamReader、InputStreamWriter
  • 适配者:字符流,Reader、Writer
  • 被适配者:字节流,InputStream、InputStream

3.5.1、读

InputStreamReader

  1. 将字节输入流转换为字符输入流

  2. 可显式设置解码方式(Decoder)

    public void read(String name) throws IOException {
        // 1、创建流
        FileInputStream fis = new FileInputStream(name);
        // 可指定解码方式:CharsetDecoder
        InputStreamReader reader = new InputStreamReader(fis);
        // 2、读
        int data = 0;
        while ((data = reader.read()) != -1) {
            System.out.print((char) data);
        }
        // 3、关闭
        reader.close();
    }
    

3.5.2、写

OutputStreamWriter

  1. 将字节输出流转换为字符输出流

  2. 可显式设置解码方式(Encoder)

    public void write(String location) throws IOException {
        // 1、创建流
        FileOutputStream fos = new FileOutputStream(location);
        // 可指定解码方式:CharsetEncoder
        OutputStreamWriter writer = new OutputStreamWriter(fos);
        // 2、写
        String str = "Hello, the bridge from byte streams to character stream";
        writer.write(str);
        writer.flush();
        // 3、关闭
        writer.close();
    }
    

4、File

使用 I/O 流实现对磁盘文件的读写操作,针对的是文件内容。

File:代表磁盘中的一个文件或文件夹,提供操作文件本身的方法。

4.1、常用属性和方法

4.1.1、属性

  • 路径分隔符pathSeparatorpathSeparatorChar;

  • 名称分隔符SeparatorSeparatorChar\

  • 抽象路径path,即创建文件时指定的路径

    // 抽象路径为myFile.txt
    File file1 = new File("myFile.txt");
    // 抽象路径为F:\myFile.txt
    File file2 = new File("F:\\myFile.txt");
    

4.1.2、创建、删除

:使用 new 关键字创建的 File 实例,仅在堆内存中存在,磁盘中未必有对应路径的文件。

  1. 创建文件createNewFile(),当指定路径的文件不存在时创建。
  2. 创建文件夹
    • mkdir():单级目录
    • mkdirs():级联创建多级目录
  3. 删除文件(目录):若为多级目录则只删除最内层目录,当目录为空时才删除
    • delete():直接删除
    • deleteOnExit:虚拟机终止时删除

4.1.3、文件信息

  1. getPath():抽象路径
  2. getAbsolutePath:绝对路径
  3. getName():文件(目录)名
  4. getParent():文件所在目录(父级目录)
  5. getLenth():文件长度
  6. lastModified():文件最后修改时间

4.1.4、判断

  1. 类型
    • isFile():是否为文件
    • isDirectory:是否为目录
  2. 存在exists()
  3. 隐藏isHidden()

4.2、文件夹操作

文件夹主要涉及 2 个操作

  1. 列出内容:包括文件和子目录
  2. 遍历

4.2.1、列出内容

File 类提供了两个方法,用于列出目录中的所有内容

  • list():返回一个字符串数组,包含文件夹中所有文件和子目录的文件名。
  • listFiles():返回一个文件数组,包含文件夹中的所有文件和子目录的对应 File 对象。

4.2.2、文件过滤器

File 类的 list() 和 listFiles() 可以搭配文件过滤器,定义文件筛选规则

  • FileNameFilter(JDK 1.0)

    • dir:使用该过滤器的文件夹对象
    • name:文件夹中的文件(目录)名
  • FileFilter(JDK 1.2)

    • pathname:文件夹中的文件(目录)
    @FunctionalInterface
    public interface FilenameFilter {
        boolean accept(File dir, String name);
    }
    
    @FunctionalInterface
    public interface FileFilter {
        boolean accept(File pathname);
    }
    

测试

以 listFiles() 和 FileFilter 为例,列出 dir 中的 .jpg 文件名。

// 测试
@Test
public void testFilter() {
    File dir = new File("目录路径");
    
    File[] files = dir.listFiles(new FileFilter() {
        @Override
        public boolean accept(File pathname) {
            return pathname.getName().endsWith(".jpg");
        }
    });

    for (File file : files) {
        System.out.println(file);
    }
}

匿名内部类简化写法:lambda表达式

// 简化
files = dir.listFiles((pathName) -> {
    return pathName.getName().endsWith(".jpg");
});
// 更简化
files = dir.listFiles((pathName) -> pathName.getName().endsWith(".jpg"));

4.2.3、递归

思路:遍历当前文件夹,若为文件夹则递归调用,否则执行相应动作。

递归遍历

public void recursiveList(File dir) {
    // 当前目录路径
    System.out.println(dir.getAbsoluteFile());

    File[] files = dir.listFiles();

    if (files != null && files.length > 0) {
        // 遍历
        for (File file : files) {
            if (file.isDirectory()) {
                // !递归!
                recursiveList(file);
            } else {
                System.out.println(file.getAbsolutePath());
            }
        }
    }
}

递归删除

注意:递归删除子文件夹内容后,删除子文件夹本身。

public void recursiveDelete(File dir) {
    // 当前目录路径
    System.out.println("===== " + dir.getAbsoluteFile() + " =====");

    File[] files = dir.listFiles();

    if (files != null && files.length > 0) {
        // 遍历
        for (File file : files) {
            if (file.isDirectory()) {
                // !递归!
                recursiveDelete(file);
                // 删除子文件夹本身
                file.delete();
            } else {
                System.out.println(file.getAbsolutePath() + " was deleted? " + file.delete());
            }
        }
    }
}

5、小结

5.1、流

:内存与存储设备之间传输数据的通道。

回顾前言

  • 字节流和字符流的操作相同,只是操作的数据类型不同。
  • 节点流和过滤流的关系,是 的体现。
  • 转换流,是 的体现。

分类

  • 方向:输入流、输出流
  • 单位:字节流、字符流
  • 功能:节点流、过滤流

5.2、字节流

Inputstream、OutputStream

  • 节点流:FileInputstream、FileOutputStream
  • 过滤流
    • 缓冲流:BufferedInputStream、BufferedOutputStream
    • 对象流:ObjectInputStream、ObjectOutputStream
      • Serializable 接口、transient
      • 序列化与反序列化

5.3、字符流

Reader、Writer

  • 编码问题
  • 节点流:FileReader、FileWriter
  • 过滤流
    • 缓冲流:BufferedFileReader、BufferedFileWriter
    • 打印流:PrintWriter
  • 转换流:InputStreamReader、InputStreamWriter()

5.4、File

  • 使用 I/O 流实现对磁盘文件的读写操作,针对的是文件内容。
  • 使用 File 操作文件本身。
  1. 常用属性和方法
    • 分隔符
    • 创建、删除
    • 文件信息:路径、名称、父级目录、长度、最后修改时间
    • 判断:类型、存在、隐藏
  2. 文件夹操作
    • 列出内容、Filter 筛选
    • 递归

总结 I/O 两句话

  1. 流的使用方式基本相同,一通百通。
  2. 结合设计模式学习 I/O,融会贯通。