传统 IO 中使用流来传递、操纵、转换数据,流就是简单字节、原始数据类型、本地化字符或者对象组成的数据序列,流只支持单向传输。
一次 IO 流就是数据从一个源到一个目的地的过程,它可能存在磁盘文件、外围设备(键盘等)、其它程序(通过网络)或者内存序列之间。其中,向内存写入的流称为输入流,从内存中输出的流称为输出流

流的数据类型

Java 在内存中是以字节为存储单位的,读写数据也都是按字节一个个顺序读写,所以最基本的流就是字节流,所有与内存的操作最后都会转换为字节流。
java.io 包中主要有四个抽象基类,它定义了两种流数据传输格式:以字节为单位的字节流和以字符为单位的字符流。字节流主要用于处理原始数据,比如图像,要处理文档等字符类型要考虑使用字符流。
我们知道,抽象基类只能被继承,无法实例化对象。字节流和字符流的继承类图如下:
jdk-io-streams-bytes.png
jdk-io-streams-characters.png

字节流

字节流提供的常用抽象方法如下:

  • 输入流
    • read() 方法,每次读取一个字节并以 int 的形式返回值(低八位),如果没有字节可读则返回 -1
    • read(byte[]) 方法,每次读取指定个数的字节并放到缓存数组中,返回实际读取的字节数,没有字节可读返回 -1
    • close() 方法,关闭输入流,并释放该流关联的所有系统资源
    • transferTo(OutputStream out) 方法,将此输入流写入到指定输出流,默认使用缓冲区大小为 8M
  • 输出流

    • write(int b) 方法,每次输出一个字节
    • flush() 方法,强制将缓冲区的数据输出
    • close() 方法,关闭输出流,并释放该流关联的所有系统资源

      字符流

      字符流提供的抽象方法和字节流类似。

      流的分类

      继承自字节流类和字符流类的类分为以下几种,这里列出一些常见的:

      节点流

      节点流是真正处理数据的流,有以下几种:

      内存流

      在内存中操作,不需要 close() 关闭的流。
  • 字符串流
    StringReader 类读取字符串,StringWriter 类输出字符串。

    1. try (final StringReader stringReader = new StringReader("Hello IO");
    2. final StringWriter stringWriter = new StringWriter()) {
    3. int read;
    4. while ((read = stringReader.read()) != -1) {
    5. stringWriter.write(read);
    6. }
    7. System.out.println(stringWriter.toString());
    8. } catch (IOException e) {
    9. e.printStackTrace();
    10. }
  • 数组缓冲流
    内置缓冲数组的流,包含 ByteArrayInputStream 类、ByteArrayOutputStream 类、CharArrayReader 类、CharArrayWriter 类。

    1. try (final ByteArrayInputStream inputStream = new ByteArrayInputStream(new byte[]{'H', 'E', 'L', 'L', 'O'});
    2. final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    3. final CharArrayReader reader = new CharArrayReader(new char[]{'你', '好'});
    4. final CharArrayWriter writer = new CharArrayWriter()) {
    5. inputStream.transferTo(outputStream);
    6. reader.transferTo(writer);
    7. System.out.println(outputStream.toString());
    8. System.out.println(writer.toString());
    9. } catch (IOException e) {
    10. e.printStackTrace();
    11. }

    文件流

    文件流用于读写磁盘文件,使用完流需要 close() 关闭。

  • 读取磁盘文件,支持指定字符集

    • FileInputStream 类和 FileReader 类
  • 写入磁盘文件,支持指定字符集,和选择是否追加在末尾(默认覆盖文件)

    • FileOutputStream 类和 FileWriter 类

      1. try (final FileWriter writer = new FileWriter("test.txt", StandardCharsets.UTF_8, false);
      2. final FileReader reader = new FileReader("test.txt", StandardCharsets.UTF_8);
      3. final CharArrayWriter charArrayWriter = new CharArrayWriter()) {
      4. writer.write("This is the first line." + System.lineSeparator());
      5. writer.append("这是第二行。");
      6. writer.flush(); // 注意一定要强制输出缓存中的数据,否则不会立马看到
      7. reader.transferTo(charArrayWriter);
      8. System.out.println(charArrayWriter.toString());
      9. } catch (IOException e) {
      10. e.printStackTrace();
      11. }

      网络流

      网络流用于网络通信。

      管道流

      PipedOutputStream 类、PipedInputStream 类和 PipedWriter 类、PipedReader 类提供线程之间的通信,输入流和输出流一定成对出现,并且需要 connect() 连接。 ```java public class ThreadSender implements Runnable {

      private final PipedOutputStream outputStream = new PipedOutputStream();

      private String message;

      public ThreadSender(String message) { this.message = message; }

      @Override public void run() { try (outputStream) {

      1. outputStream.write(message.getBytes());

      } catch (IOException e) {

      1. e.printStackTrace();

      } }

      public PipedOutputStream getOutputStream() { return outputStream; } }

public class ThreadReceiver implements Runnable { private final PipedInputStream inputStream = new PipedInputStream();

  1. @Override
  2. public void run() {
  3. try (inputStream) {
  4. byte[] read = new byte[1024];
  5. while ((inputStream.read(read) != -1)) {
  6. System.out.println(new String(read));
  7. }
  8. } catch (IOException e) {
  9. e.printStackTrace();
  10. }
  11. }
  12. public PipedInputStream getInputStream() {
  13. return inputStream;
  14. }

}

final ThreadSender sender = new ThreadSender(“hello”); final ThreadReceiver receiver = new ThreadReceiver(); try { sender.getOutputStream().connect(receiver.getInputStream()); new Thread(sender).start(); new Thread(receiver).start(); } catch (IOException e) { e.printStackTrace(); }

  1. ---
  2. <a name="uo182"></a>
  3. ## 装饰流
  4. 使用[装饰者模式](https://www.yuque.com/pycrab/idea/pyyp2d?view=doc_embed),包装节点流添加额外的行为,这些流大部分继承自 FilterXXX 类:
  5. <a name="GOEH1"></a>
  6. ### 缓冲流
  7. 缓冲流包含 BufferedInputStream 类、BufferedOutputStream 类、BufferedReader 类、BufferedWriter 类。<br />创建缓冲流时会创建一个内部缓冲数组,通过批量操作,字节流可以减少对底层系统直接调用,字符流可以减少字节和字符之间的转换效率。<br />关闭缓冲流会使用 CAS 清空缓冲数组。
  8. ```java
  9. try (final FileWriter writer = new FileWriter("test.txt", StandardCharsets.UTF_8, false);
  10. final BufferedReader bufferedReader = new BufferedReader(new FileReader("test.txt", StandardCharsets.UTF_8))) {
  11. writer.write("This is the first line." + System.lineSeparator());
  12. writer.append("这是第二行。");
  13. writer.flush();
  14. String line;
  15. while ((line = bufferedReader.readLine()) != null) {
  16. System.out.println(line);
  17. }
  18. } catch (IOException e) {
  19. e.printStackTrace();
  20. }

