Java从入门到精通(十五) ~ IO流

 晚上好,愿这深深的夜色给你带来安宁,让温馨的夜晚抚平你一天的疲惫,美好的梦想在这个寂静的夜晚悄悄成长。

目录

前言

前置知识 File

在没学习File之前想想,我们以前写的程序有什么问题?

 但是有些文件想长期保存下去咋整呢?

怎么操作文件?

1. 创建和初始化 File 对象

1.1 使用文件绝对路径初始化

1.2 使用文件相对路径初始化

2. 判断信息相关的方法 

​编辑 3. 创建文件和删除文件的方法

4. 遍历文件夹的方法

5. 搜索盘符下的指定文件

前置知识 编码表

1. ASCll字符集 (不包含汉字)

2. GBK字符集 (国标码)

3. Unicode字符集 (万国码)

3.1 UTF-32 编码方案

3.2 UTF-8 编码方案

什么是IO流?

IO流的作用?

前置知识:操作系统在IO做了什么事情?

一、基础流

1. 字节流

1.1 字节输入流 FileInputStream

一次读取多个字节(重点)

读取出多少倒出多少

1.2 字节输出流 FileOutputStream

1.3 新的释放资源方式

1.4 案例:文件复制

2. 字符流

2.1 字符输入流 FileReader

​编辑2.1.1 一次读取一个字符

2.1.2 一次读取多个字符        

2.2 字符输出流 FileWriter

二、高级流

1 字节缓冲流

1.1 字节缓冲输入流 BufferedInputStream

解释和注意事项:

1.2 字节缓冲输出流 BufferedOutputStream

解释和注意事项:

2 字符缓冲流

2.1 字符缓冲输入流 BufferedReader

2.2 字符缓冲输出流 BufferedWriter

3 转换流

3.1 输入转换流 InputStreamReader

3.2 输出转换流 InputStreamWriter

4. 打印流

4.1 PrintStream 

5. 序列化流

5.1 序列化流 ObjectOutputStream

1. 基本使用

2. transient 关键字的作用

3. 序列化版本号的作用

4. 序列化的好处和应用场景

5. 序列化的注意事项

5.2 反序列化流 ObjectInputStream

关键概念和注意事项包括:


前言

当我们做java开发时,需要在内存,磁盘,网络中传输数据时,可能要一次性要传输的数据很大,而我们的内存空间有限,无法完成大文件的批量传输,这时候我们就可以使用IO流,IO流传输数据就是像流水一样缓缓的流动传输。


前置知识 File

在没学习File之前想想,我们以前写的程序有什么问题?

是不是只有在IDEA程序启动的时候会输出结果,当程序结束数组、集合、变量、对象存的数据都不会存在。

它们都是内存中的数据容器。记录的数据,断电或者程序终止时会丢失。

但是有些文件想长期保存下去咋整呢?

这时候就需要引入文件的概念了,它是非常重要的存储在计算机硬盘的存储方式。

 即便断电,或者程序终止了,存储在硬盘文件中的数据也不会丢失。

而我们熟知的Mysql就是存储在磁盘上的一个文件,游戏的存档也是这样,这就是我们上次玩的内容,为什么下次跳转还是原处的原因。

怎么操作文件?

File只能对文件本身进行操作,不能读写文件里面存储的数据。而真正读写就需要IO流。

  1. File 类的作用:File 类确实是 Java 中用来表示文件和目录路径名的抽象表示。它提供了一系列方法来进行文件和目录的操作,包括创建、查找、修改、删除等。

  2. 文件操作的便捷性:File 类使得文件系统操作更加便捷,但需要注意的是,它主要用于操作文件和目录的元数据(如路径、文件名、大小等),而不提供读写文件内容的方法。对于文件内容的读写,通常需要配合其他类(如 FileInputStream、FileOutputStream、BufferedReader、BufferedWriter 等)来完成。

1. 创建和初始化 File 对象

1.1 使用文件绝对路径初始化

文件可以/或者\\

File file = new File("D:/test/path/to/file.txt");

 注意:反斜杠代表转义字符,两个斜杠表示目录中的一个 /

File file = new File("D:\\test\\path\\to\\file.txt");

1.2 使用文件相对路径初始化

不带盘符,默认系统会认为是相对路径,会在当前工程下去找文件

File file = new File("path/to/file.txt");

2. 判断信息相关的方法 

 3. 创建文件和删除文件的方法

4. 遍历文件夹的方法

