Java-基础IO整理

Java-IO

1. File文件操作类

在 java.io 包之中,用 File 类来对文件进行操作(创建、删除、取得信息等)

1.1 File类使用

可以先看看官网文档中关于File类的说明
https://docs.oracle.com/javase/8/docs/api/
重点关注构造函数
java.io.File 类是一个普通的类,如果要实例化对象,则常用到两个构造方法

方法解释
public File(String pathname)创建指定路径文件对象
public File(String parent, String child)同上,但可指明父路径和子路径

1.2 File类常用方法-基本文件操作

方法说明
public boolean exists()测试指定路径中文件或者目录是否存在
public boolean isDirectory()判定是否是为目录
public boolean isFile()判定是否是为文件
public boolean delete()删除文件
public boolean createNewFile() throws IOException创建一个新文件

示例 创建文件:

import java.io.File;
import java.io.IOException;
import java.util.Scanner;

public class CreateFile {
    public static void main(String[] args) throws IOException {
        Scanner sc = new Scanner(System.in);
        //创建的只是对象,没有创建文件
        File file = new File("FileTest.txt");

        System.out.println("File 对象创建成功,输入回车创建文件");
        sc.nextLine();

        file.createNewFile();
        System.out.println("文件创建成功");
    }
}

1.3 File类常用方法-目录操作

方法解释
public boolean mkdir()创建一个空目录
public boolean mkdirs()创建目录(无论有多少级父目录,都会创建)
public String getParent()取得父路径
public File getParentFile()取得父File对象

示例 创建目录并删除:

import java.io.File;
import java.util.Scanner;

public class MakeDirectory {
    public static void main(String[] args) {

        Scanner sc = new Scanner(System.in);

        File file = new File("NewDir");
        System.out.println("创建对象");
        sc.nextLine();
        file.mkdir();
//        file.mkdirs();   创建多级目录
        System.out.println("文件夹创建成功");
        sc.nextLine();
        System.out.println("文件夹删除");
        delete();
        sc.nextLine();
    }

    public static void delete() {
        File file = new File("NewDir");
        file.delete();
    }
}

说明:键入回车后会看到当前工作目录新建了目录文件NewDir,再次键入回车之后会看到目录被删除;
值得一提的是,删除目录文件的时候,当目录不为空的时候,目录是无法删除的,想要详细了解的可以查阅官方文档

1.4 File类常用方法-文件属性操作

方法解释
public long length()取得文件大小(字节)
public long lastModified()最后一次修改日期

示例 取得文件信息:

import java.io.File;
import java.io.IOException;
import java.util.Date;
public class FileDemo {

    public static void main(String[] args) throws IOException {
        String path = "E:\\java_code\\file\\";
        String name = "demo.txt";//可以替换成你想检测的文件
        String pathname = path + name;
        File file = new File(pathname);
        if (!file.exists()) {
            file.createNewFile();
        }
        System.out.println("文件 " + name + "size : " + file.length());

        System.out.println("文件 " + name + "最近修改时间: " + new Date(file.lastModified()));
    }
}

1.5 File类常用方法-其他操作

方法解释
public File[] listFiles()列出一个目录指定的全部组成

示例:列出目录中的全部组成

import java.io.File;

public class LsFiles {
    public static void main(String[] args) {

        File file = new File(".");
        File[] files = file.listFiles();
        if (files == null){
            System.out.println("error, This is not a dir.");
        }
        if (files.length == 0) {
            System.out.println("This dir is empty.");
        }

        for (File f : files) {
            System.out.println(f.getAbsolutePath());
        }
    }
}

1.6 综合-打印指定文件下目录列表

虽然File提供有 listFiles() 方法,但是这个方法本身只能够列出本目录中的第一级信息。如果要求列出目录中所有级的信息,必须自己来处理。这种操作就必须通过递归的模式来完成。
示例:

import java.io.File;
import java.util.LinkedList;
import java.util.Queue;

