JavaSE学习笔记之NIO

NIO概述

BIO,也称为阻塞IO,是在JDK1.4之前使用的IO模型。从JDK1.4开始,Java增强了输入/输出的功能,称为新IO(New IO,简称NIO),实现这些功能的类都放在java.nio包及其子包下。NIO采用内存映射文件的方式处理输入/输出,NIO将文件或文件的一段区域映射到内存中,这样就可以通过访问内存一样访问文件了。主要涉及Channel、Buffer、Charset和Selector等类及其子类。

Buffer类及其子类


Buffer类及其子类位于java.nio包下,Buffer(缓冲区)类是一个抽象类,其最常用的类是ByteBuffer,其余子类采用相似的方法管理不同类型的数据。Buffer是特定基本类型元素的线性有限序列。除内容外,缓冲区的基本属性还包括容量、限制和位置:

  • capacity(容量): 是它所包含的元素的数量。Buffer的capacity不能为负并且不能更改。
  • limit(限制):是第一个不应该读取或写入的元素的索引,即位于limit后的数据不能被读或写。Buffer的限制不能为负,并且不能大于其容量。
  • position(位置) :是下一个要读取或写入的元素的索引。Buffer的位置不能为负,并且不能大于其限制。
  • mark(标记):类似于BIO中的mark,Buffer允许将position的位置定位到mark处。标记的定义是可选的。

四者的关系:0 <= mark <= position <= limit <= capacity

Buffer的所有子类都提供了get()和set()方法,用于向Buffer中获取和放置数据。每个子类都定义了两种获取和放置操作:

  • 相对操作:读取或写入一个或多个元素,它从当前位置开始,然后将位置增加所传输的元素的个数。如果请求的传输超出限制,则相对获取操作将抛出 BufferUnderflowException,相对放置操作将抛出 BufferOverflowException;这两种情况下,都没有数据被传输。
  • 绝对操作:直接根据索引向Buffer中写入或读取数据,该操作不影响position(位置)。如果索引参数超出限制,绝对获取 操作和放置操作将抛出IndexOutOfBoundsException。

新创建的Buffer对象总有一个0的position和一个未定义的mark。初始limit可以为0,也可以为其他值,这取决于Buffer类型及其构建方式。一般情况下,Buffer的初始内容是未定义的。

除了访问position、limit、capacity的方法以及做标记和重置的方法外,此类还定义了以下可对Buffer进行的操作:

  • clear():它将limit设置为capacity大小,将position设置为0。使Buffer为下一次读取或相对放置操作(put()方法)做好准备。
  • flip():它将limit设置为当前position,然后将position设置为0。使Buffer为下一次写入或相对获取操作(get()方法)做好准备。
  • rewind():它使limit保持不变,将位置设置为 0。使缓冲区为重新读取已包含的数据做好准备。

Buffer主要的作用就是装入数据,然后输出数据。通过allocate()方法创建Buffer子类对象,初始时position为0,limit为capacity,可通过put()方法向Buffer中放入一些数据(或者从Channel中获取一些数据),随着数据放入,position位置会相应地后移。装载好数据后,调用flip()方法,将limit设置为position所在位置,然后将position设置为0,为输出数据做好准备。当Buffer输出数据结束后,可以调用clear()方法,将position置为0,将limit置为capacity,为下一次读取做好准备。常用方法如下:

  • int capacity() :返回此缓冲区的容量。
  • int limit() :返回此缓冲区的限制。
  • int position() :返回此缓冲区的位置。
  • Buffer mark() :在此缓冲区的位置设置标记。
  • Buffer limit(int newLimit) :设置此缓冲区的限制。
  • Buffer position(int newPosition) :设置此缓冲区的位置。
  • int remaining() :返回当前位置与限制之间的元素数。
  • Buffer reset() :将此缓冲区的位置重置为以前mark的位置。
  • boolean hasRemaining() :告知在当前位置和限制之间是否有元素。
  • abstract Object array() :返回此缓冲区的底层实现数组(可选操作)。
  • abstract int arrayOffset() :返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量(可选操作)。
  • abstract boolean hasArray() :告知此缓冲区是否具有可访问的底层实现数组。
  • abstract boolean isDirect() : 告知此缓冲区是否为直接缓冲区。
  • abstract boolean isReadOnly():告知此缓冲区是否为只读缓冲区。

