JAVA 探究NIO
事情的开始
1.4版本开始,java提供了另一套IO系统,称为NIO,(New I/O的意思),NIO支持面向缓冲区的、基于通道的IO操作。
1.7版本的时候,java对NIO系统进行了极大的扩展,增强了对文件处理和文件系统特性的支持。
在不断的进化迭代之中,IO的很多应用场景应该推荐使用NIO来取代。
NIO系统构建于两个基础术语之上:缓冲区和通道。
缓冲区
Buffer类
缓冲区是一个固定数据量的指定基本类型的数据容器,可以将它理解成一块内存,java将它封装成了Buffer类。
每个非布尔基本数据类型都有各自对应的缓冲区操作类,所有缓冲区操作类都是Buffer类的子类。
除了存储的内容之外,所有的缓冲区都具有通用的核心功能:当前位置、界限、容量。
当前位置是要读写的下一个元素的索引
界限是缓冲区中最后一个有效位置之后下一个位置的索引值
容量是缓冲区能够容纳的元素的数量,一般来说界限等于容量。
对于标记、位置、限制和容量值遵守以下不变式:0 <= 标记 <= 位置 <= 限制 <= 容量
方法列表:
方法 | 描述 |
Object array() | 返回此缓冲区的底层实现数组 |
int arrayOffset() | 返回此缓冲区的底层实现数组中第一个元素的索引 |
int capacity() | 返回此缓冲区的容量 |
Buffer clear() | 清除此缓冲区并返回缓冲区的引用 |
Buffer flip() | 将缓冲区的界限设置为当前位置,并将当前位置重置为0,即反转缓冲区 |
boolean hasArray() | 返回缓冲区是否具有可访问的底层实现数组。 |
boolean hasRemaining() | 返回缓冲区中是否还有剩余元素 |
boolean isDirect() | 返回此缓冲区是否是直接缓冲区(直接缓冲区可以直接对缓冲区进行IO) |
boolean isReadOnly() | 该缓冲区是否只读 |
int limit() | 返回缓冲区的界限 |
Buffer limit(int n) | 将缓冲区的界限设置为n |
Buffer mark() | 设置标记 |
int position() | 返回此缓冲区的位置 |
Buffer position(int n) | 将缓冲区的当前位置设置为n |
int remaining() | 返回当前位置与界限之间的元素数量(即界限减去当前位置的结果值) |
Buffer reset() | 将缓冲区的位置重置为之前设置标记的位置 |
Buffer rewind() | 将缓冲区的位置设置为0 |
清除、反转、和重绕
这三个词是在查阅JDK文档看到的,对应Buffer类的三个方法,个人觉得非常有助于理解。
clear()使缓冲区为一系列新的通道读取或相对放置 操作做好准备:它将限制设置为容量大小,将位置设置为 0。
flip()使缓冲区为一系列新的通道写入或相对获取 操作做好准备:它将限制设置为当前位置,然后将位置设置为 0。
rewind()使缓冲区为重新读取已包含的数据做好准备:它使限制保持不变,将位置设置为 0。
数据传输
下面这些特定的缓冲区类派生字Buffer,这些类的名称暗含了他们所能容纳的数据类型:
ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、MappedByteBuffer、ShortBuffer
其中 MappedByteBuffer是ByteBuffer的子类,用于将文件映射到缓冲区。
所有的缓冲区类都定义的有get()和put()方法,用于存取数据。(当然,如果缓冲区是只读的,就不能使用put操作)
通道
通道的用处
通道,表示到实体,如硬件设备、文件、网络套接字或可以执行一个或多个不同 I/O 操作(如读取或写入)的程序组件的开放的连接,用于 I/O 操作的连接。
通过通道,可以读取和写入数据。拿 NIO与原来的I/O 做个比较,通道就像是流,但它是面向缓冲区的。
正如前面提到的,所有数据都通过 Buffer 对象来处理。你永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而 通道 可以用于读、写或者同时用于读写。
通道实现了Channel接口并且扩展了Closeable接口和AutoCloseable接口,通过实现AutoCloseable接口,就可以使用带资源的try语句管理通道,那么当通道不再需要时会自动关闭。
获取通道
获取通道的一种方式是对支持通道的对象调用getChannel()方法。
例如,以下IO类支持getChannel()方法:
DatagramSocket、FileInputStream、FileOutputStream、RandomAccessFile、ServerSocket、Socket
根据调用getChannel()方法的对象类型返回特定类型的通道,比如对FileInputStream、FileOutputStream或RandomAccessFile对象调用getChannel()方法时,会返回FileChannel类型的通道,对Socket对象调用getChannel()方法时,会返回SocketChannel类型的通道。
通道都支持各种read()和write()方法,使用这些方法可以通过通道执行IO操作。
方法如下:
方法 | 描述 |
int read(ByteBuffer b) | 将字节读取到缓冲区,返回实际读取的字节数 |
int read(ByteBuffer b,long start) | 从start指定的文件位置开始,从通道读取字节,并写入缓冲区 |
int write(ByteBuffer b) | 将字节从缓冲区写入通道 |
int write(ByteBuffer b,long start) | 从start指定的文件位置开始,将字节从缓冲区写入通道 |
字符集和选择器
NIO使用的另外两个实体是字符集和选择器。
字符集定义了将字节映射为字符的方法,可以使用编码器将一系列字符编码成字节,使用解码器将一系列字节解码成字符。
字符集、编码器和解码器由java.nio.charset包中定义的类支持,因为提供了默认的编码器和解码器,所以通常不需要显式的使用字符集进行工作。
选择器支持基于键的,非锁定的多通道IO,也就是说,它可以通过多个通道执行IO,当然,前提是通道需要调用register方法注册到选择器中,
选择器的应用场景在基于套接字的通道。
Path接口
Path是JDK1.7新增进来的接口,该接口封装了文件的路径。
因为Path是接口,不是类,所以不能通过构造函数直接创建Path实例,通常会调用Paths.get()工厂方法来获取Path实例。
get()方法有两种形式:
Path get(String pathname,String ...more)
Path get(URI uri)
创建链接到文件的Path对象不会导致打开或创建文件,理解这一点很重要,这仅仅只是创建了封装文件目录路径的对象而已。
以下代码示例常用用法(1.txt是一个不存在的文件):
Path path = Paths.get("./nio/src/1.txt"); System.out.println("自身路径:"+path.toString());//输出.\nio\src\1.txt System.out.println("文件或目录名称:"+path.getFileName());//输出1.txt System.out.println("路径元素数量:"+path.getNameCount());//输出4 System.out.println("路径中第3截:"+path.getName(2));//输出src System.out.println("父目录的路径"+path.getParent());//输出.\nio\src System.out.println(path.getRoot());//输出null System.out.println("是否绝对路径:"+path.isAbsolute());//输出false Path p = path.toAbsolutePath();//返回与该路径等价的绝对路径 System.out.println("看看我这个是不是绝对路径:"+p.toString());//输出E:\JAVA\java_learning\.\nio\src\1.txt File file = path.toFile();//从该路径创建一个File对象 System.out.println("文件是否存在:"+file.exists());//false Path path1 = file.toPath();//再把File对象转成Path对象 System.out.println("是不是同一个对象:"+path1.equals(path));//输出true
为基于通道的IO使用NIO
通过通道读取文件
手动分配缓冲区
这是最常用的方式,手动分配一个缓冲区,然后执行显式的读取操作,读取操作使用来自文件的数据加载缓冲区。
try(FileChannel seekableByteChannel = (FileChannel) Files.newByteChannel(Paths.get("./nio/src/2.txt"))){ ByteBuffer buffer = ByteBuffer.allocate(5);//指定缓冲区大小 int count = seekableByteChannel.read(buffer);//将文件中的数据读取到缓冲区 buffer.rewind(); while (count > 0){ System.out.println((char)buffer.get());//读取缓冲区中的数据 count --; } }catch (Exception e){ e.printStackTrace(); }
该示例使用了SeekableByteChannel对象,该对象封装了文件操作的通道,可以转成FileChannel(不是默认的文件系统不能转)。这里注意,分配缓冲区大小就代表了最多读取的数据字节大小,比如我的示例文件中字节数是8个,但是我只分配了5个字节的缓冲区,因此只能读出前5个字节的数据。
为什么会有buffer.rewind()这行代码呢?因为调用了read()方法将文件内容读取到缓冲区后,当前位置处于缓冲区的末尾,所以要重绕缓冲区,将指针重置到缓冲区的起始位置。
将文件映射到缓冲区
这种方式的优点是缓冲区自动包含文件的内容,不需要显式的读操作。同样的要先获取Path对象,再获取文件通道。
用newByteChannel()方法得到的SeekableByteChannel对象转成FileChannel类型的对象,因为FileChannel对象有map()方法,将通道映射到缓冲区。
map()方法如下所示:
MappedByteBuffer map(FileChannel.MapMode how,long begin,long size) throws IOException
参数how的值为:MapMode.READ_ONLY、MapMode.READ_WRITE、MapMode.PRIVATE 之一。
映射的开始位置由begin指定,映射的字节数由size指定,作为MappedByteBuffer返回指向缓冲区的引用,MappedByteBuffer是ByteBuffer的子类,一旦将文件映射到缓冲区,就可以从缓冲区读取文件了。
try(FileChannel fileChannel = (FileChannel) Files.newByteChannel(Paths.get("./nio/src/2.txt"))){ long size = fileChannel.size();//获取文件字节数量 MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY,0,size); for(int i=0;i < size; i ++){ System.out.println((char)mappedByteBuffer.get()); } }catch (Exception e){ e.printStackTrace(); }
通过通道写入文件
手动分配缓冲区
try(FileChannel seekableByteChannel = (FileChannel) Files.newByteChannel(Paths.get("./nio/src/2.txt"),StandardOpenOption.WRITE,StandardOpenOption.CREATE,StandardOpenOption.APPEND)){ ByteBuffer buffer = ByteBuffer.allocate(5);//指定缓冲区大小 for(int i=0;i<5;i++){ buffer.put((byte)('A'+i)); } buffer.rewind(); seekableByteChannel.write(buffer); }catch (Exception e){ e.printStackTrace(); }
因为是针对写操作而打开文件,所以参数必须指定为StandardOpenOption.WRITE,如果希望文件不存在就创建文件,可以指定StandardOpenOption.CREATE,但是我还希望是以追加的形式写入内容,所以又指定了StandardOpenOption.APPEND。
需要注意的是buffer.put()方法每次调用都会向前推进当前位置,所以在调用write()方法之前,需要将当前位置重置到缓冲区的开头,如果没有这么做,write()方法会认为缓冲区中没有数据。
将文件映射到缓冲区
Path path = Paths.get("./nio/src/4.txt"); try(FileChannel fileChannel = (FileChannel) Files.newByteChannel(path,StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE)){ MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE,0,5); for(int i=0;i < 5; i ++){ buffer.put((byte) ('A'+i)); } }catch (Exception e){ e.printStackTrace(); }
可以看出,对于通道自身并没有显式的写操作,因为缓冲区被映射到文件,所以对缓冲区的修改会自动反映到底层文件中。
映射缓冲区要么是只读,要么是读/写,所以这里必须是READ和WRITE两个选项都得要。一旦将文件映射到缓冲区,就可以向缓冲区中写入数据,并且这些数据会被自动写入文件,所以不需要对通道执行显式的写入操作。
另外,写入的文件大小不能超过缓冲区的大小,如果超过了之后会抛出异常,但是已经写入的数据仍然会成功。比如缓冲区5个字节,我写入10个字节,程序会抛出异常,但是前5个字节仍然会写入文件中。
使用NIO复制和移动文件
Path path = Paths.get("./nio/src/4.txt"); Path path2 = Paths.get("./nio/src/40.txt"); try{ Files.copy(path2,path, StandardCopyOption.REPLACE_EXISTING); //Files.move(path,path2, StandardCopyOption.REPLACE_EXISTING); }catch (Exception e){ e.printStackTrace(); }
StandardCopyOption.REPLACE_EXISTING选项的意思是如果目标文件存在则替换。
为基于流的IO使用NIO
如果拥有Path对象,那么可以通过调用Files类的静态方法newInputStream()或newOutputStream()来得到连接到指定文件的流。
方法原型如下:
static InputStream newInputStream(Path path,OpenOption... how) throws IOException
how的参数值必须是一个或多个由StandardOpenOption定义的值,如果没有指定选项,默认打开方式为StandardOpenOption.READ。
一旦打开文件,就可以使用InputStream定义的任何方法。
因为newInputStream()方法返回的是常规流,所以也可以在缓冲流中封装流。
try(BufferedInputStream inputStream = new BufferedInputStream(Files.newInputStream(Paths.get("./nio/src/2.txt")))){ int s = inputStream.available(); for(int i=0;i<s;i++){ int c = inputStream.read(); System.out.print((char) c); } }catch (Exception e){ e.printStackTrace(); }
OutputStream和前面的InputStream类似:
try(BufferedOutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(Paths.get("./nio/src/2.txt")))){ for(int i=0;i<26;i++){ outputStream.write((byte)('A'+i)); } }catch (Exception e){ e.printStackTrace(); }
为基于文件系统使用NIO
Files类
要进行操作的文件是由Path指定的,但是对文件执行的许多操作都是由Files类中的静态方法提供的。
java.nio.file.Files类就是为了替代java.io.File类而生。
以下列出部分常用方法:
方法 | 描述 |
static Path copy(Path from,Path to,CopyOption... opts) | 将from复制到to,返回to |
static Path move(Path from,Path to,CopyOption... opts) | 将from移动到to,返回to |
static Path createDirectory(Path path,FileAttribute<?> attribs) | 创建一个目录,目录属性是由attribs指定的。 |
static Path createFile(Path path,FileAttribute<?> attrbs) | 创建一个文件,文件属性是由attribs指定的。 |
static void delete(Path path) | 删除一个文件 |
static boolean exists(Path path) | path代表的路径是否存在(无论文件还是目录) |
static boolean notExists(Path path) | path代表的路径是否不存在(无论文件还是目录) |
static boolean isRegularFile(Path path) | 是否是文件 |
static boolean isDirectory(Path path) | 是否是目录 |
static boolean isExecutable(Path path) | 是否是可执行文件 |
static boolean isHidden(Path path) | 是否是隐藏文件 |
static boolean isReadable(Path path) | 是否可读 |
static boolean isWritable(Path path) | 是否可写 |
static long size(Path path) | 返回文件大小 |
static SeekableByteChannel newByteChannel(Path path,OpenOption... opts) | 打开文件,opts指定打开方式,返回一个通道对象 |
static DirectoryStream<Path> newDirectoryStream(Path path) | 打开目录,返回一个目录流 |
static InputStream newInputStream(Path path,OpenOption... opts) | 打开文件,返回一个输入流 |
static OutputStream newOutputStream(Path path,OpenOption... opts) | 打开文件,返回一个输出流 |
参数列表中出现的有类型为OpenOption的参数,它是一个接口,真实传入的参数是StandardOpenOption类中的枚举,这个枚举参数与newBufferedWriter/newInputStream/newOutputStream/write方法一起使用。
StandardOpenOption类中的枚举 | 描述 |
READ | 用于读取打开文件 |
WRITE | 用于写入打开文件 |
APPEND | 如果是写入,则内容追加到末尾 |
CREATE | 自动在文件不存在的情况下创建新文件 |
CREATE_NEW | 创建新文件,如果文件已存在则抛出异常 |
DELETE_ON_CLOSE | 当文件被关闭时删除文件 |
DSYNC | 对文件内容的修改被立即写入物理设备 |
SYNC | 对文件内容或元数据的修改被立即写入物理设备 |
TRUNCATE_EXISTING | 如果用于写入而打开,那么移除已有内容 |
下面演示追加写入文件操作:
try{ Path path = Paths.get("./nio/src/8.txt"); String str = "今天天气不错哦\n"; Files.write(path,str.getBytes(),StandardOpenOption.CREATE, StandardOpenOption.APPEND); }catch (Exception e){ e.printStackTrace(); }
目录流
遍历目录
如果Path中的路径是目录,那么可以使用Files类的静态方法newDirectoryStream()来获取目录流。
方法原型如下:
static DirectoryStream<Path> newDirectoryStream(Path dir) throw IOException
调用此方法的前提是目标必须是目录,并且可读,否则会抛异常。
try(DirectoryStream<Path> paths = Files.newDirectoryStream(Paths.get("./nio/src"))){ for(Path path : paths){ System.out.println(path.getFileName()); } }catch (Exception e){ e.printStackTrace(); }
DirectoryStream<Path>实现了Iterable<Path>,所以可以用foreach循环对其进行遍历,但是它实现的迭代器针对每个实例只能获取一次,所以只能遍历一次。
匹配内容
Files.newDirectoryStream方法还有一种形式,可以传入匹配规则:
static DirectoryStream<Path> newDirectoryStream(Path dir,String glob) throws IOException
第二个参数就是匹配规则,但是它不支持强大的正则,只支持简单的匹配,如"?"代表任意1个字符,"*"代表任意个任意字符。
使用示例 匹配所有.java结尾的文件:
try(DirectoryStream<Path> paths = Files.newDirectoryStream(Paths.get("./nio/src"),"*.java")){ for(Path path : paths){ System.out.println(path.getFileName()); } }catch (Exception e){ e.printStackTrace(); }
复杂匹配
这种方式的原型为:
static DirectoryStream<Path> newDirectoryStream(Path dir,DirectoryStream.Filter<? super Path> filter) throws IOException
其中的DirectoryStream.Filter是定义了以下方法的接口:
boolean accept(T entry) throws IOException
这个方法中如果希望匹配entry就返回true,否则就返回false,这种形式的优点是可以基于文件名之外的其他内容过滤,比如说,可以只匹配目录、只匹配文件、匹配文件大小、创建日期、修改日期等各种属性。
下面是匹配文件大小的示例:
String dirname = "./nio/src"; DirectoryStream.Filter<Path> filter = (entry)->{ if(Files.size(entry) > 25){ return true; } return false; }; try(DirectoryStream<Path> paths = Files.newDirectoryStream(Paths.get(dirname),filter)){ for(Path path : paths){ System.out.println(path.getFileName()); } }catch (Exception e){ e.printStackTrace(); }
目录树
遍历目录下的所有资源以往的做法都是用递归来实现,但是在NIO.2的时候提供了walkFileTree方法,使得遍历目录变得优雅而简单,其中涉及4个方法,根据需求选择重写。
示例如下:
String dir = "./nio"; try{ Files.walkFileTree(Paths.get(dir), new SimpleFileVisitor<Path>(){ @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { System.out.println("正在访问文件:"+file); return super.visitFile(file, attrs); } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { System.out.println("正在访问目录:"+dir); return super.preVisitDirectory(dir, attrs); } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { System.out.println("访问失败的文件:"+file); return super.visitFileFailed(file, exc); } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { System.out.println("这个目录访问结束了:"+dir); return super.postVisitDirectory(dir, exc); } }); }catch (Exception e){ e.printStackTrace(); }
打印结果如图:
文件加锁机制
JDK1.4引入的文件加锁机制,要锁定一个文件,可以调用FileChannel类的lock或tryLock方法
FileChannel channel = FileChannel.open(path);
FileLock lock = channel.lock() 或者 FileLock lock1 = channel.tryLock()
第一个调用会阻塞直到获得锁,第二个调用立刻就会返回 要么返回锁 要么返回Null。
获得锁后这个文件将保持锁定状态,直到这个通道关闭,或者释放锁:lock.release(); 点进源码可以轻易发现,FileChannel实现了AutoCloseable接口,也就是说,可以通过try语句来自动管理资源,不需要手动释放锁。
还可以锁定文件内容的一部分:
FileLock lock(long start,long size,boolean shared) FileLock lock(long start,long size,boolean shared)
锁定区域为(从start到start+size),那么在start+size之外的部分不会被锁定。shared参数为布尔值,代表是否是读锁,读锁就是共享锁,写锁就是排他锁。
源码分享
https://gitee.com/zhao-baolin/java_learning/tree/master/nio