Java I/O 扩展
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