Java核心技术·卷一·第五章笔记

第五章:继承

5.1类,超类,子类

5.1.1定义子类

Java中,所有继承都是public继承,而没有private和protected继承

对于超类中的private是不可访问的。不同的包继承protected

多称之为超类—子类

5.1.2覆盖方法

super.超类方法 调用超类方法。

对于super的一些理解:

super其实和this是不完全相同的概念的。super不是一个对象的引用,例如,不能将super赋值给另一个对象变量,它只是一个只是编译器调用超类方法的特殊关键字

5.1.3子类构造器

super(…)调用超类的构造器,如果不写,则默认调用无参构造器。

注:是不是和this很相像呢Java真是太有趣了

JVM能够自动进行动态绑定实现多态。动态绑定是默认的行为,如果不希望一个方法是虚拟的,可以将他标记为final。

5.1.4继承层次

由一个公共超类派生出来的所有类的集合称之为继承层次。

在继承层次中,从一个特定的类到其祖先的路径成为该类的继承链

注意:Java中不支持多继承。

5.1.5多态

替换规则:程序中出现超类对象的任何地方都可以使用子类对象替换

子类多态成为超类之后,只能调用超类中的方法

不能将超类的引用赋给子类变量

警告:在Java中,子类的引用的数组可以转换成超类引用的数组,而不需要使用强制类型转换。

package com.package1;
public class Test {
    public static void main(String[] args) {
        Son[] sons = new Son[10];
        Father[] fathers =sons;//fathers和sons引用的都是同一个数组
        fathers[0] = new Father();
        sons[0].fun();
    }
}
class Father{

}
class Son extends Father{
    public void fun(){
        System.out.println("fun");
    }
}
//运行时报错:Exception in thread "main" java.lang.ArrayStoreException: com.package1.Father
 Son son = (Father) new Father();
//而这样写会直接报错,上面的错误更隐蔽,更难发现,所以一定要注意
5.1.6理解方法调用

对于覆写的一些注意事项:

  • 虽然返回类型不是签名的一部分,但是在覆盖一个方法时,需要保证返回类型的兼容性。允许子类将覆盖方法的返回类型改为原返回类型的子类型。

    class Father{
        public int fun(){
            System.out.println("Father fun");
            return 1;
        }
    
    }
    class Son extends Father{
        public void fun(){
            System.out.println("Son fun");
        }
    }
    //报错:java: com.package1.Son中的fun()无法覆盖com.package1.Father中的fun()返回类型void与int不兼容
    
    class Father{
        public Father fun(){
            return null;
        }
    
    }
    class Son extends Father{
        public Son fun(){
            return null;
        }
    }
    //这样是可以通过编译的,而且称fun方法有 可协变 的返回类型
    
  • 在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。public>protected>defult(默认,不写)>private。

    修饰词本类同一个包的类继承类其他类
    private×××
    无(默认)××
    protected×
    public

方法调用的顺序,对于x.f(a)方法,为类C中的方法

  1. 编译器查看方法名和声明类型,一一列举C类在含有f的方法和超类中可访问的方法。
  2. 根据方法签名,找到合适的方法,这个过程称之为重载解析。而且由于存在类型转换,这个过程可能会很复杂。如果找不到,就报错。现在编译器已经知道了要调用的方法的名字和参数类型
  3. 如果是static,private,final方法,编译器可以准确的知道指向那一个方法,称之为静态绑定。否则,动态绑定。
  4. 如果是动态绑定,先在子类寻找这个方法,如果子类没有,就在超类寻找这个方法。

由于每次调用都会循环这个寻找的过程,很耗时。实际上,JVM会预先为每一个类计算了一个方法表。

5.1.7组织继承:final类和方法

final类:不能被继承,final方法:不能被覆写

将类声明为final,只有其中的方法自动变成final

应当仔细思考哪些方法应该声明为final。多态性的混乱使用会造成程序混乱

如果应该方法很短,并且没有被覆盖,编译器就能对他就行内联优化处理。例如内联调用e.getName会被替换成访问e.name。

