---
url: /doc/Java/sgw1i185/index.md
---
## 17.1 文件

> 文件就是保存数据的地方。
>
> 文件流：文件 在 程序 中是以 流 的形式来操作的。
>
> 流：数据在数据源（文件）和程序（内存）之间经历的路径
>
> 输入流：数据从数据源到程序的路径
>
> 输出流：数据从程序到数据源的路径

### 17.1.1 常用的文件操作

> Java 提供了 File 类，用于处理文件相关的操作

1. 创建文件对象相关构造器和方法

   * `new File(String pathname)`：根据路径创建一个 File 对象

     > ```java
     > String path1 = "d:/test.jpg";
     > String path2 = "d:\\test.jpg";
     > File file1 = new File(path1);
     > File file2 = new File(path2);			//此时只是在内存中产生了一个对象
     > ```

   * `new File(File parent, String child)`：根据父目录文件 + 子路径构建

     > ```java
     > File parentFile1 = new File("d:\\");	//目录是特殊的文件
     > String fileName1 = "test.txt";
     > File file3 = new File(parentFile1, fileName1);
     > ```

   * `new File(String parent, String child)`：根据父路径 + 子路径构建

   * `creatNewFile()`：创建新文件

     > ```java
     > try {
     >  file.createNewFile();				//这个场合，内存对象才写入磁盘
     > } catch (IOException e) {
     >  e.printStackTrace();
     > }
     > ```

2. 获取文件相关信息

   * `getName()`：获取名称

   * \==`getAbsolutePath()`：获取文件绝对路径==

   * `getParent()`：获取文件父级目录

   * `long length()`：获取文件大小（字节）

   * `exists()`：文件是否存在，返回 boolean 类型。

   * `isFile()`：是不是一个文件

   * `isDirectory()`：是不是一个目录

   * `isAbsolute()`：是不是绝对路径

   * `canRead()`：是否可读

     `canWirte()`：是否可写

   * `long lastModified()`：最后修改时间

   * `String[] list()`：列出符合模式的文件名

3. 目录的操作和文件删除

   * `mkdir`：创建一级目录，返回 boolean 类型
   * `mkdirs`：创建多级目录
   * `delete`：删除空目录或文件
   * `boolean renameTo(File newName)`：更改文件名

   其实目录（在内存看来）就是特殊的文件

注意事项：

* File 类可以获取文件的各种相关属性，可以对其进行改名，甚至删除。但除了文件名外的属性没有修改方法
* File 类可以用来描述一个目录，但不能改变目录名，也不能删除目录

## 17.2 IO 流

1. I / O 是 Input / Output 的缩写。IO 技术是非常实用的技术，用于处理数据传输。如 读 / 写 文件，网络通讯等。
2. Java 程序中，对于数据的 输入 / 输出 操作以 “流（stream）”的方式进行
3. `java.io` 包下提供了各种 “流” 类和接口，用以获取不同种类的数据，并通过方法输入或输出数据
4. 输入（input）：读取外部数据（磁盘、光盘、网络数据等）到程序（内存）中
5. 输出（output）：将程序（内存）数据输出到外部存储

### 17.2.1 IO 流的分类

* 按操作数据单位不同分为：

  * 字节流（8 bit）：二进制文件用该方法，==能确保文件无损==
  * 字符流（按照字符，字符的字节数由编码决定）：文本文件，==效率更高==

* 按数据流的流向不同分为：

  * 输入流：读取外部数据（磁盘、光盘、网络数据等）到程序（内存）中
  * 输出流：将程序（内存）数据输出到外部存储(==不只是硬件存储设备，可以是数据库、网络等==)

* 按流的角色不同分为：

  * 节点流
  * 处理流 / 包装流

  | （抽象基类） | 字节流       | 字符流 |
  | ------------ | ------------ | ------ |
  | 输入流       | InputStream  | Reader |
  | 输出流       | OutputStream | Writer |

Java 的 IO 流 总共涉及 40 多个类，实际上都是上述 4 类的**抽象基类**(不能实例化)派生的

由这 4 个类派生的子类名称都是以其父类名作为子类名后缀

### 17.2.2 IO 流 常用类

#### 17.2.2.1 `FileInputStream`：文件字节输入流