注意:

  • 通过allocate()方法创建的Buffer对象是普通Buffer对象,ByteBuffer对象还提供了一个allocateDirect()方法来创建直接Buffer。直接Buffer的创建成本高于普通Buffer,所以直接Buffer只适用于创建长期生存的Buffer。但是直接Buffer效率高于普通Buffer。只有ByteBuffer提供了allocateDirect()方法。
  • 每个缓冲区都是可读取的,但并非每个缓冲区都是可写入的。只读缓冲区不允许更改其内容,但其mark、position和limit值是可变的。可以调用其 isReadOnly 方法确定缓冲区是否为只读。
  • 多个当前线程使用缓冲区是不安全的。如果一个缓冲区由不止一个线程使用,则应该通过适当的同步来控制对该缓冲区的访问。
  • Buffer允许将方法调用组成一个链。如:
 b.flip();
 b.position(23);
 b.limit(42);

可以由以下更紧凑的一个语句代替

 b.flip().position(23).limit(42);

示例:

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());
    }
}

Channel接口及其实现类类

Channel(通道)接口继承自Closeable接口。Channel(通道)表示到实体(如硬件设备、文件、网络资源或可执行多个 I/O 操作的程序组件)开放的连接。用于将指定文件的部分或全部映射成Buffer。不能直接访问Channel中的数据,只能通过Buffer间接对Channel中的数据进行读写。

创建Channel时它处于打开状态,一旦将其关闭,则保持关闭状态。一旦关闭了某个Channel,试图对其调用 I/O 操作就会导致 ClosedChannelException 被抛出。通过调用Channel的isOpen()方法可测试通道是否处于打开状态。

一般情况下通道对于多线程的访问是安全的。
所有的Channel都不应该通过构造器创建,而是应该通过传统节点的getChannel()方法来创建对应的Channel实现类。不同的节点流获得不同的Channel。如FileInputStream、FileOutputStream的getChannel()方法返回的是FileChannel,而PipedInputStream、PipedOutputStream的getChannel()方法返回的是Pipe.SinkChannel、Pipe.SourceChannel。

这里以FileChannel为例,FileChannel抽象类中常用方法有map方法和read、write方法。

  • abstract MappedByteBuffer map(FileChannel.MapMode mode, long position, long size) :将此通道的文件区域直接映射成ByteBuffer。第一个参数控制映射的模式为只读或读写模式。
  • long read(ByteBuffer[] dsts) :将字节序列从此通道读入给定的缓冲区。
  • abstract int read(ByteBuffer dst) : 将字节序列从此通道读入给定的缓冲区。
  • abstract long read(ByteBuffer[] dsts, int offset, int length) :将字节序列从此通道读入给定缓冲区的子序列中。
  • abstract int read(ByteBuffer dst, long position) :从给定的文件位置开始读取字节序列,并写入给定的缓冲区。
  • long write(ByteBuffer[] srcs) :将字节序列从给定的缓冲区写入此通道。
  • abstract int write(ByteBuffer src) :将字节序列从给定的缓冲区写入此通道。
  • abstract long write(ByteBuffer[] srcs, int offset, int length) :将字节序列从给定缓冲区的子序列写入此通道。
  • abstract int write(ByteBuffer src, long position) :从给定的文件位置开始,将字节序列从给定缓冲区写入此通道。
  • abstract long position() :返回此通道的文件位置。
  • abstract FileChannel position(long newPosition) :设置此通道的文件位置。
  • abstract long size() :返回此通道的文件的当前大小。

实例:

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();
        }
    }
}

RandomAccessFile类也有一个getChannel()方法可以返回FileChannel,其读写模式取决于RandomAccessFile的打开方式。实例:

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);
        }
    }
}

如果担心Channel中一次映射的内容太多而导致性能下降,也可以选择多次小批量重复读写数据。示例:

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(256);
            // 将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();
            }
        }
    }
}

字符集和Charset类

字符集:
先说字符,字符(Character)是文字与符号的总称,包括文字、图形符号、数学符号等。 一组抽象字符的集合就是字符集(Charset)。 字符集常常和一种具体的语言文字对应起来,该文字中的所有字符或者大部分常用字符就构成了该文字的字符集,比如、简体中文字符集、英文字符集。
字符编码:
任何文件存储在计算机里都是二进制的形式,包括文本、图片、视频、程序等。文本之所以能显示出文字来,是因为系统将底层的二进制序列转换成字符的缘故。文本和二进制序列的转换就涉及到编码(Encode)和解码(Decode)的问题。编码就是将文本按照指定的规则解析成二进制序列,解码就是按照一定的规则将二进制序列转换成文本。编码和解码所遵循的规则如果不同就会导致文本显示为乱码,而这个规则就是字符编码。不同的字符集都有对应的字符编码。常见字符集名称及其字符编码:

字符集描述字符编码及占用字节
ASCII字符集现代英语和其他西欧语言的字符集采用ASCII编码,单字节
GB2312字符集简体中文字符集采用GB2312编码,双字节
BIG5字符集繁体中文字符集采用BIG5编码双,双字节
GB18030字符集所有汉字、日文、朝鲜语和中国少数民族文字组成的大字符集采用GB18030编码,单字节、双字节和四字
Unicode字符集支持世界上所有语言字符采用多种字符编码

Unicode 的实现方式不同于编码方式。一个字符的Unicode编码是确定的,但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。
Unicode的实现方式称为Unicode转换格式(Unicode Translation Format,简称为 UTF)。

  • UTF-8: 8bit变长编码,对于大多数常用字符集(ASCII中0~127字符)它只使用单字节,而对其它常用字符(特别是朝鲜和汉语会意文字),它使用3字节。
  • UTF-16: 16bit编码,是变长码,大致相当于20位编码,值在0到0x10FFFF之间,基本上就是unicode编码的实现,与CPU字序有关。

字符集和编码方式如此之多,我们只需选择一种兼容性最好的编码方式和字符集,即UTF-8。毕竟GBK/GB2312是国内的标准,当我们大量使用国外的开源软件时,UTF-8才是编码界最通用的语言。

Java默认使用Unicode字符集,但是有的操作系统并不是用Unicode字符集,JDK1.4提供了Charset来处理字符序列和字节序列的转换关系。该类定义了用于创建解码器和编码器以及获取与 charset 关联的各种名称的方法。此类的实例是不可变的。常用方法如下:

  • static SortedMap<String,Charset> availableCharsets() :返回当前JDK所支持的所有字符集。
  • static Charset forName(String charsetName) :返回指定名称的字符编码格式的 charset 对象。
  • abstract CharsetDecoder newDecoder() :为此 charset 构造新的解码器。
  • abstract CharsetEncoder newEncoder() :为此 charset 构造新的编码器。
  • ByteBuffer encode(CharBuffer cb) :将此 charset 中的 Unicode 字符编码成字节的便捷方法。
  • ByteBuffer encode(String str) :将此 charset 中的字符串编码成字节的便捷方法。
  • CharBuffer decode(ByteBuffer bb) :将此 charset 中的字节解码成 Unicode 字符的便捷方法。
  • String name() : 返回此 charset 的规范名称。

其中,CharsetDecoder和CharsetEncoder分别代表编码器和解码器,利用CharsetDecoder的decode()方法可以将ByteBuffer转换成CharBuffer,调用CharsetEncoder的encoder()方法可以将CharBuffer或者String转换成ByteBuffer。如果仅需要简单的编码和解码操作,使用Charset的encode()方法和decode()方法即可。
示例:

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));
        }
    }
} 
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));
    }
}

文件锁

文件锁可以控制文件的全部或部分字节的访问,使用文件锁机制可以有效阻止多个进程并发修改同一个文件,所以多数操作系统都提供了文件锁功能。从JDK1.4开始提供FileLock来支持文件锁功能。FileChannel抽象类的获取文件锁的方法如下:

  • FileLock lock() :获取对此通道的文件的独占锁定。 无法获得锁时一直阻塞。
  • FileLock tryLock() :试图获取对此通道的文件锁。如果获得了文件锁则返回文件锁,否则返回null而不会一直阻塞。
  • abstract FileLock lock(long position, long size, boolean shared) : 同上面的lock方法,只是获取部分文件的锁。若shared参数为ture,则请求获得共享锁,允许多个进程读取文件,若为false,则请求获得一个排它锁,仅当前进程可以读写进行操作。
  • abstract FileLock tryLock(long position, long size, boolean shared) :同上面的trylock方法,只是获取部分文件的锁。

FileLock抽象类常用方法如下:

  • FileChannel channel() :返回文件通道,此锁定保持在该通道的文件上。
  • boolean isShared() :判断此锁定是否为共享的。
  • abstract boolean isValid() :判断此锁定是否有效。
  • long position() : 返回文件内锁定区域中第一个字节的位置。
  • abstract void release() :使用完文件后应该释放此锁定。
  • long size() :返回锁定区域的大小,以字节为单位。
  • String toString() : 返回描述此锁定的范围、类型和有效性的字符串。
  • boolean overlaps(long position, long size) : 判断此锁定是否与给定的锁定区域重叠。

示例:

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();
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值