Java高效编程


Java高效编程之一【创建和销毁对象】

一、考虑用静态工厂方法替代构造函数

代表实现:java.util.Collection Framework

Boolean类的简单例子:

public static Boolean valueOf (boolean b){
return(b ? Boolean.TRUE: Boolean.FALSE);
}

优点:

1、与构造函数不同,静态工厂方法具有名字。

一个类看起来需要多个构造函数,并且它们的运行特征相同,应考虑使用静态工厂方法来替代其中一个或多个构造函数,并且要慎重选择它们的名字以明显标示他们的不同。

2、与构造函数不同,他们每次调用的时候,不要求非得创建一个新的对象。

当一个程序要频繁的创建相同的对象,并且创建对象的代价是昂贵的,这项技术可以极大地提高性能。

3、与构造函数不同,它们可以返回一个原返回类型的子类型的对象。

缺点:

1、类如果不含公有的或者受保护的构造函数,就不能被继承。

2、它们与其他的静态方法没有任何区别。

使用静态工厂方法要遵循命名习惯,其中两个已经很流行了

valueOf : 非常有效的类型转换符

getInstance : 返回实例,对于单例模式返回唯一的实例。

结论:构造函数和静态工厂要合理选择使用。

二、使用私有构造函数强化singleton属性

实现singleton有两种方法,这两种方法都要把构造函数保持为私有的,并且提供一个静态成员,以便能够允许客户访问该类的唯一实例

1、带有公有final域的方法

复制代码
// Singleton with final field
public class Elvis{
public static final Elvis INSTANCE=new Elvis();

 private Elvis(){

        ……
}
         ……//Remained omitted
}
复制代码

2、静态工厂方法

复制代码
// Singleton with static factory
public class Elvis{
private static final Elvis INSTANCE=new Elvis();

 private Elvis(){

        ……
}
public static Elvis getInstance(){
              return INSTANCE;
}
         ……//Remained omitted
}
复制代码

如果确定永远是一个singleton,用第一种方法是有意义的,如果保留余地,让以后可以修改,使用第二种方法比价好。为了使一个singleton方法是可以序列化(serializable)的,仅仅在声明中加上"implements Serializable"是不够的。为了维护singleton性,必须加上一个readResolve方法,否则的话一个序列化的实例在反复序列化的时候,都会导致创建一个新的实例。为了防止这种情况在readResolve方法:

// readResolve 方法保持单例属性
private Object readResolve() throws ObjectStreamException{
/**
 *返回真正的Elvis,让垃圾收集器注意假冒的Elvis
*/
}

三、通过私有构造函数强化不可实例化的能力

将一个类包含单个显示的私有构造函数,则它就不可以被实例化了:

复制代码
//不可实例化的实体类
public class UtilityClass {
//不能实例化的抑制默认构造函数
private UtilityClass(){
//该构造函数将永远不能被调用
}
……//其余的省略
}
复制代码

四、避免创建重复的对象

当你重用一已有对象的时候,请不要创建新的对象,而同样的,当你创建一个新的对象的时候,请不要重用一个已有对象。在提倡使用保护行拷贝的场合,因重用一个对象儿招致的代价要远远大于因创建重复对象而招致的代价。在要求保护性拷贝的情况下却没有实施保护性拷贝,将会导致错误和安全漏洞;而不必要的创建对象仅仅会影响程序的风格和性能。

五、消除过期的对象引用

 考虑下面栈实现的例子,你能否发现内存泄露的位置:

复制代码
public class Stack { 
    private Object[] elements; 
    private int size = 0; 
 
    public Stack(int initialCapacity) { 
        this.elements = new Object[initialCapacity]; 
    } 
public void push(Object e) { 
        ensureCapacity(); 
        elements[size++] = e; 
    } 
 
    public Object pop() { 
        if (size == 0) 
            throw new EmptyStackException(); 
        return elements[--size]; 
    } 
 
