NIO NIO与IO区别 缓冲区(Buffer)与通道(Channel) 分散与聚集 编码与解码 NIO 的非阻塞式网络通信 选择器(Selector) SocketChannel与ServerSocketChannel DatagramChannel 管道 (Pipe) NIO.2新增的类 自动资源管理

NIO
NIO与IO区别
缓冲区(Buffer)与通道(Channel)
分散与聚集
编码与解码
NIO 的非阻塞式网络通信
选择器(Selector)
SocketChannel与ServerSocketChannel
DatagramChannel
管道 (Pipe)
NIO.2新增的类
自动资源管理

Java NIO(New IO)是从Java 1.4版本就开始引入的 一个新的IO API,可以用来替代标准的Java IO API。 NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO会以更加高效的方式进行文件的读写操作。Java1.7又对NIO进行了升级,我们也可以对1.7之后的NIO称之为NIO.2。

IO NIO
面向流(Stream) 面向通道(Channel)
阻塞IO(Blocking) 非阻塞IO(Non Blocking)

缓冲区(Buffer)与通道(Channel)

Java NIO系统的核心在于:通道(Channel)和缓冲区 (Buffer)。通道表示打开到 IO 设备(例如:文件、 套接字)的连接,缓冲区用于存储数据。

缓冲区(Buffer)

在传统的IO中,我们一般创建一个byte/char数组作为缓冲区,一次读一个数组大小的数据进这个缓冲区,而不是一次只读一字节/一字符,这样的方式可以明显提升速度。在NIO中,Java为我们提供了缓冲区,我们可以直接使用。根据数据类型不同(boolean 除外) ,有以下 Buffer 常用子类:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

其中ByteBuffer使用的最多,因为所有的数据都是以字节存储的嘛

这些缓冲区的使用都是类似的,它们底层都是一个数组。创建一个这样的缓冲区使用静态方法:

static XxxBuffer allocate(int capacity); //创建一个容量为 capacity 的 XxxBuffer 对象(非直接缓冲区)

static XxxBuffer allocateDirect(int capacity); //创建一个capacity大小的直接缓冲区

Buffer 中的重要概念

  • 容量 (capacity) :表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创建后不能更改。
  • 限制 (limit):第一个不应该读取或写入的数据的索引,即位于 limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。
  • 位置 (position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制
  • 标记 (mark)与重置 (reset):标记是一个索引,通过 Buffer 中的 mark() 方法 指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这 个 position.

标记、位置、限制、容量遵守以下不变式: 0 <= mark <= position <= limit <= capacity

Buffer的数据操作

Buffer 所有子类提供了两个用于数据操作的方法:get() 与 put() 方法

放入数据到 Buffer 中

  • put(byte b):将给定单个字节写入缓冲区的当前位置
  • put(byte[] src):将 src 中的字节写入缓冲区的当前位置
  • put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动 position)

获取 Buffer 中的数据

  • get() :读取单个字节
  • get(byte[] dst):批量读取多个字节到 dst 中
  • get(int index):读取指定索引位置的字节(不会移动 position)

在使用get方法之前,必须使用flip()方法反转此缓冲区,也将是说将写模式反转为读模式,要不然是无法读的

    @Test
    public void test1(){
        String str="abcde";
        
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());
        
        buffer.put(str.getBytes());
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());
        
        
        //切换为读模式
        buffer.flip();
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());
        
        //开始读
        byte[] bs=new byte[buffer.limit()];
        buffer.get(bs);
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());
        System.out.println(new String(bs));
        
        //重置指针
        buffer.clear();
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());
        System.out.println((char)buffer.get());
    }

Buffer 的其他常用方法

方法 描述
Buffer clear() 清空缓冲区并返回对缓冲区的引用
int capacity() 返回 Buffer 的 capacity 大小
int limit() 返回 Buffer 的界限(limit) 的位置
int position() 返回缓冲区的当前位置 position
boolean hasRemaining() 判断缓冲区中是否还有元素
int remaining() 返回 position 和 limit 之间的元素个数(获取缓冲区中可以操作的数量)
Buffer mark() 对缓冲区设置标记
Buffer reset() 将位置 position 转到以前设置的 mark 所在的位置
Buffer rewind() 将位置设为为 0, 取消设置的 mark
    @Test
    public void test2(){
        //mark 标记position位置
        String str="abcde";
        
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.put(str.getBytes());
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());
        
