Java基础---反射详解

Java基础—反射详解



反射定义

在认识反射之前,我们先来回顾一下java程序的执行过程:

Java程序运行时,必须经过编译和运行两个步骤。首先将后缀名为.java的源文件进行编译,生成后缀名为.class的字节码文件。然后Java虚拟机将编译好的字节码文件加载到内存(这个过程被称为类加载,是由加载器完成的),最后虚拟机针对加载到内存的java类进行解释执行,显示结果。

Java的反射就是利用上面加载到jvm中的.class文件来进行操作的。.class文件中包含java类的所有信息,当你不知道某个类具体信息时,可以使用反射获取class,然后进行各种操作。】

Java反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。

获取Class类对象实例

反射的根源是Class类,在Java中对Class类的实例化提供三种操作方法:
1.利用getClass方法,通过实例化对象调用;(通常用于知道实例化对象,但不知道属于哪个类的情况)
2.利用“类.Class”方式获取;
3.通过Class对象的静态方法forName()获取;(最常用的方法,但是可能抛出异常)
观察这三种对象实例化方式:



//获取Class类实例化对象
class Book{

}
public class Test01{
    public static void main(String[] args) {
        //第一种,通过对象调用getClass()
        Book book = new Book();
        Class<?> classA = book.getClass();
        System.out.println(classA.getName());
        //第二种,通过类名.class方式
        Class<?> classB = Book.class;
        System.out.println(classB.getName());
        //第三种,通过forName(),需要进行异常处理
        try{
            Class<?> classC = Class.forName("zhang.da.pao.Book");
            System.out.println(classC.getName());
        }catch(ClassNotFoundException e){
            e.printStackTrace();
        }
    }
}

执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=57037:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
zhang.da.pao.Book
zhang.da.pao.Book
zhang.da.pao.Book

Process finished with exit code 0

需要注意的是,使用Class内部的forName()方法时,利用此方法可以实现Class的实例化,在方法内传入类名称的时候要求使用类的完整名称“包.类”,如果该类不存在则会出现“ClassNotFoundException”异常,进行加载的类一定要在CLASSPATH可以识别的路径中。

反射基本操作

反射获取类结构信息

Class作为反射操作的源头,在Class类中可以获取一些结构上的信息,例如类的包、类继承的父类、类实现的接口。方法定义如下:

No.方法名称类型描述
1public Package getPackage()方法获取类所在的包名称
2public Class<?super T>getSuperclass()方法获取类所继承的父类
3public Class<?>[] getInterfaces()方法获取实现的所有接口,通过集合返回

使用上述方法,实现class的结构信息获取:


import java.util.Arrays;

//获取Class源头信息
interface IBook{}
interface IStudy{}
class People{}
class Student extends People implements  IBook,IStudy{}
public class Test01{
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> clazz = Class.forName("zhang.da.pao.Student");
        System.out.println("包名:"+clazz.getPackage());
        System.out.println("接口:"+Arrays.toString(clazz.getInterfaces()));
        System.out.println("父类名:"+clazz.getSuperclass());
    }
}

执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=62382:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
包名:package zhang.da.pao
接口:[interface zhang.da.pao.IBook, interface zhang.da.pao.IStudy]
父类名:class zhang.da.pao.People

Process finished with exit code 0

反射调用构造方法

类中可能包含大量构造方法,构造方法是类实现初始化的必要方法,Class类中包含如下获取构造的方法:

No.方法名称类型描述
1public Constructor<?>getContructor(Class<?>…parameterTypes)throws NoSuchMethodException,SecurityException方法根据指定的参数类型获取指定的构造方法(public型)
2public Constructor<?>getContructors()throws SecurityException方法获取所有的构造方法(public型)
3public Constructor<?>getDeclaredContructor(Class<?>…parameterTypes)throws NoSuchMethodException,SecurityException方法获取指定的构造方法(public、protected、default型)
4public Constructor<?>getDeclaredContructors()throws SecurityException方法获取所有的构造方法(public、protected、default型)


观察获取构造方法的使用过程:

import java.lang.reflect.Constructor;

//获取Class源头信息
class Book{
    private String name;
    private int price;
    public Book() {
    }

    public Book(String name,int price){
        this.name = name;
        this.price = price;
    }
}
public class Test01{
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
        Class<?> clazz = Class.forName("zhang.da.pao.Book");
        //获取所有构造方法
        Constructor<?> con[] = clazz.getDeclaredConstructors();
        for(Constructor cont: con){
            System.out.println("所有构造方法:"+cont);
        }
        //获取指定构造方法
        System.out.println("指定构造方法:"+clazz.getDeclaredConstructor(String.class,int.class));
    }
}

执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=65121:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
所有构造方法:public zhang.da.pao.Book()
所有构造方法:public zhang.da.pao.Book(java.lang.String,int)
指定构造方法:public zhang.da.pao.Book(java.lang.String,int)

Process finished with exit code 0

此时可以获取本类的所有构造方法,获取构造方法的意义在于进行反射的构造调用,方法如下:

public T newInstance​(Object... initargs)
              throws InstantiationException,
IllegalAccessException,
IllegalArgumentException,
InvocationTargetException

Constructor类中提供了一个newInstance()方法,这个方法的功能是可以调用指定的构造进行对象实例化处理。简单来说就是把Class类获取的对象实例化,这个方法非常重要,可以说是反射功能实现的基础保证!!!下面使用newInstance()进行对象实例化操作:


import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

//获取Class源头信息
class Book{
    private String name;
    private int price;
    public Book(String name,int price){
        this.name = name;
        this.price = price;
    }
    @Override
    public String toString() {
        return "书名:"+this.name+"价格:"+this.price;
    }
}
public class Test01{
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class<?> clazz = Class.forName("zhang.da.pao.Book");
        //获取指定构造方法
        Constructor<?> constructor = clazz.getDeclaredConstructor(String.class,int.class);
        //暂时不考虑异常的处理问题,直接抛出
        Book book = (Book)constructor.newInstance("java",26);
        System.out.println(book);
    }
}

执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=57909:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
书名:java价格:26

Process finished with exit code 0

上述操作方法实现了对象的实例化操作,但这个操作是在JDK1.9之后推荐的做法,在JDK1.8及以前有两种操作方法:
1.Class类中:

@Deprecated(since="9")
public T newInstance()
              throws InstantiationException,
IllegalAccessException

默认调用的是无参构造,如果没有无参构造则会抛出异常,所以在JDK1.9之后改为使用Constructor类调用该方法。
2.Constructor类:

public T newInstance​(Object... initargs)
              throws InstantiationException,
IllegalAccessException,
IllegalArgumentException,
InvocationTargetException

反射调用成员属性

类中的结构主要有三个组成部分:构造、方法、成员属性。前两种在前面的部分都已经介绍了怎样通过反射获取,成员属性也是可以获取的。

No.方法名称类型描述
1public Field[] getDeclaredFields()throws SecurityException方法获取本类中定义的所有成员属性
2public Field getDeclaredField(String name)throws NoSuchMethodException,SecurityException方法获取本类一个指定的成员属性
3public Field[] getFields()throws SecurityException方法获取所有继承来的public成员属性
4public Field getFields(String name)throws NoSuchMethodException,SecurityException方法获取一个本类定义的成员属性

使用上述方法进行属性的获取:

import java.lang.reflect.Field;
import java.util.Arrays;

//获取类中的属性
class Book{
    public String name;
    public int price;
}
class Student extends Book{
    private String score;
}
public class Test01{
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> clazz = Class.forName("zhang.da.pao.Book");
        //获取所有的继承的父类中的public属性
        {
            Field field[] = clazz.getFields();
            System.out.println("继承的父类中的public属性:" + Arrays.toString(field));
        }
        //获取本类中定义的属性
        {
            Field field[] = clazz.getDeclaredFields();
            System.out.println("本类中的所有属性:" + Arrays.toString(field));
        }
    }
}

通过以上方法实现了成员属性的获取,在Field类中有几个重要方法:

No.方法名称类型描述
1public Class<?>getType()方法获取属性类型
2public Object get(Object obj)throws IllegalArgumentException,IllegalAccessException方法获取属性内容
3public void set(Object obj,Object value)throws IllegalArgumentException,IllegalAccessException方法设置属性内容
4public void setAccessible(boolean flag)方法取消封装

使用上述方法实现对属性的类型返回,以及对属性进行赋值操作:



import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

class Book{
    private String name;
    private int price;
}
public class Test01{
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class<?> clazz = Class.forName("zhang.da.pao.Book");
        //获取类对象
        Object obj = clazz.getDeclaredConstructor().newInstance();
        //获取本类中定义的属性
        Field field = clazz.getDeclaredField("name");
        //取消封装
        field.setAccessible(true);
        System.out.println("属性的类型:"+field.getType().getName());
        //传入对象和属性值
        field.set(obj,"编程思想");
        System.out.println("书名:"+field.get(obj));
    }
}

执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=59103:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
属性的类型:java.lang.String
编程思想

Process finished with exit code 0

可以观察到上面的代码中使用了一个setAccessible()方法,该方法可以解除封装操作。

反射调用方法

上面我们讲了类和构造方法的调用方式,在类中出了构造方法外,最多的就是类中的普通方法。Class类中定义了很多获取类中方法的操作:

No.方法名称类型描述
1public Method[] getDeclaredMethods()throws SecurityException方法获取本类中的所有方法
2public Method getDeclaredMethod(String name,Class<?>…parameterTypes)throws NoSuchMethodException,SecurityException方法获取本类一个指定的方法
3public Method[] getMethods()throws SecurityException方法获取所有的public方法
4public Method getMethod(String name,Class<?>…parameterTypes)throws NoSuchMethodException,SecurityException方法获取一个公共的方法,包括父类的方法


//获取类中的方法
import java.lang.reflect.Method;

//获取Class源头信息
class Book{
    public String toString() {
        return null;
    }
}
public class Test01{
    public static void main(String[] args) throws ClassNotFoundException{
        Class<?> clazz = Class.forName("zhang.da.pao.Book");
        //获取类中的所有方法
        Method method[] = clazz.getDeclaredMethods();
        for(Method mod: method){
            System.out.println(mod);
        }
    }
}

执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=59518:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
public java.lang.String zhang.da.pao.Book.toString()

Process finished with exit code 0

此时就获取了类中的方法,但是方法输出是依靠Method类默认的toString()方法,此时返回的方法内容并不能直接使用,如果想要返回一个标准格式的方法(标准格式的方法包括:访问修饰符、返回值类型、方法体、参数列表),需要进行以下操作:



//以标准格式获取类中的方法
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

class Book{
    private String name;
    private int price;
    public Book(String name,int price){
        this.name = name;
        this.price = price;
    }
    public void buy(String name,int price){
        System.out.println("购买书名:"+this.name+"购买价格:"+this.price);
    }
    public String toString() {
        return null;
    }
}
public class Test01{
    public static void main(String[] args) throws ClassNotFoundException{
        //获取反射对象
        Class<?> clazz = Class.forName("zhang.da.pao.Book");
        //获取类中的所有方法
        Method method[] = clazz.getDeclaredMethods();
        for(int x = 0; x < method.length;x++ ){
            //1.输出方法的修饰符
            //method[x].getModifiers()返回特定的字节码,
            // Modifier.toString(int)来解释此字节码int,会返回修饰符的字符串形式。
            System.out.print(Modifier.toString(method[x].getModifiers()));
            //2.输出方法的返回值类型
            System.out.print(method[x].getReturnType().getName());
            //3.输出方法名称
            System.out.print(method[x].getName());
            System.out.print("(");
            //4.输出参数列表
            //获取所有参数类型
            Class<?> params[] = method[x].getParameterTypes();
            for(int y = 0 ; y < params.length ; y++) {
                System.out.print(params[y].getSimpleName()+"arg"+y);
                //","比参数少一个
                if(y<params.length-1){
                    System.out.print(",");
                }
            }
            System.out.println(")");
            //5.输出异常情况
            Class<?> [] exps = method[x].getExceptionTypes();
            if(exps.length>0){
                System.out.print("throws");
                for(int z = 0 ; z < exps.length;z++ ){
                    System.out.println(exps[z].getSimpleName());
                    if(z<exps.length-1){
                        System.out.print(",");
                    }
                }
            }
            System.out.println();
        }
    }
}

执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=62298:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
publicjava.lang.StringtoString()

publicvoidbuy(Stringarg0,intarg1)


Process finished with exit code 0

  在java中可以使用Method类中方法描述可以调用的方法结构,也可以输出全部的方法。

以上介绍了反射获取方法属性的方式,但对于反射对方法的操作来说,最重要的是Method类中提供的反射调用方法:Invoke()方法,先来看看方法的定义:

//Method类的invoke(Object obj,Object args[])方法接收的参数必须为对象;
//如果参数为基本类型数据,必须转换为相应的包装类型的对象。invoke()方法的返回值总是对象。
//如果实际被调用的方法的返回类型是基本类型数据,那么invoke()方法会把它转换为相应的包装类型的对象,再将其返回
public Object invoke​(Object obj,
Object... args)
              throws IllegalAccessException,
IllegalArgumentException,
InvocationTargetException

传入的参数说明:(该方法所在类的一个对象,方法中的参数)
创建一个StringUtil功能类,用于首字母大写操作:


package zhang.da.util.Imp;

//定义一个首字母大写处理类
public class StringUtil {
    public static String initcap(String str){
        if(str == null || " ".equals(str)){
            return str;
        }
        if(str.length() == 1){
            return str.toUpperCase();
        }
        return str.substring(0,1).toUpperCase()+str.substring(1);
    }
}

使用invoke()实现方法调用操作:


import zhang.da.util.Imp.StringUtil;

import java.lang.reflect.Method;

//使用invoke()调用类中的方法
class Book{
    private String name ;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
public class Test01{
    public static void main(String[] args) throws Exception {
        String fieldName = "name" ;
        String fieldValue = "java";
        Class<?> clazz = Book.class;
        //通过反射类的构造方法,获取实例化对象
        Object obj = clazz.getDeclaredConstructor().newInstance();
        //获取Method类对象set,用来调用set方法
        Method setMethod = clazz.getDeclaredMethod("set"+
                //使用首字母大写方法接收获取到的变量名;获取变量的类型作为获取方法的参数传入
                StringUtil.initcap(clazz.getDeclaredField(fieldName).getName()),clazz.getDeclaredField(fieldName).getType());
        setMethod.invoke(obj,fieldValue);
        //获取Method类对象get,用来调用get方法
        Method getMethod = clazz.getDeclaredMethod("get"+
                //使用首字母大写方法接收获取到的变量名;
                StringUtil.initcap(clazz.getDeclaredField(fieldName).getName()));
        //使用Object类型接收invoke()方法返回的对象
        Object returnValue = getMethod.invoke(obj);
        System.out.println(returnValue);
    }
}

执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=51863:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
java

Process finished with exit code 0

此时反射就实现了方法的调用,这也就是为什么在简单java类中要存在有setter、getter方法。

反射的应用场景

反射与工厂设计模式

通过对反射机制的了解,现在对象实例化有两种操作方式:使用new关键字、使用反射机制,为什么类中原有new的操作方式还要增加反射机制呢?原因在于工厂类的使用:


//传统的工厂类设计
interface Book{
    public void study(String str);
}
class Student implements Book{

    @Override
    public void study(String str) {
        System.out.println("学习课程:"+str);
    }
}
class Factory{
	//设置静态方法,返回Student类型的对象
    public static Student getInstance(String className){
        if(className.equals("student")){
        return new Student();
        }
    }
}
public class Test01{
    public static void main(String[] args) {
        Book book = Factory.getInstance("student");
        book.study("java" );
    }
}

执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=62965:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
学习课程:java

Process finished with exit code 0


此时虽然实现了一个工厂类,但如果现在有一种情况,需要频繁向类中增加子类,修改方法为在工厂类的静态方法中增加判断,if(新类.equals(“类名称”)){return new 新类};如果要频繁增加或减少类,对工厂类的冲击很大。



//使用反射修改工厂类
interface Book{
    public void study(String str);
}
class Student implements Book{

    @Override
    public void study(String str) {
        System.out.println("学习课程:"+str);
    }
}
class Factory{
    public static Student getInstance(String className){
        Student instance = null;
        try{
        	//通过反射获取实例化对象
            instance = (Student) Class.forName(className).getDeclaredConstructor().newInstance();
        }catch(Exception e){}
        return instance;
    }
}
public class Test01{
    public static void main(String[] args) {
        Book book = Factory.getInstance("zhang.da.pao.Student");
        if(book != null) {
            book.study("java");
        }
    }
}

执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=50682:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
学习课程:java

Process finished with exit code 0

使用反射实现工厂类,使工厂类内部有更高的内聚性。

反射与单例设计模式

单例设计模式的核心在于,一个类在一个JVM中只允许有一个实例化对象,对于单例模式本身提供有两种结构:懒汉式单例模式、饿汉式单例模式。其中懒汉式单例模式比较复杂。
首先观察传统的懒汉式单例设计:


//懒汉式单例设计
class Singleton{
    private static Singleton instance;
    private Singleton(){
        System.out.println("构造,实例化Singleton对象");
    }
    public static Singleton getInstance(){
        if(instance==null){
            instance = new Singleton();
        }
        return instance;
    }
}
public class Test01{
    public static void main(String[] args) {
        for(int x = 0 ; x < 6 ; x ++){
            //使用多线程运行singleton对象实例化
            new Thread(()->{
                Singleton singleton = Singleton.getInstance();
            }).start();
        }
    }
}


执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=53819:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
构造,实例化Singleton对象
构造,实例化Singleton对象
构造,实例化Singleton对象

Process finished with exit code 0

上述代码显示,单例模式下使用多线程会导致单例失效(出现了对象的多次实例化),原因为在单例程序代码中的判断依据(if(instance == null));这条语句在多线程模式下可能会有多条线程进入此判断内,在执行结果产生之前多次实现对象实例化。
该怎样解决这样的问题呢?通过多线程的知识可以想到使用synchronized追加同步处理,修改代码段:

public synchronized static Singleton getInstance(){
	if(instance==null){
		instance = new Singleton();
	}
	return instance;
}

这样就避免了多个线程同时执行,但这样处理的效率又是怎样呢?假设现在有以万记位的线程数,那么这些线程就要以排队的方式进入判断,效率很低。
这样看来同步并不是随便添加的,在读取判断的时候增加同步会严重影响程序性能,那么如果在处理的过程中增加同步呢?看下面的的处理代码:



//优化后的懒汉式单例设计
class Singleton{
    private static Singleton instance;
    private Singleton(){
        System.out.println("构造,实例化Singleton对象");
    }
    //先判断是否存在实例化对象
    public static Singleton getInstance(){
        if(instance==null){
            //增加同步处理,进一步判断是否存在,此时只有少量线程排队
            //使用Singleton.class类锁
            synchronized (Singleton.class){
                if(instance==null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
public class Test01{
    public static void main(String[] args) {
        for(int x = 0 ; x < 6 ; x ++){
            //使用多线程运行singleton对象实例化
            new Thread(()->{
                Singleton singleton = Singleton.getInstance();
            }).start();
        }
    }
}

执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=60043:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
构造,实例化Singleton对象

Process finished with exit code 0


此时的懒汉式单例设计既保证了getInstance()方法的性能,同时满足了对象实例化的次数为一次。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值