- Java的NIO
实际开发中NIO使用到的并不多,我并不是说NIO使用情景不多,是说我自己接触的并不是很多,前面我在博客园和CSDN上转载了2篇别人写的文章,这里来大致总结下Java的NIO,大概了解下。
- NIO和传统IO的对比:
在使用传统IO的时候,不管是使用节点流这种底层流还是使用处理流这种高级流,在底层操作的都是字节,所以性能就不会很好,在使用BufferedReader这种高级流的时候还会阻塞该线程,所以Java1.4后出现了NIO。
NIO相关的类都放在了Java.nio包下,他的功能比较强大的。NIO使用了不同的方式(内存映射文件)来处理输入和输出,也就是说NIO将文件或文件的一段区域映射到内存中,这样子就可以象访问内存一样访问文件了。
NIO的2个核心:Channel(通道)和Buffer(缓存)。
- 说明下这2个核心:
Buffer就是一个数组。他的作用就是装入数据和输出数据。Buffer有2个重要的方法:flip(从Buffer中取出数据)和clear(向Buffer中放入数据)
Channel可以直接将指定文件的部分或者全部直接映射成buffer。程序也不能直接和Channel来交互数据,包括读和写,都要经过Buffer这个中间的东西才可以。
- 从理论到实践:NIO 中的读和写
读和写是 I/O 的基本过程。
1,从一个通道中读取很简单:只需创建一个缓冲区,然后让通道将数据读到这个缓冲区中。
2,写入也相当简单:创建一个缓冲区,用数据填充它,然后让通道用这些数据来执行写入操作。
- 从文件中读取
如果使用原来的 I/O,那么我们只需创建一个 FileInputStream 并从它那里读取。而在 NIO 中,情况稍有不同:我们首先从FileInputStream 获取一个 FileInputStream 对象,然后使用这个通道来读取数据。
在 NIO 系统中,任何时候执行一个读操作,都是从通道中读取,但是不能直接从通道读取。因为所有数据最终都驻留在缓冲区中,所以要从通道读到缓冲区中。
因此读取文件涉及三个步骤:(1) 从 FileInputStream 获取 Channel,(2) 创建 Buffer,(3) 将数据从 Channel 读到 Buffer 中。
第一步:从FileInputStream获取通道:
FileInputStream fin = new FileInputStream( "LinkinPark.txt" );
FileChannel fc = fin.getChannel();
第二步:创建缓冲区:
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
第三步:将数据从通道读到缓冲区中:
fc.read( buffer );
注意:我们不需要告诉通道要读多少数据到缓冲区中。每一个缓冲区都有复杂的内部统计机制,它会跟踪已经读了多少数据以及还有多少空间可以容纳更多的数据
- 写入文件
在 NIO 中写入文件类似于从文件中读取。
第一步:从 FileOutputStream 获取一个通道:
FileOutputStream fout = new FileOutputStream( "LinkinPark.txt" );
FileChannel fc = fout.getChannel();
第二步:创建一个缓冲区并在其中放入一些数据
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
for (int i=0; i<message.length; ++i) {
buffer.put( message[i] );
}
buffer.flip();
第三步:写入缓冲区中
fc.write( buffer );
注意:在这里同样不需要告诉通道要写入多数据。缓冲区的内部统计机制会跟踪它包含多少数据以及还有多少数据要写入。
- 读写结合
下面我们将看一下在结合读和写时会有什么情况。我们以一个名为 CopyFile.java 的简单程序作为这个练习的基础,它将一个文件的所有内容拷贝到另一个文件中。CopyFile.java 执行三个基本操作:首先创建一个 Buffer,然后从源文件中将数据读到这个缓冲区中,然后将缓冲区写入目标文件。这个程序不断重复 ― 读、写、读、写 ― 直到源文件结束。
CopyFile 程序让您看到我们如何检查操作的状态,以及如何使用 clear() 和 flip() 方法重设缓冲区,并准备缓冲区以便将新读取的数据写到另一个通道中。下面是一个例子:
1,因为缓冲区会跟踪它自己的数据,所以 CopyFile 程序的内部循环 (inner loop) 非常简单
fcin.read( buffer );
fcout.write( buffer );
第一行将数据从输入通道 fcin 中读入缓冲区,第二行将这些数据写到输出通道 fcout 。
2,检查状态,检查拷贝何时完成。当没有更多的数据时,拷贝就算完成,并且可以在 read() 方法返回 -1 是判断这一点
int r = fcin.read( buffer );
if (r==-1) {
break;
}
3,重设缓冲区
最后,在从输入通道读入缓冲区之前,我们调用 clear() 方法。同样,在将缓冲区写入输出通道之前,我们调用 flip() 方法
buffer.clear();
int r = fcin.read( buffer );
if (r==-1) {
break;
}
buffer.flip();
fcout.write( buffer );
clear() 方法重设缓冲区,使它可以接受读入的数据。 flip() 方法让缓冲区可以将新读入的数据写入另一个通道。
下面代码大致用来演示下Java的NIO,我从来没有用过,所以也就直接从资料粘代码过来了:
import java.nio.*; public class BufferTest { public static void main(String[] args) { // 创建Buffer CharBuffer buff = CharBuffer.allocate(8); //① System.out.println("capacity: " + buff.capacity()); System.out.println("limit: " + buff.limit()); System.out.println("position: " + buff.position()); // 放入元素 buff.put('a'); buff.put('b'); buff.put('c'); //② System.out.println("加入三个元素后,position = " + buff.position()); // 调用flip()方法 buff.flip(); //③ System.out.println("执行flip()后,limit = " + buff.limit()); System.out.println("position = " + buff.position()); // 取出第一个元素 System.out.println("第一个元素(position=0):" + buff.get()); // ④ System.out.println("取出一个元素后,position = " + buff.position()); // 调用clear方法 buff.clear(); //⑤ System.out.println("执行clear()后,limit = " + buff.limit()); System.out.println("执行clear()后,position = " + buff.position()); System.out.println("执行clear()后,缓冲区内容并没有被清除:" + "第三个元素为:" + buff.get(2)); // ⑥ System.out.println("执行绝对读取后,position = " + buff.position()); } }
import java.io.*; import java.nio.*; import java.nio.channels.*; import java.nio.charset.*; public class FileChannelTest { public static void main(String[] args) { File f = new File("FileChannelTest.java"); try( // 创建FileInputStream,以该文件输入流创建FileChannel FileChannel inChannel = new FileInputStream(f).getChannel(); // 以文件输出流创建FileBuffer,用以控制输出 FileChannel outChannel = new FileOutputStream("a.txt") .getChannel()) { // 将FileChannel里的全部数据映射成ByteBuffer MappedByteBuffer buffer = inChannel.map(FileChannel .MapMode.READ_ONLY , 0 , f.length()); // ① // 使用GBK的字符集来创建解码器 Charset charset = Charset.forName("GBK"); // 直接将buffer里的数据全部输出 outChannel.write(buffer); // ② // 再次调用buffer的clear()方法,复原limit、position的位置 buffer.clear(); // 创建解码器(CharsetDecoder)对象 CharsetDecoder decoder = charset.newDecoder(); // 使用解码器将ByteBuffer转换成CharBuffer CharBuffer charBuffer = decoder.decode(buffer); // CharBuffer的toString方法可以获取对应的字符串 System.out.println(charBuffer); } catch (IOException ex) { ex.printStackTrace(); } } }
import java.io.*; import java.nio.*; import java.nio.channels.*; public class RandomFileChannelTest { public static void main(String[] args) throws IOException { File f = new File("a.txt"); try( // 创建一个RandomAccessFile对象 RandomAccessFile raf = new RandomAccessFile(f, "rw"); // 获取RandomAccessFile对应的Channel FileChannel randomChannel = raf.getChannel()) { // 将Channel中所有数据映射成ByteBuffer ByteBuffer buffer = randomChannel.map(FileChannel .MapMode.READ_ONLY, 0 , f.length()); // 把Channel的记录指针移动到最后 randomChannel.position(f.length()); // 将buffer中所有数据输出 randomChannel.write(buffer); } } }
import java.io.*; import java.nio.*; import java.nio.channels.*; import java.nio.charset.*; public class ReadFile { public static void main(String[] args) throws IOException { try( // 创建文件输入流 FileInputStream fis = new FileInputStream("ReadFile.java"); // 创建一个FileChannel FileChannel fcin = fis.getChannel()) { // 定义一个ByteBuffer对象,用于重复取水 ByteBuffer bbuff = ByteBuffer.allocate(64); // 将FileChannel中数据放入ByteBuffer中 while( fcin.read(bbuff) != -1 ) { // 锁定Buffer的空白区 bbuff.flip(); // 创建Charset对象 Charset charset = Charset.forName("GBK"); // 创建解码器(CharsetDecoder)对象 CharsetDecoder decoder = charset.newDecoder(); // 将ByteBuffer的内容转码 CharBuffer cbuff = decoder.decode(bbuff); System.out.print(cbuff); // 将Buffer初始化,为下一次读取数据做准备 bbuff.clear(); } } } }
最后,关于Java的NIO还有2个知识,1个是编码和解码,1个是文件锁
编码和解码: 我们在玩j2ee的时候经常要最数据做编码和解码,因为要前后台交互数据,这里的编码和解码主要是说我们在使用NIO的时候对文件做读写操作时用来的编码和解码。编码就是把明文的字符串序列转换成计算机理解的字节序列,解码就是把字节序列转化成普通人能看得懂的明文字符串。 对于我来说使用最大的就2种字符串集,一个是GBK(简体中文字符集)一个是UTF-8(8位的UCS转化格式)。
import java.nio.charset.*; import java.util.*; public class CharsetTest { public static void main(String[] args) { // 获取Java支持的全部字符集 SortedMap<String,Charset> map = Charset.availableCharsets(); for (String alias : map.keySet()) { // 输出字符集的别名和对应的Charset对象 System.out.println(alias + "----->" + map.get(alias)); } } }
import java.nio.*; import java.nio.charset.*; public class CharsetTransform { public static void main(String[] args) throws Exception { // 创建简体中文对应的Charset Charset cn = Charset.forName("GBK"); // 获取cn对象对应的编码器和解码器 CharsetEncoder cnEncoder = cn.newEncoder(); CharsetDecoder cnDecoder = cn.newDecoder(); // 创建一个CharBuffer对象 CharBuffer cbuff = CharBuffer.allocate(8); cbuff.put('孙'); cbuff.put('悟'); cbuff.put('空'); cbuff.flip(); // 将CharBuffer中的字符序列转换成字节序列 ByteBuffer bbuff = cnEncoder.encode(cbuff); // 循环访问ByteBuffer中的每个字节 for (int i = 0; i < bbuff.capacity() ; i++) { System.out.print(bbuff.get(i) + " "); } // 将ByteBuffer的数据解码成字符序列 System.out.println("\n" + cnDecoder.decode(bbuff)); } }
文件锁在操作系统上是很平常的事情,使用文件锁可以有效的阻止多个进程并发修改同一个文件。其实和数据库的锁是差不多的。
import java.io.*; import java.nio.*; import java.nio.channels.*; public class FileLockTest { public static void main(String[] args) throws Exception { try( // 使用FileOutputStream获取FileChannel FileChannel channel = new FileOutputStream("a.txt") .getChannel()) { // 使用非阻塞式方式对指定文件加锁 FileLock lock = channel.tryLock(); // 程序暂停10s Thread.sleep(10000); // 释放锁 lock.release(); } } }