字节流

InputSteam「字节输入流」

InputStream 用于从源头(通常是文件)读取数据(字节信息)到内存中。

  • read():返回输入流中下一个字节的数据。如果未读取任何字符,返回-1
  • close()
  • readAllBytes():读取输入流中的所有字节

FileInputStream是一个比较常用的字节输入流对象,可直接指定文件路径。

OutputStream「字节输出流」

OutputStream 用于将数据(字节信息)写入到目的地(通常是文件)。

  • write(int b):将特定字节写入输出流
  • flush():刷新此输出流并强制写出所有缓冲的输出字节
  • close()

FileOutputStream 是最常用的字节输出流对象,可直接指定文件路径。

字符流

如果不知道编码类型,字节流很容易出现乱码。因此,I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。

Reader「字符输入流」

Reader 用于从源头(通常是文件)读取数据(字符信息)到内存中。

  • read():返回输入流中下一个字节的数据。如果未读取任何字符,返回-1
  • close()

FileReader是基于该基础上的封装,可以直接操作字符文件

Writer「字符输出流」

Writer 用于将数据(字符信息)写入到目的地(通常是文件)。

  • write(char c):写入单个字符
  • write(String str):写入字符串
  • flush():刷新此输出流并强制写出所有缓冲的输出字符
  • close():关闭输出流释放相关的系统资源

FileWriter时基于该基础上的封装,可以直接将字符写入到文件。

缓冲流

IO操作时很消耗性能的,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁的IO操作,提高流的传输效率。

举个例子,我们可以通过 BufferedInputStream(字节缓冲输入流)来增强 FileInputStream 的功能。

1
2
// 新建一个 BufferedInputStream 对象
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt"));

BufferedInputStream「字节缓冲输入流」

先将读取到的字节存放在缓存区,并从内部缓冲区中单独读取字节。

BufferedOutputStream「字节缓冲输出流」

将数据写入到目的地的过程中不会一个字节一个字节的写入,而是会先将要写入的字节存放在缓存区,并从内部缓存区中单独写入字节。

BufferedReader「字符缓冲输入流」

BufferedWriter「字符缓冲输出流」

装饰器模式

装饰器模式可以在不改变原有对象的情况下拓展其功能。装饰器模式通过组合替代继承来扩展原始类的功能,在一些继承关系比较复杂的场景(IO 这一场景各种类的继承关系就比较复杂)更加实用。

举个例子,我们可以通过BufferedInputStream「字节缓冲输入流」来增强FileInputStream的功能。

BufferedInputStream构造函数如下:

1
2
3
4
5
6
7
8
9
10
11
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}

public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}

可以看出,BufferedInputStream的构造函数其中的一个参数就是InputStream

为啥不直接弄一个BufferedFileInputStream

1
BufferedFileInputStream bfis = new BufferedFileInputStream("input.txt");

如果 InputStream 的子类比较少的话,这样做是没问题的。不过, InputStream 的子类实在太多,继承关系也太复杂了。如果我们为每一个子类都定制一个对应的缓冲输入流,那岂不是太麻烦了。

同样,装饰器模式很重要的一个特征就是可以对原始类嵌套使用多个装饰器。为了实现这一效果,装饰器类需要跟原始类即成相同的抽象类或者实现相同的接口。

1
2
3
4
5
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName));
ZipInputStream zis = new ZipInputStream(bis);

BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fileName));
ZipOutputStream zipOut = new ZipOutputStream(bos);

适配器模式

适配器模式主要用于接口互不兼容的类的协调工作。

适配器模式中存在被适配的对象或者类称为适配者「Adaptee」,作用于适配者的对象或者类称为适配器「Adapter」

举个例子,IO 流中的字符流和字节流的接口不同,它们之间可以协调工作就是基于适配器模式来做的,更准确点来说是对象适配器。通过适配器,我们可以将字节流对象适配成一个字符流对象,这样我们可以直接通过字节流对象来读取或者写入字符数据。