5. 搜索盘符下的指定文件

  1. searchFile 方法用于在给定的目录 dir 下搜索名为 fileName 的文件。
  2. 首先检查传入的目录是否为合法目录,如果不是,则直接返回。
  3. 使用 listFiles() 方法获取目录下的所有文件和文件夹。
  4. 遍历这些文件和文件夹:
    1. 如果是文件 (isFile() 返回 true),则检查文件名是否与目标文件名 fileName 匹配,如果匹配则打印文件的绝对路径。
    2. 如果是目录 (isDirectory() 返回 true),则递归调用 searchFile 方法,继续搜索该目录下的文件。
import java.io.File;

public class Main {
    public static void main(String[] args) {
        searchFile(new File("D:/"),"3bdb662e620d6792fdc3fb9f69fd7dc.png");
    }

    /**
     * 去目录下搜索某个文件
     * @param dir 目录
     * @param fileName 要搜索的文件
     */
    public static void searchFile(File dir, String fileName){
        //1.处理非法情况 递归出口文件路径不存在或者是文件(因为判断过了子文件夹是否符合了)
        if (dir == null || !dir.exists() || !dir.isDirectory()){
            return;
        }
        //2. 剩下的情况只有文件夹了
        File[] files = dir.listFiles();
        //3. 判断当前目录下存在一级文件对象
        if (files != null){
            //4. 遍历全部一级文件对象
            for (File file : files) {
                //5. 判断是否是文件还是文件夹
                if (file.isFile()){
                    //文件存在
                    if (file.getName().equals(fileName)){
                        System.out.println("找到了" + file.getAbsoluteFile());
                        //启动exe程序
//                        Runtime runtime = Runtime.getRuntime();
//                        runtime.exec(file.getAbsolutePath());
                    }
                }else {
                    //文件夹,继续重复这个过程
                    searchFile(file,fileName);
                }
            }
        }
    }
}

前置知识 编码表

计算机中底层都是硬件,只支持二进制数据进行操作。但是我们汉语和英语的字符,是怎么存储到计算机中的呢?

此时就做出引出了:编码表的概念

  1. 编码:字符按照制定的字符集转换成字节。
  2. 解码:字节按照制定的字符集转换成字符。

编码:字符串转字节数组。解码:字节数组转字符串。

1. ASCll字符集 (不包含汉字)

将每个字符都与对应的数字码点进行对应。然后进行编码变成二进制。

ASCll码表只有128个码点,其中对应0~128。1个字节存储。

编码:将数字转成二进制并且最后一位补0。

2. GBK字符集 (国标码)

ASCll码表对于美国人来说,完全够了,但是中国人存在汉字,ASCll码表就完全处理不了,我们汉子存储的问题了。

因此就引出了:GBK编码表。

GBK字符集,包含了2万多个汉字等字符,GBK中一个中文字符编码成两个字节的形式存储。

并且GBK兼容ASCll字符集。ASCll中的东西,GBK编码不能改变。 并且ASCll里面有的东西都符合标准,一个字节首位为0,但GBK中的汉字第一个字符首位是1。也就是说看完第一个字节的首位1后会把后一个字节连接来进行读取。因此ASCll可以包含2^15个字符。

3. Unicode字符集 (万国码)

如果每个国家都因为美国的ASCll码表不兼容,就发明了一个新的码表,不同语言的国家就会出现码表,这样计算机在世界数据互通的时候,就会出现很多的问题,因此国际组织的耗时者就推出了Unicode字符集。

Unicode字符集是国际组织制定的,可以容纳世界上所有文字和符合的字符集。

3.1 UTF-32 编码方案

采用了4个字节表示一个字符,有容乃大的思想。

但大家都觉得这种编码方案,占用存储空间。通信效率就会降低。

3.2 UTF-8 编码方案

UTF-8就很好的解决UTF-32奢侈的思想。

UTF-8:采用可变长编码方案,共有四个长度区:1个字节、2个字节、3个字节、4个字节。

1个字节:英文字符和数字(兼容ASCll码表)

3个字节:汉语字符 (兼容大部分国家字符) 

2个字节:常见的非拉丁文字符和日语的平假名、片假名,如希腊文、西里尔文。

4个字节:较为罕见的或者历史上用于稀有文字的字符。

为了防止读和写不一致的情况,底层也会有一种编码规则:

  • 注意1:编码的方式要和解码的方式一致,否则会出现乱码。不符合我的编码方式,我无法解析。比如GBK 1xxxxxx UTF-8不认识你,就会输出?。
  • 注意2:数字和字母不会出现乱码,因为所有语言都兼容了ASCll码表。  

什么是IO流?

        IO流是用于处理设备之间的数据传输的机制,Java中的IO流主要分为两大类:输入流和输出流。输入流用于从源读取数据,输出流用于向目标写入数据。流的概念可以看作是数据在源和目标之间的传输管道,数据可以是字节或字符。

