|
| 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` 接口是对象可序列化的前提。 |
0 commit comments