【Java I/O 流】4 - 4 字符流的文件输入输出流

§4-4 字符流的文件输入输出流

4-4.1 字符流

在上一节我们讲到,字节流在读取文件时,会逐个字节读取,这种读取方式会使得在读取多字节所表示的字符时发生乱码的现象。此外,即使能够调用重载的 read 方法读取多字节,但是由于方法实现的特性,想要将其中的字节数据转换为对应字符集的内容,往往会遇到许多困难(可能发生数组内容覆盖导致旧数据可能未完全读取的问题)。这是我们不愿意看到的,因此,我们往往会使用字符流来读取纯文本文件。

字符流实际上就是字节流 + 字符集。它会根据指定的字符集,在读写字符时适当地选取读写长度,适用于纯文本文件的读写操作。

字符流主要有两个抽象类 ReaderWriter,二者的子类有很多,但它们的命名都体现了它们的作用和继承结构,如下图所示:

image

4-4.2 FileReader 文件阅读器

FileReader 位于 java.io,是 Reader 的一个子类,用于读取纯文本文件。FileReader 使用平台默认的字符集或指定字符集将字节解码为字符。

FileReader 目的是读取字符流,若要读取原生字节流,考虑使用 FileInputStream

书写步骤

  1. 创建文件阅读器对象;
    • 构造器具有多种重载,可传入表示指定文件路径的字符串,也可以是一个对应的 File 对象;
    • 若文件不存在,则会抛出异常 FileNotFoundException
  2. 读取文件内容;
    • 使用 read 方法读取文件中的数据,方法返回一个整型(int)数据;
    • 方法会自动根据对象所指定的字符集正确读取一个或多个字节;
    • read 方法具有多种重载;
    • 可以多次调用该方法,不断读取文件中剩余的数据,若无更多内容可读,则返回 -1
  3. 释放资源;
    • 使用 close 方法关闭流,解除 Java 程序对文件的占用;
    • 应当在每次使用完流后释放资源,解除占用;

三个方法都会抛出编译时异常,应当及时处理。

构造方法

构造方法 描述
FileReader(File file) 使用平台默认字符集,创建一个关联指定文件的文件阅读器对象
FileReader(File file, Charset charset) 使用指定字符集,创建一个关联指定文件的文件阅读器对象
FileReader(String fileName) 适用平台指定字符集,创建一个关联指定文件的文件阅读器对象
FileReader(String fileName, Charset charset) 使用指定字符集,创建一个关联指定文件的的文件阅读器对象

其中,Charset 是一个位于 java.nio.charset 的抽象类,在传入参数时,可以传入位于相同位置的 StandardCharsets 中所定义的 Charset 实现类对象。

StandardCharsets 中所定义的枚举字符集常量有:

字段 描述
ISO_8859_1 ISO 拉丁字母表 No.1,又称 ISO-LATIN-1
US_ASCII 七位 ASCII,又称 ISO646-US、Unicode 字符集的基本拉丁块
UTF_16 十六位 UCS 转换格式,其字节顺序由一个可选的字节顺序标记来标识
UTF_16BE 十六位 UCS 转换格式,大端字节顺序
UTF_16LE 十六位 UCS 转换格式,小端字节顺序
UTF-8 八位 UCS 转换格式

读取文件

方法 描述
int read() 读取一个字符
int read(char[] cbuf) 将字符读取到一个数组中,返回读取的字符数
int read(char[] cbuf, int off, int len) 将字符读取到一个数组的一部分中,返回读取的字符数
  • 方法会尝试尽可能多地读取字符;
  • 空参方法会返回该字符的十进制编码,可强制转型为 char 获取对应字符;
  • 若已经来到了流的末端,无更多数据可读时,方法返回 -1

4-4.3 FileWriter 文件写入器

FileWriter 位于 java.io,是 Writer 的一个子类,用于写出一个纯文本文件。FileWriter 使用平台默认字符集或指定字符集写出文件。

FileWriter 目的是写出字符流,若要写出原生字节流,考虑使用 FileOutputStream

书写步骤

  1. 创建文件写入器对象;
    • 构造器具有多种重载,可传入表示指定文件路径的字符串,也可以是一个对应的 File 对象;
    • 必须保证父级路径存在,否则抛出异常 FileNotFoundException
    • 若文件不存在(父级路径必须存在),则会新建该文件;
    • 方法的最后一个参数 boolean append 用于控制续写(追加)行为,默认为 false,则会在创建对象时清空文件;
    • boolean appendtrue,则启动追加,在文件末尾写入新数据;
  2. 写出数据;
    • 使用 write 方法向文件写出数据;
    • write 方法具有多种重载;
    • 可多次调用该方法,不断地向文件写出数据;
  3. 释放资源;
    • 使用 close 方法关闭流,解除 Java 程序对文件的占用;
    • 应当在每次使用完流后释放资源,解除占用;

三个方法都会抛出编译时异常,应当及时处理。

写出数据的方式

方法 描述
void write(int c) 写出单字符
void write(char[] cbuf) 写出一个字符数组
void write(String str) 写出一个字符串
void write(char[] cbuf, int off, int len) 写出一个字符数组的一部分
void write(String str, int off, int len) 写出一个字符串的一部分
  • 若传入参数为整数,则实际输出的为其对应编码的字符,
  • 不同于字节输出流,受到字节的限制,部分字符编码可能会因为超出字节上限发生溢出,进而导致乱码;

4-4.4 底层原理

FileReader:对象创建成功后,对象内部会创建一个长度为 8192 的字节型数组,用作缓冲区。每次调用 read 方法,会先判断缓冲区中是否有数据,若没有,则读取文件中一部分数据,并放入缓冲区中;若有,则读取缓冲区中的数据。

