字节输入流与字节输出流 | JavaSE

字节输入流与字节输出流

一、学习目标

完成本章后,你应该能够:

  • 解释什么是字节输入流、字节输出流,以及输入和输出的方向。
  • 使用 FileInputStream 从文件中读取字节数据。
  • 理解 read()read(byte[])readAllBytes() 的返回值及适用场景。
  • 使用 FileOutputStream 向文件写入字节数据,并控制覆盖写入和追加写入。
  • 使用字节数组完成任意文件的复制。
  • 理解为什么字节流可以复制图片、视频、压缩包等任意类型文件。
  • 能够排查字节流中常见的文件不存在、数据重复、中文乱码、覆盖原文件等问题。

二、核心知识

2.1 什么是字节流

字节流(Byte Stream)就是:

以字节为基本单位读取和写出数据的 IO 流。

计算机中的文件,无论是:

  • .txt
  • .java
  • .jpg
  • .png
  • .mp3
  • .mp4
  • .zip
  • .pdf

最终存储在磁盘中的本质都是一串二进制数据。

而:

8 bit = 1 byte

因此,只要能够按照原始字节读取和写出,就可以处理任何文件。

字节流主要分为两大体系:

InputStream
    ↓
FileInputStream

OutputStream
    ↓
FileOutputStream

其中:

| 类 | 作用 | | ------------------ | ------------------------ | | InputStream | 所有字节输入流的抽象父类 | | FileInputStream | 从文件中读取字节 | | OutputStream | 所有字节输出流的抽象父类 | | FileOutputStream | 向文件中写入字节 |

本章重点学习:

FileInputStream
FileOutputStream

2.2 输入和输出到底以谁为参照物

这里非常容易理解反。

Java IO 中的输入和输出:

始终以内存,也就是正在运行的 Java 程序为参照物。

例如磁盘上存在:

data.txt

如果把磁盘文件读取到 Java 程序:

磁盘文件
   ↓
Java程序 / 内存

这叫:

Input
输入

所以使用:

FileInputStream

如果把 Java 程序中的数据写到磁盘:

Java程序 / 内存
   ↓
磁盘文件

这叫:

Output
输出

所以使用:

FileOutputStream

可以记住一句话:

数据进入程序叫输入,数据离开程序叫输出。


2.3 FileInputStream:文件字节输入流

FileInputStream 的作用是:

把磁盘文件中的原始字节读取到 Java 程序中。

基本关系:

文件
 ↓
FileInputStream
 ↓
Java程序

常见创建方式:

InputStream inputStream = new FileInputStream("data.txt");

或者:

File file = new File("data.txt");
InputStream inputStream = new FileInputStream(file);

常用构造器:

| 构造器 | 作用 | | ------------------------------ | ------------------------------ | | FileInputStream(String name) | 根据文件路径创建字节输入流 | | FileInputStream(File file) | 根据 File 对象创建字节输入流 |


2.4 FileInputStream 三种主要读取方式

本阶段重点掌握三种:

read()
read(byte[])
readAllBytes()

它们解决的是三个不同的问题。


2.5 read():一次读取一个字节

方法:

int read()

作用:

每调用一次,从文件中读取一个字节。

例如:

InputStream inputStream = new FileInputStream("data.txt");

int data = inputStream.read();

System.out.println(data);

假设文件内容是:

abc

字符 a 对应的字节值是:

97

因此第一次:

inputStream.read();

得到:

97

第二次:

98

第三次:

99

第四次已经没有数据了:

-1

所以 read() 的规则非常重要:

读取到数据 → 返回该字节对应的整数
到达文件末尾 → 返回 -1

因此实际读取整个文件时通常写成:

try (InputStream inputStream =
             new FileInputStream("data.txt")) {

    int data;

    while ((data = inputStream.read()) != -1) {
        System.out.print((char) data);
    }
}

执行过程可以理解为:

第一次 read() → 97
第二次 read() → 98
第三次 read() → 99
第四次 read() → -1
                         ↓
                     循环结束

2.6 为什么 read() 返回 int 而不是 byte

可能会产生一个疑问:

明明读取的是一个 byte,为什么 read() 返回的是 int

原因在于:

一个字节可能具有:

0 ~ 255

这样的无符号数值范围。

同时 API 还需要一个特殊值表示:

文件读取结束

这个特殊值就是:

-1

如果直接返回 byte,无法同时完整表达:

0 ~ 255

和:

-1

因此 Java 使用:

int

作为返回类型。

这里要明确:

int 只是为了能够表达“一个字节的数据 + 文件结束标记 -1”,并不代表一次读取了 4 个字节。


2.7 单字节读取的问题

虽然:

read()

能够正确读取文件,但是一次只读取一个字节。

假设一个文件大小为:

100 MB

如果一次只读一个字节,就意味着要进行大量读取操作。

因此:

单字节读取适合理解 IO 工作机制,但通常不是复制大文件的推荐方式。

更常见的方案是:

read(byte[])

2.8 read(byte[]):一次读取一批字节

方法:

int read(byte[] buffer)

首先创建一个字节数组:

byte[] buffer = new byte[1024];

然后读取:

int len = inputStream.read(buffer);

含义是:

尝试最多读取 buffer.length 个字节,并把读取到的数据装进数组。

例如:

byte[] buffer = new byte[1024];

意味着:

一次最多读取 1024 字节

但是这里有一个极其重要的问题:

返回值不是数组长度,而是本次实际读取到的字节数。

例如文件还剩:

300 Byte

而数组容量是:

1024 Byte

那么:

int len = inputStream.read(buffer);

得到:

len == 300

因此正确读取方式:

try (InputStream inputStream =
             new FileInputStream("data.txt")) {

    byte[] buffer = new byte[1024];

    int len;

    while ((len = inputStream.read(buffer)) != -1) {
        String text = new String(buffer, 0, len);
        System.out.print(text);
    }
}

核心循环:

while ((len = inputStream.read(buffer)) != -1) {
    // 处理 buffer 中 [0, len) 范围的数据
}

这个结构非常重要。

后面进行:

  • 文件复制
  • 网络通信
  • 缓冲流
  • Socket
  • 文件上传下载

都会反复看到这种模式。


2.9 为什么一定要使用 len

下面这种写法存在问题:

byte[] buffer = new byte[1024];

while (inputStream.read(buffer) != -1) {
    System.out.println(new String(buffer));
}

问题在于:

new String(buffer)

会把整个数组全部转换。

但最后一次读取时,数组可能没有被完全填满。

例如:

buffer.length = 4

第一次读取:

A B C D

第二次只读取两个字节:

E F

数组中可能形成类似:

E F C D

因为后面的旧数据仍然保留在数组中。

所以必须记录:

int len

然后只处理:

buffer[0] ~ buffer[len - 1]

例如:

new String(buffer, 0, len);

文件复制时同样必须:

outputStream.write(buffer, 0, len);

而不能无脑:

outputStream.write(buffer);

2.10 readAllBytes():一次读取剩余全部字节

还可以使用:

byte[] readAllBytes()

例如:

try (InputStream inputStream =
             new FileInputStream("data.txt")) {

    byte[] data = inputStream.readAllBytes();

    System.out.println(data.length);
}

它的特点非常简单:

文件剩余数据
      ↓
一次全部读取
      ↓
byte[]

如果是 UTF-8 文本:

import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

public class ByteInputDemo {
    public static void main(String[] args) throws Exception {
        try (InputStream inputStream =
                     new FileInputStream("data.txt")) {

            byte[] data = inputStream.readAllBytes();

            String text =
                    new String(data, StandardCharsets.UTF_8);

            System.out.println(text);
        }
    }
}

但是:

readAllBytes() 不应该作为读取大文件的默认方案。

假设文件大小为:

5 GB

