4.Java程序优化-NIO

前言       

       Java标准I/O中,提供了基于流的I/O实现,即InputStream和OutputStream,以字节为单位处理数据,并且非常容易建立各种过滤器。
       NIO是New I/O的简称,具有以下的几个特点:
       1.为所有的原始类型提供(Buffer)缓存支持;

       2.使用Java.nio.charset.Charset作为字符集编码解码解决方案;
       3.增加通道(Channel)对象,作为新的原始I/O抽象;
       4.支持锁和内存映射文件的文件访问接口;
       5.提供基于Selector的异步网络I/O;
     与流式I/O不同的是,NIO是基于块(Block)的,它以块为基本单位处理数据。最重要的两个组件是缓冲Buffer和通道Channel。缓冲是一块连续的内存块,是NIO读写数据的中转地。通道Channel表示缓冲数据的源头或目的地,它用于向缓冲写入或读取数据,是访问数据的接口。

Buffer和Channel用法概述

        Buffer是一个抽象类,JDK为每一种Java原生类型都创建了Buffer实现类。除了ByteBuffer外,其他的Buffer实现类都具有完全一样的操作方法。因为ByteBuffer一般用于标准I/O操作。

       Channel是一个双向通道,即可读,也可写。有点类似于Stream,但Stream是单向的。
//读取文件时,首先将文件打开,并取得文件的Channel
FileInputStream fin = new FileInputStream(new File("d:\\tmp_buffer.tmp"));
FileChannel fc = fin.getChannel();
//要从文件Channel中读取数据,必须使用Buffer
ByteBuffer  byteBuffer = ByteBuffer.allocate(1024);
fc.read(bytrBuffer);
//此时,文件的内容已经存在于ByteBuffer中,因此可以关闭通道了,并准备读取ByteBuffer
fc.close();
byteBuffer.flip();
//通过NIO进行文件读取和文件写入操作例子如下
public static void nioCopyFile(String resource,String destination) throw IOException {
    FileInputStream fis = new FileInputStream(resource);
    FileOutputStream fos = new FileOutputStream(destination);

    FileChannel readChannel = fis.getChannel();    //读文件通道
    FileChannel writeChanel = fos.getChannel();    //写文件通道

    ByteBuffer buffer = ByteBuffer.allocate(1024); //分配数据缓存
    while(true){
         buffer.clear();
         int len = readChanel.read(buffer);  //从文件中读取文件到Buffer中去
         if(len == -1){
             break;  //读取完毕
         }
         buffer.flip();                 //重置position的值,通常,将Buffer从写模式转换为读模式时需要执行该方法
         writeChannel.write(buffer);    //从Buffer获得数据写入到文件中去
    }
    readChannel.close();
    writeChannel.close():
}

Buffer的基本原理

Buffer有三个重要的参数:位置(position),容量(capacity)和上限()

Buffer的三个参数的含义
参数读模式写模式
位置当前缓冲区的位置,将从position的下一个位置写数据当前缓冲区读取的位置,将从此位置后,读取数据
容量缓冲区的总容量上限缓冲区的总容量上限
上限缓冲区的实际上限,它总是小于等于容量。通常情况下,和容量相等代表可读取的总容易,和上次写入的数据量相等
public class BufferTest {
	public static void main(String[] args) {
		ByteBuffer buffer=ByteBuffer.allocate(15);  //15个字节的缓冲区
		System.out.println("limit="+buffer.limit()+" capacity="+buffer.capacity()+" position="+buffer.position());
		//存入10个字节的数据
		for(int i=0;i<10;i++) {
			buffer.put((byte)i);
		}
		System.out.println("limit="+buffer.limit()+" capacity="+buffer.capacity()+" position="+buffer.position());
		buffer.flip();     //重置position
		System.out.println("limit="+buffer.limit()+" capacity="+buffer.capacity()+" position="+buffer.position());
		for(int i=0;i<5;i++) {
			System.out.print(buffer.get());
		}
		System.out.println();
		System.out.println("limit="+buffer.limit()+" capacity="+buffer.capacity()+" position="+buffer.position());
        buffer.flip();
        System.out.println("limit="+buffer.limit()+" capacity="+buffer.capacity()+" position="+buffer.position());
	}
}

      首先分配一个15个字节大小的缓冲,初始情况如下图所示,需要注意的是,索引为15的位置实际上是不存在的。


      接着,Buffer中的被放入10个byte,因此,position的位置会向前移动,因为position位置始终指向下一个即将输入的位置,所以position变为10。

       接着执行flip()操作,该操作会重置position,通常,将Buffer从写模式转换为读模式时需要执行该方法。flip()操作不仅会重置当前position为0,还将limit设置到之前position的位置。这样做,是为了在读模式中,越界。

       接着执行5次读操作。
      最后,再调用一次flip(),将position归零,同时将limit设置到position的位置。