输出流实现了 flushable 接口,缓冲流重写了其中的 flush() 方法,调用 flush() 方法可以将缓冲区的内容写入基本流。
缓冲流调用 close() 方法,会先尝试调用 flush() 方法清空缓冲区后关闭流。
如果目的地是磁盘,调用 flush() 方法只能保证写入基本流,不保证已写入磁盘。

数据流

DataInputStream 类和 DataOutputStream 类提供 Java 原始数据类型的处理,数据流支持基本类型和 String 类型的数据,使用浮点数表示货币值,不支持精确浮点值。程序员需保证写入和读出的数据类型一致。

  1. try (final DataOutputStream outputStream = new DataOutputStream(new FileOutputStream("test.txt"));
  2. final DataInputStream inputStream = new DataInputStream(new FileInputStream("test.txt"))) {
  3. outputStream.writeBoolean(true);
  4. outputStream.writeInt(123);
  5. outputStream.writeUTF("欧克");
  6. System.out.println(inputStream.readBoolean());
  7. System.out.println(inputStream.readInt());
  8. System.out.println(inputStream.readUTF());
  9. } catch (IOException e) {
  10. e.printStackTrace();
  11. }

格式化流

PrintStream 类和 PrintWriter 类。针对格式化的输入可以通过 Scanner 类进行包装,从而进行格式化扫描。

回读流