如果尝试一次全部读入内存,相当于希望:

磁盘 5 GB
   ↓
内存中创建巨大的 byte[]

可能造成:

内存压力
甚至 OutOfMemoryError

因此通常可以这样选择:

小文件 + 明确知道大小
        ↓
readAllBytes()

普通 / 大文件
        ↓
read(byte[])
循环读取

2.11 字节流读取中文为什么可能乱码

假设文本文件采用 UTF-8:

我爱Java

其中中文字符:

在 UTF-8 中通常由多个字节组成。

如果使用:

read()

一次只得到其中一个字节,再直接:

(char) data

就相当于:

一个完整中文字符
      ↓ 编码
多个字节
      ↓
只取其中一个字节
      ↓
强行当字符解释

结果自然可能乱码。

因此:

字节流本身并不会产生“乱码”,乱码发生在“把字节按照某种字符集解释成字符”的过程中。

例如:

byte[] bytes = inputStream.readAllBytes();

String text =
        new String(bytes, StandardCharsets.UTF_8);

这里真正决定是否正确的是:

文件编码
    =
解码时使用的字符集

例如:

文件是 UTF-8
解码也是 UTF-8
        ↓
正常

但:

文件是 GBK
解码使用 UTF-8
        ↓
仍然可能乱码

所以:

readAllBytes() 本身不是解决乱码的本质,正确的字符边界和正确的字符集才是关键。

对于纯文本的日常读取,下一章学习的:

FileReader

会更加自然。


三、使用方法

3.1 FileOutputStream:文件字节输出流

FileOutputStream 的作用:

把 Java 程序中的字节数据写入磁盘文件。

方向:

Java程序
   ↓
FileOutputStream
   ↓
磁盘文件

基本写法:

OutputStream outputStream =
        new FileOutputStream("data.txt");

常见构造器:

| 构造器 | 作用 | | ----------------------------------------------- | ---------------------------- | | FileOutputStream(String name) | 创建文件字节输出流 | | FileOutputStream(File file) | 根据 File 创建输出流 | | FileOutputStream(String name, boolean append) | 创建输出流并指定是否追加 | | FileOutputStream(File file, boolean append) | 根据 File 创建并指定是否追加 |


3.2 覆盖写入

默认:

new FileOutputStream("data.txt");

表示:

如果文件已经存在,原来的文件内容会被清空,再重新写入。

例如原来:

Java
MySQL
Spring

执行:

try (OutputStream outputStream =
             new FileOutputStream("data.txt")) {

    outputStream.write(97);
}

最后文件内容将不再是原来的内容。

而是新写进去的数据。

因此必须注意:

创建 FileOutputStream 本身就可能改变目标文件。

不要认为一定执行到:

write()

之后文件才发生变化。


3.3 追加写入

如果希望:

保留原文件内容,在文件末尾继续写。

使用:

new FileOutputStream("data.txt", true);

第二个参数:

true

表示:

append = true
追加模式

例如:

try (OutputStream outputStream =
             new FileOutputStream("data.txt", true)) {

    outputStream.write(97);
}

可以记成:

false / 不写第二参数
        ↓
覆盖

true
 ↓
追加

3.4 write(int):写一个字节

方法:

void write(int b)

例如:

try (OutputStream outputStream =
             new FileOutputStream("data.txt")) {

    outputStream.write(97);
}

写入的实际是:

97 对应的低 8 位

如果用常见字符编码查看文本文件,可能看到:

a

这里虽然参数类型是:

int

但:

write(int) 实际只写入参数最低的 8 个二进制位,也就是一个字节。


3.5 write(byte[]):写一个字节数组

比一个字节一个字节写更加常用:

void write(byte[] b)

例如:

import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

public class ByteOutputDemo {
    public static void main(String[] args) throws Exception {
        try (OutputStream outputStream =
                     new FileOutputStream("data.txt")) {

            byte[] data =
                    "Java IO".getBytes(StandardCharsets.UTF_8);

            outputStream.write(data);
        }
    }
}