public class 遍历目录下的所有文件 {
    public static void main(String[] args) {
        String path = ".";
        File root = new File(path);

        DFS(root);    //深度优先遍历
        System.out.println("======================================");
        BFS(root);	  //广度优先遍历
    }

    private static void BFS(File root) {
        Queue<File> queue = new LinkedList<>();
        queue.offer(root);

        while (!queue.isEmpty()) {
            File file = queue.poll();
            System.out.println(file.getAbsolutePath());
            if (file.isDirectory()) {
                File[] files = file.listFiles();
                if (files != null) {
                    for (File f :
                            files) {
                        queue.offer(f);
                    }
                }
            }
        }
    }

    private static void DFS(File root) {
        System.out.println(root.getAbsolutePath());
        //root is not dir OR root is empty -> end
        if (root.isFile()) {
            return;
        }
        if (!root.isDirectory()) {
            return;
        }
        File[] files = root.listFiles();
        if (files == null) {
            return;
        }
        if (files.length == 0) {
            return;
        }
        for (File file : files) {
            DFS(file);
        }
    }
}

2. 流

2.1 流的概念

流:在 Java中所有数据都是使用流读写的。流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。

1.按照流向分:输入流;输出流
2.按照处理数据的单位分:字节流(8位的字节);字符流(16位的字节)

2.2 什么是输入输出流

输入就是将数据从各种输入设备(包括文件、键盘等)中读取到内存中。
输出则正好相反,是将数据写入到各种输出设备(比如文件、显示器、磁盘等)。
例如键盘就是一个标准的输入设备,而显示器就是一个标准的输出设备,但是文件既可以作为输入设备,又可以作为输出设备。

2.3 什么是字节流,字符流

File类不支持文件内容处理,如果要处理文件内容,必须要通过流的操作模式来完成。
在java.io包中,流分为两种:字节流与字符流

  • 字节流:数据流中最小的数据单元是字节 。InputStream、OutputStream
  • 字符流:数据流中最小的数据单元是字符。Reader、Writer

在这里插入图片描述

2.4 字节流

1、 FileInputStreamFileOutputStream

public class FileInputStream extends InputStream {}

  • FileInputStream 从文件系统中的某个文件中获得输入字节。
  • FileInputStream 用于读取诸如图像数据之类的原始字节流。
方法解释
FileInputStream(File file)通过打开与实际文件的连接创建一个 FileInputStream ,该文件由文件系统中的 File 对象 file 命名
FileInputStream(String name)通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name 命名

public class FileOutputStream extends OutputStream

  • 文件输出流是用于将数据写入到输出流 File 或一个 FileDescriptor 。 文件是否可用或可能被创建取决于底层平台。
  • 特别是某些平台允许一次只能打开一个文件来写入一个 FileOutputStream (或其他文件写入对象)。 在这种情况下,如果所涉及的文件已经打开,则此类中的构造函数将失败。
方法解释
FileOutputStream(File file)创建文件输出流以写入由指定的 File 对象表示的文件
FileOutputStream(String name)创建文件输出流以指定的名称写入文件

示例使用 FileInputStream 和 FileOutputStream 复制图片:

在这里插入代码片

2、字节缓冲流 BufferedInputStreamBufferedOutputStream

问题 :为什么需要有缓冲流?

答:当我们用read()读取文件时,每读一个字节,访问一次硬盘,效率很低。文件过大时,操作起来也不是很方便。因此我们需要用到buffer缓存流,当创建buffer对象时,会创建一个缓冲区数组。当我们读一个文件时,先从硬盘中读到缓冲区,然后直接从缓冲区输出即可,效率会更高。

public class BufferedInputStream extends FilterInputStream

  • BufferedInputStream 为另一个输入流添加了功能,即缓冲输入和支持 mark 和 reset 方法的功能。 当创建 BufferedInputStream 时,将创建一个内部缓冲区数组。
  • 当从流中读取或跳过字节时,内部缓冲区将根据需要从所包含的输入流中重新填充,一次有多个字节。 mark 操作会记住输入流中的一点,并且 reset 操作会导致从最近的 mark 操作之后读取的所有字节在从包含的输入流中取出新的字节之前重新读取。
