Java学习笔记09 IO

1.概述

通过数据流、序列化和文件系统提供系统输入和输出。 用于处理设备上的数据。

2.流

可以理解数据的流动,就是一个数据流。IO流最终要以对象来体现,对象都存在IO包中。

流也进行分类:

  1. 输入流(读)和输出流(写)。
  2. 因为处理的数据不同,分为字节流字符流

字节流:
处理字节数据的流对象。设备上的数据无论是图片或者dvd,文字,它们都以二进制存储的。二进制的最终都是以一个8位为数据单元进行体现,所以计算机中的最小数据单元就是字节。意味着,字节流可以处理设备上的所有数据,所以字节流一样可以处理字符数据。

字符流:
字符每个国家都不一样,所以涉及到了字符编码问题,那么GBK编码的中文用unicode编码解析是有问题的,所以需要获取中文字节数据的同时+ 指定的编码表才可以解析正确数据。为了方便于文字的解析,所以将字节流和编码表封装成对象,这个对象就是字符流。只要操作字符数据,优先考虑使用字符流体系。

注意:流的操作只有两种:读和写。

流的体系因为功能不同,但是有共性内容,不断抽取,形成继承体系。该体系一共有四个基类,而且都是抽象类。

类型输入输出
字节流InputStreamOutputStream
字符流ReaderWriter

在这四个系统中,它们的子类,都有一个共性特点:子类名后缀都是父类名,前缀名都是这个子类的功能名称。

代码示例:

FileWriter fw = null;//将对象的引用定义在try块的外面方便在finally块中使用。
try 
{
    /*
    1. 创建一个字符输出流对象,用于操作文件。该对象一建立,就必须明确数据存储位置,是一个文件。
    2. 对象产生后,会在堆内存中有一个实体,同时也调用了系统底层资源,在指定的位置创建了一个存储数据的文件。
    3. 如果指定位置出现了同名文件,文件会被覆盖。
    4. 如果是要续写数据,要在构造函数中传入参数true。
    */
    fw = new FileWriter("demo.txt",true);
    /*
    调用Writer类中的write方法写入字符串。字符串并未直接写入到目的地中,而是写入到了流中,(其实是写入到内存缓冲区中)。
    */
    fw.write("abcde");
    fw.flush(); // 刷新缓冲区,将缓冲区中的数据刷到目的地文件中。
}
catch (IOException e )//捕获IO异常
{
    System.out.println(e.toString()+"....");
}
finally
{
    if(fw!=null)//先要判断流是否创建成功,否则下面可能发生空指针异常。
    try
    {
        fw.close();// 关闭流,其实关闭的就是java调用的系统底层资源。在关闭前,会先刷新该流。
    }
    catch (IOException e)
    {
        System.out.println("close:"+e.toString()); 
    }
}

close()和flush()的区别:

  • flush():将缓冲区的数据刷到目的地中后,流可以使用。
  • close():将缓冲区的数据刷到目的地中后,流就关闭了,该方法主要用于结束调用的底层资源。这个动作一定做。

注意在处理IOException时一定要写finally块。用于关闭流和释放其他的资源。

FileReader:使用Reader体系,读取一个文本文件中的数据。示例代码:

FileReader fr = null;
try 
{
    fr = new FileReader("demo.txt"); //创建读取流对象和指定文件关联。
    /*因为要使用read(char[])方法,将读取到字符存入数组。所以要创建一个字符数组,一般数组的长度都是1024的整数倍。
    */
    char[] buf = new char[1024];
    int len = 0;
    while(( len=fr.read(buf)) != -1) //若返回 -1 ,标志读到结尾。
    {
        System.out.println(new String(buf,0,len));
    }
}
catch (IOException e )//捕获IO异常
{
    System.out.println(e.toString()+"....");
}
finally
{
    if(fr!=null)
    try
    {
        fr.close();// 关闭流。
    }
    catch (IOException e)
    {
        System.out.println("close:"+e.toString()); 
    }
}

3.字符流

Reader

用于读取字符流的抽象类。子类必须实现的方法只有 read(char[], int, int) 和 close()。

|—BufferedReader:从字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。 可以指定缓冲区的大小,或者可使用默认的大小。大多数情况下,默认值就足够大了。

