Java I/O 擴展
NIO
Java 的 NIO (新IO)和傳統的IO有著相同的目的: 輸入 輸出 .但是NIO使用了不同的方式來處理IO,NIO利用 內存映射文件 (此處文件的含義可以參考Unix的名言 一切皆文件 )來處理IO, NIO將文件或文件的一段區域映射到內存中(類似于操作系統的虛擬內存),這樣就可以像訪問內存一樣來訪問文件了.
Channel 和 Buffer 是NIO中的兩個核心概念:
- Channel 是對傳統的IO系統的模擬,在NIO系統中所有的數據都需要通過 Channel 傳輸; Channel 與傳統的 InputStream OutputStream 最大的區別在于它提供了一個 map() 方法,可以直接 將一塊數據映射到內存中 .如果說傳統的IO系統是面向流的處理, 則NIO則是面向 塊 的處理;
- Buffer 可以被理解成一個容器, 他的本質是一個數組; Buffer作為Channel與程序的中間層 , 存入到 Channel 中的所有對象都必須首先放到 Buffer 中( Buffer -> Channel ), 而從 Channel 中讀取的數據也必須先放到 Buffer 中( Channel -> Buffer ).
Buffer
從原理來看, java.nio.ByteBuffer 就像一個數組,他可以保存多個 類型相同 的數據. Buffer 只是一個抽象類,對應每種基本數據類型(boolean除外)都有相應的Buffer類: CharBuffer ShortBuffer ByteBuffer 等.
這些Buffer除了 ByteBuffer 之外, 都采用相同或相似的方法來管理數據, 只是各自管理的數據類型不同而已.這些Buffer類都沒有提供構造器, 可以通過如下方法來得到一個Buffer對象.
// Allocates a new buffer. static XxxBuffer allocate(int capacity);
其中 ByteBuffer 還有一個子類 MappedByteBuffer ,它表示 Channel 將磁盤文件全部映射到內存中后得到的結果, 通常 MappedByteBuffer 由 Channel 的 map() 方法返回.
Buffer中的幾個概念:
- capacity: 該Buffer的最大數據容量;
- limit: 第一個不應該被讀出/寫入的緩沖區索引;
- position: 指明下一個可以被讀出/寫入的緩沖區索引;
- mark: Buffer允許直接將position定位到該mark處.
0 <= mark <= position <= limit <= capacity
Buffer中常用的方法:
方法 | 解釋 |
---|---|
int capacity() | Returns this buffer’s capacity. |
int remaining() | Returns the number of elements between the current position and the limit. |
int limit() | Returns this buffer’s limit. |
int position() | Returns this buffer’s position. |
Buffer position(int newPosition) | Sets this buffer’s position. |
Buffer reset() | Resets this buffer’s position to the previously-marked position. |
Buffer clear() | Clears this buffer.(并不是真的清空, 而是為下一次插入數據做好準備 |
Buffer flip() | Flips this buffer.(將數據 封存 ,為讀取數據做好準備) |
除了這些在 Buffer 基類中存在的方法之外, Buffer的所有子類還提供了兩個重要的方法:
- put() : 向Buffer中放入數據
- get() : 從Buffer中取數據
當使用put/get方法放入/取出數據時, Buffer既支持單個數據的訪問, 也支持(以數組為參數)批量數據的訪問.而且當使用put/get方法訪問Buffer的數據時, 也可分為相對和絕對兩種:
- 相對 : 從Buffer的當前position處開始讀取/寫入數據, position按處理元素個數后移.
- 絕對 : 直接根據索引讀取/寫入數據, position不變.
/** * @author jifang * @since 16/1/9下午8:31. */ public class BufferTest { @Test public void client() { ByteBuffer buffer = ByteBuffer.allocate(64); displayBufferInfo(buffer, "init"); buffer.put((byte) 'a'); buffer.put((byte) 'b'); buffer.put((byte) 'c'); displayBufferInfo(buffer, "after put"); buffer.flip(); displayBufferInfo(buffer, "after flip"); System.out.println((char) buffer.get()); displayBufferInfo(buffer, "after a get"); buffer.clear(); displayBufferInfo(buffer, "after clear"); // 依然可以訪問到數據 System.out.println((char) buffer.get(2)); } private void displayBufferInfo(Buffer buffer, String msg) { System.out.println("---------" + msg + "-----------"); System.out.println("position: " + buffer.position()); System.out.println("limit: " + buffer.limit()); System.out.println("capacity: " + buffer.capacity()); } }
通過 allocate() 方法創建的Buffer對象是普通Buffer, ByteBuffer 還提供了一個 allocateDirect() 方法來創建 DirectByteBuffer . DirectByteBuffer 的創建成本比普通Buffer要高, 但 DirectByteBuffer 的讀取效率也會更高.所以 DirectByteBuffer 適用于生存期比較長的Buffer.
只有 ByteBuffer 才提供了 allocateDirect(int capacity) 方法, 所以只能在 ByteBuffer 級別上創建 DirectByteBuffer , 如果希望使用其他類型, 則可以將Buffer轉換成其他類型的Buffer.
Channel
像上面這樣使用 Buffer 感覺是完全沒有誘惑力的(就一個數組嘛,還整得這么麻煩⊙﹏⊙b).其實 Buffer 真正的強大之處在于與 Channel 的結合,從 Channel 中直接映射一塊內存進來,而沒有必要一一的get/put.
java.nio.channels.Channel 類似于傳統的流對象, 但與傳統的流對象有以下兩個區別:
- Channel 可以直接將指定文件的部分或者全部映射成 Buffer
- 程序不能直接訪問 Channel 中的數據, 必須要經過 Buffer 作為中間層.
Java為Channel接口提供了 FileChannel DatagramChannel Pipe.SinkChannel Pipe.SourceChannel SelectableChannel
SocketChannel ServerSocketChannel . 所有的 Channel 都不應該通過構造器來直接創建, 而是通過傳統的 InputStream OutputStream 的 getChannel() 方法來返回對應的 Channel , 當然不同的節點流獲得的 Channel 不一樣. 例如, FileInputStream FileOutputStream 返回的是 FileChannel , PipedInputStream PipedOutputStream 返回的是 Pipe.SourceChannel Pipe.SinkChannel ;
Channel 中最常用的三個方法是 MappedByteBuffer map(FileChannel.MapMode mode, long position, long size) read() write() , 其中 map() 用于將Channel對應的部分或全部數據映射成 ByteBuffer , 而read/write有一系列的重載形式, 用于從Buffer中讀寫數據.
/** * @author jifang * @since 16/1/9下午10:55. */ public class ChannelTest { private CharsetDecoder decoder = Charset.forName("utf-8").newDecoder(); @Test public void client() throws IOException { try (FileChannel inChannel = new FileInputStream("save.txt").getChannel(); FileChannel outChannel = new FileOutputStream("attach.txt").getChannel()) { MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, new File("save.txt").length()); displayBufferInfo(buffer, "init buffer"); // 將Buffer內容一次寫入另一文件的Channel outChannel.write(buffer); buffer.flip(); // 解碼CharBuffer之后輸出 System.out.println(decoder.decode(buffer)); } } // ... }
Charset
Java從1.4開始提供了 java.nio.charset.Charset 來處理字節序列和字符序列(字符串)之間的轉換, 該類包含了用于創建解碼器和編碼器的方法, 需要注意的是, Charset 類是不可變類.
Charset 提供了 availableCharsets() 靜態方法來獲取當前JDK所支持的所有字符集.
/** * @author jifang * @since 16/1/10下午4:32. */ public class CharsetLearn { @Test public void testGetAllCharsets() { SortedMap<String, Charset> charsetMap = Charset.availableCharsets(); for (Map.Entry<String, Charset> charset : charsetMap.entrySet()) { System.out.println(charset.getKey() + " aliases -> " + charset.getValue().aliases() + " chaset -> " + charset.getValue()); } } }
執行上面代碼可以看到每個字符集都有一些字符串別名(比如 UTF-8 還有 unicode-1-1-utf-8 UTF8 的別名), 一旦知道了字符串的別名之后, 程序就可以調用Charset的 forName() 方法來創建對應的Charset對象:
@Test public void testGetCharset() { Charset utf8 = Charset.forName("UTF-8"); Charset unicode11 = Charset.forName("unicode-1-1-utf-8"); System.out.println(utf8.name()); System.out.println(unicode11.name()); System.out.println(unicode11 == utf8); }
在Java 1.7 之后, JDK又提供了一個工具類 StandardCharsets , 里面提供了一些靜態屬性來表示標準的常用字符集:
@Test public void testGetCharset() { // 使用UTF-8屬性 Charset utf8 = StandardCharsets.UTF_8; Charset unicode11 = Charset.forName("unicode-1-1-utf-8"); System.out.println(utf8.name()); System.out.println(unicode11.name()); System.out.println(unicode11 == utf8); }
獲得了 Charset 對象之后,就可以使用 decode() / encode() 方法來對 ByteBuffer CharBuffer 進行編碼/解碼了
方法 | 功能 |
---|---|
ByteBuffer encode(CharBuffer cb) | Convenience method that encodes Unicode characters into bytes in this charset. |
ByteBuffer encode(String str) | Convenience method that encodes a string into bytes in this charset. |
CharBuffer decode(ByteBuffer bb) | Convenience method that decodes bytes in this charset into Unicode characters. |
或者也可以通過 Charset 對象的 newDecoder() newEncoder() 來獲取 CharsetDecoder 解碼器和 CharsetEncoder 編碼器來完成更加靈活的編碼/解碼操作(他們肯定也提供了 encode 和 decode 方法).
@Test public void testDecodeEncode() throws IOException { File inFile = new File("save.txt"); FileChannel in = new FileInputStream(inFile).getChannel(); MappedByteBuffer byteBuffer = in.map(FileChannel.MapMode.READ_ONLY, 0, inFile.length()); // Charset utf8 = Charset.forName("UTF-8"); Charset utf8 = StandardCharsets.UTF_8; // 解碼 // CharBuffer charBuffer = utf8.decode(byteBuffer); CharBuffer charBuffer = utf8.newDecoder().decode(byteBuffer); System.out.println(charBuffer); // 編碼 // ByteBuffer encoded = utf8.encode(charBuffer); ByteBuffer encoded = utf8.newEncoder().encode(charBuffer); byte[] bytes = new byte[(int) inFile.length()]; encoded.get(bytes); for (int i = 0; i < bytes.length; ++i) { System.out.print(bytes[i]); } System.out.println(); }
String類里面也提供了一個 getBytes(String charset) 方法來使用指定的字符集將字符串轉換成字節序列.
使用 WatchService 監控文件變化
在以前的Java版本中,如果程序需要監控文件系統的變化,則可以考慮啟動一條后臺線程,這條后臺線程每隔一段時間去遍歷一次指定目錄的文件,如果發現此次遍歷的結果與上次不同,則認為文件發生了變化. 但在后來的NIO.2中, Path 類提供了 register 方法來監聽文件系統的變化.
WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events); WatchKey register(WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers);
其實是 Path 實現了 Watchable 接口, register 是 Watchable 提供的方法.
- WatchService 代表一個 文件系統監聽服務 , 它負責監聽 Path 目錄下的文件變化.而 WatchService 是一個接口, 需要由 FileSystem 的實例來創建, 我們往往這樣獲取一個 WatchService
WatchService service = FileSystems.getDefault().newWatchService();
一旦 register 方法完成注冊之后, 接下來就可調用 WatchService 的如下方法來獲取被監聽的目錄的文件變化事件:
方法 | 釋義 |
---|---|
WatchKey poll() | Retrieves and removes the next watch key, or null if none are present. |
WatchKey poll(long timeout, TimeUnit unit) | Retrieves and removes the next watch key, waiting if necessary up to the specified wait time if none are yet present. |
WatchKey take() | Retrieves and removes next watch key, waiting if none are yet present. |
- 獲取到 WatchKey 之后, 就可調用其方法來查看到底發生了什么事件, 得到 WatchEvent
方法 | 釋義 |
---|---|
List<WatchEvent<?>> pollEvents() | Retrieves and removes all pending events for this watch key, returning a List of the events that were retrieved. |
boolean reset() | Resets this watch key. |
- WatchEvent
方法 | 釋義 |
---|---|
T context() | Returns the context for the event. |
int count() | Returns the event count. |
WatchEvent.Kind<T> kind() | Returns the event kind. |
/** * @author jifang * @since 16/1/10下午8:00. */ public class ChangeWatcher { public static void main(String[] args) { watch("/Users/jifang/"); } public static void watch(String directory) { try { WatchService service = FileSystems.getDefault().newWatchService(); Paths.get(directory).register(service, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY); while (true) { WatchKey key = service.take(); for (WatchEvent event : key.pollEvents()) { System.out.println(event.context() + " 文件發生了 " + event.kind() + " 事件!"); } if (!key.reset()) { break; } } } catch (IOException | InterruptedException e) { throw new RuntimeException(e); } } }
通過使用 WatchService , 可以非常優雅的監控指定目錄下的文件變化, 至于文件發生變化后的處理, 就取決于業務需求了, 比如我們可以做一個日志分析器, 定時去掃描日志目錄, 查看日志大小是否改變, 當發生改變時候, 就掃描發生改變的部分, 如果發現日志中有異常產生(比如有Exception/Timeout類似的關鍵字存在), 就把這段異常信息截取下來, 發郵件/短信給管理員.
Guava IO
- 平時開發中常用的IO框架有Apache的 commons-io 和Google Guava 的IO模塊; 不過Apache的 commons-io 包比較老,更新比較緩慢(最新的包還是2012年的); 而Guava則更新相對頻繁, 最近剛剛發布了 19.0 版本, 因此在這兒僅介紹Guava對Java IO的擴展.
- 使用Guava需要在 pom.xml 中添加如下依賴:
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>19.0</version> </dependency>
最近我在寫一個網頁圖片抓取工具時, 最開始使用的是Java的 URL.openConnection() + IOStream 操作來實現, 代碼非常繁瑣且性能不高(詳細代碼可類似參考 java 使用URL來讀取網頁內容 ). 而使用了Guava之后幾行代碼就搞定了網頁的下載功能:
public static String getHtml(String url) { if (StringUtils.isBlank(url)) { return null; } try { return Resources.toString(new URL(url), StandardCharsets.UTF_8); } catch (IOException e) { LOGGER.error("getHtml error url = {}", url, e); throw new RuntimeException(e); } }
代碼清晰多了.
- 還可以使用 Resources 類的 readLines(URL url, Charset charset, LineProcessor<T> callback) 方法來實現只抓取特定的網頁內容的功能:
public static List<String> processUrl(String url, final String regexp) { try { return Resources.readLines(new URL(url), StandardCharsets.UTF_8, new LineProcessor<List<String>>() { private Pattern pattern = Pattern.compile(regexp); private List<String> strings = new ArrayList<>(); @Override public boolean processLine(String line) throws IOException { Matcher matcher = pattern.matcher(line); while (matcher.find()) { strings.add(matcher.group()); } return true; } @Override public List<String> getResult() { return strings; } }); } catch (IOException e) { LOGGER.error("processUrl error, url = {}, regexp = {}", url, regexp, e); throw new RuntimeException(e); } }
而性能的話, 我記得有這么一句話來評論STL的
STL性能可能不是最高的, 但絕對不是最差的!
我認為這句話同樣適用于Guava; 在Guava IO中, 有三類操作是比較常用的:
- 對Java傳統的IO操作的簡化;
- Guava對 源 與 匯 的支持;
- Guava Files Resources 對文件/資源的支持;
Java IO 簡化
- 在Guava中,用 InputStream/OutputStream Readable/Appendable 來對應Java中的字節流和字符流( Writer 實現了 Appendable 接口, Reader 實現了 Readable 接口).并用 com.google.common.io.ByteStreams 和 com.google.common.io.CharStreams 來提供對傳統IO的支持.
這兩個類中, 實現了很多static方法來簡化Java IO操作,如:
- static long copy(Readable/InputStream from, Appendable/OutputStream to)
- static byte[] toByteArray(InputStream in)
- static int read(InputStream in, byte[] b, int off, int len)
- static ByteArrayDataInput newDataInput(byte[] bytes, int start)
- static String toString(Readable r)
/** * 一行代碼讀取文件內容 * * @throws IOException */ @Test public void getFileContent() throws IOException { FileReader reader = new FileReader("save.txt"); System.out.println(CharStreams.toString(reader)); }
關于 ByteStreams 和 CharStreams 的詳細介紹請參考 Guava文檔
Guava源與匯
- Guava提出源與匯的概念以避免總是直接跟流打交道.
- 源與匯是指某個你 知道如何從中打開流的資源 ,如File或URL.
- 源是可讀的,匯是可寫的.
Guava的源有 ByteSource 和 CharSource ; 匯有 ByteSink CharSink
- 源與匯的好處是它們提供了一組通用的操作(如:一旦你把數據源包裝成了ByteSource,無論它原先的類型是什么,你都得到了一組按字節操作的方法). 其實就源與匯就類似于Java IO中的 InputStream/OutputStream , Reader/Writer . 只要能夠獲取到他們或者他們的子類, 就可以使用他們提供的操作, 不管底層實現如何.
/** * @author jifang * @since 16/1/11下午4:39. */ public class SourceSinkTest { @Test public void fileSinkSource() throws IOException { File file = new File("save.txt"); CharSink sink = Files.asCharSink(file, StandardCharsets.UTF_8); sink.write("- 你好嗎?\n- 我很好."); CharSource source = Files.asCharSource(file, StandardCharsets.UTF_8); System.out.println(source.read()); } @Test public void netSource() throws IOException { CharSource source = Resources.asCharSource(new URL("http://www.sun.com"), StandardCharsets.UTF_8); System.out.println(source.readFirstLine()); } }
獲取源與匯
- 獲取字節源與匯的常用方法有:
字節源 | 字節匯 |
---|---|
Files.asByteSource(File) | Files.asByteSink(File file, FileWriteMode... modes) |
Resources.asByteSource(URL url) | - |
ByteSource.wrap(byte[] b) | - |
ByteSource.concat(ByteSource... sources) | - |
- 獲取字符源與匯的常用方法有:
字符源 | 字符匯 |
---|---|
Files.asCharSource(File file, Charset charset) | Files.asCharSink(File file, Charset charset, FileWriteMode... modes) |
Resources.asCharSource(URL url, Charset charset) | - |
CharSource.wrap(CharSequence charSequence) | - |
CharSource.concat(CharSource... sources) | - |
ByteSource.asCharSource(Charset charset) | ByteSink.asCharSink(Charset charset) |
使用源與匯
- 這四個源與匯提供通用的方法進行讀/寫, 用法與Java IO類似,但比Java IO流會更加簡單方便(如 CharSource 可以一次性將源中的數據全部讀出 String read() , 也可以將源中的數據一次拷貝到Writer或匯中 long copyTo(CharSink/Appendable to) )
@Test public void saveHtmlFileChar() throws IOException { CharSource source = Resources.asCharSource(new URL("http://www.google.com"), StandardCharsets.UTF_8); source.copyTo(Files.asCharSink(new File("save1.html"), StandardCharsets.UTF_8)); } @Test public void saveHtmlFileByte() throws IOException { ByteSource source = Resources.asByteSource(new URL("http://www.google.com")); //source.copyTo(new FileOutputStream("save2.html")); source.copyTo(Files.asByteSink(new File("save2.html"))); }
其他詳細用法請參考 Guava文檔
Files與Resources
-
上面看到了使用 Files 與 Resources 將 URL 和 File 轉換成 ByteSource 與 CharSource 的用法,其實這兩個類還提供了很多方法來簡化IO, 詳細請參考 Guava文檔
-
Resources 常用方法
Resources 方法 | 釋義 |
---|---|
static void copy(URL from, OutputStream to) | Copies all bytes from a URL to an output stream. |
static URL getResource(String resourceName) | Returns a URL pointing to resourceName if the resource is found using the context class loader. |
static List<String> readLines(URL url, Charset charset) | Reads all of the lines from a URL. |
static <T> T readLines(URL url, Charset charset, LineProcessor<T> callback) | Streams lines from a URL, stopping when our callback returns false, or we have read all of the lines. |
static byte[] toByteArray(URL url) | Reads all bytes from a URL into a byte array. |
static String toString(URL url, Charset charset) | Reads all characters from a URL into a String, using the given character set. |
- Files 常用方法
Files 方法 | 釋義 |
---|---|
static void append(CharSequence from, File to, Charset charset) | Appends a character sequence (such as a string) to a file using the given character set. |
static void copy(File from, Charset charset, Appendable to) | Copies all characters from a file to an appendable object, using the given character set. |
static void copy(File from, File to) | Copies all the bytes from one file to another. |
static void copy(File from, OutputStream to) | Copies all bytes from a file to an output stream. |
static File createTempDir() | Atomically creates a new directory somewhere beneath the system’s temporary directory (as defined by the java.io.tmpdir system property), and returns its name. |
static MappedByteBuffer map(File file, FileChannel.MapMode mode, long size) | Maps a file in to memory as per FileChannel.map(java.nio.channels.FileChannel.MapMode, long, long) using the requested FileChannel.MapMode. |
static void move(File from, File to) | Moves a file from one path to another. |
static <T> T readBytes(File file, ByteProcessor<T> processor) | Process the bytes of a file. |
static String readFirstLine(File file, Charset charset) | Reads the first line from a file. |
static List<String> readLines(File file, Charset charset) | Reads all of the lines from a file. |
static <T> T readLines(File file, Charset charset, LineProcessor<T> callback) | Streams lines from a File, stopping when our callback returns false, or we have read all of the lines. |
static byte[] toByteArray(File file) | Reads all bytes from a file into a byte array. |
static String toString(File file, Charset charset) | Reads all characters from a file into a String, using the given character set. |
static void touch(File file) | Creates an empty file or updates the last updated timestamp on the same as the unix command of the same name. |
static void write(byte[] from, File to) | Overwrites a file with the contents of a byte array. |
static void write(CharSequence from, File to, Charset charset) | Writes a character sequence (such as a string) to a file using the given character set. |
來自: http://blog.csdn.net/zjf280441589/article/details/50526810