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("\n"+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("\n"+cnDecoder.decode(bbuff));**
    }
}
孙悟空
0

-53 
-17 
-50 
-14 
-65 
-43 

孙悟空

ByteBuffer bbuff=cnEncoder.encode(cbuff);
System.out.println("\n"+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。

posted @ 2020-05-06 20:36  小新和风间  阅读(221)  评论(0编辑  收藏  举报