Java I/O 扩展
Java I/O 扩展
标签: Java基础
NIO
Java 的
NIO(新IO)和传统的IO有着同样的目的:输入输出.可是NIO使用了不同的方式来处理IO,NIO利用内存映射文件(此处文件的含义能够參考Unix的名言一切皆文件)来处理IO, NIO将文件或文件的一段区域映射到内存中(相似于操作系统的虚拟内存),这样就能够像訪问内存一样来訪问文件了.
Channel 和 Buffer是NIO中的两个核心概念:
Channel是对传统的IO系统的模拟,在NIO系统中全部的数据都须要通过Channel传输;Channel与传统的InputStreamOutputStream最大的差别在于它提供了一个map()方法,能够直接将一块数据映射到内存中.假设说传统的IO系统是面向流的处理, 则NIO则是面向块的处理;Buffer能够被理解成一个容器, 他的本质是一个数组; Buffer作为Channel与程序的中间层, 存入到Channel中的全部对象都必须首先放到Buffer中(Buffer->Channel), 而从Channel中读取的数据也必须先放到Buffer中(Channel->Buffer).
Buffer
从原理来看,
java.nio.ByteBuffer就像一个数组,他能够保存多个类型同样的数据.Buffer仅仅是一个抽象类,相应每种基本数据类型(boolean除外)都有相应的Buffer类:CharBufferShortBufferByteBuffer等.
这些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和GoogleGuava的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
FilesResources对文件/资源的支持;
Java IO 简化
- 在Guava中,用
InputStream/OutputStreamReadable/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. |

浙公网安备 33010602011771号