    /** 
     * 确保空间能存储一个以上的元素,当数组需要增加的时候仅仅是简单的将容量加倍    
     */ 
    private void ensureCapacity() { 
        if (elements.length == size) { 
            Object[] oldElements = elements; 
            elements = new Object[2 * elements.length + 1]; 
            System.arraycopy(oldElements, 0, elements, 0, size); 
        } 
    } 
} 
复制代码

 从栈中弹出来的对象将不会当做垃圾回收,即使使用栈的客户程序不再引用这些对象,它们将不会回收。这是因为,栈内部维护这对这些对象的过期引用(过期引用,即永远也不会再被解除的引用),本例中凡是elements数组活动区域之外的引用都是过期的,elements的活动区域是指下标小于size的那一部分。

 要想修复该问题很简单,一旦对象引用已经过期,只需要清空这些引用即可。pop方法的修订版如下:

复制代码
public Object pop() { 
    if (size==0) 
        throw new EmptyStackException(); 
    Object result = elements[--size]; 
    elements[size] = null; // 删除过期引用
    return result; 
} 
复制代码

 “清空度对象引用”这样的操作应该是一个例外,而不是一种规范行为。消除过期引用最好的办法是重用一个本来已经包含对象引用的变量,或者让这个 变量结束期声明周期。何时清除一个引用,Stack哪方面的特性使得它遭受到了内存泄露的影响?

   根源在于Stack自己管理内存,存储池包含了elements数组(对象引用单元,而不是引用本身)的元素。数组活动区域中的元素是已分配的,而数组其余部分的元素是自由的。但垃圾回收器不知道,需要程序员手动清空。

六、避免使用终结函数

显示的终止方法通常与try-finally结构结合起来使用,以确保及时终止。

复制代码
// try-finally 块保证终结方法的执行
Foo foo = new Foo(...); 
try { 
    // 对fool执行一些必须操作
    ... 
} finally { 
    foo.terminate();  // 显示终结方法
} 
复制代码

另一种方法是“终结函数链(finalizeer chaining)”不会自动执行,如果一个类(不是Object类)有一个终结函数,并且只有一个子类改写了终结函数,那么子类的终结函数必须手工调用父类的终结函数。

复制代码
 // 手工终结链
protected void finalize() throws Throwable { 
    try { 
        // 终结子类的状态
        ... 
    } finally { 
        super.finalize(); 
    } 
} 
复制代码

即使子类的终结过程中跑出一个异常,超类的终结函数也会被执行,反之亦然。
但是如果子类改写了超累的终结函数,但是忘记调用超累的终结函数(或者有意不调用),会使得超类的终结函数永远得不到执行。为了解决这一问题,我们使用匿名类的单个实例(即终结函数守卫者)来终结其外围实例。

复制代码
// 终结函数守卫者
public class Foo { 
   // 此对象的唯一目的是终结微微的Fool对象 
   private final Object finalizerGuardian = new Object() { 
      protected void finalize() throws Throwable { 
         // 终结外围的 Foo 对象
         ... 
      } 
   }; // 匿名类
   ...  // 余下省略
} 
复制代码

公有类Fool并没有终结函数(除了它从Object中继承了一个无关紧要的finalize之外),所以子类的终结函数是否调用super.finalize并不重要,对于每一个带有终结函数的公有非final类,都应该考虑使用这项技术。


对于所有对象都通用的方法,即Object类的所有非final方法(equals、hashCode、toString、clone和finalize)都有明确的通用约定,都是为了要被改写(override)而设计的。

七、在改写equals的时候请遵循约定

  • 一个类的每个实例实质上都是唯一的。对于代表了实体活动实体而不是值(value)的类,确实是这样的,比如Thread。Object所提供equals实现对于这些类是正确的。
  • 不关心一个类是否提供了“逻辑相等(logical equality)”的测试功能。如java.util.Random改写的equals,用于检查两个Random是否产生随机数序列是否相等,但是设计者并不认为客户会需要或者期望这样的功能。在这样的情况下,从Object继承得到的equals实现就已经足够了。
  • 超类已经改写了equals,从超类继承过来的行为对于子类也是合适的。例如,Set实现都从AbstractSet继承了equals实现,List实现从AbstractList继承了equals实现,Map实现从AbstractMap继承了equals实现。
  • 一个类是私有的,或者是包级私有的,并且可以确定它的equals方法永远也不会被调用。尽管这样,应该也要改写equals方法,以免万一以后会被调用。改写如下
public boolean equals(Object o){
  throw new UnsupportedOperationException();
}

那么什么时候要改写equals呢?

