Skip to content

Commit 191d01f

Browse files
committed
docs: update 计算机科学/面向对象设计与Java编程/8 Java 输入-输出(I-O)/8-4 Java 字节流与字符流的转换.md
1 parent 660a948 commit 191d01f

File tree

2 files changed

+217
-0
lines changed

2 files changed

+217
-0
lines changed
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
# 8.4 Java 字节流与字符流的转换
2+
3+
我们已经知道,Java 的 I/O 分为字节流和字符流。字节流操作原始的字节数据,而字符流操作经过编码和解码的字符数据。但在实际开发中,我们经常会遇到需要将这两种流进行转换的场景。
4+
5+
例如:
6+
7+
- 从文件中读取文本数据。文件本身是以字节形式存储的,我们需要用字符流来读取,这就需要将字节流转换为字符流。
8+
- 将文本数据写入网络连接。网络传输的是字节,但我们程序中处理的是字符串,需要将字符流转换为字节流。
9+
10+
为了解决这个问题,Java 提供了两个“**转换流**”(也称为“**桥接流**”):`InputStreamReader``OutputStreamWriter`。它们是字节流和字符流之间的桥梁。
11+
12+
## 8.4.1 为什么需要转换流?
13+
14+
根本原因在于**数据源和我们想要处理的数据类型不匹配**
15+
16+
- **字节流**是底层 I/O 的基础,无论是文件、网络套接字还是内存块,其原始形式都是字节序列。
17+
- **字符流**是为处理文本数据而设计的,它封装了复杂的**字符编码**转换过程。
18+
19+
当我们从一个字节源(如 `FileInputStream`)读取文本时,我们需要一个机制来将这些原始字节**解码**成我们能理解的字符。同样,当我们要将程序中的字符串写入一个字节目标(如 `FileOutputStream`)时,需要一个机制将这些字符**编码**成字节。
20+
21+
转换流正是扮演了这个角色。它们在内部处理字节到字符(或字符到字节)的转换,同时允许我们指定使用哪种**字符集**(如 `UTF-8`, `GBK` 等),从而确保文本数据的正确性,避免乱码。
22+
23+
## 8.4.2 `InputStreamReader`:字节输入流 → 字符输入流
24+
25+
`InputStreamReader` 是一个 `Reader`,它接收一个 `InputStream`(字节输入流)作为其底层数据源。它会从这个字节流中读取字节,并使用指定的字符集将其解码为字符。
26+
27+
**核心作用**:将字节输入流转换为字符输入流。
28+
29+
**构造方法:**
30+
31+
| 方法定义 | 功能 |
32+
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
33+
| `InputStreamReader(InputStream in)` | 创建一个使用平台默认字符集的 `InputStreamReader`**(不推荐,因为平台默认值可能不确定)** |
34+
| `InputStreamReader(InputStream in, String charsetName)` | 创建一个使用指定字符集的 `InputStreamReader`。例如 `"UTF-8"`, `"GBK"`**(推荐)** |
35+
| `InputStreamReader(InputStream in, Charset cs)` | 创建一个使用给定 `Charset` 对象的 `InputStreamReader`|
36+
| `InputStreamReader(InputStream in, CharsetDecoder dec)` | 创建一个使用给定字符集解码器的 `InputStreamReader`|
37+
38+
**示例:以 UTF-8 编码读取文件**
39+
40+
假设我们有一个以 `UTF-8` 编码保存的文本文件 `note.txt`
41+
42+
```java
43+
import java.io.FileInputStream;
44+
import java.io.InputStreamReader;
45+
import java.io.BufferedReader;
46+
import java.io.IOException;
47+
48+
public class InputStreamReaderExample {
49+
public static void main(String[] args) {
50+
String filePath = "note.txt";
51+
// 为了提高效率,通常会用 BufferedReader 包装 InputStreamReader
52+
try (FileInputStream fis = new FileInputStream(filePath);
53+
InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
54+
BufferedReader br = new BufferedReader(isr)) {
55+
56+
String line;
57+
System.out.println("以 UTF-8 编码读取文件内容:");
58+
while ((line = br.readLine()) != null) {
59+
System.out.println(line);
60+
}
61+
} catch (IOException e) {
62+
e.printStackTrace();
63+
}
64+
}
65+
}
66+
```
67+
68+
在这个例子中,数据流动的过程是:
69+
70+
1. `FileInputStream``note.txt` 文件中读取原始的**字节**
71+
2. `InputStreamReader``FileInputStream` 获取这些字节,并使用 `UTF-8` 字符集将它们**解码****字符**
72+
3. `BufferedReader``InputStreamReader` 获取字符,并进行缓冲,以提供高效的 `readLine()` 方法。
73+
74+
## 8.4.3 `OutputStreamWriter`:字符输出流 → 字节输出流
75+
76+
`OutputStreamWriter` 是一个 `Writer`,它接收一个 `OutputStream`(字节输出流)作为其底层数据目标。它会将程序中的字符根据指定的字符集**编码**为字节,然后写入到底层的字节流中。
77+
78+
**核心作用**:将字符输出流转换为字节输出流。
79+
80+
**构造方法:**
81+
82+
| 方法定义 | 功能 |
83+
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------ |
84+
| `OutputStreamWriter(OutputStream out)` | 创建一个使用平台默认字符集的 `OutputStreamWriter`**(不推荐)** |
85+
| `OutputStreamWriter(OutputStream out, String charsetName)` | 创建一个使用指定字符集的 `OutputStreamWriter`。例如 `"UTF-8"`, `"GBK"`**(推荐)** |
86+
| `OutputStreamWriter(OutputStream out, Charset cs)` | 创建一个使用给定 `Charset` 对象的 `OutputStreamWriter`|
87+
| `OutputStreamWriter(OutputStream out, CharsetEncoder enc)` | 创建一个使用给定字符集编码器的 `OutputStreamWriter`|
88+
89+
**示例:以 GBK 编码写入文件**
90+
91+
```java
92+
import java.io.FileOutputStream;
93+
import java.io.OutputStreamWriter;
94+
import java.io.BufferedWriter;
95+
import java.io.IOException;
96+
97+
public class OutputStreamWriterExample {
98+
public static void main(String[] args) {
99+
String filePath = "output_gbk.txt";
100+
// 同样,为了效率,用 BufferedWriter 包装 OutputStreamWriter
101+
try (FileOutputStream fos = new FileOutputStream(filePath);
102+
OutputStreamWriter osw = new OutputStreamWriter(fos, "GBK");
103+
BufferedWriter bw = new BufferedWriter(osw)) {
104+
105+
bw.write("你好,世界!");
106+
bw.newLine();
107+
bw.write("这段文字将以 GBK 编码保存。");
108+
109+
System.out.println("文件已成功以 GBK 编码写入。");
110+
111+
} catch (IOException e) {
112+
e.printStackTrace();
113+
}
114+
}
115+
}
116+
```
117+
118+
在这个例子中,数据流动的过程是:
119+
120+
1. 程序通过 `BufferedWriter` 写入**字符**(字符串)。
121+
2. `OutputStreamWriter``BufferedWriter` 接收这些字符,并使用 `GBK` 字符集将它们**编码****字节**
122+
3. `FileOutputStream` 接收这些字节,并将它们写入到 `output_gbk.txt` 文件中。
123+
124+
## 8.4.4 序列化与反序列化
125+
126+
除了字节流和字符流的转换,Java I/O 中还有一个重要的概念,即**对象序列化**。它允许我们将 Java 对象转换为字节序列,以便可以将其存储到文件、数据库或通过网络传输。之后,我们还可以将这个字节序列恢复为原始对象。
127+
128+
- **序列化 (Serialization)**:将对象转换为字节序列的过程。由 `ObjectOutputStream` 完成。
129+
- **反序列化 (Deserialization)**:将字节序列恢复为对象的过程。由 `ObjectInputStream` 完成。
130+
131+
### `ObjectOutputStream`:对象序列化
132+
133+
`ObjectOutputStream` 是一个过滤流,它可以将 Java 对象写入到底层 `OutputStream`
134+
135+
要使一个类的对象能够被序列化,该类必须实现 `java.io.Serializable` 接口。这个接口是一个**标记接口**,没有任何方法,只是用来标记该类的对象是可以被序列化的。
136+
137+
**示例:将对象写入文件**
138+
139+
```java
140+
import java.io.*;
141+
142+
// 必须实现 Serializable 接口
143+
class User implements Serializable {
144+
// 建议显式声明 serialVersionUID
145+
private static final long serialVersionUID = 1L;
146+
String name;
147+
transient String password; // transient 关键字标记的字段不会被序列化
148+
149+
public User(String name, String password) {
150+
this.name = name;
151+
this.password = password;
152+
}
153+
154+
@Override
155+
public String toString() {
156+
return "User{name='" + name + "', password='" + password + "'}";
157+
}
158+
}
159+
160+
public class ObjectSerializationExample {
161+
public static void main(String[] args) {
162+
User user = new User("Alice", "123456");
163+
164+
try (FileOutputStream fos = new FileOutputStream("user.dat");
165+
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
166+
167+
oos.writeObject(user); // 将 user 对象写入文件
168+
System.out.println("对象序列化成功!");
169+
170+
} catch (IOException e) {
171+
e.printStackTrace();
172+
}
173+
}
174+
}
175+
```
176+
177+
### `ObjectInputStream`:对象反序列化
178+
179+
`ObjectInputStream` 可以从底层 `InputStream` 读取通过 `ObjectOutputStream` 写入的数据和对象。
180+
181+
**示例:从文件中读取对象**
182+
183+
```java
184+
import java.io.*;
185+
186+
public class ObjectDeserializationExample {
187+
public static void main(String[] args) {
188+
try (FileInputStream fis = new FileInputStream("user.dat");
189+
ObjectInputStream ois = new ObjectInputStream(fis)) {
190+
191+
User user = (User) ois.readObject(); // 从文件读取对象
192+
System.out.println("对象反序列化成功!");
193+
System.out.println(user); // 输出:User{name='Alice', password='null'}
194+
195+
} catch (IOException | ClassNotFoundException e) {
196+
e.printStackTrace();
197+
}
198+
}
199+
}
200+
```
201+
202+
**重要注意事项:**
203+
204+
1. **`Serializable` 接口**:要序列化的类必须实现 `Serializable` 接口。
205+
2. **`serialVersionUID`**:强烈建议为可序列化的类显式声明一个 `serialVersionUID`。它用于在反序列化时验证发送方和接收方的类版本是否兼容。如果不指定,JVM 会自动生成一个,但它可能因编译器实现不同而异,导致意外的 `InvalidClassException`
206+
3. **`transient` 关键字**:如果某个字段不希望被序列化(例如密码、数据库连接等敏感或不可序列化的信息),可以使用 `transient` 关键字修饰它。反序列化后,该字段的值将为 `null`(对象类型)或默认值(基本类型)。
207+
4. **`ClassNotFoundException`**`readObject()` 方法可能会抛出 `ClassNotFoundException`,如果找不到序列化对象的类定义。
208+
209+
## 8.4.5 总结
210+
211+
1. **转换流是桥梁**`InputStreamReader``OutputStreamWriter` 是连接字节流和字符流的关键。
212+
2. **编码是核心**:使用转换流时,**必须**考虑并显式指定正确的字符编码,这是避免乱码问题的根本。
213+
3. **装饰器模式**:转换流是装饰器模式的又一个绝佳示例。它们包装了底层的字节流,并为其增加了编码/解码的功能。为了获得更好的性能,我们通常会进一步用 `BufferedReader`/`BufferedWriter` 来包装转换流。
214+
4. **标准实践**:读取文本文件的标准方式是 `new BufferedReader(new InputStreamReader(new FileInputStream(...), "UTF-8"))`
215+
5. **对象序列化**`ObjectOutputStream``ObjectInputStream` 提供了将 Java 对象与字节流相互转换的能力,是实现对象持久化和网络传输的基础。实现 `Serializable` 接口是对象可序列化的前提。

计算机科学/面向对象设计与Java编程/8 Java 输入-输出(I-O)/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@
55
### 8.2 [Java 字节流](8-2%20Java%20字节流)
66

77
### 8.3 [Java 字符流](8-3%20Java%20字符流)
8+
9+
### 8.4 [Java 字节流与字符流的转换](8-4%20Java%20字节流与字符流的转换)

0 commit comments

Comments
 (0)