|———–LineNumberReader:跟踪行号的缓冲字符输入流。此类定义了方法 setLineNumber(int) 和getLineNumber(),它们可分别用于设置和获取当前行号。

|—InputStreamReader:是字节流通向字符流的桥梁:它使用指定的 charset 读取字节并将其解码为字符。它使用的字符集可以由名称指定或显式给定,或者可以接受平台默认的字符集。

|————FileReader:用来读取字符文件的便捷类。此类的构造方法假定默认字符编码和默认字节缓冲区大小都是适当的。要自己指定这些值,可以先在FileInputStream 上构造一个 InputStreamReader。

|—CharArrayReader:此类实现一个可用作字符输入流的字符缓冲区。

|—StringReader:其源为一个字符串的字符流。

Writer

写入字符流的抽象类。子类必须实现的方法仅有 write(char[], int, int)、flush() 和 close()。

|—BufferedWriter:将文本写入字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入。

|—OutputStreamWriter:是字符流通向字节流的桥梁:可使用指定的 charset 将要写入流中的字符编码成字节。它使用的字符集可以由名称指定或显式给定,否则将接受平台默认的字符集。

|—FileWriter:用来写入字符文件的便捷类。此类的构造方法假定默认字符编码和默认字节缓冲区大小都是可接受的。要自己指定这些值,可以先在FileOutputStream 上构造一个 OutputStreamWriter。

|—PrintWriter:向文本输出流打印对象的格式化表示形式。

|—CharArrayWriter:此类实现一个可用作 Writer 的字符缓冲区。

|—StringWriter:一个字符流,可以用其回收在字符串缓冲区中的输出来构造字符串。

4.字节流

InputStream

是表示字节输入流的所有类的超类。

|— FileInputStream:从文件系统中的某个文件中获得输入字节。哪些文件可用取决于主机环境。FileInputStream 用于读取诸如图像数据之类的原始字节流。要读取字符流,请考虑使用 FileReader。

|— FilterInputStream:包含其他一些输入流,它将这些流用作其基本数据源,它可以直接传输数据或提供一些额外的功能。

|———- BufferedInputStream:该类实现缓冲的输入流。

|———- DataInputStream:数据输入流允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型。

|— ObjectInputStream:ObjectInputStream 对以前使用 ObjectOutputStream 写入的基本数据和对象进行反序列化。

|— PipedInputStream:管道输入流应该连接到管道输出流;管道输入流提供要写入管道输出流的所有数据字节。

OutputStream

此抽象类是表示输出字节流的所有类的超类。

|— FileOutputStream:文件输出流是用于将数据写入 File 或 FileDescriptor 的输出流。

|— FilterOutputStream:此类是过滤输出流的所有类的超类。

|— BufferedOutputStream:该类实现缓冲的输出流。

|— PrintStream:PrintStream 为其他输出流添加了功能,使它们能够方便地打印各种数据值表示形式。

|— DataOutputStream:数据输出流允许应用程序以适当方式将基本 Java 数据类型写入输出流中。

|— ObjectOutputStream:ObjectOutputStream 将 Java 对象的基本数据类型和图形写入 OutputStream。

|— PipedOutputStream:可以将管道输出流连接到管道输入流来创建通信管道。

5.缓冲区

缓冲区是提高效率用的。

BufferedWriter:是给字符输出流提高效率用的,那就意味着,缓冲区对象建立时,必须要先有流对象。明确要提高具体的流对象的效率。

FileWriter fw = new FileWriter("bufdemo.txt");
BufferedWriter bufw = new BufferedWriter(fw);//让缓冲区和指定流相关联。

for(int x=0; x<4; x++)
{
    bufw.write(x+"abc");
    bufw.newLine(); //写入一个换行符,这个换行符可以依据平台的不同写入不同的换行符。
    bufw.flush();//对缓冲区进行刷新,可以让数据到目的地中。
}
bufw.close();//关闭缓冲区,其实就是在关闭具体的流。

BufferedReader:从字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。 可以指定缓冲区的大小,或者可使用默认的大小。大多数情况下,默认值就足够大了。

