Java基础——IO

182 阅读19分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

一、概念

IO是指Input/Output,即输入和输出。以内存为中心:

  • Input指从外部读入数据到内存,例如,把文件从磁盘读取到内存,从网络读取数据到内存等等。
  • Output指把数据从内存输出到外部,例如,把数据从内存写入到文件,把数据从内存输出到网络等等。

InputStream代表输入字节流,OuputStream代表输出字节流,这是最基本的两种IO流。

Reader / Writer

如果我们需要读写的是字符,并且字符不全是单字节表示的ASCII字符,那么,按照char来读写显然更方便,这种流称为字符流

Java提供了ReaderWriter表示字符流,字符流传输的最小数据单位是char

例如,我们把char[]数组Hi你好这4个字符用Writer字符流写入文件,并且使用UTF-8编码,得到的最终文件内容是8个字节,英文字符Hi各占一个字节,中文字符你好各占3个字节:

 0x48
 0x69
 0xe4bda0
 0xe5a5bd

反过来,我们用Reader读取以UTF-8编码的这8个字节,会从Reader中得到Hi你好这4个字符。

因此,ReaderWriter本质上是一个能自动编解码的InputStreamOutputStream

使用Reader,数据源虽然是字节,但我们读入的数据都是char类型的字符,原因是Reader内部把读入的byte做了解码,转换成了char。使用InputStream,我们读入的数据和原始二进制数据一模一样,是byte[]数组,但是我们可以自己把二进制byte[]数组按照某种编码转换为字符串。究竟使用Reader还是InputStream,要取决于具体的使用场景。如果数据源不是文本,就只能使用InputStream,如果数据源是文本,使用Reader更方便一些。WriterOutputStream是类似的。

Java标准库的包java.io提供了同步IO,而java.nio则是异步IO。上面我们讨论的InputStreamOutputStreamReaderWriter都是同步IO的抽象类,对应的具体实现类,以文件为例,有FileInputStreamFileOutputStreamFileReaderFileWriter

二、File对象

Java标准库的java.io.File对象表示一个文件或者目录,构造一个File对象,即使传入的文件或目录不存在,代码也不会出错,因为构造一个File对象,并不会导致任何磁盘操作。只有当我们调用File对象的某些方法的时候,才真正进行磁盘操作。

路径

File对象有3种形式表示的路径,一种是getPath(),返回构造方法传入的路径,一种是getAbsolutePath(),返回绝对路径,一种是getCanonicalPath,它和绝对路径类似,但是返回的是规范路径(没有 ..)。

因为Windows和Linux的路径分隔符不同,File对象有一个静态变量用于表示当前平台的系统分隔符。

Java标准库还提供了一个Path对象,它位于java.nio.file包。Path对象和File对象类似,但操作更加简单,如果需要对目录进行复杂的拼接、遍历等操作,使用Path对象更方便。

创建和删除文件

当File对象表示一个文件时,可以通过createNewFile()创建一个新文件,用delete()删除该文件。

有些时候,程序需要读写一些临时文件,File对象提供了createTempFile()来创建一个临时文件,以及deleteOnExit()在JVM退出时自动删除该文件。

遍历文件和目录

当File对象表示一个目录时,可以使用list()listFiles()列出目录下的文件和子目录名。listFiles()提供了一系列重载方法,可以过滤不想要的文件和目录。

