当前位置: 首页 > news >正文

【Java】I/O 流篇 —— 字节 I/O 流

目录

  • I/O 流
    • 概述
    • I/O 流的分类
    • I/O 流的体系
  • 字节 I/O 流
    • FileOutputStream 类
      • 基本操作
      • 写数据的 3 种方式
      • 换行和续写
      • 原理
        • 基本工作流程
        • 原理详细剖析
    • FileInputStream 类
      • 基本操作
      • 循环读取
      • 文件拷贝
        • 小文件拷贝
        • 大文件拷贝

I/O 流

概述

定义:I/O流(Input/Output Stream) 是计算机中数据传输的一种抽象概念,用于描述数据在程序和外部设备(如文件、网络、内存等)之间的流动方式。可以将它想象成一条“数据管道”,数据像水流一样在这条管道中连续传输。

作用:I/O 流用于读写文件中的数据(可以是读写文件,或网络中的数据)。

I/O 流中,以程序为参照物进行读写:

  1. 读(Read) → Input(输入)
    • 当程序从外部(如文件、键盘、网络等)获取数据时,称为“读”操作。
    • 例如:读取文件内容、接收用户键盘输入、获取网络数据等。
    • 这属于输入(Input),因为数据是从外部流向程序的。
  2. 写(Write) → Output(输出)
    • 当程序向外部(如文件、屏幕、打印机等)发送数据时,称为“写”操作。
    • 例如:将结果保存到文件、在屏幕上显示信息、发送数据到网络等。
    • 这属于输出(Output),因为数据是从程序流向外部。

I/O 流的分类

流的方向:输入流(读取)和输出流(写出)

操作文件类型:

  • 字节流:能操作所有类型的文件
  • 字符流:只能操作纯文本文件(用 Windows 自带的记事本打开能读懂的文件)

I/O 流的体系

在这里插入图片描述

这四个类为 I/O 流中的超类,但由于是抽象类,无法进行实例化,下面将学习这四个类的子类。

字节 I/O 流

FileOutputStream 类

操作本地文件的字节输出流,可以把程序中的数据写到本地文件中

基本操作

操作步骤

  1. 创建字节输出流对象

    FileOutputStream fos = new FileOutputStream("aaa.txt");
    
  2. 写数据

    fos.write();
    
  3. 释放资源

    fos.close();
    

代码示例

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

public class ByteStreamDemo {
	public static void main(String[] args) throws IOException {
        // 创建字节输出流对象,可按需修改路径
		FileOutputStream fos = new FileOutputStream("aaa.txt");
		// 写数据
        fos.write(97); // 文件中输出 a
        // 释放资源
		fos.close();
	}
}

注意事项

  1. 操作字节输出流需要抛出异常,可以抛出父类异常 IOException

  2. 创建字节输出流对象

    • 参数可以是字符串表示的路径,也可以是 File 对象

    • 如果文件不存在则创建一个新的文件,但要保证父路径是存在的

    • 如果文件已经存在,则会清空文件原有的内容

  3. 写入数据

    • write() 的参数是整数,但是输出到文件中的是该整数对应的 ASCII 字符
  4. 释放资源

    • 每次使用完流之后都要释放资源,否则资源会被一直占用

写数据的 3 种方式

  • void write(int b) 一次写一个字节数据
  • void write(byte[] b) 一次写一个字节数组数据
  • void write(byte[] b int off,int len) 一次写一个字节数组的部分数据

代码示例:

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

public class ByteStreamDemo {
	public static void main(String[] args) throws IOException {
		FileOutputStream fos = new FileOutputStream("aaa.txt");
		// 一次写一个字节数据
		fos.write(97); // a
		// 一次写一个字节数组数据
		byte[] bytes = {97,98,99,100,101};
		fos.write(bytes); // abcde
		// 一次写一个字节数组的部分数据
		fos.write(bytes, 1, 3); // bcd
		fos.close();
	}
}

换行和续写

  1. 换行写:写出一个换行符即可
    • Windows —— \r\n
    • Linux —— \n
    • Mac —— \r

代码示例:

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