FileReader fr = new FileReader("bufdemo.txt");
BufferedReader bufr = new BufferedReader(fr);
String line = null;

while((line=bufr.readLine())!=null)//readLine方法返回的时候是不带换行符的。
{  
    System.out.println(line);
}
bufr.close();

常用代码:
键盘录入

BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));

输出到控制台

BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(System.out));

流对象:其实操作只有读取和写入。但是因为功能的不同,流的体系中提供很多的对象。

流的操作规律:

  1. 明确源和目的。
    数据源:就是需要读取,可以使用两个体系:InputStream、Reader;
    数据汇:就是需要写入,可以使用两个体系:OutputStream、Writer;

  2. 操作的数据是否是纯文本数据?
    如果是:数据源:Reader
    数据汇:Writer
    如果不是:数据源:InputStream
    数据汇:OutputStream

  3. 虽然确定了一个体系,但是该体系中有太多的对象,到底用哪个呢?
    明确操作的数据设备。
    数据源对应的设备:硬盘(File),内存(数组),键盘(System.in)
    数据汇对应的设备:硬盘(File),内存(数组),控制台(System.out)。

  4. 需要在基本操作上附加其他功能吗?比如缓冲。
    如果需要就进行装饰。

6.转换流

特有功能:转换流可以将字节转成字符,原因在于,将获取到的字节通过查编码表获取到指定对应字符。

转换流的最强功能就是基于 字节流 + 编码表 。没有转换,没有字符流。
发现转换流有一个子类就是操作文件的字符流对象:

InputStreamReader
|——–FileReader

OutputStreamWriter
|——–FileWrier

想要操作文本文件,必须要进行编码转换,而编码转换动作转换流都完成了。所以操作文件的流对象只要继承自转换流就可以读取一个字符了。

但是子类有一个局限性,就是子类中使用的编码是固定的,是本机默认的编码表,对于简体中文版的系统默认码表是GBK。

FileReader fr = new FileReader("a.txt");

InputStreamReader isr = new InputStreamReader(new FileInputStream("a.txt"),"gbk");

以上两句代码功能一致,如果仅仅使用平台默认码表,就使用

FileReader fr = new FileReader("a.txt"); 

因为简化。如果需要制定码表,必须用转换流。

  • 转换流 = 字节流+编码表。
  • 转换流的子类File = 字节流 + 默认编码表。

凡是操作设备上的文本数据,涉及编码转换,必须使用转换流。

7.File类

文件和目录路径名的抽象表示形式。将文件系统中的文件和文件夹封装成了对象。提供了更多的属性和行为可以对这些文件和文件夹进行操作。这些是流对象办不到的,因为流只操作数据。

File类常见方法:
1 . 创建。

boolean createNewFile();//在指定目录下创建文件,如果该文件已存在,则不创建。而对操作文件的输出流而言,输出流对象已建立,就会创建文件,如果文件已存在,会覆盖。除非续写。

boolean mkdir();//创建此抽象路径名指定的目录。

boolean mkdirs();//创建多级目录。 

2 . 删除。

boolean delete();//删除此抽象路径名表示的文件或目录。

/*在虚拟机退出时删除。注意:在删除文件夹时,必须保证这个文件夹中没有任何内容,才可以将该文件夹用delete删除。window的删除动作,是从里往外删。注意:java删除文件不走回收站。要慎用。*/
void deleteOnExit();

3 . 获取.

long length();//获取文件大小。

String getName();//返回由此抽象路径名表示的文件或目录的名称。

String getPath();//将此抽象路径名转换为一个路径名字符串。

String getAbsolutePath();//返回此抽象路径名的绝对路径名字符串。

String getParent();//返回此抽象路径名父目录的抽象路径名,如果此路径名没有指定父目录,则返回 null。

long lastModified();//返回此抽象路径名表示的文件最后一次被修改的时间。

File.pathSeparator;//返回当前系统默认的路径分隔符,windows默认为 “;”。

File.Separator;//返回当前系统默认的目录分隔符,windows默认为 “\”。

/*列出指定目录下的当前的文件和文件夹的名称。包含隐藏文件。如果调用list方法的File 对象中封装的是一个文件,那么list方法返回数组为null。如果封装的对象不存在也会返回null。只有封装的对象存在并且是文件夹时,这个方法才有效。*/
String[] list();