和文件操作类似,File对象如果表示一个目录,可以通过以下方法创建和删除目录:

  • boolean mkdir():创建当前File对象表示的目录;
  • boolean mkdirs():创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;
  • boolean delete():删除当前File对象表示的目录,当前目录必须为空才能删除成功。
 package com.study.IO;
 ​
 import java.io.File;
 import java.io.FilenameFilter;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Arrays;
 ​
 public class FileDemo {
     public static void main(String[] args) throws IOException {
         File file = new File("8.IO/resources/readme.txt");
         System.out.println(file); // 8.IO/resources/readme.txt
 ​
         File f = new File("..");
         System.out.println(f.getPath()); // ..
         System.out.println(f.getAbsolutePath()); // /Users/dxm/java/study/..
         System.out.println(f.getCanonicalPath()); // /Users/dxm/java
         System.out.println("========================");
 ​
         pathsDemo();
         System.out.println("========================");
 ​
         System.out.println(File.separator); // 根据当前平台打印""或"/"
         System.out.println("========================");
 ​
         createAndDeleteFile();
         System.out.println("========================");
 ​
         scanFiles();
     }
 ​
     public static void pathsDemo() {
         Path path = Paths.get(".", "src", "com");
         System.out.println(path); // ./src/com
 ​
         System.out.println(path.toAbsolutePath()); // /Users/dxm/java/study/./src/com
         System.out.println(path.normalize()); // src/com
         System.out.println(path.toFile()); // ./src/com 转换为File对象
 ​
         for (Path p : Paths.get("..").toAbsolutePath()) { // 可以直接遍历Path
             System.out.println("    " + p);
         }
     }
 ​
     public static void createAndDeleteFile() throws IOException {
         File file = new File("8.IO/resources/newFile.txt");
         if (file.createNewFile()) {
             System.out.println("文件创建成功");
         } else {
             System.out.println("文件创建失败");
         }
 ​
         if (file.delete()) {
             System.out.println("文件删除成功");
         } else {
             System.out.println("文件删除失败");
         }
 ​
         // 创建临时文件
         File tempFile = File.createTempFile("tmp-", ".txt"); // 提供临时文件的前缀和后缀
         tempFile.deleteOnExit(); // JVM退出时自动删除
         System.out.println(tempFile.isFile());
         System.out.println(tempFile.getAbsolutePath());
     }
 ​
     public static void scanFiles() {
         File file = new File("8.IO");
         System.out.println(Arrays.toString(file.list()));
         System.out.println("============");
 ​
         File[] files = file.listFiles();
         printFiles(files);
         System.out.println("============");
 ​
         File[] files1 = file.listFiles(new FilenameFilter() {
             @Override
             public boolean accept(File dir, String name) {
                 return name.endsWith(".iml");
             }
         });
         printFiles(files1);
     }
 ​
     public static void printFiles(File[] files) {
         if (files != null) {
             for (File f : files) {
                 System.out.println(f);
             }
         }
     }
 }
 ​

三、InputStream

InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。这个抽象类定义的一个最重要的方法就是int read(),签名如下:

 public abstract int read() throws IOException;

这个方法会读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回-1表示不能继续读取了。

FileInputStreamInputStream的一个子类。顾名思义,FileInputStream可以从文件获取输入流,就是从文件流中读取数据。

此外,ByteArrayInputStream可以在内存中模拟一个InputStream

try ... finally来编写关闭流会感觉比较复杂,更好的写法是利用Java 7引入的新的try(resource)的语法,只需要编写try语句,让编译器自动为我们关闭资源。

实际上,编译器并不会特别地为InputStream加上自动关闭。编译器只看try(resource = ...)中的对象是否实现了java.lang.AutoCloseable接口,如果实现了,就自动加上finally语句并调用close()方法。InputStreamOutputStream都实现了这个接口,因此,都可以用在try(resource)中。

阻塞

InputStreamread()方法是阻塞的。

缓冲

