聊聊什么是IO流

目录

Java IO

IO 基础

Java IO 流了解吗?

IO 即 Input/0utput ,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储 (比如数据库,文件,远程主机)的过程即输出。

数据传输过程类似于水流,因此称为 IO 流。

  • IO 流在 java 中分为输入流输出流
  • 而根据数据的处理方式又分为字节流字符流

Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的:

  • InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
  • OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流

IO 设计模式

1、装饰器模式

装饰器模式是一种结构型设计模式,允许在运行时动态地将责任附加到对象上。

是一种通过组合替代继承 来扩展原始类的功能,可以在不改变原有对象的情况下增强其功能的模式。

java 复制代码
// 输入
// 将FileInputStream对象 装饰 成了一个带有缓冲区的输入流对象
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName));
// 将带有缓冲区的输入流对象 再次装饰 成了一个能够处理ZIP文件的输入流对象
ZipInputStream zis = new ZipInputStream(bis);

// 输出
// 将FileOutputStream对象 装饰 成了一个带有缓冲区的输出流对象
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fileName));
// 将带有缓冲区的输出流对象 再次装饰 成了一个能够处理ZIP文件的输出流对象。
ZipOutputStream zipOut = new ZipOutputStream(bos);

其中 BufferedInputStream、ZipInputStream、BufferedOutputStream 和 ZipOutputStream 都是 Java 中的装饰器类。

2、适配器模式

可以用于将不兼容的类或接口转换为兼容的类或接口,从而使它们能够协同工作。

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

java 复制代码
// InputStreamReader 是适配器,用于将 FileInputStream 实现的字节流接口适配成字符流接口
InputStreamReader isr = new InputStreamReader(new FileInputStream(fileName), "UTF-8");
// BufferedReader 增强 InputStreamReader 的功能(装饰器模式)
BufferedReader bufferedReader = new BufferedReader(isr);

适配器模式和装饰器模式有什么区别呢?

适配器模式和装饰器模式虽然都属于结构型设计模式,但是它们的设计目的和实现方式有所不同。

设计目的

  1. 适配器模式(Adapter Pattern)的设计目的是:将一个类的接口转换为客户端所期望的另一个接口,以满足客户端的需求 。适配器模式通常用于解决接口不兼容的问题,它通过一个适配器来将一个类的接口转换为另一个接口,使得客户端可以像调用另一个接口一样来调用原来的接口。适配器模式通常是在已有的系统中进行接口升级或者系统集成时使用的。
  2. 装饰器模式(Decorator Pattern)的设计目的是:在不改变原有对象的基础上,动态地给对象添加一些新的功能 。装饰器模式通常用于解决类的功能扩展问题,它通过在原有对象的基础上添加一个装饰器来为对象添加新的功能,使得客户端可以在不改变原有对象的情况下使用新的功能。装饰器模式通常是在需要为一个对象动态地添加一些功能时使用的。

实现方式

适配器模式通常是通过一个适配器类来转换接口 ,而装饰器模式通常是通过继承或者组合的方式来实现功能的添加

  • 装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。(同一个父类,字节流 -- 字节流)
  • 适配器和适配者两者不需要继承相同的抽象类或者实现相同的接口。(不同的父类,字符流 -- 字节流)

3、工厂模式

Java IO 中的工厂模式指的是:通过工厂类来创建不同类型的输入输出流对象。从而实现更加灵活的 IO 操作。

常用的 IO 工厂模式有 InputStreamFactory、OutputStreamFactory、ReaderFactory 和 WriterFactory 等。

代码示例如下:

  1. 首先创建四个工厂对象:InputStreamFactory、OutputStreamFactory、ReaderFactory 和 WriterFactory,分别用于创建输入流、输出流、字符输入流和字符输出流。
  2. 然后,通过调用它们的 create 方法,创建了对应的输入流对象和输出流对象
  3. 最后,使用创建的输入流对象和输出流对象,对文件进行读写操作
