# NIO 新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。 NIO核心组件: - 通道(Channels) - 缓冲区(Buffers) - 选择器(Selectors) ## 流与块 I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。 面向流的 I/O 一次处理一个**字节**数据:一个输入流产生一个字节数据,一个输出流消费一个字节数据。 为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。 面向块的 I/O 一次处理一个**数据块**,按块处理数据比按流处理数据要快得多。 但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。 I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。 例如,java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。 ## 通道与缓冲区 ### 1. 通道 通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。 通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而通道是**双向**的,可以用于读、写或者同时用于读写。 通道包括以下类型: - FileChannel:从文件中读写数据; - DatagramChannel:通过 UDP 读写网络中数据; - SocketChannel:通过 TCP 读写网络中数据; - ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。 ### 2. 缓冲区 发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。 缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。 缓冲区包括以下类型: - ByteBuffer - CharBuffer - ShortBuffer - IntBuffer - LongBuffer - FloatBuffer - DoubleBuffer ## 缓冲区状态变量 - capacity:最大容量; - position:当前已经读写的字节数; - limit:还可以读写的字节数。 状态变量的改变过程举例: ① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。

② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。

③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。

④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。

⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。

## 文件 NIO 实例 ### FileChannel的使用 1. 开启FileChannel 2. 从FileChannel读取数据/写入数据 3.关闭FileChannel ```java public class FileChannelDemo { public static void main(String[] args) throws IOException { //1.创建一个RandomAccessFile(随机访问文件)对象通过RandomAccessFile对象的getChannel()方法。 RandomAccessFile raf=new RandomAccessFile("demo6.txt","rw"); FileChannel fc=raf.getChannel(); //使用FileChannel的read()方法读取数据: ByteBuffer byteBuffer=ByteBuffer.allocate(1024); int bys=fc.read(byteBuffer); //使用FileChannel的write()方法写入数据: ByteBuffer byteBuffer2=ByteBuffer.allocate(1024); byteBuffer2.put("hello".getBytes()); fc.write(byteBuffer2); //3.关闭FileChannel fc.close(); } } ``` - 以下展示了使用 NIO 快速复制文件的实例: ```java public class CopyFile { public static void main(String[] args) throws IOException { String srcFile="国旗歌.mp4"; String destFile="demo3.mp4"; long start = System.currentTimeMillis(); //copyFile(srcFile,destFile); //共耗时:75309毫秒 //copyFile2(srcFile,destFile); //共耗时:153毫秒 //copyFile3(srcFile,destFile);//共耗时:282毫秒 //copyFile4(srcFile,destFile);//共耗时:44毫秒 copyFile5(srcFile,destFile);//共耗时:共耗时:113毫秒 long end = System.currentTimeMillis(); System.out.println("共耗时:" + (end - start) + "毫秒"); } /** * 基本字节流一次读写一个字节 */ public static void copyFile(String srcFile,String destFile) throws IOException { FileInputStream fis=new FileInputStream(srcFile); FileOutputStream fos=new FileOutputStream(destFile); int by=0; while((by=fis.read())!=-1){ fos.write(by); } fis.close(); fos.close(); } /** * 基本字节流一次读写一个字节数组 */ public static void copyFile2(String srcFile,String destFile) throws IOException{ FileInputStream fis=new FileInputStream(srcFile); FileOutputStream fos=new FileOutputStream(destFile); int len=0; byte[] bys=new byte[1024]; while((len=fis.read(bys))!=-1){ fos.write(bys,0,len); } fis.close(); fos.close(); } /** * 高效字节流一次读写一个字节 */ public static void copyFile3(String srcFile,String destFile) throws IOException{ BufferedInputStream bis=new BufferedInputStream(new FileInputStream(srcFile)); BufferedOutputStream bos=new BufferedOutputStream(new FileOutputStream(destFile)); int by=0; while((by=bis.read())!=-1){ bos.write(by); } bis.close(); bos.close(); } /** * 高效字节流一次读写一个字节数组 */ public static void copyFile4(String srcFile,String destFile) throws IOException{ BufferedInputStream bis=new BufferedInputStream(new FileInputStream(srcFile)); BufferedOutputStream bos=new BufferedOutputStream(new FileOutputStream(destFile)); int len=0; byte[] bys=new byte[1024]; while((len=bis.read(bys))!=-1){ bos.write(bys,0,len); } bis.close(); bos.close(); } /** * 使用FileChannel复制文件 */ public static void copyFile5(String srcFile,String destFile) throws IOException{ FileInputStream fis=new FileInputStream(srcFile); //获取输入字节流的文件通道 FileChannel fcin=fis.getChannel(); FileOutputStream fos=new FileOutputStream(destFile); //获取输出字节流的文件通道 FileChannel fcout=fos.getChannel(); //为缓冲区分配 1024 个字节 ByteBuffer buffer = ByteBuffer.allocateDirect(1024); while(true){ //从输入通道中读取数据到缓冲区中 int r = fcin.read(buffer); // read() 返回 -1 表示 EOF if(r==-1){ break; } //切换读写 buffer.flip(); //把缓冲区的内容写入输出文件中 fcout.write(buffer); //清空缓冲区 buffer.clear(); } } } ``` ### SocketChannel和ServerSocketChannel的使用 SocketChannel用于创建基于TCP协议的客户端对象,因为SocketChannel中不存在accept()方法, 所以,它不能成为一个服务端程序。 通过**connect()方法**,SocketChannel对象可以连接到其他TCP服务器程序。 ServerSocketChannel允许我们监听TCP协议请求,通过ServerSocketChannel的**accept()**方法创建一个SocketChannel对象用户从客户端读/写数据。 - 服务端: 1. 通过ServerSocketChannel 绑定ip地址和端口号 2. 通过ServerSocketChannel的accept()方法创建一个SocketChannel对象用户从客户端读/写数据 3. 创建读数据/写数据缓冲区对象来读取客户端数据或向客户端发送数据 4. 关闭SocketChannel和ServerSocketChannel ```java public class Server { public static void main(String[] args) throws IOException { //通过ServerSocketChannel 的open()方法创建一个ServerSocketChannel对象 ServerSocketChannel ssc=ServerSocketChannel.open(); //1. 通过ServerSocketChannel 绑定ip地址和端口号 ssc.socket().bind(new InetSocketAddress(InetAddress.getByName("LAPTOP-D9966H06"),8888)); //2. 通过ServerSocketChannel的accept()方法创建一个SocketChannel对象用户从客户端读/写数据 SocketChannel sc=ssc.accept(); //3. 创建读数据/写数据缓冲区对象来读取客户端数据或向客户端发送数据 //读取客户端发送的数据 ByteBuffer buffer=ByteBuffer.allocate(1024); //从通道中读取数据到缓冲区 sc.read(buffer); StringBuffer sb=new StringBuffer(); buffer.flip(); while(buffer.hasRemaining()){ sb.append((char)buffer.get()); } System.out.println(sb.toString()); ByteBuffer buffer2=ByteBuffer.allocate(1024); //向客户端发送数据 buffer2.put("data has been received.".getBytes()); buffer2.flip(); sc.write(buffer2); //4. 关闭SocketChannel和ServerSocketChannel sc.close(); ssc.close(); } } ``` - 客户端: 1.通过SocketChannel连接到远程服务器 2.创建读数据/写数据缓冲区对象来读取服务端数据或向服务端发送数据 3.关闭SocketChannel ```java public class Client { public static void main(String[] args) throws IOException { //1.通过SocketChannel连接到远程服务器 SocketChannel sc=SocketChannel.open(); sc.connect(new InetSocketAddress(InetAddress.getByName("LAPTOP-D9966H06"),8888)); //2.创建读数据/写数据缓冲区对象来读取服务端数据或向服务端发送数据 //向通道中写入数据 ByteBuffer buffer=ByteBuffer.allocate(1024); buffer.put("hello".getBytes()); buffer.flip(); sc.write(buffer); //读取从客户端中获取的数据 ByteBuffer buffer2=ByteBuffer.allocate(1024); sc.read(buffer2); StringBuffer sb=new StringBuffer(); buffer2.flip(); while(buffer2.hasRemaining()){ sb.append((char)buffer2.get()); } System.out.println(sb.toString()); //3.关闭SocketChannel sc.close(); } } ``` ### DatagramChannel的使用 DataGramChannel,类似于java 网络编程的DatagramSocket类; **使用UDP进行网络传输**, UDP是无连接,面向数据报文段的协议。 - 服务端: ```java public class Server { public static void main(String[] args) throws IOException { DatagramChannel dc= DatagramChannel.open(); dc.bind(new InetSocketAddress(InetAddress.getByName("LAPTOP-D9966H06"),8888)); //创建读数据/写数据缓冲区对象来读取客户端数据或向客户端发送数据 //读取客户端发送的数据 ByteBuffer buffer=ByteBuffer.allocate(1024); //从通道中读取数据到缓冲区 dc.receive(buffer); StringBuffer sb=new StringBuffer(); buffer.flip(); while(buffer.hasRemaining()){ sb.append((char)buffer.get()); } System.out.println(sb.toString()); ByteBuffer buffer2=ByteBuffer.allocate(1024); //向客户端发送数据 buffer2.put("data has been received.".getBytes()); buffer2.flip(); dc.send(buffer2,new InetSocketAddress(InetAddress.getByName("LAPTOP-D9966H06"),9999)); dc.close(); } } ``` - 客户端: ```java public class Client { public static void main(String[] args) throws IOException { DatagramChannel dc= DatagramChannel.open(); dc.bind(new InetSocketAddress(InetAddress.getByName("LAPTOP-D9966H06"),9999)); //创建读数据/写数据缓冲区对象来读取服务端数据或向服务端发送数据 //向通道中写入数据 ByteBuffer buffer=ByteBuffer.allocate(1024); buffer.put("hello".getBytes()); buffer.flip(); dc.send(buffer,new InetSocketAddress(InetAddress.getByName("LAPTOP-D9966H06"),8888)); //读取从客户端中获取的数据 ByteBuffer buffer2=ByteBuffer.allocate(1024); dc.receive(buffer2); StringBuffer sb=new StringBuffer(); buffer2.flip(); while(buffer2.hasRemaining()){ sb.append((char)buffer2.get()); } System.out.println(sb.toString()); dc.close(); } } ``` ### 通道之间的数据传输 在Java NIO中如果一个channel是FileChannel类型的,那么他可以直接把数据传输到另一个channel。 ```java transferFrom() :transferFrom方法把数据从通道源传输到FileChannel transferTo() :transferTo方法把FileChannel数据传输到另一个FileChhannel ``` ```java public static void copyFile6(String srcFile,String destFile) throws IOException { FileInputStream fis = new FileInputStream(srcFile); //获取输入字节流的文件通道 FileChannel fcin = fis.getChannel(); FileOutputStream fos = new FileOutputStream(destFile); //获取输出字节流的文件通道 FileChannel fcout = fos.getChannel(); //fcin通道中读出count bytes ,并写入fcout通道中 //fcin.transferTo(0,fcin.size(),fcout); //或者 fcout.transferFrom(fcin,0,fcin.size()); } ``` ## 选择器 NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。 NIO 实现了 IO 多路复用中的 **Reactor 模型**,一个线程 Thread 使用一个选择器 Selector 通过**轮询的方式** 去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。 通过配置监听的通道 Channel 为**非阻塞**,那么当 Channel 上的 IO 事件还未到达时, 就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。 因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件, 对于 IO 密集型的应用具有很好地性能。 应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能, 为 FileChannel 配置非阻塞也没有意义。