过程:

"Java IO"
    ↓ getBytes()
byte[]
    ↓
FileOutputStream
    ↓
data.txt

3.6 write(byte[], offset, length)

非常重要的方法:

void write(byte[] b, int off, int len)

三个参数:

b   → 从哪个字节数组写
off → 从数组哪个位置开始
len → 写多少个字节

例如:

byte[] data = {65, 66, 67, 68, 69};

outputStream.write(data, 1, 3);

表示:

数组:

下标    0   1   2   3   4
数据    A   B   C   D   E
            ↑
          从1开始
          写3个

最终写入:

BCD

这个方法尤其重要,因为:

read(byte[])

不保证每次把数组填满。

因此文件复制时通常形成:

int len = inputStream.read(buffer);

outputStream.write(buffer, 0, len);

3.7 如何写入字符串

FileOutputStream 是字节流,因此不能直接:

outputStream.write("Hello");

字符串必须先编码成:

byte[]

例如:

byte[] data =
        "Hello Java".getBytes(StandardCharsets.UTF_8);

outputStream.write(data);

完整代码:

import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

public class WriteTextDemo {
    public static void main(String[] args) throws Exception {
        try (OutputStream outputStream =
                     new FileOutputStream("data.txt")) {

            outputStream.write(
                    "Hello Java".getBytes(StandardCharsets.UTF_8)
            );
        }
    }
}

这实际上再次体现了上一章学习的:

字符
 ↓ 编码
字节
 ↓
写入磁盘

3.8 如何写入换行

换行本质上同样是字符数据。

可以:

outputStream.write(
        System.lineSeparator()
              .getBytes(StandardCharsets.UTF_8)
);

例如:

try (OutputStream outputStream =
             new FileOutputStream("data.txt")) {

    outputStream.write(
            "Java".getBytes(StandardCharsets.UTF_8)
    );

    outputStream.write(
            System.lineSeparator()
                  .getBytes(StandardCharsets.UTF_8)
    );

    outputStream.write(
            "MySQL".getBytes(StandardCharsets.UTF_8)
    );
}

最终类似:

Java
MySQL

在 Windows 中经常还能看到:

"\r\n"

不过:

System.lineSeparator()

具有更好的跨平台表达能力。


四、原理与进阶

4.1 字节数组读取为什么更高效

对比:

read()

一次读取:

1 Byte

而:

read(byte[])

一次可以读取:

1024 Byte
8192 Byte
32768 Byte
...

例如复制:

32 MB

文件。

如果一个字节一个字节处理:

32 MB
↓
大约三千多万次单字节处理

而使用:

byte[] buffer = new byte[8192];

一次处理:

8192 Byte

调用次数会大幅减少。

因此:

批量读取和写入通常比一个字节一个字节操作更适合实际文件处理。

不过下一章之后还会学习专门用于提高 IO 效率的:

BufferedInputStream
BufferedOutputStream

本章先掌握最基础的文件字节流。


4.2 为什么字节流可以复制所有文件

假设图片中存在这样一串字节:

10110101 00101101 11100010 ...

使用 FileInputStream

原文件字节
    ↓
原样读取

再使用 FileOutputStream

读取到的字节
    ↓
原样写出

只要最终:

源文件每个字节
=
目标文件对应字节

复制后的文件就是完整的。

整个复制过程根本不需要理解:

这些字节代表图片?
音乐?
视频?
PDF?
ZIP?

字节流只负责:

原样搬运字节。

所以复制二进制文件时:

图片
视频
音频
压缩包
PDF
程序文件

都适合使用字节流。


五、实践应用

5.1 使用字节流完成文件复制

这是本章最重要的综合案例。

需求:

source.jpg
    ↓
复制
    ↓
target.jpg

实现:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

public class FileCopyDemo {

    public static void main(String[] args) throws Exception {
        copy("source.jpg", "target.jpg");
    }

