不学无数——Java中IO和NIO

简介: JAVA中的I/O和NIOI/O 问题是任何编程语言都无法回避的问题,可以说 I/O 问题是整个人机交互的核心问题,因为 I/O 是机器获取和交换信息的主要渠道。

JAVA中的I/O和NIO

I/O 问题是任何编程语言都无法回避的问题,可以说 I/O 问题是整个人机交互的核心问题,因为 I/O 是机器获取和交换信息的主要渠道。在当今这个数据大爆炸时代,I/O 问题尤其突出,很容易成为一个性能瓶颈。

什么是I/O

I/O ? 或者输入/输出 ? 指的是计算机与外部世界或者一个程序与计算机的其余部分的之间的接口。它对于任何计算机系统都非常关键,因而所有 I/O 的主体实际上是内置在操作系统中的。单独的程序一般是让系统为它们完成大部分的工作。

  • I:就是从硬盘将内容读取到内存
  • O:就是从内存将内容读取到硬盘

Java中的I/O操作类在包java.io下面,大概将近有80多个类,但是这些类可以分为三组

  • 基于字节操作的I/O接口:InputStreamOutputStream
  • 基于字符操作的 I/O 接口:WriterReader
  • 基于磁盘操作的 I/O 接口:File

然后在各个接口下还有其各自的包装类,其运用到了装饰模式,为其增加一些功能,而Java的I/O复杂也在这,不同的装饰模式创建类的代码也不同。

基于字节操作

InputStream的作用是用来表示那些从不同数据源产生输入的类,这些数据源包括

  • 字节数组
  • String对象
  • 文件
  • 管道,工作方式和实际中的管道相同,从一端输入,从另一端输出
  • 其他的数据源,例如Internet中的Socket连接

InputStream的类图,OutputStream类图和这个类似

img_59e53b216ec3ffb6e4d9290c31e6a67f.png
image
功能 构造器参数 如何使用
ByteArrayInputStream 允许将内存的缓冲区当做InputStream使用 缓冲区,字节将其从中取出 作为数据源:将其与FilterInputStream对象相连以提供有用的接口
StringBufferInputStream 将String转换成InputStream 字符串,底层实现实际使用StringBuffer 作为数据源:将其与FilterInputStream对象相连提供有用接口
FileInputStream 用于从文件中读取信息 字符串,表示文件名,文件或者FileDescriptor对象 作为一种数据源,将其与FilterInputStream对象相连提供有用接口
PipedInputStream 产生用于写入相关PipedOutputStream的额数据,实现管道化的概念 PipedOutputStream 作为多线程的数据源:将其与FilterInputStream对象相连提供有用接口
FilterInputStream 抽象类,作为装饰器的接口,为其他的InputStream提供有用的功能

使用过滤器添加有用的属性和有用的接口

Java的I/O类库需要多种不同功能的组合,这正是装饰模式的理由所在。而这也是java的I/O类库中存在Filter(过滤器)类的原因所在,Filter作为所有装饰类的基类。

功能
BufferedInputStream 使用它可以防止每次读取都进行与磁盘的交互,使用缓冲区进行一次性读取固定值的以后再向磁盘中执行写操作,减少了与磁盘的交互次数。提高速度
DataInputStream 允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型

举个简单使用过滤器进行读取一个文件的内容并输出,例子如下:

public static void main(String[] args) throws IOException {
    InputStream inputStream = new BufferedInputStream(new FileInputStream("/Users/hupengfei/Downloads/a.sql"));
    byte[] buffer = new byte[1024];
    while ( inputStream.read(buffer)!=-1){
        System.out.println(new String(buffer));
    }
    inputStream.close();
}

复制一个文件的例子:

  public static void main(String[] args) throws IOException {
        InputStream inputStream =new BufferedInputStream(new FileInputStream("/Users/hupengfei/Downloads/leijicheng.png"));
        OutputStream outputStream =new BufferedOutputStream(new FileOutputStream("/Users/hupengfei/Downloads/fuzhi.png"));
        byte [] buffer = new byte[1024];
        while (inputStream.read(buffer)!=-1){
            outputStream.write(buffer);
        }
        outputStream.flush();
        inputStream.close();
        outputStream.close();
    }

如果要使用BufferedOutputStream进行在文件中写入的话,那么在缓冲区写完之后要记得调用flush()清空缓冲区。强行将缓冲区中的数据写出。否则可能无法写出数据。

基于字符的操作

不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符,但是为啥有操作字符的 I/O 接口呢?这是因为我们的程序中通常操作的数据都是以字符形式,为了操作方便当然要提供一个直接写字符的 I/O 接口。

还是老规矩,我们先来看一下关于Reader的类图,对应的字节流是InputStream

img_fdb11791dccaa3aca32bd6fb4132edc5.png
Reader类图

