1. 程式人生 > >Java入門系列-23-NIO(使用緩衝區和通道對檔案操作)

Java入門系列-23-NIO(使用緩衝區和通道對檔案操作)

NIO 是什麼

java.nio全稱java non-blocking(非阻塞) IO(實際上是 new io),是指jdk1.4 及以上版本里提供的新api(New IO) ,為所有的原始型別(boolean型別除外)提供快取支援的資料容器,使用它可以提供非阻塞式的高伸縮性網路。

NIO與IO的區別

IO NIO
面向流(Stream Oriented) 面向緩衝區(Buffer Oriented)
阻塞IO(Blocking IO) 非阻塞(Non Blocking IO)
選擇器(Selectors)

NIO系統的核心是:通道(Channel)和緩衝區(Buffer)

緩衝區(Buffer)

位於 java.nio 包,所有緩衝區都是 Buffer 抽象類的子類,使用陣列對資料進行緩衝。

除了 boolean 型別,Buffer 對每種基本資料型別都有針對的實現類:

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

建立緩衝區通過 xxxBuffer.allocate(int capacity)方法

ByteBuffer buf1 = ByteBuffer.allocate(512);
LongBuffer buf2 = LongBuffer.allocate(1024);
……

緩衝區的屬性

容量(capacity):表示緩衝區儲存資料的最大容量,不能為負數,建立後不可修改。

限制:第一個不可以讀取或寫入的資料的索引,即位於 limit 後的資料不能讀寫。不能為負數,不能大於容量。

位置(position):下一個要讀取或寫入的資料的索引,位置不能為負數,不能大於 limit

標記(mark):標記是一個索引,通過 Buffer 中的 mark() 方法指 Buffer 中一個特定的 position,之後可以通過 reset() 方法回到這個 postion。

Buffer 的常用方法

方法名稱 說明
Buffer clear() 清空緩衝區並返回對緩衝區的引用
Buffer flip() 將緩衝區的 limit 設定為當前位置,並將當前位置重置為0
int capacity() 返回 Buffer 的容量大小
boolean hasRemaining() 判斷緩衝區是否還有元素
int limit() 返回 限制的位置
Buffer limit(int n) 將設定緩衝區界限為 n,並返回一個具有新 limit 的緩衝區物件
Buffer mark() 對緩衝區設定標記
int position() 返回緩衝區的當前位置 position
Buffer position(int n) 將設定緩衝區的當前位置為 n,並返回修改後的 Buffer 物件
int remaining() 返回 position 和 limit 之間的元素個數
Buffer reset() 將位置 position 轉到以前設定的 mark 所在的位置
Buffer rewind() 將位置設定為 0,取消設定的 mark

Buffer 所有子類提供了兩個操作的資料的方法:get() 方法和 put() 方法

緩衝區存取資料操作

package testnio;

import java.nio.ByteBuffer;

public class TestBuffer1 {

    public static void main(String[] args) {
        testuse();
    }

    public static void testuse() {
        //1.分配一個指定大小的緩衝區
        ByteBuffer buf=ByteBuffer.allocate(1024);

        System.out.println("---------------allocate()----------------");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());

        //2.利用 put() 存入資料到緩衝區中
        String str="hello";
        //將字串轉為 byte 陣列存入緩衝區
        buf.put(str.getBytes());
        System.out.println("---------------put()----------------");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());

        //3.切換讀取資料模式
        buf.flip();
        System.out.println("---------------flip()----------------");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());

        //4.利用get() 讀取緩衝區中的資料
        byte[] data=new byte[buf.limit()];
        System.out.println("---------------get()----------------");
        buf.get(data);
        System.out.println(new String(data,0,data.length));
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());

        //5.rewind() 重複讀
        buf.rewind();
        System.out.println("---------------rewind()----------------");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());

        //6.clear() 清空緩衝區,但緩衝區中的資料依然存在
        buf.clear();
        System.out.println("---------------clear()----------------");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());
        System.out.println((char)buf.get());
    }
}

使用 mark()方法標記

package testnio;

import java.nio.ByteBuffer;

public class TestBuffer2 {

    public static void main(String[] args) {
        testmark();
    }

    public static void testmark() {
        String str="jikedaquan.com";
        //建立緩衝區
        ByteBuffer buf=ByteBuffer.allocate(1024);
        //存入資料
        buf.put(str.getBytes());
        //切換模式
        buf.flip();
        //臨時陣列用於接收緩衝區獲取的資料,長度與緩衝區 limit 一值
        byte[] data=new byte[buf.limit()];
        //獲取緩衝區的資料從0開始獲取4個,存入 data 陣列中
        buf.get(data, 0, 4);
        //將陣列轉為字串列印
        System.out.println(new String(data,0,4));
        //列印 position
        System.out.println(buf.position());

        //標記
        buf.mark();
        System.out.println("---------------再次獲取----------------");
        //從索引4開始,獲取6個位元組(餘下資料)
        buf.get(data, 4, 6);
        System.out.println(new String(data,4,6));
        System.out.println(buf.position());

        //恢復到標記位置
        buf.reset();
        System.out.println("---------------reset()----------------");
        System.out.println(buf.position());

        //判斷緩衝區是是有還有剩餘資料
        if (buf.hasRemaining()) {
            //獲取緩衝區中可以操作的數量
            System.out.println("可運算元量:"+buf.remaining());
        }
    }
}