“值类”的情形:程序员在利用equals类比较指向值对象的应用的时候,希望知道它们逻辑上是否相等,而不是是否指向同一个对象。
                    有一种“值类”可以不要求改写equals方法,即类型安全枚举类型。因为类型安全枚举类型保证每一个值至多只存在一个对象,所以对于这样的类而言,Object的equals方法等同于逻辑上的equals方法。

改写equals的时候要遵循如下的通用约定,来自于java.lang.Object的通用规范:

实现了等价关系。自反性、对称性、传递性、一致性,且对于任意的非空引用值x(即要求所有的对象都不为空),x.equals(null)一定返回false。

要想在扩展一个可实例化的类的同时,既要增加行的特性,同时还要保留equals约定,没有一个简单的办法可以做到这一点。

根据十四的建议,组合优先于继承,满足对称性的实现如下:

复制代码
// 在没有破坏对称性的前提下增加了视图 
public class ColorPoint { 
   private Point point; 
   private Color color; 
 
   public ColorPoint(int x, int y, Color color) { 
      point = new Point(x, y); 
      this.color = color; 
   } 
 
   /** 
     * 返回有色点的点视图 
     */ 
   public Point asPoint() { 
      return point; 
   } 
   public boolean equals(Object o) { 
      if (!(o instanceof ColorPoint)) 
         return false; 
      ColorPoint cp = (ColorPoint)o; 
       return cp.point.equals(point) && cp.color.equals(color); 
   } 
 
   ...  // 余下省略
} 
复制代码

特例:TimeStamp类,java.util.TimeStamp对java.util.Date进行子类化,并且增加了nanoseconds(纳秒)域,TimeStamp违反了对称性。 可以在一个抽象(abstract)类的子类中增加新的特性,而不会违反equals约定。这一点根据第20条的建议“用类层次(class hierarchies)来代替联合(union)”

为了测试实参与当前对象的相等情况,equals必须首先把实参转换为一种适当的类型,以便可以调用它的访问方法或者访问它的域。在做转换之前,equals方法必须使用instanceof操作符,检查它的实参是否为正确的类型。检查后不必单独做null的检查,因为如果instanceof 的第一个操作位为null,不管第二个操作数是哪种类型,按照instanceof操作符的规定,都返回false。

public boolean equals(Object o){
     if(!(o instanceof MyType))
       return false;
      ……
}

如果漏掉了检查,且传递给equals方法的实参有事错误的类型,那么equals方法将会抛出一个ClassCastException异常,这违反了equals约定。
实现高质量的equals的一个处方:

  1. 使用==操作符检查“实参是否为指向对象的一个引用”。如果是,返回true。如果比较操作符比较耗时,这样做能使性能得到优化。
  2. 使用instanceof查看“实参是否为正确的类型”。
  3. 把实参转化为正确的类型。注:使用强制类型转换。
  4. 对于该类中每一个“关键(significant)”域,检查实参中的域与当前对象中的域值是否相匹配。注:先比较最有可能不一致的域。
  5. 自检:是否是对称的、传递的、一致的。

最后的告诫:

  • 当你改写equals的时候,总是要改写hashCode(见八)。
  • 不要企图让equals方法过于聪明。
  • 不要使equals方法过于依赖不可靠的资源。如,java.net.URL,当把主机名转换成ip地址的时候需要访问网络,当不能保证每次都会产生相同的结果。
  • 不要将equals声明中的Object对象替换为其他的类型。如下替换,可能导致出现问题很久都发现不了。  
public boolean equals(MyClass o){
         ……
}

因为Object.equals的实参类型为Object,而这个方法并没有改写override(override)Object.equals,相反它重载(overload)了Object.equals(见第二十六),在原有equals的基础上提供了一个“强类型化(strongly typed)”的equals方法,通常不推荐这样做。

八、改写的equals方法总是要改写hashCode

在每一个改写了equals的的方法中,你必须也要改写hashCode方法。如果不这样做,就会违反Object.hashCode的通用约定,从而导致该类无法与所有基于散列值(hash)集合类地在一起正常运作,这样的集合包括HashMap、HashSet和HashTable,它们存储散列键(hash keys)。