5.1.8强制类型转换
  • 只能在继承层次内进行强制类型转换

  • 在将超类强制转换成子类之前,应该用instanceof进行检查。

    if(staff[1] instanceof Manager){//boolean result = obj instanceof Class
        boos = (Manager) staff[1];
    }
    

    对于向下转型的理解:本质上要求,对于堆内存来讲,不能将超类转化为子类,这是核心原则

    Son son = (Son) new Father();
    //报错,试图将超类堆内存转化为子类
    Father father = new Son();
    Son son = (Son) father;
    //不报错,因为father指向的就是子类的堆内存,第二句的转换本质上是换一个名字而已。
    
5.1.9抽象类

abstract

abstract的方法不需要实现,包含abstract方法的类必须声明为abstract。abstract类中也可以包含字段和具体方法,但是不推荐这样用。

哪怕没有抽象方法也可以声明为abstract类,抽象类不能实例化。

注:c++虚函数标明这个方法使用动态绑定,纯虚函数还标明不需要实现,相当于Java中的抽象方法

5.1.10受保护访问

protected数据访问类型只能由同一个包中的类访问。例如自继承的另一个包的子类不能直接访问超类的字段

5.2Object类

5.2.1Object类的对象

只有基本类型(整型,浮点型,布尔型,数值…)才不是Object的子类,才不是对象

所有数组类型,包括对象数组或者基本类型都扩展了Object类

5.2.2 equals方法

比较的是是否指向同一个对象

5.2.3相等与继承

equals方法要求满足:自反性,对称性,传递性,一致性(引用不变的情况下,反复调用,结果不变)

为了满足对称性,使用getclass方法,如果类不同,则直接返回flase

但是这又与软件设计原则“替换原则”(超类能做到的事情,替换成子类完全没影响),又有些设计人员使用了instanceof测试,核心思想是,对应的字段相同,就返回true,但是这又违反了自反性。

综上,两种实现的目的不同,不同的类使用了不同的设计,还需具体问题具体分析

static boolean equals(xxx[] a, xxx[] b);
//数组长度相同,对应元素相同返回true
static boolean equals(Object a, Object b);
//如果a,b都为null,返回true,只有有一个为null,返回false,否则返回a.equals(b);
5.2.4 hashCode方法

不同的对象的hashcode不同。Object类定义了hashcode方法,

Object类中hashcode是基于储存地址得出的。String重写了hashcode方法,是基于内容的

String a = "wuhu";
StringBuilder  b = new StringBuilder(a);
System.out.println(a.hashCode()+"   "+b.hashCode());
String c = "wuhu";
StringBuilder d = new StringBuilder(c);
System.out.println(c.hashCode()+"  "+d.hashCode());
//result:
//        3660907   460141958
//        3660907  1163157884

如果覆写equals方法,必须覆写hashcode方法:

equals与hashCode的定义必须相容,如果x.equals(y)返回true。那么x.hashCode和y.hashCode的值必须相同。例如,重写了equals方法,只比较员工的ID,那么必须重写hashCode方法,重写的方法只能以ID来生成hashcode

import java.util.Objects;

public class Application {
    public static void main(String[] args) {
        Employee a = new Employee(1,"wuhu");
        Employee b = new Employee(1,"qifei");
        System.out.println(a.equals(b));
        System.out.println(a.hashCode());
        System.out.println(b.hashCode());
    }
}
class Employee{
    private int id;
    private String name;
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return id == employee.id;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    public Employee(int id, String name) {
        this.id = id;
        this.name = name;
    }
}
//rsult:
//	  true
//    32
//    32

对于数组类型的字段,可以使用静态的Arrays.hashCode方法,它基于数组元素的散列码(hashCode)形成。

Objects.hashCode(Object a),a为null返回0,否则返回a.hashCode()

5.2.5 toString()方法

格式一般为 ObjectName[name1=data1,name2=data2…]

只要对象与一个字符串以 + 相连,就会自动调用这个对象的toString方法。

int[] a = {1,2,3,4};
System.out.println(a.toString());