其中的InputStreamReader是可以将InputStream转换为Reader即将字节翻译为字符。其中为什么要设计ReaderWriter,主要是为了国际化,之前的字节流仅仅支持8位的字节流,不能很好的处理16位的Unicode字符,由于Unicode用于字符国际化,所以添加了ReaderWriter是为了在所有的I/O操作中都支持Unicode。

在某些场合,面向字节流InputStreamOutputStream才是正确的解决方案,特别是在java.util.zip类库就是面向字节流而不是面向字符的。因此,最明智的做法就是尽量优先使用ReaderWriter,一旦程序无法编译,那么我们就会发现自己不得不使用面向字节类库。

还是写一个相关的读取文件的简单例子

public static void main(String[] args) throws IOException {
    BufferedReader bufferedReader = new BufferedReader(new FileReader("/Users/hupengfei/Downloads/a.sql"));
    String date;
    StringBuilder stringBuilder = new StringBuilder();
    while ((date = bufferedReader.readLine()) != null){
        stringBuilder.append(date +"\n");
    }
    bufferedReader.close();
    System.out.println(stringBuilder.toString());
}

调用readLine()方法时要添加换行符,因为readLine()自动将换行符给删除了

NIO又是什么

JDK1.4中添加了NIO类,我们也可以称之为新I/O。NIO 的创建目的是为了让 Java 程序员可以实现高速 I/O 而无需编写自定义的本机代码。NIO 将最耗时的 I/O 操作(即填充和提取缓冲区)转移回操作系统,因而可以极大地提高速度。

速度的提高来自于所使用的结构更接近于操作系统执行I/O的方式:通道(Channel)和缓冲器(Buffer)

通道和缓冲器是NIO中的核心对象,几乎每一个I/O操作中都会使用它们。通道是对原I/O包中的流的模拟。到任何地方(来自任何地方)的数据都得必须通过一个Channel对象。一个Buffer实质上是一个容器对象。发送给一个通道的所有对象都必须首先放到Buffer缓冲器中。

我们可以将它们想象成一个煤矿,通道就是一个包含煤矿(数据)的矿藏,而缓冲器就是派送到矿藏中的矿车,矿车载满煤炭而归,我们再从矿车上获取煤炭。也就是说,我们并没有直接和通道交互,我们只是和缓冲器进行交互。

缓冲器(Buffer)介绍

Buffer是一个对象,它包含着一些需要读取的数据或者是要传输的数据。在NIO中加入了Buffer对象,体现了和之前的I/O的一个重要的区别。在面向流的I/O中我们直接通过流对象直接和数据进行交互的,但是在NIO中我们和数据的交互必须通过Buffer了。

缓冲器实质上是一个数组。通常它是一个字节的数组,但是也可以使用其他种类的数组。但是一个缓冲器不仅仅是一个数组,缓冲器提供了对数据结构化的访问,而且还可以跟踪系统的读写进程。

接下来我们可以看一下Buffer相关的实现类

img_c1611f3f7fbaa46be4f430bf53495a1c.png
Buffer相关实现类

每一个 Buffer 类都是 Buffer 接口的一个实例。 除了 ByteBuffer,每一个 Buffer 类都有完全一样的操作,只是它们所处理的数据类型不一样。因为大多数标准 I/O 操作都使用 ByteBuffer,所以它具有所有共享的缓冲区操作以及一些特有的操作。

ByteBuffer是唯一一个直接与通道交互的缓冲器——也就说,可以存储未加工字节的缓冲器。当我们查看ByteBuffer源码时会发现其通过告知分配多少存储空间来创建一个ByteBuffer对象,并且还有一个方法选择集,用于以原始的字节形式或者基本数据类型输出和读取数据。但是,也没办法输出或者读取对象,即是是字符串的对象也不行。这种处理方式虽然很低级,但是正好,因为这是大多数操作系统中更有效的映射方式。

通道(Channel)介绍

Channel是一个对象,缓冲器可以通过它进行读取和写入数据。和原来的I/O做个比较,通道就像个流。正如前面所提到的,Channel是不和数据进行交互。但是它和流有一点不同,就是通道是双向的,而流只能是单向的(只能是InputStream或者OutputStream),但是通道可以用于读、写或者是同时用于读写。

在之前的I/O中有三个类被修改,可以用来产生FileChannel对象。这三个类是FileInputStreamFileOutputStream以及既用于读也用于写的RandomAccessFile

下面就举个创建FileChannel的例子。

 FileChannel in = new FileInputStream("fileName").getChannel();

NIO的使用

