JDK1.5

1. 泛型
1.1 泛型的概念
所谓泛型是指类型参数化(parameterized types)。

Java是一种强类型的语言, 在J2SE 1.4以及以前的版本中, 我们在定义一个Java类, 接口或者方法的时候, 必须指定变量的类型。

在声明泛型类、接口或者函数时, 定义变量的时候不指定某些变量的具体类型, 而是用一个类型参数代替。 在使用这个类、 接口、 或者方法的时候, 这个类型参数由一个具体类型所代替。

1.2 泛型的用法
1.2.1 泛型类
创建一个最简单泛型类

最简单的泛型类



public class GenSample<T> {}


类名后面带有<T>表明了这个类是泛型类, 其中T被称为类型参数(type parameter), 在使用泛型的时候, 类型参数可以被替换为任何的类类型, 但是不能是原始类型(primitive type), 例如int, double.

下面通过一个列表的例子来具体说明如何声明泛型类和类型参数的用法.



public class GenList <T>{

private T[] elements;

private int size = 0;

private int length = 0;



public GenList(int size) {

elements = (T[])new Object[size];

this.size = size;

}



public T get(int i) {

if (i < length) {

return elements[i];

}

return null;

}



public void add(T e) {

if (length < size - 1)

elements[length++] = e;

}

}




1.2.2 泛型接口
声明泛型接口和声明泛型类的语法类似, 也是在接口命称后面加上<T>. 例如:



public interface GenInterface<T> {

void func(T t);

}


在声明泛型类的时候, 可是使用多个类型参数. 多个类型参数之间用逗号分开, 例如:



public class GenMap<T, V> {}




1.2.2 泛型方法
声明泛型方法的一般格式是



<type-list> return-type method-name(parameter-list) {}


清单3.2.6 泛型方法示例



public <T> String getString(T obj) {

return obj.toString();

}


1.2.3 特殊用法
受限泛型

受限泛型是指类型参数的取值范围是受到限制的. extends关键字不仅仅可以用来声明类的继承关系, 也可以用来声明类型参数(type parameter)的受限关系.例如, 我们只需要一个存放数字的列表, 包括整数(Long, Integer, Short), 实数(Double, Float), 不能用来存放其他类型, 例如字符串(String), 也就是说, 要把类型参数T的取值泛型限制在Number极其子类中.在这种情况下, 我们就可以使用extends关键字把类型参数(type parameter)限制为数字,

受限泛型示例:



public class Limited<T extends Number> {

public static void main(String[] args) {

Limited<Integer> number; //正确

Limited<String> str; //编译错误

}

}




使用通配符

前面我们创建了泛型的列表, 如果我需要一个方法来处理泛型列表, 例如, 我们希望把列表中的每个元素都打印出来, 但是类型参数(type parameter)只能使用在声明一个泛型类的时候, 如果类型参数使用在函数定义里会导致编译错误





public static void print(GenList<T> list){} //编译错误


在这种情况下, 我们需要用另外一种方法来表示一个泛型类, 否则, 就可能需要书写多个print函数







public static void print(GenList<Integer> list){}

public static void print(GenList<Double> list){}



public static void print(GenList<String> list){}


J2SE 5.0中提供了泛型的通配符"?", "?"可以用来代替任何类型, 例如使用通配符来实现print方法



public static void print(GenList<?> list) {}




1.3 特点
ü 类型安全限制

ü 不再需要强制转换类型

1.4 局限性
泛型的一些局限型
(1) 类型参数不能实例化, 例如,



T t= new T(); //编译错误


(2) 不能实例化类型参数的数组



T[] ts= new T[10]; //编译错误


(3) 类的静态变量不能声明为类型参数类型



public class GenClass<T> {

private static T t; //编译错误

}




(4) 泛型类不能继承自Throwable以及其子类



public GenExpection<T> extends Exception{} //编译错误




2. 枚举
2.1 枚举的概念及用法
枚举就是定义一个固定的、封闭的值集合。枚举类型的值就是枚举类型的实例。

