1. 需掌握的 15 个 IO 流
1.1. 节点流
直接绑定文件路径。
名称 | 描述 |
---|---|
FileInputerStream | 文件的字节输入流 |
FileOutputStream | 文件的字节输出流 |
FileReader | 文件的字符输入流 |
FileWriter | 文件的字符输出流 |
1.2. 缓存流
又称处理流,通过修饰器模式实现,是对节点流的包装,起到增强作用。
名称 | 描述 |
---|---|
BufferedReader | 带缓冲的字符输入流 |
BufferedWriter | 带缓冲的字符输出流 |
BufferedInputStream | 带缓冲的字节输入流 |
BufferedOutputStream | 带缓冲的字节输出流 |
1.3. 转换流
可以见字节流转换成字符流,常用于处理字节编码问题。
名称 | 描述 |
---|---|
InputStreamReader | 转换输入流,将字节流转换成字符流 |
OutputStreamWriter | 转换输出流,将字节流转换成字符流 |
1.4. 对象流
用于序列化和反序列化对象。
名称 | 描述 |
---|---|
ObjectOutputStream | 对象输出流 |
ObjectInputStream | 对象输入流 |
1.5. 打印流
名称 | 描述 |
---|---|
PrintStream | 字节打印流 |
1.6. 数据流
名称 | 描述 |
---|---|
DataInputStream | 数据输入流 |
DataOutputStream | 数据输出流 |
2. 15 个 IO 流的具体使用
2.1. FileInputStream & FileOutputStream
FileInputStream & FileOutputStream 可以说是万能输入输出流(字节流),其又被称之为 节点流(有直接的数据源-文件路径)。它们可以操作各种文件格式,下面使用 FileInputStream & FileOutputStream 完成对文件的复制:
public class Client {
public static void main(String[] args) {
try (
// 使用try-with-resources,可以自动关闭实现了 AutoCloseable 或 Closeable 接口的资源
FileInputStream fileInputStream = new FileInputStream("file.txt");
FileOutputStream fileOutputStream = new FileOutputStream("file_copy.txt", true)
) {
// 记录每次读取的数量
int count;
// 存放读取数据的容器
byte[] container = new byte[12];
// 读取数据,直到读完为止
while ((count = fileInputStream.read(container)) != -1) {
// 将读取的数据进行写入
fileOutputStream.write(container, 0, count);
}
// 将缓冲器的数据立即写入输出流,并情况缓冲区
fileOutputStream.flush();
System.out.println("OK");
} catch (Exception e) {
e.printStackTrace();
}
}
}
通过上面的例子不难发现对于输入流而言,其需要能定位出目标文件,可以通过 文件路径
进行直接定位,或者同步 File
进行定位,一般而言,我们在对文件进行操作之前会先判断文件是否存在,或者删除已经存在的文件,那么此时我们会用到 File
对象对目标文件进行一系列的操作。
对于 FileInputStream
而言,我们需要定义一个读取数据的容器,容器的大小就是每次读取数据的最大数据量。
2.2. FileReader & FileWriter
FileReader & FileWriter 为文件字符流,其特点是以 char (字符) 为单位进行数据的读写,字符流对文本文件比较友好,可以直接操作字符集和编码(在后端的缓冲流中会有所体现)。其又被称之为 节点流(有直接的数据源-文件路径)。
public class Client {
public static void main(String[] args) {
try (
FileReader fileReader = new FileReader("file.txt");
FileWriter fileWriter = new FileWriter("file_copy.txt", true)
){
int count;
char[] container = new char[12];
while ((count = fileReader.read(container)) != -1) {
fileWriter.write(container, 0, count);
}
fileWriter.flush();
System.out.println("OK");
} catch (Exception e){
e.printStackTrace();
}
}
}
2.3. BufferedInputStream & BufferedOutputStream
字节缓冲流(又称处理流)是对字节节点流进行了封装。一方面,缓冲流在读写数据时会先将数据存储在内部缓冲区中,减少了频繁的系统调用,从而提高了读写性能;另一方面,缓冲流作为对接点流的包装,提供了一些高级方法,简化了操作的复杂性。
下面是使用字节缓冲流实现对文件的复制
public class Client {
public static void main(String[] args) {
try (
// 定义字节缓存输入流
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("file.txt"));
// 定义字节缓存输出流
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream("file_copy.txt"))
) {
byte[] buffer = new byte[1024];
int bytesRead;
// 读取数据
while ((bytesRead = bufferedInputStream.read(buffer)) != -1) {
// 写入数据
bufferedOutputStream.write(buffer, 0, bytesRead);
}
// 刷新缓冲区
bufferedOutputStream.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
上面的演示有一对缓存流和一对字节流,存在 4 个流,如何不使用 try-with-resources
语法,那么我们需要如何关闭流呢,需要将4个流都统一关闭吗?答案时否定的,我们只需要关闭最外层的缓冲流即可。当我们调用缓冲流的 close 方法时,其内部会间接的调用其包装的字节流的 close 方法。
2.4. BufferedReader& BufferedWriter
字符缓冲流(又称处理流)是对字符节点流进行了封装。一方面,缓冲流在读写数据时会先将数据存储在内部缓冲区中,减少了频繁的系统调用,从而提高了读写性能;另一方面,缓冲流作为对接点流的包装,提供了一些高级方法,简化了操作的复杂性。
下面是使用字符缓冲流实现对文件的复制
public class Client {
public static void main(String[] args) {
try (
// 定义字符缓冲输入流,其是对字符输入流的包装
BufferedReader bufferedReader = new BufferedReader(new FileReader("file.txt"));
// 定义字符缓冲输出流,其是对字符输出流的包装
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("file_copy.txt", true))
) {
String line;
// 去取数据
while ((line = bufferedReader.readLine()) != null) {
// 写入数据
bufferedWriter.write(line);
// 换行
bufferedWriter.newLine();
}
// 刷新缓存区
bufferedWriter.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.5. InputStreamReader & OutputStreamWriter
InputStreamReader & InputStreamWriter 为转换流,可以方便的将字节流转成成字符流,从其命名上我们也能看出 InputStream/OutputStream 为字节流,Reader/Writer 为字符流。
输入字节流转输出字符流,一般而言我们会在文件编码存在问题的场景下会使用这种模式,通过使用转换流,我们可以将字节流进行合适转码,我们知道,字节输入输出流模式的编码使用的是 UTF8 ,这就意味着直接使用字节流读取编码为 GBK 的文件会出现中文乱码。
下面我将举一个例子,显示如何使用字节输入流读取文件,并使用转换流指定编码格式。
public class Client {
public static void main(String[] args) {
try (
// 通过字节流定位捕获文件
FileInputStream fileInputStream = new FileInputStream("file.txt");
// 将字节流转换成固定编码格式的字符流
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
// 使用处理流包装一下上面的字符流
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
) {
// 正常的读取
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (Exception ignored) {}
}
}
下面我将举一个例子,演示如何将输出字节流转成输出字符流,并指定输出内容的字符集
public class Client {
public static void main(String[] args) {
try (
// 获取输出字节流
FileOutputStream fileOutputStream = new FileOutputStream("file.txt");
// 将输出字节流转成输出字符流,并指定字符集
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8);
// 利用处理流(缓冲流)包装一个上面的字节流
BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
) {
// 进行文件写入
bufferedWriter.write("BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);");
bufferedWriter.flush();
} catch (Exception ignore) {}
}
}
2.6. ObjectInputStream & ObjectOutputStream
对象输入输出流是对数据进行序列化和反序列化的,所谓的序列化就是在保存数据时,保存了数据的值和类型;反序列化就是在恢复数据时,恢复了数据的值和类型。
下面的 Person 类就是我们需要尽心序列化的对象,通过观察这个对象,我们不难发现该对象存在两个特点 实现了Serializable
和 指定了一个属性 serialVersionUID
。
在Java中规定,只有实现了 Serializable 接口的类才可以被实例化,所以Serializable 的目的就是标记该类可以被实例化。 有人可能会问,“不对呀,int、float 等基本数据类型也可以被序列化呢 ?”
是的,在 Java 中基本数据类型也可以被实例化,这是因为 Java 中的基本数据类型是存在包装类的,Java 中的包装类都直接或间接实现了 Serializable 接口。
对支持序列化的类指定 serialVersionUID 是为了反序列化时提高版本的兼容性。在类的结构发生变化(例如添加新字段、修改字段类型等)后,如果没有显式指定 serialVersionUID ,Java会根据类的结构自动生成一个 serialVersionUID 。而手动指定一个固定的 serialVersionUID 可以保证即使类的结构发生变化,该值也保持不变,从而确保序列化兼容性,避免在反序列化时出现 InvalidClassException。
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
下面我们将使用对象输出流实现对对象的序列化,并使用对象输入流实现对数据的反序列化。
public class Client {
public static void main(String[] args) {
// 实例化一个 Person 对象
Person person = new Person("寒江", 18);
try(
// 初始化对象输出流,实现对对象的序列化
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("Person.ser"))
) {
objectOutputStream.writeObject(person);
objectOutputStream.flush();
} catch (Exception e) {
e.printStackTrace();
}
try(
// 初始化对象输入流,实现对数据的反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file))
) {
Person deserialPerson = (Person) objectInputStream.readObject();
System.out.println("Deserial person: " + deserialPerson);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.7. PrintStream
在 Java 中,我们可以使用 system.out.printf
和 system.out.println
等向控制台打印文本数据,我们可以改变 System.out
中的输出流实现将 system.out.printf
和 system.out.println
等的出处对象从数据台改为指定文件。
public class Client {
public static void main(String[] args) throws Exception{
// 保存原始的标准输出流
PrintStream originalOut = System.out;
// 改变标准输出流的输出方向,标准输出流不再指向控制台,指向“data.log”文件。
try (PrintStream printStream = new PrintStream(new FileOutputStream("data.log", true))) {
// 修改输出方向,将输出方向修改到"data.log"文件。
System.setOut(printStream);
// 输出到data.log文件中
System.out.printf("[%s] 打印了一条记录\n", LocalDateTime.now());
System.out.printf("[%s] 打印了一条记录\n", LocalDateTime.now());
System.out.printf("[%s] 打印了一条记录\n", LocalDateTime.now());
} catch (Exception e) {
e.printStackTrace();
}
// 将标准输出重新设置为控制台输出
System.setOut(originalOut);
// 输出到控制台
System.out.println("OK");
}
}
2.8. DataInputStream & DataOutputStream
DataInputStream 和 DataOutputStream 是 Java 中用于读写基本数据类型(如整数、浮点数、字符等)的类。
从上面的描述中我们可以得出该类输入输出流的最直接的作用就是实现对数据的序列化和反序列化。
下面我们演示文件的写入与读取:
public class Client {
public static void main(String[] args) {
File file = new File("data.bin");
// 初始化文件
if (!file.exists()) {
// 初始化一个数据输出流
try (DataOutputStream dataOutputStream = new DataOutputStream(new FileOutputStream(file))) {
// 写入整型
dataOutputStream.writeInt(20240301);
// 写入字符串
dataOutputStream.writeUTF("晴天,3度");
dataOutputStream.writeInt(20240302);
dataOutputStream.writeUTF("晴天,8度");
dataOutputStream.writeInt(20240303);
dataOutputStream.writeUTF("阴天,-3度");
} catch (Exception e) {
e.printStackTrace();
}
}
try (DataInputStream dataInputStream = new DataInputStream(new FileInputStream(file))) {
// 这里我们使用自旋实现读尽目标文件中的数据,读不到数据将会抛出异常
while (true) {
// 读取和写入的顺序需要保持一致
// 读取整型
int time = dataInputStream.readInt();
// 读取字符串
String info = dataInputStream.readUTF();
System.out.println(time);
System.out.println(info);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
3. 总结
从上面的介绍我们不难发现,IO 流的使用并不复杂,重点在于我们在什么场景下使用什么流。上面介绍的 15 个 IO 流大概可以分为如下的 3 大模块 6 种类型。
输入流 & 输出流
输入流:xxxInputStream 、xxxReader
输出流:xxxOutputStream 、xxxWriter
字节流 & 字符流
字节流:xxxStream
字符流:xxxReader、xxxWriter
节点流 & 处理流
节点流:FileInputStream、FileOutputStream、FileReader、FileWriter
处理流:其他
对于纯文本的读写,推荐使用字符流。推荐使用对应的缓冲流,提高读写效率。
对于二进制文件的读写,推荐使用字节流。推荐使用对应的缓冲流,提高读写效率。
对于具有编码问题的文件的读写推荐使用处理流中的转换流。推荐使用对应的缓冲流,提高读写效率。
对于有序列化&反序列化场景的推荐使用处理流中的 ObjectInputStream & ObjectOutputStream。