Java NIO 之前我们讲过JavaIO,我们曾把JavaIO比喻为从一个竹筒中取水,一滴滴地输入或者输出。新的IO方式使用了不同的方式来处理输入与输出,新IO采用内存映射文件的方式来处理输入和输出,新IO将文件或者文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了。
Channel(通道)和Buffer(缓冲) 是新IO中两个核心对象。新IO系统中所有数据都需要通过通道来传输;你可能会说,都是用通道的话,两者究竟有啥不同?
答案是Channel提供了一个map方法,通过该方法可以直接将一块数据映射到内存中。
使用Buffer Buffer是一个抽象类,从内部结构上来看,Buffer有点像数组,一类数据的容器。Buffer常用的子类是ByteBuffer
,当然也有其他对应基础数据类型的子类。例如CharBuffer、IntBuffer等。
Buffer中有三个重要概念:
容量(capacity):缓冲区的容积表示该Buffer最多能容纳多少数据。
界限(limit):第一个不能被读出或写入的缓冲区索引,也就是说处于界限之后的数据既不可被读取也不能写入。
位置(position):这个很好理解,指明下个能被读出或写入的缓冲区位置索引,类似于IO流中的记录指针,或者RandomAccessFile类的文件指针。(关于前面IO流和RandomAccessFile类我在之前的博客中(将JavaIO那一块都有涉及)
还有一个概念-mark,Buffer允许直接把position直接定位到mark的位置。
常用方法
方法
描述
static XxxBuffer allocate(int capacity)
Buffer没有提供构造器,通过使用静态方法可以创建Buffer容器,Xxx代表着基础数据类型。比如Char,Short等。
int capacity()
返回Buffer容积大小
boolean hasRemaining()
判断当前位置(position)和界限(limit)之间是否还有元素可供处理
int limit()
返回Buffer的界限
Buffer limit(int newLt)
重新设置界限的值,并返回一个具有新界限的缓冲区对象
Buffer mark()
设置Buffer的mark位置,它只能在0和position之间
int position(int newPs)
返回buffer的位置
Buffer position()
设置Buffer的position,并返回新对象
int remaining()
返回当前位置和界限之间的元素个数
Buffer reset()
将位置转到mark所在的位置
Buffer rewind()
将位置设置成0,取消设置的mark
put(Object)
放入数据
Buffer put(Object)
读取数据
Buffer clean()
将position设置为0,将limit设置为capacity,也就是说做好了写入数据的准备,当然也可以作为输入数据的准备。
Buffer flip()
将limit设置为position所在位置,并将position设为0。这就使的Buffer的读写指针回到了开始位置,也就是说做好了输出数据的准备。
示例程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import java.nio.Buffer;import java.nio.CharBuffer;public class BufferTest { public static void main (String[] args) { CharBuffer buff = CharBuffer.allocate(8 ); System.out.println("capacity" +buff.capacity()); System.out.println("limit" +buff.limit()); System.out.println("position" +buff.position()); buff.put('a' ); buff.put('b' ); buff.put('c' ); System.out.println("加入3个元素后,position" +buff.position()); buff.flip(); System.out.println("调用flip方法,limit" +buff.limit()); System.out.println("position" +buff.position()); System.out.println("第一个元素(position=0:" +buff.get()); System.out.println("position" +buff.position()); buff.clear(); System.out.println("调用clear()后,limit" +buff.limit()); System.out.println("调用clear()后,position" +buff.position()); System.out.println("执行clear后,缓冲区内容并没有被清除:第三个元素为" +buff.get(2 )); System.out.println("执行绝对读取后,position=" +buff.position()); } }
通过 allocate() 方法创建的Buffer 对象是普通 Buffer, ByteBuffer 还提供了一个 allocateDirect() 方法来创建直接Buffer。 直接 Buffer 的创建成本比普通 Buffer 的创建成本高, 但直接 Buffer 的读取效率更高。如果只是使用一次就丢弃的话,不建议直接创建Buffer,而且只有ByteBuffer才提供了直接创建Buffer的方法
使用Channel Channel类似于传统的流对象,不过与传统的流对象主要有两个区别
Channel可以直接将指定文件的部分或全部直接映射成Buffer
程序不能直接访问Channel的数据,包括读写。Channel只能与Buffer进行交互
所有的Channel都不应该通过构造器来创建,而是通过传统的节点的getChannel()
方法来创建,所以一般节点流都有与之对应的CHannel。例如FileInputStream()就是返回FileChannel。
方法的话,read和write用法与传统流差不多。提供了一个特殊的map方法。
MappedByteBuffer map(FileChannel.MapMode mode,long position,long size)
第一个参数执行映射的模式(只读、读写等模式);第二个、第三个参数用于控制将Channel的哪些数据映射为ByteBuffer
下面程序示范了直接将FileChannel的全部数据映射成ByteBuffer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.nio.CharBuffer;import java.nio.MappedByteBuffer;import java.nio.channels.FileChannel;import java.nio.charset.Charset;import java.nio.charset.CharsetDecoder;public class FileChannelTest { public static void main (String[] args) { File f = new File("FileCHannelTest.java" ); try ( FileChannel inChannel = new FileInputStream(f).getChannel(); FileChannel outChannel = new FileOutputStream("a.txt" ).getChannel(); ) { MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0 , f.length()); Charset charset = Charset.forName("UTF-8" ); outChannel.write(buffer); buffer.clear(); CharsetDecoder decoder = charset.newDecoder(); CharBuffer charBuffer = decoder.decode(buffer); System.out.println(charBuffer); } catch (Exception e) { e.printStackTrace(); } } }
注意,虽然FileChannel虽然可以读写,但是FileInputStream获取的FileChannel只能读,而另一个只能写。RandomAccessFile也包含了一个getChannel()的方法,这个方法创建的可读性取决于RandomAccessFile打开文件的模式。
下面是示范程序,将会对a.txt文件的内容进行复制,追加到文件后面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import java.io.File;import java.io.RandomAccessFile;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;public class RandomFileChannelTest { public static void main (String[] args) { File f = new File("a.txt" ); try ( RandomAccessFile raf = new RandomAccessFile(f, "rw" ); FileChannel randomChannel = raf.getChannel()) { ByteBuffer buffer = randomChannel.map(FileChannel.MapMode.READ_ONLY, 0 , f.length()); randomChannel.position(f.length()); randomChannel.write(buffer); } catch (Exception e) { e.printStackTrace(); } } }
字符集和Charset 计算机中的所有文件只是一种表象,所有文件在底层都是二进制文件。对于文本文件来说,我们能看到字符,完全是因为使用了特定的字符集来转换。也就是所谓的编码和解码。
Charset 类提供了一个 availableCharsets() 静态方法来获取当前 JDK 所支持的所有字符集。所以程序可以使用如下程序来获取该 JDK 所支持的全部字符集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import java.nio.charset.Charset;import java.util.SortedMap;public class CharsetTest { public static void main (String[] args) { SortedMap<String, Charset> map = Charset.availableCharsets(); for (String alias : map.keySet()) { System.out.println(alias+"----->" +map.get(alias)); } } }
一旦我们知道了字符集的别名之后,就可以调用Charset.forName()
方法来创建对应的Charset对象,然后通过该对象的newDecoder()
和newEncoder()
这两个方法分别返回CharsetDecoder和CharsetEncoder对象,分别代表着Charset的解码器和编码器。
下面程序完成了ByteBuffer和CharBuffer之间的转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import java.nio.ByteBuffer;import java.nio.CharBuffer;import java.nio.charset.CharacterCodingException;import java.nio.charset.Charset;import java.nio.charset.CharsetDecoder;import java.nio.charset.CharsetEncoder;public class CharsetTransform { public static void main (String[] args) throws CharacterCodingException { Charset cn = Charset.forName("GBK" ); CharsetEncoder cnEncoder = cn.newEncoder(); CharsetDecoder cnDecoder = cn.newDecoder(); CharBuffer cbuff = CharBuffer.allocate(8 ); cbuff.put('孙' ); cbuff.put('悟' ); cbuff.put('空' ); cbuff.flip(); ByteBuffer bbuff = cnEncoder.encode(cbuff); for (int i = 0 ;i<bbuff.capacity();i++) { System.out.println(bbuff.get(i)+" " ); } System.out.println("\n" +cnDecoder.decode(bbuff)); } }
如果仅仅需要进行简单的编码、解码操作,直接调用Charset的encode()和decode()方法进行编码、解码即可。
文件锁 NIO中,Java提供了FileLock来支持文件锁定功能,防止在多个运行程序需要并发地修改同一个文件。
lock()
上锁,tryLock()
可以获得文件锁对象,从而锁定对象。前者会直接上锁,但假如无法得到文件锁,程序将一直阻塞,后者是尝试锁定,如果获得文件锁就返回,没有就返回null。如果想锁定文件地部分内容,则可以使用他们的重载方法。
lock( long position, long size, boolean shared): 对文件从 position 开始, 长度为 size 的内容 加锁,该方法是阻塞式的。
tryLock( long position, long size, boolean shared): 非阻塞式的加锁方法。 参数的作用与上一个 方法类似。
下面程序示范了FileLock类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import java.io.FileOutputStream;import java.nio.channels.FileChannel;import java.nio.channels.FileLock;public class FileLockTest { public static void main (String[] args) { try ( FileChannel channel = new FileOutputStream("a.txt" ).getChannel(); ) { FileLock lock = channel.tryLock(); Thread.sleep(10000 ); lock.release(); } catch (Exception e) { e.printStackTrace(); } } }
关于文件锁的要点:
某些平台上,文件锁仅仅是建议,并不是强制的。这意味着就算上锁还是可以读写。
某些平台上,不能同步地锁定一个文件并把它映射到内存中
文件锁是Java虚拟机所持有,两个Java程序使用同一个虚拟机,不能对同一个文件上锁。
在某些平台上关闭FileChannel时,会释放Java虚拟机在该文件上地所有锁,因此应该避免对同一个被锁定地文件打开多个FileChannel。
NIO.2的功能 Java7对原有的NIO进行了重大改进。提供了全面的文件IO和文件系统访问支持,基于异步Channel的IO
Path、Paths和Files核心API Path代表了一种与平台无关的平台路径,下面是示例程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import java.nio.file.Path;import java.nio.file.Paths;public class PathTest { public static void main (String[] args) { Path path = Paths.get("." ); System.out.println("path里包含的路径数量:" +path.getNameCount()); Path absolutePath = path.toAbsolutePath(); System.out.println(absolutePath); System.out.println("absolutePath的根路径:" +absolutePath.getRoot()); System.out.println("absolutePath里包含的路径数量:" +absolutePath.getNameCount()); System.out.println(absolutePath.getName(0 )); Path path2 = Paths.get("d:" ,"publish" ,"codes" ); System.out.println(path2); } }
Files是一个操作文件的工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.IOException;import java.nio.charset.Charset;import java.nio.file.FileStore;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;import java.util.ArrayList;import java.util.List;public class FilesTest { public static void main (String[] args) throws FileNotFoundException, IOException { Path path = Paths.get("FilesTest.java" ); Charset forName = Charset.forName("UTF-8" ); Files.copy(path, new FileOutputStream("a.txt" )); System.out.println("FileTest.java是否为隐藏文件" +Files.isHidden(path)); List<String> lines = Files.readAllLines(path,forName); System.out.println(lines); System.out.println("FilesTest.java的大小为:" + Files.size(path)); List<String> poem = new ArrayList<>(); poem.add("水晶谭底银鱼跃" ); poem.add("清徐风中碧杆横" ); Files.write(Paths.get("poem.txt" ), poem, forName); Files.list(Paths.get("." )).forEach(path1 ->System.out.println(path1)); Files.lines(path,forName).forEach(line -> System.out.println(line)); FileStore cStore = Files.getFileStore(Paths.get("C:" )); System.out.println("C:共有空间:" +cStore.getTotalSpace()); System.out.println("C:可有空间:" +cStore.getUsableSpace()); } }
使用FileVisitor遍历文件和目录
方法
描述
walkFileTree( Path start, FileVisitor<? super Path> visitor)
遍历 start 路径下的所有文件和子目录。
walkFileTree( Path start, Set< FileVisitOption> options, int maxDepth, FileVisitor<? super Path> visitor)
与上一个方法类似,该方法最多遍历maxDepth深度的文件
FileVisitResult postVisitDirectory( T dir, IOException exc)
访问子目录之后触发该方法。
FileVisitResult preVisitDirectory( T dir, BasicFileAttributes attrs)
访问子目录之前触发该方法。
FileVisitResult visitFile( T file, BasicFileAttributes attrs)
访问 file 文件时触发该方法。
FileVisitResult visitFileFailed( T file, IOException exc)
访问 file 文件失败时触发该方法。
上面方法返回的FileVisitResult 对象是一个枚举类,代表了访问之后的后续行为:
CONTINUE: 代表“ 继续访问” 的后续行为。
SKIP_ SIBLINGS: 代表“ 继续访问” 的后续行为,但不访问该文件或目录的兄弟文件或目录。
SKIP_ SUBTREE: 代表“ 继续访问” 的后续行为,但不访问该文件或目录的子目录树
TERMINATE: 代表“ 中止访问” 的后续行为。
实际使用的时候没必要4个方法都实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import java.io.IOException;import java.nio.file.FileVisitResult;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;import java.nio.file.SimpleFileVisitor;import java.nio.file.attribute.BasicFileAttributes;public class FileVisitorTest { public static void main (String[] args) throws IOException { Files.walkFileTree(Paths.get("D:" ,"java_project" ,"test" ), new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile (Path file,BasicFileAttributes attrs) { System.out.println("正在访问" +file+"文件" ); if (file.endsWith("FileVisitorTest.java" )) { System.out.println("--已经找到目标文件--" ); return FileVisitResult.TERMINATE; } return FileVisitResult.CONTINUE; } @Override public FileVisitResult preVisitDirectory (Path dir,BasicFileAttributes attrs) { System.out.println("正在访问:" +dir+"路径" ); return FileVisitResult.CONTINUE; } }); } }
使用WatchService监控文件变化 Path类提供了register方法来监听文件系统的变化
register( WatchService watcher, WatchEvent. Kind<?>... events)
用 watcher 监听该 path 代表的目录下的文件变化。 events 参数指定要监听哪些类型的事件。
通过注册以后就可以通过WatchSrvice代表一个文件系统监听服务。
WatchKey poll(): 获取 下一个 WatchKey, 如果 没有 WatchKey 发生 就 立即 返回 null。
WatchKey poll( long timeout, TimeUnit unit): 尝试等待 timeout 时间去获取下一个 WatchKey。
WatchKey take(): 获取下一个 WatchKey, 如果没有 WatchKey 发生就一直等待。
如果程序需要一直监控,则应该选择使用 take() 方法; 如果程序只需要监控指定时间,则可考虑使用 poll() 方法。
如下程序监听了C盘创建文件,修改文件,删除文件的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import java.io.IOException;import java.nio.file.FileSystems;import java.nio.file.Paths;import java.nio.file.StandardWatchEventKinds;import java.nio.file.WatchEvent;import java.nio.file.WatchKey;import java.nio.file.WatchService;public class WatchServiceTest { public static void main (String[] args) throws IOException, InterruptedException { WatchService watchService = FileSystems.getDefault().newWatchService(); Paths.get("C:/" ).register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE); while (true ) { WatchKey key = watchService.take(); for (WatchEvent<?> event : key.pollEvents()) { System.out.println(event.context()+" 文件发生了" +event.kind()+" 事件!" ); } boolean valid = key.reset(); if (!valid) { break ; } } } }
访问文件属性 Java7的NIO.2在java.nio.file.attribute包下提供了大量的工具类。这些工具类主要分为以下两类
XxxAttributeView: 代表某种文件属性的“ 视图”。
XxxAttributes: 代表某种文件属性的“ 集合”,程序一般通过 XxxAttributeView 对象来获取 XxxAttributes。
下面是一些方法的官方记录:
方法
描述
AclFileAttributeView
通过 AclFileAttributeView,开发者可以为特定文件设置ACL( Access Control List)及文件所有者属性。它的 getAcl() 方法返回 List< AclEntry> 对象,该返回值代表了该文件的权限集。通过 setAcl( List)方法可以修改该文件的 ACL。
BasicFileAttributeView
它可以获取或修改文件的基本属性,包括文件的最后修改时间、最后访问时间、创建时间、大小、是否为目录、是否为符号链接等。它的 readAttributes() 方法返回一个 BasicFileAttributes 对象,对文件夹基本属性的修改是通过 BasicFileAttributes 对象完成的。
DosFileAttributeView
它主要用于获取或修改文件 DOS 相关属性,比如文件是否只读、是否隐藏、是否为系统文件、是否是存档文件等。 它的 readAttributes() 方法返回一个 DosFileAttributes 对象,对这些属性的修改其实是由 DosFileAttributes 对象来完成的。
FileOwnerAttributeView
它主要 用于获取或修改文件的所有者。 它的 getOwner() 方法返回一个 UserPrincipal 对象来 代表文件所有者;也可调用 setOwner( UserPrincipal owner)方法来改变文件的所有者。
PosixFileAttributeView
它主要用于获取或修改 POSIX( Portable Operating System Interface of INIX)属性,它的 readAttributes() 方法返回一个PosixFileAttributes 对象,该对象可用于获取或修改文件的所有者、组所有者、访问权限信息( 就是 UNIX 的 chmod 命令负责干的事情)。这个 View 只在 UNIX、 Linux 等系统上有用。
示例程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 import java.io.IOException;import java.nio.ByteBuffer;import java.nio.charset.Charset;import java.nio.file.FileSystems;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;import java.nio.file.attribute.BasicFileAttributeView;import java.nio.file.attribute.BasicFileAttributes;import java.nio.file.attribute.DosFileAttributeView;import java.nio.file.attribute.FileOwnerAttributeView;import java.nio.file.attribute.UserDefinedFileAttributeView;import java.nio.file.attribute.UserPrincipal;import java.util.Date;import java.util.List;public class AttributeViewTest { public static void main (String[] args) throws IOException { Path testPath = Paths.get("AttributeViewTest.java" ); BasicFileAttributeView basicView = Files.getFileAttributeView(testPath, BasicFileAttributeView.class); BasicFileAttributes basicAttribs = basicView.readAttributes(); System.out.println("创建时间:" +new Date(basicAttribs.creationTime().toMillis())); System.out.println("最后访问时间:" +new Date(basicAttribs.lastAccessTime().toMillis())); System.out.println("最后修改时间:" +new Date(basicAttribs.lastModifiedTime().toMillis())); System.out.println("文件大小:" +basicAttribs.size()); FileOwnerAttributeView ownerView = Files.getFileAttributeView(testPath, FileOwnerAttributeView.class); System.out.println(ownerView.getOwner()); UserPrincipal user = FileSystems.getDefault().getUserPrincipalLookupService().lookupPrincipalByName("guest" ); UserDefinedFileAttributeView userView = Files.getFileAttributeView(testPath, UserDefinedFileAttributeView.class); List<String> attrNames = userView.list(); for (String name: attrNames) { ByteBuffer buf = ByteBuffer.allocate(userView.size(name)); userView.read(name, buf); buf.flip(); String value = Charset.defaultCharset().decode(buf).toString(); System.out.println(name+"--->" +value); } userView.write("发行者" , Charset.defaultCharset().encode("疯狂Java联盟" )); DosFileAttributeView dosView = Files.getFileAttributeView(testPath, DosFileAttributeView.class); dosView.setHidden(true ); dosView.setReadOnly(true ); } }
NIO很强很强,猛得一批。