    public static void copy(String source,
                            String target) throws Exception {

        try (
                InputStream inputStream =
                        new FileInputStream(source);

                OutputStream outputStream =
                        new FileOutputStream(target)
        ) {
            byte[] buffer = new byte[8192];

            int len;

            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
        }
    }
}

核心只有三步。

第一步:

InputStream inputStream =
        new FileInputStream(source);

建立:

源文件 → 程序

第二步:

OutputStream outputStream =
        new FileOutputStream(target);

建立:

程序 → 目标文件

第三步:

byte[] buffer = new byte[8192];

int len;

while ((len = inputStream.read(buffer)) != -1) {
    outputStream.write(buffer, 0, len);
}

不断执行:

源文件
 ↓
读取一批字节
 ↓
buffer
 ↓
写入这一批有效字节
 ↓
目标文件

直到:

read(...)

返回:

-1

复制结束。


5.2 为什么必须 write(buffer, 0, len)

错误:

while ((len = inputStream.read(buffer)) != -1) {
    outputStream.write(buffer);
}

正确:

while ((len = inputStream.read(buffer)) != -1) {
    outputStream.write(buffer, 0, len);
}

假设:

buffer.length = 1024

最后一次只读取:

100 Byte

此时只有:

buffer[0] ~ buffer[99]

属于本轮新读取的数据。

因此只能:

write(buffer, 0, 100);

否则可能把数组中之前残留的数据也再次写出去。

所以一定记住这一组:

int len = inputStream.read(buffer);

outputStream.write(buffer, 0, len);

这几乎是字节流文件复制的经典结构。


5.3 文件复制不应该把二进制数据转成字符串

下面这种思路是错误的:

图片
 ↓
FileInputStream
 ↓
String
 ↓
FileOutputStream

例如:

byte[] data = inputStream.readAllBytes();

String text = new String(data);

outputStream.write(text.getBytes());

如果复制的是:

jpg
mp4
zip
pdf

这些数据根本不是普通文本。

经过:

字节 → 字符 → 字节

可能发生数据变化。

正确方式:

byte[]
 ↓
原样读
 ↓
原样写

即:

byte[] buffer = new byte[8192];

int len;

while ((len = inputStream.read(buffer)) != -1) {
    outputStream.write(buffer, 0, len);
}

六、常见问题

6.1 FileInputStream 读取不到文件

例如:

new FileInputStream("data.txt");

如果文件不存在,就无法建立读取通道。

常见原因:

文件不存在
路径写错
相对路径理解错误
当前工作目录与预期不同
没有访问权限

排查时首先检查:

File file = new File("data.txt");

System.out.println(file.getAbsolutePath());
System.out.println(file.exists());

6.2 FileOutputStream 会不会自动创建文件

如果:

new FileOutputStream("data.txt");

目标文件不存在,在满足文件系统条件时可以创建目标文件。

但是:

它不会自动创建不存在的父目录。

例如:

data/
    io/
        result.txt

如果:

data/io

目录根本不存在:

new FileOutputStream("data/io/result.txt");

就可能失败。

因此需要保证父目录已经存在。


6.3 为什么文件内容突然没了

检查是不是:

new FileOutputStream("data.txt");

默认采用:

覆盖模式

原文件内容会被截断。

如果业务需求是追加:

new FileOutputStream("data.txt", true);

6.4 为什么 read() 不能直接存进 byte

不要:

byte data = inputStream.read();

因为:

read()

返回:

int

而且需要使用:

-1

表示文件结束。

正确:

int data;

while ((data = inputStream.read()) != -1) {
    // ...
}

6.5 为什么最后一次读取出现重复数据

重点检查:

read(buffer)

返回的:

len

有没有使用。

错误:

new String(buffer);

正确:

new String(buffer, 0, len);

错误:

outputStream.write(buffer);

正确:

outputStream.write(buffer, 0, len);

6.6 readAllBytes() 能不能读取所有文件