java 复制代码
public class IOFactoryDemo {
    public static void main(String[] args) {
        // 创建输入流工厂对象
        InputStreamFactory inputStreamFactory = new FileInputStreamFactory();

        // 创建输出流工厂对象
        OutputStreamFactory outputStreamFactory = new FileOutputStreamFactory();

        // 创建字符输入流工厂对象
        ReaderFactory readerFactory = new FileReaderFactory();

        // 创建字符输出流工厂对象
        WriterFactory writerFactory = new FileWriterFactory();

        // 创建文件输入流对象
        InputStream inputStream = inputStreamFactory.createInputStream("C:\\myfiles\\input.txt");

        // 创建文件输出流对象
        OutputStream outputStream = outputStreamFactory.createOutputStream("C:\\myfiles\\output.txt");

        // 创建字符输入流对象
        Reader reader = readerFactory.createReader("C:\\myfiles\\input.txt");

        // 创建字符输出流对象
        Writer writer = writerFactory.createWriter("C:\\myfiles\\output.txt");

        // 从文件输入流中读取数据
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 向文件输出流中写入数据
        try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) {
            String data = "Hello, world!";
            byte[] bytes = data.getBytes();
            bufferedOutputStream.write(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 从字符输入流中读取数据
        try (BufferedReader bufferedReader = new BufferedReader(reader)) {
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 向字符输出流中写入数据
        try (BufferedWriter bufferedWriter = new BufferedWriter(writer)) {
            String data = "Hello, world!";
            bufferedWriter.write(data);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用工厂模式的好处是什么?

通过工厂模式创建输入输出流对象,可以避免 直接使用具体的类来创建对象,降低 了客户端和具体类的耦合度,提高 了系统的灵活性和扩展性。此外,工厂方法还可以对输入输出流对象进行统一管理和控制,从而实现一些特定的功能,如数据压缩、加密等。

4、观察者模式

IO 观察者模式是一种设计模式,用于监控文件或目录的变化

在 Java 中,可以使用 java.nio.file 包中的 WatchService 类来实现 IO 观察者模式。

WatchService 类充当观察者,用于监控指定目录中的文件或子目录的变化。当目录中的文件或子目录发生变化时,WatchService通过 Path 事件通知注册的观察者

具体地说,使用 WatchService 实现 IO 观察者模式的步骤如下:

  1. 创建一个 WatchService 对象,通过 Path 对象的 register() 方法将其注册到需要监控的目录中。

  2. 创建一个线程,不断从 WatchService 对象中获取 事件,并处理这些事件。

  3. WatchService 对象检测到目录中的文件或子目录发生变化时,会生成 一个 Path 事件,并将其添加WatchService 对象的事件队列中。

  4. 在处理事件的线程中,可以通过 WatchEvent 对象获取事件类型、事件发生的路径等信息,并根据这些信息进行相应的处理

java 复制代码
/**
 * 文件观察者类,用于监测指定目录中文件的变化
 */
public class FileWatcher implements Runnable {
    private Path path;              // 目录路径
    private WatchService watchService;  // WatchService对象用于监测目录中文件的变化

    /**
     * 构造函数,创建一个FileWatcher对象
     * @param path 目录路径
     * @throws IOException 抛出IO异常
     */
    public FileWatcher(Path path) throws IOException {
        this.path = path;
        // 获取默认的WatchService对象
        this.watchService = FileSystems.getDefault().newWatchService();
        // 注册监测事件类型
        this.path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
    }

    /**
     * run方法,用于启动监测线程
     */
    @Override
    public void run() {
        try {
            while (true) {
                // 获取WatchKey对象,它代表了被监测的目录中的事件队列
                WatchKey key = watchService.take();
                // 遍历事件队列
                for (WatchEvent<?> event : key.pollEvents()) {
                    // 输出事件类型和受影响的文件名
                    System.out.println("Event kind: " + event.kind() + ". File affected: " + event.context() + ".");
                }
                // 重置WatchKey对象,以便继续监测
                key.reset();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * main方法,用于启动文件监测程序
     */
    public static void main(String[] args) throws IOException {
        // 创建监测的目录路径
        Path path = Paths.get("/path/to/monitor");
        // 创建FileWatcher对象
        FileWatcher fileWatcher = new FileWatcher(path);
        // 创建监测线程,并启动
        Thread thread = new Thread(fileWatcher);
        thread.start();
    }
}

IO 观察者模式可以用于监控文件或目录的变化,从而实现一些特定的功能,如自动备份、自动同步等。在实际开发中,IO 观察者模式可以提高代码的可维护性和可扩展性,使得代码更加灵活。

IO 模型

有哪些常见的 IO 模型?

Java 中有 3 种常见 IO 模型:

BIO(Blocking I/O)

BIO 属于同步阻塞 IO 模型

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

NIO (Non-blocking/New I/O)

NIO 属于同步非阻塞 IO 模型

NIO(New I/O)是 Java NIO(New Input/Output)API 的简称,是 Java SE 1.4 中引入的一组新的 I/O API,用于替代 Java 标准 I/O API(java.io 包)中的一部分功能。

NIO 提供了一种基于通道(Channel)和缓冲区(Buffer)的 I/O 模型,相较于传统的基于流的 I/O 模型,它具有更高的性能和更好的扩展性。

  • 通道表示数据源与目标之间的连接,可以用于读取和写入数据。
  • 缓冲区则是存储数据的区域,数据通过缓冲区进行传输。

在同步非阻塞I/O模型中,应用程序发起 read 调用后(轮询操作),线程不会被阻塞。相反,线程会立即返回,并继续执行后续的操作,而不必等待数据从内核空间拷贝到用户空间。

通过不断发起 read 调用来检查是否有新的数据可用

但是它可以通过多路复用技术 来实现高效的 IO 操作,减少无效的系统调用,减少了对 CPU 资源的消耗。

IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用 。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。

AIO (Asynchronous I/O)

AIO 是异步 IO 模型

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

BIO、NIO、AIO 有什么区别?

它们主要的区别在于数据的读取和处理方式不同。

  1. BIO(Blocked IO):**BIO 是同步阻塞 I/O 模型。**在该模型下,所有的 I/O 操作都是阻塞的,即当一个线程在进行 I/O 操作时,它会一直等待直到数据准备就绪或者发生异常。(只会发起一次 read 调用,但会阻塞)

    BIO 模型适用于连接数比较少且通信线程比较短的场景,例如传统的客户端/服务器模型。

  2. NIO(Non-Blocked IO):**NIO 是同步非阻塞 I/O 模型。**在该模型下,所有的 I/O 操作都是非阻塞的,即当一个线程在进行 I/O 操作时,它会立即返回一个特定的状态(通常是非阻塞状态,如EAGAIN或EWOULDBLOCK),表示当前没有可用的数据,而不会等待数据准备就绪。(会一直发起 read 调用)

    NIO 模型适用于连接数比较多且通信线程比较长的场景,例如聊天室和在线游戏。

  3. AIO(Asynchronous IO):**AIO 是异步非阻塞 I/O 模型。**在该模型下,所有的 I/O 操作都是异步的,即当一个线程在进行 I/O 操作时,它不需要等待数据准备就绪,而是通过回调函数的方式来处理数据。(只会发起一次 read 调用)

    AIO 模型适用于连接数非常多且通信线程比较长的场景,例如高性能的网络服务器。

总的来说,

  • BIO 模型的开销比较大,但是编程模型比较简单;(性能较低)
  • NIO 模型的开销比较小,但是编程模型比较复杂;(性能较高)
  • AIO 模型相对于 NIO 模型来说,编程模型更为简单,但是在性能方面与 NIO 模型相当或者略逊一筹。(综合型)

参考

Java基础常见面试题总结(下)

相关推荐
zjw_rp26 分钟前
Spring-AOP
java·后端·spring·spring-aop
Oneforlove_twoforjob39 分钟前
【Java基础面试题033】Java泛型的作用是什么?
java·开发语言
TodoCoder1 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
向宇it1 小时前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
小蜗牛慢慢爬行1 小时前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate
星河梦瑾2 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富2 小时前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想2 小时前
JMeter 使用详解
java·jmeter
言、雲2 小时前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
TT哇2 小时前
【数据结构练习题】链表与LinkedList
java·数据结构·链表