事情的開始
1.4版本開始,java提供了另一套IO系統,稱為NIO,(New I/O的意思),NIO支援面向緩衝區的、基於通道的IO操作。
1.7版本的時候,java對NIO系統進行了極大的擴充套件,增強了對檔案處理和檔案系統特性的支援。
在不斷的進化迭代之中,IO的很多應用場景應該推薦使用NIO來取代。
NIO系統構建於兩個基礎術語之上:緩衝區和通道。
緩衝區
Buffer類
緩衝區是一個固定資料量的指定基本型別的資料容器,可以將它理解成一塊記憶體,java將它封裝成了Buffer類。
每個非布林基本資料型別都有各自對應的緩衝區操作類,所有緩衝區操作類都是Buffer類的子類。
除了儲存的內容之外,所有的緩衝區都具有通用的核心功能:當前位置、界限、容量。
當前位置是要讀寫的下一個元素的索引
界限是緩衝區中最後一個有效位置之後下一個位置的索引值
容量是緩衝區能夠容納的元素的數量,一般來說界限等於容量。
對於標記、位置、限制和容量值遵守以下不變式:0 <= 標記 <= 位置 <= 限制 <= 容量
方法列表:
方法 | 描述 |
Object array() | 返回此緩衝區的底層實現陣列 |
int arrayOffset() | 返回此緩衝區的底層實現陣列中第一個元素的索引 |
int capacity() | 返回此緩衝區的容量 |
Buffer clear() | 清除此緩衝區並返回緩衝區的引用 |
Buffer flip() | 將緩衝區的界限設定為當前位置,並將當前位置重置為0,即反轉緩衝區 |
boolean hasArray() | 返回緩衝區是否具有可訪問的底層實現陣列。 |
boolean hasRemaining() | 返回緩衝區中是否還有剩餘元素 |
boolean isDirect() | 返回此緩衝區是否是直接緩衝區(直接緩衝區可以直接對緩衝區進行IO) |
boolean isReadOnly() | 該緩衝區是否只讀 |
int limit() | 返回緩衝區的界限 |
Buffer limit(int n) | 將緩衝區的界限設定為n |
Buffer mark() | 設定標記 |
int position() | 返回此緩衝區的位置 |
Buffer position(int n) | 將緩衝區的當前位置設定為n |
int remaining() | 返回當前位置與界限之間的元素數量(即界限減去當前位置的結果值) |
Buffer reset() | 將緩衝區的位置重置為之前設定標記的位置 |
Buffer rewind() | 將緩衝區的位置設定為0 |
清除、反轉、和重繞
這三個詞是在查閱JDK文件看到的,對應Buffer類的三個方法,個人覺得非常有助於理解。
clear()使緩衝區為一系列新的通道讀取或相對放置 操作做好準備:它將限制設定為容量大小,將位置設定為 0。
flip()使緩衝區為一系列新的通道寫入或相對獲取 操作做好準備:它將限制設定為當前位置,然後將位置設定為 0。
rewind()使緩衝區為重新讀取已包含的資料做好準備:它使限制保持不變,將位置設定為 0。
資料傳輸
下面這些特定的緩衝區類派生字Buffer,這些類的名稱暗含了他們所能容納的資料型別:
ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、MappedByteBuffer、ShortBuffer
其中 MappedByteBuffer是ByteBuffer的子類,用於將檔案對映到緩衝區。
所有的緩衝區類都定義的有get()和put()方法,用於存取資料。(當然,如果緩衝區是隻讀的,就不能使用put操作)
通道
通道的用處
通道,表示到實體,如硬體裝置、檔案、網路套接字或可以執行一個或多個不同 I/O 操作(如讀取或寫入)的程式元件的開放的連線,用於 I/O 操作的連線。
通過通道,可以讀取和寫入資料。拿 NIO與原來的I/O 做個比較,通道就像是流,但它是面向緩衝區的。
正如前面提到的,所有資料都通過 Buffer 物件來處理。你永遠不會將位元組直接寫入通道中,相反,您是將資料寫入包含一個或者多個位元組的緩衝區。同樣,您不會直接從通道中讀取位元組,而是將資料從通道讀入緩衝區,再從緩衝區獲取這個位元組。
通道與流的不同之處在於通道是雙向的。而流只是在一個方向上移動(一個流必須是 InputStream 或者 OutputStream 的子類), 而 通道 可以用於讀、寫或者同時用於讀寫。
通道實現了Channel介面並且擴充套件了Closeable介面和AutoCloseable介面,通過實現AutoCloseable介面,就可以使用帶資源的try語句管理通道,那麼當通道不再需要時會自動關閉。
獲取通道
獲取通道的一種方式是對支援通道的物件呼叫getChannel()方法。
例如,以下IO類支援getChannel()方法:
DatagramSocket、FileInputStream、FileOutputStream、RandomAccessFile、ServerSocket、Socket
根據呼叫getChannel()方法的物件型別返回特定型別的通道,比如對FileInputStream、FileOutputStream或RandomAccessFile物件呼叫getChannel()方法時,會返回FileChannel型別的通道,對Socket物件呼叫getChannel()方法時,會返回SocketChannel型別的通道。
通道都支援各種read()和write()方法,使用這些方法可以通過通道執行IO操作。
方法如下:
方法 | 描述 |
int read(ByteBuffer b) | 將位元組讀取到緩衝區,返回實際讀取的位元組數 |
int read(ByteBuffer b,long start) | 從start指定的檔案位置開始,從通道讀取位元組,並寫入緩衝區 |
int write(ByteBuffer b) | 將位元組從緩衝區寫入通道 |
int write(ByteBuffer b,long start) | 從start指定的檔案位置開始,將位元組從緩衝區寫入通道 |
字符集和選擇器
NIO使用的另外兩個實體是字符集和選擇器。
字符集定義了將位元組對映為字元的方法,可以使用編碼器將一系列字元編碼成位元組,使用解碼器將一系列位元組解碼成字元。
字符集、編碼器和解碼器由java.nio.charset包中定義的類支援,因為提供了預設的編碼器和解碼器,所以通常不需要顯式的使用字符集進行工作。
選擇器支援基於鍵的,非鎖定的多通道IO,也就是說,它可以通過多個通道執行IO,當然,前提是通道需要呼叫register方法註冊到選擇器中,
選擇器的應用場景在基於套接字的通道。
Path介面
Path是JDK1.7新增進來的介面,該介面封裝了檔案的路徑。
因為Path是介面,不是類,所以不能通過建構函式直接建立Path例項,通常會呼叫Paths.get()工廠方法來獲取Path例項。
get()方法有兩種形式:
Path get(String pathname,String ...more)
Path get(URI uri)
建立連結到檔案的Path物件不會導致開啟或建立檔案,理解這一點很重要,這僅僅只是建立了封裝檔案目錄路徑的物件而已。
以下程式碼示例常用用法(1.txt是一個不存在的檔案):
Path path = Paths.get("./nio/src/1.txt"); System.out.println("自身路徑:"+path.toString());//輸出.\nio\src\1.txt System.out.println("檔案或目錄名稱:"+path.getFileName());//輸出1.txt System.out.println("路徑元素數量:"+path.getNameCount());//輸出4 System.out.println("路徑中第3截:"+path.getName(2));//輸出src System.out.println("父目錄的路徑"+path.getParent());//輸出.\nio\src System.out.println(path.getRoot());//輸出null System.out.println("是否絕對路徑:"+path.isAbsolute());//輸出false Path p = path.toAbsolutePath();//返回與該路徑等價的絕對路徑 System.out.println("看看我這個是不是絕對路徑:"+p.toString());//輸出E:\JAVA\java_learning\.\nio\src\1.txt File file = path.toFile();//從該路徑建立一個File物件 System.out.println("檔案是否存在:"+file.exists());//false Path path1 = file.toPath();//再把File物件轉成Path物件 System.out.println("是不是同一個物件:"+path1.equals(path));//輸出true
為基於通道的IO使用NIO
通過通道讀取檔案
手動分配緩衝區
這是最常用的方式,手動分配一個緩衝區,然後執行顯式的讀取操作,讀取操作使用來自檔案的資料載入緩衝區。
try(FileChannel seekableByteChannel = (FileChannel) Files.newByteChannel(Paths.get("./nio/src/2.txt"))){ ByteBuffer buffer = ByteBuffer.allocate(5);//指定緩衝區大小 int count = seekableByteChannel.read(buffer);//將檔案中的資料讀取到緩衝區 buffer.rewind(); while (count > 0){ System.out.println((char)buffer.get());//讀取緩衝區中的資料 count --; } }catch (Exception e){ e.printStackTrace(); }
該示例使用了SeekableByteChannel物件,該物件封裝了檔案操作的通道,可以轉成FileChannel(不是預設的檔案系統不能轉)。這裡注意,分配緩衝區大小就代表了最多讀取的資料位元組大小,比如我的示例檔案中位元組數是8個,但是我只分配了5個位元組的緩衝區,因此只能讀出前5個位元組的資料。
為什麼會有buffer.rewind()這行程式碼呢?因為呼叫了read()方法將檔案內容讀取到緩衝區後,當前位置處於緩衝區的末尾,所以要重繞緩衝區,將指標重置到緩衝區的起始位置。
將檔案對映到緩衝區
這種方式的優點是緩衝區自動包含檔案的內容,不需要顯式的讀操作。同樣的要先獲取Path物件,再獲取檔案通道。
用newByteChannel()方法得到的SeekableByteChannel物件轉成FileChannel型別的物件,因為FileChannel物件有map()方法,將通道對映到緩衝區。
map()方法如下所示:
MappedByteBuffer map(FileChannel.MapMode how,long begin,long size) throws IOException
引數how的值為:MapMode.READ_ONLY、MapMode.READ_WRITE、MapMode.PRIVATE 之一。
對映的開始位置由begin指定,對映的位元組數由size指定,作為MappedByteBuffer返回指向緩衝區的引用,MappedByteBuffer是ByteBuffer的子類,一旦將檔案對映到緩衝區,就可以從緩衝區讀取檔案了。
try(FileChannel fileChannel = (FileChannel) Files.newByteChannel(Paths.get("./nio/src/2.txt"))){ long size = fileChannel.size();//獲取檔案位元組數量 MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY,0,size); for(int i=0;i < size; i ++){ System.out.println((char)mappedByteBuffer.get()); } }catch (Exception e){ e.printStackTrace(); }
通過通道寫入檔案
手動分配緩衝區
try(FileChannel seekableByteChannel = (FileChannel) Files.newByteChannel(Paths.get("./nio/src/2.txt"),StandardOpenOption.WRITE,StandardOpenOption.CREATE,StandardOpenOption.APPEND)){ ByteBuffer buffer = ByteBuffer.allocate(5);//指定緩衝區大小 for(int i=0;i<5;i++){ buffer.put((byte)('A'+i)); } buffer.rewind(); seekableByteChannel.write(buffer); }catch (Exception e){ e.printStackTrace(); }
因為是針對寫操作而開啟檔案,所以引數必須指定為StandardOpenOption.WRITE,如果希望檔案不存在就建立檔案,可以指定StandardOpenOption.CREATE,但是我還希望是以追加的形式寫入內容,所以又指定了StandardOpenOption.APPEND。
需要注意的是buffer.put()方法每次呼叫都會向前推進當前位置,所以在呼叫write()方法之前,需要將當前位置重置到緩衝區的開頭,如果沒有這麼做,write()方法會認為緩衝區中沒有資料。
將檔案對映到緩衝區
Path path = Paths.get("./nio/src/4.txt"); try(FileChannel fileChannel = (FileChannel) Files.newByteChannel(path,StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE)){ MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE,0,5); for(int i=0;i < 5; i ++){ buffer.put((byte) ('A'+i)); } }catch (Exception e){ e.printStackTrace(); }
可以看出,對於通道自身並沒有顯式的寫操作,因為緩衝區被對映到檔案,所以對緩衝區的修改會自動反映到底層檔案中。
對映緩衝區要麼是隻讀,要麼是讀/寫,所以這裡必須是READ和WRITE兩個選項都得要。一旦將檔案對映到緩衝區,就可以向緩衝區中寫入資料,並且這些資料會被自動寫入檔案,所以不需要對通道執行顯式的寫入操作。
另外,寫入的檔案大小不能超過緩衝區的大小,如果超過了之後會丟擲異常,但是已經寫入的資料仍然會成功。比如緩衝區5個位元組,我寫入10個位元組,程式會丟擲異常,但是前5個位元組仍然會寫入檔案中。
使用NIO複製和移動檔案
Path path = Paths.get("./nio/src/4.txt"); Path path2 = Paths.get("./nio/src/40.txt"); try{ Files.copy(path2,path, StandardCopyOption.REPLACE_EXISTING); //Files.move(path,path2, StandardCopyOption.REPLACE_EXISTING); }catch (Exception e){ e.printStackTrace(); }
StandardCopyOption.REPLACE_EXISTING選項的意思是如果目標檔案存在則替換。
為基於流的IO使用NIO
如果擁有Path物件,那麼可以通過呼叫Files類的靜態方法newInputStream()或newOutputStream()來得到連線到指定檔案的流。
方法原型如下:
static InputStream newInputStream(Path path,OpenOption... how) throws IOException
how的引數值必須是一個或多個由StandardOpenOption定義的值,如果沒有指定選項,預設開啟方式為StandardOpenOption.READ。
一旦開啟檔案,就可以使用InputStream定義的任何方法。
因為newInputStream()方法返回的是常規流,所以也可以在緩衝流中封裝流。
try(BufferedInputStream inputStream = new BufferedInputStream(Files.newInputStream(Paths.get("./nio/src/2.txt")))){ int s = inputStream.available(); for(int i=0;i<s;i++){ int c = inputStream.read(); System.out.print((char) c); } }catch (Exception e){ e.printStackTrace(); }
OutputStream和前面的InputStream類似:
try(BufferedOutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(Paths.get("./nio/src/2.txt")))){ for(int i=0;i<26;i++){ outputStream.write((byte)('A'+i)); } }catch (Exception e){ e.printStackTrace(); }
為基於檔案系統使用NIO
Files類
要進行操作的檔案是由Path指定的,但是對檔案執行的許多操作都是由Files類中的靜態方法提供的。
java.nio.file.Files類就是為了替代java.io.File類而生。
以下列出部分常用方法:
方法 | 描述 |
static Path copy(Path from,Path to,CopyOption... opts) | 將from複製到to,返回to |
static Path move(Path from,Path to,CopyOption... opts) | 將from移動到to,返回to |
static Path createDirectory(Path path,FileAttribute<?> attribs) | 建立一個目錄,目錄屬性是由attribs指定的。 |
static Path createFile(Path path,FileAttribute<?> attrbs) | 建立一個檔案,檔案屬性是由attribs指定的。 |
static void delete(Path path) | 刪除一個檔案 |
static boolean exists(Path path) | path代表的路徑是否存在(無論檔案還是目錄) |
static boolean notExists(Path path) | path代表的路徑是否不存在(無論檔案還是目錄) |
static boolean isRegularFile(Path path) | 是否是檔案 |
static boolean isDirectory(Path path) | 是否是目錄 |
static boolean isExecutable(Path path) | 是否是可執行檔案 |
static boolean isHidden(Path path) | 是否是隱藏檔案 |
static boolean isReadable(Path path) | 是否可讀 |
static boolean isWritable(Path path) | 是否可寫 |
static long size(Path path) | 返回檔案大小 |
static SeekableByteChannel newByteChannel(Path path,OpenOption... opts) | 開啟檔案,opts指定開啟方式,返回一個通道物件 |
static DirectoryStream<Path> newDirectoryStream(Path path) | 開啟目錄,返回一個目錄流 |
static InputStream newInputStream(Path path,OpenOption... opts) | 開啟檔案,返回一個輸入流 |
static OutputStream newOutputStream(Path path,OpenOption... opts) | 開啟檔案,返回一個輸出流 |
引數列表中出現的有型別為OpenOption的引數,它是一個介面,真實傳入的引數是StandardOpenOption類中的列舉,這個列舉引數與newBufferedWriter/newInputStream/newOutputStream/write方法一起使用。
StandardOpenOption類中的列舉 | 描述 |
READ | 用於讀取開啟檔案 |
WRITE | 用於寫入開啟檔案 |
APPEND | 如果是寫入,則內容追加到末尾 |
CREATE | 自動在檔案不存在的情況下建立新檔案 |
CREATE_NEW | 建立新檔案,如果檔案已存在則丟擲異常 |
DELETE_ON_CLOSE | 當檔案被關閉時刪除檔案 |
DSYNC | 對檔案內容的修改被立即寫入物理裝置 |
SYNC | 對檔案內容或後設資料的修改被立即寫入物理裝置 |
TRUNCATE_EXISTING | 如果用於寫入而開啟,那麼移除已有內容 |
下面演示追加寫入檔案操作:
try{ Path path = Paths.get("./nio/src/8.txt"); String str = "今天天氣不錯哦\n"; Files.write(path,str.getBytes(),StandardOpenOption.CREATE, StandardOpenOption.APPEND); }catch (Exception e){ e.printStackTrace(); }
目錄流
遍歷目錄
如果Path中的路徑是目錄,那麼可以使用Files類的靜態方法newDirectoryStream()來獲取目錄流。
方法原型如下:
static DirectoryStream<Path> newDirectoryStream(Path dir) throw IOException
呼叫此方法的前提是目標必須是目錄,並且可讀,否則會拋異常。
try(DirectoryStream<Path> paths = Files.newDirectoryStream(Paths.get("./nio/src"))){ for(Path path : paths){ System.out.println(path.getFileName()); } }catch (Exception e){ e.printStackTrace(); }
DirectoryStream<Path>實現了Iterable<Path>,所以可以用foreach迴圈對其進行遍歷,但是它實現的迭代器針對每個例項只能獲取一次,所以只能遍歷一次。
匹配內容
Files.newDirectoryStream方法還有一種形式,可以傳入匹配規則:
static DirectoryStream<Path> newDirectoryStream(Path dir,String glob) throws IOException
第二個引數就是匹配規則,但是它不支援強大的正則,只支援簡單的匹配,如"?"代表任意1個字元,"*"代表任意個任意字元。
使用示例 匹配所有.java結尾的檔案:
try(DirectoryStream<Path> paths = Files.newDirectoryStream(Paths.get("./nio/src"),"*.java")){ for(Path path : paths){ System.out.println(path.getFileName()); } }catch (Exception e){ e.printStackTrace(); }
複雜匹配
這種方式的原型為:
static DirectoryStream<Path> newDirectoryStream(Path dir,DirectoryStream.Filter<? super Path> filter) throws IOException
其中的DirectoryStream.Filter是定義了以下方法的介面:
boolean accept(T entry) throws IOException
這個方法中如果希望匹配entry就返回true,否則就返回false,這種形式的優點是可以基於檔名之外的其他內容過濾,比如說,可以只匹配目錄、只匹配檔案、匹配檔案大小、建立日期、修改日期等各種屬性。
下面是匹配檔案大小的示例:
String dirname = "./nio/src"; DirectoryStream.Filter<Path> filter = (entry)->{ if(Files.size(entry) > 25){ return true; } return false; }; try(DirectoryStream<Path> paths = Files.newDirectoryStream(Paths.get(dirname),filter)){ for(Path path : paths){ System.out.println(path.getFileName()); } }catch (Exception e){ e.printStackTrace(); }
目錄樹
遍歷目錄下的所有資源以往的做法都是用遞迴來實現,但是在NIO.2的時候提供了walkFileTree方法,使得遍歷目錄變得優雅而簡單,其中涉及4個方法,根據需求選擇重寫。
示例如下:
String dir = "./nio"; try{ Files.walkFileTree(Paths.get(dir), new SimpleFileVisitor<Path>(){ @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { System.out.println("正在訪問檔案:"+file); return super.visitFile(file, attrs); } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { System.out.println("正在訪問目錄:"+dir); return super.preVisitDirectory(dir, attrs); } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { System.out.println("訪問失敗的檔案:"+file); return super.visitFileFailed(file, exc); } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { System.out.println("這個目錄訪問結束了:"+dir); return super.postVisitDirectory(dir, exc); } }); }catch (Exception e){ e.printStackTrace(); }
列印結果如圖:
檔案加鎖機制
JDK1.4引入的檔案加鎖機制,要鎖定一個檔案,可以呼叫FileChannel類的lock或tryLock方法
FileChannel channel = FileChannel.open(path);
FileLock lock = channel.lock() 或者 FileLock lock1 = channel.tryLock()
第一個呼叫會阻塞直到獲得鎖,第二個呼叫立刻就會返回 要麼返回鎖 要麼返回Null。
獲得鎖後這個檔案將保持鎖定狀態,直到這個通道關閉,或者釋放鎖:lock.release(); 點進原始碼可以輕易發現,FileChannel實現了AutoCloseable介面,也就是說,可以通過try語句來自動管理資源,不需要手動釋放鎖。
還可以鎖定檔案內容的一部分:
FileLock lock(long start,long size,boolean shared) FileLock lock(long start,long size,boolean shared)
鎖定區域為(從start到start+size),那麼在start+size之外的部分不會被鎖定。shared引數為布林值,代表是否是讀鎖,讀鎖就是共享鎖,寫鎖就是排他鎖。
原始碼分享
https://gitee.com/zhao-baolin/java_learning/tree/master/nio