首页 > 代码库 > Java I/O 扩展

Java I/O 扩展

Java I/O 扩展


NIO

Java 的NIO(新IO)和传统的IO有着同样的目的: 输入 输出 .可是NIO使用了不同的方式来处理IO,NIO利用内存映射文件(此处文件的含义能够參考Unix的名言一切皆文件)来处理IO, NIO将文件或文件的一段区域映射到内存中(相似于操作系统的虚拟内存),这样就能够像訪问内存一样来訪问文件了.

ChannelBuffer是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将磁盘文件全部映射到内存中后得到的结果, 通常MappedByteBufferChannelmap()方法返回.

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 OutputStreamgetChannel()方法来返回相应的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编码器来完毕更加灵活的编码/解码操作(他们肯定也提供了encodedecode方法).

@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接口, registerWatchable提供的方法.

  • 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.ByteStreamscom.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));
}

关于ByteStreamsCharStreams的具体介绍请參考Guava文档


Guava源与汇

  • Guava提出源与汇的概念以避免总是直接跟流打交道.
  • 源与汇是指某个你知道怎样从中打开流的资源,如File或URL.
  • 源是可读的,汇是可写的.

Guava的源有 ByteSourceCharSource; 汇有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

  • 上面看到了使用FilesResourcesURLFile转换成ByteSourceCharSource的用法,事实上这两个类还提供了非常多方法来简化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官方文档
<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

Java I/O 扩展