数组类型是没有覆写toString方法的。若要得到想要的输出,需要调用Arrays.toString(),对于二维数组,如上文提到的,需要用Arrays.deepToString方法

int[] a = {1,2,3,4};
int[][] b = {{1,2},{3,4}};
System.out.println(Arrays.toString(a));
System.out.println(Arrays.deepToString(b));
// result:
//        [1, 2, 3, 4]
//        [[1, 2], [3, 4]]

5.3 泛型数组列表

ArrayList,一个有类型参数的泛型类

5.3.1声明数组列表
ArrayList<Employee> employees1 = new ArrayList<Employee>();
ArrayList<Employee> employees2 = new ArrayList<>();//这种省略称之为棱形语法
var employees3 = new ArrayList<Employee>();
//但是使用棱形语法+var,就会成为一个ArrayList<Object>
var errorDemo = new ArrayList<>();
ArrayList<Employee> employees1 = new ArrayList<>();
employees1.ensureCapacity(100);//设置所需的最小容量,或者说已经确定了使用过程中不会超过这个值
//也可以在定义的时候直接标明初始容量
ArrayList<Employee> employees2 = new ArrayList<>(10);

如果add的时候数组列表满了,他就会新建更大的+拷贝,这无疑会影响响应时间,所以可以添加一个初始值来提高效率

size()方法返回当前元素个数。

一旦确定了数组列表的大小保持核定,就可以调用trimToSize方法,去除未使用的空间。但是注意,如果要新加元素,这又势必会带来移动拷贝存储块的时间开销。

5.3.2访问数组元素

不能使用[]来访问。但可以使用set和get方法

ArrayList<Employee> employees = new ArrayList<>(10);
employees.set(0,new Employee());
//报错:
/*
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
	at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
	at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70)
	at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:248)
*/
//因为set方法是用来修改  已有    的值的。
//应该用 add 方法来添加新值
//可以用remove来删除

没有指明泛型类时,可能会带来强转的错误

5.3.3类型化与始数组列表的兼容性

将原始的ArrayList赋值给一个类型化的ArrayList会得到警告,而且使用强制类型转换符并不能避免这个警告

Java中有一个泛型类型限制规则:出于兼容性的考虑,编译器检查到没有发现违反规则的现象之后,就将所有的类型化数组全部转化为原始ArrayList对象。在程序运行时,所有的数组列表都是一样的,即虚拟机中没有类型参数。

确定了问题是可以忽略的,可以使用@SuppressWarnings(“unchecked”)来忽略警告

5.4对象包装器与自动装箱

包装器:与基本类型对应的类(例如Integer Long …)

包装器是不可变的,而且定义为final,不能有子类。

ArrayList 其中T必须为类,而不能为基本数据类型。

除非使用ArrayList 的操作便利性重要于int[] 的效率性,否则由于值包装在对象中,会造成处理效率的损失。

自动装箱:

ArrayList<Integer>  list = new ArrayList<>();
list.add(12);
//会自动变换成
list.add(Integer.valueOf(12));

自动拆箱

int a = list.get(0);
//会自动变换成
int a = list.get(0).intValue();
Integer n = 3;
n++;
//并没有违背上面提到的包装器是不可变的这一点
//实际操作是,进行拆箱,拆箱之后自增,自增只会再装箱,并指向新的包装器

比较两个包装器对象的时候应该使用equals方法。

装箱与拆箱都是编译器的操作,而不是虚拟机.

5.5参数数量可变方法

亦称之为”变参“方法

对于printf方法的声明:

public PrintStream printf(String fmt, Object... args){
    return format(fmt,args);
}
// 其中...是具体代码的一部分

Object…等同于Object[]

package com.package1;
public class Test {
    public static void main(String[] args) {
        System.out.println(Util.max(10,11,10.99,29));
        //编译器将会传递 new double[] {10,11,10.99,29}
    }

}
class Util{
    public static double max(double... a){
        double max = Double.NEGATIVE_INFINITY;
        for (double v:a
             ) {
            if(v>max){
                max=v;
            }
        }
        return max;
    }
}
//result is 29