若缓冲区还存在尚未读取完的数据,则继续读取缓冲区中的内容,否则,则继续从文件中读取下一部分数据,并放入缓冲区中读取。读到文件末尾,则返回 -1

空参 read 一次读取一个字符,遇到多字节字符则读取多个字节,将字符解码为十进制编码后返回。

有参 read 一次读取多个字符,尽可能多地读取数据,并将解码所的字符存放到数组中,并返回读取的字符个数。

设置缓冲区的目的在于,不同于字节流的逐个字节读取,字符流每次读取是逐个字符读取,而不同语言的字符可能由多个字节表示,逐个字节读取并不合适。为了提高效率,就设置了长度适中的缓冲区(在内存中),尽可能地减少上下文切换,提高效率。

FileWriter:对象创建成功后,对象内部也会创建一个长度为 8192 的字节型数组,用作缓冲区。每次调用 write 方法,都是往缓冲区中写入数据,而并非立刻写进文件中。当且仅当缓冲区已满,或调用 flush 方法手动刷新,或关闭流时,缓冲区中的数据才会被写进文件中。

值得注意的是,调用了 flush 后,仍能够继续调用 write 方法,因为此时流尚未关闭。但一旦关闭了流,就不能够再调用 write 写数据了,否则会抛出异常 IOException

缓冲区:缓冲区是字符流的一个特点,这一特点在字节流中并不存在。

4-4.5 案例演示

需求一:复制文件夹(含子目录)

public static void copy(File src, File dest) throws IOException {
    Objects.requireNonNull(src);
    Objects.requireNonNull(dest);

    //判断源文件
    if (!src.exists()) {
        System.err.println("错误:源文件不存在");
        return;
    }

    //判断目标路径
    if (dest.isFile()) {
        //目标路径是文件,舍弃
        System.err.println("错误:目标路径是文件,应当为目录。");
        return;
    } else {
        //无论是否存在,都先创建目录
        dest.mkdirs();
    }

    //开始复制:深度优先
    File[] files = src.listFiles(); //获取源文件夹下的子文件(夹)

    //若为空:不存在(排除)、为文件、需要权限(暂不考虑)
    if (files == null) {
        //复制单文件
        copyFile(src, dest);
    } else {
        //复制文件夹
        //创建文件夹(自己)
        File self = new File(dest, src.getName());
        self.mkdirs();

        //遍历文件夹,将内容复制到目标路径的 self
        for (File file : files) {
            if (file.isFile()) {
                //判断为文件
                copyFile(file, self);
            } else {
                //判断为文件夹
                //深度优先
                copy(file, self);
            }
        }
    }
}

private static void copyFile(File src, File dest) throws IOException {
    Objects.requireNonNull(src);
    Objects.requireNonNull(dest);

    //判断存在性
    if (!src.exists() || !dest.exists()) {
        System.err.println("错误:源文件或目标路径不存在");
        return;
    }
    //判断源和目标属性
    if (src.isDirectory() || dest.isFile()) {
        System.err.println("错误:源为目录或目标为文件");
        return;
    }

    //链接文件
    FileInputStream fis = new FileInputStream(src); //链接源文件
    FileOutputStream fos = new FileOutputStream(new File(dest, src.getName())); //链接输出文件

    //边读边写
    byte[] bytes = new byte[1 << 22];   //4 MB
    int len;
    while ((len = fis.read(bytes)) != -1) {
        fos.write(bytes, 0, len);
    }

    //关闭流
    fos.close();
    fis.close();
}

需求二:文件加密与解密

public static void encrypt(File src) throws IOException {
    Objects.requireNonNull(src);

    if (!src.exists()) {
        System.err.println("错误:源文件不存在");
        return;
    }
    if (src.isDirectory()) {
        System.err.println("错误:不支持目录加密解密");
        return;
    }

    //开始加密
    FileInputStream fis = new FileInputStream(src);
    FileOutputStream fos = new FileOutputStream(new File(src.getParent(), "Processed.txt"));

    //读写
    byte[] bytes = new byte[1 << 22];   //4 MB
    int len;
    while ((len = fis.read(bytes)) != -1) {
        //使用异或运算,再次进行异或可解密
        for (int i = 0; i < len; i++) {
            bytes[i] ^= 100;
        }
        fos.write(bytes, 0, len);
    }

    //关闭流
    fos.close();
    fis.close();
}

需求三:对文件中的数据进行排序:2-1-9-4-7-8,要求格式不变

public static void sortData(File src) throws IOException {
    Objects.requireNonNull(src);

    if (!src.exists()) {
        System.err.println("错误:文件不存在");
        return;
    }
    if (src.isDirectory()) {
        System.err.println("错误:不支持文件夹");
        return;
    }

    //读取
    ArrayList<Integer> data = null;
    FileReader fr = new FileReader(src);
    StringBuilder sb = new StringBuilder();
    int ch;
    while ((ch = fr.read()) != -1) {
        sb.append((char) ch);
    }
    fr.close();

    //使用流解决问题
    data = (ArrayList<Integer>) Arrays.stream(sb.toString().split("-"))
        .map(Integer::parseInt)
        .sorted(Integer::compare)
        .collect(Collectors.toList());

    //写入
    //使用String类成员方法
    FileWriter fw = new FileWriter(src);
    String res = data.toString().replace(", ", "-");
    res = res.substring(1, res.length() - 1);
    fw.write(res);
    fw.close();
}
posted @ 2023-08-18 23:18  Zebt  阅读(59)  评论(0)    收藏  举报