IO系列(一) -一文带你读懂 java 中的IO流!

一、摘要

说到 IO,相信大家都不陌生,英文全称:Input/Output即输入/输出,通常指数据在内部存储器和外部存储器或其他周边设备之间的输入和输出。

比如我们常用的SD卡、U盘、移动硬盘等等存储文件的硬件设备,当我们将其插入电脑的 usb 硬件接口时,我们就可以从电脑中读取设备中的信息或者写入信息,这个过程就涉及到 I/O 的操作。

当然,涉及 I/O 的操作,也不仅仅局限于硬件设备的读写,还有网络数据的传输。比如,我们在电脑上用浏览器搜索互联网上的信息,这个信息的过程也涉及到 I/O 的操作。

无论是从磁盘中读写文件,还是在网络中传输数据,可以说 I/O 主要为处理人机交互、机与机交互中获取和交换信息提供的一套解决方案。

在 Java 的 IO 体系中,类将近有 80 个,位于java.io包下,初步看起来感觉非常复杂,但是经过一番梳理之后,你会发现还是有规律可循的。

传输数据的格式角度看,可以大致分为两组:

  • 基于字节操作的 I/O 接口:InputStream 和 OutputStream
  • 基于字符操作的 I/O 接口:Reader 和 Writer

传输数据的方式角度看,也可以大致分为两组:

  • 基于磁盘操作的 I/O 接口:File
  • 基于网络操作的 I/O 接口:Socket

虽然 Socket 类并不在java.io包下,但是我们仍然把它们划分在一起,因为 I/O 的核心问题,要么是数据格式影响 I/O 操作,要么是传输方式影响 I/O 操作,也就是将什么样的数据写到什么地方的问题。

I/O 只是人与机器或者机器与机器交互的手段,除了在它们能够完成这个交互功能外,我们关注的就是如何提高它的运行效率,而数据格式和传输方式是影响效率最关键的因素。

下面我们基于这两点,来展开分析!

二、传输格式的分类

从传输格式角度看,可以分两类:字节流和字符流。

  • 基于字节的输入和输出操作接口分别是:InputStream 和 OutputStream
  • 基于字符的输入和输出操作接口分别是:Reader 和 Writer 。

2.1、字节流接口

字节流,是 I/O 流中最底层的流,能处理任何类型的数据传输,比如文字、图片、视频、文件等。

2.1.1、基于字节输入流的接口

打开 JDK 源码,整理之后,InputStream 输入流接口的类继承层次如下图所示:

这些输入流类,根据角色不同,还可以进行分类,分为:节点流和处理流。

  • 节点流:指的是向指定的设备,比如磁盘、网络,进行读/写数据,也被称为底层流,直接和数据源相接
  • 处理流:指的是在已存在的节点流或者处理流基础上,包装一些更加方便操作 io 流的功能,比如压缩、序列化、缓冲操作等,也被称为包装流

输入流类,根据角色的划分类别如下:

OutputStream 输出流的类层次结构也是类似。

2.1.2、基于字节输出流的接口

OutputStream 输入流接口的类继承层次如下图所示:

字节输出流类,根据角色的划分类别如下:

这里就不详细的介绍各个子类的使用方法,有兴趣的朋友可以查看 JDK 的 API 说明文档,笔者也会在后期的系列文章会进行详细的介绍。

这里只是重点想说一下,无论是输入还是输出,操作数据的方式可以组合使用,各个处理流的类并不是只操作固定的节点流,比如如下输出方式:

//将文件输出流包装到序列化输出流中,再将序列化输出流包装到缓冲中
OutputStream out = new BufferedOutputStream(new ObjectOutputStream(new FileOutputStream(new File("fileName")))

另外,输出流最终写到什么地方必须要指定,要么是写到硬盘中,要么是写到网络中,从图中可以发现,写网络实际上也是写文件,只不过写到网络中,需要经过底层操作系统将数据发送到其他指定的计算机中,而不是写入到本地硬盘中。

2.2、字符流接口

不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符

那为什么要有操作字符的 I/O 接口呢?

这是因为我们的程序中通常操作的数据都是以字符形式,为了程序操作更方便而提供一个直接写字符的 I/O 接口,仅此而已

除此之外,使用字节流操控文字时不是很方便,容易乱码,由此诞生了不同的字符集以及对应的字符编码规则

由于全世界的文字博大精深,不同的字符集,占用的字节位数不同,以中文为例,在GBK编码规则中,一个中文使用二个字节存储;而在UTF-8编码规则中,一个中文使用三个字节存储,如果写入和读取的编码规则不一样,读取的字节数很容易裂开,导致出现乱码。

比如以下案例:

public static void main(String[] args) throws Exception {byte[] bytes = "学习Java语言".getBytes("ISO8859-1");File file = new File("encoding.txt");OutputStream out = new FileOutputStream(file);out.write(bytes);out.close();
}

文件的内容如下:

??Java??

为了更方便地处理中文这些字符,计算机就推出了字符编码规则。

实现原理:字节流 + 编码表。

  • 当写入一段文字时,会使用指定的字符集,将该 String 编码为一系列字节,将结果存储到新的字节数组中,进行传输
  • 当读取一段文字时,通过指定的字符集,解码指定的字节数组来构造新的 String,从而解决文字乱码的问题。
2.2.1、基于字符输入流的接口

Reader 输入流接口的类继承层次如下图所示:

同样的,字符输入流类,根据角色的划分类别如下:

2.2.2、基于字符输出流的接口

Writer 输出流的类继承层次如下图所示:

字符输出流类,根据角色的划分类别如下:

2.3、字节与字符的转化

刚刚我们说到,不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,设计字符的原因是为了程序更方便的操作文本。

那么怎么将字符转化成字节或者将字节转化成字符呢?

其中,InputStreamReaderOutputStreamWriter就是转化桥梁。

2.3.1、输入流转换方案

输入流字符解码相关类结构的转化过程如下图所示:

从图上可以看到,InputStreamReader类是字节到字符的转化桥梁,
其中StreamDecoder指的是一个解码操作类,Charset指的是字符集。

InputStreamReader的过程需要指定编码字符集,否则将采用操作系统默认字符集,很可能会出现乱码问题,StreamDecoder则是完成字节到字符的解码的实现类。

案例如下:

File file = new File("encoding.txt");
FileInputStream inputStream =new FileInputStream(file);
//字节输入流转为字符输入流
InputStreamReader streamReader =new InputStreamReader(inputStream, Charset.forName("UTF-8"));
2.3.2、输出流转换方案

输出流转化过程也是类似,如下图所示:

通过OutputStreamWriter类完成字符到字节的编码过程,由StreamEncoder 完成编码过程。

案例如下:

File file = new File("output.txt");
FileOutputStream outputStream =new FileOutputStream(file);
//字符输出流转字节输出流
OutputStreamWriter streamWriter =new OutputStreamWriter(outputStream, Charset.forName("UTF-8"));

三、传输方式的分类

上文我们介绍了数据的传输格式,可以通过字节流和字符流接口来完成数据的传输,至于数据写到何处,主要取决于数据的传输方式。

从传输方式角度看,可以分两类:磁盘和网络。

  • 基于磁盘操作的操作接口是:File
  • 基于网络操作的操作接口是:Socket
3.1、文件接口

我们知道数据在磁盘的唯一最小描述就是文件,也就是说上层应用程序只能通过文件来操作磁盘上的数据,文件也是操作系统和磁盘驱动器交互的一个最小单元。

在 Java I/O 体系中,File类是唯一代表磁盘文件本身的对象

File 类定义了一些与平台无关的方法来操作文件,包括检查一个文件是否存在、创建、删除文件、重命名文件、判断文件的读写权限是否存在、设置和查询文件的最近修改时间等等操作。

值得注意的是 Java 中通常的 File 并不代表一个真实存在的文件对象,当你通过指定一个路径描述符时,它就会返回一个代表这个路径相关联的一个虚拟对象,这个可能是一个真实存在的文件或者是一个包含多个文件的目录。

例如,读取一个文件内容,程序如下:

public static void main(String[] args) throws Exception {StringBuilder str = new StringBuilder();char[] buf = new char[1024];// 读取文件的内容FileReader f = new FileReader("input.txt");while(f.read(buf)>0){str.append(buf);}str.toString();
}

以上面的程序为例,从硬盘中读取一段文本字符,操作流程如下图:

当我们传入一个指定的文件名来创建File对象,通过FileReader来读取文件内容时,会自动创建一个FileInputStream对象来读取文件内容,也就是我们上文中所说的字节流来读取文件。

紧接着,会创建一个FileDescriptor的对象,其实这个对象就是真正代表一个存在的文件对象的描述。

由于我们需要读取的是字符格式,所以需要StreamDecoder类通过解码方法decode,将字节转字符,至于如何从磁盘驱动器上读取一段数据,由操作系统帮我们完成。

3.2、网络接口

继续来说说数据传输的另一种处理方式:网络通信。

3.2.1、Socket 简介

在 Java 网络体系中,Socket是描述计算机之间完成相互通信一种抽象定义。

光从描述看可能很难理解,打个比方,可以把Socket比作为两个城市之间的交通工具,有了它,就可以在城市之间来回穿梭了;并且,交通工具有多种,每种交通工具也有相应的交通规则。

Socket 也一样,也有多种,大部分情况下我们使用的都是基于 TCP/IP 的流套接字,它是一种稳定的通信协议。

比较典型的基于 Socket 通信的应用程序场景,如下图:

主机 A 的应用程序要想和主机 B 的应用程序通信,必须通过 Socket 建立连接,而建立 Socket 连接必须需要底层 TCP/IP 协议来建立 TCP 连接。

3.2.2、建立通信链路

我们知道网络层使用的 IP 协议可以帮助我们根据 IP 地址来找到目标主机,但是一台主机上可能运行着多个应用程序,如何才能与指定的应用程序通信呢?

这个时候需要通过 TCP 或 UPD 协议,也就是指定对应的端口号

通过 IP + 端口号,就可以创建一个代表唯一一个主机上的一个应用程序的通信链路了,创建后的通信链路我们称它为 Socket 实例。

以 TCP 协议为例,为了准确无误地把数据送达目标处,TCP 协议采用了三次握手策略,如下图:

其中,SYN 全称为 Synchronize Sequence Numbers,表示同步序列编号,是 TCP/IP 建立连接时使用的握手信号。

ACK 全称为 Acknowledge character,即确认字符,表示发来的数据已确认接收无误。

在客户机和客户机之间建立正常的 TCP 网络连接时,发送端首先发出一个 SYN 消息,接收端使用 SYN + ACK 应答表示接收到了这个消息,最后发送端再以 ACK 消息响应。

整体流程如下:

  • 发送端 –(发送带有 SYN 标志的数据包 )–> 接受端(第一次握手);
  • 接受端 –(发送带有 SYN + ACK 标志的数据包)–> 发送端(第二次握手);
  • 发送端 –(发送带有 ACK 标志的数据包) –> 接受端(第三次握手);

完成三次握手之后,发送端和接收端之间建立起可靠的 TCP 连接,客户端应用程序与服务器应用程序就可以开始传送数据了。

3.2.3、传输数据

当客户端要与服务端通信时,客户端首先要创建一个 Socket 实例,也就是指定目标服务器的 IP 和端口。

默认操作系统将为这个 Socket 实例分配一个没有被使用的本地端口号,并创建一个包含本地、远程地址和端口号的套接字数据结构,这个数据结构将一直保存在系统中直到这个连接关闭。

  • 客户端简单示例
public static void main(String[] args) throws IOException {//通过IP和端口与服务端建立连接Socket socket =new Socket("127.0.0.1",8080);//将字符流转化成字节流,并输出BufferedWriter bufferedWriter =new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));String str="Hello,我是客户端!";bufferedWriter.write(str);bufferedWriter.flush();bufferedWriter.close();
}
  • 服务端简单示例
public static void main(String[] args) throws Exception {//初始化服务端socket并且绑定 8080 端口ServerSocket serverSocket = new ServerSocket(8080);//循环监听所有连接的客户端请求while (true){try {//等待客户端的连接Socket socket = serverSocket.accept();//将字节流转化成字符流,读取客户端输入的内容BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream()));//读取一行数据String str = bufferedReader.readLine();//输出打印System.out.println("服务端收到客户端发送的信息:" + str);} catch (Exception e) {}}
}

我们先启动服务端程序,再运行客户端,服务端收到客户端发送的信息,打印结果如下:

服务端收到客户端发送的信息:Hello,我是客户端!

注意,客户端只有与服务端建立三次握手成功之后,才会发送数据,而 TCP/IP 握手过程,底层操作系统已经帮我们实现了!

当连接已经建立成功,服务端和客户端都会拥有一个Socket实例,每个Socket实例都有一个InputStreamOutputStream,正如我们前面所说的,网络 I/O 都是以字节流传输的,Socket正是通过这两个对象来交换数据。

Socket对象创建时,操作系统同时将会为InputStreamOutputStream分别分配一定大小的缓冲区,数据的写入和读取都是通过这个缓存区完成的。

发送端将数据写到OutputStream对应的SendQ队列中,当队列填满时,数据将被发送到另一端InputStreamRecvQ队列中,如果这时RecvQ已经满了,那么OutputStreamwrite方法将会阻塞直到RecvQ队列有足够的空间容纳SendQ发送的数据。

值得特别注意的是,缓存区的大小以及写入端的速度和读取端的速度非常影响这个连接的数据传输效率,由于可能会发生阻塞,所以网络 I/O 和磁盘 I/O 在数据的写入和读取还要有一个协调的过程,如果两边同时传送数据,可能会产生死锁的问题。

如何提高网络 IO 传输效率、保证数据传输的可靠,这个我们后面单独开篇进行讲解。

四、小结

本文阐述的内容较多,整合了很多有用的信息,从 Java 基本的 I/O 类库结构开始说起,主要介绍了 IO 的传输格式和传输方式,包括字节流和字符流接口相关的分类介绍,以及磁盘 I/O 和网络 I/O 的基本工作方式。

内容难免有所遗漏,和理解不到的位置,欢迎网友留言指出!

五、参考

1、https://developer.ibm.com/zh/articles/j-lo-javaio/

六、写到最后

最近无意间获得一份阿里大佬写的技术笔记,内容涵盖 Spring、Spring Boot/Cloud、Dubbo、JVM、集合、多线程、JPA、MyBatis、MySQL 等技术知识。需要的小伙伴可以点击如下链接获取,资源地址:技术资料笔记。

不会有人刷到这里还想白嫖吧?点赞对我真的非常重要!在线求赞。加个关注我会非常感激!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/695671.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Cosmo Bunny Girl

可爱的宇宙兔女郎的3D模型。用额外的骨骼装配到Humanoid上,Apple混合了形状。完全模块化,包括不带衣服的身体。 技术细节 内置,包括URP和HDRP PDF。还包括关于如何启用URP和HDRP的说明。 LOD 0:面:40076,tris 76694,verts 44783 装配了Humanoid。添加到Humanoid中的其他…

前端部署真的不简单

现在大部分的中小型公司部署前端代码都是比较简单的,主要步骤如下: 首先,通过脚手架提供的命令npm run build打包前端代码,生成dist文件夹; 最后,将dist文件夹丢给后台开发人员放在他们的工程里面,随后台…

APNGToGifConverter for Mac:一键转换APNG为GIF的神奇小工具!

APNGToGifConverter for Mac 💻 是一款为Mac用户量身定制的、简单易用的图片格式转换工具。它能够将APNG(动画PNG)文件快速转换为GIF(图形交换格式)文件,让你的图片动画轻松在各种平台和设备上播放。 &…

Spring Boot整合ElasticSearch实战 - 第511篇

历史文章(文章累计500) 《国内最全的Spring Boot系列之一》 《国内最全的Spring Boot系列之二》 《国内最全的Spring Boot系列之三》 《国内最全的Spring Boot系列之四》 《国内最全的Spring Boot系列之五》 《国内最全的Spring Boot系列之六》 《…

抱怨无用,行动破局

故事的开始 这个专栏,以及本文的目的,是为了记录我从创立盘多啦这个平台开始,到后续的发展历程的专栏。同时也是给自己一个坚持的动力和警醒的作用。 首先,我是一名程序员,并且对于自身感兴趣的东西,都有…

算法学习笔记(3)-差分

#差分 差分和前缀和互为逆运算: 给定一个原数组s,差分数组h,两者的关系如下所示: s[i] h[1] h[2] h[3] …… h[i] 针对于上面的公式,由差分数组h推导而来 h[1] s[1] h[2] s[2] - s[1] h[3] s[3] - [2] …… h[…

什么是 RAG,大模型微调,向量数据库的应用场景

原来向量数据库的应用场景是这样的!按照我的理解,大模型其实是没有学习能力的,它就相当于一个真值表或者矩阵,给它输入,它就输出,在使用它的过程中它不会自己训练自己,改变既有的参数&#xff0…

【深度学习】实验3 特征处理

特征处理 python 版本 3.7 scikit-learn 版本 1.0.2 1.标准化 from sklearn.preprocessing import StandardScaler from sklearn.preprocessing import MinMaxScaler from matplotlib import gridspec import numpy as np import matplotlib.pyplot as plt cps np.random.…

开源免费的定时任务管理系统:Gocron

Gocron:精准调度未来,你的全能定时任务管理工具!- 精选真开源,释放新价值。 概览 Gocron是github上一个开源免费的定时任务管理系统。它使用Go语言开发,是一个轻量级定时任务集中调度和管理系统,用于替代L…

免费获取SSL证书的几种方法

免费获取SSL证书的方法有很多种,以下是一些常见的途径: 1、Lets Encrypt:Lets Encrypt是一个由非营利组织提供的免费SSL证书服务,其安装部署简单、方便,且已被Firefox、Chrome、IE等浏览器所支持。您可以通过其官方网…

C++入门——引用(2)

前言 上一节我们开始学习了C,并且对C有了初步的了解,这一节我们继续学习C的基础,那么废话不多说,我们正式进入今天的学习 C中的引用 1.1引用的概念 引用不是新定义一个变量,而是给已存在变量取了一个别名&#xff0…

【Ubuntu永久授权串口设备读取权限“/dev/ttyUSB0”】

Ubuntu永久授权串口设备读取权限 1 问题描述2 解决方案2.1 查看ttyUSB0权限,拥有者是root,所属用户组为dialout2.2 查看dialout用户组成员,如图所示,普通用户y不在dialout组中2.3 将普通用户y加入dialout组中2.4 再次查看dialout用…