Java IO 流编程进阶:字节流、字符流与 NIO 实战

一、IO 流编程进阶:字节流、字符流与 NIO 实战

在这里插入图片描述

1.1 本章学习目标与重点

💡 掌握 Java IO 流的核心体系(字节流、字符流、缓冲流、转换流)及设计模式
💡 深入理解字节流与字符流的本质差异,熟练处理文件、网络等场景的 IO 操作
💡 精通 NIO(New IO)的核心组件(Channel、Buffer、Selector)及非阻塞 IO 原理
💡 掌握 IO 流性能优化技巧,解决大文件读写、并发 IO 等实际开发问题
⚠️ 重点关注流的关闭机制、编码问题、NIO 非阻塞模型等高频面试考点
✅ 最终目标:能够根据业务场景灵活选择 IO 方案,实现高效、安全的 IO 操作,应对大文件、高并发等复杂场景

1.2 Java IO 核心概念与体系结构

在深入学习具体 IO 流实现之前,我们先明确 IO 的核心定义、设计思想与整体体系,为后续学习奠定基础。

1.2.1 IO 的核心定义

IO(Input/Output)即输入/输出,是程序与外部设备(如文件、网络、键盘、显示器)之间的数据传输过程:

  • 输入流(Input Stream):数据从外部设备流入程序(如读取文件内容到程序);
  • 输出流(Output Stream):数据从程序流出到外部设备(如将程序数据写入文件)。

💡 笔记:Java IO 设计的核心思想是“流”——数据以字节或字符为单位,像水流一样连续传输,程序通过操作流来实现数据的读写,无需关注底层设备的具体实现。

1.2.2 IO 流的核心设计模式

Java IO 流基于两大设计模式,保证了体系的灵活性和扩展性:

  1. 装饰器模式(Decorator Pattern)

    • 核心:以“基础流 + 装饰流”的方式组合功能,基础流提供核心读写能力,装饰流增强功能(如缓冲、编码转换);
    • 示例:FileInputStream(基础流)+ BufferedInputStream(缓冲装饰流),通过装饰流为基础流添加缓冲功能,提升读写效率;
    • 优势:无需修改原有类,即可动态扩展功能,符合“开闭原则”。
  2. 适配器模式(Adapter Pattern)

    • 核心:将一个类的接口转换为另一个类的接口,使不兼容的类能够一起工作;
    • 示例:InputStreamReader(转换流)将字节流接口适配为字符流接口,解决字节流与字符流的转换问题;
    • 优势:解决不同接口之间的兼容性问题,简化开发。

1.2.3 Java IO 流体系结构总览

Java IO 流分为四大类,基于“字节/字符”和“输入/输出”两个维度划分,核心体系如下:

类型输入流(读数据)输出流(写数据)核心用途
字节流InputStream(抽象基类)OutputStream(抽象基类)读写二进制数据(文件、图片、视频)
字符流Reader(抽象基类)Writer(抽象基类)读写文本数据(字符串、配置文件)
缓冲流BufferedInputStream/BufferedReaderBufferedOutputStream/BufferedWriter增强缓冲,提升读写效率
转换流InputStreamReaderOutputStreamWriter字节流与字符流的转换
文件流FileInputStream/FileReaderFileOutputStream/FileWriter读写本地文件
数组流ByteArrayInputStream/StringReaderByteArrayOutputStream/StringWriter读写内存中的数组/字符串
打印流-PrintStream/PrintWriter格式化输出(如 System.out)
对象流ObjectInputStreamObjectOutputStream对象的序列化与反序列化

💡 核心继承关系:

  • 字节流:InputStream → 所有字节输入流的父类;OutputStream → 所有字节输出流的父类;
  • 字符流:Reader → 所有字符输入流的父类;Writer → 所有字符输出流的父类;
  • 所有流类均实现 Closeable 接口,需手动关闭流资源(释放文件句柄、网络连接等)。

1.3 字节流深度解析:二进制数据的读写

字节流是 Java IO 的基础,以字节(byte)为单位读写数据,适用于所有二进制数据(如文件、图片、视频、网络数据包)。本节重点解析核心字节流的实现与实战。

1.3.1 字节流核心基类:InputStream 与 OutputStream

1. InputStream(字节输入流基类)

InputStream 是抽象类,定义了字节输入流的核心方法,所有字节输入流均继承自它:

public abstract class InputStream implements Closeable {
    // 读取一个字节(返回值:0~255,若到达流末尾返回 -1)
    public abstract int read() throws IOException;

    // 读取多个字节到字节数组(返回值:实际读取的字节数,流末尾返回 -1)
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

    // 读取多个字节到字节数组的指定范围(offset:起始索引,length:读取长度)
    public int read(byte b[], int offset, int length) throws IOException {
        // 抽象方法的默认实现,子类可重写
    }

    // 跳过指定字节数(返回值:实际跳过的字节数)
    public long skip(long n) throws IOException {
        // 默认实现,子类可重写
    }

    // 估计剩余可读取的字节数
    public int available() throws IOException {
        return 0;
    }

    // 关闭流(释放资源,必须调用)
    public void close() throws IOException {}

    // 标记流的当前位置(仅部分子类支持,如 BufferedInputStream)
    public synchronized void mark(int readlimit) {}

    // 重置流到上次标记的位置
    public synchronized void reset() throws IOException {
        throw new IOException("mark/reset not supported");
    }

    // 判断流是否支持 mark/reset 操作
    public boolean markSupported() {
        return false;
    }
}

💡 核心方法说明:

  • read():读取单个字节,返回值为 0~255 的整数(字节的无符号表示),流末尾返回 -1;
  • read(byte[] b):批量读取字节到数组,减少 IO 次数,提升效率(推荐使用);
  • close():必须调用,否则会导致资源泄漏(如文件句柄未释放)。
2. OutputStream(字节输出流基类)