在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。InputStream提供了两个重载方法来支持读取多个字节:

  • int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
  • int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数
 package com.study.IO;
 ​
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Arrays;
 ​
 public class InputStreamDemo {
     public static void main(String[] args) throws IOException {
         readFile();
         System.out.println("=================");
 ​
         readFile2();
         System.out.println("=================");
 ​
         readFile3();
         System.out.println("=================");
 ​
         readFile4();
     }
 ​
     public static void readFile() throws IOException {
         InputStream input = null;
         try {
             // 创建一个FileInputStream对象:
             input = new FileInputStream("8.IO/resources/readme.txt");
             int n;
             while ((n = input.read()) != -1) { // 利用while同时读取并判断
                 System.out.println(n); // 打印byte的值
             }
         } finally {
             if (input != null) {
                 input.close(); // 关闭流
             }
         }
     }
 ​
     public static void readFile2() throws IOException {
         try (InputStream input = new FileInputStream("8.IO/resources/readme.txt")) {
             int n;
             while ((n = input.read()) != -1) {
                 System.out.println(n);
             }
         } // 编译器在此自动为我们写入finally并调用close()
     }
 ​
     public static void readFile3() throws IOException {
         try (InputStream input = new FileInputStream("8.IO/resources/readme.txt")) {
             // 定义1000个字节大小的缓冲区:
             byte[] buffer = new byte[1000];
             int n;
             while ((n = input.read(buffer)) != -1) { // 读取到缓冲区
                 System.out.println("read " + n + " bytes.");
                 System.out.println(Arrays.toString(buffer));
             }
         }
     }
 ​
     public static void readFile4() throws IOException {
         String s;
         try (InputStream input = new FileInputStream("8.IO/resources/readme.txt")) {
             s = readAsString(input);
         }
         System.out.println(s);
     }
 ​
     // 可用 ByteArrayInputStream 模拟输入流
     public static String readAsString(InputStream input) throws IOException {
         int n;
         StringBuilder sb = new StringBuilder();
         while ((n = input.read()) != -1) {
             sb.append((char) n);
         }
         return sb.toString();
     }
 }
 ​
 public class InputStreamTest {
     public static void main(String[] args) throws IOException {
         byte[] data = {72, 101, 108, 108, 111, 33};
         try (InputStream input = new ByteArrayInputStream(data)) {
             String s = InputStreamDemo.readAsString(input);
             System.out.println(s);
         }
     }
 }

四、OutputStream

OutputStream也是抽象类,它是所有输出流的超类。这个抽象类定义的一个最重要的方法就是void write(int b),签名如下:

 public abstract void write(int b) throws IOException;

这个方法会写入一个字节到输出流。要注意的是,虽然传入的是int参数,但只会写入一个字节,即只写入int最低8位表示字节的部分(相当于b & 0xff)。

InputStream类似,OutputStream也提供了close()方法关闭输出流,以便释放系统资源。要特别注意:OutputStream还提供了一个flush()方法,它的目的是将缓冲区的内容真正输出到目的地。

阻塞

InputStream一样,OutputStreamwrite()方法也是阻塞的。

关闭

同时操作多个AutoCloseable资源时,在try(resource) { ... }语句中可以同时写出多个资源,用;隔开。

 package com.study.IO;
 ​
 import java.io.*;
 import java.nio.charset.StandardCharsets;
 ​
 public class OutputStreamDemo {
     public static void main(String[] args) throws IOException {
         writeFile();
         System.out.println("=================");
 ​
         writeFile2();
         System.out.println("=================");
 ​
         writeFile3();
         System.out.println("=================");
 ​
         // writeFile4();
     }
 ​
     public static void writeFile() throws IOException {
         OutputStream output = new FileOutputStream("8.IO/resources/out.txt");
         output.write(72); // H
         output.write(101); // e
         output.write(108); // l
         output.write(108); // l
         output.write(111); // o
         output.close();
     }
 ​
     public static void writeFile2() throws IOException {
         try (OutputStream output = new FileOutputStream("8.IO/resources/out.txt")) {
             output.write("Hello".getBytes(StandardCharsets.UTF_8)); // Hello
         } // 编译器在此自动为我们写入finally并调用close()
     }
 ​
     public static void writeFile3() throws IOException {
         byte[] data;
         try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
             output.write("Hello ".getBytes("UTF-8"));
             output.write("world!".getBytes("UTF-8"));
             data = output.toByteArray();
         }
         System.out.println(new String(data, "UTF-8"));
     }
 ​
     public static void writeFile4() throws IOException {
         // 读取input.txt,写入output.txt:
         try (InputStream input = new FileInputStream("8.IO/resources/readme.txt");
              OutputStream output = new FileOutputStream("8.IO/resources/out.txt")) {
             // input.transferTo(output); // transferTo的作用是?
         }
     }
 }
 ​

五、Filter模式

为了解决依赖继承会导致子类数量失控的问题,JDK首先将InputStream分为两大类:

一类是直接提供数据的基础InputStream,例如:

  • FileInputStream:从文件读取数据,是最终数据源;
  • ByteArrayInputStream:从内存读取数据,是最终数据源;
  • ServletInputStream:从HTTP请求读取数据,是最终数据源;
  • Socket.getInputStream():从TCP连接读取数据,是最终数据源;
  • ...

一类是提供额外附加功能的InputStream,例如:

  • BufferedInputStream:缓冲功能
  • DigestInputStream:计算签名的功能
  • CipherInputStream:加密/解密功能
  • ...

当我们需要给一个“基础”InputStream附加各种功能时,我们先确定这个能提供数据源的InputStream,因为我们需要的数据总得来自某个地方,例如,FileInputStream,数据来源自文件:

 InputStream file = new FileInputStream("test.gz");

紧接着,我们希望FileInputStream能提供缓冲的功能来提高读取的效率,因此我们用BufferedInputStream包装这个InputStream,得到的包装类型是BufferedInputStream,但它仍然被视为一个InputStream

 InputStream buffered = new BufferedInputStream(file);

最后,假设该文件已经用gzip压缩了,我们希望直接读取解压缩的内容,就可以再包装一个GZIPInputStream

InputStream gzip = new GZIPInputStream(buffered);

无论我们包装多少次,得到的对象始终是InputStream,我们直接用InputStream来引用它,就可以正常读取:

┌─────────────────────────┐
│GZIPInputStream          │
│┌───────────────────────┐│
││BufferedFileInputStream││
││┌─────────────────────┐││
│││   FileInputStream   │││
││└─────────────────────┘││
│└───────────────────────┘│
└─────────────────────────┘

上述这种通过一个“基础”组件再叠加各种“附加”功能组件的模式,称之为Filter模式(或者装饰器模式:Decorator)。它可以让我们通过少量的类来实现各种功能的组合:

                 ┌─────────────┐
                 │ InputStream │
                 └─────────────┘
                       ▲ ▲
┌────────────────────┐ │ │ ┌─────────────────┐
│  FileInputStream   │─┤ └─│FilterInputStream│
└────────────────────┘ │   └─────────────────┘
┌────────────────────┐ │     ▲ ┌───────────────────┐
│ByteArrayInputStream│─┤     ├─│BufferedInputStream│
└────────────────────┘ │     │ └───────────────────┘
┌────────────────────┐ │     │ ┌───────────────────┐
│ ServletInputStream │─┘     ├─│  DataInputStream  │
└────────────────────┘       │ └───────────────────┘
                             │ ┌───────────────────┐
                             └─│CheckedInputStream │
                               └───────────────────┘
package com.study.IO;

import java.io.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

// 编写自己的FilterInputStream,以便可以把自己的FilterInputStream“叠加”到任何一个InputStream中
public class FilterDemo {
    public static void main(String[] args) throws IOException {
        byte[] data = "hello, world!".getBytes();
        try (CountInputStream input = new CountInputStream(new ByteArrayInputStream(data))) {
            int n;
            while ((n = input.read()) != -1) {
                System.out.println((char) n);
            }
            System.out.println("Total read " + input.getBytesRead() + " bytes");
        }
    }
}

// 作用是对输入的字节进行计数
class CountInputStream extends FilterInputStream {
    private int count = 0;

    CountInputStream(InputStream in) {
        super(in);
    }

    public int getBytesRead() {
        return this.count;
    }

    @Override
    public int read() throws IOException {
        int n = in.read();
        if (n != -1) {
            this.count++;
        }
        return n;
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        int n = in.read(b, off, len);
        if (n != -1) {
            this.count += n;
        }
        return n;
    }
}

六、操作Zip

ZipInputStream是一种FilterInputStream,它可以直接读取zip包的内容:

┌───────────────────┐
│    InputStream    │
└───────────────────┘
          ▲
          │
┌───────────────────┐
│ FilterInputStream │
└───────────────────┘
          ▲
          │
┌───────────────────┐
│InflaterInputStream│
└───────────────────┘
          ▲
          │
┌───────────────────┐
│  ZipInputStream   │
└───────────────────┘
          ▲
          │
┌───────────────────┐
│  JarInputStream   │
└───────────────────┘

