Java File和IO流

File

  • File类是指 java.io.File类,是Java用来抽象表示文件和文件目录路径的形式,本身与平台无关
  • 值得注意的是,File类能够新建、新建、删除、重命名文件和目录,却File 不能访问文件内容本身,如果需要访问文件内容本身,则需要使用输入/输出流
  • 想要在Java程序中表示一个真实存在的文件或目录,那么必须有一个File对 象,但是Java程序中的一个File对象,可能没有一个真实存在的文件或目录。
  • File对象可以作为参数传递给流的构造器

File常用构造器

  • public File(String pathname):pathname为路径创建File对象
    pathname可以为绝对路径或者相对路径,如果 pathname是相对路径,则默认的当前路径在系统属性user.dir中存储
    绝对路径:是一个固定的路径,从盘符开始
    相对路径:是相对于某个位置开始
  • public File(String parent,String child):以parent为父路径,child为子路径创建File对象
  • public File(File parent,String child):根据一个父File对象和子文件路径创建File对象

路径分隔符

  • 路径中的每级目录之间用一个路径分隔符隔开
  • 路径分隔符和系统有关:windows和DOS系统默认使用“\”来表示 , UNIX和URL使用“/”来表示 (Java程序支持跨平台运行,因此路径分隔符要慎用)
  • 为了解决路径分隔符与系统息息相关导致的隐患,File类提供了一个常量public static final String separator,根据操作系统,动态的提供分隔符,不过会比较麻烦
File file1 = new File("d:\\javaio\\info.txt"); 
//使用separator
File file2 = new File("d:" + File.separator + "javaio" + File.separator + "info.txt"); 

File常用方法

获取相关信息的方法(前两个用的多,其他稍作了解):
public String getAbsolutePath():获取绝对路径
public String getParent():获取上层文件目录路径。若无,返回null
public String getPath() :获取路径
public String getName() :获取名称
public long length():获取文件长度(即:字节数)。不能获取目录的长度。
public long lastModified():获取最后一次的修改时间,毫秒值
public String[] list():获取指定目录下的所有文件或者文件目录的名称数组
public File[] listFiles():获取指定目录下的所有文件或者文件目录的File数组

重命名方法:
public boolean renameTo(File dest):把文件重命名为指定的文件路径
示例:

//将file1的文件复制并命名到file2目录下的hi.txt
//必须保证file目录下的文件是不存在的,否则方法返回失败
//因为renameTo方法无法复制重命名在覆盖掉原来的同名文件
//目录无所谓存不存在,但是如果有目录,目录下一定不能有file2的文件
File file1 = new File("hello.txt");
File file2 = new File("D:\\javaio\\hi.txt");
boolean renameRel = file1.renameTo(file2);

判断方法(前三个常用,其他稍作了解)
public boolean isDirectory():判断是否是文件目录
public boolean isFile():判断是否是文件
public boolean exists():判断是否存在
public boolean canRead():判断是否可读
public boolean canWrite():判断是否可写
public boolean isHidden():判断是否隐藏

创建功能方法
public boolean delete():删除文件或者文件夹 (注意,Java中删除不走回收站滴)
还有一件事:要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录

File创建的对象本身的文件或文件夹可以不存在,而存在的文件或文件夹和不存在的文件或文件夹区别就是File类属性的值,File创建的对象对应的文件或文件夹不存在,则对象中存储的是默认值,如果存在,File对象就包含了文件或文件夹本身的一些属性,供方法调用

I/O

  • I/O即是Input/Output,是一种实用的技术,处理设备之间的数据传输,比如读/写文件,网络通信等
  • Java程序中,对于数据的输入/输出操作以“流(stream)” 的 方式进行
  • java.io包下提供了各种“流”类和接口,用以获取不同种类的 数据,并通过标准的方法输入或输出数据

输入(Input):一般以程序作为参照,读取外部设备(硬盘、U盘)的数据到程序(内存)中去,即是将输入流的数据读取到程序中去
输出(Output):一般以程序作为参照,程序将数据输出到外部设备(硬盘、U盘),即是将程序的数据写出到输出流中去

流的分类

按数据单位分为:字节流(8bit)、字符流(16bit)
按流向分为:输入流、输出流
按角色分为:节点流、处理流

Java提供的流的体系非常庞大,我们需要学习其中常用的一些流

以下图是流的体系,可以见到,非常庞大

在这里插入图片描述

流的抽象基类