OutputStream 也是抽象类,定义了字节输出流的核心方法:

public abstract class OutputStream implements Closeable, Flushable {
    // 写入一个字节(参数 b:仅低 8 位有效,高 24 位被忽略)
    public abstract void write(int b) throws IOException;

    // 写入字节数组的所有字节
    public void write(byte b[]) throws IOException {
        write(b, 0, b.length);
    }

    // 写入字节数组的指定范围
    public void write(byte b[], int offset, int length) throws IOException {
        // 抽象方法的默认实现,子类可重写
    }

    // 刷新流(将缓冲区的数据强制写入目标设备)
    public void flush() throws IOException {}

    // 关闭流(关闭前会自动调用 flush())
    public void close() throws IOException {}
}

💡 核心方法说明:

  • write(int b):写入单个字节,参数需传入 0~255 的整数(若传入字符,需强转为 byte);
  • flush():缓冲流必须调用,否则缓冲区数据可能未写入目标设备(如 BufferedOutputStream);
  • close():关闭流时会自动刷新缓冲区,因此无需手动调用 flush() 后再关闭。

1.3.2 核心字节流实现类实战

1. FileInputStream/FileOutputStream:文件字节流

专门用于读写本地文件的字节流,是最常用的字节流实现类。

(1)FileInputStream:读取文件
/**
 * 示例 1:使用 FileInputStream 读取文件(单个字节读取,效率低)
 */
public class FileInputStreamDemo1 {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            // 1. 创建 FileInputStream 对象,指定文件路径(相对路径或绝对路径)
            fis = new FileInputStream("test.txt");