另一个JarInputStream是从ZipInputStream派生,它增加的主要功能是直接读取jar文件里面的MANIFEST.MF文件。因为本质上jar包就是zip包,只是额外附加了一些固定的描述文件。

读取zip包

我们要创建一个ZipInputStream,通常是传入一个FileInputStream作为数据源,然后,循环调用getNextEntry(),直到返回null,表示zip流结束。

一个ZipEntry表示一个压缩文件或目录,如果是压缩文件,我们就用read()方法不断读取,直到返回-1

try (ZipInputStream zip = new ZipInputStream(new FileInputStream(...))) {
    ZipEntry entry = null;
    while ((entry = zip.getNextEntry()) != null) {
        String name = entry.getName();
        if (!entry.isDirectory()) {
            int n;
            while ((n = zip.read()) != -1) {
                ...
            }
        }
    }
}

写入zip包

ZipOutputStream是一种FilterOutputStream,它可以直接写入内容到zip包。我们要先创建一个ZipOutputStream,通常是包装一个FileOutputStream,然后,每写入一个文件前,先调用putNextEntry(),然后用write()写入byte[]数据,写入完毕后调用closeEntry()结束这个文件的打包。

try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(...))) {
    File[] files = ...
    for (File file : files) {
        zip.putNextEntry(new ZipEntry(file.getName()));
        zip.write(Files.readAllBytes(file.toPath()));
        zip.closeEntry();
    }
}

上面的代码没有考虑文件的目录结构。如果要实现目录层次结构,new ZipEntry(name)传入的name要用相对路径。

七、读取classpath资源

有没有路径无关的读取文件的方式呢?

我们知道,Java存放.class的目录或jar包也可以包含任意其他类型的文件,例如:

  • 配置文件,例如.properties
  • 图片文件,例如.jpg
  • 文本文件,例如.txt.csv
  • ……

从classpath读取文件就可以避免不同环境下文件路径不一致的问题:如果我们把default.properties文件放到classpath中,就不用关心它的实际存放路径。

在classpath中的资源文件,路径总是以开头,我们*先获取当前的Class对象,然后调用getResourceAsStream()*就可以直接从classpath读取任意的资源文件,调用getResourceAsStream()需要特别注意的一点是,如果资源文件不存在,它将返回null。因此,我们需要检查返回的InputStream是否为null,如果为null,表示资源文件在classpath中没有找到:

try (InputStream input = getClass().getResourceAsStream("/default.properties")) {
    if (input != null) {
        // TODO:
    }
}

如果我们把默认的配置放到jar包中,再从外部文件系统读取一个可选的配置文件,就可以做到既有默认的配置文件,又可以让用户自己修改配置:

Properties props = new Properties();
props.load(inputStreamFromClassPath("/default.properties"));
props.load(inputStreamFromFile("./conf.properties"));

这样读取配置文件,应用程序启动就更加灵活。

八、序列化

序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。

一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口,它的定义如下:

public interface Serializable {
}

Serializable接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。

把一个Java对象变为byte[]数组,需要使用ObjectOutputStream。它负责把一个Java对象写入一个字节流

ObjectOutputStream既可以写入基本类型,如intboolean,也可以写入String(以UTF-8编码),还可以写入实现了Serializable接口的Object

反序列化

ObjectOutputStream相反,ObjectInputStream负责从一个字节流读取Java对象:

try (ObjectInputStream input = new ObjectInputStream(...)) {
    int n = input.readInt();
    String s = input.readUTF();
    Double d = (Double) input.readObject();
}

除了能读取基本类型和String类型外,调用readObject()可以直接返回一个Object对象。要把它变成一个特定类型,必须强制转型。

readObject()可能抛出的异常有:

  • ClassNotFoundException:没有找到对应的Class;
  • InvalidClassException:Class不匹配。

对于ClassNotFoundException,这种情况常见于一台电脑上的Java程序把一个Java对象,例如,Person对象序列化以后,通过网络传给另一台电脑上的另一个Java程序,但是这台电脑的Java程序并没有定义Person类,所以无法反序列化。

