【Java】I/O 操作详解

【Java】I/O 操作详解

📃个人主页island1314

⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞

【Java】I/O 操作详解

目录

1. 引言 🚀

2. File 类 📕

2.1 创建 File 对象

2.2 File 类的常用方法

2.3 遍历目录下的文件

2.4 删除文件及目录

3. 字节流 💦

3.1 基本概念

3.2 字节流读文件

3.3 字节流写文件

3.4 字节流复制文件

4. 字符流 💧

4.1 字符流定义及基本用法

4.2 字符流读文件

4.3 字符流写文件

4.4 数据编码解码问题

5. 转换流 🖊

6. 缓冲流 🔍

6.1 字节缓冲流

6.2 字符缓冲流

7. 序列化反序列化 🔖

8. 小结 📖


1. 引言 🚀

💥I/O 操作主要是指 使用 Java 程序完成输入(Input)、输出(Output) 操作。输入是指将文件内容以数据流的形式读入内存,输出是指通过 Java 程序将内容中的数据写入文件,输入输出操作在实际开发中比较广泛。

  1. IO:输入/输出(Input/Output)
  2. 流:是一种抽象概念,是对数据传输的总称.也就是说数据在设备间的传输称为流,流的本质是数据传输
  3. IO流就是用来处理设备间数据传输问题的.常见的应用: 文件复制; 文件上传; 文件下载

IO流的分类:

(1)按照数据的流向

  • 输入流:读数据
  • 输出流:写数据

(2)按照数据类型来分:

  1. 字节流
    • 字节输入流
    • 字节输出流
  2. 字符流
    • 字符输入流
    • 字符输出流

IO流的使用场景

  1. 如果操作的是纯文本文件,优先使用字符流
  2. 如果操作的是图片、视频、音频等二进制文件,优先使用字节流
  3. 如果不确定文件类型,优先使用字节流,字节流是万能的流

2. File 类 📕

java.io 包中的 *File 类 是唯一一个可以代表磁盘文件的对象,它定义了一些用于操作文件的方法。通过调用 File 类 提供的各种方法,可以创建、删除或者重命名文件,判断硬盘上某个文件是否存在,查询文件最后修改时间,等等。本节将针对File 类* 进行详细讲解。创建File对象

2.1 创建 File 对象

*File 类 提供了多个构造方法用于创建 File 对象。File 类* 的常用构造方法如下所示:

方法声明 功能描述
FILE(String pathname) 通过指定的 一个字符串类型的文件路径 创建一个 FILE 对象
FILE(String parent, String child) 根据指定的 一个字符串类型的父路径和一个字符串类型的子路径 (包括文件名称)创建一个 FILE 对象
FILE (FILE parent, String child) 根据指定的 一个FILE 类的父路径和一个字符串类型的子路径 (包括文件名称)创建一个 FILE 对象

所有的构造方法都需要传入文件路径,那么我们应该如何去用呢?

  • 如果程序只处理一个目录和文件,并且知道该目录或文件的路径,就建议使用 构造 1
  • 如果程序处理的是一个公共目录中的若干子目录或文件,就建议使用 构造 2 和 构造 3

案例:

```java
public static void main(String[] args) {
    // 方法一:通过绝对路径创建
    File f1 = new File("D:\\file\\a.txt");
    // 方法二:通过相对路径创建
    File f2 = new File("src\\Hello.txt");
}
```


**注:** 目录符号(\) 用 \\ 表示,因为 \ 在Java中是特殊字符,具有转义作用,因此用 \\ 表示,此外我们也可以用 / 来作目录符号。

2.2 File 类的常用方法

方法声明 功能描述
*boolean exists()* 判定 File 对象对应的文件或目录是否存在
*boolean delete()* 删除 File 对象对应的文件或目录
*boolean createNewFile()* 当 File 对象对应文件不存在时则创建新文件,并且将新建的 File 对象指向新文件
*String getName()* 返回 File 对象 表示的文件或目录的名称
*String getPath()* 返回 File 对象 表示的文件或目录的路径
*String getAbsolutePath()* 返回 File 对象 表示的文件或目录的相对路径
  • 在 UNIX / Linux 等系统上,如果路径以 斜线 / 开始,则这个路径为绝对路径
  • 在 Widows 等系统上,如果路径以 盘符 开始,则这个路径为绝对路径

*String getParentFile()*| 返回 File 对象 对应目录的父目录(注:返回的目录不包含最后一级子目录)
boolean canRead()| 判定 File 对象对应的文件或目录是否可读
boolean canWrite()| 判定 File 对象对应的文件或目录是否可写
boolean isFile()| 判断 File 对象对应的是否是文件
boolean isDirectory()| 判断 File 对象对应的是否是目录
boolean isAbsolute()| 判断 File 对象对应的是否是绝对路径
long lastModified()| 返回 1970 年 1 月 1 日 0 时 0 分 0 秒 到文件最后修改时间的 毫秒值
long length()| 返回文件内容的长度(注:单位为 字节)
String[] list()| 递归列出指定目录的全部内容(包括子目录和文件),只列出名称
File[] listFiles()**| 返回一个包含 File 对象所有子文件和子目录的 File 数组