public class ByteStreamDemo {
	public static void main(String[] args) throws IOException {
		FileOutputStream fos = new FileOutputStream("aaa.txt");
		// 写入第一行数据
		String str1 = "hello";
		byte[] bytes1 = str1.getBytes();
		fos.write(bytes1);
		
		// 写入换行符
		String str2 = "\r\n";
		byte[] bytes2 = str2.getBytes();
		fos.write(bytes2);
		
		// 写入第二行数据
		String str3 = "world";
		byte[] bytes3 = str3.getBytes();
		fos.write(bytes3);
		
		fos.close();
	}
}
  1. 续写

    FileOutputStream 有 2 个可以接收布尔类型参数的构造函数,该布尔参数用于指定是否以追加模式打开文件。当这个参数设为 true 时,新写入的数据就会被添加到文件的末尾,实现续写功能。构造函数的具体形式如下:

    // 创建一个向指定 File 对象表示的文件中写入数据的文件输出流。如果 append 为 true,则将字节写入文件末尾处,而不是写入文件开始处
    public FileOutputStream(File file, boolean append) throws FileNotFoundException;
    
    // 创建一个向具有指定 name 的文件中写入数据的输出文件流。如果 append 为 true,则将字节写入文件末尾处,而不是写入文件开始处
    public FileOutputStream(String name, boolean append) throws FileNotFoundException;
    

代码示例:

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

public class ByteStreamDemo {
	public static void main(String[] args) throws IOException {
		FileOutputStream fos = new FileOutputStream("aaa.txt",true);
		// 写入前部分数据
		String str1 = "hello";
		byte[] bytes1 = str1.getBytes();
		fos.write(bytes1);
		
		// 写入后部分数据
		String str2 = " world";
		byte[] bytes2 = str2.getBytes();
		fos.write(bytes2);
		// 输出 "hello world"
		fos.close();
	}
}

原理

基本工作流程

FileOutputStream 的主要功能是将字节数据从程序写入到文件中,其基本工作流程如下:

  1. 创建流对象:通过构造函数创建 FileOutputStream 对象,该对象会关联一个文件,后续的写入操作将针对这个文件进行。
  2. 写入数据:调用 FileOutputStreamwrite() 方法将字节数据写入到关联的文件中。
  3. 关闭流:调用 close() 方法关闭流,释放系统资源。
原理详细剖析
  1. 构造函数

    FileOutputStream 提供了多个构造函数,用于创建不同方式关联文件的流对象,常见的构造函数如下:

    // 创建一个向指定 File 对象表示的文件中写入数据的文件输出流。如果文件存在,则会覆盖原有内容
    public FileOutputStream(File file) throws FileNotFoundException;
    
    // 创建一个向指定 File 对象表示的文件中写入数据的文件输出流。如果 append 为 true,则将字节写入文件末尾处,而不是写入文件开始处
    public FileOutputStream(File file, boolean append) throws FileNotFoundException;
    
    // 创建一个向具有指定名称的文件中写入数据的输出文件流。如果文件存在,则会覆盖原有内容
    public FileOutputStream(String name) throws FileNotFoundException;
    
    // 创建一个向具有指定 name 的文件中写入数据的输出文件流。如果 append 为 true,则将字节写入文件末尾处,而不是写入文件开始处
    public FileOutputStream(String name, boolean append) throws FileNotFoundException;
    

    在创建 FileOutputStream 对象时,Java 会在操作系统层面打开一个文件描述符,该描述符用于标识和操作文件。

  2. 写入数据

    FileOutputStream 提供了多种 write() 方法用于写入数据,常见的方法如下:

    // 将指定的字节写入此文件输出流
    public void write(int b) throws IOException;
    
    // 将 b.length 个字节从指定 byte 数组写入此文件输出流中
    public void write(byte[] b) throws IOException;
    
    // 将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此文件输出流
    public void write(byte[] b, int off, int len) throws IOException;
    

    当调用 write() 方法时,Java 会将数据从用户空间(Java 程序所在的内存空间)复制到内核空间的缓冲区中。内核空间是操作系统管理的内存空间,用于与硬件设备进行交互。

  3. 关闭流

    在使用完 FileOutputStream 后,必须调用 close() 方法关闭流,以释放系统资源。关闭流时,操作系统会将缓冲区中的数据全部写入磁盘,并关闭文件描述符。

    // 关闭此文件输出流并释放与此流有关的所有系统资源
    public void close() throws IOException;
    

FileInputStream 类

操作本地文件的字节输入流,可以把本地文件中的数据读取到程序中来

基本操作

操作步骤

  1. 创建字节输入流对象

    FileInputStream fis = new FileInputStream("aaa.txt");
    
  2. 读取数据

    fis.read();
    
  3. 释放资源

    fis.close();
    

代码示例

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

public class ByteStreamDemo {
	public static void main(String[] args) throws IOException {
		// 创建字节输入流对象
        FileInputStream fis = new FileInputStream("aaa.txt");
        // 读取数据
		int b = fis.read(); // 文件中的数据为 a
		System.out.println(b); // 输出 97
        // 释放资源
		fis.close();
	}
}

