zoukankan      html  css  js  c++  java
  • Java NIO 的简单介绍和使用

    在使用 BufferedReader 读取输入流中的数据时,如果没有读到有效数据,程序将在此出阻塞该线程的执行(使用 InputStream 的 read() 方法从流中读取数据时,如果数据流中没有数据,它也会阻塞该线程)。也就是说,传统的输入流、输出流都是阻塞式输入、输出。

    从 JDK 1.4 开始,Java 提供了一系列新的IO(New IO,简称 NIO),位于 java.nio 包及子包下,并且对原 java.io 包中的很多类以 NIO 为基础进行了改写,新增了满足 NIO 的功能。

    Channel(通道)和 Buffer(缓冲)是 NIO 中的两个核心对象。

    Buffer

    Buffer 可以被理解成一个容器,它的本质是一个数组。

    Buffer 是一个抽象类,其最常用的子类是 ByteBuffer。除此之外,还有对应于其他基本数据(boolean除外)对应的 Buffer 类:CharBuffer、ShorBuffer......等。

    在 Buffer 中有三个重要的概念:capacity(容量)、limit(界限)、position(位置)

    • capacity(容量):缓冲区的容量,创建后不可改变,不可能为负值。
    • limit(界限):limit 后的数据不可被读写。
    • position(位置):用于指明下一个可以被读出的或者写入的缓冲区位置索引。

    此外,Buffer 里还支持一个可选标记(mark),这些值满足下列关系:

    0 << mark << position << limit << capacity
    

    如图:

    Buffer 的主要作用是装入数据,然后输出数据。

    开始时 Buffer 的 position 为 0,limit 为 capacity,程序可以通过 put() 方法向 Buffer 中放入数据,没放入一些数据,position 相应地向后移动一些位置。

    当 Buffer 装入数据结束后,调用 Buffer 的 flip() 方法,该方法将limit 设置为 position 所在位置,并将 position 设为 0,这就使得 Buffer 的读写指针又回到了开始位置,之后 Buffer 为输出数据做好准备。(调用 get() 方法可输出数据)

    当 Buffer 输出数据结束后,Buffer 可调用 clear() 方法,这个方法不是清空 Buffer 的数据,它仅仅是将 position 设置为0,将 limit设置为 capacity,这样为再次向 Buffer 装入数据做准备。但之前已有的数据还是在 Buffer 中,后面装数据时会将原来位置的数据覆盖。

    import java.nio.CharBuffer;
    
    public class BufferDemo2 {
    
        public static void main(String[] args) {
            CharBuffer buffer = CharBuffer.allocate(8);     //创建 Buffer 时指定 capacity为8
            //开始时 limit = capacity = 8, position = 0
            System.out.println("capacity: " + buffer.capacity() 
                               + ", limit: " + buffer.limit() 
                               + ", position: " + buffer.position());
    
            // 输入数据
            buffer.put('a');
            buffer.put('b');
            buffer.put('c');
            // capacity: 8, limit: 8, position: 3
            System.out.println("capacity: " + buffer.capacity() 
                               + ", limit: " + buffer.limit() 
                               + ", position: " + buffer.position());  
    
            buffer.flip();
            System.out.println("******* flip  **");
            // capacity: 8, limit: 3, position: 0
            System.out.println("capacity: " + buffer.capacity() 
                               + ", limit: " + buffer.limit() 
                               + ", position: " + buffer.position()); 
    
            // 输出数据,返回a
            System.out.println("1: " + buffer.get());
            // capacity: 8, limit: 3, position: 1
            System.out.println("capacity: " + buffer.capacity() 
                               + ", limit: " + buffer.limit() 
                               + ", position: " + buffer.position()); 
    
            System.out.println("******* clear  **");
            buffer.clear();
            // capacity: 8, limit: 8, position: 0
            System.out.println("capacity: " + buffer.capacity() 
                               + ", limit: " + buffer.limit() 
                               + ", position: " + buffer.position());  
    
            System.out.println("clear 后,第一个元素: " + buffer.get(0));  // a
            System.out.println("clear 后,第三个元素: " + buffer.get(2));  // c
            buffer.put('d');
            System.out.println("limit: " + buffer.limit());     // 8
            System.out.println("position: " + buffer.position());   // 1
            System.out.println("clear 后,第一个元素: " + buffer.get(0));  // d
            System.out.println("clear 后,第三个元素: " + buffer.get(2));  // c
        }
    }
    

    Channel

    Channel 类似于传统的流对象,但也有两个主要区别。

    • Channel 可以直接将指定文件的部分或全部直接映射成 Buffer。
    • 程序不能直接访问 Channel 中的数据,包括读写都不行,Channel 只能与 Buffer 进行交互。

    Java为 Channel 接口提供了 DatagramChannel、FileChannel、Pipe.SinkChannel,、Pipe.SourceChannel、 SelectableChannel,、ServerSocketChannel,、SocketChannel 等实现类。

    其中, Pipe.SinkChannel,、Pipe.SourceChannel 是用于支持线程之间通信的管道 Channel ;ServerSocketChannel,、SocketChannel 是用于支持 TCP 网络通信的 Channel ;DatagramChannel 则是用于支持 UDP 网络通信的 DatagramChannel。

    所有的 Channel 都不应该通过构造器来创建,而是通过传统的节点流 InputStram、OutputStream 的 getChannel() 方法来返回对应的 Channel ,不同节点流获得的 Channel 不一样。

    例如,FileInputStream、FileOutputStream 的getChannel() 返回的是 FileChannel

    Channel 中最常用的三类方法是 map()、read()、write(),其中:

    map() 方法用于将 Channel 对应的部分或全部数据映射成 ByteBuffer;

    read() 或 write() 方法都有一系列重载形式,这些方法用于从 Buffer 中读取或写入数据。

    MappedByteBuffer map(FileChannel.MapMode mode,long position,long size) : 第一个参数执行映射时的模式,分别为只读、读写、专用 这3种模式;

    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.nio.CharBuffer;
    import java.nio.MappedByteBuffer;
    import java.nio.channels.FileChannel;
    import java.nio.charset.Charset;
    import java.nio.charset.CharsetDecoder;
    
    /**
     * 将文件 object.txt 的内容复制到文件 b.txt 中,并在控制台输出
     */
    public class FileChannelTest {
        public static void main(String[] args) {
    
            File f = new File("object.txt");
            try {
                //创建 FileInputStream,以该文件输入流创建 FileChannel
                FileChannel inChannel = new FileInputStream(f).getChannel();
                // 以文件输出流创建 FileChannel,用以控制输出
                FileChannel outChannel = new FileOutputStream("b.txt").getChannel();
    
                //将 inChannel 里的全部数据映射成 ByteBuffer
                MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
                
                Charset charset = Charset.forName("GBK");
                // 直接将 buffer 里的数据全部输出
                outChannel.write(buffer);
                
                buffer.clear();
                // 创建解码器(CharsetDecoder)对象
                CharsetDecoder decoder = charset.newDecoder();
                // 使用解码器将 ByteBuffer 转换成 CharBuffer
                CharBuffer charBuffer = decoder.decode(buffer);
                // CharBuffer 的 toString 方法可以获取对应的字符串
                System.out.println(charBuffer);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    }
    

    以上的通过 map() 方法一次将所有的文件内容映射到内存当中,如果担心读取的文件过大引起性能下降,也可以使用 Channel 和 Buffer 传统的 ”用竹筒多次重复取水“ 的方式。

    import java.io.FileInputStream;
    import java.io.IOException;
    import java.nio.ByteBuffer;
    import java.nio.CharBuffer;
    import java.nio.channels.FileChannel;
    import java.nio.charset.Charset;
    import java.nio.charset.CharsetDecoder;
    
    /**
     * 使用Channel 和 Buffer 传统的 ”用竹筒多次重复取水“ 的方式
     */
    public class ChannelTest2 {
    
        public static void main(String[] args) throws IOException {
            // 创建文件输入流
            FileInputStream fis = new FileInputStream("object.txt");
            // 获取一个 FileChannel
            FileChannel channel = fis.getChannel();
    
            //定义一个 ByteBuffer 对象,用于多次取水
            ByteBuffer bbuff = ByteBuffer.allocate(256);
            // 将 FileChannel 中的数据放入 ByteBuffer 中
            while (channel.read(bbuff) != -1) {
                // 锁定 Buffer 的空白区
                bbuff.flip();
                Charset charset = Charset.forName("GBK");
                CharsetDecoder decoder = charset.newDecoder();
                CharBuffer cbuff = decoder.decode(bbuff);
                System.out.println(cbuff);
                // 将 Buffer 的 position 重置为 0,为下一次读取数据做准备
                bbuff.clear();
            }
        }
    }
    

    上面代码的处理方式和使用 InputStream、byte[] 来读取文件的方式几乎一样,都是采用 ”用竹筒多次重复取水“ 的方式。不过 Buffer 提供了 flip() 和 clear() 两个方法,每次读取数据后调用 flip() 方法将没有数据的区域 “封印” 起来,避免程序从 Buffer 中取出 null 值;数据取出后又调用 clear() 方法将 Buffer 的 position 重置为 0,为下一次读取数据做准备。


    字符集和 Charset

    编码(Encode)解码(Decode):通常而言,把明文的字符序列转换成计算机理解的二进制序列成为编码;把二进制序列转换成普通人能看懂的明文字符串称为解码。

    Java 默认使用 Unicode 字符集,但很多操作系统并不使用 Unicode 字符集,所以当从系统读取数据到 Java 程序时,就可能出现乱码等问题。

    JDK 1.4 提供了 Charset 来处理字节序列和字符序列之间的转换关系

    Charset 类提供了一个 availableCharsets() 静态方法来获取当前 JDK 所支持的所有字符集。

    public class CharsetDemo {
    
        public static void main(String[] args) {
            // 获取 Java 支持的所有字符集
            SortedMap<String, Charset> map = Charset.availableCharsets();
            // 遍历
            for (String s : map.keySet()) {
                System.out.println(s + "------>" + map.get(s));
            }
        }
    }
    

    每一个字符集都有一个字符串别名,常用的字符串别名有:GBK、ISO-8859-1、UTF-8、UTF-16 等。

    一旦知道了字符集的别名,就可以调用 Charset 的 forName() 方法来创建对应的 Charset 对象,forName() 方法参数就是相应字符集的别名:

    Charset charset = Charset.forName("GBK");
    Charset cs = Charset.forName("IOS-8859-1");
    

    获取了 Charset 对象之后,就可以通过 newDecoder()、newEncoder() 这两个方法分别返回 CharDecode 和 CharEncode 对象,代表该 Charset 的解码器和编码器。

    调用 CharDecode 的 decode() 方法可以将 ByteBuffer(字节序列) 转换成 CharBuffer(字符序列);

    调用 CharEncode 的 encode() 方法可以将 CharBuffer() 或 String(字符序列) 转换成 ByteBuffer(字符序列)。

  • 相关阅读:
    SpringBoot-14-MyBatis预热篇,MySQL小结
    SpringBoot-13-插曲之Node文件重命名+自动生成json对象
    八月十九风雨大作
    诉世书
    《仪式》
    珊瑚墓地
    新生
    《应龙》
    《枝·你是树的狂舞》
    golang中使用etcd
  • 原文地址:https://www.cnblogs.com/luler/p/15252656.html
Copyright © 2011-2022 走看看