4 . 判断:

boolean exists();//判断文件或者文件夹是否存在。

boolean isDirectory();//测试此抽象路径名表示的文件是否是一个目录。

boolean isFile();//测试此抽象路径名表示的文件是否是一个标准文件。

boolean isHidden();//测试此抽象路径名指定的文件是否是一个隐藏文件。

boolean isAbsolute();//测试此抽象路径名是否为绝对路径名。

5 . 重命名。

boolean renameTo(File dest);//可以实现移动的效果。剪切+重命名。

8.打印流

PrintStream

为其他输出流添加了功能,使它们能够方便地打印各种数据值表示形式。特点有:

  1. 提供了更多的功能,比如打印方法。可以直接打印任意类型的数据。
  2. 它有一个自动刷新机制,创建该对象,指定参数,对于指定方法可以自动刷新。
  3. 它使用的本机默认的字符编码.
  4. 该流的print方法不抛出IOException。

打印流的的构造函数。

PrintStream(File file);//创建具有指定文件且不带自动行刷新的新打印流。 

PrintStream(File file, String csn);//创建具有指定文件名称和字符集且不带自动行刷新的新打印流。 

PrintStream(OutputStream out);//创建新的打印流。 

PrintStream(OutputStream out, boolean autoFlush);//创建新的打印流。 

PrintStream(OutputStream out, boolean autoFlush, String encoding);//创建新的打印流。 

PrintStream(String fileName);//创建具有指定文件名称且不带自动行刷新的新打印流。 

PrintStream(String fileName, String csn);//创建具有指定文件名称和字符集且不带自动行刷新的新打印流。 

PrintStream可以操作目的:

  • File对象。
  • 字符串路径。
  • 字节输出流。

前两个都JDK1.5版本才出现。而且在操作文本文件时,可指定字符编码了。

当目的是一个字节输出流时,如果使用的println方法,可以在printStream对象上加入一个true参数。这样对于println方法可以进行自动的刷新,而不是等待缓冲区满了再刷新。最终print方法都将具体的数据转成字符串,而且都对IO异常进行了内部处理。

既然操作的数据都转成了字符串,那么使用PrintWriter更好一些。因为PrintWrite是字符流的子类,可以直接操作字符数据,同时也可以指定具体的编码。

PrintWriter

具备了PrintStream的特点同时,还有自身特点:
该对象的目的地有四个:

  • File对象。
  • 字符串路径。
  • 字节输出流。
  • 字符输出流。

开发时尽量使用PrintWriter。
示例代码:

PrintWriter out = new PrintWriter(new FileWriter("out.txt"),true);//设置true后自动刷新

9.序列流

SequenceInputStream:作用就是将多个读取流合并成一个读取流。实现数据合并。

表示其他输入流的逻辑串联。它从输入流的有序集合开始,并从第一个输入流开始读取,直到到达文件末尾,接着从第二个输入流读取,依次类推,直到到达包含的最后一个输入流的文件末尾为止。

这样做,可以更方便的操作多个读取流,其实这个序列流内部会有一个有序的集合容器,用于存储多个读取流对象。

该对象的构造函数参数是枚举,想要获取枚举,需要有Vector集合,但不高效。需用ArrayList,但ArrayList中没有枚举,只有自己去创建枚举对象。

但是方法怎么实现呢?因为枚举操作的是具体集合中的元素,所以无法具体实现,但是枚举和迭代器是功能一样的,所以,可以用迭代替代枚举。

合并原理:多个读取流对应一个输出流。
切割原理:一个读取流对应多个输出流。

import java.io.*;
import java.util.*;

class SplitFileDemo
{
    private static final String CFG = ".properties";
    private static final String SP = ".part";

    public static void main(String[] args) throws IOException
    {
        File file = new File("c:\\0.bmp");
        File dir = new File("c:\\partfiles");
        meger(dir);
    }