//切换模式 buffer.flip();
byte[] bs=new byte[buffer.limit()]; buffer.get(bs, 0, 2); System.out.println(new String(bs)); System.out.println(buffer.position()); //标记 buffer.mark(); buffer.get(bs,2,2); System.out.println("-------"+new String(bs)); System.out.println(buffer.position()); //重置,回到mark标记处 buffer.reset(); System.out.println(buffer.position()); //判断缓冲区中是否还有数据 if(buffer.hasRemaining()){ //获取缓冲区中可以操作的数量 System.out.println(buffer.remaining()); } }

直接与非直接缓冲区

缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区(使用allocateDirect()),则 Java 虚拟机会尽最大努力直接在物理内存中分配空间,而非直接缓冲区是在JVM中占用空间的。因为涉及在物理内存中分配空间,所以分配和取消分配所需成本通常高于非直接缓冲区(使用allocate()),所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。

直接缓冲区还可以通过 FileChannel(通道) 的 map() 方法将文件区域直接映射到内存中来创建。该方法返回 MappedByteBuffer 。Java 平台的实现有助于通过 JNI (Java native interface)从本机代码创建直接字节缓冲区。

可以使用isDirect()方法判断是否是直接缓冲区

    @Test
    public void test3(){
        //直接缓冲区
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
        System.out.println(buffer.isDirect());
        
        //非直接缓冲区
        ByteBuffer buffer2 = ByteBuffer.allocate(1024);
        System.out.println(buffer2.isDirect());
    }

通道(Channel)

通道(Channel):由 java.nio.channels 包定义的。Channel 表示 IO 源与目标打开的连接, Channel 类似于传统的“流”。

Java 为 Channel 接口提供的最主要实现类如下:

  • FileChannel:用于读取、写入、映射和操作文件的通道。
  • DatagramChannel:通过 UDP 读写网络中的数据通道。
  • SocketChannel:通过 TCP 读写网络中的数据。
  • ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。

FileChannel主要用于本地文件的,而后三种用于网络。

获取通道的一种方式是对支持通道的对象调用 getChannel() 方法。支持通道的类如下:

  • FileInputStream
  • FileOutputStream
  • RandomAccessFile
  • DatagramSocket
  • Socket
  • ServerSocket

可以看到可以使用Stream流来创建通道,这应该算是一种对传统IO的兼容与支持。

在Java1.7之后可以这两种方式创建通道:

  • 使用 Files 类的静态方法 newByteChannel() 获取字节通道。
  • 使用通道的静态方法 open() 打开并返回指定通道。

我们先来个使用传统流来获得通道,并复制一个文件的操作:

//使用非直接缓冲区
    @Test
    public void test1(){
        //通过IO的流获得通道
        FileInputStream fin=null;
        FileOutputStream fout=null;
        
        //通道
        FileChannel inChannel=null;
        FileChannel outChannel=null;
        try {
            fin=new FileInputStream("1.jpg");
            fout=new FileOutputStream("2.jpg");
            //输入通道
            inChannel = fin.getChannel();
            //输出通道
            outChannel = fout.getChannel();
            
            //缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            
            while(inChannel.read(buffer)!=-1){
                //切换模式
                buffer.flip();
                outChannel.write(buffer);
                //清空缓冲区
                buffer.clear();
            }
            
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (inChannel!=null) {
                try {
                    inChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(outChannel!=null){
                try {
                    outChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(fin!=null){
                try {
                    fin.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(fout!=null){
                try {
                    fout.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

就这里而言的代码量同传统IO没有什么太大差别。现在使用NIO中的transferTo(transferFrom)方法直接进行通道之间传输数据,体会NIO的方便之处:

    @Test
    public void test2(){
        try {
            long start = System.currentTimeMillis();
            FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
            FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
            
            inChannel.transferTo(0, inChannel.size(), outChannel);
            
            inChannel.close();
            outChannel.close();
            
            long end = System.currentTimeMillis();
            System.out.println("所用时间:"+(end-start));
        } catch (IOException e) {
            e.printStackTrace();
        }
        
    }

这里我打印了运行所用的时间,测试之后明显NIO的transferTo(transferFrom)方式快于第一种方式。

注意:open()方法也是Java1.7之后新增的方法;Paths这个工具类用于指定路径,也是Java1.7后添加的类。使用open方法需要指定 StandardOpenOption 来说明通道的类型,这是一个枚举类,由read,write,create(没有则创建,有则覆盖),create_new(没有则创建,有则报错)等等模式。

之前说了可以使用通道的map方法来创建直接缓冲区,返回的是MappedByteBuffer,这个类继承于ByteBuffer,我们来看看如何操作:

    //使用直接缓冲区(内存映射文件)open是1.7之后的方法
    @Test
    public void test3(){
        FileChannel inChannel=null;
        FileChannel outChannel=null;
        try {
            long start = System.currentTimeMillis();
            //通道
            inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
            outChannel=FileChannel.open(Paths.get("3.jpg"),StandardOpenOption.READ,StandardOpenOption.WRITE, StandardOpenOption.CREATE);
            
            //内存映射文件
            MappedByteBuffer inMap = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
            MappedByteBuffer outMap = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
            
            //对内存映射文件操作
            byte[] bs=new byte[inMap.limit()];
            inMap.get(bs);
            outMap.put(bs);
            
            long end = System.currentTimeMillis();
            System.out.println("所用时间:"+(end-start));
            
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if(inChannel!=null){
                try {
                    inChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(outChannel!=null){
                try {
                    outChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

注意:调用map()方法也需要指定mode(具体来说是MapMode),因为MappedByteBuffer是根据通道来获得,所以这两个的模式需要一致,不然也会报异常。

分散与聚集

分散:是指从 Channel 中读取的数据“分散”到多个 Buffer 中。通道的read方法可以接受一个Buffer数组,所以可以将数据分散到这个缓冲区数组中,因为数组是有顺序的,所以会按照缓冲区的顺序,依次将 Buffer 填满。

       //建立多个缓冲区,用于分散读取
            ByteBuffer buf1=ByteBuffer.allocate(100);
            ByteBuffer buf2=ByteBuffer.allocate(1024);
            
            ByteBuffer[] dsts={buf1,buf2};
            channel.read(dsts);

聚集:是指将多个 Buffer 中的数据“聚集” 到 Channel。也是按照缓冲区的顺序,写入 position 和 limit 之间的数据到 Channel 。

            //使用一下RandomAccessFile
            accessFile=new RandomAccessFile("1.txt", "rw");
            
            //建立通道
            channel = accessFile.getChannel();
            
            //建立多个缓冲区,用于分散读取
            ByteBuffer buf1=ByteBuffer.allocate(100);
            ByteBuffer buf2=ByteBuffer.allocate(1024);
            
            ByteBuffer[] dsts={buf1,buf2};
            channel.read(dsts);
            
            //转换filp
            for (ByteBuffer byteBuffer : dsts) {
                byteBuffer.flip();
            }
            
            System.out.println(new String(buf1.array(),0,buf1.limit()));
            System.out.println("--------------------------------");
            System.out.println(new String(buf2.array(),0,buf2.limit()));
            
            //聚集写入
            accessFile2=new RandomAccessFile("2.txt", "rw");
            channel2 = accessFile2.getChannel();
            channel2.write(dsts);

这里使用了RandomAccessFile来获得管道,没有特殊的意思,只是说明RandomAccessFile与FileInputStram,FileOutputStream一样都是可以通过传统流的方式获得管道

编码与解码

将文本从字符串转为字节数组时,可以进行编码,再从字节数组读出来转为字符的时候也可以进行解码,但是一定要使用相同的编码进行编解码,不然就会乱码。当然页不是说编解码是一定要进行的,因为不指定的情况下,会使用平台默认的编码格式进行编解码。

Charset表示编码方式,我们也可以使用它的forName方法来指定编码方式。

CharsetEncoder:编码器,使用字符集(charset)的newEncoder()方法获得。操作它的encode()方法可以完成编码

CharsetDecoder :解码器,使用字符集(charset)的newDecoder()方法获得,操作它的decode()方法可以完成解码

//编码与解码
    @Test
    public void test05() throws Exception{
        Charset charset = Charset.forName("GBK");
        
        //得到编码器与解码器
        CharsetEncoder encoder = charset.newEncoder();
        CharsetDecoder decoder = charset.newDecoder();
        
        String string="lz666";
        
        //字符通道
        CharBuffer cBuf = CharBuffer.allocate(1024);
        cBuf.put(string);
        cBuf.flip();
        
        //编码
        ByteBuffer BBuf1 = encoder.encode(cBuf);
        
        //切换读模式
        for (int i = 0; i < 7; i++) {
            System.out.println(BBuf1.get());
        }
        
        //解码
        BBuf1.flip();
        CharBuffer CBuf2 = decoder.decode(BBuf1);
        System.out.println(CBuf2.toString());
    }

NIO 的非阻塞式网络通信

传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端请求都提供一个独立的线程进行处理, 当服务器端需要处理大量客户端时,性能急剧下降。

Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。

选择器(Selector)

Selector 可同时监控多个 SelectableChannel 的 IO 状况,利用 Selector 可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心。

创建 Selector :通过调用 Selector.open() 方法创建一个 Selector。

//获得选择器
Selector selector = Selector.open();

向选择器注册通道:使用非阻塞的NIO,需要将通道在selector选择器中进行注册。使用的是SelectableChannel.register(Selector sel, int ops)

//将通道注册到选择器上,并指定"监听接收事件"
ssChannel.register(selector, SelectionKey.OP_ACCEPT);

选择器对通道的监听事件,需要通过第二个参数 ops 指定,可选值(用 SelectionKey 的四个常量)由:

  • 读 : SelectionKey.OP_READ
  • 写 : SelectionKey.OP_WRITE
  • 连接 : SelectionKey.OP_CONNECT
  • 接收 : SelectionKey.OP_ACCEPT

若注册时不止监听一个事件,则可以使用“按位或”操作符连接

int interestSet=SelectionKey.OP_WRITE|SelectionKey.OP_READ;

SelectionKey:表示 SelectableChannel 和 Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。它常用的API:

方法 描述
SelectableChannel channel() 获取注册通道
boolean isReadable() 检测 Channal 中读事件是否就绪
boolean isWritable() 检测 Channal 中写事件是否就绪
boolean isConnectable() 检测 Channel 中连接是否就绪
boolean isAcceptable() 检测 Channel 中接收是否就绪

Selector 的常用API有:

方法 描述
selectedKeys() 被选择的 SelectionKey 集合。返回此Selector的已选择键集
int select() 监控所有注册的Channel,当它们中间有需要处理的 IO 操作时, 该方法返回大于0的值
int select(long timeout) 可以设置超时时长的 select() 操作
int selectNow() 执行一个立即返回的 select() 操作,该方法不会阻塞线程
void close() 关闭该选择器

SocketChannel与ServerSocketChannel

在本地我们一直使用的是FileChannel,但是在网络环境下,(TCP)需要使用SocketChannel和ServerSocketChannel。

Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。

Java NIO中的 ServerSocketChannel 是一个可以 监听新进来的TCP连接的通道,就像标准IO中的ServerSocket一样。

它们获得通道是使用open方法:

//建立SocketChannel
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));

//获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();

这里贴一个使用SocketChannel与ServerSocketChannel实现聊天室的功能:

package cn.lynu.nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Iterator;
import java.util.Scanner;

import org.junit.Test;


//使用非阻塞NIO(模拟聊天室)
//先运行server,再运行client
public class TestNonBlockingNIO {

    @Test
    public void client() throws Exception{
        //1.建立SocketChannel
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        //2.切换非阻塞方式
        socketChannel.configureBlocking(false);
        //3.分配指定大小的缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //4.发送数据给服务器端
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String string=scanner.next();
            byteBuffer.put((new Date().toString()+"	"+string).getBytes());
            byteBuffer.flip();
            socketChannel.write(byteBuffer);
            byteBuffer.clear();
        }
        
        //5.关闭通道
        socketChannel.close();
    }
    
    @Test
    public void server()throws Exception{
        //1.获取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        //2.切换非阻塞模式
        ssChannel.configureBlocking(false);
        //3.绑定端口号
        ssChannel.bind(new InetSocketAddress(9898));
        //4.获得选择器
        Selector selector = Selector.open();
        //5.将通道注册到选择器上,并指定"监听接收事件"
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);
        
        //6.轮询式的获得选择器上已经"就绪"的事件
        while(selector.select()>0){
            
            //7.获得所有已注册在选择器的事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while(iterator.hasNext()){
                //8.获取就绪事件
                SelectionKey selectionKey = iterator.next();
                //9.判断具体是什么事件就绪,如果是"接受就绪"则获得客户端连接
                if(selectionKey.isAcceptable()){
                    SocketChannel socketChannel = ssChannel.accept();
                    //10.切换非阻塞
                    socketChannel.configureBlocking(false);
                    //11.将通道注册到选择器
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }else if(selectionKey.isReadable()){
                    //否则是"读就绪",获得读就绪通道
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    //12.缓冲区读取数据
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    int len=0;
                    //read可以返回0,表示是空格,返回-1表示已到达流的末尾
                    while((len=socketChannel.read(byteBuffer))>0){
                        byteBuffer.flip();
                        System.out.println(new String(byteBuffer.array(),0,len));
                        byteBuffer.clear();
                    }
                    
                }
                //13.取消选择
                iterator.remove();
            }
        }
        
    }
}

DatagramChannel

Java NIO中的DatagramChannel是一个能收发 UDP包的通道。也是使用open方法打开。

DatagramChannel datagramChannel = DatagramChannel.open();

贴一个使用DatagramChannel完成聊天室的功能,其实与使用使用SocketChannel与ServerSocketChannel的方式一致:

package cn.lynu.nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Date;
import java.util.Iterator;
import java.util.Scanner;

import org.junit.Test;

//使用UDP方式实现聊天室
//先开接收端,再开发送端
public class TestNonBlockingNIO2 {
    
    @Test
    public void send() throws Exception{
        DatagramChannel datagramChannel = DatagramChannel.open();
        datagramChannel.configureBlocking(false);
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNext()){
            String text = scanner.next();
            byteBuffer.put((new Date().toString()+"	"+text).getBytes());
            byteBuffer.flip();
            //发送
            datagramChannel.send(byteBuffer, new InetSocketAddress("127.0.0.1", 9898));
            byteBuffer.clear();
        }
        datagramChannel.close();
    }
    
    @Test
    public void receive() throws Exception{
        DatagramChannel datagramChannel = DatagramChannel.open();
        datagramChannel.configureBlocking(false);
        datagramChannel.bind(new InetSocketAddress(9898));
        Selector selector = Selector.open();
        datagramChannel.register(selector, SelectionKey.OP_READ);
        while(selector.select()>0){
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while(iterator.hasNext()){
                SelectionKey key = iterator.next();
                if(key.isReadable()){
                    ByteBuffer buffer=ByteBuffer.allocate(1024);
                    //接收
                    datagramChannel.receive(buffer);
                    buffer.flip();
                    System.out.println(new String(buffer.array(),0,buffer.limit()));
                    buffer.clear();
                }
                iterator.remove();
            }
        }
    }

}

管道 (Pipe)

Java NIO 管道是2个线程之间的单向数据连接,而通道Channel则是双向的数据连接。 Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。

获得管道使用的是Pipe的静态方法:

//获得管道
Pipe pipe = Pipe.open();

向管道写数据,需要向 sink  通道中写

         //1.获得管道
        Pipe pipe = Pipe.open();
        
        //2.将缓冲区中的数据写入管道
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        
        SinkChannel sink = pipe.sink();
        byteBuffer.put("使用管道传输数据".getBytes());
        byteBuffer.flip();
         
         while(byteBuffer.hasRemaining()){
             sink.write(byteBuffer);
          }                    

从读取管道的数据,需要访问source通道。

//访问source通道
SourceChannel source = pipe.source();

调用source通道的read()方法来读取数据

byteBuffer.flip();
int len=source.read(byteBuffer);
System.out.println(new String(byteBuffer.array(),0,len));

最后,关闭这两个通道

//关闭管道
sink.close();
source.close();

NIO.2新增的类

Path 与 Paths

java.nio.file.Path 接口代表一个平台无关的平台路径,描述了目录结构中文件的位置。Paths 工具类提供的 get() 方法用来获取 Path 对象。

Path get(String first, String … more) : 用于将多个字符串串连成路径。

Path 常用API:

boolean endsWith(String path) : 判断是否以 path 路径结束
boolean startsWith(String path) : 判断是否以 path 路径开始
boolean isAbsolute() : 判断是否是绝对路径
Path getFileName() : 返回与调用 Path 对象关联的文件名
Path getName(int idx) : 返回的指定索引位置 idx 的路径名称
int getNameCount() : 返回Path 根目录后面元素的数量
Path getParent() :返回Path对象包含整个路径,不包含 Path 对象指定的文件路径
Path getRoot() :返回调用 Path 对象的根路径
Path resolve(Path p) :将相对路径解析为绝对路径
Path toAbsolutePath() : 作为绝对路径返回调用 Path 对象
String toString() : 返回调用 Path 对象的字符串表示形式
    @Test
    public void test1(){
        Path path = Paths.get("e:/", "nio/hello.txt");
        
        System.out.println(path.endsWith("hello.txt"));
        System.out.println(path.startsWith("e:/"));
        
        System.out.println(path.isAbsolute());
        System.out.println(path.getFileName());
        
        for (int i = 0; i < path.getNameCount(); i++) {
            System.out.println(path.getName(i));
        }
    }
    
    @Test
    public void test2(){
        Path path = Paths.get("e:/nio/hello.txt");
        
        System.out.println(path.getParent());
        System.out.println(path.getRoot());
        
        Path path2 = Paths.get("1.jpg");
        Path newPath = path2.toAbsolutePath();
        System.out.println(newPath);
        
        System.out.println(path.toString());
    }

Files 类

java.nio.file.Files 用于操作文件或目录的工具类。Files类常用API:

Path copy(Path src, Path dest, CopyOption … how) : 文件的复制
Path createDirectory(Path path, FileAttribute<?> … attr) : 创建一个目录
Path createFile(Path path, FileAttribute<?> … arr) : 创建一个文件
void delete(Path path) : 删除一个文件
Path move(Path src, Path dest, CopyOption…how) : 将 src 移动到 dest 位置
long size(Path path) : 返回 path 指定文件的大小
boolean exists(Path path, LinkOption … opts) : 判断文件是否存在
boolean isDirectory(Path path, LinkOption … opts) : 判断是否是目录
boolean isExecutable(Path path) : 判断是否是可执行文件
boolean isHidden(Path path) : 判断是否是隐藏文件
boolean isReadable(Path path) : 判断文件是否可读
boolean isWritable(Path path) : 判断文件是否可写
boolean notExists(Path path, LinkOption … opts) : 判断文件是否不存在
public static <A extends BasicFileAttributes> A readAttributes(Path path,Class<A> type,LinkOption...
options) : 获取与 path 指定的文件相关联的属性。
Files常用方法:用于操作内容
SeekableByteChannel newByteChannel(Path path, OpenOption…how) : 获取与指定文件的连接,
how 指定打开方式。
DirectoryStream newDirectoryStream(Path path) : 打开 path 指定的目录
InputStream newInputStream(Path path, OpenOption…how):获取 InputStream 对象
OutputStream newOutputStream(Path path, OpenOption…how) : 获取 OutputStream 对象@Test
    @Test
    public void test1() throws IOException{
        Path dir = Paths.get("e:/nio/nio2");
        Path file = Paths.get("e:/nio/nio2/hello3.txt");
        Files.deleteIfExists(file);
    }
    
    @Test
    public void test2() throws IOException{
        Path path1 = Paths.get("e:/nio/hello.txt");
        Path path2 = Paths.get("e:/nio/hello2.txt");
        
        Files.copy(path1, path2, StandardCopyOption.REPLACE_EXISTING);
    }

copy方法的第二个参数用于表明拷贝的模式,StandardCopyOption枚举类可选值REPLACE_EXISTING(存在则覆盖),COPY_ATTRIBUTES(将拷贝文件作为一个新文件),ATOMIC_MOVE(将移动作为一个原子操作)

自动资源管理

Java 7 增加了一个新特性,该特性提供了另外 一种管理资源的方式,这种方式能自动关闭文件。这个特性有时被称为自动资源管理, 该特性以 try 语句的为基础进行拓展。自动资源管理主要用于,当不再需要文件(或其他资源)时, 可以防止无意中忘记释放它们(例如我们可能会忘记在finally中关闭资源,或者是在finally块中过多的关闭语句)。但是需要关闭的资源,必须实现了 AutoCloseable 接口或其子接口 Closeable(这个接口中只有一个close方法),Channel接口继承了Closeable,所以其实现类都是可以使用的自动资源管理,那么传统的字节流,字符流也可以吗?在1.7及其之后的版本中,reader/writer,inputStream/outputStream也继承了Closeable接口,所以传统的IO也是可以使用的。

自动资源管理基于 try 语句的扩展形式:

try(需要关闭的资源声明){
//可能发生异常的语句
}catch(异常类型 变量名){
//异常的处理语句
}
……
finally{
//不再需要显示调用各种close
}

可以在一条 try 语句中管理多个资源,每个资源以“;” 隔开即可。

//自动资源管理:自动关闭实现 AutoCloseable 接口的资源
    @Test
    public void test1(){
        try(FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
            FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE, StandardOpenOption.CREATE)){
            
            ByteBuffer buf = ByteBuffer.allocate(1024);
            inChannel.read(buf);
            
        }catch(IOException e){
            
        }
    }