            // 2. 读取文件(单个字节读取)
            int data;
            System.out.println("文件内容(单个字节读取):");
            while ((data = fis.read()) != -1) {
                // 字节转换为字符(默认使用系统默认编码)
                System.out.print((char) data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 3. 关闭流(必须在 finally 中执行,确保无论是否异常都能关闭)
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
(2)批量读取优化(推荐)

单个字节读取效率极低(频繁 IO 操作),实际开发中优先使用批量读取:

/**
 * 示例 2:FileInputStream 批量读取文件(效率高)
 */
public class FileInputStreamDemo2 {
    public static void main(String[] args) {
        // try-with-resources 语法(Java 7+):自动关闭实现 Closeable 接口的资源,无需手动 close()
        try (FileInputStream fis = new FileInputStream("test.txt")) {
            byte[] buffer = new byte[1024]; // 缓冲区大小(1KB,可根据文件大小调整)
            int len; // 实际读取的字节数
            System.out.println("文件内容(批量读取):");
            while ((len = fis.read(buffer)) != -1) {
                // 字节数组转换为字符串(需指定编码,避免乱码)
                System.out.print(new String(buffer, 0, len, StandardCharsets.UTF_8));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

💡 关键优化:

  • 使用 byte[] buffer 批量读取,减少 IO 次数(缓冲区越大,效率越高,但占用内存越多,推荐 1KB~8KB);
  • 使用 try-with-resources 语法,自动关闭流资源,简化代码并避免资源泄漏;
  • 转换字符串时指定编码(如 UTF-8),避免系统默认编码导致的乱码问题。
(3)FileOutputStream:写入文件
/**
 * 示例 3:FileOutputStream 写入文件(覆盖写入/追加写入)
 */
public class FileOutputStreamDemo {
    public static void main(String[] args) {
        // 追加写入:第二个参数传 true,默认 false(覆盖写入)
        try (FileOutputStream fos = new FileOutputStream("output.txt", true)) {
            String content = "Hello, FileOutputStream!\n";
            // 字符串转换为字节数组(指定 UTF-8 编码)
            byte[] data = content.getBytes(StandardCharsets.UTF_8);
            // 批量写入字节数组
            fos.write(data);
            System.out.println("写入成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

⚠️ 注意事项:

  • 若文件不存在,FileOutputStream 会自动创建文件(但不会创建父目录,需手动创建);
  • 覆盖写入:默认情况下,写入会覆盖文件原有内容,若需追加,需在构造函数中传入 true
  • 写入后无需手动调用 flush(),因为 close() 会自动刷新,但若需立即写入(如日志输出),可手动调用。
2. BufferedInputStream/BufferedOutputStream:缓冲字节流

缓冲流是装饰流,通过内置缓冲区(默认 8KB)减少 IO 次数,提升读写效率(核心优化手段)。

(1)缓冲流的工作原理
  • 读取时:缓冲流先从目标设备读取数据到缓冲区,程序从缓冲区读取数据,避免每次读取都触发底层 IO;
  • 写入时:程序先将数据写入缓冲区,缓冲区满后自动写入目标设备,也可通过 flush() 手动触发写入。
(2)缓冲流实战(大文件读写优化)
/**
 * 示例 4:缓冲流读写大文件(推荐实战方案)
 */
public class BufferedStreamDemo {
    public static void main(String[] args) {
        // 记录耗时
        long start = System.currentTimeMillis();

        // 缓冲流包装文件流(装饰器模式)
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("large_file.zip"));
             BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy_large_file.zip"))) {

            byte[] buffer = new byte[8192]; // 8KB 缓冲区(与默认缓冲区大小一致,可调整)
            int len;
            while ((len = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }

            long end = System.currentTimeMillis();
            System.out.println("大文件复制完成!耗时:" + (end - start) + "ms");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

💡 性能对比:

  • 无缓冲流:复制 100MB 文件约耗时 500ms;
  • 有缓冲流:复制 100MB 文件约耗时 80ms,效率提升 6 倍以上;
  • 结论:读写文件时,必须使用缓冲流包装基础流,这是 IO 性能优化的核心手段。
3. ByteArrayInputStream/ByteArrayOutputStream:内存字节流

内存流以内存中的字节数组为读写目标,无需外部设备,读写速度极快,适用于临时数据处理(如数据转换、缓存)。

(1)ByteArrayInputStream:读取内存数组
/**
 * 示例 5:ByteArrayInputStream 读取内存字节数组
 */
public class ByteArrayInputStreamDemo {
    public static void main(String[] args) {
        byte[] data = "Hello, ByteArrayInputStream!".getBytes(StandardCharsets.UTF_8);
        // 从字节数组读取数据
        try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) {
            byte[] buffer = new byte[10];
            int len;
            System.out.println("读取内存数据:");
            while ((len = bais.read(buffer)) != -1) {
                System.out.print(new String(buffer, 0, len));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
(2)ByteArrayOutputStream:写入内存数组
/**
 * 示例 6:ByteArrayOutputStream 写入内存字节数组(无大小限制)
 */
public class ByteArrayOutputStreamDemo {
    public static void main(String[] args) {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            // 写入多个数据
            baos.write("Hello, ".getBytes());
            baos.write("ByteArrayOutputStream!".getBytes());
            baos.write(65); // 写入单个字节(ASCII 65 = 'A')

            // 获取写入的字节数组
            byte[] result = baos.toByteArray();
            System.out.println("内存中的数据:" + new String(result, StandardCharsets.UTF_8));
            // 输出:Hello, ByteArrayOutputStream!A
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

💡 核心优势:

  • 无大小限制:ByteArrayOutputStream 会自动扩容,无需指定初始大小;
  • 速度极快:读写均在内存中进行,无磁盘 IO 开销;
  • 适用场景:数据转换(如字符串 → 字节数组)、临时缓存、网络数据处理(如 HTTP 响应数据)。
4. ObjectInputStream/ObjectOutputStream:对象流(序列化)

对象流用于实现对象的序列化(将对象转换为字节流)和反序列化(将字节流恢复为对象),适用于对象的持久化存储(如写入文件)或网络传输。

(1)序列化的核心要求
  • 对象所属类必须实现 Serializable 接口(标记接口,无抽象方法);
  • 类中的所有属性必须可序列化(基本类型默认可序列化,引用类型需也实现 Serializable);
  • 若属性无需序列化,可使用 transient 关键字修饰(如密码、敏感数据)。
(2)对象流实战
import java.io.*;
import java.util.Date;

// 序列化实体类(必须实现 Serializable 接口)
class User implements Serializable {
    // 序列化版本号(建议显式声明,避免类结构变化导致反序列化失败)
    private static final long serialVersionUID = 1L;

    private Long id;
    private String name;
    private int age;
    private transient String password; // transient 修饰,不参与序列化
    private Date createTime;

    // 构造函数、getter、setter、toString() 省略

    public User(Long id, String name, int age, String password) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.password = password;
        this.createTime = new Date();
    }
}

/**
 * 示例 7:对象的序列化与反序列化
 */
public class ObjectStreamDemo {
    public static void main(String[] args) {
        // 1. 对象序列化(写入文件)
        User user = new User(1L, "张三", 25, "123456");
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
            oos.writeObject(user);
            System.out.println("对象序列化成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 2. 对象反序列化(从文件读取)
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
            User deserializedUser = (User) ois.readObject();
            System.out.println("对象反序列化成功!");
            System.out.println("反序列化结果:" + deserializedUser);
            System.out.println("密码(transient 修饰):" + deserializedUser.getPassword()); // 输出 null
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

⚠️ 注意事项:

  • 序列化版本号(serialVersionUID):显式声明后,即使类结构变化(如新增字段),仍可反序列化旧版本对象;若未声明,Java 会自动生成,类结构变化后会导致反序列化失败;
  • transient 关键字:修饰的属性不参与序列化,反序列化后值为默认值(null 或 0);
  • 静态字段:不参与序列化(序列化的是对象实例,而非类);
  • 反序列化:无需调用构造函数,直接通过字节流恢复对象状态。

1.3.3 字节流常见问题与解决方案

1. 问题 1:流未关闭导致资源泄漏

💡 解决方案:

  • 优先使用 try-with-resources 语法(Java 7+),自动关闭流;
  • 若使用 Java 7 之前版本,必须在 finally 中关闭流,且需判断流不为 null;
  • 错误示例(未关闭流):
// 错误
FileInputStream fis = new FileInputStream("test.txt");
fis.read();
// 未调用 fis.close(),导致文件句柄泄漏
  • 正确示例:
// 正确(try-with-resources)
try (FileInputStream fis = new FileInputStream("test.txt")) {
    fis.read();
} catch (IOException e) {
    e.printStackTrace();
}
2. 问题 2:读取大文件时内存溢出

💡 解决方案:

  • 使用批量读取(byte[] buffer),避免一次性读取整个文件到内存;
  • 缓冲区大小建议 1KB~8KB(平衡内存占用与 IO 效率);
  • 错误示例(一次性读取大文件):
// 错误(大文件会导致 OOM)
byte[] data = new byte[fis.available()];
fis.read(data);
  • 正确示例(批量读取):
byte[] buffer = new byte[8192];
int len;
while ((len = fis.read(buffer)) != -1) {
    // 处理 buffer 中的数据
}
3. 问题 3:序列化时出现 NotSerializableException

💡 解决方案:

  • 确保对象所属类及所有引用属性类均实现 Serializable 接口;
  • 对无需序列化的引用属性,使用 transient 修饰;
  • 检查是否存在不可序列化的属性(如 ThreadSocket 等),若有需排除。

1.4 字符流深度解析:文本数据的读写

字符流以字符(char)为单位读写数据,专门用于处理文本数据(如 TXT 文件、配置文件、JSON 字符串),核心优势是自动处理字符编码问题(避免字节流读取文本时的乱码)。

1.4.1 字符流核心基类:Reader 与 Writer

1. Reader(字符输入流基类)

Reader 是抽象类,定义了字符输入流的核心方法,与 InputStream 类似,但以字符为单位:

public abstract class Reader implements Closeable, Readable {
    // 读取一个字符(返回值:0~65535,流末尾返回 -1)
    public int read() throws IOException {
        // 抽象方法的默认实现,子类可重写
    }

    // 批量读取字符到字符数组
    public int read(char cbuf[]) throws IOException {
        return read(cbuf, 0, cbuf.length);
    }

    // 批量读取字符到字符数组的指定范围
    abstract public int read(char cbuf[], int offset, int length) throws IOException;

    // 跳过指定字符数
    public long skip(long n) throws IOException {
        // 默认实现,子类可重写
    }

    // 关闭流
    abstract public void close() throws IOException;

    // 其他方法(mark、reset 等,与 InputStream 类似)
}

💡 核心差异:

  • 字节流读取 byte(8 位),字符流读取 char(16 位,Unicode 编码);
  • 字符流读取时会自动根据编码将字节转换为字符,避免乱码。
2. Writer(字符输出流基类)

Writer 是抽象类,定义了字符输出流的核心方法:

public abstract class Writer implements Closeable, Flushable {
    // 写入一个字符
    public void write(int c) throws IOException {
        // 抽象方法的默认实现,子类可重写
    }

    // 写入字符数组
    public void write(char cbuf[]) throws IOException {
        write(cbuf, 0, cbuf.length);
    }

    // 写入字符数组的指定范围
    abstract public void write(char cbuf[], int offset, int length) throws IOException;

    // 写入字符串
    public void write(String str) throws IOException {
        write(str, 0, str.length());
    }

    // 写入字符串的指定范围
    public void write(String str, int offset, int length) throws IOException {
        // 默认实现,子类可重写
    }

    // 刷新缓冲区
    abstract public void flush() throws IOException;

    // 关闭流
    abstract public void close() throws IOException;
}

💡 核心优势:

  • 直接支持字符串写入(write(String str)),无需手动转换为字节数组;
  • 自动处理字符编码,写入时将字符转换为指定编码的字节流。

1.4.2 核心字符流实现类实战

1. FileReader/FileWriter:文件字符流

专门用于读写文本文件的字符流,简化了文本读写操作(默认使用系统编码,建议显式指定编码)。

(1)FileReader 读取文本文件
/**
 * 示例 8:FileReader 读取文本文件(指定 UTF-8 编码)
 */
public class FileReaderDemo {
    public static void main(String[] args) {
        // 注意:FileReader 无法直接指定编码,需通过 InputStreamReader 包装(推荐)
        try (InputStreamReader isr = new InputStreamReader(
                new FileInputStream("test.txt"), StandardCharsets.UTF_8);
             BufferedReader br = new BufferedReader(isr)) {

            String line;
            System.out.println("文本文件内容:");
            // 按行读取(BufferedReader 的特有方法,效率高)
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

💡 关键说明:

  • FileReader 无法直接指定编码,需通过 InputStreamReader 包装(转换流),显式指定编码(如 UTF-8);
  • BufferedReaderreadLine() 方法按行读取文本,是处理文本文件的常用方法(避免逐字符读取)。
(2)FileWriter 写入文本文件
/**
 * 示例 9:FileWriter 写入文本文件(追加写入 + UTF-8 编码)
 */
public class FileWriterDemo {
    public static void main(String[] args) {
        // FileWriter 无法直接指定编码,需通过 OutputStreamWriter 包装
        try (OutputStreamWriter osw = new OutputStreamWriter(
                new FileOutputStream("output.txt", true), StandardCharsets.UTF_8);
             BufferedWriter bw = new BufferedWriter(osw)) {

            // 写入字符串
            bw.write("Hello, FileWriter!");
            bw.newLine(); // 写入换行符(跨平台兼容,Windows \r\n,Linux \n)
            bw.write("这是中文内容,指定 UTF-8 编码避免乱码");
            bw.flush(); // 缓冲流需手动刷新(或关闭时自动刷新)

            System.out.println("文本写入成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

⚠️ 注意事项:

  • 写入中文时必须指定编码(如 UTF-8),否则会使用系统默认编码(如 Windows 的 GBK),导致其他平台读取乱码;
  • BufferedWriternewLine() 方法会根据操作系统自动生成换行符,跨平台兼容(推荐使用)。
2. BufferedReader/BufferedWriter:缓冲字符流

与缓冲字节流类似,缓冲字符流通过内置缓冲区提升文本读写效率,同时提供了按行读取(readLine())等便捷方法。

(1)缓冲字符流实战(文本文件复制)
/**
 * 示例 10:缓冲字符流复制文本文件(保留格式 + 无乱码)
 */
public class BufferedCharStreamDemo {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        try (BufferedReader br = new BufferedReader(
                new InputStreamReader(new FileInputStream("source.txt"), StandardCharsets.UTF_8));
             BufferedWriter bw = new BufferedWriter(
                new OutputStreamWriter(new FileOutputStream("target.txt"), StandardCharsets.UTF_8))) {

            String line;
            // 按行读取并写入(保留换行符)
            while ((line = br.readLine()) != null) {
                bw.write(line);
                bw.newLine(); // 写入换行符
            }

            long end = System.currentTimeMillis();
            System.out.println("文本文件复制完成!耗时:" + (end - start) + "ms");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

💡 性能优势:

  • 按行读取:readLine() 方法批量读取一行文本,效率远高于逐字符读取;
  • 缓冲优化:减少磁盘 IO 次数,复制大文本文件时效率提升明显;
  • 编码安全:通过转换流指定编码,避免乱码。
3. InputStreamReader/OutputStreamWriter:转换流

转换流是字节流与字符流的桥梁,核心功能是实现字节流到字符流的转换(处理编码):

  • InputStreamReader:将字节输入流转换为字符输入流(读数据时,字节 → 字符);
  • OutputStreamWriter:将字符输出流转换为字节输出流(写数据时,字符 → 字节)。
(1)转换流的核心作用

解决字节流读取文本时的编码问题,例如:

  • 用字节流读取 UTF-8 编码的文本文件,直接转换为字符串会乱码;
  • 通过 InputStreamReader 指定 UTF-8 编码,自动将字节转换为正确的字符。
(2)转换流实战(处理网络字节流为文本)
import java.io.*;
import java.net.Socket;

/**
 * 示例 11:转换流处理网络字节流(Socket 读取 HTTP 响应文本)
 */
public class ConvertStreamDemo {
    public static void main(String[] args) {
        try (Socket socket = new Socket("www.baidu.com", 80);
             OutputStream os = socket.getOutputStream();
             InputStream is = socket.getInputStream();
             // 字节流转换为字符流(指定 UTF-8 编码)
             InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
             BufferedReader br = new BufferedReader(isr)) {

            // 发送 HTTP 请求(字节流写入)
            String request = "GET / HTTP/1.1\r\n" +
                            "Host: www.baidu.com\r\n" +
                            "Connection: close\r\n\r\n";
            os.write(request.getBytes());
            os.flush();

            // 读取 HTTP 响应(字符流读取,无乱码)
            String line;
            System.out.println("HTTP 响应内容:");
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

💡 核心场景:

  • 网络通信:网络传输的是字节流,需转换为字符流处理文本数据(如 HTTP、TCP 协议);
  • 文件读写:字节流读取文本文件时,通过转换流指定编码,避免乱码;
  • 设备输入:如 System.in(字节流)转换为字符流,读取键盘输入。
4. StringReader/StringWriter:字符串字符流

与字节流的 ByteArrayInputStream/ByteArrayOutputStream 类似,字符串流以字符串为读写目标,适用于内存中文本数据的处理。

(1)StringReader:读取字符串
/**
 * 示例 12:StringReader 读取字符串
 */
public class StringReaderDemo {
    public static void main(String[] args) {
        String content = "Hello, StringReader!\n这是第二行文本。";
        try (StringReader sr = new StringReader(content)) {
            char[] buffer = new char[20];
            int len;
            System.out.println("读取字符串内容:");
            while ((len = sr.read(buffer)) != -1) {
                System.out.print(new String(buffer, 0, len));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
(2)StringWriter:写入字符串
/**
 * 示例 13:StringWriter 写入字符串(拼接文本)
 */
public class StringWriterDemo {
    public static void main(String[] args) {
        try (StringWriter sw = new StringWriter()) {
            sw.write("姓名:张三\n");
            sw.write("年龄:25\n");
            sw.write("职业:Java 开发工程师");

            // 获取写入的字符串
            String result = sw.toString();
            System.out.println("拼接后的字符串:");
            System.out.println(result);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

💡 核心优势:

  • 无需处理编码:字符串本身是 Unicode 编码,读写过程无编码转换;
  • 高效便捷:适用于文本拼接、模板生成、内存中文本处理等场景;
  • 线程安全:StringWriter 的方法均被 synchronized 修饰,支持多线程写入。

1.4.3 字符流与字节流的选择原则

场景推荐流类型原因
读写二进制文件(图片、视频、压缩包)字节流二进制数据无编码问题,字节流直接读写
读写文本文件(TXT、配置文件、JSON)字符流自动处理编码,避免乱码,支持按行读取
网络通信(TCP/UDP 传输文本)字符流网络字节流转换为字符流,处理编码
网络通信(传输二进制数据)字节流直接传输字节,无需编码转换
内存中数据处理(字节数组)字节流ByteArrayInputStream/ByteArrayOutputStream
内存中数据处理(字符串)字符流StringReader/StringWriter 高效便捷
对象序列化/反序列化字节流仅对象流(字节流)支持序列化

✅ 核心原则:

  • 文本数据(字符相关)→ 字符流(避免乱码,操作便捷);
  • 二进制数据(非字符相关)→ 字节流(直接读写,无编码转换)。

1.5 NIO 详解:非阻塞 IO 与高并发处理

Java NIO(New IO,JDK 1.4 引入)是对传统 IO 的升级,核心优势是非阻塞 IO多路复用,适用于高并发、大吞吐量的场景(如服务器、网络框架)。NIO 与传统 IO 的设计思想完全不同,需重点理解其核心组件与工作模式。

1.5.1 NIO 核心组件与核心概念

NIO 由三大核心组件构成:Channel(通道)、Buffer(缓冲区)、Selector(选择器),三者协同工作实现非阻塞 IO。

1. Channel(通道)
  • 定义:Channel 是 NIO 中数据传输的通道,类似于传统 IO 中的流,但支持双向传输(既可以读也可以写),且支持非阻塞模式。
  • 核心实现类
    • FileChannel:文件通道,用于文件的读写(类似文件流);
    • SocketChannel:TCP 客户端通道,用于与服务器通信;
    • ServerSocketChannel:TCP 服务器通道,用于监听客户端连接;
    • DatagramChannel:UDP 通道,用于无连接的数据传输;
  • 核心方法
    • read(Buffer buf):从通道读取数据到缓冲区;
    • write(Buffer buf):从缓冲区写入数据到通道;
    • configureBlocking(boolean block):设置通道为非阻塞模式(block = false);
    • register(Selector sel, int ops):将通道注册到选择器,监听指定事件(如读、写)。

💡 与传统流的差异:

  • 双向性:流是单向的(输入流/输出流),通道是双向的(可读可写);
  • 非阻塞:通道支持非阻塞模式,流仅支持阻塞模式;
  • 缓冲区:通道必须与缓冲区配合使用(数据先读入缓冲区,再从缓冲区写入通道)。
2. Buffer(缓冲区)
  • 定义:Buffer 是 NIO 中用于存储数据的缓冲区(数组封装),所有数据的读写都必须通过缓冲区进行(通道不直接操作数据,仅操作缓冲区)。
  • 核心实现类
    • ByteBuffer:字节缓冲区(最常用,适用于二进制数据);
    • CharBuffer:字符缓冲区(适用于文本数据);
    • IntBufferLongBuffer 等:基本类型缓冲区;
  • 核心属性(Buffer 的状态由以下三个属性控制):
    • capacity:缓冲区容量(创建时指定,不可修改);
    • position:当前操作位置(初始为 0,读写时自动递增);
    • limit:缓冲区的限制(读模式下为实际数据长度,写模式下为 capacity);
    • mark:标记位置(用于 reset() 重置 position);
  • 核心方法
    • put(T data):写入数据到缓冲区(写模式);
    • get():从缓冲区读取数据(读模式);
    • flip():切换为读模式(设置 limit = position,position = 0);
    • rewind():重置 position 为 0(重读缓冲区数据);
    • clear():清空缓冲区(position = 0,limit = capacity,数据未实际删除);
    • compact():压缩缓冲区(保留未读取的数据,移动到缓冲区开头)。

💡 Buffer 读写流程:

  1. 写模式:创建缓冲区 → put() 写入数据 → flip() 切换为读模式;
  2. 读模式:get() 读取数据 → 读取完成后,clear()(清空)或 compact()(压缩)→ 切换为写模式。
3. Selector(选择器)
  • 定义:Selector 是 NIO 实现多路复用的核心组件,用于监听多个 Channel 的事件(如读就绪、写就绪、连接就绪),一个线程即可管理多个 Channel,提升并发处理能力。
  • 核心事件(SelectionKey 定义的事件):
    • SelectionKey.OP_READ:通道可读事件;
    • SelectionKey.OP_WRITE:通道可写事件;
    • SelectionKey.OP_CONNECT:通道连接就绪事件(客户端 SocketChannel);
    • SelectionKey.OP_ACCEPT:通道接受连接事件(服务器 ServerSocketChannel);
  • 核心方法
    • open():创建选择器;
    • register(Channel ch, int ops):将通道注册到选择器,监听指定事件;
    • select():阻塞等待事件就绪(返回就绪的事件数);
    • select(long timeout):超时阻塞等待事件就绪;
    • selectedKeys():获取所有就绪的事件(SelectionKey 集合);
    • wakeup():唤醒阻塞在 select() 方法上的线程。

💡 多路复用原理:

  • 传统 IO 中,一个线程只能处理一个连接(阻塞),高并发时需要大量线程,资源消耗大;
  • NIO 中,一个线程通过 Selector 监听多个 Channel,仅当 Channel 事件就绪时才处理,线程无需阻塞等待,资源消耗低,支持高并发。

1.5.2 NIO 核心组件实战:文件读写

FileChannel 是 NIO 中用于文件读写的通道,配合 ByteBuffer 实现高效的文件操作(性能优于传统 IO 流)。

1. FileChannel 读取文件
/**
 * 示例 14:NIO FileChannel 读取文件
 */
public class NIOFileReadDemo {
    public static void main(String[] args) {
        try (RandomAccessFile raf = new RandomAccessFile("test.txt", "r");
             FileChannel channel = raf.getChannel()) {

            // 1. 创建 ByteBuffer(容量 1KB)
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            // 2. 从通道读取数据到缓冲区
            int bytesRead;
            System.out.println("NIO 读取文件内容:");
            while ((bytesRead = channel.read(buffer)) != -1) {
                // 3. 切换为读模式(limit = position,position = 0)
                buffer.flip();

                // 4. 读取缓冲区数据(转换为字符串)
                byte[] data = new byte[bytesRead];
                buffer.get(data);
                System.out.print(new String(data, StandardCharsets.UTF_8));

                // 5. 清空缓冲区(切换为写模式,准备下次读取)
                buffer.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
2. FileChannel 写入文件
/**
 * 示例 15:NIO FileChannel 写入文件
 */
public class NIOFileWriteDemo {
    public static void main(String[] args) {
        try (RandomAccessFile raf = new RandomAccessFile("nio_output.txt", "rw");
             FileChannel channel = raf.getChannel()) {

            // 1. 准备数据,写入 ByteBuffer
            String content = "Hello, NIO FileChannel!\n这是 NIO 写入的中文内容。";
            ByteBuffer buffer = ByteBuffer.allocate(content.getBytes(StandardCharsets.UTF_8).length);
            buffer.put(content.getBytes(StandardCharsets.UTF_8));

            // 2. 切换为读模式(通道从缓冲区读取数据)
            buffer.flip();

            // 3. 从缓冲区写入数据到通道(文件)
            channel.write(buffer);
            System.out.println("NIO 写入文件成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
3. FileChannel 复制文件(高效)
/**
 * 示例 16:NIO FileChannel 复制大文件(性能最优)
 */
public class NIOFileCopyDemo {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        try (FileChannel sourceChannel = new FileInputStream("large_file.zip").getChannel();
             FileChannel targetChannel = new FileOutputStream("nio_copy_large_file.zip").getChannel()) {

            // 方法 1:直接传输(推荐,底层优化,效率最高)
            sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);

            // 方法 2:缓冲区传输(手动控制缓冲区)
            // ByteBuffer buffer = ByteBuffer.allocateDirect(8192); // 直接缓冲区(堆外内存,效率更高)
            // while (sourceChannel.read(buffer) != -1) {
            //     buffer.flip();
            //     targetChannel.write(buffer);
            //     buffer.clear();
            // }

            long end = System.currentTimeMillis();
            System.out.println("NIO 复制大文件完成!耗时:" + (end - start) + "ms");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

💡 NIO 文件操作优势:

  • 高效传输:transferTo()/transferFrom() 方法直接在通道间传输数据,无需经过用户空间(零拷贝),效率远高于传统 IO;
  • 直接缓冲区:ByteBuffer.allocateDirect() 创建堆外内存缓冲区,避免 JVM 堆与内核空间的数据拷贝,提升性能;
  • 非阻塞支持:FileChannel 虽不支持非阻塞模式(文件操作天然阻塞),但其他通道(如 SocketChannel)支持,统一 NIO 编程模型。

1.5.3 NIO 非阻塞模式与 Selector 实战(TCP 服务器)

NIO 的核心价值在于非阻塞模式与 Selector 多路复用,下面通过一个简单的 TCP 服务器示例,演示 NIO 的工作流程。

1. NIO TCP 服务器核心流程

① 创建 ServerSocketChannel,绑定端口,设置为非阻塞模式;
② 创建 Selector,将 ServerSocketChannel 注册到 Selector,监听 ACCEPT 事件;
③ 循环调用 Selector 的 select() 方法,阻塞等待事件就绪;
④ 事件就绪后,遍历 SelectedKeys 集合,处理对应事件:

  • ACCEPT 事件:ServerSocketChannel 接受客户端连接,创建 SocketChannel,设置为非阻塞模式,注册到 Selector,监听 READ 事件;
  • READ 事件:SocketChannel 读取客户端数据,处理后响应;
    ⑤ 移除已处理的 SelectionKey,重复循环。
2. 实战代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

/**
 * 示例 17:NIO 非阻塞 TCP 服务器(支持多客户端并发连接)
 */
public class NIOServer {
    public static void main(String[] args) throws IOException {
        // 1. 创建 ServerSocketChannel
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(8080)); // 绑定 8080 端口
        serverChannel.configureBlocking(false); // 设置为非阻塞模式

        // 2. 创建 Selector
        Selector selector = Selector.open();

        // 3. 将 ServerSocketChannel 注册到 Selector,监听 ACCEPT 事件
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("NIO 服务器启动,监听端口 8080...");

        // 4. 循环处理事件
        while (true) {
            // 阻塞等待事件就绪(返回就绪的事件数)
            int readyChannels = selector.select();
            if (readyChannels == 0) {
                continue; // 无事件就绪,继续等待
            }

            // 5. 获取就绪的事件集合
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectedKeys.iterator();

            // 6. 遍历处理每个事件
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();

                // 处理 ACCEPT 事件(客户端连接)
                if (key.isAcceptable()) {
                    handleAccept(key, selector);
                }

                // 处理 READ 事件(客户端发送数据)
                if (key.isReadable()) {
                    handleRead(key);
                }

                // 移除已处理的事件(避免重复处理)
                iterator.remove();
            }
        }
    }

    /**
     * 处理客户端连接事件
     */
    private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        // 接受客户端连接(非阻塞,无连接时返回 null)
        SocketChannel clientChannel = serverChannel.accept();
        if (clientChannel != null) {
            clientChannel.configureBlocking(false); // 设置为非阻塞模式
            System.out.println("客户端连接成功:" + clientChannel.getRemoteAddress());

            // 注册到 Selector,监听 READ 事件,附带一个缓冲区(用于存储客户端数据)
            clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
        }
    }

    /**
     * 处理客户端读事件(读取数据并响应)
     */
    private static void handleRead(SelectionKey key) throws IOException {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment(); // 获取注册时附带的缓冲区

        // 读取客户端数据到缓冲区
        int bytesRead = clientChannel.read(buffer);
        if (bytesRead == -1) {
            // 客户端断开连接
            System.out.println("客户端断开连接:" + clientChannel.getRemoteAddress());
            clientChannel.close();
            key.cancel();
            return;
        }

        // 切换为读模式,读取数据
        buffer.flip();
        byte[] data = new byte[buffer.remaining()];
        buffer.get(data);
        String message = new String(data, StandardCharsets.UTF_8);
        System.out.println("收到客户端消息:" + message);

        // 响应客户端(发送数据)
        String response = "服务器已收到消息:" + message;
        ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8));
        clientChannel.write(responseBuffer);

        // 清空缓冲区,准备下次读取
        buffer.clear();
    }
}
3. 测试客户端(使用 Telnet 或代码测试)
  • 打开命令行,输入 telnet localhost 8080,连接服务器;
  • 输入任意字符串,服务器会响应并打印消息;
  • 可同时打开多个 Telnet 窗口,测试多客户端并发连接(服务器仅用一个线程处理所有客户端)。

💡 NIO 服务器优势:

  • 单线程处理多客户端:通过 Selector 多路复用,一个线程即可管理所有客户端连接,无需创建大量线程;
  • 非阻塞:线程无需阻塞等待客户端数据,仅在事件就绪时处理,资源消耗低;
  • 高并发:支持上万甚至几十万客户端连接(受操作系统文件描述符限制),远超传统 IO 服务器。

1.5.4 NIO 与传统 IO 的核心差异

特性传统 IO(BIO)NIO(New IO)
阻塞模式阻塞(线程等待数据/连接)非阻塞(线程无需等待,事件驱动)
并发处理一个线程处理一个连接(高资源消耗)单线程处理多连接(多路复用)
核心组件流(InputStream/OutputStream)通道、缓冲区、选择器
数据传输方式流直接读写数据通道通过缓冲区传输数据
适用场景低并发、简单 IO 操作高并发、大吞吐量(服务器、框架)
编程复杂度简单(面向流)复杂(面向缓冲区、事件驱动)
性能低(线程切换、阻塞等待开销大)高(非阻塞、零拷贝、多路复用)

✅ 核心结论:

  • 简单场景、低并发 → 传统 IO(编程简单,无需理解 NIO 复杂模型);
  • 高并发、大吞吐量 → NIO(如服务器、网络框架、中间件);
  • 现代 Java 网络框架(如 Netty)均基于 NIO 实现,封装了 NIO 的复杂性,提供更易用的 API。

1.6 IO 性能优化实战技巧

无论是传统 IO 还是 NIO,性能优化都是实际开发中的重点。本节总结常用的 IO 性能优化技巧,覆盖文件读写、网络通信等场景。

1.6.1 传统 IO 性能优化

1. 使用缓冲流包装基础流
  • 核心优化:缓冲流减少 IO 次数,提升效率(必须使用);
  • 示例:BufferedInputStream 包装 FileInputStreamBufferedReader 包装 FileReader
  • 缓冲区大小:默认 8KB,可根据文件大小调整(如大文件使用 16KB~64KB)。
2. 批量读写,避免单字节/单字符操作
  • 单字节读写效率极低(频繁触发底层 IO),优先使用 read(byte[]/char[])write(byte[]/char[])
  • 缓冲区大小建议:1KB~8KB(平衡内存占用与 IO 效率)。
3. 正确关闭流资源,避免资源泄漏
  • 使用 try-with-resources 语法(Java 7+),自动关闭流;
  • 避免在循环中创建流对象(如每次读写都创建 FileInputStream),应复用流。
4. 字符流指定编码,避免乱码与编码转换开销
  • 读写文本时显式指定编码(如 UTF-8),避免系统默认编码导致的乱码;
  • 避免频繁的编码转换(如字节 → 字符 → 字节),尽量在 IO 边界完成编码转换。
5. 内存流替代临时文件
  • 临时数据处理(如数据转换、拼接)使用 ByteArrayInputStream/ByteArrayOutputStreamStringReader/StringWriter,避免磁盘 IO 开销。

1.6.2 NIO 性能优化

1. 使用直接缓冲区(Direct Buffer)
  • 直接缓冲区位于堆外内存,避免 JVM 堆与内核空间的数据拷贝(零拷贝);
  • 适用场景:大文件传输、网络通信(频繁读写的场景);
  • 注意:直接缓冲区创建和销毁开销大,应复用,避免频繁创建。
2. 合理设置 Selector 超时时间
  • selector.select(timeout) 避免无限阻塞,超时时间建议 100~1000ms,便于处理其他任务(如心跳检测);
  • 避免使用 selector.selectNow()(非阻塞,CPU 占用高)。
3. 复用缓冲区,避免频繁创建
  • 缓冲区创建开销大,应复用(如为每个客户端连接分配一个缓冲区,而非每次读写都创建);
  • 可通过 SelectionKey.attachment() 为通道绑定缓冲区,实现复用。
4. 避免 Selector 空轮询
  • 某些 JDK 版本存在 Selector 空轮询 bug(select() 返回 0 但有事件就绪),可通过设置超时时间或检测空轮询次数解决;
  • 解决方案:记录连续空轮询次数,超过阈值时重建 Selector。
5. 使用 FileChannel 的 transferTo/transferFrom 方法
  • 文件复制、通道间数据传输优先使用这两个方法,底层实现零拷贝,效率远高于传统 IO 和手动缓冲区传输。

1.6.3 大文件处理专项优化

1. 大文件读取优化
  • 采用“分块读取”:将大文件分为多个小块,逐块读取处理,避免一次性读取整个文件到内存导致 OOM;
  • 示例:读取 1GB 文件,分 1024 块,每块 1MB,逐块处理。
2. 大文件写入优化
  • 批量写入:收集一定量的数据后批量写入,减少 IO 次数;
  • 异步写入:使用多线程异步写入(如生产者-消费者模式),提升写入吞吐量;
  • 避免同步:关闭文件系统的同步机制(如 FileChannel.force(false)),但需注意数据安全性。
3. 大文件复制优化
  • 推荐方案:NIO FileChannel + 直接缓冲区 + transferTo 方法;
  • 对比:传统 IO 复制 1GB 文件约耗时 10 秒,NIO 优化后约耗时 2 秒,效率提升 5 倍。

1.7 本章总结

本章全面讲解了 Java IO 流与 NIO 的核心知识,从传统 IO 的字节流、字符流到 NIO 的非阻塞 IO,覆盖了 IO 编程的全场景,主要总结如下:

  1. 传统 IO 流体系

    • 核心分为字节流(处理二进制数据)和字符流(处理文本数据),基于装饰器模式和适配器模式设计;
    • 字节流核心类:InputStream/OutputStream 及其子类(FileInputStream、BufferedInputStream、ObjectInputStream);
    • 字符流核心类:Reader/Writer 及其子类(FileReader、BufferedReader、InputStreamReader);
    • 选择原则:文本数据用字符流(避免乱码),二进制数据用字节流(直接读写)。
  2. IO 流实战要点

    • 必须关闭流资源(优先使用 try-with-resources);
    • 批量读写提升效率(避免单字节/单字符操作);
    • 字符流需指定编码(如 UTF-8),避免乱码;
    • 缓冲流是性能优化的核心(减少 IO 次数)。
  3. NIO 核心知识

    • 三大组件:Channel(通道)、Buffer(缓冲区)、Selector(选择器),协同实现非阻塞 IO;
    • 核心优势:非阻塞、多路复用、零拷贝,适用于高并发场景;
    • 核心实现:FileChannel(文件操作)、SocketChannel/ServerSocketChannel(网络通信);
    • 编程模型:事件驱动,单线程处理多连接,资源消耗低。
  4. 性能优化技巧

    • 传统 IO:缓冲流、批量读写、内存流替代临时文件;
    • NIO:直接缓冲区、复用缓冲区、Selector 超时设置、transferTo 零拷贝;
    • 大文件处理:分块读写、异步处理、NIO 优化。

通过本章学习,希望你能熟练掌握传统 IO 流的使用场景与技巧,理解 NIO 的核心思想与非阻塞模型,能够根据业务需求选择合适的 IO 方案,应对文件读写、网络通信、高并发等复杂场景,实现高效、安全的 IO 操作。

评论 64
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值