三条通用约定:

  • 在一个应用程序执行期间,无论调用多少次hashCode返回的结果都是同一个整数。在同一个应用程序的多次执行过程中,这个整数可以不相同。
  • 相等的对象必须具有相等的散列码(hash Code)。 
  • 不等的对象必须产生不相等的散列码。

理想情况下,一个散列函数应该把一个集合中不相等的实例均匀分布到可能的散列值上。

简单的处方如下:

  1. 把一个非零变量值,比如说23,保存在一个叫result的int类型的变量中。(23这个值任选,但最好不要选择0)
  2. 对于该对象中的一个关键域(字段)f(指equals方法中考虑的每一个域),完成以下步骤:
    a. 为每个域字段计算int类型的散列码: 
    i. 若是 boolean, 则计算 (f ? 0 : 1). 
    ii. 若是 byte, char, short, or int,则计算(int)f. 
    iii.若是 long,则计算(int)(f ^ (f >>> 32)). 
    iv. 若是 float,则计算Float.floatToIntBits(f). 
    v. 若是 double,则计算Double.doubleToLongBits(f),然后按照步骤2.a.iii对long计算hash值. 
    vi. 若该域是一个引用对象,并且equals方法通过递归调用方式来比较这个域,则递归调用这个域的hashCode. 
       如果这个域的值为null,则返回0.
    vii. 如果域是一个数组,则把每一个元素当做一个单独的域来处理。也就是说递归的应用上述规则,对于每一个重要的元素计算一个散列码,然后根据2.b把这些散列组合起来。

           b. 按照下面的公式把步骤a中计算得到的散列码c组合到result中: 
                result = 37*result + c;
         3. 返回result

         4. 写好方法之后,思考是否满足“相等的实例具有相等的散列码”

 在相等的比较中没有用到的任何域,要将它们排除在外,而且是必须要求 。

下面是带有hashCode方法的PhoneNumber类:

复制代码
public final class PhoneNumber { 
    private final short areaCode; 
    private final short exchange; 
    private final short extension; 
 
    public PhoneNumber(int areaCode, int exchange, 
                       int extension) { 
        rangeCheck(areaCode,   999, "area code"); 
  rangeCheck(exchange,   999, "exchange"); 
        rangeCheck(extension, 9999, "extension"); 
            this.areaCode  = (short) areaCode; 
            this.exchange  = (short) exchange; 
            this.extension = (short) extension; 
        } 
 
        private static void rangeCheck(int arg, int max, 
                                       String name) { 
            if (arg < 0 || arg > max) 
               throw new IllegalArgumentException(name +": " + arg); 
       } 
 
       public boolean equals(Object o) { 
           if (o == this) 
               return true; 
           if (!(o instanceof PhoneNumber)) 
               return false; 
           PhoneNumber pn = (PhoneNumber)o; 
           return pn.extension == extension && 
                  pn.exchange  == exchange  && 
                  pn.areaCode  == areaCode; 
       } 
 
       // hashCode方法 
      public int hashCode() { 
    int result = 23; 
    result = 37*result + areaCode;
    result = 37*result + exchange;
    result = 37*result + extension
    return result; 
   } 
       ... // 余下省略
} 
复制代码

如果一个类是非可变的,并且计算散列码的代价也比较大,那么你应该把散列键缓存在对象内部,而不是每次请求的时候都重新计算散列码。如果你觉得这种类型的大多数对象会被用做散列键(hash keys),那么你应该是在实例被创建的时候计算散列码。否则,可以选择“延迟初始化”散列码,一直等到hashCode第一次被调用的时候才初始化。(见四十八)

复制代码
// 延迟初始化, 缓存 hashCode 
private volatile int hashCode = 0;  // (见四十八) 
 
public int hashCode() { 
    if (hashCode == 0) { 
        int result = 17; 
        result = 37*result + areaCode; 
        result = 37*result + exchange; 
        result = 37*result + extension; 
        hashCode = result; 
    } 
    return hashCode; 
} 
复制代码