方法解释
BufferedInputStream(InputStream in)创建一个 BufferedInputStream 并保存其参数,输入流 in ,供以后使用
BufferedInputStream(InputStream in, int size)创建 BufferedInputStream 具有指定缓冲区大小,并保存其参数,输入流 in ,供以后使用

public class BufferedOutputStream extends FilterOutputStream

  • 该类实现缓冲输出流。 通过设置这样的输出流,应用程序可以向底层输出流写入字节,而不必为写入的每个字节导致底层系统的调用。
方法解释
BufferedOutputStream(OutputStream out)创建一个新的缓冲输出流,以将数据写入指定的底层输出流
BufferedOutputStream(OutputStream out, int size)创建一个新的缓冲输出流,以便以指定的缓冲区大小将数据写入指定的底层输出流

示例使用 BufferedInputStreamBufferedOutputStream实现文件拷贝:

import java.io.*;
public class TestBufferStreamCopy {
    public static void main(String[] args) throws IOException {
        File file=new File("bit.txt");
        if(!file.isFile()){
            return;
        }
        try (BufferedInputStream bfis = new BufferedInputStream(new FileInputStream(file))) {
            try (BufferedOutputStream bfos = new BufferedOutputStream(new FileOutputStream("src\\" + file.getName()))) {
                //copy到src目录下
                byte bytes[] = new byte[1024];
                int temp = 0;       //边读边写
                while ((temp = bfis.read(bytes)) != -1) {       //读
                    bfos.write(bytes, 0, temp);     //写
                }
                bfos.flush();
            }
        }
        System.out.println("copy成功!");
    }
}

此处使用try-with-resources结构,省去了一些不必要的资源管理步骤。

2.5 字符流

1、字符流 FileReaderFileWriter

public class FileReader extends InputStreamReader

  • 如果要从文件中读取内容,可以直接使用 FileReader 子类。
  • FileReader 是用于读取字符流。 要读取原始字节流,请考虑使用 FileInputStream
方法解释
FileReader(File file)创建一个新的 FileReader ,给出 File 读取
FileReader(String fileName)创建一个新的 FileReader ,给定要读取的文件的名称

public class FileWriter extends OutputStreamWriter

  • 如果是向文件中写入内容,应该使用 FileWriter 子类
  • FileWriter 是用于写入字符流。 要编写原始字节流,请考虑使用 FileOutputStream
方法解释
FileWriter(File file)给一个File对象构造一个FileWriter对象
FileWriter(String fileName)构造一个给定文件名的FileWriter对象

示例使用 FileReaderFileWriter 复制文件:

public class CopyFileDemo {
    public static void main(String[] args) throws IOException {//创建输入流对象
        try (FileReader fr = new FileReader("E:\\bit\\bitSrc.java")) {
            //创建输出流对象
            try (FileWriter fw = new FileWriter("E:\\bit\\bitCopy.java")) {
                //读写数据
                int ch;
                while ((ch = fr.read()) != -1) {
                    fw.write(ch);
                }
            }
        }
    }
}

2、字符缓冲流 BufferedReaderBufferedWriter

为了提高字符流读写的效率,引入了缓冲机制,进行字符批量的读写,提高了单个字符读写的效率。
BufferedReader 用于加快读取字符的速度, BufferedWriter用于加快写入的速度。
BufferedReaderBufferedWriter类各拥有 8192个 字符的缓冲区。当 BufferedReader在 读取文本文件时,会先尽量从文件中读入字符数据并放满缓冲区,而之后若使用read()方法,会先从缓冲区中进行读取。如果缓冲区数据不足,才会再从文件中读取,使用 BufferedWriter 时,写入的数据并不会先输出到目的地,而是先存储至缓冲区中。如果缓冲区中的数据满了,才会一次对目的地进行写出。

public class BufferedReader extends Reader

  • 从字符输入流读取文本,缓冲字符,以提供字符,数组和行的高效读取