我会举一个简单的例子来演示如何使用NIO对文件进行复制的操作。还是上面所说的,NIO中对数据操作的是缓冲器,和缓冲器交互的通道,所以现在需要我们有两个对象一个是BufferChannel

    public static void main(String[] args) throws IOException {
         //获取读通道
        FileChannel in = new FileInputStream("/Users/hupengfei/Downloads/hu.sql").getChannel();
        //获取写通道
        FileChannel out = new FileOutputStream("/Users/hupengfei/Downloads/a.sql").getChannel();
        //为缓冲器进行初始化大小
        ByteBuffer byteBuffer =ByteBuffer.allocate(1024);
        while (in.read(byteBuffer)!=-1){
            //做好让人读的准备
            byteBuffer.flip();
            out.write(byteBuffer);
            //清除数据
            byteBuffer.clear();
        }
    }

一旦要用从缓冲器中读取数据的话,那么就要调用缓冲器的flip()方法,让它做好让别人读取字节的准备。那么写完数据以后就要调用缓存器的clear()方法对所有的内部的指针重新安排,以便缓冲器在另一个read()操作期间能够做好接受数据的准备。然后数据就会从源文件中源源不断的读到了目标文件中。

clear()方法在源码中有介绍,此方法不会实际的清除在缓冲器的数据。

当然上面的方法也可以简便,直接将两个通道进行相连只需要调用transferTo()方法,这个也是复制文件的效果。

    public static void main(String[] args) throws IOException {
        FileChannel in = new FileInputStream("/Users/hupengfei/Downloads/hu.sql").getChannel();
        FileChannel out = new FileOutputStream("/Users/hupengfei/Downloads/a.sql").getChannel();
        in.transferTo(0,in.size(),out);
    }

参考文章

相关文章
|
5月前
|
监控 Java API
现代 Java IO 高性能实践从原理到落地的高效实现路径与实战指南
本文深入解析现代Java高性能IO实践,涵盖异步非阻塞IO、操作系统优化、大文件处理、响应式网络编程与数据库访问,结合Netty、Reactor等技术落地高并发应用,助力构建高效可扩展的IO系统。
151 0
|
2月前
|
Java Unix Go
【Java】(8)Stream流、文件File相关操作,IO的含义与运用
Java 为 I/O 提供了强大的而灵活的支持,使其更广泛地应用到文件传输和网络编程中。!但本节讲述最基本的和流与 I/O 相关的功能。我们将通过一个个例子来学习这些功能。
179 1
|
4月前
|
Java 测试技术 API
Java IO流(二):文件操作与NIO入门
本文详解Java NIO与传统IO的区别与优势,涵盖Path、Files类、Channel、Buffer、Selector等核心概念,深入讲解文件操作、目录遍历、NIO实战及性能优化技巧,适合处理大文件与高并发场景,助力高效IO编程与面试准备。
|
4月前
|
SQL Java 数据库连接
Java IO流(一):字节流与字符流基础
本文全面解析Java IO流,涵盖字节流、字符流及其使用场景,帮助开发者理解IO流分类与用途,掌握文件读写、编码转换、异常处理等核心技术,通过实战案例提升IO编程能力。
|
5月前
|
存储 Java Linux
操作系统层面视角下 Java IO 的演进路径及核心技术变革解析
本文从操作系统层面深入解析Java IO的演进历程,涵盖BIO、NIO、多路复用器及Netty等核心技术。分析各阶段IO模型的原理、优缺点及系统调用机制,探讨Java如何通过底层优化提升并发性能与数据处理效率,全面呈现IO技术的变革路径与发展趋势。
112 2
|
9月前
|
存储 网络协议 安全
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
352 23
|
Java
Java 中 IO 流的分类详解
【10月更文挑战第10天】不同类型的 IO 流具有不同的特点和适用场景,我们可以根据具体的需求选择合适的流来进行数据的输入和输出操作。在实际应用中,还可以通过组合使用多种流来实现更复杂的功能。
400 57
|
10月前
|
缓存 网络协议 Java
JAVA网络IO之NIO/BIO
本文介绍了Java网络编程的基础与历史演进,重点阐述了IO和Socket的概念。Java的IO分为设备和接口两部分,通过流、字节、字符等方式实现与外部的交互。
297 0
|
12月前
|
监控 Java API
探索Java NIO:究竟在哪些领域能大显身手?揭秘原理、应用场景与官方示例代码
Java NIO(New IO)自Java SE 1.4引入,提供比传统IO更高效、灵活的操作,支持非阻塞IO和选择器特性,适用于高并发、高吞吐量场景。NIO的核心概念包括通道(Channel)、缓冲区(Buffer)和选择器(Selector),能实现多路复用和异步操作。其应用场景涵盖网络通信、文件操作、进程间通信及数据库操作等。NIO的优势在于提高并发性和性能,简化编程;但学习成本较高,且与传统IO存在不兼容性。尽管如此,NIO在构建高性能框架如Netty、Mina和Jetty中仍广泛应用。
316 3
|
12月前
|
存储 监控 Java
Java的NIO体系
通过本文的介绍,希望您能够深入理解Java NIO体系的核心组件、工作原理及其在高性能应用中的实际应用,并能够在实际开发中灵活运用这些知识,构建高效的Java应用程序。
357 5