Java New IO(NIO)详解

来源:互联网 发布:jsp调用java方法 编辑:程序博客网 时间:2024/06/13 23:31

I/O简介

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

2、在Java编程中,直到JDK1.4之前一直使用的是流的方式来完成I/O,所的有I/O都被看作是单个字节的移动,通过一个称为Stream的对象一次移动一个字节。流I/O用于与外部世界。它也在内部使用,用于将对象转换为字节,然后再转换回对象。

为什么要使用NIO

NIO与原来的I/O有同样的作用和目的,但是它使用不同的方式,NIO使用块I/O,而原来的I/O使用的是流I/O,在效率方面块I/O比流I/O要高很多。NIO创建的目的是为了让Java程序员可以实现高速的I/O而不需要编写自定义本机代码。NIO将最耗时的I/O操作(即填充和提取缓存区)转移回操作系统,所以才可以极大地提高速度。

NIO的特性

通道和缓冲区是NIO中的核心对象,几乎在每一个I/O操作中都要使用它们。
一、通道(Channel)
1、Channel是一个对象,可以通过它来读取和写入数据,拿NIO与原来的I/O做个比较,通道就好比是流。到任何目的地(或来自任何地方)的所有数据必须通过一个Channel对象一个 Buffer 实质上是一个容器对象。发送给一个通道的所有对象都必须首先放到缓冲区中;同样地,从通道中读取的任何数据都要读到缓冲区中
2、通道类型
通道与流的不同之处在于通道是双向的,而流只是在一个方面上移动 (一个流必须是InputStream或者是OutputStream的子类),通道可以用于读、写或者同用于读写。因为通道是双向的,流通道比流更好地反映底层操作系统的真实情况。特别是在UNIX模型中,底层操作系统的通道是双向的。
二、缓冲区(Buffer)
1、什么是缓冲区(Buffer):Buffer是一个对象, 它包含一些要写入或者刚读出的数据。 在 NIO 中加入 Buffer对象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,您将数据直接写入或者将数据直接读到 Stream对象中。在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问 NIO 中的数据,您都是将它放到缓冲区中。缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不仅仅 是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
2、缓冲区类型:最常用的缓冲区类型是 ByteBuffer。一个 ByteBuffer 可以在其底层字节数组上进行 get/set 操作(即字节的获取和设置)。ByteBuffer 不是 NIO 中唯一的缓冲区类型。事实上,对于每一种基本 Java 类型都有一种缓冲区类型:
  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