mark <= position <= limit <= capacity

雖然使用了緩衝區提高了一定的IO速度,但這樣的效率仍然不是最高的。非直接緩衝區在與物理磁碟操作中需要經過核心地址空間copy操作,直接緩衝區不經過copy操作,直接操作實體記憶體對映檔案, 使用直接緩衝區將大大提高效率。

直接緩衝區進行分配和取消分配所需成本工廠高於非直接緩衝區,一般情況下,最好僅在直接緩衝區能在程式效能方面帶來明顯好處時分配它們。

直接緩衝區可以通過呼叫此類的 allocateDirect()工廠方法建立

建立直接緩衝區

package testnio;

import java.nio.ByteBuffer;

public class TestBuffer3 {

    public static void main(String[] args) {
        testAllocateDirect();
    }

    public static void testAllocateDirect() {
        //建立直接緩衝區
        ByteBuffer buf=ByteBuffer.allocateDirect(1024);
        
        //是否是直接緩衝區
        System.out.println(buf.isDirect());
    }
}

通道(Channel)

緩衝區僅是運載資料的容器,需要對資料讀寫還需要有一條通道,這兩者是密不可分的。

Channel 介面的主要實現類:

  • FileChannel:用於讀取、寫入、對映和操作檔案的通道
  • DatagramChannel:通過 UDP 讀寫網路中的資料通道
  • ScoketChannel:通過 TCP 讀寫網路中的資料
  • ServerScoketChannel:可以監聽新進來的 TCP 連結,對每一個新進來的連線都會建立一個 SocketChannel

如何獲取通道?

1、通過支援通道的物件呼叫 getChannel() 方法

支援通道的類:

  • FileInputStream
  • FileOutputStream
  • RandomAccessFile
  • DatagramScoket
  • Socket
  • ServerScoket

使用通道和緩衝區實現檔案讀和寫

package testnio;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class TestChannel {

    public static void main(String[] args) {
        FileInputStream fis=null;
        FileOutputStream fos=null;
        
        FileChannel inChannel=null;
        FileChannel outChannel=null;
        
        try {
            //建立輸入流
            fis=new FileInputStream("F:/1.jpg");
            //建立輸出流
            fos=new FileOutputStream("F:/2.jpg");
            //獲取通道
            inChannel=fis.getChannel();
            outChannel=fos.getChannel();
            
            //分配指定大小的緩衝區
            ByteBuffer buf=ByteBuffer.allocate(1024);
            
            //將通道中的資料存入快取區
            while(inChannel.read(buf)!=-1) {
                //切換讀取資料的模式
                buf.flip();
                //將讀入的緩衝區存入寫資料的管道
                outChannel.write(buf);
                //清空快取區(清空才能再次讀入)
                buf.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (fis!=null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fos!=null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(inChannel!=null) {
                try {
                    inChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (outChannel!=null) {
                try {
                    outChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
}

2、通過通道類的靜態方法 open()

package testnio;

import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class TestOpenAndMapped {

    public static void main(String[] args) {
        FileChannel inChannel=null;
        FileChannel outChannel=null;
        try {
            //通過open建立通道
            inChannel = FileChannel.open(Paths.get("F:/a.jpg"), StandardOpenOption.READ);
            outChannel = FileChannel.open(Paths.get("F:/b.jpg"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);

            //記憶體對映檔案   直接緩衝區
            MappedByteBuffer inMappedBuf=inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
            MappedByteBuffer outMappedBuf=outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());

            //直接對緩衝區進行資料的讀寫操作
            byte[] data=new byte[inMappedBuf.limit()];
            inMappedBuf.get(data);//讀
            outMappedBuf.put(data);//寫
        } catch (IOException 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();
                }
            }
        }
    }
}

JDK 1.7 新增的方法 open(),引數 path 通常代表一個依賴系統的檔案路徑,通過Paths.get()獲取。

引數 StandardOpenOption 是一個列舉型別,常用值如下:

  • READ :開啟讀訪問
  • WRITE:開啟寫訪問
  • APPEND:向後追加
  • CREATE:建立新檔案,存在則覆蓋
  • CREATE_NEW:建立新檔案,存在則報錯

MappedByteBuffer:直接位元組緩衝區,其內容是檔案的記憶體對映區域。

通道資料傳輸

將資料從源通道傳輸到其他 Channel 中,transferTo() 和 transferFrom()

package testnio;

import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class TestChannelTransfer {

    public static void main(String[] args) {
        FileChannel inChannel=null;
        FileChannel outChannel=null;
        try {
            //通過open建立管道
            inChannel = FileChannel.open(Paths.get("F:/a.jpg"), StandardOpenOption.READ);
            outChannel = FileChannel.open(Paths.get("F:/b.jpg"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
            //將inChannel中所有資料傳送到outChannel
            //inChannel.transferTo(0, inChannel.size(), outChannel);
            outChannel.transferFrom(inChannel, 0, inChannel.size());
        } catch (IOException 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();
                }
            }
        }
    }
}

Channel 負責傳輸,Buffer 負責儲存