允许数组作为最后一个参数传递给由可变参数的方法。

5.6枚举类

本质上,枚举类型是一个类

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

    }
}
public enum Size{

}
//会报错,不能有两个public类
enum Size{
    SMALL , MEDIUM , LARGE , EXTRA_LARGE
}
//其中的为它的四个实例,而且不可能构造新的对象。所以在比较两个枚举类型的时候,可以直接用==,而不需要用equals
enum Size{
    SMALL("S") , MEDIUM("M") , LARGE("L") , EXTRA_LARGE("XL");
    //为枚举类型增加构造器,方法和字段
    private String abbreviation;

    Size(String abbreviation) {
        this.abbreviation = abbreviation;
    }

    public String getAbbreviation() {
        return abbreviation;
    }
}
System.out.println(Size.SMALL.getAbbreviation());
//result is "S"
public class Application {
    public static void main(String[] args) {
        System.out.println(Size.SMALL.getAbbreviation()+Size.SMALL.getSize());
    }
}
enum Size{
    SMALL("S",1) , MEDIUM("M",2) , LARGE("L",3) , EXTRA_LARGE("XL",4);
    private String abbreviation;
    private int size;

    Size(String abbreviation, int size) {
        this.abbreviation = abbreviation;
        this.size = size;
    }

    public String getAbbreviation() {
        return abbreviation;
    }

    public int getSize() {
        return size;
    }
}
//result is "S1"

如上文所言,本质上枚举类型是一个类,其中的枚举值它的为实例。添加的字段和方法是每一个实例都有的,而且要位于枚举实例的下方。

public class Application {
    public static void main(String[] args) {
        Size size = Size.valueOf("SMALL");
        System.out.println(size.toString());
        Size[] sizes = Size.values();
        for(Size size1 : sizes){
            System.out.print(size1.toString());
            System.out.println(" "+size1.ordinal());
            //返回枚举常量在enum声明中的位置,从0开始计数
        }
    }
}
enum Size{
    SMALL, MEDIUM, LARGE, EXTRA_LARGE
}
//result is 
//SMALL
//SMALL 0
//MEDIUM 1
//LARGE 2
//EXTRA_LARGE 3

5.7 反射

能够分析类的能力的程序称之为反射。

功能:

  • 在运行时分析类的能力
  • 在运行时检查对象。例如,编写一个适用于所有类的toString方法
  • 实现泛型数组操作代码
  • 利用Method对象,类似于C++中的函数指针
5.7.1 class类

程序运行期间,JVM为所有的对象维护一个运行时类型标识,这个信息会跟踪每一个对象所属的类,利用运行时的类型信息选择要执行的正确的方法。

package com.package1;

public class Test {
    public static void main(String... args) throws NoSuchFieldException, IllegalAccessException {
       Father father = new Son();
        Class<? extends Father> fatherClass = father.getClass();
        System.out.println(fatherClass.getName());
        //输出是 Son,再次印证了Class储存的是堆内存里的对象的信息
    }

}
class Father{

}
class Son extends Father{

}

可以用Class类访问这些信息Class类实际上是一个泛型类,例如Employee.class类型是Class。这样就更复杂了,大多数情况下,可以忽略类型参数,使用原始的class类

import java.util.Random;

public class Application {
    public static void main(String[] args) {
        Employee e = new Employee();
        Class cl =e.getClass();//可以使用getclass
        System.out.println(cl.getName());
        //如果在包里面,包的名字也将作为类的一部分
        Class clss = Random.class;//也可以只要。。.class 
        System.out.println(clss.getName());
        //也可以使用静态forName方法获得,注意有可能会有异常
        try {
            Class class2 = Class.forName("java.util.Random");
            System.out.println(class2.getName());
        }catch (Exception exception){
            exception.printStackTrace();
        }
    }
}
class Employee{

}
//Employee
//java.util.Random
//java.util.Random

getName方法在应用于数组类型的时候会返回有些奇奇怪怪的名字

Class a = int.class;
Class b = double[].class;
System.out.println(a.getName());
System.out.println(b.getName());
// int
// [D
// 鉴于历史原因,getName对于数组类型会返回奇奇怪怪的名字

