八股记录——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 次数,提高了读取效率

类似于 BufferedInputStreamBufferedOutputStream 内部也维护了一个缓冲区,并且,这个缓存区的大小也是 8192 字节。

五、字符缓冲流

BufferedReader (字符缓冲输入流)和 BufferedWriter(字符缓冲输出流)类似于 BufferedInputStream(字节缓冲输入流)和BufferedOutputStream(字节缓冲输入流),内部都维护了一个字节数组作为缓冲区。不过,前者主要是用来操作字符信息。

六、打印流

System.out 实际是用于获取一个 PrintStream 对象print方法实际调用的是 PrintStream 对象的 write 方法。

PrintStream 属于字节打印流,与之对应的是 PrintWriter (字符打印流)。PrintStreamOutputStream 的子类,PrintWriterWriter 的子类。

七、随机访问流

持随意跳转到文件的任意位置进行读写的 RandomAccessFile

RandomAccessFile 比较常见的一个应用就是实现大文件的 断点续传 。何谓断点续传?简单来说就是上传文件中途暂停或失败(比如遇到网络问题)之后,不需要重新上传,只需要上传那些未成功上传的文件分片即可。分片(先将文件切分成多个文件分片)上传是断点续传的基础。

八、Java IO设计模式总结——重要!!!

1、装饰器模式

核心:在不改变原有对象的情况下拓展其功能。装饰者设计模式主要是利用多态,将子类对象作为参数传递,达到装饰的效果。如果不采用装饰者模式,而是采用继承结构,会造成类爆炸,假如有n种属性,那所有的搭配种类就有 2n 个

装饰器模式通过组合替代继承来扩展原始类的功能,在一些继承关系比较复杂的场景(IO 这一场景各种类的继承关系就比较复杂)更加实用。

FilterInputStream (对应输入流)和FilterOutputStream(对应输出流) 是装饰器模式的核心,分别用于增强 InputStreamOutputStream子类对象的功能。

常见的BufferedInputStream(字节缓冲输入流)、DataInputStream 等等都是FilterInputStream 的子类,BufferedOutputStream(字节缓冲输出流)、DataOutputStream等等都是FilterOutputStream的子类。

装饰器模式很重要的一个特征,那就是:可以对原始类嵌套使用多个装饰器。

2、适配器模式

适配器(Adapter Pattern)模式 主要用于接口互不兼容的类的协调工作,你可以将其联想到我们日常经常使用的电源适配器。

  • 适配者(Adaptee) :被适配的对象或者类。
  • 适配器(Adapter) :作用于适配者的对象或者类。例如:InputStreamReaderOutputStreamWriter

适配器分为对象适配器和类适配器。

  • 类适配器:使用继承关系来实现
  • 对象适配器:使用组合关系来实现。

IO 流中的字符流和字节流的接口不同,它们之间可以协调工作就是基于适配器模式来做的,更准确点来说是对象适配器

InputStreamOutputStream 的子类是被适配者, InputStreamReaderOutputStreamWriter是适配器。

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 调用后,会经历两个步骤:

  1. 内核等待 I/O 设备准备好数据
  2. 内核将数据从内核空间拷贝到用户空间。

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

总结:JAVA中的三种IO模型: