JAVA学习之I/O系统

简介

为了与各种各样的I/O源进行通信,同时还需要以多种不同的方式与它们通信(顺序、随机存取、缓冲、二进制等)。Java为了避免开发者创建过多的类,因此创建了大量的I/O类来解决这个问题。



File类

File既能代表一个特定的文件也能代表一个文件集(文件夹)。如果它指的是一个文件集,我们就可以对此集合调用list()方法,这个方法会返回一组字符数组。


目录列表器

假设我们查看一个目录列表,可以使用两种方法来使用File对象。

  • 获取全部列表:list()
  • 获取一个受限列表(比如.java文件):使用目录过滤器
public class IODemo1 {
    public static void main(String[] args) {
        File file = new File("C:\\Users\\50422\\Desktop");//获取桌面文件集

        String[] files1 = file.list();//列出所有文件名
        System.out.println(Arrays.asList(files1));
        /**
         * [$电脑管家-清理垃圾$.qmgc, AbstractList.png, Angular Documentation.lnk, componentScan.txt,...]
         */

        File[] files2 =  file.listFiles();//列出所有的文件对象

        String[] file3 = file.list(new DirFilter(".*.java"));
        System.out.println(Arrays.asList(file3));
        /**
         * [MergeUtil.java, StringDemo1.java]
         */
    }
}

class DirFilter implements FilenameFilter{
    private Pattern pattern;
    
    public DirFilter(String regex){
        pattern = Pattern.compile(regex);
    }

    @Override
    /**
     * @param dir 文件name所在的目录
     * @param name 文件名           
     */
    public boolean accept(File dir, String name) {
        return pattern.matcher(name).matches();
    }
}

上面代码中我们使用list()函数获取了桌面所有的文件名集合。同时如果我们希望获得的是文件对象则可以使用file2引用所使用的listFiles()。最后,我们能够看到我们通过一个自定义的目录过滤器过滤了目录,list()函数能够接受一个FilenameFilter接口实现,它会根据accept的返回结果来判断目标文件是否需要进行过滤,所以我们在这里对FilenameFilter进行了实现,并结合正则表达式进行过滤。


File的相关操作方法

方法说明
isFile()判断是不是文件
isDirectory()判断是不是一个文件夹
getAbsoluteFile()获取文件的绝对路径
exists()判断文件/文件夹是否存在
walk()为指定目录下的整个目录树中的所有文件构造一个List<File>
createNewFile()创建对应的File
mkdir()创建对应的文件夹
canExecute()判断是否是可执行的文件
delete()删除文件
renameTo()对文件重命名

上面提到的是一些常用的方法,还有很多其他的用法,只要是对一个文件能够执行的操作这里基本上都有对应大方法



输入输出流

流代表任何有能力产出数据的数据源对象或者是有能力接收数据的接收端对象。流屏蔽了实际I/O设备中处理数据的细节。这意味我们只需要通过流就能对特定数据源进行读写


流的分类

根据方向

  • 输入流
  • 输出流

根据流的类型

  • 字节流
  • 字符流

总的来说,所有字节输出流类与字符输出流类都是继承自OutputStream和Writer,所有字节输入流类与字符输入流类都是继承自InputStream和Reader

InputStream

InputStream表示那些从不同数据源产生输入的类,每个数据源都有一个特定的类对应

数据源功能
字节数组ByteArrayInputStream将内存的缓冲区当作数据源
StringStringBufferInputStream将字符串作为数据源
文件FileInputStream将文件作为数据源
管道PipedInputStream实现管道化
InputStreamSequenceInputStream将两个或多个输入流转换成单个输入流
其他InputStreamFilterInputStream抽象类,装饰者模式的接口,用于为其他InputStream提供功能

OutputStream

OutputStream表示输出所要去的目标,每个方向有一个特定的类对应

方向功能
缓冲区ByteArrayOutputStream所有送往流的数据都会被放入内存中创建的一块缓冲区
文件FileOutputStream将数据写入文件
管道PipedInputStream实现管道化
InputStreamSequenceInputStream将两个或多个输入流转换成单个输入流
其他OutputStreamFilterInputStream抽象类,装饰者模式的接口,用于为其他OutputStream提供功能


FilterInputStream/FilterInputStream

这两个接口是为其他实现InputStream和OutputStream提供特定功能的

举例:我们可以通过FileInutStream从一个文件中读取数据,但是这只是拥有读取这一功能,在效率上我们并不满意,这时我们可通过将其传入一个BufferedInputStream来为目标流提供缓冲区的功能,无论我们需要什么功能只需要通过组合将流层层传入就可以实现,这远远比我们自己继承增强来的方便。


FilterInputStream

功能
DataInputStream可以从流中读取基本数据类型
BufferedInputStream为流提供缓冲区

FilterOutputStream

功能
DataOutputStream可以从流中写入基本数据类型
BufferedOutputStream为流提供缓冲区
PrintStream用于产生格式化输出


Reader/Writer

前面所提到的都是字节流的操作,也就是说我们需要将我们正常的所使用的字符转化成byte进行写入,这无疑不是很符合我们日常的读写习惯。因此我们需要Reader/Writer来为我们提供兼容Unicode与面向字符的I/O功能

同时,java为我们提供了将字节流转换为字符流的适配器

  • InputStreamReader可以把InputStream转换为Reader
  • OutputStreamWriter可以把OuputStream转换为Writer

设计Reader/Writer继承层次结构主要是为了国际化。老的I/O只支持8位字节流,无法很好的处理16位的Unicode字符

注意,因为Reader/Writer只是为了国际化的增强,所以在功能与数据源上基本与旧I/O没有什么区别

1.01.1
InputStreamReader
OutputStreamWriter
FileInputStreamFileReader
FileOutputStreamFileWriter
StringBufferedInputStream(已经弃用)StringReader
StringWriter
ByteArrayInputStreamCharReader
ByteArrayOutputStreamCharWriter
PipedInputStreamPipedReader
PipedOutputStreamPipedWriter

装饰类也是对应的

1.01.1
FilterInputStreamFilterReader
FilterOutputStreamFilterWriter
BufferedInputStreamBufferedReader
BufferedOutputStreamBufferedWriter
DataInputStream
PrintStreamPrintWriter


RandomAccessFile

RandomAccessFile实现了DataInput与DataOutput,所以拥有读取基本类型和UTF字符串的方法,但是它并不在我们前面提到的两个继承体系中,是独立存在的。并且同时具有读写的能力,还可以在文件上随机位置进行操作



I/O流的典型使用方式

在对I/O体系有了一个大概的介绍后我们可以开始正式对其使用了。下面例子可以作为典型的I/O用法的基本参考


缓冲输入文件

如果要打开一个文件用于字符输入,可以使用以String或File对象作为文件名的FileReader。为了提高速度,我们希望对目标文件进行缓冲,那么我们可以将输入流传给一个BufferedReader构造器。我们可以通过BufferedReader提供的readLine()方法进行读取。当readLine()返回null时表示文件已经结束。

public static void main(String[] args) {
        File file = new File("C:\\Users\\50422\\Desktop\\Enable.txt");

        if(file.exists() && file.isFile()){
            try {
                FileReader fileReader = new FileReader(file);
                BufferedReader bufferedReader = new BufferedReader(fileReader);
                String line = "";
                while ((line = bufferedReader.readLine())!=null){
                    System.out.println(line);
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

从内存输入

在下面例子中我们将从文件读出的字符串结果用来创建一个StringReader。然后调用read()读取每一个字符输出到控制台上

public static void main(String[] args) {
        File file = new File("C:\\Users\\50422\\Desktop\\Enable.txt");

        if(file.exists() && file.isFile()){
            try {
                FileReader fileReader = new FileReader(file);
                BufferedReader bufferedReader = new BufferedReader(fileReader);
                String line = "";
                while ((line = bufferedReader.readLine())!=null){
                    StringReader reader = new StringReader(line);

                    int c;
                    while ((c = reader.read()) != -1){
                        System.out.print((char)c);
                    }
                    System.out.println();
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

格式化的内存输入

要读取格式化数据,可以使用DataInputStream,它是一个面向字节的I/O流。因此我们必须使用InputStream而不是Reader类

public static void main(String[] args) {
        try{
            DataInputStream dataInputStream = new DataInputStream(new ByteArrayInputStream("aaa123.23".getBytes()));
            byte b;
            while (true){
                System.out.print((char)dataInputStream.readByte());
            }
        } catch (IOException e) {
            System.out.println("End of stream");
        }
    }

基本的文件输出

FileWriter可以向文件写入数据。首先创建一个与指定文件链接的FileWriter。然后我们通过将其包装成BufferedWriter对其进行操作。注意FileWriter构造器的第二个参数表示是否以追加形式进行写入,如果为flase将覆盖文件中原有内容,默认为false。

public static void main(String[] args) {
        try {
            BufferedWriter writer = new BufferedWriter(new FileWriter(new File("C:\\Users\\50422\\Desktop\\Enable.txt"),true));

            writer.append("Hello World!");
            writer.flush();
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

存储和恢复数据

prinytWriter可以对数据进行格式化,一片我们阅读。但是为了输出可供另一个流恢复的数据,我们需要用DataOutputStream写入数据,并用DataInputStream恢复数据。

public static void main(String[] args) {
        try{
            File file = new File("C:\\Users\\50422\\Desktop\\Enable.txt");

            DataOutputStream outputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
            outputStream.writeDouble(11.2);
            outputStream.writeUTF("hello");
            outputStream.close();

            DataInputStream inputStream = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));
            System.out.println(inputStream.readDouble());
            System.out.println(inputStream.readUTF());
            inputStream.close();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

读写随机访问文件

使用RandomAccessFile,类似于组合使用了DataInputStream和DataOutputStream。因为他们都实现了相同的接口:DataInput和DataOutput。另外,利用seek()可以在文件中到处移动,并修改文件的某个值
在使用RandomAccessFile时,我们必须指导文件排版,才能正确操作它。

public class IODemo7 {
    public static File file = new File("C:\\Users\\50422\\Desktop\\Enable.txt");
    public static void main(String[] args) {
        try{


            RandomAccessFile rf = new RandomAccessFile(file,"rw");
            for (int i=0;i<7;i++){
                rf.writeDouble(i*1.44);
            }
            rf.writeUTF("End of file");
            rf.close();
            display();

            rf = new RandomAccessFile(file,"rw");
            rf.seek(5*8);
            rf.writeDouble(47.1);
            rf.close();
            display();
            /**
             * Value 0: 0.0
             * Value 1: 1.44
             * Value 2: 2.88
             * Value 3: 4.32
             * Value 4: 5.76
             * Value 5: 7.199999999999999
             * Value 6: 8.64
             * End of file
             * Value 0: 0.0
             * Value 1: 1.44
             * Value 2: 2.88
             * Value 3: 4.32
             * Value 4: 5.76
             * Value 5: 47.1
             * Value 6: 8.64
             * End of file
             */



        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void display() throws IOException {
        RandomAccessFile rf = new RandomAccessFile(file,"rw");
        for (int i = 0;i<7;i++){
            System.out.println("Value "+i+": "+rf.readDouble());
        }

        System.out.println(rf.readUTF());
        rf.close();
    }
}



标准I/O

程序的所有输入都可以来自于标准输入,它的所有输出都可以发送到标准输出,以及所有的错误都可以发送到标准错误。
标准I/O的意义在于:我们可以很容易地把程序串联起来,一个程序的标准输出可以称为另一个程序的标准输入。


从标准输入中读取

按照标准IO模型,java提供了:

  • System.in
  • System.out
  • System.err
    后两者已经被事先包装好了PrintStream对象
    而第一个是一个未加工的InputStream,所以在操作前必须先进行包装。
public static void main(String[] args) {
        System.out.println("直接使用System.out输出一行数据");
        System.err.println("直接使用System.err输出一行数据");


        //使用System.in需要借助包装类
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        try{
            while(true){
                System.out.println(reader.readLine());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
重定向标准I/O

上述的标准输入输出对象是控制台,这其实除了测试用途不是很大,但事实上我们还可以将我们的保准I/O定向到重定向到某个字节流:

  • System.setIn(InputStream)
  • System.setOut(printStream)
  • System.setErr(printStream)

下面例子中通过标准输入来读取数据,然后将读取到的数据输出到标准输出中。注意,这里的标准输出已经被设置为file文件对应的输出流,所以数据不会输出到控制台上,而是文件中。

public static void main(String[] args) {
        try{
            File file = new File("C:\\Users\\50422\\Desktop\\Enable.txt");
            System.setOut(new PrintStream(file));

            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
            String line = "";
            while (!(line = reader.readLine()).equals("-1")){
                System.out.println(line);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


进程控制

我们经常会需要在Java内部执行其他操作系统的程序,并且要控制这些程序的输入输出,Java类库提供了执行这些操作的类。
举例,当我们允许一个程序,同时需要将产生的输出发送到控制台上

下面例子中我们使用ProcessBuilder来执行javap的反编译命令,这个命令会将class文件反编译输出。因此我们通过Process来获得它的标准输出流和标准输入流,并将其输出到控制台上

public class IODemo10 {
    public static void main(String[] args) {
        OSExecute.command("javap D:\\IODemo10.class");
        /**
         * Compiled from "IODemo10.java"
         * public class org.example.io.IODemo10 {
         *   public org.example.io.IODemo10();
         *   public static void main(java.lang.String[]);
         * }
         */
    }
}

class OSExecuteException extends RuntimeException{
    public OSExecuteException(String why){super(why);}
}

class OSExecute{
    public static void command(String command){
        boolean err = false;
        try {
            Process process = new ProcessBuilder(command.split(" ")).start();
            BufferedReader results = new BufferedReader(
                    new InputStreamReader(process.getInputStream())
            );
            String s;
            while ((s=results.readLine())!=null){
                System.out.println(s);
            }
            BufferedReader errors = new BufferedReader(
                    new InputStreamReader(process.getErrorStream(),"GBK")
            );
            while ((s=errors.readLine())!=null){
                System.err.println(s);
                throw new OSExecuteException("error executing "+command);
            }

        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}



NIO

JDK1.4的java.nio.*包中引入了新的JavaI/O类库,其目的在于提高速度
速度的提升来自于所用结构更接近于操作系统执行I/O的方式:通道缓冲器
我们可以将它想象成一个金矿,通道代表矿藏,也就是我们希望拿到的数据。而缓冲器则是矿车,负责从矿藏中运输挖出来的金矿到我们的手中。也就是说我们与通道的交互从头到尾都是缓冲器在做媒介,我们只是和缓冲器交互,并把缓冲器排到通道中。通道要么从缓冲器中获取数据,要么向缓冲器中发送数据。

  • 通道:包含数据
  • 缓冲器:通过缓冲器对通道进行读写

通道

通道对应的类是FileChannel,它可以通过FileInputStream、FileOutputStream以及RandomAccessFile的getChannel()产生

Reader/Writer不能产生通道,但是Channels类提供了产生Reader/Writer的方法

FileChannel fc = new FileInputStream(new File("d://data.txt")).getChannel();
Reader reader = Channels.newReader(fc,"utf-8");

缓冲器

唯一直接与通道交互的缓冲器是ByteBuffer,它可以存储未加工字节

可以通过put或者wrap方法将字节放入缓冲器,前者放入的是单个字节,后者放入的是字节数组。注意,后者不是复制数组,而是直接将它作为产生ByteBuffer的存储器。当然也可以通过ByteBuffer.allocate(size)为其分配长度来生成一个ByteBuffer。NIO的目标是移动大量数据,因此ByteBuffer的大小就尤为重要了

ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes());
buffer.put("world".getBytes());
ByteBuffer buffer1 = ByteBuffer.allocate(100);

通道通过write和read方法把缓冲器作为参数,对缓冲器进行读写

public static void main(String[] args) {
        try {
            FileChannel channel = new FileInputStream(new File("C:\\Users\\50422\\Desktop\\Enable.txt")).getChannel();
            ByteBuffer buffer = ByteBuffer.allocate(10);
            while (channel.read(buffer)!=-1){
                buffer.flip();
                while (buffer.hasRemaining()){
                    System.out.print((char)buffer.get());
                }
                buffer.clear();
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
方法解释
put在ByteBuffer中放入一个字节或多个字节
warp将字节数组包装成ByteBuffer
flip()将limit设置为position,position设置为0,用于读取刚写入缓冲区的数据
clear()清空缓冲区,将position设置为0,limit设置为容量
mark()把mark设置为position
position()返回position的位置
capacity()返回缓冲区容量
position(int pos)
remaining()返回limit-position
hasRemaining()如果有介于limit-position,返回true
limit()返回limit
limint(int lim)设置limit

注意:当我们通过通道向缓冲器中存入数据后,需要将缓冲器的position通过flip()方法设置为0,因为所有针对缓冲器的读写都是参考position和limit的位置,在通道刚写入缓冲器时,position在最后端,因此如果不重新设置postion,我们的读取也将从尾部开始,这样永远读取不到数据,同时,读取完后还需clear()重新设置position和limit,因为读取完后position又处于尾端,而通道发现position后面没有任何空位可以写入,将不会写入任何东西。这时候,输入流还有内容,而缓冲器一直处于满载状态,会导致这个状态无限循环。


视图缓冲器

视图缓冲器使我们可以通过某个特定的基本数据类型来查看底层的ByteBuffer,ByteBuffer依然是实际存储数据的地方,用来支持视图。
我们可以这么认为,ByteBuffer存储的时未加工的字节,而视图缓冲器相当于它的封装。我们从封装中获取对应的类型。而无需针对字节进行操作。

通过ByteBuffer的asXXXBuffer()方法,我们可以获取到用于XXX基本数据类型的视图缓冲器

ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes());

CharBuffer charBuffer = buffer.asCharBuffer();
//...
LongBuffer longBuffer = buffer.asLongBuffer();

内存映射文件

内存映射文件允许我们创建和修改那些因为太大而不能完全放入内存的文件。有了内存映射文件,我们就可以假定整个文件都在内存中
我们可以通过调用FileChannel.map(FileChannel.MapMode.XXX,start,end)来创建一个用于映射我们指定大小区域的缓冲器。

原理上就是一开始加载一部分,当我们对其他没有加载的部分进行操作时,会有一个交换操作,但是使用者完全可以当作文件都在内存中进行操作。这种操作方式极大的提高了性能和速度

File file = new File("d://data.txt");
MappedByteBuffer buffer2 = new RandomAccessFile(file,"rw").getChannel().map(FileChannel.MapMode.READ_WRITE,0,file.length());
byte[] bytes = new byte[(int)file.length()];
buffer2.get(bytes);
String info = new String(bytes,"utf-8");
System.out.println(info);

文件加锁

JDK1.4引入文件加锁机制,允许我们同步访问某个作为共享资源的文件。
文件锁对其他的操作系统进程是可见的,因为java的文件加锁直接映射到了本地操作系统的加锁工具。

通过对FileChannel调用tryLock或者lock,就可以获得整个文件的FileLock。前者是非阻塞式的,后者是阻塞式的。

注意,我们可以传入一些参数来指定我们加锁的部分(这意味着我们可以进行部分加锁)。当我们指定部分加锁,我们加锁以外的其他位置都不会锁定,而不指定参数是,将全部加锁,并随着文件的增加而扩大范围。而参数中有一个参数是指定是否共享锁,这需要本地操作系统提供这种服务,我们可以通过FileLock.isShared()来进行查询

FileOutputStream out = new FileOutputStream("d://data.txt");
//全部加锁
FileLock lock = out.getChannel().tryLock();
//FileLock lock = out.getChannel().lock();
        
//部分加锁
//FileLock lock1 = out.getChannel().tryLock(0,10,false);
//FileLock lock1 = out.getChannel().lock(0,10,false);

lock.release();
//lock1.release();

虚拟机会自动释放锁,但是我们也可以显式释放



总结

下面是通道、流、缓冲器与视图缓冲器之间的关系
在这里插入图片描述



压缩

java的IO库中的类支持读写压缩格式的数据流。我们可以用它们获取压缩功能
这些类属于InputStream和OutputStream层次结构的一部分,因为压缩类库是按字节方式处理的

压缩类功能
CheckedInputStreamGetCheckSum()为任何InputStream产生校验和
CheckedOutputStreamGetCheckSum()为任何OutputStream产生校验和
DeflaterOutputStream压缩类的基类
ZipOutputStream将数据压缩成Zip格式
GZIPOutputStream将数据压缩成GZIP
DeflaterInputStream解压缩类的基类
ZipInputStream解压Zip
GZIPInputStream解压GZIP

GZIP

GZIP适合对单个数据流进行压缩

//获取数据源
BufferedReader reader = new BufferedReader(new FileReader("d://data.txt"));
//将数据写入压缩流
BufferedOutputStream out = new BufferedOutputStream(new GZIPOutputStream(new FileOutputStream("d://data.gz")));
out.write(reader.readLine().getBytes());
reader.close();
out.close();

//读取压缩文件
reader = new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream("d://data.gz"))));
System.out.println(reader.readLine());

ZIP

zip的库可以很方便的保存多个文件

一个ZipEntry代表一个文件

package com.java.io;

import java.io.*;
import java.util.zip.Adler32;
import java.util.zip.CheckedOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class ZIPCompress {
    public static void main(String[] args) throws IOException {
        File file1 = new File("d://data1.txt");
        File file2 = new File("d://data2.txt");

        BufferedReader reader1 = new BufferedReader(new InputStreamReader(new FileInputStream(file1)));
        BufferedReader reader2 = new BufferedReader(new InputStreamReader(new FileInputStream(file2)));

        //CheckedOutputStream选用Adler32产生校验和,这种方式快些,还有一种CRC32
        ZipOutputStream zos = new ZipOutputStream(new CheckedOutputStream(new FileOutputStream("d://test.zip"),new Adler32()));
        BufferedWriter out = new BufferedWriter(new OutputStreamWriter(zos));
        zos.setComment("A test of Java Zipping");//注释

        //放入第一个文件
        zos.putNextEntry(new ZipEntry("data1.txt"));
        String info=null;
        while((info = reader1.readLine())!=null){
            out.write(info);
            out.newLine();
        }
        reader1.close();
        out.flush();

        //放入第二个文件
        zos.putNextEntry(new ZipEntry("data2.txt"));
        info=null;
        while((info = reader2.readLine())!=null){
            out.write(info);
            out.newLine();
        }
        reader2.close();
        out.flush();

        out.close();

    }
}



对象序列化

对象序列化就是将我们的对象序列化成字节流,进行持久化。然后到下次使用时再进行反向序列化到内存中。这个过程就是对象的序列化和反序列化。

对象序列化需要对象是实现Serializable空接口

序列化概念的引入主要是为了支持两种主要特性

  • java的远程方法调用RMI
  • javaBean

要序列化一个对象,首先要创建OutputStream对象,然后封装到ObjectOutputStream对象内同调用writeObject即可实现对象序列化。
同时,对象序列化还会追踪对象内包含的所有引用,并保存那些对象



XML

对象序列化可以解决java的对象传输/保存读取,但是并不是一个通用的解决方案。另一种更具交互性的解决方案是将数据转换为XML格式,这样就可以在不同的语言和平台上进行使用

具体关于XML的操作和理解可以看https://blog.csdn.net/qq_33905217/article/details/107530073

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

原来是肖某人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值