八股记录——IO
一、IO流简介
数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出
IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
InputStream
/Reader
: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。OutputStream
/Writer
: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
二、字节流
1、InputStream
常用方法
read()
:返回输入流中下一个字节的数据。read(byte b[ ])
: 从输入流中读取一些字节存储到数组b
中。读取的字节数最多等于b.length
,返回读取的字节数。这个方法等价于read(b, 0, b.length)
。read(byte b[], int off, int len)
:在read(byte b[ ])
方法的基础上增加了off
参数(偏移量)和len
参数(要读取的最大字节数)。skip(long n)
:忽略输入流中的 n 个字节 ,返回实际忽略的字节数。available()
:返回输入流中可以读取的字节数。close()
:关闭输入流释放相关的系统资源。
从 Java 9 开始,InputStream
新增加了多个实用的方法:
readAllBytes()
:读取输入流中的所有字节,返回字节数组。readNBytes(byte[] b, int off, int len)
:阻塞直到读取len
个字节。transferTo(OutputStream out)
: 将所有字节从一个输入流传递到一个输出流。
FileInputStream
是一个比较常用的字节输入流对象,可直接指定文件路径,可以直接读取单字节数据,也可以读取至字节数组中。一般我们是不会直接单独使用 FileInputStream
,通常会配合 BufferedInputStream
(字节缓冲输入流,后文会讲到)来使用。
2、OutputStream 常用方法
write(int b)
:将特定字节写入输出流。write(byte b[ ])
: 将数组b
写入到输出流,等价于write(b, 0, b.length)
。write(byte[] b, int off, int len)
: 在write(byte b[ ])
方法的基础上增加了off
参数(偏移量)和len
参数(要读取的最大字节数)。flush()
:刷新此输出流并强制写出所有缓冲的输出字节。close()
:关闭输出流释放相关的系统资源。
三、字符流
1、为什么 I/O 流操作要分为字节流操作和字符流操作
- 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时。
- 如果我们不知道编码类型就很容易出现乱码问题。
字符流默认采用的是 Unicode
编码,我们可以通过构造方法自定义编码。顺便分享一下之前遇到的笔试题:
2、常用字符编码所占字节数?
utf8
:英文占 1 字节,中文占 3 字节unicode
:任何字符都占 2 个字节gbk
:英文占 1 字节,中文占 2 字节。
3、Reader 常用方法
ead()
: 从输入流读取一个字符。read(char[] cbuf)
: 从输入流中读取一些字符,并将它们存储到字符数组cbuf
中,等价于read(cbuf, 0, cbuf.length)
read(char[] cbuf, int off, int len)
skip(long n)
:忽略输入流中的 n 个字符 ,返回实际忽略的字符数。close()
: 关闭输入流并释放相关的系统资源。
4、Writer常用方法
write(int c)
: 写入单个字符。write(char[] cbuf)
:写入字符数组cbuf
,等价于write(cbuf, 0, cbuf.length)
。write(char[] cbuf, int off, int len)
:write(String str)
:写入字符串,等价于write(str, 0, str.length())
。write(String str, int off, int len)
:在write(String str)
方法的基础上增加了off
参数(偏移量)和len
参数(要读取的最大字符数)。append(CharSequence csq)
:将指定的字符序列附加到指定的Writer
对象并返回该Writer
对象。append(char c)
:将指定的字符附加到指定的Writer
对象并返回该Writer
对象。flush()
:刷新此输出流并强制写出所有缓冲的输出字符。close()
:关闭输出流释放相关的系统资源。
四、字节缓冲流
1、BufferedInputStream(字节缓冲输入流)
BufferedInputStream
从源头(通常是文件)读取数据(字节信息)到内存的过程中不会一个字节一个字节的读取,而是会先将读取到的字节存放在缓存区,并从内部缓冲区中单独读取字节。这样大幅减少了 IO 次数,提高了读取效率。
BufferedInputStream
内部维护了一个缓冲区,这个缓冲区实际就是一个字节数组
2、BufferedOutputStream(字节缓冲输出流)
同样是先将要写入的字节存放在缓存区,并从内部缓冲区中单独写入字节。这样大幅减少了 IO 次数,提高了读取效率
类似于 BufferedInputStream
,BufferedOutputStream
内部也维护了一个缓冲区,并且,这个缓存区的大小也是 8192 字节。
五、字符缓冲流
BufferedReader
(字符缓冲输入流)和 BufferedWriter
(字符缓冲输出流)类似于 BufferedInputStream
(字节缓冲输入流)和BufferedOutputStream
(字节缓冲输入流),内部都维护了一个字节数组作为缓冲区。不过,前者主要是用来操作字符信息。
六、打印流
System.out
实际是用于获取一个 PrintStream
对象,print
方法实际调用的是 PrintStream
对象的 write
方法。
PrintStream
属于字节打印流,与之对应的是 PrintWriter
(字符打印流)。PrintStream
是 OutputStream
的子类,PrintWriter
是 Writer
的子类。
七、随机访问流
持随意跳转到文件的任意位置进行读写的 RandomAccessFile
。
RandomAccessFile
比较常见的一个应用就是实现大文件的 断点续传 。何谓断点续传?简单来说就是上传文件中途暂停或失败(比如遇到网络问题)之后,不需要重新上传,只需要上传那些未成功上传的文件分片即可。分片(先将文件切分成多个文件分片)上传是断点续传的基础。
八、Java IO设计模式总结——重要!!!
1、装饰器模式
核心:在不改变原有对象的情况下拓展其功能。装饰者设计模式主要是利用多态,将子类对象作为参数传递,达到装饰的效果。如果不采用装饰者模式,而是采用继承结构,会造成类爆炸,假如有n种属性,那所有的搭配种类就有 2n 个
装饰器模式通过组合替代继承来扩展原始类的功能,在一些继承关系比较复杂的场景(IO 这一场景各种类的继承关系就比较复杂)更加实用。
FilterInputStream
(对应输入流)和FilterOutputStream
(对应输出流) 是装饰器模式的核心,分别用于增强 InputStream
和OutputStream
子类对象的功能。
常见的BufferedInputStream
(字节缓冲输入流)、DataInputStream
等等都是FilterInputStream
的子类,BufferedOutputStream
(字节缓冲输出流)、DataOutputStream
等等都是FilterOutputStream
的子类。
装饰器模式很重要的一个特征,那就是:可以对原始类嵌套使用多个装饰器。
2、适配器模式
适配器(Adapter Pattern)模式 主要用于接口互不兼容的类的协调工作,你可以将其联想到我们日常经常使用的电源适配器。
- 适配者(Adaptee) :被适配的对象或者类。
- 适配器(Adapter) :作用于适配者的对象或者类。例如:
InputStreamReader
和OutputStreamWriter
适配器分为对象适配器和类适配器。
- 类适配器:使用继承关系来实现
- 对象适配器:使用组合关系来实现。
IO 流中的字符流和字节流的接口不同,它们之间可以协调工作就是基于适配器模式来做的,更准确点来说是对象适配器。
InputStream
和 OutputStream
的子类是被适配者, InputStreamReader
和 OutputStreamWriter
是适配器。
3、适配器模式和装饰器模式有什么区别呢?
装饰器模式:
- 更侧重于动态地增强原始类的功能
- 装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。
- 支持对原始类嵌套使用多个装饰器。
适配器模式
- 更侧重于让接口不兼容而不能交互的类可以一起工作
- 当我们调用适配器对应的方法时,适配器内部会调用适配者类或者和适配类相关的类的方法,这个过程透明的。适
- 配器和适配者两者不需要继承相同的抽象类或者实现相同的接口。
4、工厂模式
在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
意图:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
主要解决:主要解决接口选择的问题。
何时使用:我们明确地计划不同条件下创建不同实例时。
如何解决:让其子类实现工厂接口,返回的也是一个抽象的产品。
关键代码:创建过程在其子类执行。
作用
- 首先工厂模式是为了解耦:把对象的创建和使用的过程分开。就是Class A 想调用 Class B ,那么A只是调用B的方法,而至于B的实例化,就交给工厂类。
- 其次,工厂模式可以降低代码重复。如果创建对象B的过程都很复杂,需要一定的代码量,而且很多地方都要用到,那么就会有很多的重复代码。我们可以这些创建对象B的代码放到工厂里统一管理。既减少了重复代码,也方便以后对B的创建过程的修改维护。
- 另外,因为工厂管理了对象的创建逻辑,使用者并不需要知道具体的创建过程,只管使用即可,减少了使用者因为创建逻辑导致的错误。
使用环境:不仅限于以下场景):
- 对象的创建过程/实例化准备工作很复杂,需要初始化很多参数、查询数据库等。
- 类本身有好多子类,这些类的创建过程在业务中容易发生改变,或者对类的调用容易发生改变。
工厂模式用于创建对象,NIO 中大量用到了工厂模式,比如 Files
类的 newInputStream
方法用于创建 InputStream
对象(静态工厂)、 Paths
类的 get
方法创建 Path
对象(静态工厂)、ZipFileSystem
类(sun.nio
包下的类,属于 java.nio
相关的一些内部实现)的 getPath
的方法创建 Path
对象(简单工厂)。
5、观察者模式
当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。
意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。
何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
如何解决:使用面向对象技术,可以将这种依赖关系弱化。
关键代码:在抽象类里有一个 ArrayList 存放观察者们
使用场景
NIO 中的文件目录监听服务使用到了观察者模式。
NIO 中的文件目录监听服务基于 WatchService
接口和 Watchable
接口。WatchService
属于观察者,Watchable
属于被观察者。
Watchable
接口定义了一个用于将对象注册到 WatchService
(监控服务) 并绑定监听事件的方法 register
。
WatchService
用于监听文件目录的变化,同一个 WatchService
对象能够监听多个文件目录。
WatchService
内部是通过一个 daemon thread(守护线程)采用定期轮询的方式来检测文件的变化
九、Java IO模型详解
1、基本概念
从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。
**从应用程序的角度来解读一下 I/O。用户进程想要执行 IO 操作的话,必须通过 **系统调用来间接访问内核空间
根据大学里学到的操作系统相关的知识:为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space ) 。
当应用程序发起 I/O 调用后,会经历两个步骤:
- 内核等待 I/O 设备准备好数据
- 内核将数据从内核空间拷贝到用户空间。
2、常用IO模型
- 同步阻塞 I/O
- 同步非阻塞 I/O
- I/O 多路复用
- 信号驱动 I/O
- 异步 I/O
3、Java中常见的3种IO模型
(1)BIO (Blocking I/O)——同步阻塞 IO 模型 。
应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。
(2)NIO (Non-blocking/New I/O)
它是支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使用 NIO 。Java 中的 NIO 可以看作是 I/O 多路复用模型。也有很多人认为,Java 中的 NIO 属于同步非阻塞 IO 模型。
同步非阻塞 IO 模型:
应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。
问题在于:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。
I/O 多路复用模型:
线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。
Java 中的 NIO ,有一个非常重要的选择器 ( Selector ) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。
(3)AIO——(Asynchronous I/O)
异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。