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

发布于:2025-09-05 ⋅ 阅读:(20) ⋅ 点赞:(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:

ByteBuffer buffer = ByteBuffer.allocate(1024);

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

byte[] array = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(array);

写入数据

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

String message = "Hello, NIO!";
buffer.put(message.getBytes());

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

FileInputStream fis = new FileInputStream("example.txt");
FileChannel channel = fis.getChannel();
channel.read(buffer);

读取数据

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

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 方法并不会真正删除缓冲区中的数据,只是重置了缓冲区的状态,以便重新写入数据。例如:
buffer.clear();

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

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 读取和写入文件的示例:

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 客户端和服务器端示例:

服务器端

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();
        }
    }
}

客户端

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 上,监听连接事件:

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 方法会阻塞,直到有感兴趣的事件发生。例如:

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

处理就绪事件

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

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 的使用):

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 实现

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 实现

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 对象:

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 方法可以将两个路径拼接在一起。例如:
Path path1 = Paths.get("C:/Users/Username");
Path path2 = path1.resolve("Documents/example.txt");
System.out.println("拼接后的路径: " + path2);
  • 规范化路径:使用 normalize 方法可以消除路径中的冗余部分,如 “./” 和 “…/”。例如:
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 方法可以创建一个新文件。例如:

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 异常。例如:

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 选项来覆盖已存在的文件。例如:

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 方法可以读取文件的所有字节内容。例如:

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 方法获取文件的各种属性,如文件大小、修改时间、创建时间等。例如,获取文件的大小和修改时间:

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 来监听文件系统的变化,如文件的创建、修改和删除等。以下是一个简单的示例,展示如何监听指定目录下的文件变化:

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 源文件,并打印出它们的路径。

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 提供的文件和目录操作功能非常强大和灵活,能够轻松应对各种复杂的文件处理需求。


网站公告

今日签到

点亮在社区的每一天
去签到