java.io Java的IO模型设计非常优秀,它使用Decorator(装饰者)模式,按功能划分Stream,您可以动态装配这些Stream,以便获得您需要的功能。 例如,您需要一个具有缓冲的文件输入流,则应当组合使用FileInputStream和BufferedInputStream。

注意:Java 8 函数式编程中的 Stream 类和这里的 I/O stream 没有任何关系。

IO - 图1

What

同步 vs 异步 & 阻塞 vs 非阻塞

  • 同步 vs 异步

用于表示消息通信机制(简单来说就是需不需要等待返回结果)。

  • 同步:需要不断轮询数据是否准备好了,或者一直在等待数据准备好。 例如:打电话;
  • 异步:发送一个请求就立即返回,然后去干别的事情,当数据准备号了会通知进行相关处理。例如:发短信

同步实时性比较好,异步的并发性比较好。

  • 阻塞 vs 非阻塞

用于表示程序在等待调用结果时的状态。

  • 阻塞:调用结果返回之前,当前线程会被挂起;
  • 非阻塞:调用结果返回之前,该调用不会阻塞当前线程,而调用者通过同步定时轮询或异步回调获取任务状态。
  • 组合关系 |
    | 同步 | 异步 | | —- | —- | —- | | 阻塞 | 单任务,顺序执行(IO) | 单任务,自动提交任务执行状态 | | 非阻塞 | 多任务,定时轮训任务状态(NIO) | 多任务,自动提交任务执行状态(AIO) |

    • 阻塞:只能是单任务,非阻塞可以是多任务;
    • 异步阻塞:显然无用, 自断一臂,多此一举;
    • 同步非阻塞:函数A调用函数B后,函数A继续执行,并定时轮训查看函数B是否执行完毕;
    • 异步非阻塞:函数A调用函数B后,函数A继续执行,当函数B执行完毕后回调通知函数A。

How

字节流

以字节为单位。可以读任何类型数据,图片、文件、音乐视频等。 (Java 代码接收数据只能为 byte 数组)

  • FileInputStream

    • 作用:把硬盘文件中的数据读取到内存中
    • 构造方法:
      • FileInputStream(String name)
        • String name:文件路径
      • FileInputStream(File file)
        • File file:文件
    • 基本使用步骤:
      1. 创建FileInputStream对象,构造方法中绑定读取的数据源;
      2. 使用FileInputStream对象中的 read 方法读取文件;
      3. 使用FileInputStream对象中的 close 方法关闭流,释放资源。
    • 示例:
  • FileOutputStream

字符流

以字符为单位。其只能读取字符类型数据。(Java 代码接收数据为一般为 char 数组,也可以是别的)

  • GBK 编码:中文字符占 2 个字节,英文字符占 1 个字节;
  • UTF-8 编码:中文字符占 3 个字节,英文字符占 1 个字节;
  • Unicode 编码:中、英文都是占2个字节。
  • 既然有了字节流,为什么还需要字符流?

问题本质:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
回答:字符是由JVM将字节转换得到的,这个转换过程是比较消耗性能的,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。

File

用于文件或者目录的描述信息,例如生成新目录,修改文件名,删除文件,判断文件所在路径等。

RandomAccessFile

一个独立的类,直接继承至Object,它的功能丰富,可以从文件的任意位置进行存取(输入输出)操作。
RandomAccessFile 适用于由大小已知的记录组成的文件,所以我们可以使用 seek() 将文件指针从一条记录移动到另一条记录,然后对记录进行读取和修改。文件中记录的大小不一定都相同,只要我们能确定那些记录有多大以及它们在文件中的位置即可。
最初,我们可能难以相信 RandomAccessFile 不是 InputStream 或者 OutputStream 继承体系中的一部分。除了实现了 DataInput 和 DataOutput 接口(DataInputStream 和 DataOutputStream 也实现了这两个接口)之外,它和这两个继承体系没有任何关系。它甚至都不使用 InputStream 和 OutputStream 类中已有的任何功能。它是一个完全独立的类,其所有的方法(大多数都是 native 方法)都是从头开始编写的。这么做是因为 RandomAccessFile 拥有和别的 I/O 类型本质上不同的行为,因为我们可以在一个文件内向前和向后移动。在任何情况下,它都是自我独立的,直接继承自 Object。
从本质上来讲,RandomAccessFile 的工作方式类似于把 DataIunputStream 和 DataOutputStream 组合起来使用。另外它还有一些额外的方法,比如使用 getFilePointer() 可以得到当前文件指针在文件中的位置,使用 seek() 可以移动文件指针,使用 length() 可以得到文件的长度。另外,其构造器还需要传入第二个参数(和 C 语言中的 fopen() 相同)用来表示我们是准备对文件进行 “随机读”(r)还是“读写”(rw)。它并不支持只写文件,从这点来看,如果当初 RandomAccessFile能设计成继承自 DataInputStream,可能也是个不错的实现方式。
在 Java 1.4 中,RandomAccessFile 的大多数功能(但不是全部)都被 nio 中的内存映射文件(mmap)取代。

