java输入输出流(I/O)

一、定义

Java 的输入输出流(I/O流)是用于处理输入和输出操作的一组类和接口。Java I/O 流的主要功能是读取和写入数据,通常是从文件、网络连接、控制台等各种输入输出设备或介质中

二、体系结构

1、字节流(Byte Stream)

主要用于处理二进制数据。

2、字符流(Character Stream)

​​​​主要用于处理文本数据。

三、介绍

1、字节流

InputStreamOutputStream 是字节流的两个抽象超类。它们分别用于处理字节的输入和输出操作。因为它们是抽象类,所以都包含了一些抽象方法,这些方法需要在具体的子类中进行实现

InputStream抽象方法:
1.、`int read()`:核心方法,它用于读取一个字节并返回读到的字节(范围是 `0` 到 `255` 的整数值)。如果到达流的末尾,则返回 `-1`.。每次调用read()方法后,文件指针向前就会向文章末尾方向移动一个字节,以便下次读取新的字节
2. `int read(byte[] b, int off, int len)`:将字节读取到指定的字节数组中,从指定的偏移量开始填充数组,最多读取 `len` 个字节。返回实际读取的字节数,或在到达流的末尾时返回 `-1`。有助于减少系统调用,便于批量处理,提高了读取效率
3. `long skip(long n)`:跳过和丢弃输入流中的 `n` 个字节,返回实际跳过的字节数
4. `int available()`:返回可以不受阻塞地读取的字节数
5. `void close()`:关闭此输入流并释放与该流关联的所有系统资源
6. `void mark(int readlimit)`:在输入流的当前位置标记位置。将来可以通过调用 `reset()` 方法返回到此位置
7. `void reset()`:将流重新定位到最后标记的位置
8. `boolean markSupported():测试此输入流是否支持 `mark` 和 `reset` 方法

OutputStream抽象方法:
1. `void write(int b)`:核心方法,将指定的字节写入输出流
2. `void write(byte[] b, int off, int len)`:将指定字节数组中从偏移量 `off` 开始的 `len` 个字节写入输出流
3. `void flush()`:刷新该输出流并强制写出所有缓冲的输出字节
4. `void close()`:关闭该输出流并释放与该流关联的所有系统资源

1.1 基本流

1、FileInputStream & FileOutputStream:用于从文件读取和写入字节数据
使用:
1、创建FileInputStream流对象
eg:FileInputStream fileInputStream = new FileInputStream(filePath);
2、读取数据的主要方法
-  read():可以一次读取一个字节
-  read(byte[] b) :一次读取多个字节,存入字节数组 b
-  read(byte[] b, int off, int len):从输入流中读取最多 len 个字节的数据,并将其存储到缓冲区数组 b 中,从偏移量 off 开始存储,返回实际读取的字节数

1、创建FileOutputStream流对象
eg:FileOutputStream fos = new FileOutputStream(filePath, true); true代表追加数据而不是覆盖它,可不写,默认值为false覆盖
2、写入数据的主要方法
- write(int b): 将指定的一个字节数据写入输出流
- write(byte[] b):将整个字节数组 b 写入输出流
- write(byte[] b, int off, int len):将指定字节数组 b 中从偏移量 off 开始的 len 个字节写入输出流。

2、ByteArrayInputStream & ByteArrayOutputStream:用于在内存中操作字节数组的数据流
使用:
1、创建ByteArrayInputStream对象
eg:
byte[] data = "Hello, World!".getBytes();
ByteArrayInputStream inputStream = new  ByteArrayInputStream(data);
2、读取数据的主要方法
- `read()`: 从输入流中读取下一个字节的数据。
- `read(byte[] b, int off, int len)`: 从输入流中最多读取 `len` 个字节,并将它们存储到字节数组 `b` 中,从偏移量 `off` 开始存储。
- `reset()`: 将流重新定位到字节数组的开始处。
- `available()`: 返回可以读取的字节数。

1、创建ByteArrayOutputStream对象
eg:ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
2、写入数据的主要方法
- `write(int b)`: 将指定的字节写入此输出流。
- `write(byte[] b, int off, int len)`: 将指定字节数组中从偏移量 `off` 开始的 `len` 个字节写入此输出流。
- `toByteArray()`: 返回此输出流的当前内容作为一个字节数组。
- `reset()`: 将该流的计数器重置,以便重新使用输出流中的缓冲区。

3、PipedInputStream & PipedOutputStream:用于线程之间字节数据传输(即通过一个线程将数据写入 PipedOutputStream,另一个线程可以从对应的 PipedInputStream 中读取这些数据)
使用:
1、创建连接的两个管道流对象(构造函数或或 connect方法的方式)
eg:PipedOutputStream outputStream = new PipedOutputStream();
     PipedInputStream inputStream = new PipedInputStream(outputStream);
2、写入和读取数据的主要方法
在写入线程中,通过 PipedOutputStreamwrite() 方法向管道中写入数据
在读取线程中,通过 PipedInputStreamread() 方法从管道中读取数据
注意:
管道流必须是连接状态才能使用,通常是通过构造函数或者 connect() 方法连接;
管道流主要用于线程之间的数据传输,不适合在同一线程内使用,否则会导致死锁;
读取线程必须在写入线程之后执行 read() 操作,否则会阻塞,直到有数据可读

4、SequenceInputStream:将多个 InputStream 串联成一个单一的流,读取时依次读取
使用:
1、创建对象
eg:SequenceInputStream(InputStream s1, InputStream s2) 或者
// 创建多个文件输入流
FileInputStream fileInputStream1 = new FileInputStream("file1.txt");
FileInputStream fileInputStream2 = new FileInputStream("file2.txt");
...
// 将这些流添加到 Vector 中 Vector<InputStream> streams = new Vector<>(); streams.add(fileInputStream1); streams.add(fileInputStream2); ...
// 创建一个 Enumeration
Enumeration<InputStream> e = streams.elements();
// 使用 SequenceInputStream 将这些文件流串联
SequenceInputStream sequenceInputStream = new SequenceInputStream(e);
​​​​​​2、读取数据的主要方法
从 SequenceInputStream 使用read()依次读取数据

1.2 过滤流

通过装饰设计模式等增强基本流的功能。
1、BufferedInputStream & BufferedOutputStream:字节流的高级包装类,用于提高 I/O 操作的效率
使用:
1、创建BufferedInputStream对象
eg:FileInputStream fis = new FileInputStream(inputFile);
BufferedInputStream bis = new BufferedInputStream(fis);
2、读取数据的主要方法
通过 read() 方法从内部缓冲区读取数据,如果缓冲区空了,它会一次性读取较大块的数据填充缓冲区

1、创建BufferedOutputStream对象
eg:FileOutputStream fos = new FileOutputStream(outputFile);
BufferedOutputStream bos = new BufferedOutputStream(fos);
2、写入数据的主要方法
通过 write() 方法将数据写入内部缓冲区,只有当缓冲区满了或者手动调用 flush() 方法时,数据才会真正写入文件

2、DataInputStream & DataOutputStream:用于以机器无关的方式读取和写入基本数据类型(如 intlongfloatdouble 等)的数据
使用:
1、创建DataInputStream对象
eg:DataInputStream dis = new DataInputStream(new FileInputStream("data.bin")
2、读取数据的主要方法
- `readInt()`:读取 4 字节的整数。
- `readDouble()`:读取 8 字节的双精度浮点数。
- `readUTF()`:读取使用 UTF-8 编码的字符串。

1、创建DataOutputStream对象
eg:DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.bin")
2、写入数据的主要方法
- `writeInt(int v)`:写入 4 字节的整数。
- `writeDouble(double v)`:写入 8 字节的双精度浮点数。
- `writeUTF(String str)`:以 UTF-8 编码写入字符串。

3、ObjectInputStream & ObjectOutputStream:用于对象序列化和反序列化,处理对象的输入和输出,允许程序将对象写入流中或从流中读取对象
使用:
1、创建ObjectInputStream对象
eg:FileInputStream fis = new FileInputStream("object.dat");
ObjectInputStream ois = new ObjectInputStream(fis)
2、从输入流中读取对象(反序列化)的主要方法
-readObject():从 ObjectInputStream 中读取对象,并将其反序列化为一个 Java 对象。需要将读取的对象强制转换为适当的类型
eg:MyObject obj = (MyObject) ois.readObject();
-readInt()`、`readBoolean()`、`readChar()` 等:用于读取基本数据类型的值,这些值通常是由对应的 ObjectOutputStream 方法写入的