InputStream & Reader
InputStreamReader是所有输入流的基类
OutputStream & Writer
OutputStreamWriter是所有输出流的基类
按数据类型来分:InputStreamOutputStream是字节流,ReaderWriter是字符流

InputStream(典型实现:FileInputStream)

  • int read()
    从输入流中读取数据的下一个字节。返回 0 到 255 范围内的 int 字节值。如果因 为已经到达流末尾而没有可用的字节,则返回值 -1
  • int read(byte[] b)
    从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。如果因为已 经到达流末尾而没有可用的字节,则返回值 -1。否则以整数形式返回实际读取 的字节数。
  • int read(byte[] b, int off, int len)
    将输入流中最多 len 个数据字节读入 byte 数组。尝试读取 len 个字节,但读取 的字节也可能小于该值。以整数形式返回实际读取的字节数。如果因为流位于 文件末尾而没有可用的字节,则返回值
  • public void close() throws IOException
    关闭此输入流并释放与该流关联的所有系统资源,一定要记得关闭

Reader(典型实现:FileReader)

  • int read()
    读取单个字符。作为整数读取的字符,范围在 0 到 65535 之间 (0x00-0xffff)(2个 字节的Unicode码),如果已到达流的末尾,则返回 -1
  • int read(char[] cbuf)
    将字符读入数组。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数
  • int read(char[] cbuf,int off,int len)
    将字符读入数组的某一部分。存到数组cbuf中,从off处开始存储,最多读len个字符。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数
  • public void close() throws IOException
    关闭此输入流并释放与该流关联的所有系统资源,一定要记得关闭

OutputStream(典型实现:FileOutputStream)

  • void write(int b)
    将指定的字节写入此输出流。write 的常规协定是:向输出流写入一个字节。要写入的字节是参数 b 的八个低位。b 的 24 个高位将被忽略。 即写入0~255范围的。
  • void write(byte[] b)
    将 b.length 个字节从指定的 byte 数组写入此输出流。write(b) 的常规协定是:应该与调用 write(b, 0, b.length) 的效果完全相同。
  • void write(byte[] b,int off,int len)
    将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此输出流
  • public void flush() throws IOException
    刷新此输出流并强制写出所有缓冲的输出字节,调用此方法指示应将这些字节立即写入它们预期的目标
  • public void close() throws IOException
    关闭此输入流并释放与该流关联的所有系统资源,一定要记得关闭

Writer(典型实现:FileWriter)

  • void write(int c)
    写入单个字符。要写入的字符包含在给定整数值的 16 个低位中,16 高位被忽略。 即写入0 到 65535 之间的Unicode码
  • void write(char[] cbuf)
    写入字符数组
  • void write(char[] cbuf,int off,int len)
    写入字符数组的某一部分。从off开始,写入len个字符
  • void write(String str)
    写入字符串
  • void write(String str,int off,int len)
    写入字符串的某一部分
  • void flush()
    刷新该流的缓冲,则立即将它们写入预期目标
  • public void close() throws IOException
    关闭此输入流并释放与该流关联的所有系统资源,一定要记得关闭

节点流(文件流)

FileReaderFileWriter
用来读/写文件,使用父类ReaderWriter的方法

读取文件FileReader