JVM为每一种类型管理唯一的Class对象,所以可以通过==来比较。必须类完全相同才会返回true,子类都不行。

可以使用getConstructor方法得到一个Constructor对象,然后调用这个对象的newInstance方法构造一个实例。

如果这个类没有显示的无参构造方法,getConstructor方法会抛出一个异常

package com.package1;

import java.lang.reflect.Constructor;

public class Test {
    public static void main(String[] args) throws Exception {
        Employee e = new Employee();
        Class cl = e.getClass();
        Constructor constructor = cl.getConstructor();
        // Constructor getConstructor(Class...parameterTypes)
        Object e2 = constructor.newInstance();
        //Object newInstance(Object... params)将param传递到构造器,
        //来构造这个构造器声明类的一个新实例
        System.out.println(e2.toString());
    }


}
class Employee {
    public Employee() {
    }
}
5.7.2 声明异常入门

异常:

  • 检查性:编译器检查程序员是否知道这个异常并做好准备来处理后果
  • 非检查性:编译器不期望程序员为这些异常提供处理器(hander),例如数组越界,访问null值引用等等
5.7.3 资源

与类关联的数据文件,称之为资源。例如图片,文档什么的

可以使用Class的getResource方法获得资源的URL,或者getResourceAsStream获得输入流。

如果没有找到,返回null,所以不需要加错误处理。

URL getResource(String name);
InputStream getResourceAsStream(String name);
5.7.4 利用反射分析类的能力

Field,Method,Construcror分别用于描述类的字段,方法,构造器

package com.package1;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

public class Test {
    public static void main(String[] args) throws Exception {
        Employee e = new Employee();
        Class cl = e.getClass();
        Method[] methods = cl.getMethods();
        for(Method method : methods){
            int modifiers = method.getModifiers();
            //返回一个整数,用不同的0、1位描述所使用的修饰符
            //使用Modifiers的toString方法可以将这个整数转化为字符
            System.out.println(Modifier.toString(modifiers)+" "+method.getName());
        }
    }


}
class Employee {
    public void test(){

    }
}

getFields,getMethods,getConstructors 方法分别返回这个类的公共字段,公共方法,公共构造器的数组,其中也包括超类的成员

而getDeclareFields,getDeclareMethods,getDeclaredConstructors返回所有的字段,方法,构造器 的数组

package com.package1;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Scanner;

public class Test {
    public static void main(String[] args) throws Exception {
        String name;
        if(args.length> 0 ){
            name = args[0];
        }
        else {
            Scanner in = new Scanner(System.in);
            System.out.println("输入想要查看的类");
            name = in.next();
        }
        Class cl = Class.forName(name);
        Class superCl = cl.getSuperclass();
        String modifiers = Modifier.toString(cl.getModifiers());//class 的修饰符
        if(modifiers.length() > 0){
            System.out.print(modifiers+" ");
        }
        System.out.print("class "+name);
        if(superCl != null & superCl != Object.class){
            System.out.print(" extends"+superCl.getName());
        }
        System.out.print("\n{\n");
        printConstructors(cl);
        System.out.println();
        printMethods(cl);
        System.out.println();
        printFields(cl);
        System.out.println("\n}");
    }