枚举类型的定义



public enum Priority {High, Medium, Low };


它包括一个关键字enum ,一个新枚举类型的名字 Priority 以及为Priority定义的一组值。

在创建枚举类型时,注意几个重要的概念。

· 所有创建的枚举类型都扩展于 java.lang.Enum. Enum 是在J2SE 5.0 里定义的一个新类, 它本身不是枚举类型.在创建枚举类型时,必须用enum 关键字,不能直接地定义一个继承Enum的类来创建一个枚举类型,尽管所有创建的枚举类型实际上都是Enum 的子类. 如果直接继承Enum, compiler 就会报错(会导致编译错误)。

枚举类型里定义的每一个值都是枚举类型的一个实例,比方说High是Priority的一个实例.枚举类型又是扩展于Enum. 所以枚举类型的每一个值声明时, 缺省时都将映射到Enum(String name, int ordinal) 构造函数中.换句话说,enum Priority {High, Medium, Low } 的实现是调用了下面的Enum 构造函数:

映射的构造函数调用:



new Enum< Priority >("High", 0);

new Enum< Priority >("Medium", 1);

new Enum< Priority >("Low", 2);


每一个创建的枚举类型都是Enum 的子类,除了上面调用父类 Enum 的构造函数外,枚举类型可以使用参数为定义一些自己的构造函数.当声明值时,只需调用此枚举类型定义的构造函数,而且不必添加 new 关键字。


其它构造函数调用:



enum Priority {

High (38),

Medium(36.5),

Low (5.2);

double temperature;

Priority (double p)

temperature = p;

}


另外要强调的两点: 一是这些枚举类型的构造函数都是私有的.它是不能被其它的类或者其它的枚举类型调用的. 而且这个私有修饰符是由编译器自动加的,如果我们定义这些构造函数时,在前面加上public 修饰符, 就会导致编译错误

在J2SE 5.0以前,当我们实现一个枚举类时,一般都是把一个整数关联到此枚举类的某一个值的名字,出现的问题是同一个整数可以代表不同枚举类的值. 下面的例子里定义两个枚举类 Course and Grade 如下:


public class Course {

public static final int EnglishLit = 1;

public static final int Calculus = 2;

public static final int MusicTheory = 3;

}

public class Grade {

public static final int A = 1;

public static final int B = 2;

public static final int C = 3;



}


如果开发者误把student1.assignGrade(Grade.A)写成student1.assignGrade(Course.EnglishList); 在编译阶段是不能发现问题的,如果用J2SE 5.0 枚举类型(enum)可以避免这些问题。

枚举类型每一个值都是public, static and final的.也就是说,这些值是唯一的而且一旦定义了是不能被重写或修改.而且尽管在枚举类型每一个值声明时没有出现static关键字, 实际上值都是静态的, 而且我们不能在值前面加上static, public,final 修饰符。
枚举类型都实现了java.lang.Comparable,枚举类型的值是可以比较排序的,排列顺序就是枚举类型定义这些值的顺序.
2.2 枚举类型的应用
在for循环中获取枚举类型的所有值



for (Priority g: Priority.values()){

process(g);

}


转换(Switch)

我们常用的一种判断语句就是Switch-case 语句. 在Switch 语句中使用枚举类型,不仅能简化程序,而且增强了程序的可读性.



File1: Task.java

public class Task {

Priority myPriority;

public Task (Priority p) {

myPriority=p;

}

public Priority getPriority(){

return myPriority;

}}



File2: TestSwitch.java

public class TestSwitch (

Task task = new Task(Priority.Medium);

switch (task.getPriority( )) {

case High:

//do case High

break;

case Midum: // fall through to Low

case Low:

//do case Low

break;

default: throw new AssertionError("Unexpected enumerated value!");

}

}


在J2SE 5.0 的java.util 程序包中提供两个新类:EnumMap 和 EnumSet,这两个类与枚举类型的结合应用可使以前非常繁琐的程序变得简单方便.EnumMap 类提供了java.util.Map 接口的一个特殊实现,该接口中的键(key)是一个枚举类型.