注意事项

  1. 创建字节输入流对象

    • 如果文件不存在,程序直接报错
  2. 读取数据

    • 一次只读一个字节,读出来的是数据对应的 ASCII 数字

    • 读到文件末尾,read() 返回 -1

  3. 释放资源

    • 每次使用完流之后都要释放资源,否则资源会被一直占用

循环读取

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

public class ByteStreamDemo {
	public static void main(String[] args) throws IOException {
		// 创建字节输入流对象
        FileInputStream fis = new FileInputStream("aaa.txt");
        // 循环读取
		int b;
		while((b = fis.read()) != -1) {
			System.out.println((char)b);
		}
        // 释放资源
		fis.close();
	}
}

注意事项

循环体不能写成以下形式:

while(fis.read() != -1) {
			System.out.println(fis.read());
}

while 循环条件 fis.read() != -1 中调用 fis.read() 方法会读取一个字节,当这个字节不是文件结束标志(即返回值不为 -1)时,循环继续执行。
而在循环体中的 System.out.println(fis.read()); 又再次调用了 fis.read() 方法,这会导致每次循环读取了两个字节:一个用于判断循环条件,一个用于输出。
这就会出现跳过字节读取的情况,比如文件内容是 “abc”,按这个逻辑,循环条件读取了 “a” 后进入循环,循环体又读取 “b” 并输出,下一次循环条件读取 “c” 后进入循环,又试图读取下一个字节(但文件已结束),就可能出现意料之外的结果或错误。

文件拷贝

小文件拷贝

核心思想:边读边写

代码示例

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

public class ByteStreamDemo {
	public static void main(String[] args) throws IOException {
		// 创建对象
		FileInputStream fis = new FileInputStream("1.mp4");
		FileOutputStream fos = new FileOutputStream("2.mp4");
		
		// 循环读取
		int b;
		while((b = fis.read()) != -1) {
			fos.write(b);
		}
		
		// 释放资源
		fos.close();
		fis.close();
	}
}

注意事项

  1. 创建对象时,文件的后缀名要一致
  2. 释放资源时,先使用的资源要最后释放
大文件拷贝

由于 read() 方法一次只读一个字节数据,如果遇到很大的文件,那么拷贝速度就会很慢,所以我们可以使用 read() 方法的重载形式来拷贝:

  • public int read(byte[] buffer) 一次读一个字节数组数据(每次读取会尽可能把数组装满)

注意事项:数组本身也占用空间,如果创建过大的长度,内存可能奔溃,所以创建数组的时候,长度最好选择 1024B 的整数倍,例如 1024 * 1024 * 5,也就是 5MB

代码示例

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

public class ByteStreamDemo {
	public static void main(String[] args) throws IOException {
		// 创建对象
		FileInputStream fis = new FileInputStream("aaa.txt"); // 数据为 abc
		FileOutputStream fos = new FileOutputStream("bbb.txt")
		// 读取数据
		byte[] bytes = new byte[3];
		int len = fis.read(bytes);
        fos.write(bytes);
        System.out.println("本次读取到" + len + "字节的数据"); // 3
		String str = new String(bytes);
		System.out.println("本次读取的内容为:" + str); // abc
		
		// 释放资源
		fos.close();
		fis.close();
	}
}

接下来我们看另一段代码,假设 aaa.txt 中的数据是 abcde,字节数组的长度为 2:

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

public class ByteStreamDemo {
	public static void main(String[] args) throws IOException {
		// 创建对象
		FileInputStream fis = new FileInputStream("aaa.txt");
		
		// 读取数据
		byte[] bytes = new byte[2];
		int len1 = fis.read(bytes);
		System.out.println("本次读取到" + len1 + "字节的数据");
		String str1 = new String(bytes);
		System.out.println("本次读取的内容为:" + str1);
		
		int len2 = fis.read(bytes);
		System.out.println("本次读取到" + len2 + "字节的数据");
		String str2 = new String(bytes);
		System.out.println("本次读取的内容为:" + str2);
		
		int len3 = fis.read(bytes);
		System.out.println("本次读取到" + len3 + "字节的数据");
		String str3 = new String(bytes);
		System.out.println("本次读取的内容为:" + str3);
		
		int len4 = fis.read(bytes);
		System.out.println("本次读取到" + len4 + "字节的数据");
		String str4 = new String(bytes);
		System.out.println("本次读取的内容为:" + str4);
		
		// 释放资源
		fos.close();
		fis.close();
	}
}

运行结果如下:

本次读取到2字节的数据
本次读取的内容为:ab
本次读取到2字节的数据
本次读取的内容为:cd
本次读取到1字节的数据
本次读取的内容为:ed
本次读取到-1字节的数据
本次读取的内容为:ed