演示:

```java
public static void main(String[] args) {
    File file = new File("src/test.txt"); // / 也可以作 目录符号
    System.out.println("文件是否存在:" + file.exists());
    System.out.println("文件名:" + file.getName());
    // ... 具体大家自己可以实践
}
```
  • 补充学习*createTempFile() 方法 和 deleteOnExit() 方法*

在一些特定情况下,程序需要读写一些临时文件,为此,File类提供了*createTempFile() 方法 和 deleteOnExit() 方法 ,用于操作临时文件。createTempFile() 方法 用于创建一个临时文件,deleteOnExit() 方法* 在Java虚拟机退出时自动删除临时文件。

下面通过一个案例演示这两个方法的使用:

```java
public static void main(String[] args) throws IOException {
    // 提供临时文件的前缀和扩展名
    File file = File.createTempFile("itcast-", ".txt");
    file.deleteOnExit();  // java 虚拟机退出时 自动删除文件 file
    System.out.println("file 是否为文件:" + file.isFile());
    System.out.println("file 的相对路径:" + file.getPath());
}
```

2.3 遍历目录下的文件

File 类中提供了 list()方法,可以获取目录下所有文件和目录的名称。获取目录下所有文件和目录名称后,可以通过这些名称遍历目录下的文件,按照调用方法的不同,对目录下的文件遍历可分为以下3种方式。

  1. 遍历指定目录下的所有文件
  2. 遍历指定目录下指定扩展名的文件
  3. 遍历包括子目录中的文件在内的所有文件

下面分别对这3种遍历方式进行详细讲解。

(1)遍历指定目录下的所有文件

File 类的 list()方法可以遍历指定目录下的所有文件。下面通过一个案例演示如何使用 list()方法遍历目录下的所有文件,如下:

```java
public static void main(String[] args) {
    File file = new File("src/IO");
    if(file.isDirectory()) {
        String[] names = file.list(); // 获取目录下所有文件的文件名
        for(String name:names) {
            System.out.println(name);  //输出文件名
        }
    }
}
```

(2)遍历指定目录下指定扩展名的文件
🍎 上述代码实现了遍历一个目录下所有文件的功能,然而有时程序只需要获取指定类型 的文件,如获取指定目录下所有扩展名为“.java”的文件

针对这种需求,File类提供了一个重载的 list()方法,该方法接收一个 FilenameFilter 类型的参数。FilenameFilter 是一个接口,被称作文件过滤器,其中定义了抽象方法accept()用于依次对指定File的所有子目录或文件进行迭代。在调用list()方法时,需要实现 FilenameFilter,并在accept()方法中进行筛选,从而获得指定类型的文件。

下面通过一个案例演示如何遍历指定目录下所有扩展名为“.java”的文件,如下:

```java
public static void main(String[] args) {
    File file = new File("src/IO");
    // 创建文件过滤器对象
    FilenameFilter filter = new FilenameFilter() {
        // 实现 accept 方法
        @Override
        public boolean accept(File dir, String name) {
            File currFile = new File(dir, name);
            // 如果文件以 .java 结尾返回true
            if(currFile.isFile() && name.endsWith(".java")){
                return true;
            }
            else  return false;
        }
    };
    if(file.exists()) {
        String[] lists = file.list(); // 获取目录下所有文件的文件名
        for(String name:lists) {
            System.out.println(name);  //输出文件名
        }
    }
}
```

(3)遍历包括子目录下的文件在内的所有文件
🍉 前面的两个例子演示的都是遍历当前目录 下的文件。有时候在一个目录下,除了文件,还有子目录,如果想获取所有子目录下的文件,list()方法显然不能满足要求,这时可以使用File 类提供的另一个方法—— listFiles()

🍈 该方法返回一个File对象数组 ,当对数组中的元素进行遍历时,如果元素中还有子目录需要遍历,则可以递归遍历子目录。下面通过一个案例演示包括子目录文件的所有文件的遍历,如下:

```java
public static void main(String[] args) {
    // 创建一个代表目录的 File 对象
    File file = new File("src");
    fileDir(file);
}
public static void fileDir(File dir)
{
    File[] files = dir.listFiles(); // 获得表示目录下的所有文件数组
    for(File file :files) // 遍历所有子目录和文件
    {
        if(file.isDirectory()){
            fileDir(file); // 如果是目录则递归调用
        }
        System.out.println(file.getAbsolutePath()); // 获取文件的绝对路径
    }
}
```

2.4 删除文件及目录

💖在操作文件时,可能会遇到需要删除一个目录下某个文件或删除整个目录的操作,这时就可以调用File 类中的 delete() 方法。

```java
public static void main(String[] args) {
    File file = new File("src/IO");
    if(file.exists()){
        System.out.println(file.delete());
    }
}

// 输出 
false
```

为啥会输出 false 呢?

*🌸 因为文件删除失败了,File 类中的 delete() 方法只能删除一个指定的文件,假如 File 对象代表一个目录,而且这个目录下包含子目录或文件,则 File 类中的 delete() 方法 时不允许删除整个目录的。 此时就需要采用递归的方法* 来全部删除