EnumMap 例子



public void test() throws IOException {

EnumMap<Priority, String> descriptionMessages =

new EnumMap< Priority, String>( Priority.class);

descriptionMessages.put(Priority.High, "High means ...");

descriptionMessages.put(Priority.Medium, " Medium represents...");

descriptionMessages.put(Priority.Low, " Low means...");

for (Priority p : Priority.values( ) ) {

System.out.println("For priority " + p + ", decription is: " +

descriptionMessages.get(p));

}

}


EnumSet 类提供了 java.util.Set 接口的实现,该接口保存了某种枚举类型的值的集合.EnumSet的作用类似于特性的集合,或者类似于某个枚举类型的所有元素的值的子集.EnumSet 类拥有一系列的静态方法,可以用这些方法从枚举类型中获取单个元素或某些元素, 下面的程序例子显示如何这些静态方法:

EnumSet 例子



public class TestEnumSet {

public enum ColorFeature {

RED,BLUE, GREEN, YELLOW,BLACK

} ;

public static void main(String[] args) {

EnumSet allFeatures = EnumSet.allOf(ColorFeature.class);

EnumSet warmColorFeatures = EnumSet.of(ColorFeature.RED,

ColorFeature.YELLOW);

EnumSet non_warmColorFeatures = EnumSet.complementOf(warmColorFeatures);

EnumSet notBlack = EnumSet.range(ColorFeature.RED, ColorFeature.YELLOW);



for (ColorFeature cf : ColorFeature.values()){

if (warmColorFeatures.contains(cf)) {

System.out.println("warmColor "+cf.name());

}

if (non_warmColorFeatures.contains(cf)) {

System.out.println("non_WarmColor "+cf.name());

}

}

}

}






枚举类型的函数定义

在介绍创建枚举类型中曾提到枚举类型都是java.lang.Enum的子类. 也就是说, 枚举类型都是可编译的Java 的类,那么就可以在枚举类型里添加构造函数和其它函数



public enum ColorFeature {

RED(0),

BLUE(0),

GREEN(300),

YELLOW(0),

BLACK(0);



/** The degree for each kind of color*/

private int degree;

ColorFeatures(int degree) {

this.degree = degree;

}

public int getDegree( ) {

return degree;

}

public String getDescription( ) {

switch(this) {

case RED: return "the color is red";

case BLUE: return "the color is blue";

case GREEN: return "the color is green";

case BLACK: return "the color is black";

case YELLOW: return "the color is yellow"

default: return "Unknown Color";

}

}}




特定于常量的类主体

枚举类型可以定义自己的函数,其实更进一步,枚举类型的每一个值都可以实现枚举类型里定义的抽象函数,这听起来很不可思议,我们可以先看下面的例子.





public enum Priority implements Feature {

High (38) {

public void perform() {

System.out.println("high 38");

}

},

Medium(36.5) {

public void perform() {

System.out.println("medium 36.5");

}

},

Low (5.2){

public void perform() {

System.out.println("low 5.2");

}

};

public abstract void perform();

public String getDescription(Priority p) {

return null;

}

}


枚举类型Priority 定义了一个抽象函数perform(),Priority的每一个值都对perform 函数实现了重载,这就是枚举类型的特定于常量的类主体.在这种情况下,每声明一个值,枚举类型的一个子类生成,然后生成这个子类的唯一的实例来表示这个值.不同的值,就有对应的不同的子类.每个子类可以对父类的抽象函数进行重载。





public class Task {

Priority myPriority;

public Task (Priority p) {

myPriority=p;

}

public Priority getPriority(){

return myPriority;

}

public void test() throws IOException {

if (myPriority == Priority.High)

System.out.println(Priority.High.getClass().getName());

if (myPriority == Priority.Medium)

System.out.println(Priority.Medium.getClass().getName());

if (myPriority == Priority.Low)

System.out.println(Priority.Low.getClass().getName());

}}