* 构造器：

  > ```java
  > new FileInputStream(File file);				//通过一个 File 的路径指定创建
  > new FileInputStream(String path);			//通过一个路径指定创建
  > new FileInputStream(FileDescriptor fdObj);	//通过文件描述符创建
  > ```

* 方法：

  * \==`available()`：返回目前可以从流中读取的字节数==

    实际操作时，读取的字节数可能大于这个返回值

  * `close()`：关闭文件输入流，释放资源

  * `finalize()`：确保在不引用文件输入流时调用其 `close()` 方法

  * `getChannel()`：返回与此流有关的唯一的 `FileChannel` 对象

  * `getFD()`：返回描述符

  * `read()`：从该输入流中读取一个数据字节，==返回对应的 int 值。==

    如果没有输入可用，该方法会被阻止。返回 -1 的场合，说明到达文件的末尾。

    > ```java
    > File file = new File("d:\\test");
    > FileInputStream fileInputStream = null;
    > int read;
    > try {
    >  	fileInputStream = new FileInputStream(file);
    >  	while ((read = fileInputStream.read()) != -1){
    >     	 System.out.print((char) read);
    >  }
    > } catch (IOException e) {
    >  	e.printStackTrace();
    > } finally {
    >  try {
    >      	fileInputStream.close();
    >  } catch (IOException e) {
    >      	e.printStackTrace();
    >  	}
    > }										//真 TM 复杂。throw 了算了
    > ```
    >
    > 这个场合，效率较低

    `read(byte[] b)`：从该输入流中把最多 b.length 个字节的数据读入一个 byte 数组

    读取正常的场合，返回实际读取的字节数。

    > ```java
    > ...
    > byte[] b = new byte[8];					//一次读取 8 字节
    > try {
    >  fileInputStream = new FileInputStream(file);
    >  while ((read = fileInputStream.read(b)) != -1){
    >      System.out.print(new String(b, 0, read));
    >      								//这一句看不懂请看[12.2 - 4]
    >  }
    > catch
    > ...
    > finally
    > ...
    > ```

    `read(byte[] b, int off, int len)`：从该输入流中读取 len 字节数据，从数组下标 off 处起写入

  * `skip(long n)`：从该输入流中跳过并去丢弃 n 个字节的数据

  * `mark(int markArea)`：标记数据量的当前位置，并划出一个缓冲区。缓冲区大小至少为 markArea

    `reset()`：将输入流重新定位到对此流最后调用 `mark()` 方法时的位置

    `markSupported()`：测试数据流是否支持 `mark()` 和 `reset()` 操作

#### 17.2.2.2 `FileOutputStream`：文件字节输出流

* 构造器：

  > ```java
  > new FileOutputStream(File file);			//通过一个 File 的路径指定创建
  > new FileOutputStream(File file, boolean append);
  > 							//append = false，写入采用 覆盖原文件 方式
  > 							//append = true 的场合，写入采用 末尾追加 方式
  > new FileOutputStream(String path);			//通过一个路径指定创建
  > new FileOutputStream(String path, boolean append);
  > new FileOutputStream(FileDescriptor fdObj);	//通过文件描述符创建
  > ```

  * 覆盖

    * ```java
      //下1面这种情况也是覆盖，只是一次写入没有完成会造成追加的假象，每次重新写入时会再次将原来的覆盖掉。
      try{
         filewriter = new Filewirter(filepath)
      }catch{
          filewriter.write('H');
      	filewriter.write('e');
       	filewriter.write('l');
      }finally{
          ...//关闭流的操作
      }

      ```

* 方法：

  * `close()`：关闭文件输入流，释放资源

  * `flush()`：刷新此输出流并强制写出所有缓冲的输出字节，==对于字符流，一般写入的时候想要马上看到一般需要 flush()==

  * `finalize()`：确保在不引用文件输入流时调用其 `close()` 方法

  * `getChannel()`：返回与此流有关的唯一的 `FileChannel` 对象

  * `getFD()`：返回描述符

  * `write(byte[] b)`：将 b.length 个字节从指定 byte 数组写入此文件输出流

    > ```java
    > File file = new File("d:\\test1");
    > FileOutputStream fileOutputStream = null;
    > try {
    >  fileOutputStream = new FileOutputStream(file);
    >  									//此时，若文件不存在会被创建
    >  fileOutputStream.write('a');
    >  String str = "Melody";
    >  fileOutputStream.write(str.getBytes());
    > }
    > catch
    > ...
    > finally
    > ...
    > ```

    `write(byte[] b， int off, int len)`：将指定 byte 数组中下标 off 开始的 len 个字节写入此文件输出流

    `write(int b)`：将指定字节写入此文件输出流