    //数据的合并。
    public static void meger(File dir)throws IOException
    {
        if(!(dir.exists() && dir.isDirectory()))
        {
            throw new RuntimeException("指定的目录不存在,或者不是正确的目录");
        }

        File[] files = dir.listFiles(new SuffixFilter(CFG));
        if(files.length==0)
        {
            throw new RuntimeException("扩展名.proerpties的文件不存在");
        }

        //获取到配置文件
        File config = files[0];
        //获取配置文件的信息。
        Properties prop = new Properties();

        FileInputStream fis = new FileInputStream(config);
        prop.load(fis);
        String fileName = prop.getProperty("filename");
        int partcount = Integer.parseInt(prop.getProperty("partcount"));


        File[] partFiles = dir.listFiles(new SuffixFilter(SP));

        if(partFiles.length!=partcount)
        {
            throw new RuntimeException("缺少碎片文件");
        }

        ArrayList<FileInputStream> al = new ArrayList<FileInputStream>(); 

        for(int x=0; x<partcount; x++)
        {
            al.add(new FileInputStream(new File(dir,x+SP)));
        }

        Enumeration<FileInputStream> en = Collections.enumeration(al);
        SequenceInputStream sis = new SequenceInputStream(en);
        File file = new File(dir,fileName);
        FileOutputStream fos = new FileOutputStream(file);
        byte[] buf = new byte[1024];
        int len = 0;
        while((len=sis.read(buf))!=-1)
        {
            fos.write(buf,0,len);
        }

        fos.close();
        sis.close();
    }

    //带有配置信息的数据切割。
    public static void splitFile(File file)throws IOException
    {
        //用一个读取流和文件关联。
        FileInputStream fis = new FileInputStream(file);
        //创建目的地。因为有多个。所以先创建引用。
        FileOutputStream fos = null;
        //指定碎片的位置。
        File dir = new File("c:\\partfiles");
        if(!dir.exists())
        {   
            dir.mkdir();
        }

        //碎片文件大小引用。
        File f = null;
        byte[] buf = new byte[1024*1024];
        //因为切割完的文件通常都有规律的。为了简单标记规律使用计数器。
        int count = 0;
        int len = 0;
        while((len=fis.read(buf))!=-1)
        {
            f = new File(dir,(count++)+".part");
            fos = new FileOutputStream(f);
            fos.write(buf,0,len);
            fos.close();
        }

        //碎片文件生成后,还需要定义配置文件记录生成的碎片文件个数。以及被切割文件的名称。
        //定义简单的键值信息,可是用Properties。
        String filename = file.getName();
        Properties prop = new Properties();
        prop.setProperty("filename",filename);
        prop.setProperty("partcount",count+"");
        File config = new File(dir,count+".properties");
        fos = new FileOutputStream(config);
        prop.store(fos,"");
        fos.close(); 
        fis.close();
    }
}

class SuffixFilter implements FileFilter
{
    private String suffix;

    SuffixFilter(String suffix)
    {
        this.suffix = suffix;
    }

    public boolean accept(File file)
    {
        return file.getName().endsWith(suffix);
    }
}

10. RandomAccessFile

此类的实例支持对随机访问文件的读取和写入。随机访问文件的行为类似存储在文件系统中的一个大型 byte 数组。存在指向该隐含数组的光标或索引,称为文件指针;输入操作从文件指针开始读取字节,并随着对字节的读取而前移此文件指针。

特点:

  1. 该对象即可读取,又可写入。
  2. 该对象中的定义了一个大型的byte数组,通过定义指针来操作这个数组。
  3. 可以通过该对象的getFilePointer()获取指针的位置,通过seek()方法设置指针的位置。
  4. 该对象操作的源和目的必须是文件。
  5. 其实该对象内部封装了字节读取流和字节写入流。

注意:实现随机访问,最好是数据有规律。
示例代码:

class RandomAccessFileDemo
{
    public static void main(String[] args) throws IOException
    {
        write();
        read();
        randomWrite();
    }

    //随机写入数据,可以实现已有数据的修改。
    public static void randomWrite()throws IOException
    {
        RandomAccessFile raf = new RandomAccessFile("random.txt","rw");
        raf.seek(8*4);
        System.out.println("pos :"+raf.getFilePointer());
        raf.write("王武".getBytes());
        raf.writeInt(102);
        raf.close();
    }