注意:不要试图从散列码计算中排除掉一个对象的关键部分以提高性能。
String类的散列函数至多只检查16个字符,从第一个字符开始,在整个字符串中均匀选取。

 九 、总是要改写toString                       

 java.lang.Object的toString方法返回的是一个包含类名,以及一个“@”符号,接着是散列码的无符号十六进制表示,例如“PhoneNumber@163b91”,但是根据toString的通用约定指出,toString要返回“间接的,但信息丰富的,并且易于阅读的表达形式”虽然“PhoneNumber@163b91”是间接的,但是和“(408)867-5309”比较起来,他不是信息丰富的。toString的约定还进一步指出,建议所有的子类都要改写这个方法”。改写toString并不是强制要求的,但是提供一个好的toString实现可以使一个类用起来更加愉快。当对象被传递给println、字符串连接符号(+)以及1.4发型版本之后的assert的时候,toString方法会自动调用。

  • 在实际的应用中,toString方法应该返回对象中包含所有令人感兴趣的信息。
  • 不管你是否指定格式,都应该在文档中明确的表明你的意图。
  • 为toString返回值中包含的所有信息,提供一种编程访问途径,总是一个好方法。

下面是PhoneNumber的格式化输出toString方法:(例如把99格式化输出4位就应该是0099,格式化输出三位就是099)

 

复制代码
public String toString() { 
    return "(" + toPaddedString(areaCode, 3) + ") " + 
            toPaddedString(exchange,  3) + "-" + 
            toPaddedString(extension, 4); 
} 
/** 
 * 将int类型翻译成指定的长度的字符串 
 * 左边不够以0填充, 假设 i >= 0, 
 * 1 <= length <= 10, 且 Integer.toString(i) <= length. 
 */ 
private static String toPaddedString(int i, int length) { 
    String s = Integer.toString(i); 
    return ZEROS[length - s.length()] + s; 
} 

private static String[] ZEROS = 
    {"", "0", "00", "000", "0000", "00000", 
     "000000", "0000000", "00000000", "000000000"};  
复制代码

十、谨慎的改写clone

Cloneable接口的目的是作为对象的一个mixin接口(mixin interface)(见十六),表明这样的对象允许克隆(cloning),不行的是它并没有成功达到这个目的,其主要缺陷在于缺少一个clone方法,Object的clone方法是被保护的。如果不借助于映像机制reflection(见三十五),则不能仅仅因为一个对象实现了Cloneable,就可以调用clone方法。即使是映像调用也可能会失败,因为并不能够保证该对象一定具有可以访问的clone方法。事实上,对于实现了Cloneable的类,我们总是期望它也提供了一个功能适当的共有clone方法。

如果被克隆对象的每个域包含一个原语类型的值,或者包含一个指向非可变对象的引用,那么被super.clone()返回的对象可能正是你想要的。

复制代码
public Object clone(){
  try{
        super.clone();      
}catch(CloneUnsupportException e){
        throw new Error("Assertion failure");//不能发生
}
}
复制代码

如果要把上面的Stack类做成可以clone的,

public class Stack { 
    private Object[] elements; 
    private int size = 0; 
     ……
}

仅仅使用super.clone()就会出现问题,在其size域中有正确的值,到那时它的elements域将引用到与原始Stack实例相同的数据上,修改原始的实例会破坏被克隆对象的数组,反之亦然。很快就会抛出NullPointerException异常。如果调用Stack类唯一的额构造函数,那么这种情况永远不会发生。实际上,clone方法是另一个构造函数;你必须确保它不会伤害到原始的对象,并且正确建立起来被克隆对象的约束关系

public Object clone() throws CloneNotSupportedException { 
    Stack result = (Stack) super.clone(); 
    result.elements = (Object[]) elements.clone(); 
    return result; 
} 

但是,如果elements域是final的,这种方案就不能正常工作,因为clone方法是进制给elements域赋予一个新值的。这是一个基本呢问题,clone结构与指向可变对象的final域的正常用法是不兼容的。除非在原始对象和克隆对象之间可以安全的共享此可变对象,为了使一个类成为可克隆的,可能有必要从某些域中去掉final修饰符。
clone方法浅表复制和深层复制有相似的地方。

如果扩展实现了Cloneable接口的类,就必须要实现一个行为良好的clone方法。否则,最好的做法是,提供某些其他途径来代替对象拷贝,或者干脆不提供这样的能力。

另外一个实现对象拷贝的好办法是提供一个拷贝构造函数(copy constructor)。拷贝构造函数也是一个构造函数,其唯一的参数类型是包含该构造函数的类,例如:

public Yum(Yum yum);