解释方法
BufferedReader(Reader in)创建使用默认大小的输入缓冲区的缓冲字符输入流
BufferedReader(Reader in, int sz)创建使用指定大小的输入缓冲区的缓冲字符输入流

public class BufferedWriter extends Writer

  • 将文本写入字符输出流,缓冲字符,以提供单个字符,数组和字符串的高效写入。
方法解释
BufferedWriter(Writer out)创建使用默认大小的输出缓冲区的缓冲字符输出流
BufferedWriter(Writer out, int sz)创建一个新的缓冲字符输出流,使用给定大小的输出缓冲区

示例使用 BufferedReaderBufferedWriter 进行文件拷贝

import java.io.*;

public class FileWrite {
    public static void main(String[] args) throws IOException {
        FileWriter writer;
        BufferedWriter bWriter;
        try (FileReader reader = new FileReader("E:\\BIT\\bit.txt")) {
            try (BufferedReader bReader = new BufferedReader(reader)) {
                writer = new FileWriter("E:\\BIT\\bit2.txt");
                bWriter = new BufferedWriter(writer);
                String content = "";
                //readLine一行一行的读取
                while ((content = bReader.readLine()) != null) {
                    bWriter.write(content + "\r\n");
                }
            }
        }
        bWriter.close();
        writer.close();
    }
}

2.6 字节流对比字符流

1、字节流操作的基本单元是字节;字符流操作的基本单元为Unicode码元。
2、字节流在操作的时候本身不会用到缓冲区的,是与文件本身直接操作的;而字符流在操作的时候使用到缓冲区的。
3、所有文件的存储都是字节(byte)的存储,在磁盘上保留的是字节。
4、在使用字节流操作中,即使没有关闭资源(close方法),也能输出;而字符流不使用close方法的话,不会输出任何内容。

2.7 字符字节转换流

有时候我们需要进行字节流与字符流二者之间的转换,因为这是两种不同的流,所以,在进行转换的时候我们需要用到 OutputStreamWriterInputStreamReader
InputStreamReader 是Reader的子类,将输入的字节流转换成字符流。

public class InputStreamReader extends Reader

  • InputStreamReader是从字节流到字符流的桥:它读取字节,并使用指定的 charset 将其解码为字符 。
  • 它使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集。
方法解释
InputStreamReader(InputStream in)创建一个使用默认字符集的InputStreamReader
InputStreamReader(InputStream in, Charset cs)创建一个使用给定字符集的InputStreamReader

OutputStreamWriterWriter的子类,将输出的字符流转换成字节流。

public class OutputStreamWriter extends Writer

  • OutputStreamWriter是字符的桥梁流以字节流:向其写入的字符编码成使用指定的字节charset 。
  • 它使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集。
方法解释
OutputStreamWriter(OutputStream out)创建一个使用默认字符编码的OutputStreamWriter
OutputStreamWriter(OutputStream out,Charset cs)创建一个使用给定字符集的OutputStreamWriter

示例使用 InputStreamReaderOutputStreamWriter 进行文件编码转换

import java.io.*;

public class IConv {
    public static void main(String[] args) throws IOException {

        if (args.length < 4) {
            System.out.println("请在使用时附带如下参数:");
            System.out.println("IConv 源字符集 目标字符集 源文件 目标文件");
            return;
        }

        String fromCharset = args[0];
        String toCharset = args[1];
        String fromPath = args[2];
        String toPath = args[3];

        try (Reader reader = new InputStreamReader(new FileInputStream(fromPath), fromCharset)) {
            try (Writer writer =new OutputStreamWriter(new FileOutputStream(toPath), toCharset)) {
                char[] buffer = new char[4096];
                int len;

                while ((len = reader.read(buffer)) != -1) {
                    writer.write(buffer, 0, len);
                }
                writer.flush();
            }
        }
    }
}

2.8 综合-复制目标到指定目录下

复制目标目录下的目录结构以及文件内容到指定目录下