1、创建ObjectOutputStream对象
eg:FileOutputStream fos = new FileOutputStream("object.dat");
ObjectOutputStream oos = new ObjectOutputStream(fos)
2、将对象写入到输出流(序列化)的主要方法
-writeObject(Object obj):将指定的对象写入到 ObjectOutputStream 中。对象必须实现 Serializable 接口。
eg:oos.writeObject(someObject);
-writeInt(int val)`、`writeBoolean(boolean val)`、`writeChar(int val)` 等:用于写入基本数据类型的值,作为对象数据的补充。它们分别写入整型、布尔型、字符型等

4、PushBackInputStream:将读取的数据回推到流中。对于需要预读字节数据、检查数据类型或需要在读取后可能返回的数据场景非常有用
使用:
1、创建PushBackInputStream对象
eg:PushBackInputStream pbis = new PushBackInputStream(new ByteArrayInputStream(bytes))
或者 PushbackInputStream in = new PushbackInputStream(InputStream in, int size);
size: 推回缓冲区的大小,单位是字节。
比如:PushbackInputStream in = new PushbackInputStream(new ByteArrayInputStream(new byte[]{65, 66, 67, 68}), 2);指定了缓冲区大小会为2个字节
退回俩字节:
int b1 = in.read(); // 读取字节 65 ('A') int b2 = in.read(); // 读取字节 66 ('B') // 推回两个字节 in.unread(b2); // 推回字节 66 ('B') in.unread(b1); // 推回字节 65 ('A')
当回推第三个字节则会报错IOException
2、回推读取的字节数据的主要方法
-unread(int b):将一个字节 `b` 回推到输入流中。下次调用 `read()` 时,将首先读取这个回推的字节
-unread(byte[] b, int off, int len):将字节数组 `b` 中从 `off` 位置开始的 `len` 个字节回推到输入流中。下次调用 `read()` 时,将首先读取这些回推的字节

1.3 自定义

通过继承InputStreamOutputStream类来自定义输入或输出流的行为,比如创建一个类并继承自 InputStreamOutputStream,然后覆盖其中的抽象方法,如 read()write() 方法

作用:
1. 数据过滤:可以通过自定义流在数据读取或写入的过程中添加过滤器,例如压缩、解压缩、加密、解密等。
2. 数据转换:可以在数据流中自动进行某些转换操作,例如将大写字母转换为小写,或将字节数组转换为其他格式。
3. 数据拦截:可以在流的处理中插入拦截操作,进行数据的监控或记录日志。

2、字符流

ReaderWriter是字符流的两个抽象超类。它们分别用于处理符的读取和写入操作。因为它们是抽象类,所以都包含了一些抽象方法,这些方法需要在具体的子类中进行实现

Reader抽象方法:
1. `int read()`:读取单个字符并返回。如果已经到达流的末尾,则返回 -1。
2. `int read(char[] cbuf, int off, int len)`:读取字符并将其存储到字符数组中。`off` 是起始偏移量,`len` 是读取的最大字符数。返回实际读取的字符数,如果到达末尾,返回 -1。
3. `void close()`:关闭流并释放与流相关的资源。

Writer抽象方法:
1. `void write(int c)`:写入单个字符。
2. `void write(char[] cbuf, int off, int len)`:将字符数组中的一部分字符写入输出流。`off` 是起始偏移量,`len` 是写入的字符数。
3. `void flush()`:刷新流,强制将缓冲区的内容写入到目标设备。
4. `void close()`:关闭流并释放与流相关的资源。

2.1 基本流

1、FileReader & FileWriter:用于读取和写入文本文件
使用:
1、创建FileReader对象
eg:FileReader reader = new FileReader("example.txt")
2、读取数据的主要方法
-`read()`:读取单个字符并返回其对应的 Unicode 编码值。如果已到达文件末尾,则返回 `-1`。
-`read(char[] cbuf, int offset, int length)`:将字符读入数组 `cbuf` 中,从偏移量 `offset` 开始,最多读取 `length` 个字符。返回实际读取的字符数,如果已到达文件末尾,则返回 `-1`。
-`ready()`:检查流是否准备好被读取。如果流在关闭前有字符可供读取,则返回 `true`,否则返回 `false`。

1、创建FileWriter对象
eg:FileWriter writer = new FileWriter("example.txt")
2、写入数据的主要方法
- `void write(int c)`:将指定的字符写入到文件中。参数 `c` 是要写入的字符的 Unicode 编码值。
- `void write(char[] cbuf, int offset, int length)`:将指定的字符数组的一部分写入文件。`cbuf` 是要写入的字符数组,`offset` 是起始偏移量,`length` 是要写入的字符数。
 - `void write(String str)`:将指定的字符串写入文件。

2、CharArrayReader & CharArrayWriter:用于从字符数组中读取数据和向字符数组中写入数据
使用:
1、创建CharArrayReader对象
eg:char[] charArray = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'};
CharArrayReader charArrayReader = new CharArrayReader(charArray)
2、读取字符数据的主要方法
-`int read()` :读取单个字符,并返回该字符的整数表示。如果到达流的末尾,则返回 -1。
-`int read(char[] b, int off, int len)` :将字符读入指定的数组 `b` 中,从数组的 `off` 偏移量开始,最多读取 `len` 个字符。返回实际读取的字符数,如果到达流的末尾则返回 -1。
-`boolean ready()`:判断流是否准备好被读取。如果 `ready()` 返回 `true`,则意味着后续的 `read()` 调用不会阻塞。
-`long skip(long n)`:跳过 `n` 个字符,不读取这些字符。返回实际跳过的字符数。
-`void close()`:关闭流。关闭后,不能再读取该流中的字符。
-`boolean markSupported()`:判断当前流是否支持 `mark()` 和 `reset()` 方法。对于 `CharArrayReader`,总是返回 `true`。
-`void mark(int readAheadLimit)` :在流中标记当前位置。随后可以通过 `reset()` 方法返回到标记的位置。
-`void reset()`:将流重置到最后标记的位置。可以多次调用 `reset()` 返回到相同的标记点。

1、创建CharArrayWriter对象
eg:CharArrayWriter charArrayWriter = new CharArrayWriter();
2、写入字符数据的主要方法
-`void write(int c)`:将单个字符写入字符数组。
-`void write(char[] c, int off, int len)`:将指定的字符数组 `c` 中从 `off` 偏移量开始的 `len` 个字符写入字符数组。
-`void write(String str)`:将一个字符串写入字符数组。
-`void writeTo(Writer out)`:将当前 `CharArrayWriter` 中的内容写入到另一个字符输出流中。
-`CharArrayWriter append(CharSequence csq)`:将指定的字符序列追加到当前字符数组中。返回当前的 `CharArrayWriter` 对象,以便支持链式调用。
-`CharArrayWriter append(CharSequence csq, int start, int end)` :将指定字符序列的一个子序列追加到当前字符数组中。返回当前的 `CharArrayWriter` 对象,以便支持链式调用。
-`CharArrayWriter append(char c)`:将单个字符追加到当前字符数组中。返回当前的 `CharArrayWriter` 对象,以便支持链式调用。
-`void reset()`: 清空当前 `CharArrayWriter` 中的缓冲区,使其可以重新使用。
-`char[] toCharArray()`:返回当前缓冲区中的字符数组的副本。
-`int size()` :返回当前缓冲区中字符的数量。
-`String toString()`:返回当前缓冲区内容的字符串表示形式。
-`void close()` :关闭流。`CharArrayWriter` 的 `close()` 方法不会实际关闭流,因为它只是清空缓冲区的内容,但不会影响其继续使用。

3、StringReader & StringWriter:在内存中处理字符串数据
使用:
1、创建StringReader对象
eg:String data = "Hello, World!";
StringReader stringReader = new StringReader(data);
2、读取字符串主要方法
- `int read()`:从 `StringReader` 中读取单个字符,并返回对应的字符的整数值。如果到达字符串的末尾,则返回 `-1`。
- `int read(char[] cbuf, int offset, int length)`:将字符读入到一个字符数组 `cbuf` 中,从指定的 `offset` 位置开始,最多读取 `length` 个字符。返回实际读取的字符数,如果到达字符串末尾,则返回 `-1`。
- `long skip(long n)`:跳过并丢弃 `n` 个字符,返回实际跳过的字符数。
- `boolean ready()`:判断流是否准备好被读取。如果可以读取,则返回 `true`,否则返回 `false`。
- `void close()`:关闭 `StringReader`,释放相关资源。关闭后,不能再从此流中读取数据。

1、创建StringWriter对象
eg:StringWriter stringWriter = new StringWriter();
2、将字符数据写入一个内部的字符串缓冲区的主要方法
- `void write(int c)`:将单个字符写入 `StringWriter` 的缓冲区中。
- `void write(char[] cbuf, int offset, int length)`:将字符数组 `cbuf` 中从 `offset` 位置开始的 `length` 个字符写入到 `StringWriter` 中。
- `void write(String str)`:将一个字符串 `str` 写入到 `StringWriter` 中。
- `String toString()`:返回缓冲区中当前的字符串内容。这是 `StringWriter` 最常用的方法,用来获取所有写入的数据。
- `void flush()`:刷新流,这里 `StringWriter` 由于是写入到内存中的字符串缓冲区,所以 `flush()` 通常不需要显式调用。
- `void close()`:关闭 `StringWriter`,但实际上对 `StringWriter` 没有实际影响,通常可以忽略该操作。

3、PipedReader & PipedWriter:用于线程之间字节数据传输(即通过一个线程将数据写入 PipedWriter,另一个线程可以从对应的 PipedReader 中读取这些数据)
使用:
1、创建连接的两个管道流对象(构造函数或或 connect方法的方式)
eg:PipedReader reader = new PipedReader();
PipedWriter writer = new PipedWriter();
// 连接PipedReader和PipedWriter
writer.connect(reader);
2、读取数据的主要方法
-read():从管道中读取一个字符。如果没有数据可读取,当前线程将阻塞,直到有数据可读或管道关闭
- read(char[] cbuf, int off, int len):将最多 `len` 个字符的数据读入字符数组 `cbuf` 中,从偏移量 `off` 开始填充。如果没有足够的数据可读,则会阻塞,直到有足够数据或管道关闭。
3、写入数据的主要方法
-write(int c):将一个字符写入到管道。如果管道没有连接,或者如果管道的缓冲区已满,当前线程将阻塞,直到可以继续写入
-write(char[] cbuf, int off, int len):将字符数组 `cbuf` 中的 `len` 个字符从偏移量 `off` 开始写入到管道中。如果管道缓冲区已满,当前线程将阻塞,直到可以继续写入
-flush():刷新该流并强制写出所有缓冲的输出字符。通常不需要显式调用 `flush()`,因为在写入完毕后通常会调用 `close()`,这也会刷新缓冲区

2.2 过滤流

通过装饰设计模式等增强基本流的功能。
1、 BufferedReader &  BufferedWriter:字符流的高级包装类,用于提高 I/O 操作的效率
使用:
1、创建BufferedReader对象
eg:BufferedReader br = new BufferedReader(new FileReader("example.txt"))
2、从字符输入流读取字符的主要方法
-read():一次读取一个字符
-readLine():读取一行文本,返回一个字符串(不包含行终止符)。如果已经达到流的末尾,则返回 null

1、创建BufferedWriter对象
eg:BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))
2、将字符写入到字符输出流的主要方法
-write(String str):将指定的字符串写入字符输出流
-newLine():写入一个行终止符,跨平台兼容(不同的系统可能有不同的行终止符)。
-flush():将缓冲区中的内容写入目标流中,确保数据被写出

2、LineNumberReader:用于处理带行号的文本数据
使用:
1、创建LineNumberReader对象
eg:LineNumberReader lineNumberReader = new LineNumberReader(new FileReader("example.txt"))
2、读取文本文件并获取当前读取行的行号的主要方法
-getLineNumber():返回当前的行号。
-setLineNumber(int lineNumber):设置行号。
-readLine():读取一行文本。

3、PrintWriter:是一个字符输出流,它提供了打印各种数据表示形式的方法
使用:
1、创建PrintWriter对象
eg:PrintWriter writer = new PrintWriter("output.txt",true);
true参数表示自动刷新(即在调用 printlnprintf 后自动调用 flush() 方法)
2、打印数据的主要方法
-print():将各种数据类型直接写入输出流而不附加任何行分隔符
-println():与 print() 方法类似,但在输出数据后会自动附加一个行分隔符(通常是换行符 \n
-format()printf():用于格式化输出。这两个方法的作用相同
eg:printWriter.printf("Name: %s, Age: %d", "Alice", 30);  // 输出 "Name: Alice, Age: 30"
-flush():强制将缓冲区中的数据写入到目标输出流。如果你在写入数据后没有调用 flush(),数据可能不会立即写入目标(尤其是在使用缓冲输出流时)
-write():与 print()println() 方法类似(但 write() 方法不会将数据转换为字符串,也不会附加行分隔符),于直接写入字符或字符数组到输出流,

4、PushBackReader:允许在读取字符后将它们推回到流中
使用:
1、创建PushBackReader对象
eg:PushbackReader pushbackReader = new PushbackReader(new StringReader(str)) 或者
PushbackReader reader = new PushbackReader(Reader in, int size);
size: 是推回缓冲区的大小。默认大小是1,但你可以指定更大的值,以推回多个字符
2、回推读取的字符数据的主要方法
-unread(int c):将一个字符 c 推回到流中,使得下次读取时首先读取这个字符。如果推回的字符数超过了缓冲区的大小(构造方法中的size),会抛出 IOException
-unread(char[] cbuf, int off, int len):将字符数组 cbuf 中从偏移量 off 开始的 len 个字符推回到流中。如果推回的字符数超过了缓冲区的大小(构造方法中的size),会抛出 IOException

2.3 自定义

通过继承ReaderWriter类来自定义输入或输出流的行为,比如创建一个类并继承自 ReaderWriter,然后覆盖其中的抽象方法,如 read()write() 方法

作用:
1. 数据过滤:可以通过自定义流在数据读取或写入的过程中添加过滤器,例如压缩、解压缩、加密、解密等。
2. 数据转换:可以在数据流中自动进行某些转换操作,例如将大写字母转换为小写,或将字节数组转换为其他格式。
3. 数据拦截:可以在流的处理中插入拦截操作,进行数据的监控或记录日志。

  • 25
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java输入输出流(I/O)提供了在程序中读取和写入数据的方式。它们是通过处理(或过滤器)的方式工作的,这些允许你对数据进行处理和转换。 常用的输入输出流有: 1. FileInputStream:用于从文件中读取字节。 2. FileOutputStream:用于向文件中写入字节。 3. FileReader:用于从文件中读取字符。 4. FileWriter:用于向文件中写入字符。 5. ByteArrayInputStream:用于从内存中读取字节。 6. ByteArrayOutputStream:用于向内存中写入字节。 7. CharArrayReader:用于从字符数组中读取字符。 8. CharArrayWriter:用于向字符数组中写入字符。 9. StringReader:用于从字符串中读取字符。 10. StringWriter:用于向字符串中写入字符。 下面是一个简单的示例,演示了如何使用FileInputStream和FileOutputStream来复制一个文件: ```java import java.io.*; public class FileCopyDemo { public static void main(String[] args) throws IOException { FileInputStream in = null; FileOutputStream out = null; try { in = new FileInputStream("input.txt"); out = new FileOutputStream("output.txt"); int c; while ((c = in.read()) != -1) { out.write(c); } } finally { if (in != null) { in.close(); } if (out != null) { out.close(); } } } } ``` 这个程序将从名为input.txt的文件中读取字节,并将它们写入名为output.txt的文件中。在读取和写入完成后,程序会关闭这两个。 需要注意的是,在使用输入输出流时,要确保正确地处理异常和关闭。可以使用try-with-resources语句来简化这个过程,如下所示: ```java try (FileInputStream in = new FileInputStream("input.txt"); FileOutputStream out = new FileOutputStream("output.txt")) { int c; while ((c = in.read()) != -1) { out.write(c); } } catch (IOException e) { e.printStackTrace(); } ``` 在此示例中,使用了try-with-resources语句来创建和管理输入输出流。当try块结束时,会自动关闭这两个,从而避免了显式地关闭的需要。 除了上述示例中使用的之外,Java 还提供了许多其他的输入输出流,以满足不同的需求。你可以根据需要选择适合自己的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值