另一种方法是它的一个微小变形:提供一个静态工厂来替代构造函数:

public static Yum newInstance(Yum yum);

综合说起来,拷贝构造函数和静态工厂比Cloneable/clone方法更具有优势。所有的通用集合都提供了一个拷贝构造函数,它的参数类型是Collection或者Map。假设你有一个LinkedList 1,并且希望把它拷贝成一个ArrayList。clone方法没有提供这样的功能,但是用拷贝构造函数很容易实现:new ArrayList(1)。
Cloneable有上述很多安全问题,所以其他的接口不应该扩展(extends)这个接口,并且为了继承而设计的类(见十五)也不应该实现(implement)这个接口。所以专家级的程序员从来不去改写clone方法,也从来不去调用它。

十一、考虑实现comparable接口

CompareTo方法在Object并没有被声明,这点与其他方法不同,它是java.lang.Comparable接口中唯一的方法。一个类实现了Comparable接口,就表明它的实例具有内在的排序关系。若一个数组中的对象实现了Comparable(可以比较的)接口,则对整个数组进行排序就非常简单:Arrays.sort(a),a为数组。

对于存储在集合中的Comparable对象,搜索、计算极值以及自动维护都非常简答。

Java平台的所有值类都是先了Comparable。如果你正在编写一个值类,并且它具有非常明显的内在排序关系,比如按字母表排序、按数值顺序或者按年代排序,那么你几乎总是应该考虑实现这个接口。

compareTo的规范和equals方法具有相似的特征,其规范如下:

  • 满足sgn(x.compareTo(y))==-sgn(y.compareTo(x))
  • 满足比较关系可传递
  • 强烈建议(x.compareTo(y)==0)==(x.equals(y))

 就行违反了hashCode约定的类会破坏其他的依赖于散列做法的类一样,一个违反了compareTo约定的类也会破坏其他依赖于比交换关系的类。依赖于比较关系的类包括有序集合TreeSet和TreeMap,以及工具类Collections和Arrays,他们内部包含有搜索和排序算法。

有序集合TreeSet、TreeMap使用的是compareTo施加的相等测试,而Hash(HashMap、HashSet)类使用的是equals施加的相等测试。例如,BigDecimal类,它的compareTo方法与equals方法不一致。如果你创建了个HashSet,并且加入了一个new BigDecimal("1.0")和一个new BigDecimal("1.00"),这这个集合将包含两个元素,因为他们是通过equals方法来比较它们之间不相等的,BigDecimal在实现的时候equals考虑精度,而compareTo未考虑精度。然而,如果用TreeSet来实现这样的过程,则会发现集合中仅仅包含一个元素,这是因为TreeSet使用的是compareTo比较。

字段的比较本身是顺序比较,而不是相等比较,比较对象的引用字段可以通过递归调用CompareTo来实现。如果一个字段没有实现Comparable接口,或者你需要一个标准的排序关系,那么你可以使用一个显示的Comparator(比较器),或者编写专门的Comparator(实现Comparator接口,重写其中的compare方法和equals方法),或者使用已有的Comparator。譬如针对七中的CaseInsensitiveString类,compareTo方法使用一个已有的Comparator。

public int compareTo(Object o) { 
    CaseInsensitiveString cis = (CaseInsensitiveString)o; 
    return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s); 
} 

Comparable和Comparator两者比较:
相同点:两者都是接口,需要实现,都为为比较对象的实例而生的。

不同点:

  • Comparable在java.lang包下面,Comparator在java.util的包下面。
  • 编写一个对象的时候,如果implements了Comparable接口,就必须重写其compareTo方法。在实现其的类对象上强制进行整体排序,这一顺序被称为类的自然排序。实现了这一接口的对象列表或者是数组,可以通过Collection.sort,Array.sort自动排序。
  • 而comparator相当于一个比较器接口,实现该接口的类要重写其compare方法,或equals方法。它相当于一个比较函数,强制在集合类对象上进行排序。并且该比较器可以传递给一个排序方法,如 Collections.sort(List,Comparator) 、Arrays.sort(Object[],Comparator) ,以实现对排列顺序的精确控制。比较器也可以被用如有序Map和有序Set(TreeMap、TreeSet)的排序,或者是提供在没有自然Comparable的对象集合上的排序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值