类将近有 80 个,位于java.io包下,感觉很复杂,但是这些类大致可以分成四组
传输数据的数据格式:
1. 基于字节操作的 I/O 抽象类:InputStream 和 OutputStream
2. 基于字符操作的 I/O 抽象类:Writer 和 Reader

传输数据的方式:
3. 基于磁盘操作的 I/O 接口:File
4. 基于网络操作的 I/O 接口:Socket Socket 类并不在 java.io

基于字节操作的接口

基于字节的输入和输出操作抽象类分别是:InputStream 和 OutputStream 。
Java 的 IO 体系 - 图1
字节输出流OutputStream 输出流的类继承层次如下图所示:
Java 的 IO 体系 - 图2
无论是输入还是输出,操作数据的方式可以组合使用,各个处理流的类并不是只操作固定的节点流

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

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

基于字符操作的接口

不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符,但是为什么要有操作字符的 I/O 接口呢?

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

基于字符的输入和输出操作抽象类分别是:Reader 和 Writer ,下图是字符的 I/O 操作接口涉及到的类结构图。
Java 的 IO 体系 - 图3
不管是 Reader 还是 Writer 类,它们都只定义了读取或写入数据字符的方式,也就是说要么是读要么是写,但是并没有规定数据要写到哪去,写到哪去就是我们后面要讨论的基于磁盘或网络的工作机制。

字节与字符的转化

刚刚我们说到,不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,设计字符的原因是为了程序操作更方便,那么怎么将字符转化成字节或者将字节转化成字符呢?

InputStreamReader 和 OutputStreamWriter 就是转化桥梁。
其中StreamDecoder指的是一个解码操作类,Charset指的是字符集。
InputStream 到 Reader 转化过程:
Java 的 IO 体系 - 图4

Writer 到 OutputStream 转化过程
Java 的 IO 体系 - 图5

基于磁盘操作的接口

还有一个关键问题就是数据写到何处,其中一个主要的处理方式就是将数据持久化到物理磁盘。

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

File类

在 Java I/O 体系中,File 类是唯一代表磁盘文件本身的对象。
File 类定义了一些与平台无关的方法来操作文件,包括检查一个文件是否存在、创建、删除文件、重命名文件、判断文件的读写权限是否存在、设置和查询文件的最近修改时间等等操作。

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

基于网络操作的接口

Socket

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

每个 Socket 实例都有一个 InputStream 和 OutputStream,正如我们前面所说的,网络 I/O 都是以字节流传输的,Socket 正是通过这两个对象来交换数据。
learn more

IO工作方式

在计算机中,IO 传输数据有三种工作方式,分别是 BIO、NIO、AIO。

同步与异步的区别:
同步就是发起一个请求后,接受者未处理完请求之前,不返回结果。
异步就是发起一个请求后,立刻得到接受者的回应表示已接收到请求,但是接受者并没有处理完,接受者通常依靠事件回调等机制来通知请求者其处理结果。
阻塞和非阻塞的区别:
阻塞就是请求者发起一个请求,一直等待其请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
非阻塞就是请求者发起一个请求,不用一直等着结果返回,可以先去干其他事情,当条件就绪的时候,就自动回来。

而我们要讲的 BIO、NIO、AIO 就是同步与异步、阻塞与非阻塞的组合。

BIO 同步阻塞 IO

采用 BIO 通信模型的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。
Java 的 IO 体系 - 图6
我们一般在服务端通过while(true)循环中会调用accept() 方法等待监听客户端的连接, 是典型的一请求一应答通信模型 。
缺点: 线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。

伪异步 BIO

我们可以通过使用 Java 中 ThreadPoolExecutor 线程池机制来改善,让线程的创建和回收成本相对较低,保证了系统有限的资源的控制,实现了 N (客户端请求数量)大于 M (处理客户端请求的线程数量)的伪异步 I/O 模型。
Java 的 IO 体系 - 图7
当有新的客户端接入时,将客户端的 Socket 封装成一个 Task 投递到后端的线程池中进行处理。

NIO 同步非阻塞 IO

NIO 中的 N 可以理解为 Non-blocking,一种同步非阻塞的 I/O 模型
image.png
服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

NIO 这两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。
Java 的 IO 体系 - 图9

应用建议

对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发效率和更好的维护性;
对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