```java
public static void main(String[] args) {
    File file = new File("src/IO");
    deleteDir(file);
    System.out.println("删除成功!!");
}

public static void deleteDir(File dir)
{
    if(dir.exists())
    {
        File[] files = dir.listFiles(); // 获得表示目录下的所有文件数组
        for(File file :files) // 遍历所有子目录和文件
        {
            if(file.isDirectory()){
                deleteDir(file); // 如果是目录则递归调用
            }
            else file.delete();// 如果是文件直接删除
        }
        // 删除整个目录下的所有文件之后,就删除这个目录
        dir.delete();
    }
}
```

注意:

  • 删除目录是从 Java 虚拟机直接删除而不放入到回收站,文件一旦被删除就无法恢复,因此在进行文件删除操作的时候需要格外小心!💞

3. 字节流 💦

3.1 基本概念

🍊 在程序的开发中,经常需要处理设备之间的数据传输,而在计算机中,无论是文本,图片、音频还是视频,所有文件都是以二进制(字节)形式存在的。对于字节的输入输出,I/0 系统提供了一系列流,统称为字节流。字节流是程序中最常用的流,根据数据的传输方向可将其分为字节输入流和字节输出流。
🔥 JDK 提供了两个抽象类—— *InputStreamOutputStream ,它们是字节流的顶级父类,所有的字节输入流都继承 InputStream ,所有的字节输出流都继承 OutputStream* 。

字节流抽象基类

  1. *InputStream* :这个抽象类是表示字节输入流的所有类的超类​​​​​​​
  2. *OutputStream* :这个抽象类是表示字节输出流的所有类的超类
  3. 子类名特点:子类名称都是以其父类名作为子类名的后缀

*🌸*注: I/O 流的输入输出是相对于 程序而言的

  • *InputStream* 类常用方法
方法声明 功能描述
int read() 从输入流读取一字节(8位), 把它转化位 0 - 255 的整数,并返回这个整数
int read (byte[ ] b) 从输入流读取若干字节,把它们保存到参数 b 指定的字节数组中,返回的整数表示读取的字节数
int read (byte[ ] b, int off, int len) 从输入流读取若干字节,把它们保存到参数 b 指定的字节数组中,off指定字节数组保存数据的起始索引,len 表示读取的字节数
void close() 关闭输入流并且释放与其相关的所有系统资源

上表中的3个 *read()* 方法都是用来读数据的。其中:

  1. 第一个 *read()* 方法是从输入流中逐个读入字节;
  2. 而第二个和第三个read()方法则可以将若干字节以字节数组的形式一次性读入,从而提高读数据的效率。在进行 I/O 操作时,当前I/O 流 会占用一定的内存,由于系统资源非常宝贵,因此,在I/0操作结束后,应该调用close()方法关闭 I/O 流 ,从而释放当前 I/O 流 所占的系统资源。

  3. *OutputStream* 类常用方法

方法声明 功能描述
void write(int b) 将指定的字节写入此文件输出流,一次写一个字节数据
void write(byte[] b) 将参数 b指定的字节数组的所有字节写入到此文件输出流,一次写一个字节数组数据
void write(byte[] b, int off, int len) 将指定 byte 数组从偏移量off(起始索引)开始的 len字节写入此文件输出流
void flush() 刷新输出流并且强制写出所有缓冲的输出字节
void close() 关闭输出流并且释放与其关联的所有系统资源

*🌸* 上表前3个是重载的write()方法,都用于向输出流写入字节。

  1. 其中,第一个write()方法逐个写入字节;
  2. 后两个write()方法将若干字节以字节数组的形式一次性写人,从而提高写数据的效率。
  3. flush()方法用来将当前输出流缓冲区(通常是字节数组)中的数据强制写入目标设备,此过程称为刷新。
  4. close()方法用来关闭1/0流并释放与当前1/0流相关的系统资源。

*InputStreamOutputStream 这两个类虽然提供了一系列和读写数据有关的方法,但是这两个类是抽象类,不能被实例化* ,因此,针对不同的功能, InputStream 类 和 OutputStream** 类提供了不同的子类,形成了体系结构,如下图:

InputStream 体系结构图:

【Java】I/O 操作详解

OutputStream 体系结构图:

【Java】I/O 操作详解

3.2 字节流读文件

  • *🧩InputStream 就是JDK提供的基本输入流,它是所有输入流的父类,FileInputStreamInputStream*** 的子类,它是操作文件的字节输入流,专门用于读取文件中的数据。因为从文件读取数据是重复的操作,所以需要通过循环语句实现数据的持续读取。

下面通过一个案例实现字节流对文件数据的读取。在实现案例之前,先做以下操作:

  1. 首先在 Java项目的根目录下创建文本文件test.txt
  2. 在文件中输入内容“itcast” 并保存
  3. 然后使用字节输入流对象读取 test.txt文本文件

案例代码:

```java
public static void main(String[] args) throws IOException {
    // 创建一个文件字节输入流,并且指定源文件名称
    FileInputStream in = new FileInputStream("src/IO/test.txt");
    int b = 0; // 定义 int 类型的变量 b,用于 记住每次读取的 1 字节
    while(true) {
        b = in.read(); // 变量 b 记住读取的每一字节
        if(b == -1){ // 如果读取的字节 位 -1,则跳出循环
            break;
        }
        System.out.println(b + " "); // 否则输出b
    }
    in.close();
}

// 输出
105 116 99 97 115 116
```

由于计算机中的数据都是以字节的形式存在的。在test.txt文件中,字符i、 t、c、a、s、t 各占一字节,所以最终结果显示的就是文件test.txt中的6字节对应的十进制数(即这6个字母的ASCII码值)

🔥 注意:

  • 有时,在文件读取的过程中可能会发生错误。例如,由于文件不存在而导致无法读取。
  • 或者用户没有读取权限等等。这些错误都由Java虚拟机自动封装成 IOException 异常 并抛出。例如,当读取一个不存在的文件时,控制台会报告异常信息,

读取一个不存在的文件 时,程序就会有一个潜在的问题。如果文件读取过程中发生了 I/O 错误,*InputStream 就无法正常关闭,系统资源也无法及时释放,这样会造成系统资源浪费* 。

对此,可以使用 try…· finally 语句保证 *InputStream 在任何情况下都能够正确关闭。修改上述代码,将读取文件的代码放入try语句块 中,将关闭输入流的代码放入finaly语句块* 中,具体代码如下:

```java
public static void main(String[] args) throws Exception {
    InputStream input = null;
    try {
        // 创建一个文件字节输入流
        FileInputStream in = new FileInputStream("src/IO/test.txt");
        int b = 0; // 定义 int 类型的变量 b,用于 记住每次读取的 1 字节
        while (true) {
            b = in.read(); // 变量 b 记住读取的每一字节
            if (b == -1) { // 如果读取的字节 位 -1,则跳出循环
                break;
            }
            System.out.print(b + " "); // 否则输出b
        }
    } finally {
        if (input != null) {
            input.close();
        }
    }
}
```

3.3 字节流写文件

*OutputStream 是JDK提供的基本输出流,与InputStream* 类似.

  1. *OutputStream* 是所有输出流的父类。
  2. *OutputStream* 是一个抽象类,如果使用此类,则必须先通过子类实例化对象。
  3. *OutputStream 类有多个子类,其中FileOutputStream* 子类是操作文件的字节输出流,专门用于把数据写入文件。

案例演示:

```java
public static void main(String[] args) throws Exception {
    OutputStream out = new FileOutputStream("src/IO/example.txt");
    String str = "Island1314";
    byte[] b = str.getBytes();
    for(int i = 0; i < b.length; i++){
        out.write(b[i]);
    }
    out.close();
}
```

由上可知,使用 *FileOutputStream 写数据时,程序自动创建了文件example.txt ,并将数据写入example.txt 文件。需要注意的是,如果通过 FileOutputStream* 向一个已经存在的文件中写入数据,那么该文件中的数据会被覆盖。

若希望在已存在的文件内容之后追加新内容 ,我们应该怎么做:

  • 可使用 *FileOutputStream 的构造函数 public FileOutputStream(String name,boolean append)*
  • 创建文件输出流以指定的名称写入文件,并把append参数的值设置为true。如果第二个参数为true ,则字节将写入文件的末尾而不是开头

    ```java
    public static void main(String[] args) throws Exception {
    OutputStream out = new FileOutputStream("src/IO/example.txt",true);
    String str = "\r\n201314";
    byte[] b = str.getBytes();
    for(int i = 0; i < b.length; i++){
    out.write(b[i]);
    }
    out.close();
    }

    // 在 example.txt 查看
    Island1314
    201314

    // 解释:程序通过字节输出流对象out向文件example.txt写入后,并没有将文件原来的数据清空,而是将新写入的数据追加到了文件的末尾。
    ```

上面的 \r \n 又是什么意思呢 》 解释如下:

对于字节流写数据,应该如何实现换行

  1. windows:\r\n
  2. linux:\n
  3. mac:\r

需要注意的是:I/O流 在进行数据读写操作时会出现异常。为了保持代码的简洁,在*InputStream 读文件和OutputStream 写文件的程序中都使用了throws关键字 将异常抛出。然而一旦遇到 I/O异常I/O流close()方法无法得到执行I/O流 对象占用的系统资源将得不到释放* 。

因此,为了保证I/O流close()方法 必须执行,通常将关闭 I/O流 的操作写在 finally代码块 中。

3.4 字节流复制文件

在应用程序中,I/O 流通常都是成对出现的,即输入流和输出流一起使用 。例如:文件的复制就需要通过输入流读取一个文件中的数据,再通过输出流将数据写入另一个文件