    private static void printConstructors(Class cl){
        Constructor[] constructors = cl.getDeclaredConstructors();
        for(Constructor c : constructors){
            int modifies = c.getModifiers();
            System.out.print("  "+Modifier.toString(modifies)+" ");
            System.out.print(cl.getName()+"(");
            Class[] parameterTypes = c.getParameterTypes();
            for(int i = 0 ; i < parameterTypes.length ; i++){
                if(i > 0){
                    System.out.print(", ");
                }
                System.out.print(parameterTypes[i].getName());
            }
            System.out.println(")");
        }
    }
    private static void printMethods(Class cl){
        Method[] methods = cl.getDeclaredMethods();
        for(Method m : methods){
            Class returnType = m.getReturnType();
            String name = m.getName();

            System.out.print("  ");
            String modifies = Modifier.toString(m.getModifiers());
            if(modifies.length() > 0){
                System.out.print(modifies+" ");
            }
            System.out.print(returnType+" "+name+"(");

            Class[] paraTypes = m.getParameterTypes();
            for(int i = 0 ; i < paraTypes.length ; i++){
                if(i > 0 ){
                    System.out.print(", ");
                }
                System.out.print(paraTypes[i].getName());
            }
            System.out.println(")");
        }
    }
    private static void printFields(Class cl){
        Field[] declaredFields = cl.getDeclaredFields();
        for(Field f : declaredFields){
            Class type = f.getType();
            String name = f.getName();
            System.out.print("  ");
            String modifies = Modifier.toString(f.getModifiers());
            if(modifies.length() > 0){
                System.out.print(modifies+" ");
            }
            System.out.println(type.getName()+" "+name+";");
        }
    }
}

5.7.5 使用反射在运行时分析对象

利用反射机制可以查看在线编译时还不知道的对象字段

对于FIeld类型对象f,obj是某个包含f字段的对象,f.get(obj)返回一个对象,这个对象的值为obj的字段值

package com.package1;

import java.lang.reflect.Field;

public class Test {
    public static void main(String... args) throws NoSuchFieldException, IllegalAccessException {
        Util util = new Util();
        Class cl = util.getClass();
        Field field = cl.getDeclaredField("a");
        Object o = field.get(util);
        System.out.println(o);
    }

}
class Util{
    private int a = 1;
}
//报错:
Exception in thread "main" java.lang.IllegalAccessException: Class com.package1.Test can not access a member of class com.package1.Util with modifiers "private"
	at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
	at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)

由于a是一个私有字段,get和set方法都会抛出IllegalAccessException异常。

Java安全机制允许查看一个对象有哪些字段,但是除非拥有访问权限,否则不允许读写那些字段的值。

但是可以通过调用Field,Method,Constructor的超类AccessibleObject的setAccessible方法覆盖Java本省的访问控制。

如果不允许访问,setAccessible调用会抛出异常。访问可以被模块系统和安全管理器拒绝

但是:能够不受控的访问内内部的日子已经屈指可数,例如如下报错:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.package1.ObjectAnalyzer (file:/C:/Users/NieGuevara/Desktop/JavaRelearn/out/production/JavaRelearn/) to field java.util.ArrayList.serialVersionUID
WARNING: Please consider reporting this to the maintainers of com.package1.ObjectAnalyzer
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

5.7.6 使用反射编写泛型数组代码

个人觉得此处才是反射的最常用的应用场景:识别传入的Object对象实际的类型

反射是多态向下转型的解决方案。

package com.package1;

import java.lang.reflect.*;

public class Test {
    public static void main(String[] args) throws Exception {
        int b[] = {1,2,3,4};
        b = (int[]) copy(b,10);
        System.out.println(b.length);//10
        //其中:为了能够实现不仅仅堆对象数组操作,将a声明为Object而非Object[]
        //因为,例如int[]能够转化为Object,但不能转化为对象数组Object[]!

    }
    private static Object copy(Object a, int newLength){
        Class cl = a.getClass();
        if(!cl.isArray()){
            return null;
        }
        Class componentType = cl.getComponentType();
        int length = Array.getLength(a);
        Object newArray = Array.newInstance(componentType, newLength);
        System.arraycopy(a,0,newArray,0,Math.min(length,newLength));
        return newArray;
    }
}

5.7.7 调用任意方法和构造器

Method的invoke方法可以实现调用任意类的方法

Object invok(Object obj, Object... args)

第一个参数是隐式参数,对于static方法,可以忽略,即设置为null.

可以通过getDeclaredMethod获得所有的方法,或者通过getMethod(String name, Class… parameterTypes)获得指定的方法(注意:此处传递的是签名,也就是还包含参数类型为第二个参数,以此来确定方法)

出于效率,安全性,可维护性的考虑,应该减少使用Method对象,更好的方法是使用接口,以及Java8中引入的lambda表达式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值