从功能上来说,它可以读取输入流剩余的全部字节。

但不意味着:

所有文件都适合一次性读取到内存。

小文件:

几 KB
几 MB

通常问题不大。

非常大的文件:

几 GB
几十 GB

如果全部装进一个:

byte[]

会产生巨大的内存压力。

所以大文件通常使用:

read(byte[])

分批处理。


6.7 为什么使用字节流读取中文乱码

不是因为:

字节流不能读取中文

事实上字节流可以读取任何数据。

问题发生在:

字节 → 字符

的解码阶段。

典型原因:

一个中文字符被拆成多个字节
却逐字节强转成 char

或者

文件编码与解码字符集不一致

因此文本处理要注意:

正确的字符边界
+
正确的字符集

后续的字符流会进一步解决纯文本读写问题。


6.8 字节流和字符流到底怎么选

现阶段先记:

任意文件复制
图片 / 视频 / 音频 / ZIP / PDF
        ↓
字节流

而:

需要按照“字符”理解文本
读取 Java / TXT 等纯文本内容
        ↓
通常优先考虑字符流

更完整的字符流使用方式将在下一章学习。


6.9 资源为什么要关闭

文件流会占用操作系统相关资源。

因此使用完成后必须释放。

本章示例中采用了:

try (...) {
}

这种方式可以自动关闭资源。

例如:

try (
        InputStream inputStream =
                new FileInputStream("data.txt")
) {
    // 使用流
}

这是:

try-with-resources

本章只需要先知道:

IO 流用完必须释放。

关于:

close()
finally
AutoCloseable
try-with-resources

的完整机制,会在后续“资源释放与 try-with-resources”章节专门学习。


七、练习与验收

7.1 知识问答

  1. 什么是字节流?
  2. 为什么字节流能够处理所有类型的文件?
  3. Input 和 Output 是相对于谁而言的?
  4. InputStreamFileInputStream 是什么关系?
  5. OutputStreamFileOutputStream 是什么关系?
  6. FileInputStream 的主要作用是什么?
  7. FileOutputStream 的主要作用是什么?
  8. read() 每次读取多少数据?
  9. read() 为什么返回 int 而不是 byte
  10. read() 什么时候返回 -1
  11. read(byte[]) 返回的数字代表什么?
  12. 为什么不能认为 read(byte[]) 每次都会填满整个数组?
  13. readAllBytes() 适合什么场景?
  14. 为什么不推荐使用 readAllBytes() 读取特别大的文件?
  15. 字节流读取中文为什么可能出现乱码?
  16. FileOutputStream 默认是覆盖还是追加?
  17. 如何开启追加模式?
  18. write(int) 实际写出去多少个字节?
  19. write(byte[]) 的作用是什么?
  20. write(byte[], int, int) 三个参数分别表示什么?
  21. 为什么复制文件时必须使用本次读取到的 len
  22. 为什么复制图片、视频时不应该先转换成 String
  23. 为什么字节数组批量读写通常比单字节读写更加合适?
  24. FileOutputStream 可以自动创建不存在的父目录吗?
  25. 字节流使用完成后为什么需要释放?

7.2 代码阅读

阅读代码:

byte[] buffer = new byte[4];

int len;

while ((len = inputStream.read(buffer)) != -1) {
    outputStream.write(buffer, 0, len);
}

回答:

  1. buffer 的作用是什么?
  2. 每次最多读取多少个字节?
  3. len 表示什么?
  4. 为什么循环条件判断的是 != -1
  5. 如果最后一次只读取 2 个字节,len 是多少?
  6. 为什么不能改成:
outputStream.write(buffer);

阅读:

try (OutputStream outputStream =
             new FileOutputStream("data.txt", true)) {

    outputStream.write(65);
    outputStream.write(66);
    outputStream.write(67);
}

回答:

  1. 第二个参数 true 有什么作用?
  2. 如果 data.txt 原本已经存在,原内容是否会被删除?
  3. 三次 write() 分别写了多少字节?
  4. 如果按照常见字符编码查看文本文件,可能看到什么内容?