下面通过一个案例演示文件内容的复制:

  1. 首先在 src 项目的根目录下创建 source目录和 target 目录,
  2. 然后在 source 目录中存放 a.png文件,
  3. 最后将 source目录下的 a.png 复制到 target 目录下并重新命名为 b.png。

    ```java
    public static void main(String[] args) throws Exception{
    // 创建一个文件输入流,用于读取 sorce 目录的 a.png 文件
    InputStream in = new FileInputStream("src/source/a.png");

    // 创建一个文件输出流,用于将读取数据写入到 target 目录的 b.png 文件
    OutputStream out = new FileOutputStream("src/target/b.png");
    
    int len; //用于记住每次读取的 1 字节
    // 获取复制文件前的系统时间
    long begintime = System.currentTimeMillis();
    while ((len = in.read())!= -1){ // 读取 1 字节并且判断是否读到文件末尾
        out.write(len); // 将读取的 1 字节写入文件
    }
    
    // 获取文件复制结束后的时间
    long endtime = System.currentTimeMillis();
    System.out.println("复制文件所消耗时间:" + (endtime - begintime) + "ms");
    
    in.close();
    out.close();
    

    }

    // 输出:
    复制文件所消耗时间:6038ms
    ```

上述代码实现了文件的复制:

  • 通过while循环将a.png的所有字节逐个进行复制。
  • 每循环一次,就通过调用FileInputStream的read()方法读取一字节,
  • 并通过调用FileOutputStream 的write()方法将该字节写入指定文件,直到 len 的值为 -1,表示读到了文件末尾,结束循环,完成文件的复制。
  • 程序运行结束后,会在命令行窗口打印复制文件所消耗的时间。
  • 由上可知,程序复制文件共消耗了6038ms。在复制文件时,由于计算机性能等各方面原因,会导致复制文件所消耗的时间不确定,因此每次运行程序的结果未必相同。

在程序运行结束后,打开target目录,发现source目录中的 a.png 文件被成功复制到 target目录中

注意事项:

上述实现的文件复制过程是逐字节读写 ,需要频繁地操作文件,效率非常低

打个比方:

  • 从北京运送烤鸭到上海,如果有一万只烤鸭,每次运送一只,就必须运输一万次,这样的效率显然非常低。为了减少运输次数,可以先把一批烤鸭装在车厢中,这样就可以成批地运送烤鸭,这时的车厢就相当于一个缓冲区

因此在通过流的方式复制文件 时,为了提高效率,也可以定义一个字节数组作为缓冲区

  1. 在复制文件时,可以一次性读取多个字节的数据,并保存在字节数组中 ,然后将字节数组中的数据一次性写入文件。
  2. 程序中的缓冲区就是一块内存,它主要用于暂时存放输入输出的数据,由于使用缓冲区减少了对文件的操作次数 ,所以可以提高数据的读写效率

利用缓冲区复制文件 ,修改代码如下:

```java
public static void main(String[] args) throws Exception{
    // 创建一个文件输入流,用于读取 sorce 目录的 a.png 文件
    InputStream in = new FileInputStream("src/source/a.png");

    // 创建一个文件输出流,用于将读取数据写入到 target 目录的 b.png 文件
    OutputStream out = new FileOutputStream("src/target/b.png");

    // 以下是用 缓冲区 读写文件
    byte[] buff = new byte[1024]; // 定义一个字节数组作缓冲区

    int len; //用于记住每次读取的 1 字节
    // 获取复制文件前的系统时间
    long begintime = System.currentTimeMillis();
    while ((len = in.read(buff))!= -1){ // 读取 1 字节并且判断是否读到文件末尾
        out.write(buff, 0, len); // 将读取的 1 字节写入文件
    }

    // 获取文件复制结束后的时间
    long endtime = System.currentTimeMillis();
    System.out.println("复制文件所消耗时间:" + (endtime - begintime) + "ms");

    in.close();
    out.close();
}

// 输出:
复制文件所消耗时间:8ms

```

可以看出复制文件消耗时间明显减少,说明使用缓冲区读写文件可以有效地提高程序读写效率

4. 字符流 💧

4.1 字符流定义及基本用法

🔥 前面讲解的内容都是通过字节流直接对文件进行读写。如果读写的文件内容是字符,考虑到使用字节流读写字符可能存在传输效率以及数据编码问题 、此时建议使用字符流。

同字节流一样,字符流也有两个抽象的顶级父类,分别是 Reader类Writer类

  1. Reader 类是字符输入流,用于从某个源设备读取字符;
  2. Writer类是字符输出流。用于向某个目标设备写入字符。
  3. 在JDK中,Reader 类和Writer 类提供了一系列与读写数据相关的方法。

注:字符流 = 字节流 + 编码表

Reader 类的常用方法

方法声明 功能描述
int read() 以字符为单位读数据
int read(char[] cbuf) 将数据读入 char 类型的数组,并返回数组长度
int read(char[] cbuf, int off, int len) 将数据读入 char 类型的数组的指定区间,并返回数组长度
void close() 关闭数据流
long transferTo(Writer out) 将数据之间读入字符输出流

Writer 类的常用方法

方法声明 功能描述
void write(int c) 以字符为单位写数据
void write(char[] cbuf) 将 char 类型的数组中的数据写出
void write(char[] cbuf, int off, int len) 将 char 类型的数组中指定区间的数据写出
void write(String str) 将 String 类型的数据写出
void write(String str, , int off, int len) 将 String 类型中指定区间的数据写出
void flush() 强制将缓冲区的数据同步到输出流 (刷新流),之后还可以继续写数据
void close() 关闭数据流