    public static void read()throws IOException
    {
        RandomAccessFile raf = new RandomAccessFile("random.txt","r");//只读模式。

        //指定指针的位置。
        raf.seek(8*1);//实现随机读取文件中的数据。注意:数据最好有规律。
        System.out.println("pos1 :"+raf.getFilePointer());
        byte[] buf = new byte[4];
        raf.read(buf);
        String name = new String(buf);
        int age = raf.readInt(); 
        System.out.println(name+"::"+age);
        System.out.println("pos2 :"+raf.getFilePointer());
        raf.close();
    }

    public static void write()throws IOException
    {
        //rw:当这个文件不存在,会创建该文件。当文件已存在,不会创建。所以不会像输出流一样覆盖。
        RandomAccessFile raf = new RandomAccessFile("random.txt","rw");//rw读写模式

        //往文件中写入人的基本信息,姓名,年龄。
        raf.write("张三".getBytes());
        raf.writeInt(97);
        raf.close();
    }
}

11.管道流

管道读取流和管道写入流可以像管道一样对接上,管道读取流就可以读取管道写入流写入的数据。

注意:需要加入多线程技术,因为单线程,先执行read,会发生死锁,因为read方法是阻塞式的,没有数据的read方法会让线程等待。

public static void main(String[] args) throws IOException
{
    PipedInputStream pipin = new PipedInputStream();
    PipedOutputStream pipout = new PipedOutputStream();
    pipin.connect(pipout);
    new Thread(new Input(pipin)).start();
    new Thread(new Output(pipout)).start();
}

对象的序列化:目的:将一个具体的对象进行持久化,写入到硬盘上。
注意:静态数据不能被序列化,因为静态数据不在堆内存中,是存储在静态方法区中。

如何将非静态的数据不进行序列化?用transient 关键字修饰此变量即可。

Serializable:用于启动对象的序列化功能,可以强制让指定类具备序列化功能,该接口中没有成员,这是一个标记接口。这个标记接口用于给序列化类提供UID。这个uid是依据类中的成员的数字签名进行运行获取的。如果不需要自动获取一个uid,可以在类中,手动指定一个名称为serialVersionUID id号。依据编译器的不同,或者对信息的高度敏感性。最好每一个序列化的类都进行手动显示的UID的指定。

import java.io.*;
class ObjectStreamDemo 
{
    public static void main(String[] args) throws Exception
    {
        writeObj();
        readObj();
    }

    public static void readObj()throws Exception
    {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.txt"));
        Object obj = ois.readObject();//读取一个对象。
        System.out.println(obj.toString()); 
    }

    public static void writeObj()throws IOException
    {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.txt"));
        oos.writeObject(new Person("lisi",25)); //写入一个对象。
        oos.close();
    }
}

class Person implements Serializable
{
    private static final long serialVersionUID = 42L;
    private transient String name;//用transient修饰后name将不会进行序列化
    public int age;

    Person(String name,int age)
    {
        this.name = name;
        this.age = age;
    }

    public String toString()
    {
        return name+"::"+age;
    }
}

12.DataOutputStream、DataInputStream

专门用于操作基本数据类型数据的对象。

DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.txt"));
dos.writeInt(256);
dos.close();
DataInputStream dis = new DataInputStream(new FileInputStream("data.txt"));
int num = dis.readInt();
System.out.println(num);
dis.close();

13.ByteArrayInputStream、ByteArrayOutputStream

这两个流对象不涉及底层资源调用,操作的都是内存中数组,所以不需要关闭。直接操作字节数组就可以了,为什么还要把数组封装到流对象中呢?因为数组本身没有方法,只有一个length属性。为了便于数组的操作,将数组进行封装,对外提供方法操作数组中的元素。

对于数组元素操作无非两种操作:设置(写)和获取(读),而这两操作正好对应流的读写操作。这两个对象就是使用了流的读写思想来操作数组。

//创建源:
ByteArrayInputStream bis = new ByteArrayInputStream("abcdef".getBytes());
//创建目的:
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int ch = 0;
while((ch=bis.read())!=-1)
{
    bos.write(ch);
}
System.out.println(bos.toString()); 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值