AIO 异步非阻塞 IO

最后就是 AIO 了,全称 Asynchronous I/O,可以理解为异步 IO,也被称为 NIO 2,在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型,也就是我们现在所说的 AIO。

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

BIO、NIO、AIO适用场景分析:

BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。

NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。

AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

面试

1. 字节流和字符流哪个好?怎么选择?

使用字节流更好,因为所有的文件在硬盘或在传输时都是以字节的方式进行的,包括图片等都是按字节的方式存储的,而字符是只有在内存中才会形成,所以在开发中,字节流使用较为广泛。

如果对于操作需要通过 IO 在内存中频繁处理字符串的情况使用字符流会好些,因为字符流具备缓冲区,提高了性能

2. 什么是缓冲区?有什么作用?

缓冲区就是一段特殊的内存区域,很多情况下当程序需要频繁地操作一个资源(如文件或数据库)则性能会很低,所以为了提升性能就可以将一部分数据暂时读写到缓存区,以后直接从此区域中读写数据即可,这样就显著提升了性。
对于 Java 字符流的操作都是在缓冲区操作的,所以如果我们想在字符流操作中主动将缓冲区刷新到文件则可以使用 flush() 方法操作。

  1. 什么是Java序列化,如何实现Java序列化?
    序列化就是一种用来处理对象流的机制,将对象的内容进行流化。可以对流化后的对象进行读写操作,可以将流化后的对象传输于网络之间。序列化是为了解决在对象流读写操作时所引发的问题
    序列化的实现:将需要被序列化的类实现Serialize接口,没有需要实现的方法,此接口只是为了标注对象可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,再使用ObjectOutputStream对象的write(Object obj)方法就可以将参数obj的对象写出
  2. PrintStream、BufferedWriter、PrintWriter的比较?
    PrintStream类的输出功能非常强大,通常如果需要输出文本内容,都应该将输出流包装成PrintStream后进行输出。它还提供其他两项功能。与其他输出流不同,PrintStream 永远不会抛出 IOException;而是,异常情况仅设置可通过 checkError 方法测试的内部标志。另外,为了自动刷新,可以创建一个 PrintStream
    BufferedWriter:将文本写入字符输出流,缓冲各个字符从而提供单个字符,数组和字符串的高效写入。通过write()方法可以将获取到的字符输出,然后通过newLine()进行换行操作。BufferedWriter中的字符流必须通过调用flush方法才能将其刷出去。并且BufferedWriter只能对字符流进行操作。如果要对字节流操作,则使用BufferedInputStream
    PrintWriter的println方法自动添加换行,不会抛异常,若关心异常,需要调用checkError方法看是否有异常发生,PrintWriter构造方法可指定参数,实现自动刷新缓存(autoflush)
  3. BufferedReader属于哪种流,它主要是用来做什么的,它里面有那些经典的方法?
    属于处理流中的缓冲流,可以将读取的内容存在内存里面,有readLine()方法,它,用来读取一行
  4. 什么是节点流,什么是处理流,它们各有什么用处,处理流的创建有什么特征?
    节点流 直接与数据源相连,用于输入或者输出
    处理流:在节点流的基础上对之进行加工,进行一些功能的扩展
    处理流的构造器必须要 传入节点流的子类
    9.流一般需要不需要关闭,如果关闭的话在用什么方法,一般要在那个代码块里面关闭比较好,处理流是怎么关闭的,如果有多个流互相调用传入是怎么关闭的?
    流一旦打开就必须关闭,使用close方法
    放入finally语句块中(finally 语句一定会执行)
    调用的处理流就关闭处理流
    多个流互相调用只关闭最外层的流
  5. InputStream里的read()返回的是什么,read(byte[] data)是什么意思,返回的是什么值?
    返回的是所读取的字节的int型(范围0-255)
    read(byte [ ] data)将读取的字节储存在这个数组。返回的就是传入数组参数个数
  6. OutputStream里面的write()是什么意思,write(byte b[], int off, int len)这个方法里面的三个参数分别是什么意思?
    write将指定字节传入数据源
    Byte b[ ]是byte数组
    b[off]是传入的第一个字符、b[off+len-1]是传入的最后的一个字符 、len是实际长度

本文摘抄转载自

  1. Just Do Java
  2. CSDN博主「SileeLiu」