*Reader 类 和 Writer* 类作为字符流的顶级父类,也有许多子类,形成了体系结构,分别如下:

Reader 体系结构图:

【Java】I/O 操作详解

Writer 体系结构图:

【Java】I/O 操作详解

🌈 在上面我们可以看到字符流的继承关系和字节流的继承关系类似,Reader 类 和 Writer 类的很多子类都是成对出现。例如:

  1. FileReaderFileWriter 用于读写文件
  2. BufferedReaderBufferedWriter 是具有缓冲功能的字符流,使用他们可以提高读写效率

4.2 字符流读文件

🥬在程序开发中,经常需要对文本文件的内容进行读取。如果想从文件中直接读取字符,便可以使用字符输入流 *FileReader* ,通过它可以从关联的文件中读取一个或一组字符。

下面通过一个案例演示如何使用 *FileReader* 读取文件中的字符:

  1. 首先新建文本文件 test. txt 并在其中输入字符 “itcast”
  2. 然后创建字符输入流 *FileReader* 对象以读取 reader.txt文件中的内容

    ```java
    public static void main(String[] args) throws Exception {
    // 创建一个 FileReader 对象,用来读取文件字符
    FileReader reader = new FileReader("src/IO/test.txt");
    int ch; // 用于记录读取的字符
    while((ch = reader.read()) != -1){ // 循环判断是否读到文件末尾
    System.out.print((char) ch); // 不是文件末尾就打印字符
    }
    reader.close(); // 关闭字符输入流,释放资源
    }

    // 输出
    itcast
    ```

注:FileReader**** 对象的 read() 方法返回的是 int 类型的值,如果想获得字符,就必须进行强制类型转换。

4.3 字符流写文件

🍋‍🟩上面讲解了字符流对文本文件内容的读取。现在讲解通过字符流向文本文件中写入内容,此时需要使用FileWriter类,该类可以一次向文件中写人一个或一组字符。

下面通过一个案例演示如何使用 *FileWriter* 将字符写入文件

```java
public static void main(String[] args) throws Exception {
    // 创建一个 FileWriter 对象,用于向文件写入数据
    FileWriter writer = new FileWriter("src/IO/example.txt");
    String str = "IsLand1314";
    writer.write(str); // 将字符数据写入到文本文件中
    writer.write("\r\n"); //输出换行
    writer.close();
}
```

注意:

*FileWriterFileOutputStream 一样,如果指定的文件不存在,就会先创建文件,再写入数据;如果文件存在,则原文件内容会被覆盖 。如果想在文件末尾追加数据 ,同样需要调用重载的构造方法* ,将上面第三行代码修改为:

```java
FileWriter writer = new FileWriter("src/IO/example.txt", true);

```

再次运行程序就可以在文件中实现追加的功能

4.4 数据编码解码问题

由于字节流操作中文不是特别的方便,所以Java就提供字符流

  • 字符流 = 字节流 + 编码表

中文的字节存储方式

  • 用字节流复制文本文件时,文本文件也会有中文,但是没有问题,原因是最终底层操作会自动进行字节拼接成中文,如何识别是中文的呢?
  • 汉字在存储的时候,无论选择哪种编码存储,第一个字节都是负数
函数声明 功能描述
byte[] getBytes() 使用平台的默认字符集将该 String编码为一系列字节
byte[] getBytes(String charsetName) 使用指定的字符集将该 String编码为一系列字节
String(byte[] bytes) 使用平台的默认字符集解码指定的字节数组来创建字符串
String(byte[] bytes, String charsetName) 通过指定的字符集解码指定的字节数组来创建字符串

代码演示:

```java
public static void main(String[] args) throws UnsupportedEncodingException {
    //定义一个字符串
    String s = "中国";

    //byte[] bys = s.getBytes(); //[-28, -72, -83, -27, -101, -67]
    //byte[] bys = s.getBytes("UTF-8"); //[-28, -72, -83, -27, -101, -67]
    byte[] bys = s.getBytes("GBK"); //[-42, -48, -71, -6]
    System.out.println(Arrays.toString(bys));

    //String ss = new String(bys);
    //String ss = new String(bys,"UTF-8");
    String ss = new String(bys,"GBK");
    System.out.println(ss);
}
```

5. 转换流 🖊

🍊 前面提到I/0流分为字节流和字符流,字节流和字符流之间可以进行转换。JDK提供了两个类用于将字节流转换为字符流,分别是 *InputStreamReaderOutputStreamReader* 。

*InputStreamReader :是从字节流到字符流 的桥梁,父类是 Reader*

  • 它读取字节,并使用指定的编码将其解码为字符
  • 它使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集

*OutputStreamReader :是从字符流到字节流 的桥梁,父类是 Writer*

  • 是从字符流到字节流的桥梁,使用指定的编码将写入的字符编码为字节
  • 它使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集