IO典型用途

字符输入文件

如果想要打开一个文件进行字符输入,我们可以使用一个 FileInputReader 对象,然后传入一个 String 或者 File 对象作为文件名。为了提高速度,我们希望对那个文件进行缓冲,那么我们可以将所产生的引用传递给一个 BufferedReader 构造器。BufferedReader 提供了 line() 方法,它会产生一个 Stream 对象:

  1. public class BufferedInputFile {
  2. public static String read(String filename) {
  3. try (BufferedReader in = new BufferedReader(
  4. new FileReader(filename))) {
  5. return in.lines()
  6. .collect(Collectors.joining("\n"));
  7. } catch (IOException e) {
  8. throw new RuntimeException(e);
  9. }
  10. }

Collectors.joining() 在其内部使用了一个 StringBuilder 来累加其运行结果。该文件会通过 try-with-resources 子句自动关闭。

从内存输入

下面示例中,从 BufferedInputFile.read() 读入的 String 被用来创建一个 StringReader 对象。然后调用其 read() 方法,每次读取一个字符,并把它显示在控制台上:

  1. public void MemoryInput throws IOException {
  2. StringReader in = new StringReader(
  3. BufferedInputFile.read("MemoryInput.java"));
  4. int c;
  5. while ((c = in.read()) != -1)
  6. System.out.print((char) c);
  7. }

注意 read() 是以 int 形式返回下一个字节,所以必须类型转换为 char 才能正确打印。

格式化内存输出

要读取格式化数据,我们可以使用 DataInputStream,它是一个面向字节的 I/O 类(不是面向字符的)。这样我们就必须使用 InputStream 类而不是 Reader 类。我们可以使用 InputStream 以字节形式读取任何数据(比如一个文件),但这里使用的是字符串。

  1. public class FormattedMemoryInput {
  2. public static void main(String[] args) {
  3. try (
  4. DataInputStream in = new DataInputStream(
  5. new ByteArrayInputStream(
  6. BufferedInputFile.read("FormattedMemoryInput.java")
  7. .getBytes()))
  8. ) {
  9. while (true)
  10. System.out.write((char) in.readByte());
  11. } catch (EOFException e) {
  12. System.out.println("\nEnd of stream");
  13. } catch (IOException e) {
  14. throw new RuntimeException(e);
  15. }
  16. }
  17. }

ByteArrayInputStream 必须接收一个字节数组,所以这里我们调用了 String.getBytes() 方法。所产生的的 ByteArrayInputStream 是一个适合传递给 DataInputStream 的 InputStream。

文件的输出

  • 基本方式

在本例中,为了提供格式化功能,它又被装饰成了 PrintWriter。按照这种方式创建的数据文件可作为普通文本文件来读取。

  1. public class BasicFileOutput {
  2. static String file = "BasicFileOutput.dat";
  3. public static void main(String[] args) {
  4. try (
  5. BufferedReader in = new BufferedReader(
  6. new StringReader(BufferedInputFile.read("BasicFileOutput.java")));
  7. PrintWriter out = new PrintWriter(
  8. new BufferedWriter(new FileWriter(file)))
  9. ) {
  10. in.lines().forEach(out::println);
  11. } catch (IOException e) {
  12. throw new RuntimeException(e);
  13. }
  14. // Show the stored file:
  15. System.out.println(BufferedInputFile.read(file));
  16. }
  17. }
  • 快捷方式

Java 5 在 PrintWriter 中添加了一个辅助构造器,有了它,你在创建并写入文件时,就不必每次都手动执行一些装饰的工作。下面的代码使用这种快捷方式重写了 BasicFileOutput.java

  1. public class FileOutputShortcut {
  2. static String file = "FileOutputShortcut.dat";
  3. public static void main(String[] args) {
  4. try (
  5. BufferedReader in = new BufferedReader(
  6. new StringReader(BufferedInputFile.read("FileOutputShortcut.java")));
  7. // Here's the shortcut:
  8. PrintWriter out = new PrintWriter(file)
  9. ) {
  10. in.lines().forEach(out::println);
  11. } catch (IOException e) {
  12. throw new RuntimeException(e);
  13. }
  14. System.out.println(BufferedInputFile.read(file));
  15. }
  16. }

使用这种方式仍具备了缓冲的功能,只是现在不必自己手动添加缓冲了。但遗憾的是,其它常见的写入任务都没有快捷方式,因此典型的 I/O 流依旧涉及大量冗余的代码。

存储和恢复数据

PrintWriter 是用来对可读的数据进行格式化。但如果要输出可供另一个“流”恢复的数据,我们可以用 DataOutputStream写入数据,然后用 DataInputStream 恢复数据。当然,这些流可能是任何形式,在下面的示例中使用的是一个文件,并且对读写都进行了缓冲。注意 DataOutputStream 和 DataInputStream 是面向字节的,因此要使用 InputStream 和 OutputStream 体系的类。

  1. public class StoringAndRecoveringData {
  2. public static void main(String[] args) {
  3. try (
  4. DataOutputStream out = new DataOutputStream(
  5. new BufferedOutputStream(new FileOutputStream("Data.txt")))
  6. ) {
  7. out.writeDouble(3.14159);
  8. out.writeUTF("That was pi");
  9. out.writeDouble(1.41413);
  10. out.writeUTF("Square root of 2");
  11. } catch (IOException e) {
  12. throw new RuntimeException(e);
  13. }
  14. try (
  15. DataInputStream in = new DataInputStream(
  16. new BufferedInputStream(new FileInputStream("Data.txt")))
  17. ) {
  18. System.out.println(in.readDouble());
  19. // Only readUTF() will recover the
  20. // Java-UTF String properly:
  21. System.out.println(in.readUTF());
  22. System.out.println(in.readDouble());
  23. System.out.println(in.readUTF());
  24. } catch (IOException e) {
  25. throw new RuntimeException(e);
  26. }
  27. }
  28. }

如果我们使用 DataOutputStream 进行数据写入,那么 Java 就保证了即便读和写数据的平台多么不同,我们仍可以使用 DataInputStream 准确地读取数据。这一点很有价值,众所周知,人们曾把大量精力耗费在数据的平台相关性问题上。但现在,只要两个平台上都有 Java,就不会存在这样的问题。

读写随机访问文件

使用 RandomAccessFile 就像是使用了一个 DataInputStream 和 DataOutputStream 的结合体(因为它实现了相同的接口:DataInput 和 DataOutput)。另外,我们还可以使用 seek() 方法移动文件指针并修改对应位置的值。
在使用 RandomAccessFile 时,你必须清楚文件的结构,否则没法正确使用它。RandomAccessFile 有一套专门的方法来读写基本数据类型的数据和 UTF-8 编码的字符串:

  1. public class UsingRandomAccessFile {
  2. static String file = "rtest.dat";
  3. public static void display() {
  4. try (
  5. RandomAccessFile rf = new RandomAccessFile(file, "r")
  6. ) {
  7. for (int i = 0; i < 7; i++)
  8. System.out.println("Value " + i + ": " + rf.readDouble());
  9. System.out.println(rf.readUTF());
  10. } catch (IOException e) {
  11. throw new RuntimeException(e);
  12. }
  13. }
  14. public static void main(String[] args) {
  15. try (
  16. RandomAccessFile rf = new RandomAccessFile(file, "rw")
  17. ) {
  18. for (int i = 0; i < 7; i++)
  19. rf.writeDouble(i * 1.414);
  20. rf.writeUTF("The end of the file");
  21. rf.close();
  22. display();
  23. } catch (IOException e) {
  24. throw new RuntimeException(e);
  25. }
  26. try (
  27. RandomAccessFile rf = new RandomAccessFile(file, "rw")
  28. ) {
  29. rf.seek(5 * 8);
  30. rf.writeDouble(47.0001);
  31. rf.close();
  32. display();
  33. } catch (IOException e) {
  34. throw new RuntimeException(e);
  35. }
  36. }
  37. }

display() 方法打开了一个文件,并以 double 值的形式显示了其中的七个元素。在 main() 中,首先创建了文件,然后打开并修改了它。因为 double 总是 8 字节长,所以如果要用 seek() 定位到第 5 个(从 0 开始计数) double 值,则要传入的地址值应该为 5*8。
正如前面所诉,虽然 RandomAccess 实现了 DataInput 和 DataOutput 接口,但实际上它和 I/O 继承体系中的其它部分是分离的。它不支持装饰,故而不能将其与 InputStream 及 OutputStream 子类中的任何一个组合起来,所以我们也没法给它添加缓冲的功能。
该类的构造器还有第二个必选参数:我们可以指定让 RandomAccessFile 以“只读”(r)方式或“读写” (rw)方式打开文件。
除此之外,还可以使用 nio 中的“内存映射文件”代替 RandomAccessFile。

数据压缩

见 OnJava8附录

  • Gzip简单压缩


  • zip多文件存储


  • Java的jar

对象序列化

如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。 简单来说:

  • 序列化: 将数据结构或对象转换成二进制字节流的过程
  • 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
  • transient关键字
    • 只能修饰成员变量,不能修饰类和方法;
    • 被transient修饰的成员变量不能被序列化;
    • 被static修饰的成员变量不能被序列化,因为序列化的都是对象。


  • InvalidClassException

当反序列化对象时,能找到class文件,但class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出InvalidClassException。因为序列化时JVM会默认给该类添加一个SerialVersionUID(与该类是一一对应的,只要该类的属性没变这个id就不变),当反序列化时SerialVersionUID变了,就会抛出该异常。
解决
可以手动给类指定一个序列号(推荐),这样在该类序列化和反序列化时,只要有对得上的部分就能对这部分反序列化成功,对不上的部分就是默认初始化值。格式在Serializable接口中规定:
可序列化类可以通过声明名为 “SerialVersionUID”的字段,该字段必须是 static final 修饰的long类型。