问题

  • 为什么第三次读取到 1 字节的数据,却显示读取到的内容为 ed?
  • 为什么第四次数据已经读取完了,却还是显示读取到的内容为 ed?

原因

在第一次读取前,指针指向 a,字节数组内容为空

在这里插入图片描述

第一次读取,字节数组长度为 2,a 和 b 进入数组,所以指针向后移两位到 c

在这里插入图片描述

第二次读取,c 和 d 进入数组,把 a 和 b 覆盖,指针向后移两位到 e

在这里插入图片描述

第三次读取,由于只剩下 e,进入数组后只把 c 覆盖了,但是 d 还在数组里,所以读取到的内容为 ed

在这里插入图片描述

第四次读取,由于在上一次读取时,已经将数据读取完了,不会有新的数据把数组中的内容覆盖,所以输出的内容仍为 ed

在这里插入图片描述

我们可以通过使用字符串的 String(byte[] b int off,int len)方法来解决残留的数据:

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

public class ByteStreamDemo {
	public static void main(String[] args) throws IOException {
		// 创建对象
		FileInputStream fis = new FileInputStream("aaa.txt");
		FileOutputStream fos = new FileOutputStream("bbb.txt");
		
		// 读取数据
		byte[] bytes = new byte[2];
		int len1 = fis.read(bytes);
		System.out.println("本次读取到" + len1 + "字节的数据");
		String str1 = new String(bytes,0,len1);
		System.out.println("本次读取的内容为:" + str1);
		
		int len2 = fis.read(bytes);
		System.out.println("本次读取到" + len2 + "字节的数据");
		String str2 = new String(bytes,0,len2);
		System.out.println("本次读取的内容为:" + str2);
		
		int len3 = fis.read(bytes);
		System.out.println("本次读取到" + len3 + "字节的数据");
		String str3 = new String(bytes,0,len3);
		System.out.println("本次读取的内容为:" + str3);
		
		// 释放资源
		fos.close();
		fis.close();
	}
}

程序运行结果如下:

本次读取到2字节的数据
本次读取的内容为:ab
本次读取到2字节的数据
本次读取的内容为:cd
本次读取到1字节的数据
本次读取的内容为:e

那么同理我们可以得到通过使用write(byte[] b int off,int len)方法来解决读取数据时的数据残留,以下是大文件拷贝的代码示例:

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

public class ByteStreamDemo {
	public static void main(String[] args) throws IOException {
		// 创建对象
		FileInputStream fis = new FileInputStream("aaa.txt");
		FileOutputStream fos = new FileOutputStream("bbb.txt");
		
		// 读取数据
		int len;
		byte[] bytes = new byte[1024 * 1024 * 5];
		while((len = fis.read(bytes)) != -1) {
			fos.write(bytes, 0, len);
		}
		
		// 释放资源
		fos.close();
		fis.close();
	}
}

相关文章:

  • Starlink卫星动力学系统仿真建模第九讲-滑模(SMC)控制算法原理简介及卫星控制应用
  • 深入理解Self-Attention - 原理与等价表示
  • 15.1 智能销售顾问系统架构与业务价值解析:AI 如何重塑销售流程
  • RTOS系统ulTaskNotifyTake怎么知道哪个发送任务通知函数的pxcurrentTCB
  • 4.WebSocket 配置与Nginx 的完美结合
  • react路由总结
  • IDEA搭建SpringBoot,MyBatis,Mysql工程项目
  • 学习threejs,使用createMultiMaterialObject创建多材质对象
  • 小视频压缩实用方法大汇总
  • 6.2 - UART串口数据发送之轮询
  • python-leetcode 42.验证二叉搜索树
  • 嵌入式学习|C语言篇进程间通信(IPC)全面解析与示例
  • 地铁站内导航系统:基于蓝牙Beacon与AR技术的动态路径规划技术深度剖析
  • 【OMCI实践】ONT上线过程的omci消息(五)
  • 23种设计模式的cpp举例
  • 蓝桥杯 Java B 组之最短路径算法(Dijkstra、Floyd-Warshall)
  • 纯电动轻型载货车能量流测试优化研究
  • 系统架构设计:软件工程部分知识概述
  • JUC并发—12.ThreadLocal源码分析
  • 【数据结构】 最大最小堆实现优先队列 python
  • 网站建设过时了吗/商业软文案例
  • 做百科需要用什么网站做参考/佛山网站建设正规公司
  • 可不可以用p2p做视频网站/中国培训网的证书含金量
  • 公司有网站域名后如何建网站/六盘水seo