通过*InputStreamReaderOutputStreamReader* 将字节流转换为字符流,可以提高文件的读写效率

方法声明 功能描述
InputStreamReader(InputStream in) 使用默认字符编码创建InputStreamReader对象
InputStreamReader(InputStream in,String chatset) 使用指定的字符编码创建InputStreamReader对象
OutputStreamWriter(OutputStream out) 使用默认字符编码创建OutputStreamWriter对象
OutputStreamWriter(OutputStream out,String charset) 使用指定的字符编码创建OutputStreamWriter对象

下面通过一个案例演示如何将字节流转为字符流

  1. 首先.在src项目的根目录下新建文本文件test.txt
  2. 并在文件中输入“Island1314”
  3. 其次,在sre文件夹中创建一个类,在类中创建字节输入流 FileInputStream对象读取src.txt文件中的内容,并将字节输入流转换成字符输入流。
  4. 再次,创建一个字节输出流对象,并指定目标文件为des.txt
  5. 最后,将字节输出流转换成字符输出流将字符输出到文件中

    ```java
    public static void main(String[] args) throws Exception {
    // 创建字节输入流 in ,并且指定源文件 test.txt
    FileInputStream in = new FileInputStream("src/IO/test.txt");
    // 将字节输入流 in 转化为 字符输入流 isr
    InputStreamReader isr = new InputStreamReader(in);

    // 创建字节输出流 out ,并且指定源文件 des.txt
    FileOutputStream out = new FileOutputStream("src/IO/des.txt");
    // 将字节输出流 out 转化为 字符输出流 osw
    OutputStreamWriter osw = new  OutputStreamWriter(out);
    
    int ch; // 定义一个变量用于记录读取的字符
    while((ch = isr.read()) != -1) // 循环判断是否读到文件末尾
    {
        osw.write(ch);
    }
    isr.close(); // 关闭字符输入流,节省资源
    osw.close(); // 关闭字符输出流,节省资源
    

    }
    ```

6. 缓冲流 🔍

6.1 字节缓冲流

  1. BufferedOutputStream: 该类实现缓冲输出流.通过设置这样的输出流,应用程序可以向底层输出流写入字节,而不必为写入的每个字节导致底层系统的调用
  2. BufferedInputStream: 创建BufferedInputStream将创建一个内部缓冲区数组.当从流中读取或跳过字节时,内部缓冲区将根据需要从所包含的输入流中重新填充,一次很多字节
  3. 字节流缓冲区的核心优势就是一次读取多个字节数据,从而减少硬盘操作子树

构造方法:

BufferedOutputStream(OutputStream out) 创建字节缓冲输出流对象
BufferedInputStream(InputStream in) 创建字节缓冲输入流对象
```java
public static void main(String[] args) throws IOException {
    //字节缓冲输出流:BufferedOutputStream(OutputStream out)
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("src/IO/test.txt"));
    //写数据
    bos.write("hello\r\n".getBytes());
    bos.write("world\r\n".getBytes());
    //释放资源
    bos.close();

    //字节缓冲输入流:BufferedInputStream(InputStream in)
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream("src/IO/test.txt"));

    //一次读取一个字节数据
//        int by;
//        while ((by=bis.read())!=-1) {
//            System.out.print((char)by);
//        }

    //一次读取一个字节数组数据
    byte[] bys = new byte[1024];
    int len;
    while ((len=bis.read(bys))!=-1) {
        System.out.print(new String(bys,0,len));
    }

    //释放资源
    bis.close();
}
```

6.2 字符缓冲流

  1. BufferedWriter: 将文本写入字符输出流,缓冲字符,以提供单个字符,数组和字符串的高效写入,可以指定缓冲区大小,或者可以接受默认大小。默认值足够大,可用于大多数用途
  2. BufferedReader: 从字符输入流读取文本,缓冲字符,以提供字符,数组和行的高效读取,可以指定缓冲区大小,或者可以使用默认大小。 默认值足够大,可用于大多数用途
BufferedWriter(Writer out) 创建字符缓冲输出流对象
BufferedReader(Reader in) 创建字符缓冲输入流对象
```java
public static void main(String[] args) throws IOException {

    //创建字符缓冲输出流
    BufferedWriter bw = new BufferedWriter(new FileWriter("src/IO/test.txt"));

    //写数据
    for (int i = 0; i < 10; i++) {
        bw.write("hello" + i);
        //bw.write("\r\n");
        bw.newLine();
        bw.flush();
    }

    //释放资源
    bw.close();

    //创建字符缓冲输入流
    BufferedReader br = new BufferedReader(new FileReader("src/IO/test.txt"));

    String line;
    while ((line=br.readLine())!=null) {
        System.out.println(line);
    }

    br.close();
}


```

7. 序列化反序列化 🔖

🗡 程序在运行过程中,数据都保存在Java对象(内存)中,但很多情况下还需要将一些数据永久保存到磁盘上。为此,Java 提供了对象序列化机制,可以将对象中的数据保存到磁盘。