7.3 手写代码

任务一:单字节读取

使用:

FileInputStream

实现:

逐字节读取一个只包含英文字母和数字的文本文件

直到文件读取结束。

要求:

  • 必须使用 read()
  • 必须正确判断 -1

任务二:字节数组读取

使用:

byte[] buffer = new byte[1024];

循环读取文件。

要求:

  • 保存 read(buffer) 的返回值
  • 每次只处理本轮真正读取到的数据

任务三:文件写入

使用 FileOutputStream 向:

student.txt

写入:

name=LingXi
language=Java

要求:

  • 使用 UTF-8 编码
  • 两条数据分别占一行

任务四:追加日志

第一次运行程序写入:

程序启动

第二次运行后不能删除原内容,而应继续追加:

程序结束

自行选择正确的 FileOutputStream 构造器。


任务五:文件复制

定义:

public static void copy(String source,
                        String target)

要求:

  • 可以复制图片
  • 可以复制视频
  • 可以复制 PDF
  • 使用字节数组
  • 不允许先转换成字符串

7.4 Debug

下面的文件复制代码存在问题:

byte[] buffer = new byte[1024];

int len;

while ((len = inputStream.read(buffer)) != -1) {
    outputStream.write(buffer);
}

要求:

  1. 找出潜在问题。
  2. 解释为什么最后一轮读取尤其容易暴露问题。
  3. 修改为正确代码。

下面程序想在原文件末尾追加内容:

OutputStream outputStream =
        new FileOutputStream("log.txt");

outputStream.write(
        "程序运行完成".getBytes()
);

要求:

  1. 判断代码是否真正实现了追加。
  2. 如果没有,说明原因。
  3. 修改代码。

下面程序用于读取 UTF-8 中文:

int data;

while ((data = inputStream.read()) != -1) {
    System.out.print((char) data);
}

要求:

  1. 分析为什么英文可能正常而中文可能乱码。
  2. 从字符编码角度解释问题。
  3. 给出至少一种正确处理思路。

7.5 综合训练

实现一个:

通用文件复制工具

方法:

public static void copyFile(String source,
                            String target)

要求:

1. 使用 FileInputStream。
2. 使用 FileOutputStream。
3. 使用字节数组批量读取。
4. 正确处理每次读取的有效长度。
5. 可以复制图片、视频、PDF、ZIP 等文件。
6. 禁止把二进制文件转换成 String。
7. 使用完成后正确释放 IO 资源。

测试:

txt
jpg
mp3
mp4
pdf
zip

任选至少三种文件类型进行验证。

思考:

为什么完全相同的一套代码能够复制完全不同类型的文件?


7.6 本章验收

如果能够在不查资料的情况下完成下面这些任务,本章即可认为基本掌握:

  • 能画出 InputStream → FileInputStreamOutputStream → FileOutputStream 的关系。
  • 能解释 Input 和 Output 的方向。
  • 能手写 read() 循环。
  • 能手写 read(byte[]) 循环。
  • 能解释 len 为什么必须存在。
  • 能写出覆盖和追加两种 FileOutputStream
  • 能使用 write(byte[], 0, len)
  • 能独立完成任意文件复制。
  • 能解释为什么二进制文件不能随意转换成字符串。
  • 能解释字节流读取中文时乱码的真正原因。
  • 能判断什么时候使用 readAllBytes(),什么时候使用分批读取。

最终需要形成下面这条完整知识链:

磁盘中的文件
      ↓
本质都是字节
      ↓
FileInputStream
      ↓
read() / read(byte[])
      ↓
Java 内存
      ↓
FileOutputStream
      ↓
write()
      ↓
另一个磁盘文件

掌握这条链以后,后续的:

字符流
缓冲流
转换流
打印流
数据流
网络 Socket IO

都会建立在同一套 IO 思维之上。