输出流:以内存为基准,将内存中的数据以字节或者字符的形式写到磁盘或者网络中的流。

输入流:以内存为基准,将磁盘或者网络中以字节或者字符的形式读到内存中的数据的流。

IO流的作用?

文件或者网络中的数据进行读写的操作

把数据从磁盘、网络中读取到程序中来、用到的是输入流。(读数据)

把程序中的数据写入磁盘或网络中、用到的是输出流。(写数据)

游戏记录存档、文件拷贝、发qq通过IO流写到网络发给对方。

在Java编程中,IO(输入/输出)流是非常重要的概念。它们为我们提供了处理文件、网络连接和其他输入输出数据的能力。本文将深入探讨Java中的IO流,包括其类型、工作原理以及如何有效地使用它们。

前置知识:操作系统在IO做了什么事情?

  1. 文件系统处理

    • 文件定位:操作系统通过文件路径定位要写入的文件。这包括解析文件路径、检查文件是否存在等。
    • 权限检查:操作系统会检查当前进程是否有足够的权限来对文件进行写入操作。这涉及到访问控制列表(ACL)或者文件的所有者/组权限。
    • 文件打开:如果文件没有被打开,操作系统会打开文件以准备写入。这会涉及到文件描述符的分配和管理。
  2. 缓存管理

    • 写入缓冲区:为了提高性能,操作系统可能会将待写入的数据先写入到内存中的缓冲区。这些缓冲区可以是系统级的页缓存或者特定文件的缓冲区。
    • 延迟写入:有时候,操作系统会延迟实际的磁盘写入,将数据先存放在内存中,以便进行批量写入或者提高响应速度。
  3. 磁盘管理

    • 传输数据:一旦数据准备好,操作系统会调用磁盘驱动程序来实际传输数据到磁盘。这包括通过总线(如SATA、SCSI等)发送数据到磁盘控制器。
    • 写入磁盘:磁盘管理模块负责将数据写入到磁盘的相应位置。这可能涉及到磁盘寻道、数据区域的写入等物理操作。
  4. 数据安全性保证

    • 事务处理:对于一些文件系统,操作系统可能会使用事务处理来确保数据的原子性。这意味着要么写入操作完全成功,要么完全失败,不会出现部分写入的情况。
    • 日志记录:某些文件系统会在写入前记录写操作的日志,以便在发生系统崩溃或中断时可以恢复文件系统到一致的状态。
  5. 更新元数据

    • 文件元数据更新:成功写入后,操作系统会更新文件的元数据,例如文件大小、修改时间、访问时间等。
    • 文件系统元数据更新:此外,文件系统可能还需要更新其自身的元数据结构,如索引节点(inode)或文件分配表,以反映文件的最新状态。
  6. 返回写入结果

    • 写入成功:如果写入成功,操作系统会返回成功的状态给应用程序,通常还会返回写入的字节数或其他相关信息。
    • 错误处理:如果写入过程中出现错误(如磁盘空间不足、权限错误等),操作系统会返回相应的错误码或异常,通知应用程序写入操作失败。

操作系统会负责将数据从内核空间传输到用户空间(程序的地址空间),或者从用户空间传输到内核空间,这取决于是读取还是写入操作。

一、基础流

1. 字节流

1.1 字节输入流 FileInputStream

字节流以字节为单位处理数据,本质上所有文件都是以字节组成,适合处理二进制文件、图像、视频等数据

  1. 首先创建FileInputStream文件字节输入流管道、与源文件接通。
  2. 使用read()读取一个文件,然后强转成一个字符。-1表示数据已经读完。

import java.io.FileInputStream;
import java.io.IOException;