PushbackInputStream 类和 PushbackReader 类。可以将读到(read)的数据放到缓冲队列(unread)中重新读取(read)。

适配流

使用适配器模式,改变原始类的行为,这些流有:

转换流

根据指定字符集将字节流解码为字符流和将字符流编码为字节流的类:InputStreamReader 和 OutputStreamWriter。

  1. try (final InputStreamReader reader = new InputStreamReader(new FileInputStream("test.txt"), StandardCharsets.UTF_8)) {
  2. int read;
  3. while ((read = reader.read()) != -1) {
  4. System.out.print(Character.toString(read));
  5. }
  6. } catch (IOException e) {
  7. e.printStackTrace();
  8. }

Zip 流

jdk-io-streams-zip.png
位于 java.util.zip 包中的 ZipInputStream 类和 ZipOutputStream 类可以操作 ZIP 文件。下面演示打包一个文件:

  1. try (final ZipOutputStream outputStream = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream("test.zip")));
  2. final FileInputStream inputStream = new FileInputStream("test.txt")) {
  3. // putNextEntry 开启一个文件目录(相对路径)
  4. outputStream.putNextEntry(new ZipEntry("txt/test.txt"));
  5. // 写入数据
  6. outputStream.write(inputStream.readAllBytes());
  7. // 打包完成关闭目录
  8. outputStream.closeEntry();
  9. }

对象流

ObjectInputStream 类和 ObjectOutputStream 类提供 Java 对象类型和原始数据类型的处理,支持对象的序列化和反序列化。
ObjectOutputStream 类通过 writeObject(Object o) 方法进行序列化对象,ObjectInputStream 类通过 readObject() 方法来反序列化对象。

来了解一下序列化和反序列化的知识

  1. try (final ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("task.txt"));
  2. final ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("task.txt"))) {
  3. outputStream.writeObject(task);
  4. final Task object = (Task) inputStream.readObject();
  5. System.out.println(object.toString());
  6. } catch (ClassNotFoundException | IOException e) {
  7. e.printStackTrace();
  8. }

顺序输入流

SequenceInputStream 类提供按顺序读取多个输入流。


标准流和控制台

  • 标准输入流 System.in (InputStream)
  • 标准输出流 System.out 和 System.err (PrintStream)
  • 控制台 System.console() (Console),提供真正的字符流

    流的关闭

    字节流(InputStream、OutputStream)和字符流(Reader、Writer)都实现了 Closeable 接口中的 close() 方法。因为垃圾收集器只能回收内存中的对象,无法回收流,所以打开的流都需要手动调用 close() 方法关闭。
    通常我们需要使用 try -catch - finally 块来打开和关闭流,并捕获已检查的异常:
    1. FileInputStream inputStream = null;
    2. try {
    3. inputStream = new FileInputStream("test.txt");
    4. } catch (FileNotFoundException e) {
    5. e.printStackTrace();
    6. } finally {
    7. if (null != inputStream) {
    8. try {
    9. inputStream.close();
    10. } catch (IOException e) {
    11. e.printStackTrace();
    12. }
    13. }
    14. }
    JDK 7 提供的 try - with - resources 语法糖可以省略 finally 块来自动关闭资源:
    1. try (FileInputStream inputStream = new FileInputStream("test.txt")) {
    2. inputStream.read();
    3. } catch (FileNotFoundException e) {
    4. e.printStackTrace();
    5. }
    JDK 9 更是简化了 try - with - resources 流的声明,如果不可变的流在 try 块外已经初始化,JDK 9 之后可以直接使用,无需再次声明: ```java // JDK 9 之前 FileInputStream inputStream = new FileInputStream(“test.txt”); try (FileInputStream fileInputStream = inputStream) { fileInputStream.read(); } catch (FileNotFoundException e) { e.printStackTrace(); }

// JDK 9 之后 FileInputStream inputStream = new FileInputStream(“test.txt”); try (inputStream) { inputStream.read(); } catch (FileNotFoundException e) { e.printStackTrace(); } ``` Closeable 接口的 close() 方法可以执行多次,对已经关闭的资源是没有影响的,它是幂等的;而它的父类 AutoCloseable 接口的 close() 方法不是幂等的,多次执行可能产生副作用,所以要求实现 AutoCloseable 接口的类自己实现幂等。