#### #17.2.2.3 `FileReader`：文件字符输入流

> 与其他程序设计语言使用 ASCII 码不同，Java 使用 Unicode 码表示字符串和字符。ASCII 码的字符占用 1 字节，可以认为一个字符就是一个字节。但 Unicode 码用 2 字节表示 1 个字符，此时字符流和字节流就不相同。

* 构造器：

  > ```java
  > new FileRaeder(File file);
  > new FileRaeder(String string);
  > ```

* 方法：

  * `read()`：读取单个字符。
  * `read(char[])`：批量读取多个字符到数组。

#### #17.2.2.3 `FileWriter`：文件字符输出流

* 构造器：

  > ```java
  > new FileWriter(File path);
  > new FileWriter(String path2);
  > new FileWriter(File path3, boolean append);
  > new FileWriter(String path4, boolean append);
  > ```

* 方法：

  * `write(int)`：写入单个字符
  * `write(char[])`：写入指定数组
  * `write(char[], off, len)`：写入指定数组的指定部分
  * `write(string)`：写入字符串
  * `write(string, off, len)`：写入字符串的指定部分
  * `flush()`：刷新该流的缓冲。==如果没有执行，内容就不会写入文件==
  * \==`close()`：等于 `flush()` + 关闭==

\==使用 FileWriter 后，如果有没有执行 flush 或 close，将一直在内存中，而不会写入文件。==

**注意！`FileWriter` 使用后，必须关闭（close）或刷新（flush），否则无法真正写入**

#### #17.2.2.4 转换流 `InputStreamReader` 和 `OutputStreamWriter`

1. `InputStreamReader` 是 `Reader` 的子类。可以把 `InputStream`（字节流）转换成 `Reader`（字符流）
2. `OutputStreamWriter` 是 `Writer` 的子类。可以把 `OutputStream`（字节流）转换成 `Writer`（字符流）
3. 处理纯文本数据时，如果使用字符流效率更高，并能有效解决中文问题，建议将字节流转换成字符流。
4. 可以在使用时指定编码格式（UTF -8、GBK 等）

* 构造器

  > ```java
  > InputStreamReader isr = new InputStreamReader(fileInputStream, "UTF-8");
  > 										//传入 字节流 和 编码类型
  > BufferedReader br = new Bufferedreader(isr);
  > 										//用另一个处理流包装
  > ```

### 17.2.3 节点流和处理流