public class TestSwitch {

public static void main(String[] args) {

Task task = new Task(Priority.High);

Task task1 = new Task(Priority.Medium);

Task task2 = new Task(Priority.Low);

try {

task.test();

task1.test();

task2.test();

} catch (IOException e) {

e.printStackTrace();

}

}






3. 线程池
3.1简单的线程池实现
我们通常想要的是同一组固定的工作线程相结合的工作队列,它使用 wait() 和 notify() 来通知等待线程新的工作已经到达了。该工作队列通常被实现成具有相关监视器对象的某种链表。以下代码实现了具有线程池的工作队列。

public class WorkQueue

{

private final int nThreads;

private final PoolWorker[] threads;

private final LinkedList queue;



public WorkQueue(int nThreads)

{

this.nThreads = nThreads;

queue = new LinkedList();

threads = new PoolWorker[nThreads];



for (int i=0; i<nThreads; i++) {

threads[i] = new PoolWorker();

threads[i].start();

}

}

public void execute(Runnable r) {

synchronized(queue) {

queue.addLast(r);

queue.notify();

}

}



private class PoolWorker extends Thread {

public void run() {

Runnable r;



while (true) {

synchronized(queue) {

while (queue.isEmpty()) {

try

{

queue.wait();

}

catch (InterruptedException ignored)

{

}

}



r = (Runnable) queue.removeFirst();

}



// If we don't catch RuntimeException,

// the pool could leak threads

try {

r.run();

}

catch (RuntimeException e) {

// You might want to log something here

}

}

}

}

}






虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁,它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。

3.2 JDK1.5中的线程池
3.2.1 简单介绍
在J2SE(TM)5.0 中,Doug Lea 编写了一个优秀的并发实用程序开放源码库 util.concurrent,它包括互斥、信号量、诸如在并发访问下执行得很好的队列和散列表之类集合类以及几个工作队列实现。该包中的 PooledExecutor 类是一种有效的、广泛使用的以工作队列为基础的线程池的正确实现。Util.concurrent 定义一个 Executor 接口,以异步地执行 Runnable,另外还定义了 Executor 的几个实现,它们具有不同的调度特征。将一个任务排入 executor 的队列非常简单:







Executor executor = new QueuedExecutor();

...

Runnable runnable = ... ;

executor.execute(runnable);






PooledExecutor 是一个复杂的线程池实现,它不但提供工作线程(worker thread)池中任务的调度,而且还可灵活地调整池的大小,同时还提供了线程生命周期管理,这个实现可以限制工作队列中任务的数目,以防止队列中的任务耗尽所有可用内存,另外还提供了多种可用的关闭和饱和度策略(阻塞、废弃、抛出、废弃最老的、在调用者中运行等)。所有的 Executor 实现为您管理线程的创建和销毁,包括当关闭 executor 时,关闭所有线程,

3.2.2 线程池的使用
线程池类为 java.util.concurrent.ThreadPoolExecutor,常用构造方法为:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)


· corePoolSize
线程池维护线程的最少数量

· maximumPoolSiz
线程池维护线程的最大数量

· keepAliveTime
线程池维护线程所允许的空闲时间

· unit
线程池维护线程所允许的空闲时间的单位

· workQueue
线程池所使用的缓冲队列

· handler
线程池对拒绝任务的处理策略

一个任务通过 execute(Runnable)方法被添加到线程池,任务就是一个 Runnable类型的对象,任务的执行方法就是 Runnable类型对象的run()方法。

当一个任务通过execute(Runnable)方法欲添加到线程池时:

· 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。

· 如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。

· 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。

· 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。

也就是:处理任务的优先级为:
核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。

当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。

unit可选的参数为java.util.concurrent.TimeUnit中的几个静态属性:
NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。

workQueue我常用的是:java.util.concurrent.ArrayBlockingQueue

handler有四个选择:

· ThreadPoolExecutor.AbortPolicy()
抛出java.util.concurrent.RejectedExecutionException异常

· ThreadPoolExecutor.CallerRunsPolicy()
重试添加当前的任务,他会自动重复调用execute()方法

· ThreadPoolExecutor.DiscardOldestPolicy()
抛弃旧的任务

· ThreadPoolExecutor.DiscardPolicy()
抛弃当前的任务

用法举例

package cn.simplelife.exercise; import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit; public class TestThreadPool { private static int produceTaskSleepTime = 2; public static void main(String[] args) { //构造一个线程池 ThreadPoolExecutor producerPool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new ArrayBlockingQueue(3), new ThreadPoolExecutor.DiscardOldestPolicy()); //每隔produceTaskSleepTime的时间向线程池派送一个任务。 int i=1; while(true){ try { Thread.sleep(produceTaskSleepTime); String task = "task@ " + i; System.out.println("put " + task); producerPool.execute(new ThreadPoolTask(task)); i++; } catch (Exception e) { e.printStackTrace(); } } }}








package cn.simplelife.exercise; import java.io.Serializable; /** * 线程池执行的任务 * @author hdpan */public class ThreadPoolTask implements Runnable,Serializable{ //JDK1.5中,每个实现Serializable接口的类都推荐声明这样的一个ID private static final long serialVersionUID = 0; private static int consumeTaskSleepTime = 2000; private Object threadPoolTaskData; ThreadPoolTask(Object tasks){ this.threadPoolTaskData = tasks; } //每个任务的执行过程,现在是什么都没做,除了print和sleep,:) public void run(){ System.out.println("start .."+threadPoolTaskData); try { //便于观察现象,等待一段时间 Thread.sleep(consumeTaskSleepTime); } catch (Exception e) { e.printStackTrace(); } threadPoolTaskData = null; }}



对这两段程序的说明:

1. 在这段程序中,一个任务就是一个Runnable类型的对象,也就是一个ThreadPoolTask类型的对象。

2. 一般来说任务除了处理方式外,还需要处理的数据,处理的数据通过构造方法传给任务。

3. 在这段程序中,main()方法相当于一个残忍的领导,他派发出许多任务,丢给一个叫 threadPool的任劳任怨的小组来做。

o 这个小组里面队员至少有两个,如果他们两个忙不过来, 任务就被放到任务列表里面。

o 如果积压的任务过多,多到任务列表都装不下(超过3个)的时候,就雇佣新的队员来帮忙。但是基于成本的考虑,不能雇佣太多的队员, 至多只能雇佣 4个。

o 如果四个队员都在忙时,再有新的任务, 这个小组就处理不了了,任务就会被通过一种策略来处理,我们的处理方式是不停的派发, 直到接受这个任务为止(更残忍!呵呵)。

o 因为队员工作是需要成本的,如果工作很闲,闲到 3SECONDS都没有新的任务了,那么有的队员就会被解雇了,但是,为了小组的正常运转,即使工作再闲,小组的队员也不能少于两个。

4. 通过调整 produceTaskSleepTime和 consumeTaskSleepTime的大小来实现对派发任务和处理任务的速度的控制, 改变这两个值就可以观察不同速率下程序的工作情况。

5. 通过调整4中所指的数据,再加上调整任务丢弃策略, 换上其他三种策略,就可以看出不同策略下的不同处理方式。

6. 对于其他的使用方法,参看jdk的帮助,很容易理解和使用。





4.自动装箱/拆箱
自动装箱/拆箱大大方便了基本类型数据和它们包装类地使用。
自动装箱:基本类型自动转为包装类(int >> Integer)。
自动拆箱:包装类自动转为基本类型(Integer >> int)。
在JDK1.5之前,我们总是对集合不能存放基本类型而耿耿于怀,现在自动转换机制解决了我们的问题。

int a = 3;
Collection c = new ArrayList();
c.add(a);//自动转换成Integer.

Integer b = new Integer(2);
c.add(b + 2);



  这里Integer先自动转换为int进行加法运算,然后int再次转换为Integer.

5.元数据(注释)
元数据就是描述数据的数据(data about data)。在开发的世界里,元数据就是能够绑定到一个类的附加信息,在静态或者运行时间。在J2SE5.0中,这种元数据叫作注释(Annotation)。通过使用注释, 程序开发人员可以在不改变原有逻辑的情况下,在源文件嵌入一些补充的信息。代码分析工具,开发工具和部署工具可以通过这些补充信息进行验证或者进行部署。



public abstract class BaseClass{ //模板方法的基类

public void doWork(){

doPartI(); //先调用doPartI()方法

doPartII();//之后调用doPartII()方法

}

public abstract void doPartI();

public void doPartII(){

}

}

public class SubClass extend BaseClass{

public void doPartI(){

};

@Override

public void doPortII(){//拼写错误,产生编译错误

System.out.println("override the method of superclass");

}

}


有了元数据处理工具,很多重复的编码步骤可以减少到一个简洁的元数据标签中.比如访问JAX-RPC的时候需要的远程接口服务实现可以按照下面这样实现:
以前 Before:

public interface PingIF extends Remote {
public void ping() throws RemoteException;
}
public class Ping implements PingIF {
public void ping() {
}
}



用了元数据后 After :

public class Ping {
public @remote void ping() {
}
}




6.增强的for循环
For-Each循环得加入简化了集合的遍历。假设我们要遍历一个集合对其中的元素进行一些处理。典型的代码为:

void processAll(Collection c){
    for(Iterator i=c.iterator(); i.hasNext();){
        MyClass myObject = (MyClass)i.next();
        myObject.process();
    }
}



  使用For-Each循环,我们可以把代码改写成:

void processAll(Collection c){
    for (MyClass  myObject :c)
        myObject.process();
}



  这段代码要比上面清晰许多,并且避免了强制类型转换。



7.静态引入
要使用用静态成员(方法和变量)我们必须给出提供这个方法的类。使用静态导入可以使被导入类的所有静态变量和静态方法在当前类直接可见,使用这些静态成员无需再给出他们的类名。

import static java.lang.Math.*;
…….
r = sin(PI * 2); //无需再写r = Math.sin(Math.PI);



  不过,过度使用这个特性也会一定程度上降低代码地可读性。

8.格式化输入输出
格式化的输出 Formatted Output
开发人员现在可以使用类似C语言中的printf这样的功能来产生格式化的输出.现在可以象C语言中一样使用printf,而且语言上基本没有变化,一般可以不变,有些格式可能需要稍微的变化一下.
大部分的通用C printf格式都可以使用,同时一些Java的类例如Date和BigInteger也拥有了格式化规则.可以在java.util.Formatter类中找到更多的信息.


System.out.printf("name count\n");
System.out.printf("%s %5d\n", user,total);



格式化的输入 Formatted Input
这个Scanner API提供了基本的读入数据的功能,例如从控制台上或者其他任何的数据流中读入数据的功能。

下面的范例从标准输入中读入了一个字符串并且希望字符串随后是一个int值。
Scanner中的方法例如next和nextInt会在没有数据的时候自动失效.如果你需要处理一个非常复杂的输入,那么还可以使用java.util.Formatter类中的模式匹配算法。

Scanner s=Scanner.create(System.in);
String param= s.next();
int value=s.nextInt();
s.close();




9.可变参数
可变参数使程序员可以声明一个接受可变数目参数的方法。注意,可变参数必须是函数声明中的最后一个参数。假设我们要写一个简单的方法打印一些对象,

util.write(obj1);
util.write(obj1,obj2);
util.write(obj1,obj2,obj3);




  在JDK1.5之前,我们可以用重载来实现,但是这样就需要写很多的重载函数,显得不是很有效。如果使用可变参数的话我们只需要一个函数就行了

public void write(Object... objs) {
   for (Object obj: objs)
      System.out.println(obj);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值