Buffer的相关操作

Buffer的创建

//Buffer的创建有两种方式,

//使用静态方法allocate()从堆中分配
ByteBuffer buffer = ByteBuffer.allocate(1024);

//从既有数组中创建
byte array[] = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(array);

重置和清空缓冲区

//Buffer提供了三个重置Buffer的各项标志位,并不真正清空Buffer的内容
public final Buffer rewind()
public final Buffer clear()
public final Buffer flip()

//将position置为0,并清除标志位(mark),它的作用在于为提取Buffer的有效数据做准备
out.write(buf);  //从Buffer读取数据写入到Channel
buf.rewind();    //回滚Buffer
buf.get(array);  //将Buffer的有效数据复制到数据中

//clear()将position设置为0,将limit设置为capacity的大小,清除标志mark
//由于清空了limit,因此无法知道Buffer内哪些数据是有效的
//这个方法为重新写Buffer做准备
buf.clear();
//in.read(buf);

//flip()将limit设置到position的位置,然后将position设置为0,并清除标志位mark。它通常在读写转换的时候使用
buf.put(magic);
in.read(buf);
buf.flip();        //将Buffer从写状态转换为读状态
out.write(buf);
Buffer重置函数的作用
 rewind()clear()flip()
position设置为0设置为0设置为0
mark清空清空清空
limit未改动设置为capacity设置为position
作用为读取Buffer中的有效数据做准备为重新写入Buffer做准备在读写切换时调用

读写缓冲区

//以ByteBuffer为例子,下面只是常用的几个方法
//返回当前position上的数据,并将position位置向后移一位
public byte get()
//读取当前Buffer的数据道dst中,并恰当地移动position位置
public ByteBuffer get(byte[] dst)
//读取给定index索引上的数据,不改变position的位置
public byte get(int index)
//当前位置写入给定的数据,position向后移动一位
public ByteBuffer put(byte b)
//将数据b写入当前Buffer的index位置
public ByteBuffer put(int index,byte b)
//将给定的数据写入当前Buffer
public final ByteBuffer put(byte[] src)

标志缓冲区

       标志(mark)缓冲区,像书签一样,在数据处理过程中,可以随时记录当前位置。然后在任意时刻,回到这个位置,从而加快或简化数据处理流程。

//用于记录当前位置
public final Buffer mark()
//用于恢复到mark所在的位置
public final Buffer reset()
ByteBuffer b = ByteBuffer.allocate(15);
for(int i=0;i<10;i++){
    b.put((byte)i);
}

b.flip();   //切换为读模式
for(int i=0;i<b.limit();i++){
   System.out.print(b.get());
   if(i==4){
      b.mark();   //在第4个位置做mark
      System.out.print("(mark at "+i+")");
   }
}
b.reset();     //回到mark的位置,并处理后续数据
System.out.println("reset to mark");
while(b.hasRemaining()){
    System.out.print(b.get());    //输出mark后的所有数据
}
System.out.println();

复制缓冲区

      复制缓冲区是指以原缓冲区为基础,生成一个完全一样的新的缓冲区。新生成的缓冲区和原缓冲区共享相同的内存数据。并且,对任意一方的数据改动都是互相可见的,但二者又独立维护了各自的position,limit和mark。
      这为多方同时处理数据提供了可能。

public ByteBuffer duplicate();
ByteBuffer b = ByteBUffer.allocate(15);
for(int i=0;i<10;i++){
    b.put((byte)i);
}

ByteBuffer c = b.duplicate();      //复制当前缓冲区,会产生两个完全一样的Buffer
System.out.println("After b.duplicate()");
System.out.println(b);
System.out.println(c);

c.flip();    //重置缓冲区c,两个Buffer各自维护自己的position和limit
System.out.println("Afterc.flip()");
System.out.println(b);
System.out.println(c);

