12.9 NIO
Posted weststar
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了12.9 NIO相关的知识,希望对你有一定的参考价值。
当BufferedReader读取输入流中的数据,如果没有读到有效数据,程序将阻塞该线程的执行(使用InputStream的read()方法从流中读取数据时,如果数据源中没有数据,它也会阻塞线程),也就是传统的输入流、输出流都是阻塞式输入、输出。不仅如此传统的输入流、输出流都是通过字节的移动来处理的(即使不直接去处理字节流,但底层的实现还是依赖于字节处理),也就是说,面向流的输入输出系统一次只能处理一个字节,因此面向流的输入、输出效率不高。
JDK 1.4开始,Java提供一些改进的输入/输出新功能,这些功能被称为新IO(New IO,简称NIO),新增了许多输入输出类,这些类都被放在java.nio包及其子包下。
一、Java新IO概述
新IO和传统的IO有相同的目的,都是用于进行输入/输出功能,但新IO使用了不同的方式来处理处理输入/输出,新IO采用内存映射文件的方式来处理输入/输出,新IO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了(这种方式模拟了操作系统上的虚拟内存的概念),通过这种方式来进行输入/输出比传统的输入/输出要快得多。
下面示意图展示了Java新IO包和各种类之间的树关系:
1.1 Java中NIO相关包介绍
Java中NIO相关的包如下:
java.nio包:主要提供了一些和Buffer相关的类。
java.nio.channels包:主要包括Channel和Selector相关的类。
java.nio.charset包:主要包含和字符集相关的类。
java.nio.channels.spi包:主要包含提供Channel服务的类。
java.nio.charset.spi包:主要包含提供字符集服务的相关类。
1.2 新IO中的核心对象Channel(通道)和Buffer(缓冲)
Channel(通道)和Buffer(缓冲)是新IO中的两个核心对象,Channel是对传统输入/输出系统中里的模拟,在新IO系统中所有数据都需要通过通道传输;Channel与传统的InputStream、OutputStream最大的区别在于它提供了一个map方法,通过该map方法可以直接将“一块数据”映射到内存中。如果说传统的输入/输出系统是面向流的处理,而新IO则是面向块的处理。
Buffer可以被理解成一个容器,它的本质是一个数组,发送到Channel中的所有对象都必须首先放到Buffer中,而从Channel中读取的数据也必须先读到Buffer中。此处的Buffer有点类似于前面我们介绍的“竹筒”,但该Buffer既可以像前面那样一次、一次去Channel中取水,也允许使用Channel直接将文件的某块数据映射成Buffer。
1.3 其他类:Charset类、Selector类
除了Channel和Buffer之外,新IO还提供了用于将UNICODE字符串映射成字节序列以及逆映射操作的Charset类,还提供了用于支持非阻塞式输入/输出的Selector类。
二、使用Buffer
2.1 Buffer的种类——class XxxBuffer extends Buffer;
从内部结构上来看,Buffer就像一个数组,它可以保存多个类型相同的数据。Buffer是一个抽象类,其最常用的子类是ByteBuffer,它可以在底层字节数组上进行get/set操作,除了ByteBuffer之外,其他基本数据类型(boolean除外)都有相应的Buffer类:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。
2.2 获取Buffer对象
除了ByteBuffer之外,上面的Buffer都采用相同或相似的方法来管理数据,只是各自管理数据的类型不同而已。这些Buffer没有提供构造器,只提供如下方法来获取一个Buffer对象:
static XxxBuffer allocate(int capacity)
//allocate拨…(给); 划…(归); 分配…(给);
ByteBuffer和CharBuffer用得最多,其他Buffer子类用的较少。ByteBuffer类还有一个子类:MappedByteBuffer,它用于表示Channel将磁盘文件的部分或全部内容映射到内存中得到的结果,通常MappedByteBuffer对象由Channel的map()方法返回。
2.3 Buffer的三个重要概念
★容量(capacity):缓冲区的 容量(capacity) 表示该Buffer的最大数据容量,即最多可以存储多少数据。缓冲区的容量不可能为负值,在创建后也不能改变。
★界限(limit):第一个不应该被读出或者写入的缓冲区位置索引。也就是说,位于limit后的数据既不可被读,也不可被写。
★位置(position):用于指明下一个可以被读出的或者写入的缓冲区位置索引(类似于IO流中的记录指针)。当使用Buffer从Channel中读取数据时,position的值恰好等于已经读到了多少数据。当刚刚新建一个Buffer对象时,其position为0,如果从Channel中读取了2个数据到该Buffer中,则postion为2,指向Buffer中第三个(第一个位置的索引为0)位置。
除此之外,Buffer还支持一个可选的标记(mark,类似于传统IO流中的mark),Buffer允许直接将position定位到mark处,这些值满足的关系是:
0<=mark<=position<=limit<=capacity
如图显示了某个Buffer读入了一些数据后的示意图:
2.4 Buffer装入数据,然后输出数据的过程
Buffer的主要作用就是装入数据,然后输出数据(其作用类似于输入输出流的水管),开始时Buffer的position为0,limit为capacity,程序通过put()方法像Buffer中放入一些数据(或则从Channel中取出一些数据),没放入一些数据,Buffr的position位置相应地向后移动一些位置。
Buffer装入数据的示意图:
当Buffer装入数据结束后,调用Buffer的flip()方法,该方法将limit设置为position所在位置,并将position设置为0,这使得Buffer的读写指针又移到开始的位置。也即是说,Buffer调用flip()方法之后,Buffer为输出数据做好了准备;
当Buffer输出数据结束后,Buffer调用clean()方法,clear()方法不是清空Buffer数据,它仅仅只是将position置为0,将limit置为capacity,这样再次向Buffer中装入数据做准备。
2.5 Buffer常用方法
Buffer的基本常用方法:
(1)int capacity():返回Buffer的capacity大小。
(2)boolean hasRemaining():判断当前位置(position)和界限之间是否还具有元素可供处理。
(3)int limit():返回Buffer界限(limit)的位置。
(4)Buffer mark():设置Buffer的mark位置,它只能在0和position之间做mark。
(5)int position():返回position的值。
(6)Buffer position(int newPs):设置Buffer的position,并返回position被修改后的Buffer对象。
(7)int remianing():返回当前位置和界限之间元素个数。
(8)Buffer reset:将位置(position)转到mark所在位置。
(9)Buffer rewind():将位置position设置为0,取消设置的mark。
Buffer的所有子类还提供了两个重要的方法:put()、get()方法,用于向Buffer中放入数据和从Buffer中取出数据。支持单个数据访问,也支持毗连数据访问(以数组作为参数)。
当使用put()、get()方法访问Buffer中的数据时,分为相对和绝对两种:
(1)相对(Relative):从Buffer的当前position处开始读取或写入数据,然后将位置position的值按处理后的元素的个数增加。
(2)绝对(Absolute):直接根据索引向Buffer中读取或写入数据,使用绝对的方式访问Buffer里的数据时,并不会影响位置(position)的值。
2.6 Buffer使用示例
package section9;
import java.nio.CharBuffer;
public class BufferTest
{
public static void main(String[] args)
{
//创建Buffer,准备装入数据
CharBuffer buff=CharBuffer.allocate(8);//①
System.out.println("Buffer的容量capacity:"+buff.capacity());//8
System.out.println("Buffer的界限(limit):"+buff.limit());//8
System.out.println("Buffer的位置(position):"+buff.position());//0
//放入元素
buff.put(‘a‘);
buff.put(‘b‘);
buff.put(‘c‘);//②
System.out.println("加入三个元素后,position="+buff.position());//3
//调用flip()方法,Buffer为输出数据准备阶段
buff.flip();//③
System.out.println("执行flip()方法后,limit="+buff.limit());//3
System.out.println("执行flip()方法后,position="+buff.position());//0
//取出第一个元素
System.out.println("第一个元素(position=0):"+buff.get());//④ 输出为‘a‘
System.out.println("取出第一个元素后,position="+buff.position());//1
//调用clear()方法,为下次装入数据做好准备
buff.clear();//⑤
System.out.println("执行clear()方法后,limit="+buff.limit());//8
System.out.println("执行clear()方法后,position="+buff.position());//0
System.out.println("执行clear()方法后,Buffer中的内容并没有被清除,第三个元素为"+buff.get(2));//c
System.out.println("执行绝对读取后,position="+buff.position());//0
}
}
下面分析整个程序运行过程:
1、调用CharBuffer的静态方法allocate()创建一个capacity为8的CharBuffer,此时capacity=8,position=0,limit=0,进入准备装入数据的阶段:
2、代码2处王Buffer中装入三个元素,放入元素后的CharBuffer效果图如下所示:
3、代码3处,调用Buffer的flip()方法,该方法将把limit设为position处,再把position设置为0:
当Buffer调用flip()方法之后,limit就就移动到原来的position所在的位置,这时相当于把Buffer中没有数据的存储空间"封印"起来,从而避免读取Buffer数据时读取到null值。
4、代码4处取出第一个元素,取出第一个元素后position向后移动一位,也就是position等于1.
5、代码5处,Buffer调用clear()放啊,将position设为0,将limit设为与capacity相同。执行clear()方法后的Buffer的示意图如下所示:
6、执行代码6处依然可以取出位置2的值,也就是字符‘c‘。代码6处根据索引来取值,属于绝对方式的get(),所以不会影响到position的位置。
2.7★ Buffer的拓展
通过allocate()只能创建普通Buffer,ByteBuffer还提供了一个allocateDirect()方法来创建直接Buffer,直接Buffer的创建成本比普通Buffer的创建成本高,但直接Buffer的读取效率更高。
直接Buffer只适用于生存期长的Buffer,而不适用于短期、一次性用完就丢弃的Buffer。且只有ByteBuffer才提供allocateDirect()方法,所以只能再ByteBuffer的级别上创建直接Buffer。如果希望使用其他类型,则应该将该Buffer转换为其他类型的Buffer。
直接Buffer的用法上和普通Buffer用法基本相同,没有太大区别
三、使用Channel
3.1 Channel与传统流对象的区别
Channel类似于传统的流对象,但与传统的流不同的是,Channel有两个主要的区别:
1、Channel可以直接将指定文件的部分或全部映射成Buffer。
2、程序不能直接访问Channel中的数据,包括读取、写入都不行,Channel只能与Buffer进行交互。
也就是说,如果要从Channel中取得数据,必须先用Buffer从Channel中取出一些数据,然后让程序从Buffer中取出这些数据;如果要将程序中的数据写入Channel,一样先让程序将数据放入Buffer中,程序再将Buffer里的输入写入Channel中。
3.2 Channel及其实现类
Channel是一个接口,位于java.nio.channels包下,系统为该接口提供了DatagramChannel、 FileChannel、Pipe.SinkChannel、Pipe.SourceChannel、SelectableChannel、ServerSocketChannel, SocketChannel等实现类,本节主要介绍FileChannel的用法,根据这些Channel的名字我们不难发现新IO里的Channel是按功能来划分的,例如Pipe.SinkChannel、Pipe.SourceChannel用于支持线程之间通信的管道Channel,而ServerSocketChannel、SocketChannel则是用于支持TCP网络通信的Channel。
3.3 Channel获取
所有Channel都不应该通过构造器来直接创建,而是通过传统节点InputStream、OutputStream的getChannel()方法返回对应的Channel,不同节点流获取的Channel不一样。例如FileInputStream、FileOutputStream的getChannel()返回的是FileChannel,而PipedInputStream、PipedOutputStream的getChannel()返回的是Pipe.SinkChannel、Pipe.SourceChannel。
3.4 Channel的常用方法
1、map():用于将Channel对应的部分或全部数据映射成ByteBuffer;
方法签名->MappedByteBuffer map(FileChannel.MapMode mode,long position,long size),第一个参数执行映射时的模式,分别有只读、读写等模式;第二个、第三个参数用于控制将Channel的哪些数据映射成ByteBuffer.
2、read()和write():都有一系列的重载形式,这些方法用于从Buffer中读取数据或向Buffer中写入数据。
3.5 从FileInputStream和FileOutputStream中获取FileChannel应用举例
下面程序示范了直接将FileChannel的全部数映射成ByteBuffer的效果:
package section9;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
public class FileChannelTest
{
public static void main(String[] args)
{
File f=new File("src//section9//FileChannelTest.java");
System.out.println(f.length());//返回文件内容的长度:1045
try(
//创建FileInputStream,以该文件输入流创建FileChannel
var inChannel=new FileInputStream(f).getChannel();
//以文件输出流创建FileChannel,用于控制输出
var outChannel=new FileOutputStream("src//section9//a.txt").getChannel()
)
{
//将FileChannel里的全部数据全部映射成ByteBuffer
MappedByteBuffer buffer=inChannel.map(FileChannel.MapMode.READ_ONLY,0,f.length());//代码1
for(int i=0;i<20;i++)//每个转义字符转换占两个位置,一个字母占一个byte
{System.out.print((char)buffer.get());}
System.out.println("
"+buffer.position());
buffer.position(0);
System.out.println(buffer.position());
//使用GBK的字符集创建解码器
Charset charset=Charset.forName("utf-8");
//直接将Buffer中的数据全部输出
outChannel.write(buffer);//代码2
//再次调用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 ioe)
{
ioe.printStackTrace();
}
}
}
2159
package section9;
i
20
0
package section9;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.CharBuffer;
...
上面分别使用了FileInputStream、FileOutputStream来获取Channel,虽然FileChannel既可以读也可以写入,但是FileInputSream获取的FileChannel只能读,而FileOutputStrem获取的FileChannel只能写/代码1处直接将Channel中的全部数据映射成ByteBuffer,然后代码2处直接将整个ByteBuffer的全部数据写入一个FileChannel中,这就完成了文件的复制。
程序后面为了将FileChannelTest.java文件中的内容全部打印出来,使用了Charset类和CharsetDecoder类将ByteBuffer全部转换成CharSetBuffer。后面会详细介绍。
3.6 RandomAccessFile获取FileChannel应用举例
在RandomAccessFile中也包含一个getChannel()方法,RandomAccessFile返回得FileChannel是只读还是读写,则取决于RandomAccessFile打开文件得模式,例如下面程序将会对a.txt文件得内容进行复制,最佳在该文件后面。
package section9;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class RandomAccessFileChannelTest
{
public static void main(String[] args)
throws IOException
{
var f=new File("src//section9//a.txt");
try(
//创建一个RandomAccessFile对象
var 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);
}
}
}
上面得程序将Channel得记录指针移动到Channel得最后,从而让程序指定ByteBuffer得数据追加到Channel得后面。每次运行运行上面得程序都会将a.txt文件得内容全部复制一遍,并将全部得内容追加到文件得后面。
3.7 Channel通道可以像传统IO一样多次重复读写数据
如果习惯了传统IO的“用竹筒多次重复取水”的过程,或者担心Channel对应的文件过大,使用map()方法一次将所有文件内容全部映射到内存中引起性能下降,也可以使用Channel和Buffer传统的“用竹筒多次取水”的方式:
package section9;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
public class ReadFile
{
public static void main(String[] args)
throws IOException
{
try(
//创建文件输入流
var fis=new FileInputStream("src//section9//ReadFile.java");
//创建文件输出流
var fos=new FileOutputStream("src//section9//b.txt");
//创建一个FileChannel
var fcIn=fis.getChannel();
var fcOut=fos.getChannel()
)
{
//定义一个Buffer对象,用于重复取水
ByteBuffer buff=ByteBuffer.allocate(256);
//将FileChannel中的全部数据放入ByteBuffer中
while(fcIn.read(buff)!=-1)
{
//锁定Buffer的空白区域
buff.flip();//将limit移动带position处
//将buffer中的数据写入输出Channel中
fcOut.write(buff);
//将buffer初始化,为下一次读取数据做准备
buff.clear();
}
}
}
}
上面的程序将生成一个b.txt文件,复制了Read.java文件的内容。
下面程序改为打印出ReadFile.java文件的内容
package section9;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
public class ReadFile
{
public static void main(String[] args)
throws IOException
{
try(
//创建文件输入流
var fis=new FileInputStream("src//section9//ReadFile.java");
//创建文件输出流
var fos=new FileOutputStream("src//section9//b.txt");
//创建一个FileChannel
var fcIn=fis.getChannel();
var fcOut=fos.getChannel()
)
{
//定义一个Buffer对象,用于重复取水
ByteBuffer bbuff=ByteBuffer.allocate(256);
//将FileChannel中的全部数据放入ByteBuffer中
while(fcIn.read(bbuff)!=-1)
{
//锁定Buffer的空白区域
bbuff.flip();//将limit移动带position处
//创建Charser对象
Charset charset=Charset.forName("UTF-8");
//创建解码器对对象
CharsetDecoder decoder=charset.newDecoder();
//将ByteBuffer中的内容转码
CharBuffer cbuffer=decoder.decode(bbuff);
//打印数
System.out.print(cbuffer);
//将buffer中的数据写入输出Channel中
fcOut.write(bbuff);
// //将buffer中的数据写入输出Channel中—
// fcOut.write(bbuff);
//将buffer初始化,为下一次读取数据做准备
bbuff.clear();
}
}
}
}
上面代码虽然使用了FileChannel和Buffer来读取文件,但处理方式使用了InputStream、Byte[]来读取文件的方式几乎一样,都是采用"用竹筒多次重复取水"的方式。但Buffer的flip()和clear()两个方法,程序处理起来也比较方便。每次读写数据后调用flip()方法将没有数的区域封印起来,避免程序从Buffer中取出null值;数据取出后立即调用clear方法将Buffer的position取为0,为下一次读取数据做好准备。
四、字符集和Charset
4.1 解码和编码简介
通常而言,把明文的字符串序列转换成计算机理解的字节序列(二进制文件,普通人看不懂)成为编码,把字节序列转化成普通人能看懂的明文字符串称为解码。
计算机底层是没有文本文件、图片文件之分的,它只是忠诚地记录每个文件的二进制序列而已,当需要保存文本文件时,程序先把文件中的每个字符翻译成二进制序列;当需要读取文本文件时,程序必须把二进制序列转化为一个个的字符。
Java默认使用Unicode字符集,但很多操作系统并不采用Unicode字符集,那么从系统中读取数据到Java程序中时,就可能出现乱码问题。
4.2 Charset介绍和常用字符集
JDK1.4提供了Charset处理字节序列和字符序列之间的转换关系,该类用于创建解码器和编码器,还提供Charset所支持字符集方法,Charset类是不可变。
Charset类提供了一个availableCharsets()静态方法来获取当前JDK所支持的所有字符集。所以程序可以使用以下程序来获取JDK所支持的全部字符集。
package section9;
import java.nio.charset.Charset;
import java.util.SortedMap;
public class CharsetTest
{
public static void main(String[] args)
{
//获取Java支持的全部字符集
SortedMap<String, Charset> map=Charset.availableCharsets();
for(var alias:map.keySet())
{
//输出字符集的别名和对应的Charset对象
System.out.println(alias+"--->"+map.get(alias));
}
}
}
Big5--->Big5
Big5-HKSCS--->Big5-HKSCS
CESU-8--->CESU-8
EUC-JP--->EUC-JP
EUC-KR--->EUC-KR
GB18030--->GB18030
GB2312--->GB2312
...
上面程序SortedMap<String, Charset> map=Charset.availableCharsets();获取了Java所支持的全部字符集,并用遍历的方式打印了所有字符集的别名(字符集的字符串名称)和Charset对象。从上面的程序可以看出每个字符集都有一个字符串名称,也成为字符串的别名。
对于中国程序员,有几个常用字符集:
字符集 | 介绍 |
---|---|
GBK | 简体中文字符集 |
BIG5 | 繁体字中文字符集 |
ISO-8859-1 | ISO拉丁字母表,也叫作ISO-LATIN-1 |
UTF-8 | 8位UCS转换格式 |
UTF-16BE | 16位UCS转换格式,Big-endian(最低地址存放高位字节)字节顺序 |
UTF-16LE | 16位UCS转换格式,Little-endian(最高地址存放低位字节)字节顺序 |
UTF-16 | 16位UCS转换格式,字节顺序由可选的字节顺序标记来标识 |
一旦知道了字符集别名后,程序就可以调用Charset的forName()方法来创建Charset对象,forName()方法的参数就是相应字符集的别名。例如下面代码:
Charset cs=Charset.forName("ISO-8859-1");
Charset csCn=Charset.forName("GBK");
拓展:java 7 新增了一个StandardCharsets类,该类里包含ISO-8859-1、UTF-8、UTF-16等常用变量,这些变量代表了最常用的字符集对应的Charset对象:
4.3 解码器和编码器使用
一旦获取了Charset对象后,就可以通过该对象的newDecoder()、newEncoder()这两个方法分别返回CharsetDecoder和CharsetEncoder对象,代表了CharSet的解码器和编码器。
调用CharsetDecoder的decode()就可以将ByteBuffer(字节序列)转换为CharBuffer(字符序列),调用CharsetEncoder的encode()就可以将CharBuffer(字符序列转换为ByteBuffer(字节序列)。
如下程序就使用了CharsetDecoder的decode()和CharsetEncoder的encode()完成ByteBuffer和CharBuffer之间的转换。
package section9;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
public class CharsetTransform
{
public static void main(String[] args) throws CharacterCodingException {
//创建简体中文所对应的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();//将limit设置到position,position=0
System.out.println(cbuff);
System.out.println(cbuff.position());
//将CharBuffer中字符系列转化为字节序列
** ByteBuffer bbuff=cnEncoder.encode(cbuff);**
System.out.println(cbuff);//此时cbuff不存在了,也就输出为空
//循环输出ByteBuffer中的每个字节
for(var i=0;i<bbuff.capacity();i++)
{
System.out.println(bbuff.get()+" ");
}
bbuff.flip();
//将ByteBuffer的数据全部解码成字符序列
** System.out.println("
"+cnDecoder.decode(bbuff));**
}
}
孙悟空
0
-53
-17
-50
-14
-65
-43
孙悟空
ByteBuffer bbuff=cnEncoder.encode(cbuff);
System.out.println("
"+cnDecoder.decode(bbuff));
上面两行代码分别实现了将CharBuffer转换成ByteBuffer,将ByteBuffer转换成CharBuffer的功能。实际上Charset里也提供了如下三个方法:
CharBuffer decode(ByteBuffer bb):将ByteBuffer中字节序列转换成字符序列的的便捷方法。
ByteBuffer encode(CharBuffer cb):将CharBuffer中的字符序列转换成字节序列的便捷方法。
ByteBuffer encode(String str):将String中的字符序列转换成字节序列的便捷方法。
也就是说,获取了Charset对象后,如果仅仅只需要进行简单的编码、解码操作,起始可以不用创建CharsetEncoder和CharsetDecoder对象,直接调用Charset的encode()和decode()方法进行编码、解码。
package section9;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
public class CharsetTransformTest
{
public static void main(String[] args)
{
//创建简体中文的Charset
Charset cn=Charset.forName("GBK");
//创建一个Buffer对象
CharBuffer cbuff=CharBuffer.allocate(8);
cbuff.put(‘孙‘);
cbuff.put(‘悟‘);
cbuff.put(‘空‘);
//将limit移动到position所在位置,position=0
cbuff.flip();
System.out.println(cbuff);//孙悟空
System.out.println(cbuff.position());//0
//将字符序列转化为字节序列的便捷方式
ByteBuffer bbuff=cn.encode(cbuff);
System.out.println(cbuff.position());//3
cbuff.flip();//执行上面的准换后,position位置发生改变
System.out.println(cbuff);//孙悟空
System.out.println(bbuff);//java.nio.HeapByteBuffer[pos=0 lim=6 cap=6]
//将字节序列转化为字符序列的便捷方式
CharBuffer cbuff1=cn.decode(bbuff);
System.out.println(cbuff1.position());
System.out.println(cbuff1);//孙悟空
}
}
提示:在String类里也提供了一个getBytes(String charset)方法,该方法返回byte[],该方法也是使用指定的字符集将字符串转换为字节序列。
五、文件锁
如果多个系统需要并发修改同一个文件时,程序之间需要某种机制来进行通信,使用文件锁可以有效地阻止多个进程并发修改同一个文件,所以现在大部分操作系统都提供了文件锁的功能。
文件锁控制文件或者文件部分字节的访问,但文件锁在不同操作系统的差别较大,所以早期的JDK版本并未提供文件锁的支持。从JDK1.4的新IO开始,Java开始提供文件锁的支持。
5.1 锁定文件
在NIO中,Java提供了FileLock来支持文件锁定功能,在FileChannel中提供的lock()/tryLock()方法可以获取文件锁FileLock对象,从而锁定文件。
Lock()和tryLock()方法的区别在于:
当Lock试图锁定某个文件时,如果无法得到文件锁,程序将一直阻塞;而tryLock()是尝试锁定文件,他将直接返回而不是阻塞,如果获得了文件锁,该方法返回文件锁,否则将返回null。
5.2 部分锁定
如果FileChannel只是像锁定文件部分内容,而不是锁定全部内容,可以使用如下的lock()或tryLock()方法:
1、lock(long position,long size,boolean shared):对文件的position开始,长度为size的内容加锁,该方法是阻塞式的。
2、tryLock(long position,long size,boolean shared):非阻塞式的加锁方式。参数同上类似。
当shared为true时,表明该锁是一个共享锁,它允许多个线程来读取该文件,但阻止其他进程获得对该文件的排他锁。当shared为false时,表明该锁是一个排他锁,它将锁住对文件的读写。程序可以通过调用FileLock的isShared来判断它获得的锁是不是共享锁。
直接使用lock()或tryLock()方法获取的文件锁是排他锁
处理完文件后通过FileLock的release()释放文件锁。下面程序示范了使用FileLock锁定文件。
package section9;
import java.io.FileOutputStream;
import java.nio.channels.FileLock;
public class FileLockTest
{
public static void main(String[] args)
{
try(
//使用FileOutputStream获取FileChannel
var channel=new FileOutputStream("src//section9//a.txt").getChannel()
)
{
//使用非阻塞式方式对文件加锁
FileLock lock=channel.tryLock();
System.out.println(lock);//sun.nio.ch.FileLockImpl[0:9223372036854775807 exclusive valid]
//程序暂停10s
Thread.sleep(10000);
//释放锁
lock.release();
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
上面程序FileLock lock=channel.tryLock();对指定文件加锁,接着程序调用Thread.sleep(10000)暂停10s后才释放文件锁,因此在这10s内,其他程序无法对a.txt文件进行修改。
5.3 文件锁的注意事项
在某些平台上,文件锁仅仅是建议性的,并不是强制式的。这意味着即使一个程序不能获得文件锁,它也可以对该文件进行读写。
在某些平台上,不能同步地锁定一个文件并把它映射到内存中。
文件锁是由Java虚拟机所持有的,如果两个Java程序使用同一个Java虚拟机运行,则它们不能对同一个文件进行加锁。
在某些平台上当关闭 FileChannel时,会释放Java虚拟机在该文件上的所有锁,因此应该避免对同一个被锁定的文件打开多个FileChannel。
以上是关于12.9 NIO的主要内容,如果未能解决你的问题,请参考以下文章