对于InvalidClassException,这种情况常见于序列化的Person对象定义了一个int类型的age字段,但是反序列化时,Person类定义的age字段被改成了long类型,所以导致class不兼容。

为了避免这种class定义变动导致的不兼容,Java的序列化允许class定义一个特殊的serialVersionUID静态变量,用于标识Java类的序列化“版本”,通常可以由IDE自动生成。如果增加或修改了字段,可以改变serialVersionUID的值,这样就能自动阻止不匹配的class版本:

public class Person implements Serializable {
    private static final long serialVersionUID = 2709425275741743919L;
}

要特别注意反序列化的几个重要特点:

反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行

实际上,Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String)的内容,而不存储任何与代码相关的信息。

package com.study.IO;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Arrays;

public class SerializeDemo {
    public static void main(String[] args) throws Exception {
        // 序列化
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        try (final ObjectOutputStream output = new ObjectOutputStream(buffer)) {
            // 写入int
            output.writeInt(12345);
            // 写入String
            output.writeUTF("hello");
            // 写入Object
            output.writeObject(Double.valueOf(123.456));
        }
        System.out.println(Arrays.toString(buffer.toByteArray()));
        System.out.println("======================");

        // 反序列化
        ByteArrayInputStream data = new ByteArrayInputStream(buffer.toByteArray());
        try (ObjectInputStream input = new ObjectInputStream(data)) {
            int n = input.readInt();
            String s = input.readUTF();
            Double d = (Double) input.readObject();

            System.out.println(n);
            System.out.println(s);
            System.out.println(d);
        }
    }
}

九、Reader

Reader是Java的IO库提供的另一个输入流的类。和InputStream的区别是,InputStream是一个字节流,即以byte为单位读取,而Reader是一个字符流,即以char为单位读取:

InputStreamReader
字节流,以byte为单位字符流,以char为单位
读取字节(-1,0~255):int read()读取字符(-1,0~65535):int read()
读到字节数组:int read(byte[] b)读到字符数组:int read(char[] c)

java.io.Reader是所有字符输入流的超类,它最主要的方法是:

public int read() throws IOException;

这个方法读取字符流的下一个字符,并返回字符表示的int,范围是0~65535。如果已读到末尾,返回-1

Reader还提供了一次性读取若干字符并填充到char[]数组的方法:

public int read(char[] c) throws IOException

它返回实际读入的字符个数,最大不超过char[]数组的长度。返回-1表示流结束。利用这个方法,我们可以先设置一个缓冲区,然后,每次尽可能地填充缓冲区。

FileReader

FileReaderReader的一个子类,它可以打开文件并获取Reader

要避免乱码问题,我们需要在创建FileReader时指定编码:

try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8) {
    // TODO
}

public void readFile() throws IOException {
    try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8)) {
        char[] buffer = new char[1000];
        int n;
        while ((n = reader.read(buffer)) != -1) {
            System.out.println("read " + n + " chars.");
        }
    }
}

CharArrayReader

CharArrayReader可以在内存中模拟一个Reader,它的作用实际上是把一个char[]数组变成一个Reader,这和ByteArrayInputStream非常类似:

try (Reader reader = new CharArrayReader("Hello".toCharArray())) {
}

StringReader

StringReader可以直接把String作为数据源,它和CharArrayReader几乎一样:

try (Reader reader = new StringReader("Hello")) {
}

InputStreamReader

ReaderInputStream有什么关系?

除了特殊的CharArrayReaderStringReader,普通的Reader实际上是基于InputStream构造的,因为Reader需要从InputStream中读入字节流(byte),然后,根据编码设置,再转换为char就可以实现字符流。如果我们查看FileReader的源码,它在内部实际上持有一个FileInputStream

既然Reader本质上是一个基于InputStreambytechar的转换器,那么,如果我们已经有一个InputStream,想把它转换为Reader,是完全可行的。InputStreamReader就是这样一个转换器,它可以把任何InputStream转换为Reader。示例代码如下:

// 持有InputStream:
InputStream input = new FileInputStream("src/readme.txt");
// 变换为Reader:
Reader reader = new InputStreamReader(input, "UTF-8");

