【Java实战⑳】从IO到NIO:Java高并发编程的飞跃

目录

  • [一、NIO 与 IO 的深度剖析](#一、NIO 与 IO 的深度剖析)
    • [1.1 IO 的局限性](#1.1 IO 的局限性)
    • [1.2 NIO 核心特性](#1.2 NIO 核心特性)
    • [1.3 NIO 核心组件](#1.3 NIO 核心组件)
    • [1.4 NIO 适用场景](#1.4 NIO 适用场景)
  • [二、NIO 核心组件实战](#二、NIO 核心组件实战)
    • [2.1 Buffer 缓冲区](#2.1 Buffer 缓冲区)
    • [2.2 Channel 通道](#2.2 Channel 通道)
    • [2.3 Selector 选择器](#2.3 Selector 选择器)
    • [2.4 NIO 文件操作案例](#2.4 NIO 文件操作案例)
  • [三、NIO2.0 实战](#三、NIO2.0 实战)
    • [3.1 Path 类](#3.1 Path 类)
    • [3.2 Files 类](#3.2 Files 类)
    • [3.3 Files 类高级操作](#3.3 Files 类高级操作)
    • [3.4 NIO2.0 实战案例](#3.4 NIO2.0 实战案例)

一、NIO 与 IO 的深度剖析

在 Java 编程领域,输入输出(I/O)操作是与外部资源交互的基础,如文件、网络连接等。传统的 I/O 模型在处理简单场景时表现出色,但随着应用程序对性能和并发处理能力要求的不断提高,其局限性逐渐显现。Java NIO(New I/O)的出现,为开发者提供了一种更高效、更灵活的 I/O 处理方式,尤其在高并发和大数据传输场景中展现出显著优势。接下来,我们将深入探讨 NIO 与传统 IO 的区别,以及 NIO 的核心特性、组件和适用场景。

1.1 IO 的局限性

传统的 Java IO 是基于流(Stream)的操作,其主要特点是面向字节或字符序列,数据以顺序的方式从数据源读取或写入到目的地。这种方式在处理简单的 I/O 任务时非常直观和方便,但在高并发和大数据量处理场景下,暴露出了一些明显的局限性。

  • 阻塞问题:Java IO 是阻塞式的,当一个线程调用 read () 或 write () 方法时,该线程会被阻塞,直到有数据可读或数据完全写入。这意味着在 I/O 操作进行期间,线程无法执行其他任务,严重浪费了 CPU 资源。例如,在一个服务器应用中,如果同时有大量客户端连接,每个连接都需要一个独立的线程来处理 I/O 操作,那么随着连接数的增加,线程数量也会急剧增加,导致线程上下文切换开销增大,系统性能急剧下降。
  • 面向流的局限性:IO 是面向流的,流是单向的,要么是输入流,要么是输出流。这就限制了数据处理的灵活性,对于一些需要同时进行读写操作的场景,需要分别创建输入流和输出流,增加了代码的复杂性。此外,流操作是顺序的,无法随机访问数据,对于一些需要随机读写的场景,如文件的部分内容更新,处理起来比较困难。
  • 单线程处理能力有限:由于每个 I/O 操作都需要一个线程来处理,当并发连接数较多时,线程资源会被大量消耗,系统的可扩展性受到限制。而且,线程的创建和销毁也会带来一定的开销,进一步降低了系统的性能。

1.2 NIO 核心特性

Java NIO 旨在解决传统 IO 的局限性,提供了一系列新的特性,使其在高并发和大数据处理场景中表现出色。

  • 非阻塞特性:NIO 支持非阻塞 I/O 操作,当一个线程从通道请求数据时,如果没有数据可用,该线程不会被阻塞,而是立即返回。这使得一个线程可以管理多个通道,大大提高了系统的并发处理能力。例如,在一个网络服务器中,可以使用一个线程来监听多个客户端连接的事件,当有事件发生时,才对相应的连接进行处理,避免了线程的阻塞等待,提高了资源利用率。
  • 面向缓冲区:NIO 基于缓冲区(Buffer)进行数据操作,所有数据都要先写入缓冲区,然后再从缓冲区读取。缓冲区是一块连续的内存区域,提供了更灵活的数据处理方式,可以随机访问数据,并且支持数据的读写、标记、重置等操作。与面向流的 IO 相比,面向缓冲区的 NIO 在处理大数据量时效率更高,因为它减少了数据的拷贝次数。
  • 选择器:选择器(Selector)是 NIO 的一个重要组件,它可以用于同时监控多个通道的读写事件,并在有事件发生时立即做出响应。通过选择器,可以实现单线程监听多个通道的效果,从而提高系统吞吐量和运行效率。例如,在一个网络服务器中,可以将所有的客户端连接通道注册到一个选择器上,选择器不断轮询这些通道,当有通道准备好进行 I/O 操作时,选择器就会通知相应的线程进行处理,避免了每个连接都需要一个线程来处理的情况,节省了系统资源。

1.3 NIO 核心组件

NIO 的核心组件包括通道(Channel)、缓冲区(Buffer)和选择器(Selector),它们协同工作,实现了高效的 I/O 操作。

  • Channel(通道):通道是连接数据源或目的地的双向通道,可以进行读、写或同时进行读写操作。与传统的流不同,通道支持异步操作,并且可以与多个缓冲区进行交互。常见的通道类型有 FileChannel(用于文件的读写操作)、SocketChannel(用于通过 TCP 协议进行网络通信)、ServerSocketChannel(用于监听客户端的连接请求)和 DatagramChannel(用于通过 UDP 协议进行网络通信)。例如,通过 FileChannel 可以实现对文件的高效读写,支持随机访问和内存映射等高级操作;通过 SocketChannel 可以实现非阻塞的网络通信,提高网络应用的并发性能。
  • Buffer(缓冲区):缓冲区是 NIO 中数据的容器,用于存储数据。所有数据的读写都要通过缓冲区进行,缓冲区提供了对数据的结构化访问以及维护读写位置等信息。常用的缓冲区类型有 ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer 和 DoubleBuffer 等,分别用于存储不同类型的数据。在使用缓冲区时,通常需要经历分配 Buffer、写入数据到 Buffer、切换 Buffer 为读模式和从 Buffer 中读取数据等步骤。例如,通过 ByteBuffer 的 allocate () 方法可以分配一个指定容量的缓冲区,然后使用 put () 方法将数据写入缓冲区,再通过 flip () 方法将缓冲区从写模式切换为读模式,最后使用 get () 方法从缓冲区中读取数据。
  • Selector(选择器):选择器用于监听多个通道的事件,如连接打开、数据到达等。一个选择器可以注册多个通道,当其中的某些通道上有感兴趣的事件发生时,这些通道就会变为可用状态,可以在选择器的选择操作中被选中。通过选择器,一个线程可以管理多个通道,实现非阻塞的 I/O 操作,提高系统的并发处理能力。使用选择器的基本流程包括创建 Selector、将通道注册到 Selector 上并指定感兴趣的事件类型,以及不断循环地调用 Selector 的 select () 方法来检查是否有通道已经准备好进行 I/O 操作,最后处理准备就绪的通道。

1.4 NIO 适用场景

NIO 的特性使其在以下场景中具有明显的优势:

  • 高并发网络应用:在开发高性能的网络服务器或客户端时,NIO 可以处理大量并发连接,通过非阻塞 I/O 和选择器机制,提高系统的并发处理能力和资源利用率。例如,常见的网络通信框架如 Netty、Mina 等,都基于 NIO 实现,能够支持海量的并发连接,广泛应用于互联网、游戏、金融等领域。
  • 大数据传输:NIO 提供的通道和缓冲区概念,可以高效地进行大规模数据的传输。在处理大文件读写时,通过 FileChannel 和 ByteBuffer 的配合,可以减少 I/O 操作的次数,提高数据传输的效率。例如,在进行文件的拷贝、备份等操作时,使用 NIO 可以大大缩短操作时间。
  • 需要高效利用系统资源的场景:由于 NIO 可以使用较少的线程来处理大量的 I/O 操作,减少了线程上下文切换的开销,因此在对系统资源利用率要求较高的场景中,NIO 是更好的选择。例如,在一些嵌入式系统或资源受限的环境中,使用 NIO 可以在有限的资源条件下实现高效的 I/O 处理。

二、NIO 核心组件实战

了解了 NIO 的基本概念和原理后,接下来我们通过实际的代码示例来深入学习 NIO 的核心组件 ------ 缓冲区(Buffer)、通道(Channel)和选择器(Selector)的使用。

2.1 Buffer 缓冲区

在 NIO 中,Buffer 是一个用于存储数据的容器,所有数据的读写都要通过缓冲区进行。常见的缓冲区类型有 ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer 和 DoubleBuffer 等,分别用于存储不同类型的数据。下面以 ByteBuffer 为例,介绍缓冲区的创建、读写操作以及 flip 和 clear 方法的使用。

创建缓冲区

可以使用静态方法 allocate 来创建一个指定容量的缓冲区。例如,创建一个容量为 1024 字节的 ByteBuffer:

java 复制代码
ByteBuffer buffer = ByteBuffer.allocate(1024);

也可以通过 wrap 方法将一个现有的数组包装成缓冲区:

java 复制代码
byte[] array = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(array);

写入数据

使用 put 方法将数据写入缓冲区。例如,将一个字符串写入 ByteBuffer:

java 复制代码
String message = "Hello, NIO!";
buffer.put(message.getBytes());

也可以从通道中读取数据到缓冲区,假设我们有一个 FileChannel:

java 复制代码
FileInputStream fis = new FileInputStream("example.txt");
FileChannel channel = fis.getChannel();
channel.read(buffer);

读取数据

在读取数据之前,需要先调用 flip 方法将缓冲区从写模式切换为读模式。flip 方法会将 position 设置为 0,并将 limit 设置为当前 position 的值,这样就可以从缓冲区的开头开始读取数据了。例如:

java 复制代码
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String result = new String(data);
System.out.println(result);

flip 与 clear 方法

  • flip 方法:如上述所说,用于将缓冲区从写模式切换为读模式。
  • clear 方法:用于清空缓冲区,将 position 设置为 0,limit 设置为容量大小。但需要注意的是,clear 方法并不会真正删除缓冲区中的数据,只是重置了缓冲区的状态,以便重新写入数据。例如:
java 复制代码
buffer.clear();

通过以下完整代码示例,可以更清晰地看到缓冲区的工作原理:

java 复制代码
import java.nio.ByteBuffer;

public class BufferExample {
    public static void main(String[] args) {
        // 创建缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        // 写入数据
        String message = "Hello, NIO!";
        buffer.put(message.getBytes());

        // 切换为读模式
        buffer.flip();

        // 读取数据
        byte[] data = new byte[buffer.remaining()];
        buffer.get(data);
        String result = new String(data);
        System.out.println(result);

        // 清空缓冲区
        buffer.clear();
    }
}

2.2 Channel 通道

Channel 是 NIO 中用于与数据源或目的地进行数据传输的通道,它可以进行读、写或同时进行读写操作。常见的通道类型有 FileChannel(用于文件的读写操作)、SocketChannel(用于通过 TCP 协议进行网络通信)、ServerSocketChannel(用于监听客户端的连接请求)和 DatagramChannel(用于通过 UDP 协议进行网络通信)。下面分别介绍 FileChannel 和 SocketChannel 的使用。

FileChannel 文件操作

FileChannel 主要用于文件的读写操作,它可以实现对文件的高效读写,支持随机访问和内存映射等高级操作。以下是使用 FileChannel 读取和写入文件的示例:

java 复制代码
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("input.txt");
             FileOutputStream fos = new FileOutputStream("output.txt");
             FileChannel inChannel = fis.getChannel();
             FileChannel outChannel = fos.getChannel()) {

            // 创建缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            // 从输入通道读取数据到缓冲区
            while (inChannel.read(buffer) != -1) {
                // 切换缓冲区为读模式
                buffer.flip();
                // 从缓冲区写入数据到输出通道
                outChannel.write(buffer);
                // 清空缓冲区,为下一次读取做准备
                buffer.clear();
            }

            System.out.println("文件复制完成!");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

SocketChannel 网络操作

SocketChannel 用于通过 TCP 协议进行网络通信,可以实现非阻塞的网络通信,提高网络应用的并发性能。以下是一个简单的 SocketChannel 客户端和服务器端示例:

服务器端

java 复制代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NioServer {
    public static void main(String[] args) {
        try {
            // 创建选择器
            Selector selector = Selector.open();

            // 创建服务器套接字通道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.socket().bind(new InetSocketAddress(8080));

            // 将服务器通道注册到选择器上,监听连接事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            System.out.println("服务器启动,监听端口8080...");

            while (true) {
                // 阻塞直到有事件发生
                selector.select();

                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    keyIterator.remove();

                    if (key.isAcceptable()) {
                        // 处理新连接
                        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                        SocketChannel clientChannel = serverChannel.accept();
                        clientChannel.configureBlocking(false);
                        clientChannel.register(selector, SelectionKey.OP_READ);
                        System.out.println("新客户端连接: " + clientChannel.getRemoteAddress());
                    } else if (key.isReadable()) {
                        // 处理读事件
                        SocketChannel clientChannel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        int bytesRead = clientChannel.read(buffer);
                        if (bytesRead > 0) {
                            buffer.flip();
                            byte[] data = new byte[buffer.remaining()];
                            buffer.get(data);
                            String message = new String(data);
                            System.out.println("收到客户端消息: " + message);

                            // 回显消息给客户端
                            ByteBuffer outBuffer = ByteBuffer.wrap(("服务器已收到消息: " + message).getBytes());
                            clientChannel.write(outBuffer);
                        } else if (bytesRead == -1) {
                            // 客户端关闭连接
                            clientChannel.close();
                            System.out.println("客户端断开连接: " + clientChannel.getRemoteAddress());
                        }
                    }
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端

java 复制代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NioClient {
    public static void main(String[] args) {
        try {
            // 创建SocketChannel
            SocketChannel clientChannel = SocketChannel.open();
            clientChannel.configureBlocking(false);

            // 连接服务器
            clientChannel.connect(new InetSocketAddress("localhost", 8080));

            // 等待连接完成
            while (!clientChannel.finishConnect()) {
                // 可以做点别的事,或者稍微等等
            }

            System.out.println("已连接到服务器");

            // 发送消息
            String message = "你好,服务器!";
            ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
            clientChannel.write(buffer);

            // 接收服务器回显消息
            buffer.clear();
            int bytesRead = clientChannel.read(buffer);
            if (bytesRead > 0) {
                buffer.flip();
                byte[] data = new byte[buffer.remaining()];
                buffer.get(data);
                String response = new String(data);
                System.out.println("收到服务器回显: " + response);
            }

            // 关闭通道
            clientChannel.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.3 Selector 选择器

Selector 是 NIO 中的一个重要组件,它可以用于同时监控多个通道的读写事件,并在有事件发生时立即做出响应。通过选择器,可以实现单线程监听多个通道的效果,从而提高系统吞吐量和运行效率。以下是使用 Selector 的基本步骤和代码示例:

注册通道

首先需要创建一个 Selector,然后将通道注册到 Selector 上,并指定感兴趣的事件类型。例如,将 ServerSocketChannel 注册到 Selector 上,监听连接事件:

java 复制代码
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

监听事件

使用 selector 的 select 方法来监听注册通道上的事件。select 方法会阻塞,直到有感兴趣的事件发生。例如:

java 复制代码
int readyChannels = selector.select();
if (readyChannels > 0) {
    // 处理就绪事件
}

处理就绪事件

通过 selectedKeys 方法获取已就绪的 SelectionKey 集合,然后遍历该集合,根据不同的事件类型处理相应的通道。例如:

java 复制代码
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    keyIterator.remove();

    if (key.isAcceptable()) {
        // 处理新连接
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        SocketChannel clientChannel = serverChannel.accept();
        clientChannel.configureBlocking(false);
        clientChannel.register(selector, SelectionKey.OP_READ);
        System.out.println("新客户端连接: " + clientChannel.getRemoteAddress());
    } else if (key.isReadable()) {
        // 处理读事件
        SocketChannel clientChannel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int bytesRead = clientChannel.read(buffer);
        if (bytesRead > 0) {
            buffer.flip();
            byte[] data = new byte[buffer.remaining()];
            buffer.get(data);
            String message = new String(data);
            System.out.println("收到客户端消息: " + message);

            // 回显消息给客户端
            ByteBuffer outBuffer = ByteBuffer.wrap(("服务器已收到消息: " + message).getBytes());
            clientChannel.write(outBuffer);
        } else if (bytesRead == -1) {
            // 客户端关闭连接
            clientChannel.close();
            System.out.println("客户端断开连接: " + clientChannel.getRemoteAddress());
        }
    }
}

完整的服务器端代码示例(包含 Selector 的使用):

java 复制代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class SelectorExample {
    public static void main(String[] args) {
        try {
            // 创建选择器
            Selector selector = Selector.open();

            // 创建服务器套接字通道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.socket().bind(new InetSocketAddress(8080));

            // 将服务器通道注册到选择器上,监听连接事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            System.out.println("服务器启动,监听端口8080...");

            while (true) {
                // 阻塞直到有事件发生
                selector.select();

                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    keyIterator.remove();

                    if (key.isAcceptable()) {
                        // 处理新连接
                        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                        SocketChannel clientChannel = serverChannel.accept();
                        clientChannel.configureBlocking(false);
                        clientChannel.register(selector, SelectionKey.OP_READ);
                        System.out.println("新客户端连接: " + clientChannel.getRemoteAddress());
                    } else if (key.isReadable()) {
                        // 处理读事件
                        SocketChannel clientChannel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        int bytesRead = clientChannel.read(buffer);
                        if (bytesRead > 0) {
                            buffer.flip();
                            byte[] data = new byte[buffer.remaining()];
                            buffer.get(data);
                            String message = new String(data);
                            System.out.println("收到客户端消息: " + message);

                            // 回显消息给客户端
                            ByteBuffer outBuffer = ByteBuffer.wrap(("服务器已收到消息: " + message).getBytes());
                            clientChannel.write(outBuffer);
                        } else if (bytesRead == -1) {
                            // 客户端关闭连接
                            clientChannel.close();
                            System.out.println("客户端断开连接: " + clientChannel.getRemoteAddress());
                        }
                    }
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.4 NIO 文件操作案例

为了更直观地展示 NIO 在文件操作中的优势,我们通过一个大文件高效读写的案例来进行说明。假设我们有一个大小为 1GB 的大文件,需要将其读取并复制到另一个文件中,对比传统 IO 和 NIO 的实现方式和性能表现。

传统 IO 实现

java 复制代码
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class TraditionalIoExample {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

        try (FileInputStream fis = new FileInputStream("largeFile.txt");
             FileOutputStream fos = new FileOutputStream("copy_largeFile.txt")) {

            byte[] buffer = new byte[1024];
            int length;
            while ((length = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, length);
            }

        } catch (IOException e) {
            e.printStackTrace();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("传统IO复制文件耗时: " + (endTime - startTime) + " 毫秒");
    }
}

NIO 实现

java 复制代码
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class NioFileExample {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

        try (FileChannel inChannel = FileChannel.open(Paths.get("largeFile.txt"), StandardOpenOption.READ);
             FileChannel outChannel = FileChannel.open(Paths.get("copy_largeFile.txt"), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {

            ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024); // 1MB缓冲区
            while (inChannel.read(buffer) != -1) {
                buffer.flip();
                outChannel.write(buffer);
                buffer.clear();
            }

        } catch (IOException e) {
            e.printStackTrace();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("NIO复制文件耗时: " + (endTime - startTime) + " 毫秒");
    }
}

通过实际测试可以发现,NIO 在处理大文件读写时,由于其采用了缓冲区和通道的机制,减少了系统调用次数和数据拷贝次数,相比传统 IO 具有更高的效率。在上述案例中,NIO 复制文件的耗时通常会明显低于传统 IO,尤其是在处理超大文件时,这种优势更加显著。这是因为 NIO 的缓冲区可以一次性读取和写入大量数据,减少了 I/O 操作的频率,从而提高了文件读写的性能。

三、NIO2.0 实战

Java NIO2.0 是 Java 7 引入的一组增强的 I/O API,它在 NIO 的基础上提供了更强大、更便捷的文件和目录操作功能。NIO2.0 引入了新的类和接口,如 Path、Files 和 WatchService 等,使得文件系统的操作更加灵活和高效。接下来,我们将深入探讨 NIO2.0 中 Path 类和 Files 类的使用,并通过实战案例展示其强大功能。

3.1 Path 类

在 Java NIO2.0 中,Path 类用于表示文件系统中的路径,它是一个平台无关的抽象路径。Path 接口提供了一系列方法来操作路径,包括获取路径的各个部分、解析路径、规范化路径等。

路径表示

可以使用 Paths 类的 get 方法来创建 Path 对象。例如,创建一个表示文件路径的 Path 对象:

java 复制代码
import java.nio.file.Path;
import java.nio.file.Paths;

public class PathExample {
    public static void main(String[] args) {
        // 创建Path对象
        Path path = Paths.get("C:/Users/Username/Documents/example.txt");
        // 获取文件名
        System.out.println("文件名: " + path.getFileName());
        // 获取父路径
        System.out.println("父路径: " + path.getParent());
        // 获取根路径
        System.out.println("根路径: " + path.getRoot());
    }
}

路径操作

Path 接口提供了丰富的方法来操作路径,如拼接路径、规范化路径等。

  • 拼接路径:使用 resolve 方法可以将两个路径拼接在一起。例如:
java 复制代码
Path path1 = Paths.get("C:/Users/Username");
Path path2 = path1.resolve("Documents/example.txt");
System.out.println("拼接后的路径: " + path2);
  • 规范化路径:使用 normalize 方法可以消除路径中的冗余部分,如 "./" 和 ".../"。例如:
java 复制代码
Path unnormalizedPath = Paths.get("C:/Users/Username/../Documents/./example.txt");
Path normalizedPath = unnormalizedPath.normalize();
System.out.println("规范化后的路径: " + normalizedPath);

3.2 Files 类

Files 类是 NIO2.0 中用于文件操作的核心类,它提供了大量的静态方法来执行文件的创建、删除、复制、移动、读取和写入等操作。

文件创建

使用 createFile 方法可以创建一个新文件。例如:

java 复制代码
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;

public class FilesExample {
    public static void main(String[] args) {
        Path filePath = Paths.get("C:/Users/Username/newfile.txt");
        try {
            Files.createFile(filePath);
            System.out.println("文件创建成功");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

文件删除

使用 delete 方法可以删除文件或空目录。如果要删除的文件或目录不存在,会抛出 NoSuchFileException 异常;如果要删除的目录非空,会抛出 DirectoryNotEmptyException 异常。例如:

java 复制代码
Path filePath = Paths.get("C:/Users/Username/newfile.txt");
try {
    Files.delete(filePath);
    System.out.println("文件删除成功");
} catch (IOException e) {
    e.printStackTrace();
}

文件复制

使用 copy 方法可以复制文件。如果目标文件已存在,会抛出 FileAlreadyExistsException 异常。可以通过 StandardCopyOption.REPLACE_EXISTING 选项来覆盖已存在的文件。例如:

java 复制代码
Path sourcePath = Paths.get("C:/Users/Username/source.txt");
Path targetPath = Paths.get("C:/Users/Username/target.txt");
try {
    Files.copy(sourcePath, targetPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
    System.out.println("文件复制成功");
} catch (IOException e) {
    e.printStackTrace();
}

文件读取

使用 readAllBytes 方法可以读取文件的所有字节内容。例如:

java 复制代码
Path filePath = Paths.get("C:/Users/Username/data.txt");
try {
    byte[] content = Files.readAllBytes(filePath);
    String data = new String(content);
    System.out.println("文件内容: " + data);
} catch (IOException e) {
    e.printStackTrace();
}

3.3 Files 类高级操作

除了基本的文件操作,Files 类还提供了一些高级操作方法,如获取文件属性和监听文件变化。

文件属性获取

可以使用 Files 类的 getAttribute 方法获取文件的各种属性,如文件大小、修改时间、创建时间等。例如,获取文件的大小和修改时间:

java 复制代码
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;

public class FileAttributesExample {
    public static void main(String[] args) {
        Path filePath = Paths.get("C:/Users/Username/data.txt");
        try {
            BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class);
            System.out.println("文件大小: " + attrs.size() + " 字节");
            System.out.println("修改时间: " + attrs.lastModifiedTime());
            System.out.println("创建时间: " + attrs.creationTime());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

文件监听

NIO2.0 引入了 WatchService 来监听文件系统的变化,如文件的创建、修改和删除等。以下是一个简单的示例,展示如何监听指定目录下的文件变化:

java 复制代码
import java.nio.file.*;
import java.io.IOException;

public class FileWatcherExample {
    public static void main(String[] args) {
        try {
            // 创建WatchService
            WatchService watchService = FileSystems.getDefault().newWatchService();

            // 注册要监听的目录
            Path directory = Paths.get("C:/Users/Username/Documents");
            directory.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);

            System.out.println("开始监听目录: " + directory);

            while (true) {
                // 等待事件发生
                WatchKey key = watchService.take();

                for (WatchEvent<?> event : key.pollEvents()) {
                    WatchEvent.Kind<?> kind = event.kind();
                    @SuppressWarnings("unchecked")
                    WatchEvent<Path> ev = (WatchEvent<Path>) event;
                    Path fileName = ev.context();

                    System.out.println("事件类型: " + kind + ", 文件: " + fileName);
                }

                // 重置WatchKey
                boolean valid = key.reset();
                if (!valid) {
                    break;
                }
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3.4 NIO2.0 实战案例

为了更好地展示 NIO2.0 的强大功能,我们通过一个目录遍历和文件筛选的案例来进行实践。假设我们需要遍历指定目录及其子目录,筛选出所有的 Java 源文件,并打印出它们的路径。

java 复制代码
import java.nio.file.*;
import java.io.IOException;

public class DirectoryTraversalExample {
    public static void main(String[] args) {
        Path directory = Paths.get("C:/Users/Username/Projects");
        try {
            Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    if (file.getFileName().toString().endsWith(".java")) {
                        System.out.println("找到Java源文件: " + file);
                    }
                    return FileVisitResult.CONTINUE;
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,我们使用 Files 类的 walkFileTree 方法来遍历指定目录及其子目录。walkFileTree 方法接受两个参数,一个是要遍历的起始目录,另一个是实现了 FileVisitor 接口的对象。我们通过继承 SimpleFileVisitor 类(它是 FileVisitor 接口的一个适配器类,提供了默认的实现),并重写 visitFile 方法来实现文件筛选逻辑。在 visitFile 方法中,我们检查文件的扩展名是否为 ".java",如果是,则打印出文件的路径。通过这个案例,我们可以看到 NIO2.0 提供的文件和目录操作功能非常强大和灵活,能够轻松应对各种复杂的文件处理需求。

相关推荐
冬天的雪20085 分钟前
java内存性能优化工具Mat
java·开发语言
Le1Yu19 分钟前
消息队列以及RabbitMQ的使用
java·开发语言
羚羊角uou30 分钟前
【Linux】线程池
java·开发语言
阿拉-M8337 分钟前
IntelliJ IDEA Windows 系统高频快捷键使用手册
java·windows·intellij-idea
lingggggaaaa1 小时前
小迪安全v2023学习笔记(一百三十四讲)—— Windows权限提升篇&数据库篇&MySQL&MSSQL&Oracle&自动化项目
java·数据库·windows·笔记·学习·安全·网络安全
迦蓝叶1 小时前
JAiRouter v1.0.0 正式发布:企业级 AI 服务网关的开源解决方案
java·运维·人工智能·网关·spring·ai·开源
安卓开发者1 小时前
鸿蒙NEXT应用接入快捷栏:一键直达,提升用户体验
java·harmonyos·ux
yudiandian20142 小时前
03 Eclipse 配置 JDK 环境
java·ide·eclipse