1
2
3
4
// InputStreamReader 是适配器,FileInputStream 是被适配的类
InputStreamReader isr = new InputStreamReader(new FileInputStream(fileName), "UTF-8");
// BufferedReader 增强 InputStreamReader 的功能(装饰器模式)
BufferedReader bufferedReader = new BufferedReader(isr);

工厂模式

工厂模式是用来创建对象的一种最常用的设计模式,不暴露创建对象的具体逻辑,而是将将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂

其就像工厂一样重复的产生类似的产品,工厂模式只需要我们传入正确的参数,就能生产类似的产品

工厂模式根据抽象成都的不同可以分为:

  • 简单工厂模式(静态工厂模式)
  • 工厂方法模式
  • 抽象工厂模式

简单工厂模式

当我们调用工厂函数时,只需要传递相应的参数就可以获取到包含用户工作内容的实例对象。

1
2
3
4
5
6
7
8
9
10
11
class PayFactory{
public static PayService toPay(String type){
if ("ali".equals( type )){
return new AliPayService();
}
if ("wechat".equals( type )){
return new WeChatPayService();
}
return null;
}
}

工厂方法模式

和简单工厂模式差不多,不过创建对象工作委托给具体工厂。这样一来,扩展产品种类就不必修改工厂函数了,要做的是实现一个新的工厂。

总工厂就相当于一个抽象类。

1
2
3
4
5
6
7
8
9
10
11
public class FactoryTest {
public static void main(String[] args) {
ComputerFactory haseeFactory = new HaseeFactory();
Computer haseeComputer = haseeFactory.produce();
haseeComputer.use();

ComputerFactory lenovoFactory = new LenovoFactory();
Computer lenovoComputer = lenovoFactory.produce();
lenovoComputer.use();
}
}

抽象工厂模式

工厂方法模式针对的某一种产品,而抽象工厂模式可以针对多种产品。

比如生产电脑,工厂方法模式解决的是生产不同品牌的同一类型的电脑,而抽象工厂模式是生产不同品牌的多种类型的电脑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TestFactory {
public static void main(String[] args) {
ComputerFactory haseeFactory = new HaseeFactory();
DesktopComputer haseeDesktopComputer = haseeFactory.produceDesktopComputer();
haseeDesktopComputer.use();
NotebookComputer haseeNotebookComputer = haseeFactory.produceNotebookComputer();
haseeNotebookComputer.use();

ComputerFactory lenovoFactory = new LenovoFactory();
DesktopComputer lenovoDesktopComputer = lenovoFactory.produceDesktopComputer();
lenovoDesktopComputer.use();
NotebookComputer lenocoFactory = lenovoFactory.produceNotebookComputer();
lenocoFactory.use();

}
}

什么是IO

从应用程序的视角来看,我们的应用程序对操作系统的内核发起IO调用(系统调用),操作系统负责的内核执行具体的IO操作。当应用程序发起IO调用后,会经历两个步骤:

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

从计算机结构的视角来看的话,IO描述了计算机系统与外部设备之间通信的过程。

UNIX系统下, IO 模型一共有 5 种: 同步阻塞 I/O同步非阻塞 I/OI/O 多路复用信号驱动 I/O异步 I/O

Java中3种常见IO模型

BIO「Blocking」

BIO属于同步阻塞IO模型。同步阻塞IO模型中,应用程序发起read调用后,会一直阻塞,直到内核把数据拷贝到用户空间。

NIO「Non-blocking」

对于高负载、高并发的(网络)应用,应使用NIO。Java中的NIO可以看作是IO多路复用模型。

首先介绍下同步非阻塞IO模型。在同步非阻塞IO模型中,应用程序会一直发起read调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的。通过轮询操作,避免了一直阻塞。但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。

所以在IO多路复用模型中,线程首先发起select调用,询问内核数据是否准备就绪,等内核把数据准备好后,用户线程再发起read调用。在该模型中,通过减少无效的系统调用,减少了对CPU资源的消耗

IO多路复用

Java 中的 NIO ,有一个非常重要的选择器 (Selector) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。

AIO「Asynchronous」

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

IO in Java

参考链接:JavaGuide