Java I/O 擴展

jopen 8年前發布 | 19K 次閱讀 Java Java開發

Java I/O 擴展
</h2>

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.
參考:
Google Guava官方教程(中文版)
Google Guava官方文檔

來自: http://blog.csdn.net/zjf280441589/article/details/50526810

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!