c.put((byte)(100));    //向c放入数据,这个操作对两个Buffer都可见
System.out.println("After c.put((byte)(100))");
System.out.println("b.get(0)="+b.get(0));
System.out.println("c.get(0)="+c.get(0);

缓冲区分片

       用slice()方法在现有缓冲区中,创建新的子缓冲区,子缓冲区和父缓冲区共享数据。但需要处理Buffer的一个片段时,可以使用slice()方法取得一个子缓冲区,然后就像处理普通的缓冲区一样处理这个片段,而无需考虑缓冲区的边界问题。

ByteBuffer b = ByteBuffer.allocate(15);
for(int i=0;i<10;i++){
     b.put((byte)i);   //填充数据
}
b.position(2);
b.limit(6);
ByteBuffer subBuffer = b.slice();    //生成子缓冲区

只读缓冲区

       可以使用Buffer的asReadOnlyBuffer()获得一个与当前缓冲区一致的,并且共享内存数据的只读缓冲区。当需要把缓冲区作为参数传递给某个方法时,为了确保数据的安全,可以使用只读缓冲区。
       而且,由于共享内存,对原始缓冲区的修改,只读缓冲区也是可见的。
ByteBuffer readOnly = buffer.asReadOnlyBuffer();     //创建只读缓冲区

文件映射到内存

      NIO提供了一种将文件映射到内存,进行I/O操作的方法,它可以比常规的基于流的I/O的操作快很多。这个操作主要由FileChannel.map()方法实现。

RandomAccessFile raf = new RandomAccessFile("C:\\mapfile.txt","rw");
FileChannel fc = raf.getChannel();

//将文件映射到内存中
//MappedByteBuffer是ByteBuffer的子类,可以像使用ByteBuffer一样使用它
MappedByteBuffer mbb = fc.map(FileChannel.MapMod.READ_WRITE,O,raf.length());

while(mbb.hasRemaining()){
    System.out.print((char)mbb.get());   //直接从内存中读取文件
}
mbb.put(0,(byte)98);       //通过修改Buffer,将实际数据写到对应的磁盘文件中
raf.close();

处理结构化数据

       NIO提供了散射和聚集的方法处理结构化的数据。散射是指从一个Channel将数据读入一组Buffer中。而聚集与之相反,是将数据从一组Buffer写入一个Channel中。
       散射和聚集的使用方法与Buffer的操作类似。

//散射:ScatteringByteChannel的主要方法如下:
pubic long read(ByteBuffer[] dsts) throws IOException
public long read(ByteBuffer[] dsts,int offset,int length) throws IOException

//聚集:GatheringByteChannel
public long write(ByteBuffer[] srcs) throws IOException
public long write(ByteBuffer[] srcs,int offset,int length) throws IOException
       在散射读取过程中,Channel依次填充满每个Buffer。散射/聚集对处理结构化的数据非常有用。例如,对于一个有固定格式的文件的读写。在已知文件具体结构的情况下,可以构造若干个符合文件结构的Buffer,使得每个Buffer的大小正好符合文件各结构段的大小。

       此时,通过散射读的方式可以一次将文件装配到各个对应的Buffer。

       如果需要创建指定格式的文件,只有先构造好大小合适的Buffer对象,使用聚集写的方式就可以创建出文件了。

//聚集
ByteBuffer bookBuf = ByteBuffer.wrap("java编程思想".getBytes("utf-8"));
ByteBuffer autBuf  = ByteBuffer.wrap("fgg".getBytes("utf-8"));
booklen = bookBuf.limit();
authlen = autBuf.limit():
ByteBuffer[] bufs = new ByteBuffer[]{bookBuf,autBuf};

File file = new File(FPATH):
FileOutputStream fos = new FileOutputStream(file);

FileChannel fc = fos.getChannel();
fc.write(bufs);     //聚集写文件
fos.close():
//散射
ByteBuffer b1 = ByteBuffer.allocate(booklen);
ByteBuffer b2 = ByteBuffer.allocate(authlen);   //根据实际信息构造Buffer
ByteBuffer[] bufs = new ByteBuffer[]{b1,b2};

File file = new File(FPATH):
FileInputStream fis = new FileInputStream(file);


FileChannel fc = fis.getChannel();
fc.read(bufs);     //读入数据
String bookname = new String(bufs[0].array(),"utf-8");   //直接读数据

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值