Java I/O
IO基础
Java 中的 I/O(输入输出) 是指程序与外部环境(如文件、网络、控制台等)之间的数据交换。Java 提供了丰富的 I/O 类库,支持不同类型的数据流操作。Java I/O 的核心概念基于流(Stream),流可以分为字节流和字符流,具体包括以下几个重要部分:
1. 字节流
字节流是处理所有 I/O 操作的基础,适用于所有类型的数据(包括文本和二进制数据)。它通过一个字节(8 位)为单位进行数据传输。字节流有两个主要类:
InputStream
/OutputStream
:基类,定义了字节流的基本读写操作。- 常见的实现类包括
FileInputStream
、FileOutputStream
、BufferedInputStream
、BufferedOutputStream
等。
2. 字符流
字符流专门用于处理文本数据,它是基于字节流之上的封装,使用字符(16 位)作为数据传输单位。字符流可以自动进行字符编码和解码,避免手动管理字符集问题。字符流有两个主要基类:
Reader
/Writer
:基类,提供字符流的基本操作。- 常见的实现类包括
FileReader
、FileWriter
、BufferedReader
、BufferedWriter
等。
3. 缓冲流
无论是字节流还是字符流,都有缓冲流的变种,它们通过缓冲区提高读写性能。缓冲流通过为流提供内存缓冲区,减少磁盘或网络 I/O 操作的次数,从而提高效率。常用的缓冲流有:
BufferedInputStream
/BufferedOutputStream
(字节流)BufferedReader
/BufferedWriter
(字符流)
4. 数据流
数据流用于从文件中读写基本数据类型(如 int
、long
、float
等),并支持跨平台的字节序(大端、小端)问题。常用的类有:
DataInputStream
/DataOutputStream
:用于读写 Java 基本数据类型。
5. 对象流
对象流用于读写 Java 对象的序列化数据。它通过将对象转换为字节流(序列化)来保存对象的状态,并能够恢复对象(反序列化)。常用的类有:
ObjectInputStream
/ObjectOutputStream
:用于对象的序列化和反序列化。
6. NIO
NIO 是 Java 1.4 引入的 I/O API,提供了比传统 I/O 更高效的方式来处理大规模数据传输。与传统的流(Stream)不同,NIO 使用了缓冲区和通道(Channel),并支持非阻塞 I/O。核心组件包括:
- Buffer:用来读写数据的容器。
- Channel:数据的输入输出通道。
- Selector:支持多路复用的非阻塞 I/O。
通过 NIO,Java 可以实现高效的文件、网络、Socket 操作,广泛用于高性能服务器端编程和大数据处理。
7. NIO 2.0
Java 7 引入了 NIO 2.0(即 java.nio.file
包),提供了更强大的文件操作 API,如文件遍历、文件属性读取、文件元数据等,弥补了传统 I/O 的一些不足,增强了对文件系统的支持。
IO设计模式
装饰器模式
装饰器模式是一种结构型设计模式,用于动态地为一个对象添加额外的职责(功能)。它通过创建一个包装对象,委托原始对象执行基本功能,同时在此基础上添加新的行为。装饰器模式主要有两个关键特点:
- 不改变原对象的结构:通过包装对象而不是继承,来扩展功能。
- 功能扩展是动态的:可以在运行时根据需要动态地为对象添加新功能。
Java I/O 中的装饰器模式
在 Java I/O 中,尤其是缓冲流和过滤流的实现中,装饰器模式的应用非常明显。例如:
BufferedInputStream
和BufferedOutputStream
: 这些类都是对InputStream
和OutputStream
类的装饰器,它们并没有自己独立的读写功能,而是将原始的 I/O 流包装起来,通过缓冲机制来提升读写性能。BufferedInputStream
会在底层流中创建一个缓冲区,每次读取时,先从缓冲区读取数据,而不是每次都去磁盘或网络读数据,从而减少了 I/O 操作的次数,提高了性能。BufferedOutputStream
也采用类似的方式,先将数据写入缓冲区,待缓冲区填满后一次性写入磁盘,减少了磁盘的写入操作。
这两个类的工作原理就是装饰了原始流,使得原有的流功能得到了扩展,但并不改变它们原本的行为。
BufferedReader
和BufferedWriter
: 类似地,BufferedReader
和BufferedWriter
是对Reader
和Writer
类的装饰器。它们通过提供缓冲区来提高读取和写入字符数据的效率。原始的FileReader
和FileWriter
类是按字符读取和写入的,而BufferedReader
和BufferedWriter
在此基础上增加了缓冲的功能,使得 I/O 操作变得更高效。
装饰器模式在 I/O 中的作用
装饰器模式的引入,使得我们可以在不修改原有类的情况下,灵活地扩展类的功能。通过组合不同的装饰器,我们可以创建具有多种功能的流。例如:
- 使用
BufferedReader
来增加缓冲功能,提升读取效率。 - 使用
DataInputStream
来支持读取 Java 原始数据类型。 - 使用
ObjectInputStream
来支持对象的序列化和反序列化。
这种设计模式具有很大的灵活性和扩展性,使得我们可以根据需求选择不同的装饰器,组合成适合自己业务的 I/O 流。
适配器模式
适配器模式是一种结构型设计模式,它允许不兼容的接口之间进行交互。通过创建一个适配器类,将原本不兼容的接口转化为客户需要的接口。适配器模式的主要目的是解耦,使得接口的使用者不需要关心被适配者的具体实现,只需要通过适配器来与其交互。
适配器模式在 Java I/O 中的应用
在 Java I/O 中,适配器模式常常用来将不同的数据源(如文件、网络、内存等)转换为统一的输入输出流接口,使得它们能够兼容地进行读写操作。Java 的 I/O 系统中有很多类是通过适配器模式进行设计的,最典型的例子是:
InputStream 和 OutputStream 之间的适配
InputStream
和OutputStream
是字节流的基类,而它们的子类代表了不同的数据源(如文件、网络等)。这些不同的数据源提供了不同的读取写入方式,但它们都可以通过字节流接口统一处理。具体来说,每个InputStream
子类都实现了与文件、网络或内存进行交互的细节,而不需要用户去关心这些底层实现细节。比如:FileInputStream
:用于从文件中读取字节数据。ByteArrayInputStream
:用于从字节数组中读取数据。BufferedInputStream
:对其他字节流进行装饰,增加缓冲区提高性能。
每种流类型都是
InputStream
的一个适配器,它们适配了不同的数据来源,使得客户端程序可以统一使用InputStream
接口来读取数据。Reader 和 Writer 之间的适配 类似于字节流,字符流也通过适配器模式进行适配。
Reader
和Writer
是字符流的基类,用于处理字符数据的输入和输出。而实际的数据来源(文件、字符数组、字符串等)通过不同的实现类来适配。例如:FileReader
:适配从文件中读取字符数据。StringReader
:适配从字符串中读取字符数据。CharArrayReader
:适配从字符数组中读取数据。
BufferedReader
和BufferedWriter
则是对这些流的适配器,提供缓冲区支持,以提高读取和写入效率。InputStreamReader 和 OutputStreamWriter
InputStreamReader
和OutputStreamWriter
是经典的适配器模式应用。它们允许我们将字节流转换为字符流,或者将字符流转换为字节流。由于字符流是按字符(16 位)处理的,而字节流是按字节(8 位)处理的,字符流和字节流需要通过适配器来进行相互转换:InputStreamReader
将字节流(如FileInputStream
)适配为字符流。OutputStreamWriter
将字符流(如FileWriter
)适配为字节流。
这种方式使得我们可以在不同的字符编码和字节编码之间进行转换,而不需要手动处理编码问题。
适配器模式的优点
- 解耦:适配器模式让客户端代码与具体的流实现解耦,可以灵活地替换不同的数据源,而不影响客户端的业务逻辑。
- 增强灵活性:通过适配器,Java I/O 允许我们组合不同的流,实现更多样化的功能,比如缓冲、压缩、加密等,而无需修改底层的流实现。
- 便于扩展:当需要增加新的 I/O 功能时,只需要实现新的适配器类,客户端不需要做任何修改。
工厂模式
1. InputStreamReader
和 OutputStreamWriter
这两个类通过工厂模式的方式实现了字节流和字符流之间的转换。InputStreamReader
将字节流转换为字符流,而 OutputStreamWriter
则是将字符流转换为字节流。通过这些类,开发者只需要提供输入输出流和编码类型,工厂方法会根据提供的参数创建适合的流对象。
2. Files
类的流创建方法
Java 8 中的 Files
类提供了一系列的工厂方法,比如 newBufferedReader()
、newBufferedWriter()
、newInputStream()
等。这些方法负责根据不同的需求(如文件路径、编码等)返回不同类型的流对象。
3. ObjectInputStream
和 ObjectOutputStream
这两个类封装了对象的序列化和反序列化过程,从而通过工厂方式提供创建对象流的方法,简化了对象的读取和写入。
总结来说,Java I/O 通过工厂模式为流的创建提供了灵活和简洁的机制,使得开发者无需关心流对象的具体实现,提升了代码的可扩展性和可维护性。
观察者模式
NIO 中的文件目录监听服务使用到了观察者模式。
NIO 中的文件目录监听服务基于 WatchService
接口和 Watchable
接口。WatchService
属于观察者,Watchable
属于被观察者。
IO 模型
NIO(非阻塞 I/O)
NIO(New I/O) 是 Java 在 JDK 1.4 引入的一套新的 I/O 模型,用于处理大规模、高并发的 I/O 操作。它的目标是提高 I/O 操作的效率,特别是在需要处理大量并发连接的应用中,如网络服务器、数据库等。
NIO是怎么实现的?
Java NIO(New IO)是基于事件驱动和非阻塞 IO 模型实现的,它不同于传统 BIO 的一线程一连接,而是通过多路复用机制实现高效并发通信。
NIO 的核心在于三个部分:Channel、Buffer、Selector。
- Channel 表示一个双向通道,可以读也可以写,对应底层的文件或网络连接;
- Buffer 是数据的容器,所有读写操作都通过 Buffer 进行;
- Selector 是多路复用器,它可以监听多个通道的事件,比如读、写、连接等,只需要一个线程就能管理多个通道。
工作流程上,NIO 会把多个 Channel 注册到一个 Selector 上,然后单线程轮询 Selector 上是否有事件准备好(如某个通道可读),一旦就绪就处理对应的事件。这种模型大幅度减少了线程数量和上下文切换的开销。
NIO 的核心特性
- 非阻塞 I/O 与传统的阻塞 I/O(BIO)不同,NIO 支持非阻塞 I/O。传统的 BIO 模型中,线程在进行 I/O 操作时会被阻塞,直到操作完成。而在 NIO 中,线程可以发起 I/O 操作后立即返回,继续执行其他任务,直到 I/O 操作完成。这样,线程可以高效地处理多个 I/O 操作,提升了并发性能。
- I/O 多路复用 NIO 引入了
Selector
和Channel
的机制,允许单个线程通过多路复用来管理多个 I/O 通道。通过Selector
,一个线程可以同时监听多个通道(如多个网络连接),当某个通道准备好进行 I/O 操作时,线程才会被唤醒并进行处理。这避免了为每个 I/O 操作都创建一个线程,节省了资源,提升了并发处理能力。 - 缓冲区(Buffer) NIO 中的数据传输是通过
Buffer
实现的。数据从通道读取到缓冲区,再从缓冲区写入到通道。缓冲区是 NIO 的核心组件之一,提供了高效的数据存取方式。通过缓冲区,可以实现直接在内存中进行数据的读写操作,减少了 I/O 操作的开销。 - 选择器(Selector)
Selector
是 NIO 的关键组件,它用于实现 I/O 多路复用。在Selector
中,多个通道(Channel)可以注册不同的操作(如读、写),并且可以在一个线程中异步地处理多个通道的事件。当某个通道准备好进行 I/O 操作时,Selector
会通知相应的线程进行处理,从而大大提高了资源利用率。
NIO 的优势
- 高效的资源利用 由于 NIO 支持非阻塞 I/O 和 I/O 多路复用,线程可以在等待 I/O 操作时继续处理其他任务,从而避免了阻塞,提升了资源的利用效率。这对于需要处理大量并发连接的应用程序非常有用,比如 Web 服务器、聊天室等。
- 减少线程开销 在传统的 BIO 模型中,每个连接都需要一个独立的线程进行处理,这在高并发的场景下会产生很大的线程开销。而 NIO 通过
Selector
和Channel
的组合,允许一个线程处理多个 I/O 通道,大大减少了线程的数量,从而降低了线程的管理开销。 - 可扩展性强 NIO 的设计使得它在处理大规模并发时能够提供更好的扩展性。它能通过一个线程同时处理成千上万个连接,避免了传统模型中的线程过多问题,特别适用于需要处理大量客户端请求的服务器。
NIO 的缺点
- 编程复杂性 与传统的阻塞 I/O 模型相比,NIO 的编程模型更为复杂。开发者需要理解非阻塞操作、事件轮询、缓冲区管理等概念。此外,错误处理和资源管理也比 BIO 更加繁琐。
- 不适用于小规模应用 如果应用的并发量较低,NIO 的复杂性可能不值得使用。在这种情况下,使用传统的阻塞 I/O(BIO)模型可能更加简单和高效。
BIO(Blocking I/O) 和 AIO(Asynchronous I/O) 是 Java 中两种不同的 I/O 模型,它们分别有着不同的工作原理和适用场景。下面是对这两种 I/O 模型的详细介绍。
BIO(阻塞 I/O)
BIO 是传统的 I/O 模型,每个 I/O 操作都会阻塞当前线程,直到操作完成。这意味着在进行 I/O 操作时,线程无法做其他任何事情,直到数据从输入流中读取或者写入输出流中。
工作原理:
- 阻塞操作:每次 I/O 操作(如读、写)都阻塞当前线程。线程会等待数据的准备,直到操作完成。
- 每个连接一个线程:在高并发场景中,每个客户端连接需要分配一个线程,线程会在 I/O 操作完成之前被阻塞,导致线程池资源容易耗尽。
优缺点:
- 优点:
- 实现简单:BIO 模型相对简单易懂,适合小规模、低并发的应用。
- 开发成本低:由于使用同步阻塞,开发者无需管理复杂的线程调度或回调。
- 缺点:
- 低效:在高并发的情况下,每个连接都需要一个线程,而线程创建和上下文切换会带来额外的开销。
- 资源浪费:线程在等待 I/O 操作完成时会被阻塞,导致 CPU 资源被浪费,不能高效利用。
- 不可扩展:随着连接数的增加,线程数也会线性增加,导致系统无法应对大规模并发。
适用场景:
- 适用于低并发、简单的客户端-服务器模型,如小型应用程序、管理工具等。
AIO(异步 I/O)
AIO 是一种相对较新的 I/O 模型,它采用了异步非阻塞的方式来处理 I/O 操作。与 NIO 类似,AIO 使线程在发起 I/O 操作后不需要等待操作的完成,而是可以继续执行其他任务。当 I/O 操作完成时,系统会通过回调机制通知线程。
工作原理:
- 完全异步:在 AIO 中,线程可以提交 I/O 操作后直接返回,而不必等待 I/O 操作的完成。I/O 操作会在后台完成。
- 回调通知:操作系统在 I/O 操作完成后,会通过回调机制通知应用程序,应用程序可以处理数据,而不需要一直轮询或者等待。
优缺点:
- 优点:
- 高效性:由于线程无需阻塞等待 I/O 操作,AIO 模型可以高效利用系统资源,提升并发能力。
- 性能:它可以大大减少线程的数量,并且避免了传统 I/O 模型中的线程阻塞问题,适用于需要处理大量并发请求的场景。
- 低延迟:异步操作和回调机制确保线程在执行其他任务时不被阻塞,可以实现更快的响应。
- 缺点:
- 实现复杂:AIO 模型的编程复杂度较高,需要使用回调机制来处理 I/O 完成后的通知,增加了代码的复杂性。
- 回调地狱:处理大量异步回调可能导致代码难以维护,尤其是在复杂的业务场景中。
适用场景:
- 高并发、大规模系统,如分布式系统、数据库、文件服务器等。
BIO vs NIO vs AIO
BIO(blocking IO):就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。优点是代码比较简单、直观;缺点是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
NIO(non-blocking IO) :Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
AIO(Asynchronous IO) :是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。