字节输入流与字节输出流 | 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 知识问答
- 什么是字节流?
- 为什么字节流能够处理所有类型的文件?
- Input 和 Output 是相对于谁而言的?
InputStream与FileInputStream是什么关系?OutputStream与FileOutputStream是什么关系?FileInputStream的主要作用是什么?FileOutputStream的主要作用是什么?read()每次读取多少数据?read()为什么返回int而不是byte?read()什么时候返回-1?read(byte[])返回的数字代表什么?- 为什么不能认为
read(byte[])每次都会填满整个数组? readAllBytes()适合什么场景?- 为什么不推荐使用
readAllBytes()读取特别大的文件? - 字节流读取中文为什么可能出现乱码?
FileOutputStream默认是覆盖还是追加?- 如何开启追加模式?
write(int)实际写出去多少个字节?write(byte[])的作用是什么?write(byte[], int, int)三个参数分别表示什么?- 为什么复制文件时必须使用本次读取到的
len? - 为什么复制图片、视频时不应该先转换成
String? - 为什么字节数组批量读写通常比单字节读写更加合适?
- FileOutputStream 可以自动创建不存在的父目录吗?
- 字节流使用完成后为什么需要释放?
7.2 代码阅读
阅读代码:
byte[] buffer = new byte[4];
int len;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
回答:
buffer的作用是什么?- 每次最多读取多少个字节?
len表示什么?- 为什么循环条件判断的是
!= -1? - 如果最后一次只读取 2 个字节,
len是多少? - 为什么不能改成:
outputStream.write(buffer);
阅读:
try (OutputStream outputStream =
new FileOutputStream("data.txt", true)) {
outputStream.write(65);
outputStream.write(66);
outputStream.write(67);
}
回答:
- 第二个参数
true有什么作用? - 如果
data.txt原本已经存在,原内容是否会被删除? - 三次
write()分别写了多少字节? - 如果按照常见字符编码查看文本文件,可能看到什么内容?
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);
}
要求:
- 找出潜在问题。
- 解释为什么最后一轮读取尤其容易暴露问题。
- 修改为正确代码。
下面程序想在原文件末尾追加内容:
OutputStream outputStream =
new FileOutputStream("log.txt");
outputStream.write(
"程序运行完成".getBytes()
);
要求:
- 判断代码是否真正实现了追加。
- 如果没有,说明原因。
- 修改代码。
下面程序用于读取 UTF-8 中文:
int data;
while ((data = inputStream.read()) != -1) {
System.out.print((char) data);
}
要求:
- 分析为什么英文可能正常而中文可能乱码。
- 从字符编码角度解释问题。
- 给出至少一种正确处理思路。
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 → FileInputStream、OutputStream → FileOutputStream的关系。 - 能解释 Input 和 Output 的方向。
- 能手写
read()循环。 - 能手写
read(byte[])循环。 - 能解释
len为什么必须存在。 - 能写出覆盖和追加两种
FileOutputStream。 - 能使用
write(byte[], 0, len)。 - 能独立完成任意文件复制。
- 能解释为什么二进制文件不能随意转换成字符串。
- 能解释字节流读取中文时乱码的真正原因。
- 能判断什么时候使用
readAllBytes(),什么时候使用分批读取。
最终需要形成下面这条完整知识链:
磁盘中的文件
↓
本质都是字节
↓
FileInputStream
↓
read() / read(byte[])
↓
Java 内存
↓
FileOutputStream
↓
write()
↓
另一个磁盘文件
掌握这条链以后,后续的:
字符流
缓冲流
转换流
打印流
数据流
网络 Socket IO
都会建立在同一套 IO 思维之上。