构造InputStreamReader时,我们需要传入InputStream,还需要指定编码,就可以得到一个Reader对象。上述代码可以通过try (resource)更简洁地改写如下:

 try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), "UTF-8")) {
     // TODO:
 }

上述代码实际上就是FileReader的一种实现方式。

使用try (resource)结构时,当我们关闭Reader时,它会在内部自动调用InputStreamclose()方法,所以,只需要关闭最外层的Reader对象即可。

十、Writer

Reader是带编码转换器的InputStream,它把byte转换为char,而Writer就是带编码转换器的OutputStream,它把char转换为byte并输出。

WriterOutputStream的区别如下:

OutputStreamWriter
字节流,以byte为单位字符流,以char为单位
写入字节(0~255):void write(int b)写入字符(0~65535):void write(int c)
写入字节数组:void write(byte[] b)写入字符数组:void write(char[] c)
无对应方法写入String:void write(String s)

FileWriter

FileWriter就是向文件中写入字符流的Writer。它的使用方法和FileReader类似:

 try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
     writer.write('H'); // 写入单个字符
     writer.write("Hello".toCharArray()); // 写入char[]
     writer.write("Hello"); // 写入String
 }

CharArrayWriter

CharArrayWriter可以在内存中创建一个Writer,它的作用实际上是构造一个缓冲区,可以写入char,最后得到写入的char[]数组,这和ByteArrayOutputStream非常类似:

try (CharArrayWriter writer = new CharArrayWriter()) {
    writer.write(65);
    writer.write(66);
    writer.write(67);
    char[] data = writer.toCharArray(); // { 'A', 'B', 'C' }
}

StringWriter

StringWriter也是一个基于内存的Writer,它和CharArrayWriter类似。实际上,StringWriter在内部维护了一个StringBuffer,并对外提供了Writer接口。

  • CharArrayWriterStringWriter在内存中模拟一个字符流输出。

OutputStreamWriter

除了CharArrayWriterStringWriter外,普通的Writer实际上是基于OutputStream构造的,它接收char,然后在内部自动转换成一个或多个byte,并写入OutputStream。因此,OutputStreamWriter就是一个将任意的OutputStream转换为Writer的转换器:

try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) {
    // TODO:
}

上述代码实际上就是FileWriter的一种实现方式。这和上一节的InputStreamReader是一样的。

Print

PrintStream最终输出的总是byte数据,而PrintWriter则是扩展了Writer接口,它的print()/println()方法最终输出的是char数据。两者的使用方法几乎是一模一样的:

 public class Main {
     public static void main(String[] args)     {
         StringWriter buffer = new StringWriter();
         try (PrintWriter pw = new PrintWriter(buffer)) {
             pw.println("Hello");
             pw.println(12345);
             pw.println(true);
         }
         System.out.println(buffer.toString());
     }
 }

十一、使用Files

从Java 7开始,提供了*Files这个工具类*,能极大地方便我们读写文件。例如,我们要把一个文件的全部内容读取为一个byte[]

 byte[] data = Files.readAllBytes(Path.of("/path/to/file.txt"));

如果是文本文件,可以把一个文件的全部内容读取为String

 // 默认使用UTF-8编码读取:
 String content1 = Files.readString(Path.of("/path/to/file.txt"));
 // 可指定编码:
 String content2 = Files.readString(Path.of("/path", "to", "file.txt"), StandardCharsets.ISO_8859_1);
 // 按行读取并返回每行内容:
 List<String> lines = Files.readAllLines(Path.of("/path/to/file.txt"));

写入文件也非常方便:

 // 写入二进制文件:
 byte[] data = ...
 Files.write(Path.of("/path/to/file.txt"), data);
 // 写入文本并指定编码:
 Files.writeString(Path.of("/path/to/file.txt"), "文本内容...", StandardCharsets.ISO_8859_1);
 // 按行写入文本:
 List<String> lines = ...
 Files.write(Path.of("/path/to/file.txt"), lines);

此外,Files工具类还有copy()delete()exists()move()等快捷方法操作文件和目录。

最后需要特别注意的是,Files提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,不可一次读入几个G的大文件。读写大型文件仍然要使用文件流,每次只读写一部分文件内容。