import java.io.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class DirectoryCopy {
    // 利用线程池提交任务的形式向线程池提交文件复制任务以提升速度
    private static class CopyFileTesk implements Runnable {
        private final String srcPath;
        private final String destPath;

        CopyFileTesk(String srcPath, String destPath) {
            this.srcPath = srcPath;
            this.destPath = destPath;
        }

        @Override
        public void run() {
            try {
                copyFile(srcPath, destPath);
                System.out.println(destPath + "--> file copy successful");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private static String srcAbsPath;
    private static String destAbsPath;
    private static ExecutorService theadPool;

    public static void main(String[] args) throws IOException {
        if (args.length < 2) {
            System.out.println("请在使用时附带如下参数:");
            System.out.println("DirectoryCopy 源目录 目标目录");
            return;
        }

        theadPool = Executors.newFixedThreadPool(20);

        String srcPath = args[0];
        String destPath = args[1];
        File srcFile = new File(srcPath);
        File destFile = new File(destPath);
        srcAbsPath = srcFile.getAbsolutePath();
        destAbsPath = destFile.getAbsolutePath();

        // TODO: 检查源文件是否存在,是否是目录
        // TODO: 检查目标文件是否不存在
        // TODO: 检查目标文件的上一级目录是否存在

        directoryCopy(srcPath, destPath);

        theadPool.shutdown();
    }

    /**
     * 遍历所有的目录结构
     * 如果是目录,随着创建目录
     * 如果是文件,调用复制文件的方式进行复制
     * @param srcPath
     * @param destPath
     * @throws IOException
     */
    private static void directoryCopy(String srcPath, String destPath) throws IOException {
        File srcRoot = new File(srcPath);
        dfsDir(srcRoot);
    }

    private static void doDir(File dir) {
        String srcPath = dir.getAbsolutePath();
        String relativePath = srcPath.substring(srcAbsPath.length());
        String destPath = destAbsPath + relativePath;

        File destDir = new File(destPath);
        destDir.mkdir();         // 因为文件树从上往下遍历,所以,上一级的目录一定被创建了
        System.out.println(destPath + "--> dir create successful");
    }

    private static void doFile(File file) throws IOException {
        String srcPath = file.getAbsolutePath();
        String relativePath = srcPath.substring(srcAbsPath.length());
        String destPath = destAbsPath + relativePath;

        theadPool.execute(new CopyFileTesk(srcPath, destPath));
//        copyFile(file.getAbsolutePath(), destPath);
//        System.out.println(destPath + "--> file copy successful");
    }

    private static void copyFile(String srcPath, String destPath) throws IOException {
        try (InputStream is = new FileInputStream(srcPath)) {
            try (OutputStream os = new FileOutputStream(destPath)) {
                byte[] buffer = new byte[4096];
                int len;

                while ((len = is.read(buffer)) != -1) {
                    os.write(buffer, 0, len);
                }
                os.flush();
            }
        }
    }

    private static void doNode(File node) throws IOException {
        if (node.isDirectory()) {
            doDir(node);
        } else {
            doFile(node);
        }
    }

    private static void dfsDir(File root) throws IOException {
        doNode(root);  //被遍历的每个节点,都需要被调用

        if (root.isDirectory()) {
            File[] files = root.listFiles();
            if (files != null) {
                for (File f : files) {
                    dfsDir(f);
                }
            }
        }
    }
}

如图,指定要复制的目录和目标目录
在这里插入图片描述
结果如下:
在这里插入图片描述
可以看到文件复制完成

3. 序列化与反序列化

3.1 什么是序列化和反序列化

序列化:把对象转换为字节序列的过程称为对象的序列化。
反序列化:把字节序列恢复为对象的过程称为对象的反序列化。
有时候我们想把一些信息持久化保存起来,那么序列化的意思就是把内存里面的这些对象给变成一连串的字节描述的过程。 常见的就是变成文件。但是问题来了,我就算不序列化,也可以保存到文件当中。
有什么问题吗?
对象的序列化就是为了数据传输,在你的代码的里是对象格式,而在传输的时候不可能还保持这对象的样子。
当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

3.2 什么时候需要序列化

记住以下几点:
1、把内存中的对象状态保存到一个文件中或者数据库中时候;
2、 用套接字在网络上传送对象的时候;

3.3 实现序列化的方式

实现序列化本身是跟语言无关的:
列举出一些其他实现序列化的方式:
0、Java对象序列化
1、JSON序列化
2、XML
3、Protostuff
4、Hession(它基于HTTP协议传输,使用Hessian二进制序列化,对于数据包比较大的情况比较友好。)
5、Dubbo Serialization(阿里dubbo序列化)
6、FST(高性能、序列化速度大概是JDK的4-10倍,大小是JDK大小的1/3左右)
7、自定义协议进行序列化

3.4 如何实现序列化(Java对象序列化)

如下代码,我们自己实现一个Person类,在类当中定义成员变量 name , age , sex , stuId ,count ,如果要讲Person进行序列化需要实现Serializable接口即可 。

import java.io.Serializable

class Person implements Serializable{

    //private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    private String sex;
    //transient修饰的变量,不能被序列化
    transient private int stuId;
    //static修饰的变量,不能被序列化
    private static int count = 99;
    public String getName() {
        return name;
    }
    public int getAge() {
        return age;
    }
    public String getSex() {
        return sex;
    }
    public int getStuId() {
        return stuId;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public void setSex(String sex) {
        this.sex = sex;
    }
    public void setStuId(int stuId) {
        this.stuId = stuId;
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                ", stuId=" + stuId +
                ", count=" + count +
                '}';
    }
}

序列化反序列化测试:

public class TestDemo3 {
    public static void main(String[] args) throws Exception {
        serializePerson();
        Person person = deserializePerson();
        System.out.println(person.toString());
    }
    /**
     * 序列化
     */
    private static void serializePerson() throws IOException {
        Person person = new Person();
        person.setName("bit");
        person.setAge(10);
        person.setSex("男");
        person.setStuId(100);
        // ObjectOutputStream 对象输出流,将 person 对象存储到E盘的
        // person.txt 文件中,完成对 person 对象的序列化操作
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("person.txt")))) {
            oos.writeObject(person);
            System.out.println("person 对象序列化成功!");
        }
    }
    /**
     * 反序列化
     */
    private static Person deserializePerson() throws Exception {
        Person person;
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("person.txt")))) {
            person = (Person) ois.readObject();
        }
        System.out.println("person 对象反序列化成功!");
        return person;
    }
}