FileReader fr = null; 
try {
 	fr = new FileReader(new File(d:\\test.txt))
 	//FileReader按数据类型来分属于字符流,所以用char
 	char[] buf = new char[1024];
 	int len;
 	while(len = read(buf) != -1){
		System.out.print(new String(buf,0,len));
	}
}catch(IOException e){
	System.out.println(("read-Exception :" + e.getMessage());
}finally{
	if(fr != null){
		try { 
			fr.close()
		}catch (IOException e) { 
			System.out.println("close-Exception :" + e.getMessage());
		}	
	}
}

读取文件FileInputStream

FileInputStream fs = null; 
try {
 	fs = new FileInputStream(new File(d:\\test.txt))
 	//FileInputStream按数据类型来分属于字节流,所以用byte
 	byte[] buf = new byte[1024];
 	int len;
 	while((len = read(buf)) != -1){
		System.out.print(new String(buf,0,len));
	}
}catch(IOException e){
	System.out.println(("read-Exception :" + e.getMessage());
}finally{
	if(fr != null){
		try { 
			fr.close()
		}catch (IOException e) { 
			System.out.println("close-Exception :" + e.getMessage());
		}	
	}
}

写入文件FileWriter

FileWriter fw = null;
try{
	fw = new FileWriter(new File("d:\\text1.txt"));
	fw.write("helloworld");
}catch(IOException e){
	e.printStackTrace(); 
}finally{
	if (fw != null) {
		try {
			fw.close(); 
		} catch (IOException e) { 
		e.printStackTrace();
		}
	}
}

写入文件FileOutputStream

FileOutputStream fw = null;
try{
	fw = new FileOutputStream(new File("d:\\text1.txt"));
	fw.write("helloworld");
}catch(IOException e){
	e.printStackTrace(); 
}finally{
	if (fw != null) {
		try {
			fw.close(); 
		} catch (IOException e) { 
		e.printStackTrace();
		}
	}
}
注意点
  • 定义文件路径时,注意:可以用“/”或者“\”。(根据系统不同而定)
  • 在写入一个文件时,如果使用构造器FileOutputStream(file),则目录下有同名文 件将被覆盖。
  • 如果使用构造器FileOutputStream(file,true),则目录下的同名文件不会被覆盖, 在文件内容末尾追加内容
  • 在读取文件时,必须保证该文件已存在,否则报异常
  • 字节流操作字节,比如:.mp3,.avi,.rmvb,mp4,.jpg,.doc,.ppt
  • 字符流操作字符,只能操作普通文本文件。最常见的文本文 件:.txt,.java,.c,.cpp 等语言的源代码。尤其注意.doc,excel,ppt这些不是文 本文件
  • 抛出异常时最好使用try-catch-finally,防止程序因为异常停止,导致流没有关闭

处理流

这里我们介绍以下处理流之中的缓冲流,为什么叫处理流,缓冲流时提供给I/O节点流一个缓冲区的,可以看作将节点流包裹在内的一个流,对节点流做优化处理,如下图
在这里插入图片描述
为了提高数据读写的速度,Java API提供了带缓冲功能的流类,在使用这些流类 时,会创建一个内部缓冲区数组,缺省(默认)使用8192个字节(8Kb)的缓冲区

缓冲流要“套接”在相应的节点流之上,根据数据操作单位可以把缓冲流分为:

  • BufferedInputStreamBufferedOutputStream
  • BufferedReaderBufferedWriter

通过上面节点流的知识,我们大概知道缓冲流和文件流是一一对应的

对于缓冲流的认识:

  • 当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区
  • 当使用BufferedInputStream读取字节文件时,BufferedInputStream会一次性从文件中读取8192个(8Kb),存在缓冲区中,直到缓冲区装满了,才重新从文件中 读取下一个8192个字节数组
  • 向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区写满,BufferedOutputStream才会把缓冲区中的数据一次性写到文件里。使用方法 flush()可以强制将缓冲区的内容全部写入输出流
  • 关闭流的顺序和打开流的顺序相反(即关闭流从下往上关闭)。只要关闭最外层流即可,关闭最外层流也会相应关闭内层节点流
  • flush()方法的使用:手动将buffer(缓冲区)中内容写入文件
  • 如果是带缓冲区的流对象的close()方法,不但会关闭流,还会在关闭流之前刷 新缓冲区,关闭后不能再写出

缓冲流处理节点流图示:
在这里插入图片描述
接下来以一个综合的题目来认识缓冲流
题目:使用节点流:FileInputStream、FileOutputStream和缓冲流: BufferedInputStream、BufferedOutputStream实现非文本文件(图片/视频)。使用节点流:FileReader、FileWriter和缓冲流:BufferedReader、BufferedWriter实现文本文件的复制。

package com.atguigu.java;

import org.junit.Test;

import java.io.*;

/**
 * 处理流之一:缓冲流的使用
 *
 * 1.缓冲流:
 * BufferedInputStream
 * BufferedOutputStream
 * BufferedReader
 * BufferedWriter
 *
 * 2.作用:提供流的读取、写入的速度提高读写速度的原因:内部提供了一个缓冲区
 * 3. 处理流,就是“套接”在已有的流的基础上。
 *
 */
public class BufferedTest {

    /*
    实现非文本文件的复制(字节流)
     */
    @Test
    public void copyNoTextFile(String srcFile, String destFile) {
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;

        try {
            //这里我用的是IDEA,创建模块的方式,在junit的Test方法下,相对路径是从Model开始的,而不是工程目录
            //而Eclipse在junit的Test方法下,无论是一个工程还是一个模块,相对路径都是从工程目录下开始的
            File srcFile = new File(srcPath);
            File destFile = new File(destPath);
            //2.造流
            //2.1 造节点流
            FileInputStream fis = new FileInputStream(srcFile);
            FileOutputStream fos = new FileOutputStream(destFile);
            //2.2 造缓冲流
            bis = new BufferedInputStream(fis);
            bos = new BufferedOutputStream(fos);

            //3.复制的细节:读取、写入
            byte[] buffer = new byte[10];
            int len;
            while((len = bis.read(buffer)) != -1){
                bos.write(buffer,0,len);
				//刷新缓冲区的作用是将buffer的内容手动写到文件
//              bos.flush();//刷新缓冲区
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //4.资源关闭
            //要求:先关闭外层的流,再关闭内层的流
            if(bos != null){
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
            if(bis != null){
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
            //说明:关闭外层流的同时,内层流也会自动的进行关闭。所以我们只需关闭外层处理流
//        fos.close();
//        fis.close();
        }

  }

    /*
    *实现文本文件复制的方法(字符流)
    */
    public void copyTextFile(String srcPath,String destPath){
		BufferedReader br = null;
        BufferedWriter bw = null;
        try {
            //创建文件和相应的流
            br = new BufferedReader(new FileReader(new File(srcPath)));
            bw = new BufferedWriter(new FileWriter(new File(destPath)));

            //读写操作
            //方式一:使用char[]数组
//            char[] cbuf = new char[1024];
//            int len;
//            while((len = br.read(cbuf)) != -1){
//                bw.write(cbuf,0,len);
//    //            bw.flush();
//            }

            //方式二:使用String
            String data;
            while((data = br.readLine()) != null){
                //方法一:
//                bw.write(data + "\n");//data中不包含换行符
                //方法二:
                bw.write(data);//data中不包含换行符
                bw.newLine();//提供换行的操作

            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭资源
            if(bw != null){

                try {
                    bw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(br != null){
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }

    }
}
	
	//非文本文件复制
    @Test
    public void testcopyNoTextFile(){
       

        String srcPath = "d:\\view\\视频.avi";
        String destPath = "d:\\view\\视频-copy.avi";

        copyNoTextFile(srcPath,destPath);

    }



    //文本文件的复制
    @Test
    public void testcopyTextFile(){
        
        String srcPath = "d:\\text\\java.txt";
        String destPath = "d:\\txt\\java-copy.txt"
		
		copyTextFile(srcPath, destPath);
}

转换流(处理流的一种)

Java API提供了两个转换流

  • InputStreamReader:将InputStream转换为Reader
  • OutputStreamWriter:将Writer转换为OutputStream

字节流中的数据都是字符时,转成字符流操作更高效
很多时候我们使用转换流来处理文件乱码问题。实现编码和解码的功能

解码:将 字节流 转换为 字符流
编码:将 字符流 转换为 字节流
通俗理解就是,解码就是解开密码,解开我们看不懂的东西,字节我们看不懂,字符我们看得懂,那么解码就是将字节流转换为字符流。编码就是反过来

InputStreamReader
  • 实现将 字节的输入流 按指定字符集转换为 字符的输入流
  • 需要和InputStream“套接”

构造器:

  • public InputStreamReader(InputStream in)
  • public InputSreamReader(InputStream in,String charsetName) :charsetName是被转换文件存储的字符编码,如果被转换文件的编码格式为UTF-8,那么charsetName就要设置为UTF-8,即用UTF-8的编码方式将文件转换,如果不一致会导致转换出来的文件出现乱码(默认值为系统默认编码,开发环境下默认值为开发工具指定的编码,例如IDEA设定默认编码为UTF-8)
OutputStreamWriter
  • 实现将 字符的输出流 按指定字符集转换为 字节的输出流
  • 需要和OutputStream“套接”

构造器:

  • public OutputStreamWriter(OutputStream out)
  • public OutputSreamWriter(OutputStream out,String charsetName):charsetName是转换后文件存储的字符编码

可以用下图来理解
在这里插入图片描述
接下来用例题来使用转换流来实现文件的读入和写出

package com.atguigu.java;

import org.junit.Test;

import java.io.*;

/**
 * 处理流之二:转换流的使用
 * 1.转换流:属于字符流
 *   InputStreamReader:将一个字节的输入流转换为字符的输入流
 *   OutputStreamWriter:将一个字符的输出流转换为字节的输出流
 *
 * 4.字符集
 * ASCII:美国标准信息交换码。
 * 用一个字节的7位可以表示。
 * ISO8859-1:拉丁码表。欧洲码表
 * 用一个字节的8位表示。
 * GB2312:中国的中文编码表。最多两个字节编码所有字符
 * GBK:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码
 * Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码。所有的文字都用两个字节来表示。
 * UTF-8:变长的编码方式,可用1-4个字节来表示一个字符。
 *
 */
public class InputStreamReaderTest {

    //综合使用InputStreamReader和OutputStreamWriter
    @Test
    public void test2() throws Exception {

        try {
			//1.造文件、造流
	        File file1 = new File("Java_utf-8.txt");
	        File file2 = new File("Java_gbk.txt");
	        
	        FileInputStream fis = new FileInputStream(file1);
	        FileOutputStream fos = new FileOutputStream(file2);
	
	        InputStreamReader isr = new InputStreamReader(fis,"utf-8");
	        OutputStreamWriter osw = new OutputStreamWriter(fos,"gbk");
	
			//2.读写过程
	        char[] cbuf = new char[20];
	        int len;
	        while((len = isr.read(cbuf)) != -1){
	            osw.write(cbuf,0,len);
	        }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //3.关闭资源
            if(isr != null){
                try {
                    isr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(osw != null){
                try {
                    osw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
	}
}

对象流

ObjectInputStream 和 和OjbectOutputSteam
  • 对象流和数据流(DataInputStream、DataOutputStream)在存储和读取基本数据类型上功能是一样的,不过对象流能够存储和读取对象,而数据流不能存储和读取对象
  • 用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来

序列化:用ObjectOutputStream保存基本类型数据或对象的机制 (持久化起来,应用于网络传输)
反序列化:用ObjectInputStream读取基本类型数据或对象的机制

通俗来讲,序列化即是把对象从内存中保存到数据源中(硬盘,数据库) ,而反序列化就是将持久化的数据重新读取到内存中

  • ObjectOutputStream和ObjectInputStream不能序列化statictransient修 饰的成员变量
对象序列化的方式

对象序列化的方式主要有两个,实现以下类

  • Serializable(常用)
  • Externalizable

我们比较常用的是实现Serializable类,以下将一些关于实现Serializable类需要注意的细节

  • 凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量
    private static final long serialVersionUID = 12039585L;(12039585L可以随便写一个*******L)
    建议在每个需要序列化的类中都要写上这个静态变量,这个静态变量可以理解为Java对序列化对象的一个标识符。虽然没有写的话,Java会隐式的自动创建,但是有一个细节需要注意,用Java自动创建隐式标识符的类对象在序列化后,如果你对这个自动创建隐式标识符的类进行了修改(修改成员变量,添加新内容等),此时,Java也会对这个自动创建的隐式标识符进行修改,在反序列化的时候会因为找不到原先的标识符而反序列化失败报错

强调:如果某个类需要序列化,必须保证其内部所有属性也是可序列化的(基本数据类型和String、Map等都是实现了序列化的,需要注意的是在类的内部引用别的类,那么别的类也是必须可序列化的)

序列化的机制
  • 对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在硬盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。
使用对象流序列化和反序列化自定义对象

首先自定义一个实体类


import java.io.Serializable;

/**
 * Person需要满足如下的要求,方可序列化
 * 1.需要实现接口:Serializable
 * 2.当前类提供一个全局常量:serialVersionUID
 * 3.除了当前Person类需要实现Serializable接口之外,还必须保证其内部所有属性
 *  也必须是可序列化的。(默认情况下,基本数据类型可序列化)
 */
public class Person implements Serializable{

    public static final long serialVersionUID = 475463534532L;

    private String name;
    private int age;
    private int id;

    public Person(String name, int age, int id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }
    
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", id=" + id +
                '}';
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Person() {
    }
}

对Person序列化和反序列化

package com.atguigu.java;

import org.junit.Test;

import java.io.*;

public class ObjectInputOutputStreamTest {

    /*
    序列化过程:将内存中的java对象保存到磁盘中或通过网络传输出去
    使用ObjectOutputStream实现
     */
    @Test
    public void testObjectOutputStream(){
        ObjectOutputStream oos = null;

        try {
        	//将对象的数据保存在object.dat文件中
            oos = new ObjectOutputStream(new FileOutputStream("object.dat"));
            oos.writeObject(new Person("张学良",23,1001);
            oos.flush();

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(oos != null){
                //3.
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }

    }

    /*
    反序列化:将磁盘文件中的对象还原为内存中的一个java对象
    使用ObjectInputStream来实现
     */
    @Test
    public void testObjectInputStream(){
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream("object.dat"));

            Person p = (Person) ois.readObject();

            System.out.println(p);

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if(ois != null){
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }
    }

}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值