# 深入理解JavaIO流

深入理解JavaIO流

背景

导入导出、后端文件解析等功能会经常使用Java流,熟练掌握JavaIo流对日常开发很有帮助。

引言

  • 流(Stream)是一个抽象的概念,是指一连串的数据(字符或字节),以先进先出的方式发送信息的通道。
  • 数据的传输,可以看做是一种数据的流动,按照流动的方向,以内存为基准,分为输入input 和输出output ,即流向内存是输入流,流出内存的输出流。

Java IO 流的重要性

  • 数据持久化:把程序运行过程中的重要数据写入文件。
  • 网络通信:通过 IO 流来实现数据的发送和接收,进而实现客户端与服务器之间的通信。
  • 标准输入输出:利用System.in(标准输入流)读取用户从控制台输入的数据,使用System.out(标准输出流)向控制台输出信息

Java IO流核心概念

输入输出流本质

  • 输入:把硬盘中的数据加载到内存
  • 输出:把内存中的数据写入到磁盘
    在这里插入图片描述

JavaIO流分类

以流的处理单位为维度主要有字节和字符两种方式
在这里插入图片描述

字节流

  • 处理二进制文件:像图片、音频、视频等文件都是二进制文件,处理这些文件时必须使用字节流,因为字节流能精确处理每一个字节,不会对数据进行任何转换。
  • 网络通信:网络传输的数据本质上是二进制的,所以在进行网络编程时,字节流是不可或缺的
  • 底层数据处理:当需要对底层数据进行操作,如文件系统的读写、设备驱动的交互等,字节流可以直接操作字节数据,更符合底层操作的需求。

字符流

  • 处理文本文件:对于文本文件(如 .txt.java.xml 等),使用字符流更为方便,因为字符流会自动处理字符编码和解码,避免了字节流处理文本时可能出现的乱码问题。
  • 文本数据的处理和转换:在进行文本数据的读取、写入、替换、拼接等操作时,字符流可以直接处理字符,代码实现更加简洁易懂。
  • 用户输入输出:当程序需要与用户进行文本交互时,如从控制台读取用户输入或向控制台输出文本,使用字符流可以方便地处理字符数据。

处理二进制数据时应优先考虑字节流,而处理文本数据时使用字符流会更加合适。

关键设计模式

装饰器模式
  • 缓冲流 BufferedInputStream 包装普通输入 FileInputStream
 // 创建基础的 FileInputStream
InputStream fis = new FileInputStream("test.txt");
// 使用 BufferedInputStream 装饰 FileInputStream
InputStream bis = new BufferedInputStream(fis);
适配器模式
  • InputStreamReaderOutputStreamWriter 是适配器类,InputStreamReader 把字节输入流转换为字符输入流,OutputStreamWriter 把字节输出流转换为字符输出流。
// 创建字节输入流
InputStream fis = new FileInputStream("test.txt");
// 使用 InputStreamReader 将字节输入流转换为字符输入流
InputStreamReader isr = new InputStreamReader(fis);

实战技巧与性能优化

避免加载全部流对象

  • 读取文件、流的时候流式读取,避免所有的加载到内存中

资源释放规范

  • 使用 try-resource 优雅释放资源