使用Selector的优点: 使用更少的线程来就可以来处理通道了, 相比使用多个线程, 避免了线程上下文切换带来的开销。 ### 1. 创建选择器 ```java Selector selector = Selector.open(); ``` ### 2. 将通道注册到选择器上 ```java ServerSocketChannel ssChannel = ServerSocketChannel.open(); ssChannel.configureBlocking(false);//通道必须配置为非阻塞模式 ssChannel.register(selector, SelectionKey.OP_ACCEPT); ``` 通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。 在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类: - SelectionKey.OP_CONNECT - SelectionKey.OP_ACCEPT - SelectionKey.OP_READ - SelectionKey.OP_WRITE 它们在 SelectionKey 的定义如下: ```java public static final int OP_READ = 1 << 0; public static final int OP_WRITE = 1 << 2; public static final int OP_CONNECT = 1 << 3; public static final int OP_ACCEPT = 1 << 4; ``` 可以看出每个事件可以被当成一个位域,从而组成事件集整数。例如: ```java int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE; ``` ### 3. 监听事件 ```java int num = selector.select(); ``` 使用 select() 来监听到达的事件,它会**一直阻塞直到有至少一个事件到达**。 ### 4. 获取到达的事件 ```java Set keys = selector.selectedKeys(); Iterator keyIterator = keys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isAcceptable()) { // ... } else if (key.isReadable()) { // ... } keyIterator.remove(); } ``` ### 5. 事件循环 因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。 ```java while (true) { int num = selector.select(); Set keys = selector.selectedKeys(); Iterator keyIterator = keys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isAcceptable()) { // ... } else if (key.isReadable()) { // ... } keyIterator.remove(); } } ``` ## Reactor 模型 我们知道,在我们使用传统方法进行网络IO操作的时候,需要使用一个线程监听IO事件,当事件到达之后,创建一个线程去处理接收到的IO事件,这种模型需要创建大量的线程,会极大的浪费内存等资源。 这种情况下,有人提出了Reactor模式,在Reactor中,拆分为不同的小线程或者子过程,这些被拆分的小线程和子过程对应的是handler,每一种handler都会处理一种事件(event)。这个需要一个全局的管理者selector,向channel注册感兴趣的事件,selector不断在channel中监测是否有该事件发生,如果没有,那么主线程会被阻塞,否则会调用相应的事件处理函数来处理。这些事件典型的有连接、读取、写入,我们需要为这些事件分别提供处理器,事件到达后分发到处理器中就可以返回处理后面的事件,吞吐量能够极大的提高。其中定义了以下三种角色: - Reactor:负责将IO事件分派给指定的处理器(Handler) - Acceptor:负责处理新的连接,并将请求移交给Reactor - Handler:负责读写的处理器 ### 单Reactor单线程模型 这是最基本的单Reactor单线程模型。其中Reactor线程,负责多路复用socket,有新连接到来触发事件之后,交由Acceptor进行处理,有IO读写事件之后交给hanlder 处理。 Acceptor主要任务就是构建handler ,在获取到和client相关的SocketChannel之后 ,绑定到相应的hanlder上,对应的SocketChannel有读写事件之后,基于reactor 分发,hanlder就可以处理了(所有的IO事件都绑定到selector上,有Reactor分发)。 该模型 适用于处理器链中业务处理组件能快速完成的场景。不过,这种单线程模型不能充分利用多核资源,所以实际使用的不多。
### 单Reactor多线程模型 相对于第一种单线程的模式来说,在处理业务逻辑,也就是获取到IO的读写事件之后,交由**线程池**来处理,这样可以减小主reactor的性能开销,从而更专注的做事件分发工作了,从而提升整个应用的吞吐量。
### 多Reactor多线程模型 第三种模型比起第二种模型,是将Reactor分成两部分: - mainReactor:负责监听server socket,用来**处理新连接的建立**,将建立的socketChannel指定注册给subReactor; - subReactor:维护自己的selector, accept会将连接交给它来处理,读写(read、send)网络数据,对于业务逻辑的处理,将其扔给线程池中的worker来处理。 在该模型中,mainReactor 主要是用来处理网络IO 连接建立操作,通常一个线程就可以处理,而subReactor主要做和建立起来的socket做数据交互和事件业务处理操作,它的个数上一般是和CPU个数等同,一个subReactor对应一个线程处理。 此种模型中,每个模块的工作更加专一,耦合度更低,性能和稳定性也大量的提升,支持的可并发客户端数量可达到上百万级别。关于此种模型的应用,目前有很多优秀的框架已经应用,比如Netty。
### 优缺点 **优点:** 1. 响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的 2. 编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销 3. 可扩展性,可以方便的通过增加Reactor实例个数来充分利用CPU资源 4. 可复用性,reactor框架本身与具体事件处理逻辑无关,具有很高的复用性 **缺点:** 1. 相比传统的简单模型,Reactor增加了一定的复杂性,因而有一定的门槛,并且不易于调试 2. Reactor模式需要底层的Synchronous Event Demultiplexer支持,比如Java中的Selector支持,操作系统的select系统调用支持,如果要自己实现Synchronous Event Demultiplexer可能不会有那么高效 3. Reactor模式在IO读写数据时还是在同一个线程中实现的,即使使用多个Reactor机制的情况下,那些共享一个Reactor的Channel如果出现一个长时间的数据读写,会影响这个Reactor中其他Channel的相应时间,比如在大文件传输时,IO操作就会影响其他Client的相应时间,因而对这种操作,使用传统的Thread-Per-Connection或许是一个更好的选择,或则此时使用Proactor模式 ## 套接字 NIO 实例 ```java public class NIOServer { public static void main(String[] args) throws IOException { //1. 创建选择器 Selector selector = Selector.open(); //2.将通道注册到选择器上 ServerSocketChannel ssChannel = ServerSocketChannel.open(); ssChannel.configureBlocking(false); //通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了 ssChannel.register(selector, SelectionKey.OP_ACCEPT); ServerSocket ss=ssChannel.socket(); ss.bind(new InetSocketAddress("127.0.0.1",8888)); while (true){ //3. 监听事件 selector.select(); //4. 获取到达的事件 Set keys = selector.selectedKeys(); Iterator keyIterator = keys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isAcceptable()) { ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel(); // 服务器会为每个新连接创建一个 SocketChannel SocketChannel sChannel = ssChannel1.accept(); sChannel.configureBlocking(false); // 这个新连接主要用于从客户端读取数据 sChannel.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { SocketChannel sChannel = (SocketChannel) key.channel(); System.out.println(readDataFromSocketChannel(sChannel)); sChannel.close(); } keyIterator.remove(); } } } private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException { ByteBuffer buffer = ByteBuffer.allocate(1024); StringBuilder data = new StringBuilder(); while (true) { buffer.clear(); int r = sChannel.read(buffer); if (r == -1) { break; } buffer.flip(); int limit = buffer.limit(); char[] dst = new char[limit]; for (int i = 0; i < limit; i++) { dst[i] = (char) buffer.get(i); } data.append(dst); buffer.clear(); } return data.toString(); } } ``` ```java public class NIOClient { public static void main(String[] args) throws IOException { Socket socket = new Socket("127.0.0.1", 8888); OutputStream out = socket.getOutputStream(); String s = "hello world"; out.write(s.getBytes()); out.close(); } } ``` ## 内存映射文件 内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。 向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。 下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。 ```java MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024); ``` ## NIO与IO对比 NIO 与普通 I/O 的区别主要有以下三点: - NIO 是非阻塞的; - NIO 面向块,I/O 面向流。 - NIO有选择器,而I/O没有。 ## Path Java7中文件IO发生了很大的变化,专门引入了很多新的类来取代原来的 基于java.io.File的文件IO操作方式。 ### 创建一个Path 使用Paths工具类的get()方法创建Path对象 ```java public class PathDemo { public static void main(String[] args) { //方式一 Path path=Paths.get("demo5.txt"); System.out.println(path); //方式二 Path path2 = FileSystems.getDefault().getPath("demo5.txt"); System.out.println(path2); } } ``` ### File和Path之间的转换,File和URI之间的转换 ```java public class PathDemo2 { public static void main(String[] args) { Path path=Paths.get("demo5.txt"); File file=path.toFile(); URI uri=path.toUri(); System.out.println(path); System.out.println(file); System.out.println(uri); } } ``` ```html demo5.txt demo5.txt file:///F:/Java_Review/05Java/JavaIO/demo5.txt ``` ### 获取Path的相关信息 ```java public class PathDemo3 { public static void main(String[] args) { Path path= Paths.get("demo3\\test3.txt"); System.out.println("文件名:"+ path.getFileName()); System.out.println("名称元素的数量:"+path.getNameCount()); System.out.println("父路径:"+ path.getParent()); System.out.println("根路径:"+ path.getRoot()); System.out.println("是否是绝对路径:"+path.isAbsolute()); //startWith() 参数既可以是字符串,也可以是Path System.out.println("是否是以路径demo3开头:"+path.startsWith(Paths.get("demo3"))); System.out.println("该路径的字符串形式:"+path.toString()); } } ``` ```html 文件名:test3.txt 名称元素的数量:2 父路径:demo3 根路径:null 是否是绝对路径:false 是否是以路径demo3开头:true 该路径的字符串形式:demo3\test3.txt ``` ### 移除Path中的冗余项 \\ .表示的是当前目录 ​ \\ ..表示父目录或者说是上一级目录 normalize() : 返回一个路径,该路径是取出冗余项的路径。 toRealPath() : 可以看成,先进行toAbsolutePath()操作,然后进行normalize()操作 ```java public class PathDemo4 { public static void main(String[] args) throws IOException { Path path= Paths.get("./demo3"); System.out.println("original :"+ path.toAbsolutePath()); System.out.println("after normalize:"+ path.toAbsolutePath().normalize()); System.out.println("after toRealPath:"+ path.toRealPath()); } } ``` ```html original :F:\Java_Review\05Java\JavaIO\.\demo3 after normalize:F:\Java_Review\05Java\JavaIO\demo3 after toRealPath:F:\Java_Review\05Java\JavaIO\demo3 ``` ```java public class PathDemo5 { public static void main(String[] args) throws IOException { Path path= Paths.get("../JavaIO"); System.out.println("original :"+ path.toAbsolutePath()); System.out.println("after normalize:"+ path.toAbsolutePath().normalize()); System.out.println("after toRealPath:"+ path.toRealPath()); } } ``` ```html original :F:\Java_Review\05Java\JavaIO\..\JavaIO after normalize:F:\Java_Review\05Java\JavaIO after toRealPath:F:\Java_Review\05Java\JavaIO ``` ## Files java.nio.file.Files类是和java.nio.file.Path相结合使用的 ### 检查给定的Path在文件系统中是否存在 Files.exists():检测文件路径是否存在 ```java public class FilesDemo { public static void main(String[] args) { Path path = Paths.get("demo5.txt"); //LinkOptions.NOFOLLOW_LINKS:表示检测时不包含符号链接文件。 boolean isExist= Files.exists(path,new LinkOption[]{LinkOption.NOFOLLOW_LINKS}); System.out.println(isExist); } } ``` ### 创建文件/文件夹 Files.createFile():创建文件 Files.createDirectory(): 创建文件夹 Files.createDirectories(): 创建文件夹 ```java public class FilesDemo2 { public static void main(String[] args) throws IOException { Path path= Paths.get("demo7.txt"); if(!Files.exists(path)){ Files.createFile(path); } Path path2=Paths.get("demo4"); if(!Files.exists(path2)){ Files.createDirectory(path2); } Path path3=Paths.get("demo5\\test"); if(!Files.exists(path3)){ Files.createDirectories(path3); } } } ``` ### 删除文件或目录 Files.delete():删除一个文件或目录 ```java public class FilesDemo3 { public static void main(String[] args) throws IOException { Path path= Paths.get("demo7.txt"); Files.delete(path); } } ``` ### 把一个文件从一个地址复制到另一个位置 Files.copy():把一个文件从一个地址复制到另一个位置 ```java public class FilesDemo4 { public static void main(String[] args) throws IOException { Path srcPath= Paths.get("demo6.txt"); Path destPath=Paths.get("demo7.txt"); //Files.copy(srcPath,destPath); //强制覆盖已经存在的目标文件 Files.copy(srcPath,destPath, StandardCopyOption.REPLACE_EXISTING); } } ``` ### 获取文件属性 ```java public class FilesDemo5 { public static void main(String[] args) throws IOException { Path path= Paths.get("demo7.txt"); System.out.println(Files.getLastModifiedTime(path)); System.out.println(Files.size(path)); System.out.println(Files.isSymbolicLink(path)); System.out.println(Files.isDirectory(path)); System.out.println(Files.readAttributes(path,"*")); } } ``` ### 遍历一个文件夹 ```java public class FilesDemo6 { public static void main(String[] args) throws IOException { Path path= Paths.get("demo3\\demo2"); DirectoryStream paths=Files.newDirectoryStream(path); for(Path p:paths){ System.out.println(p.getFileName()); } } } ``` ### 遍历整个文件目录 FileVisitor需要调用方自行实现,然后作为参数传入walkFileTree(); FileVisitor的每个方法会在遍历过程中被调用多次。 ```java public class FilesDemo7 { public static void main(String[] args) throws IOException { Path path= Paths.get("demo3\\demo2"); List paths=new ArrayList<>(); Files.walkFileTree(path,new FileVisitor(paths)); System.out.println("paths:"+paths); } private static class FileVisitor extends SimpleFileVisitor { private List paths; public FileVisitor(List paths){ this.paths=paths; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if(file.toString().endsWith(".txt")){ paths.add(file.getFileName()); } return super.visitFile(file, attrs); } } } ``` 输出结果: ```html paths:[a.txt, test2.txt, test.txt, test3.txt] ``` # 参考资料 - https://juejin.im/post/5b4570cce51d451984695a9b