![image-20231020181607114](https://gitee.com/kilomi/pic-bed/raw/master/img/202310201816224.png)

1. 节点流：从一个特定数据源读写数据。
2. 处理流（包装流）：是 “连接” 在已存在的流（节点流或处理流）上，为程序提供更强大的读写功能。

#### 节点流和处理流的区别和联系

1. 节点流是 底层流 / 低级流。直接和数据源相接。
2. 处理流（包装流）包装节点流，既可以消除不同节点流的实现差异，也可以提供更方便的方法完成输入输出
3. \==处理流对**节点流**进行包装，使用了**修饰器设计模式**。不会直接与数据源相连(只是调用)==
4. 处理流的功能主要体现在

* 性能的提高：以增加缓冲的方式提高输入输出的效率
* 操作的便捷：处理流可能提供了一系列便捷方法来一次性输入大量数据，使用更加灵活方便

5. \==关闭时关闭外层流即可==

#### ==17.2.3.1 缓冲区流==

> 缓冲区流是一种包装流。缓冲区字节流有 BufferedInputStream 和 BufferedOutputStream；缓冲区字符流有 BufferedWriter 和 BufferedReader。他们是在数据流上加了一个缓冲区。读写数据时，数据以块为单位进入缓冲区，其后的读写操作则作用于缓冲区。
>
> 这种方式能降低不同硬件设备间的速度差异，提高 I/O 效率。

构造器：

```java
new BufferedReader(reader);					//传入一个 Reader，也可以时Reader的子类，相当于向上转型
new BufferedReader(reader, 1024);			//传入 Reader 并指定缓冲区大小
new BufferedWriter(writer);					//传入一个 Writer
new BufferedWriter(writer, 1024);			//传入 Writer 并指定缓冲区大小
											//追加还是覆盖，取决于 writer
```

方法：

* `bufferedReader.readLine()`：按行读取（不含换行符）。

  会返回一个字符串。返回 null 时，表示读取完毕。

  > ```java
  > String line;//readline()是拓展的按行读取文件的方法
  > while (line = bufferedReader.readLine() != null){
  >  ...
  > }
  > bufferedReader.close();//关闭外部处理流，在底层会关闭传入的结点流
  > ```

* `bufferedWriter.write(String str)`：插入字符串

* `bufferedWriter.newLine()`：插入一个（和系统相关的）换行

#### 17.2.3.2 数据数据流

> 除了字节或字节数组外，处理的数据还有其他类型。为解决此问题，可以使用 DataInputStream 和 DataOutputStream。它们允许通过数据流来读写 Java 基本类型，如布尔型（boolean）、浮点型（float）等

构造器：

```java
new DataInputStream(inputStream);
new DataOutputStream(outputStream);
```

方法：

* `byte readByte()`：读取下一个 byte

  `int readInt()`、`double readDouble()`、`String readUTF()`……

* `void writeByte(byte b)`：写入一个 byte

  `void writeInt(int n)`、`void writeUTF(String str)`……

  虽然有对字符串的读写方法，但应避免使用这些方法，转而使用字符输入/输出流。

#### ==17.2.3.3 对象流==

> 当我们保存数据时，同时也把 数据类型 或 对象 保存。
>
> 以上要求，就是能够将 基本数据类型 或 对象 进行 序列化·反序列化 操作

**序列化和反序列化**

1. 把对象转成字符序列的过程称为**序列化**。保存数据时，保存数据的值和数据类型
2. 把字符序列转成对象的过程称为反序列化。恢复数据时，恢复数据的值和数据类型
3. 需要让某个对象支持序列化机制，则必须让其类是 可序列化的。由此，该类必须实现下列接口之一
   * \==`Serializable`：推荐。因为是标记接口，没有方法==
   * `Externalizable`：该接口有方法需要实现，一般使用 Serializable 接口

**transient 关键字**

1. 有一些对象状态不具有可持久性（如 Thread 对象或流对象），这样的成员变量必须用 transient 关键字标明。任何标有 transient 关键字的成员变量都不会被保存。
2. 一些需要保密的数据，不应保存在永久介质中。为保证安全，这些变量前应加上 transient 关键字。

* 构造器：

  > ```java
  > new ObjectInputStream(InputStream inputStream);
  > new ObjectOutputStream(OutputStream outputStream);
  > ```

* 方法：

  反序列化顺序需要和序列化顺序一致，否则出现异常。

  * `writeInt(Integer)`：写入一个 int

    `readInt()`：读取一个 int

  * `writeBoolean(Boolaen)`：写入一个 boolean

    `readBoolean()`：读取一个 boolean

  * `writeChar(Character)`：写入一个 char

    `readChar()`：读取一个 char

  * `writeDouble(Double)`：写入一个 double

    `readDouble()`：读取一个 double

  * `writeUTF(String)`：写入一个 String

    `readUTF()`：读取一个 String

  * `writeObject(Serializable)`：写入一个 Obj

    `readObject()`：读取一个 Obj

    读取的场合，如果想要调用方法，需要向下转型。

    为此，==需要该类其引入。该类必须是公共的可访问的类==

* \==**注意事项**==

  1. \==读写顺序要一致,(序列化时的写入顺序和反序列化的读取顺序)==

  2. 实现序列化或反序列化的对象，要实现 `Serializable` 或 `Externalizable` 接口

  3. 序列化的类中建议添加 `SerialVersionUID` 以提高版本兼容性

     > ```java
     > private static final long serialVersionUID = 1L;//序列化的版本号
     > ```

  有此序列号的场合，后续修改该类，==系统会认为只是版本修改，而非新的类==

4. 序列化对象时，默认将其中所有属性进行序列化（除了 `static` 和 `tansient` 修饰的成员）

5. \==序列化对象时，要求其属性(尤其是当属性为自定义类型的时候)也实现序列化接口==

6. 序列化具备可继承性。某类若实现可序列化，则其子类也可序列化

**序列化 Dog 对象**

```java
		//序列化：
        FileOutputStream fileOutputStream = new FileOutputStream("f:\\dog.dat");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(dog);
        objectOutputStream.close();
		//反序列化：
        FileInputStream fileInputStream = new FileInputStream("f:\\dog.dat");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        try {
            Dog dog1 = (Dog)objectInputStream.readObject();//这里回返回一个Object，可以强转为需要的类型。抛出一个异常。
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
		objectInputStream.close();
```

#### ==17.2.3.4 标准输入 / 输出流==

| Σ( ° △ °lll）            | 编译类型      | 运行类型              | 默认设备 |
| ------------------------ | ------------- | --------------------- | -------- |
| `System.in`：标准输入流  | `InputStream` | `BufferedInputStream` | 键盘     |
| `System.out`：标准输出流 | `PaintStream` | `PaintStream`         | 显示器   |

#### #17.2.3.5 打印流 `PrintStream` 和 `PrintWriter`

> 打印流只有输出流，没有输入流

1. `PrintStream` 是 `OutputStream` 的子类。`PrintWriter` 是 `Writer` 的子类。

2. 默认情况下，`System.out` 输出位置是 标准输出（即：显示器）

   修改默认输出位置：

   > ```java
   > System.setOut(new PrintStream(path));
   > System.out.println("hello");//这次会输出到path的文件中
   > ```

```java
Printwriter printwriter =new PrintWriter(System.out);//System.out是一个PrintStream流
printwriter.print("hello");//打印到控制台

```

#### ==17.2.3.6 `Properties` 类==

1. `Properties` 是专门用于读写配置文件的集合类

   底层维护了一个 `Entry` 数组

2. 配置文件格式：

   ```
   键=值
   键=值
   …ABNF
   ```

   **注意：键值对不需要空格，值不需要引号（值默认 `String`）**

3. 常见方法：

   * `load(InputStream)`

     `load(Reader)`：加载配置文件的键值对到 `Properties` 对象

     > ```java
     > Properties properties = new Properties();
     > properties.load(new FileReader("d:\\data.data"));
     > ```

   * `list(PrintStream)`

     `list(PrintWriter)`：将数据显示到指定设备

     > ```java
     > properties.list(System.out);			//在控制台显示
     > ```

   * `getProperty(key)`：根据键获取值

     > ```java
     > properties.get("IQ");
     > ```

   * `setProperty(key, value)`：设置键值对到 `Properties` 对象

     如果没有该 key，就是创建。如有，就是替换。

     > ```java
     > properties.set("IQ", 0);
     > properties.set("Balance", 0);
     > ```

   * `store(Writer, String)`

     `store(OutputStream, String)`：把 `Properties` 中的键值对存储到配置文件。

     后面的 `String` 是注释。如有，会被用 `#` 标记并写在文件最上方。注释可以为 null。

     IDEA 中，如果含有中文，会储存为 unicode 码

   **读取配置文件创建对象。**

   1. ```java
      public class Homework03 {
          public static void main(String[] args) throws IOException {
              Properties properties = new Properties();
              properties.load(new FileReader("src\\dog.properties"));
              String name =  properties.get("name")+"";//任何对象和字符串相加都会转换为字符串
              int age = Integer.parseInt(properties.get("age")+"");
              String color =  properties.get("color")+"";
              //读取配置文件信息创建对象
              Dog dog = new Dog(name, age, color);
              System.out.println(dog);
          }
      }

      class Dog implements Serializable {
          private String name;
          private int age;
          private String color;

          public Dog(String name, int age, String color) {
              this.name = name;
              this.age = age;
              this.color = color;
          }
      	//实现了tostring方法和getter、setter方法
          ...
      }
      ```

   [查询 unicode 码](http://tool.chinaz.com/tools/unicode.aspx)

#### #17.2.3.7 随机访问文件

> 程序阅读文件时不仅要从头读到尾，还要实现每次在不同位置进行读取。此时可以使用 RandomAccessFile

构造器：

```
new RandomAccessFile(String name, String mode);		//通过文件名
new RandomAccessFile(File file, String mode);		//通过文件对象JAVA
```

> 参数 mode 决定以只读方式 `mode = "r"` 还是读写方式 `mode = "rw"` 访问文件。

方法：

* `long getFilePointer()`：返回文档指针的当前位置

* `void seek(long pos)`：将文档指针置于指定的绝对位置 pos

  文档指针的位置从文档开始的字符处开始计算，`pos = 0L` 表示文档的开始

* `long length()`：返回文件长度

#### ==17.2.3.8 节点流和处理流复制文件：==

##### 节点流复制

```java
 @Test
    public void copy(){
        FileInputStream fileInputStream = null;//扩大作用域，可以在finally中关闭流
        FileOutputStream outputStream = null;
        String filepath = "f:\\k-on.png";	//复制图片
        String despath = "f:\\k-on2.png";
        byte[] buffer = new byte[1024];
        int readlen = 0;
        try {
            fileInputStream = new FileInputStream(filepath);
            outputStream = new FileOutputStream(despath);
            while ((readlen = fileInputStream.read(buffer))!=-1){
                outputStream.write(buffer,0,readlen);//这里必须用这个形式，如果只传一个buffer，
                // 那么每次读完小于1024的数据时下次读取会导致数据丢失，因为上一次数据的残留数据还在buffer数组的末尾。
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if(fileInputStream!=null) {
                    fileInputStream.close();
                }
                if(outputStream!=null){
                    outputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
```

##### 处理流复制

```java
/**
 *处理二进制文件，也可以处理文本文件，相比使用节点流处理文本文件在控制台输出不会报乱码。
 **/
@Test
public void copy_2(){
    String srcpath = "f:\\hello.txt";
    String despath = "f:\\hello2.txt";
    BufferedInputStream bufferedInputStream =null;
    BufferedOutputStream bufferedOutputStream = null;
    byte[] buffer = new byte[1024];
    int readlen =0;
    try {
        bufferedInputStream = new BufferedInputStream(new FileInputStream(srcpath));
        bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(despath));
        while ((readlen=bufferedInputStream.read(buffer))!=-1){
            bufferedOutputStream.write(buffer,0,readlen);
            System.out.println(new String(buffer,0,readlen));
        }
        System.out.println("文件复制成功...");
    } catch (IOException e) {
        e.printStackTrace();
    }finally {
        try {
            if(bufferedInputStream!=null){
                bufferedInputStream.close();
            }
            if (bufferedOutputStream!=null){
                bufferedOutputStream.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}
```

```java
/**
     * 处理流复制文件,不要处理二进制文件，可能造成文件损坏
     */
    @Test
    public void copy(){
        BufferedReader br=null;
        BufferedWriter bw=null;
        String srcPath = "f:\\hello.txt";
        String desPath = "f:\\hello3.txt";
        String line;					//文本行数
        try {
            br = new BufferedReader(new FileReader(srcPath));
            bw = new BufferedWriter(new FileWriter(desPath));
            while ((line=br.readLine())!=null){
                bw.write(line);
                bw.newLine();//插入换行，否则都将在同一行
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if(br!=null) {
                    br.close();
                }
                if(bw!=null) {
                    bw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
```

> `if (br != null)` 的目的是检查 `BufferedReader` 对象 `br` 是否已经被成功初始化，因为在 `try` 块内部的 `br` 对象可能会在打开文件时发生异常而变为 `null`。如果 `br` 没有被初始化，调用 `br.close()` 就会导致 `NullPointerException` 异常。因此，在 `finally` 块中使用 `if (br != null)` 来检查 `br` 是否非空，以确保在关闭文件之前先检查它是否已经初始化。
>
> 同样的逻辑也适用于 `bw.close()`，为了代码的健壮性，通常会在关闭资源之前检查资源是否已经被成功初始化。这是一种防御性编程的做法，有助于避免潜在的异常情况。