// 传统写法
public static void main(String[] args) {
    InputStream inputStream = null;
    try {
        inputStream = new FileInputStream("test.txt");
        int data;
        while ((data = inputStream.read()) != -1) {
            System.out.print((char) data);
        }
    } catch (IOException e) {
       logger.error(e);
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  • try-with-resources 语句大大简化了资源管理的代码,提高了代码的可读性和可维护性,同时减少了资源泄漏的风险。
public static void main(String[] args) {
    try (InputStream inputStream = new FileInputStream("source.txt");
         OutputStream outputStream = new FileOutputStream("destination.txt")) {
        int data;
        while ((data = inputStream.read()) != -1) {
            outputStream.write(data);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

缓冲区流的使用

  • 缓冲区是提高 I/O 操作效率的重要手段,它能减少与外部设备(如磁盘、网络)的交互次数,从而显著提升性能。
  • 下面截图中显示设置缓冲区大小为8k

在这里插入图片描述

缓冲区大小设置

BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter 都提供了可以指定缓冲区大小的构造函数。默认情况下,缓冲区大小是合适的,可以根据实际需求调整缓冲区大小。

注意事项
  • 及时刷新缓冲区:在使用 BufferedOutputStreamBufferedWriter 时,为确保数据及时写入目标,在完成写入操作后,要调用 flush() 方法刷新缓冲区。在 try-with-resources 语句中,资源关闭时也会自动刷新缓冲区。
  • 避免不必要的嵌套:虽然可以多次嵌套缓冲流,但这可能会增加内存开销,在性能提升不明显时,应避免不必要的嵌套。
缓冲流使用测试
  • 普通方式读写文件 1分钟30m左右
@Test
public void test1() throws FileNotFoundException {
    // 记录开始时间
    long start = System.currentTimeMillis();
    // 创建流对象
    try (
            FileInputStream fis = new FileInputStream("C:\\Users\\Administrator\\Desktop\\tempfold\\1.exe");
            FileOutputStream fos = new FileOutputStream("C:\\Users\\Administrator\\Desktop\\tempfold\\2.exe")
    ) {
        // 读写数据
        int b;
        while ((b = fis.read()) != -1) {
            fos.write(b);
        }
    } catch (IOException e) {

    }
    // 记录结束时间
    long end = System.currentTimeMillis();
    System.out.println("普通流复制时间:" + (end - start) + " 毫秒");
}

  • 使用缓冲流复制时间 3305 毫秒
@Test
public void test2() throws FileNotFoundException {
    // 记录开始时间
    long start = System.currentTimeMillis();
    // 创建流对象
    try (
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream("C:\\Users\\Administrator\\Desktop\\tempfold\\1.exe"));
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("C:\\Users\\Administrator\\Desktop\\tempfold\\3.exe"));
    ) {
        // 读写数据
        int b;
        while ((b = bis.read()) != -1) {
            bos.write(b);
        }
    } catch (IOException e) {
        logger.error(e.getMessage(), e);
    }
    // 记录结束时间
    long end = System.currentTimeMillis();
    System.out.println("缓冲流复制时间:" + (end - start) + " 毫秒");
}

大文件处理策略

逐行读取
  • 针对文本文件,逐行读取是常见且有效的方法。例如: BufferedReader 逐行读取文件内容
分块读取
  • 对于二进制文件或者需要处理大文件中的特定部分时,可采用分块读取的方法。实现前端分片下载文件。
多线程处理
  • 对于特别大的文件,可采用多线程处理来提高处理速度。将文件分成多个块,每个线程负责处理一个块。

编码问题避坑

明确指定字符编码
  • 使用 InputStreamReaderOutputStreamWriter,并明确指定字符编码。
  • 避免直接使用 FileReaderFileWriter,因为它们无法指定编码。
// 明确指定 UTF-8 编码
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("input.txt"), StandardCharsets.UTF_8))) 
处理字节流和字符流的转换
  • 使用 InputStreamReaderOutputStreamWriter 进行字节流到字符流的转换,并明确指定编码。
  • 确保读取和写入时使用相同的编码。
// 读取文件时指定相同的编码
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("output.txt"), StandardCharsets.UTF_8)))
处理网络流的编码

在网络编程中,客户端和服务器之间的数据交换需要明确指定编码,否则可能导致乱码。

  • 在发送和接收数据时,明确指定编码。
  • 使用 InputStreamReaderOutputStreamWriter 包装网络流。
try (Socket socket = new Socket("localhost", 8080);
     OutputStreamWriter osw = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8);

读取文件时候始终主动设置字符编码

主动flush()

  • flush() 方法是用于强制将缓冲区的数据写入目标(如文件、网络等)的操作。对于缓冲流(如 BufferedOutputStreamBufferedWriter),数据通常会在缓冲区满时自动写入目标,可以通过调用 flush() 方法手动触发写入。
优点
  • 确保数据及时写入
  • 避免数据丢失
    • 在程序崩溃或异常终止的情况下,未刷新到目标的数据可能会丢失。
    • 主动调用 flush() 可以减少数据丢失的风险。
  • 提高交互性:对于需要实时交互的应用(如聊天程序)非常重要
缺点
  • 频繁调用 flush() 会增加 I/O 操作的次数,导致性能下降
  • 增加代码复杂性,如果出现错误需要额外的处理异常。
  • 不必要的数据写入:频繁调用 flush() 可能会导致不必要的数据写入,尤其是在数据量较小或写入频率较高的情况下。
场景是否主动调用 flush()原因
需要实时传输数据(如网络通信)确保数据及时发送到目标。
需要避免数据丢失(如日志记录)确保数据在程序崩溃前写入目标。
性能敏感的场景频繁调用 flush() 会增加 I/O 操作次数,降低性能。
使用 try-with-resources流关闭时会自动调用 flush(),无需手动调用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

全栈程序员

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

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

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

打赏作者

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

抵扣说明:

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

余额充值