public class FileInputStreamExample {
    public static void main(String[] args) {
        String filePath = "input.txt"; // 替换为你的文件路径

        try (FileInputStream fis = new FileInputStream(filePath)) {
            int data;
            while ((data = fis.read()) != -1) {
                System.out.println((char)data);
            }

            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

注意:

一个字节的读取可能会出现乱码(汉字三个字节,你读完一个就强转成char肯定会出现错误),我们可以一次性读取全部字节,然后把全部字节转换为一个字符串,就不会出现乱码了(但编码和解码要保持一致)。

一次读取多个字节(重点)

每一次读取一个字节的形式是效率是非常的差的,假如我这个文件有1024字节,意味着就需要1024次系统调用,每次读一次就需要调用一次系统资源。因此需要使用字节数组减少调用系统资源的次数。

  1. 数据传输方式:字节数组可以一次性传输多个字节或多个数据块,而不是逐个字节或逐个数据块进行传输。这种批量传输可以减少在传输过程中的固定开销,例如系统调用的次数,从而提高效率。

  2. 系统调用开销每次进行 IO 操作时,操作系统需要执行系统调用系统调用本身会引入一定的开销,包括上下文切换、参数传递等。如果能够减少系统调用的次数,就可以减少这些开销,提高 IO 性能。

  3. 缓存利用:使用字节数组可以更有效地利用操作系统的缓存机制。一次性读取或写入多个数据块可以更好地利用内存和磁盘缓存,减少了频繁的访问开销。

  4. 数据块大小:选择适当的数据块大小也会影响 IO 性能。过小的数据块会增加系统调用的频率,而过大的数据块可能会导致资源浪费或不必要的延迟。字节数组允许程序员在不同的场景下选择最优的数据块大小,以获得最佳的性能表现。

按照操作系统介绍:

系统调用(System Call)是操作系统(OS)提供给应用程序使用的一种服务机制。它允许应用程序请求操作系统内核执行某些操作,如文件操作、进程控制、网络通信等,这些操作通常涉及到底层硬件资源的管理和控制。

具体来说,当应用程序需要进行一些只有操作系统才能完成的任务时(例如打开文件、读取数据、创建进程等),它就会通过系统调用接口向操作系统发出请求。操作系统收到请求后,会在内核态(Kernel Mode)执行相应的操作,并将结果返回给应用程序。

系统调用的执行涉及到从用户态(User Mode)切换到内核态的过程,这个切换需要一些开销,例如保存和恢复进程的状态、权限检查、参数传递等。因此,系统调用的频率和执行效率会直接影响应用程序的性能。

关于数据块大小的问题,如果数据块过小,应用程序可能需要频繁地进行系统调用来完成读取或写入操作,增加了系统调用的开销;而数据块过大时,虽然每次系统调用传输的数据量更多,但可能会导致内存或磁盘资源的浪费,或者造成不必要的延迟,因为大块数据可能需要更多时间来传输和处理。

因此,为了优化性能,通常需要在系统调用的开销和数据块大小之间寻找一个平衡点,以获得最佳的 IO 性能。

import java.io.FileInputStream;
import java.io.IOException;

public class ReadFileToStringExample {
    public static void main(String[] args) {
        String filePath = "input.txt"; // 替换为你的文件路径
        int bufferSize = 1024; // 设置每次读取的字节数 filePath.length()读取全部一次

        try (FileInputStream fis = new FileInputStream(filePath)) {
            StringBuilder contentBuilder = new StringBuilder();
            byte[] buffer = new byte[bufferSize];
            int bytesRead;

            // 每次读取 bufferSize 大小的字节到缓冲区,直到文件末尾
            while ((bytesRead = fis.read(buffer)) != -1) {
                // 将读取的字节转换为字符串,指定字符集(例如UTF-8)
                String part = new String(buffer, 0, bytesRead, "UTF-8");
                contentBuilder.append(part);
            }

            // 输出文件内容
            String content = contentBuilder.toString();
            System.out.println("文件内容如下:");
            System.out.println(content);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
byte[] buffer = is.readAllBytes(); // 读取输入流中的所有字节到缓冲区
System.out.println(new String(buffer)); // 将字节数组转换为字符串并打印输出
  • is.readAllBytes() 方法会从输入流 is 中读取所有可用的字节,并将其存储在一个字节数组 buffer 中。这个方法在 Java 9 及更高版本中可用。
读取出多少倒出多少

String part = new String(buffer, 0, bytesRead, "UTF-8");读取出多少倒出多少,防止上一次读取内容影响到它。

因为你可能上次数组中存满元素了,下次读取只会从0索引覆盖到老数组中,但老数组的元素并没有被完全覆盖。

比如:abcde

你的字节数组长度是3,第一读取就是abc  第二次就是dec,就会出现数据不一致的问题。

1.2 字节输出流 FileOutputStream

import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputStreamExample {

    public static void main(String[] args) {
        String filePath = "output.txt"; // 文件路径,可以是任何你想要写入的文件路径

        try {
            // 创建文件字节输出流对象,如果文件存在则会被覆盖
            FileOutputStream fos = new FileOutputStream(filePath);

            // 写入一个字节到文件
            fos.write(65); // 写入字符 'A'

            // 写入一个字节数组到文件
            byte[] data = "Hello, FileOutputStream!".getBytes();
            fos.write(data);

            // 写入字节数组的一部分到文件
            fos.write(data, 0, 5); // 写入 "Hello"

            // 关闭流
            fos.close();

            System.out.println("数据成功写入文件 " + filePath);
        } catch (IOException e) {
            System.out.println("写入文件时出现异常:" + e.getMessage());
        }
    }
}

1.3 新的释放资源方式

使用 try-with-resources 语句

  • try-with-resources 是一种在 JDK 7 中引入的语法,用于自动关闭实现了 AutoCloseable 或 Closeable 接口的资源,这些资源在 try 代码块结束时会自动关闭,不再需要显式调用 close() 方法。
  • 在 JDK 8 中,try-with-resources 语句得到了改进,可以处理带有资源的多个声明,而不需要额外的嵌套。
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"));
     BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
    // 使用 reader 和 writer 进行读写操作
} catch (IOException e) {
    // 处理异常
}
  1. 在这个例子中,无论是否发生异常,BufferedReaderBufferedWriter 都会在 try 语句结束时自动关闭。如果发生异常,会先关闭资源,然后再抛出异常。

  2. 关闭顺序

    • 如果在 try-with-resources 语句中声明了多个资源,它们的关闭顺序与它们被声明的顺序相反,即先声明的后关闭。
    • 例如,在上面的示例中,writer 会先于 reader 关闭。
  3. 扩展:finally的一些细节

1.4 案例:文件复制

  1. 文件路径定义

    • String sourceFile = "source.txt"; 和 String destFile = "destination.txt"; 分别指定了源文件和目标文件的路径。这里假设 source.txt 是要复制的源文件,destination.txt 是复制后生成的目标文件。
  2. 流的创建和使用

    • FileInputStream fis = new FileInputStream(sourceFile); 创建了一个 FileInputStream 对象,用于读取 source.txt 文件的内容。
    • FileOutputStream fos = new FileOutputStream(destFile); 创建了一个 FileOutputStream 对象,用于写入数据到 destination.txt 文件中。
  3. 复制过程

    • 使用 byte[] buffer 作为缓冲区,大小为 1024 字节。
    • fis.read(buffer) 从输入流中读取数据到缓冲区,并返回实际读取的字节数。当返回值为 -1 时表示已经读取到文件末尾。
    • fos.write(buffer, 0, length) 将缓冲区中的数据写入到输出流中,其中 length 是实际读取的字节数。
  4. 关闭流

    • 在复制完成后,使用 fis.close() 和 fos.close() 分别关闭输入流和输出流,释放资源。
  5. 异常处理

    • 使用 try-catch 块捕获可能出现的 IOException 异常,在异常发生时输出异常信息。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileCopyExample {

    public static void main(String[] args) {
        String sourceFile = "source.png"; // 源文件路径
        String destFile = "destination.png"; // 目标文件路径

        try {
            // 创建文件输入流对象
            FileInputStream fis = new FileInputStream(sourceFile);
            // 创建文件输出流对象,如果文件不存在则会被创建
            FileOutputStream fos = new FileOutputStream(destFile);

            // 设置缓冲区
            byte[] buffer = new byte[1024];
            int length;

            // 从输入流中读取数据到缓冲区,然后写入输出流
            while ((length = fis.read(buffer)) > 0) {
                fos.write(buffer, 0, length);
            }

            // 关闭流
            fis.close();
            fos.close();

            System.out.println("文件复制成功。");
        } catch (IOException e) {
            System.out.println("文件复制时出现异常:" + e.getMessage());
        }
    }
}

2. 字符流

流中都是一个个的字符,只适合操作纯文本文件。

英文的话就读取一个字节,中文就读取三个字节。

2.1 字符输入流 FileReader

2.1.1 一次读取一个字符
import java.io.FileReader;
import java.io.Reader;
import java.io.IOException;

public class CharacterInputStreamExample {

    public static void main(String[] args) {
        try {
            // 创建一个字符输入流,从文件中读取数据
            Reader reader = new FileReader("input.txt");

            // 一次读取一个字符并输出
            int character;
            while ((character = reader.read()) != -1) {
                System.out.print((char) character);
            }

            // 关闭流
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
2.1.2 一次读取多个字符        
import java.io.FileReader;
import java.io.Reader;
import java.io.IOException;

public class CharacterInputStreamExample {

    public static void main(String[] args) {
        try {
            Reader reader = new FileReader("input.txt");
            char[] buffer = new char[1024]; // 缓冲区大小可以根据实际需求调整
            int numCharsRead;

            while ((numCharsRead = reader.read(buffer, 0, buffer.length)) != -1) {
                // 将读取到的字符数组转换为字符串进行处理或输出
                String data = new String(buffer, 0, numCharsRead);
                System.out.print(data);
            }

            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.2 字符输出流 FileWriter

字符输出流写出数据后,必须刷新流,或者关闭流,写出去的数据才能生效。

因为字符输出流在底层会自动创建一个缓冲区,写在缓冲区(内存)中是非常快的,然后系统会在释放资源(包含刷新操作)或者刷新流的时候才会写出去。

如果缓冲区装满了还没释放资源和刷新流,系统会自动帮我们将缓冲区的数据一次。

import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;

public class CharacterOutputStreamExample {

    public static void main(String[] args) {
        String fileName = "output.txt";
        String text = "Hello, world!";
        
        try (Writer writer = new FileWriter(fileName)) {
            // 一次写多个字符
            writer.write(text);
            
            System.out.println("写入字符成功到文件 " + fileName);
        } catch (IOException e) {
            System.err.println("写入文件时发生错误: " + e.getMessage());
        }
    }
}

二、高级流

 对基本流进行包装,以提高原始流读写数据的性能。

1 字节缓冲流

系统会:来一个数组里面都是字节,一次全部读出来然后进行写。

字节数组:读取多少、倒出多少。

缓冲区可以减少对数据存取的频率,通过一次性读取或写入一定量的数据,减少了与外部设备的交互次数,提高了数据读写的效率和整体系统性能。

底层还是低级流,只不过会自动帮我们创建一个字节数组:

1.1 字节缓冲输入流 BufferedInputStream

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class BufferedInputStreamExample {

    public static void main(String[] args) {
        String fileName = "input.txt";

        try (InputStream fileInputStream = new FileInputStream(fileName);
             BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream)) {
            
            // 读取数据
            int data;
            while ((data = bufferedInputStream.read()) != -1) {
                System.out.print((char) data);
            }

        } catch (IOException e) {
            System.err.println("读取文件时发生错误: " + e.getMessage());
        }
    }
}

解释和注意事项:

  • BufferedInputStream 的使用:在 BufferedInputStream 的构造方法中传入一个 FileInputStream 对象,它将从文件中读取数据并缓存到内存中,以提高读取效率。

  • 数据读取:通过 bufferedInputStream.read() 方法每次读取一个字节数据,直到读取到文件末尾(返回 -1)为止。在示例中,我们将读取的每个字节数据转换为字符并打印出来。

  • 异常处理:与之前一样,使用 try-with-resources 结构确保流在使用完毕后正确关闭,并捕获可能的 IO 异常。

1.2 字节缓冲输出流 BufferedOutputStream

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class BufferedOutputStreamExample {

    public static void main(String[] args) {
        String fileName = "output.txt";
        String data = "Hello, BufferedOutputStream!";
        
        try (OutputStream fileOutputStream = new FileOutputStream(fileName);
             BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream)) {

            // 写入数据
            bufferedOutputStream.write(data.getBytes());

            System.out.println("数据写入成功到文件 " + fileName);

        } catch (IOException e) {
            System.err.println("写入文件时发生错误: " + e.getMessage());
        }
    }
}

解释和注意事项:

  • BufferedOutputStream 的使用:在 BufferedOutputStream 的构造方法中传入一个 FileOutputStream 对象,它将把数据写入到文件中并进行缓冲,以提高写入效率。

  • 数据写入:通过 bufferedOutputStream.write(byte[]) 方法将指定的字节数组数据写入到文件中。在示例中,我们将字符串 "Hello, BufferedOutputStream!" 转换成字节数组并写入文件。

  • 异常处理:同样地,使用 try-with-resources 结构确保流在使用完毕后正确关闭,并捕获可能的 IO 异常。

2 字符缓冲流

2.1 字符缓冲输入流 BufferedReader

自带一个特有的readLine()方法,自动读取一整行。

如果缓冲区有数据回到缓冲区中去读,没有回到文件中读,并且加载到缓冲区。

2.2 字符缓冲输出流 BufferedWriter

自带一个特有的newLine()方法,换行。

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class BufferedStreamExample {
    public static void main(String[] args) {
        String inputFile = "input.txt";
        String outputFile = "output.txt";

        try (
            BufferedReader reader = new BufferedReader(new FileReader(inputFile));
            BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile));
        ) {
            String line;
            while ((line = reader.readLine()) != null) {
                // 每次读取一行数据并写入到输出文件
                writer.write(line);
                writer.newLine(); // 写入换行符,保持原始文件的行结构
            }
            System.out.println("文件复制成功!");
        } catch (IOException e) {
            System.err.println("文件操作出错: " + e.getMessage());
        }
    }
}
  1. 定义文件路径inputFile 和 outputFile 分别是输入文件和输出文件的路径。

  2. try-with-resources:使用 try-with-resources 语句来自动管理流的关闭。在 Java 7 中引入了这一特性,可以有效地简化代码并确保资源得到正确关闭。

  3. BufferedReader 读取文件:使用 FileReader 包装器来创建 BufferedReader 对象,用于逐行读取 inputFile 中的内容。

  4. BufferedWriter 写入文件:使用 FileWriter 包装器来创建 BufferedWriter 对象,用于将读取的内容写入 outputFile 中。writer.write(line) 写入每一行的内容,writer.newLine() 写入换行符以保持原始文件的行结构。

  5. 异常处理:捕获可能抛出的 IOException,并在控制台输出错误消息。

  6. 成功消息:如果没有异常抛出,输出 "文件复制成功!"。

3 转换流

可以解决输入文件和读取文件所使用的编码不一致的情况下会出现乱码的问题。

解决思路:先获取文件的原始字节流,再将其按真实的字符集编码转成字符输入流,这样字符输入流中的字符就不乱码了。

3.1 输入转换流 InputStreamReader

当提到输入输出转换流 InputStreamReaderOutputStreamWriter 时,它们通常用于处理字符数据和字节数据之间的转换。这些流是字符流和字节流之间的桥梁,可以方便地将字节流转换为字符流或者将字符流转换为字节流。

// 示例 1:使用 InputStreamReader 将字节流转换为字符流
InputStream inputStream = new FileInputStream("input.txt"); // 字节输入流
Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); // 将字节流转换为字符流

// 使用 BufferedReader 包装 InputStreamReader,以便按行读取
BufferedReader bufferedReader = new BufferedReader(reader);
String line;
while ((line = bufferedReader.readLine()) != null) {
    System.out.println(line); // 输出每行内容
}

// 关闭流
bufferedReader.close();

3.2 输出转换流 InputStreamWriter

// 示例 2:使用 OutputStreamWriter 将字符流转换为字节流
OutputStream outputStream = new FileOutputStream("output.txt"); // 字节输出流
Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); // 将字符流转换为字节流

// 使用 BufferedWriter 包装 OutputStreamWriter,以便写入数据
BufferedWriter bufferedWriter = new BufferedWriter(writer);
bufferedWriter.write("Hello, world!\n"); // 写入字符数据
bufferedWriter.write("你好,世界!\n");
bufferedWriter.flush(); // 刷新缓冲区,确保数据写入到文件中

// 关闭流
bufferedWriter.close();

4. 打印流

4.1 PrintStream 

打印流(PrintStream)是Java中用于将各种数据类型格式化输出到目标设备(如控制台、文件等)的类。它是Java标准库中的一部分,位于java.io包中。

  1. 格式化输出:PrintStream提供了多种用于格式化输出的方法,例如print()println()和printf()系列方法,可以输出各种数据类型(如基本类型、字符串、对象)的值,并自动进行格式化处理。

  2. 目标设备:PrintStream可以输出到多种目标设备,最常见的是输出到控制台(System.out),也可以通过构造方法指定输出到文件或其他输出流。

  3. 异常处理:PrintStream处理输出过程中可能出现的I/O异常,它的方法通常不会抛出IOException,而是通过checkError()方法来检查是否发生了错误。

  4. 自动刷新:PrintStream可以设置是否在每次调用println()方法后自动刷新缓冲区。

代码如下(示例):

import java.io.*;

public class PrintStreamExample {
    public static void main(String[] args) {
        try {
            // 输出到控制台
            PrintStream ps = System.out;
            ps.println("Hello, PrintStream!");

            // 输出到文件
            File file = new File("output.txt");
            FileOutputStream fos = new FileOutputStream(file);
            PrintStream fileStream = new PrintStream(fos);
            fileStream.println("Output to file.");

            // 格式化输出
            double pi = Math.PI;
            ps.printf("Pi value: %.2f\n", pi);

            // 关闭流
            fileStream.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

5. 序列化流

5.1 序列化流 ObjectOutputStream

1. 基本使用
2. transient 关键字的作用
3. 序列化版本号的作用

Java序列化机制通过版本号来确定序列化对象的兼容性,具体包括以下几点:

  1. 手动指定版本号

    • 当一个类实现了 Serializable 接口但没有显式指定 serialVersionUID(序列化版本号)时,Java序列化机制会自动生成一个版本号。这个自动生成的版本号基于类的成员(字段、方法等)生成的散列值。
    • 手动为类添加 serialVersionUID 可以避免在修改类结构后导致自动生成的版本号发生变化,从而引起反序列化失败的问题。
  2. 自动生成版本号的问题

    • 如果一个类没有显式指定版本号,而后来修改了类的结构(例如增加、删除或者修改了字段或方法),Java序列化机制会重新自动生成一个不同的版本号。
    • 这样的话,使用旧版本序列化的对象尝试反序列化时,由于版本号不匹配,会抛出 InvalidClassException 异常,导致反序列化失败。
  3. 手动指定版本号的优点

    • 手动指定 serialVersionUID 可以确保在类结构发生变化时,版本号不变,从而保证序列化和反序列化的兼容性。
    • 这对于在分布式系统中特别重要,因为不同的系统可能会使用相同类的不同版本。

在实际开发中,推荐对需要序列化的类显式地添加 serialVersionUID,并保持这个版本号不变,除非有意为之修改类结构。这样做可以避免由于版本号不匹配而导致的反序列化异常,提高系统的稳定性和兼容性。

import java.io.Serializable;

public class MyClass implements Serializable {
    private static final long serialVersionUID = 123456789L; // 手动指定版本号

    // 类的成员和方法定义...
}

通过这种方式,可以明确控制类的序列化版本号,避免不必要的问题,并确保在类结构发生变化时能够正确地处理已经序列化的对象。

4. 序列化的好处和应用场景
  1. 持久化存储

    • 序列化允许将对象以二进制形式存储在磁盘上,这样可以实现对象的持久化存储,即使程序结束或重启,数据仍然可以被恢复和使用。
  2. 对象传输

    • 序列化也是在网络上传输对象的常用方式。通过将对象序列化为字节流,可以在网络上传输,接收端再将字节流反序列化为对象进行处理。
  3. 跨平台通信

    • 序列化使得不同平台上的程序可以通过序列化的方式交换数据,例如,Java程序序列化对象后可以通过网络传给运行在不同语言的系统的应用程序。
5. 序列化的注意事项
  1. 保存对象状态

    • 序列化只保存对象的状态(即成员变量的值),不保存方法。反序列化时会重新构建对象,但是类的构造方法不会被调用。
  2. 版本控制

    • 最好为实现 Serializable 接口的类显式指定 serialVersionUID,避免自动生成的版本号在类结构改变后发生变化而导致反序列化失败。
  3. 类的可序列化性

    • 如果一个类实现了 Serializable 接口,它的所有非瞬态(非 transient)成员变量都会被序列化,包括其引用的其他可序列化的对象。
  4. 稳定性

    • 序列化的类最好是稳定的,即不经常修改其结构,因为序列化的二进制数据在类结构变化后可能无法正确反序列化。

5.2 反序列化流 ObjectInputStream

当需要使用反序列化流 ObjectInputStream 时,它的主要作用是将之前通过序列化保存在文件或网络中的对象数据重新转换回内存中的对象实例。这种操作常见于需要持久化存储对象数据或在不同系统间传输对象数据的场景中。

关键概念和注意事项包括:
  1. 反序列化的目的

    • 数据恢复:通过反序列化,可以将之前序列化的对象数据恢复为原始的对象实例,使得对象的状态得以保留和重用。
    • 数据传输:在网络传输中,对象可以通过序列化为字节流,再通过反序列化还原为对象,从而实现跨平台和跨语言的数据交换。
  2. 使用方式

    • 输入源:通常是一个包含序列化数据的输入流,如文件流 (FileInputStream) 或网络流。
    • ObjectInputStream:用于从输入源中读取字节并将其转换为对应的对象实例。
  3. 版本号控制

    • serialVersionUID:是一个长整型常量,用于控制对象序列化和反序列化的版本兼容性。
    • 显式指定 serialVersionUID 可以确保在类结构发生变化时仍能够正确反序列化,避免因自动生成的版本号不匹配而导致的反序列化异常。
  4. 类的稳定性

    • 序列化的类最好是稳定的,即不经常修改其结构。因为一旦类的结构发生变化,可能导致之前序列化的数据无法正确反序列化。
  5. 异常处理

    • 在反序列化过程中可能会抛出 IOException 或 ClassNotFoundException 异常,需要适当处理这些异常以确保程序的稳定性和可靠性。

总之,ObjectInputStream 提供了重要的功能,使得 Java 应用程序能够方便地将对象持久化到磁盘或通过网络传输,同时保证数据的完整性和版本兼容性。

import java.io.*;

public class DeserializeDemo {
    public static void main(String[] args) {
        String filename = "data.ser"; // 序列化后的文件名

        try (FileInputStream fileIn = new FileInputStream(filename);
             ObjectInputStream in = new ObjectInputStream(fileIn)) {

            MyClass obj = (MyClass) in.readObject(); // 反序列化对象
            System.out.println("Deserialized Object:");
            System.out.println(obj); // 假设 MyClass 类有合适的 toString 方法

        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

  • 29
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风止￴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值