每一个 Buffer 类都是 Buffer 接口的一个实例。 除了 ByteBuffer,每一个 Buffer 类都有完全一样的操作,只是它们所处理的数据类型不一样。因为大多数标准 I/O 操作都使用 ByteBuffer,所以它具有所有共享的缓冲区操作以及一些特有的操作。
FolatBuffer的简单使用示例:
package com.io.newio;import java.nio.FloatBuffer;public class UseFloatBuffer {public static void main(String[] args) {       /*allocation()方法表示初始化缓冲区,注意在ByteBuffer中,allocate和allocateDirect都可以初始化缓冲区,它们的区别如下:        * allocate和allocateDirect方法都做了相同的工作,不同的是allocateDirect方法直接使用操作系统来分配Buffer。因而它将提供更快的访问速度。        * 不幸的是,并非所有的虚拟机都支持这种直接分配的方法。Sun推荐将以字节为单位的直接型缓冲区allocateDirect用于与大型文件相关并具有较长生命周期的缓冲区。          * */FloatBuffer buffer=FloatBuffer.allocate(10);for(int i=0;i<buffer.capacity();i++){float f=(float)Math.sin((((float)i)/10)*(2*Math.PI));buffer.put(f);}/*在读取缓冲区数据之前,必须调用flip方法。具体的原因如下:在对缓冲区读,写的时候,会共用Buffer的position,limit,capacity属性, * 而读写中,position和limit的意思是不一样的。position和limit的意思总结起来如下:position:缓冲区中的一个可以读写的位置,limit:缓冲区中的一个不可以读写的位置。   * 在完成缓冲区的读写操作以后,如果想复用缓冲区,那么应该有一个clear操作。 * */ buffer.flip();buffer.mark();// 注意mark和reset方法的配合使用  while(buffer.hasRemaining()){System.out.println(buffer.get());}System.out.println("*********************");buffer.reset();float [] fs=new float[buffer.limit()];buffer.get(fs); // 注意这里的get方法会有移位操作,也就是改变position的值 for(float f:fs){System.out.println(f);}}}

缓冲区(Buffer)的内部细节

一、NIO 中两个重要的缓冲区组件:状态变量和访问方法 (accessor)。状态变量是"内部统计机制"的关键。每一个读/写操作都会改变缓冲区的状态。通过记录和跟踪这些变化,缓冲区就可能够内部地管理自己的资源。在从通道读取数据时,数据被放入到缓冲区。在有些情况下,可以将这个缓冲区直接写入另一个通道,但是在一般情况下,您还需要查看数据。这是使用 访问方法 get() 来完成的。同样,如果要将原始数据放入缓冲区中,就要使用访问方法 put()
1、状态变量
可用三个值来指定缓冲区在任意时刻的状态,这三个变量可以跟踪缓冲区的状态和它所包含的数据。
a、position:缓冲区实际上就是美化了的数组。在从通道读取时,会将所读取的数据放到底层的数组中,psoition变量用来跟踪已经写了多少数据。更准确地说,position它指定了下一个字节放到数组的那一个元素中。因此如果从通道中读取三个字节到缓冲区中,那么缓冲区的position将会置为3,指向数组的第四个元素。同样,在写入通道时,是从缓冲区获取数据,position跟踪从缓冲区获取了多少数据。如果从缓冲区写了5个字节到通道中,那么缓冲区的position会被设置为5,指向数组的第六个元素。
b、limit:表示正在被使用缓冲区的大小,当我们为缓冲区分配buffer.allocation(64),这时limit就为64,然后进行buffer.putInt(30);//表示4byte;最后再buffer.flip(); 经过这两步后,这时limit就为4,所以limit的值一定大于或等于position。
c、capacity:表明可以存储在缓冲区的最大数据容量。实际上就是指定了底层数组的大小 。limit绝对不能大于capacity
2、访问方法
在ByteBuffer类中有四种get()方法
  1. byte get();
  2. ByteBuffer get( byte dst[] );
  3. ByteBuffer get( byte dst[], int offset, int length );
  4. byte get( int index );

ByteBuffer 类中有五个 put() 方法:

  1. ByteBuffer put( byte b );
  2. ByteBuffer put( byte src[] );
  3. ByteBuffer put( byte src[], int offset, int length );
  4. ByteBuffer put( ByteBuffer src );
  5. ByteBuffer put( int index, byte b );
get()与put()方法还有类型化的版本如:getByte()、getChar()、getInt()、putFloat()、putInt()、putDouble()等等。
关于put和get方法的简单使用:
package com.io.newio;import java.nio.ByteBuffer;/*put方法与get方法的使用*/public class TypesInByteBuffer {<span style="white-space:pre"></span>public static void main(String[] args) {<span style="white-space:pre"></span>ByteBuffer buffer = ByteBuffer.allocate(64);<span style="white-space:pre"></span>buffer.putInt(30);<span style="white-space:pre"></span>buffer.putLong(70000000000L);<span style="white-space:pre"></span>buffer.putDouble(Math.PI);<span style="white-space:pre"></span>buffer.flip();<span style="white-space:pre"></span>System.out.println(buffer.getInt());// 根据放入的顺序来取<span style="white-space:pre"></span>System.out.println(buffer.getLong());<span style="white-space:pre"></span>System.out.println(buffer.getDouble());<span style="white-space:pre"></span>System.out.println("************");<span style="white-space:pre"></span>buffer.clear();<span style="white-space:pre"></span>buffer.putInt(10);<span style="white-space:pre"></span>buffer.putInt(20);<span style="white-space:pre"></span>buffer.putInt(30);<span style="white-space:pre"></span>buffer.flip();<span style="white-space:pre"></span>// 如下三行输出代码,注意使用的index值,请读者自己考虑原因。(ps:1 int = 4 byte)<span style="white-space:pre"></span>System.out.println(buffer.getInt(4));<span style="white-space:pre"></span>System.out.println(buffer.getInt(0));<span style="white-space:pre"></span>System.out.println(buffer.getInt(8));<span style="white-space:pre"></span>System.out.println("***********************");<span style="white-space:pre"></span>// 上面的getInt(int index)方法并不影响position和limit,所以下面的语句可以奏效<span style="white-space:pre"></span>System.out.println(buffer.getInt());<span style="white-space:pre"></span>System.out.println(buffer.getInt());<span style="white-space:pre"></span>System.out.println(buffer.getInt());<span style="white-space:pre"></span>}}
二、只读缓冲区

    只读缓冲区非常简单 ― 您可以读取它们,但是不能向它们写入。可以通过调用缓冲区的 asReadOnlyBuffer() 方法,将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区(并与其共享数据),只不过它是只读的。只读缓冲区对于保护数据很有用。在将缓冲区传递给某个对象的方法时,您无法知道这个方法是否会修改缓冲区中的数据。创建一个只读的缓冲区可以 保证 该缓冲区不会被修改。不能将只读的缓冲区转换为可写的缓冲区。

三、直接和间接缓冲区

    另一种有用的 ByteBuffer 是直接缓冲区。 直接缓冲区 是为加快 I/O 速度,而以一种特殊的方式分配其内存的缓冲区。实际上,直接缓冲区的准确定义是与实现相关的。Sun 的文档是这样描述直接缓冲区的:给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据)。还可以用内存映射文件创建直接缓冲区。

四、内存映射文件I/O

        内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。内存映射文件 I/O 是通过使文件中的数据神奇般地出现为内存数组的内容来完成的。这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。一般来说,只有文件中实际读取或者写入的部分才会送入(或者 映射 )到内存中。内存映射并不真的神奇或者多么不寻常。现代操作系统一般根据需要将文件的部分映射为内存的部分,从而实现文件系统。Java 内存映射机制不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。尽管创建内存映射文件相当简单,但是向它写入可能是危险的。仅只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的在下面的例子中,我们要将一个 FileChannel (它的全部或者部分)映射到内存中。为此我们将使用 FileChannel.map() 方法。下面代码行将文件的前 1024 个字节映射到内存中:map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,您可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行行映射。

package com.io.newio;import java.io.RandomAccessFile;import java.nio.MappedByteBuffer;import java.nio.channels.FileChannel;public class UseMappedFile {static private final int start=0;static private final int size=1024;public static void main(String[] args) throws Exception{RandomAccessFile raf=new RandomAccessFile("user.txt", "rw");FileChannel fc=raf.getChannel();MappedByteBuffer mbb=fc.map(FileChannel.MapMode.READ_WRITE, start, size);mbb.put(0,(byte)97);mbb.put(1023,(byte)122);System.out.println(raf.length());raf.close();}}





0 0