对象序列化(serialize) 是指将一个Java对象转换成一个I/O流的字节序列的过程

  1. 对象序列化机制可以使内存中的Java对象转换成与平台无关的二进制流,
  2. 通过编写程序,既可以将这种二进制流持久地保存在磁盘上,
  3. 又可以通过网络将其传输到另一个网络节点。

🥁 其他程序在获得了二进制流后,还可以将二进制流恢复成原来的Java对象,这种将I/O流 中的字节序列恢复为Java对象的过程 称为 反序列化(deserialize)

🍇 如果想让某个对象支持序列化机制,那么这个对象所属的类必须是可序列化的。在Java中,可序列化的类必须实现 SerializableExternalizable 两个接口之一。

Serializable 接口或 Externalizable 接口实现序列化机制的主要区别

Serializable 接口 Externalizable 接口
系统自动存储必要的信息 由程序员自己决定要存储的信息
Java 内部支持,易于实现,只需实现该接口即可,不需要其他代码支持 该接口只提供了两个抽象方法,实现该接口时必须重写这两个抽象方法
性能较差 性能较好

👻 与实现 *Serializable* 接口相比,虽然实现 Externalizable** 接口可以带来性能上的一定提升,但由于后者需要实现两个抽象方法,所以将导致编程的复杂度提高。

  • 在实际开发时,大部分情况下使用*Serializable* 接口的方式实现对象序列化。

🎉使用*Serializable 接口实现对象序列化非常简单,只需要让目标类实现 Serializable 接口即可,无须实现任何方法。例如,自定义Person类,让Person类实现 Serializable* 接口,如下:

```java
public class Person implements Serializable{
    // 为该类指定 serialVersionUID 变量值
    private static final long serialVersionUID = 1L;
    // 声明变量
    private int id;
    private String name;
    private int age;
    //... 此处省略各属性的 gettter 和 setter 方法
}
```

💞 在上述代码中,Person类实现了 *Serializable 接口,并指定了 serialVersionUID变量值 ,该属性的值的作用是标识Java类的序列化版本。如果不显式定义 serialVersionUID变量值, 那么serialVersionUID* 属性的值将由 Java 虚拟机 根据类的相关信息计算得出

补充知识:serialVersionUID

🍒 serialVersionUID 适用于Java的对象序列化机制。简单来说,Java的对象序列化机制 是通过判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化 时,Java虚拟机会把字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较。如果相同,就认为是一致的,可以进行反序列化 ;否则就会抛出序列化版本不一致的异常

  • 因此,为了在反序列化时确保序列化版本的兼容性,最好在每一个要序列化的类中加入private static final long serialVersionUID 的变量值,具体数值可自定义,默认是1L。
  • 如果不显式指定 serialVersionUID 的值,系统可以根据类名、接口名、成员方法及属性等生成一个64位的哈希值,将这个哈希值作为serialVersionUID的值。
  • 定义了serialVersionUID的值,如果serialVersionUID所属类的某个对象被序列化,即使该对象对应的类被修改了,该对象也依然可以被正确地反序列化。

8. 小结 📖

本章主要介绍了 I/O流 的相关知识。

💌 包括File类,包括创建File对象、File 类的常用方法、遍历目录下的文件和删除文件及目录;字节流,包括字节流的概念、字节流读文件、字节流写文件和文件的复制;字符流,包括字符流的定义及基本用法、字符流读文件和字符流写文件;转换流的使用;序列化和反序列化。通过本章的学习,读者应该了解 I/O 流,并且熟练掌握了 I/O 流的相关知识。

补充:字节流与字符流区别

字节流 是IO中最基础的形式,它以字节(8位)为单位进行数据传输,适用于处理所有类型的数据,包括文本、图片、音频和视频等二进制数据。在Java中,字节流的基类是 InputStreamOutputStream 。字节流在操作时通常不会使用缓冲区,直接与文件本身进行操作,这意味着每次调用 read 方法都可能伴随着一次磁盘IO,因此效率相对较低。为了提高效率,可以使用如 BufferedInputStreamBufferedOutputStream 这样的缓冲字节流。

字符流 则是以Unicode码元(16位)为单位进行数据传输,主要用于处理文本数据。字符流在处理数据时会涉及字符编码的转换,如UTF-8或GBK等。在Java中,字符流的基类是 ReaderWriter 。字符流在输出前会完成Unicode码元序列到相应编码方式的字节序列的转换,并使用内存缓冲区来存放转换后的字节序列,等待都转换完毕再一同写入磁盘文件中。

主要区别 在于:

  • 字节流操作的基本单元为字节,而字符流操作的基本单元为Unicode码元。

  • 字节流不使用缓冲区,字符流使用缓冲区。

  • 字节流可以处理任何类型的数据,字符流主要处理文本数据。

  • 字节流与文件直接操作,字符流在操作时使用缓冲区。

★,°:.☆( ̄▽ ̄)/$:.°★ 】那么本篇到此就结束啦,如果我的这篇博客可以给你提供有益的参考和启示,可以三连支持一下 !!💞❤️‍🔥💞

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=1uyo4puciczbw

版权声明:程序员胖胖胖虎阿 发表于 2025年1月15日 上午3:30。
转载请注明:【Java】I/O 操作详解 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...