相关流的说明:
ObjectOutputStream 代表对象输出流: 它的 writeObject(Object obj) 方法可对参数指定的 obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
ObjectInputStream 代表对象输入流:
它的 readObject() 方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
分析:
transient修饰的属性,是不会被序列化的 。我们的 stuId 本应是100,可现在为0.注意:内置类型为对应0值。引用类型为null;
静态变量的值是不会被进行序列化的。

3.5 关于 serialVersionUID 的问题

我们将上述代码再执行一遍运行(序列化和反序列化均要执行),接着将Person类中的 private static final long serialVersionUID = 1L;注释取消,然后屏蔽掉序列化的方法:serializePerson() ,运行主函数。
目的:当我们在类中没有指定 serialVersionUID的时候,编译器会自动赋值,如果序列化是以默认的 serialVersionUID ,那么反序列化也是会以那个默认的。而我们现在的情况是,以默认的serialVersionUID 进行序列化,以自己赋值的 serialVersionUID 进行反序列化,这样代码就会出问题。

3.6 总结

1、一个类如果想被序列化,那么需要实现一个Serializable接口。
2、类中的静态变量的值是不会被进行序列化的,transient 修饰的属性,是不会被序列化的,内置类型为对应0值。引用类型为null;
3、在实现这个Serializable 接口的时候,一定要给这个 serialVersionUID赋值,最好设置为1L,这个L最好大写来区分,不然小写看起来像是1,不同的 serialVersionUID的值,会影响到反序列化 .

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值