黑马程序员_高新技术之反射和泛型

----------- android培训java培训、java学习型技术博客、期待与您交流! ----------

 

一,反射

<一>反射的基石Class类
1,提问: Person类代表人,它的实例对象就是张三,李四这样一个个具体的人, Java程序中的各个Java类属于同一类事物,描述这类事物的Java类名就是Class。对比提问:众多的人用一个什么类表示?众多的Java类用一个什么类表示?
人-->Person
Java类-->Class

2, Class类代表Java类,它的各个实例对象又分别对应什么呢?
对应各个类在内存中的字节码,例如,Person类的字节码,ArrayList类的字节码,等等。
一个类被类加载器加载到内存中,占用一片存储空间,这个空间里面的内容就是类的字节码,不同的类的字节码是不同的,所以它们在内存中的内容是不同的,这一个个的空间可分别用一个个的对象来表示。

3,如何得到各个字节码对应的实例对象( Class类型)
类名.class,例如,System.class
对象.getClass(),例如,new Date().getClass()
Class.forName("类名"),例如,Class.forName("java.util.Date");

4,九个预定义Class实例对象:
有九种预定义的 Class 对象,表示八个基本类型和 void。这些类对象由 Java 虚拟机创建,与其表示的基本类型同名,即 boolean、byte、char、short、int、long、float 和 double。
这些对象仅能通过下列声明为 public static final 的变量访问,也是使此方法返回 true 的仅有的几个 Class 对象。
比如:Int.class == Integer.TYPE

总之,只要是在源程序中出现的类型,都有各自的Class实例对象,例如,int[],void…

<二>反射
1,定义:
 反射就是把Java类中的各种成分映射成相应的java类。例如,一个Java类中用一个Class类的对象来表示,一个类中的组成部分:成员变量,方法,构造方法,包等等信息也用一个个的Java类来表示,就像汽车是一个类,汽车中的发动机,变速箱等等也是一个个的类。表示java类的Class类显然要提供一系列的方法,来获得其中的变量,方法,构造方法,修饰符,包等信息,这些信息就是用相应类的实例对象来表示,它们是Field、Method、Contructor、Package等等。

 一个类中的每个成员都可以用相应的反射API类的一个实例对象来表示,通过调用Class类的方法可以得到这些实例对象。
<三>Constructor类
Constructor类代表某个类中的一个构造方法
1,得到某个类所有的构造方法:
例子:Constructor [] constructors= Class.forName("java.lang.String").getConstructors();

2,得到某一个构造方法:
例子:      Constructor constructor = Class.forName(“java.lang.String”).getConstructor(StringBuffer.class);
 //获得方法时要用到类型

3,创建实例对象:
通常方式:String str = new String(new StringBuffer("abc"));
反射方式: String str = (String)constructor.newInstance(new StringBuffer("abc"));
 //调用获得的方法时要用到上面相同类型的实例对象

4,Class.newInstance()方法:
例子:String obj = (String)Class.forName("java.lang.String").newInstance();
该方法内部先得到默认的构造方法,然后用该构造方法创建实例对象。
该方法内部的具体代码是怎样写的呢?用到了缓存机制来保存默认构造方法的实例对象。

<四>Field类
Field类代表某个类中的一个成员变量
示例代码:

ReflectPoint pt1 =new ReflectPoint(3,5);
		Field fieldY =pt1.getClass().getField("y");
//		Field fieldX =pt1.getClass().getField("x");
		//因为X是私有的,不可见的,这里强制将X取出
		Field fieldX =pt1.getClass().getDeclaredField("x");
		//将x设置为可获取的
		fieldX.setAccessible(true);
		System.out.println(fieldY.get(pt1));
		System.out.println(fieldX.get(pt1));

ReflectPoint类的定义:
public class ReflectPoint {
	private int x;
	public int y;
	
	public ReflectPoint(int x, int y) {
		super();
		this.x = x;
		this.y = y;
	}
} 


 

得到的Field对象是对应到类上面的成员变量,还是对应到对象上的成员变量?类只有一个,而该类的实例对象有多个,如果是与对象关联,那关联的是哪个对象呢?所以字段fieldX 代表的是x的定义,而不是具体的x变量。

实例演示:

//将对象中的某个值替换成另一个值 
	private static void changeStringValue(Object obj)throws Exception {
		// TODO Auto-generated method stub
		//获取对象的字节码的字段
		Field[] fields = obj.getClass().getFields();
		for(Field field:fields){
			/*字段比较用==号 因为都只有一份
			  首先判断字段的类型是否等于String类
			 */
			if(field.getType() == String.class){
				//获取对象中的字段
				String oldValue =(String)field.get(obj);
				String newValue =oldValue.replace('b', 'a');
				field.set(obj, newValue);
			}
		}
	}


 

<五>Method类
1,Method类代表某个类中的一个成员方法

2,得到类中的某一个方法:
例子:      Method charAt = Class.forName("java.lang.String").getMethod("charAt", int.class);

3,调用方法:
通常方式:System.out.println(str.charAt(1));
反射方式: System.out.println(charAt.invoke(str, 1));
如果传递给Method对象的invoke()方法的第一个参数为null,这有着什么样的意义呢?说明该Method对象对应的是一个静态方法!

4,jdk1.4和jdk1.5的invoke方法的区别:
Jdk1.5:public Object invoke(Object obj,Object... args)
Jdk1.4:public Object invoke(Object obj,Object[] args),即按jdk1.4的语法,需要将一个数组作为参数传递给invoke方法时,数组中的每个元素分别对应被调用方法中的一个参数,所以,调用charAt方法的代码也可以用Jdk1.4改写为 charAt.invoke(“str”, new Object[]{1})形式。1会自动装箱成new Integer(){1}

<六>用反射方式执行某个类中的main方法
问题:
启动Java程序的main方法的参数是一个字符串数组,即public static void main(String[] args),通过反射方式来调用这个main方法时,如何为invoke方法传递参数呢?按jdk1.5的语法,整个数组是一个参数,而按jdk1.4的语法,数组中的每个元素对应一个参数,当把一个字符串数组作为参数传递给invoke方法时,javac会到底按照哪种语法进行处理呢?jdk1.5肯定要兼容jdk1.4的语法,会按jdk1.4的语法进行处理,即把数组打散成为若干个单独的参数。所以,在给main方法传递参数时,不能使用代码mainMethod.invoke(null,new String[]{“xxx”}),javac只把它当作jdk1.4的语法进行理解,而不把它当作jdk1.5的语法解释,因此会出现参数类型不对的问题。

解决办法:
mainMethod.invoke(null,new Object[]{new String[]{"xxx"}});
mainMethod.invoke(null,(Object)new String[]{"xxx"}); ,编译器会作特殊处理,编译时不把参数当作数组看待,也就不会数组打散成若干个参数了

<七>数组的反射
具有相同维数和元素类型的数组属于同一个类型,即具有相同的Class实例对象。
代表数组的Class实例对象的getSuperClass()方法返回的父类为Object类对应的Class。
基本类型的一维数组可以被当作Object类型使用,不能当作Object[]类型使用;非基本类型的一维数组,既可以当做Object类型使用,又可以当做Object[]类型使用。
补充:Array工具类用于完成对数组的反射操作。
代码演示:

int [] a1 = new int[]{1,2,3};
		 int [] a2 = new int[4];
		 int[][] a3 = new int[2][3];
		 String [] a4 =new String[]{"a","b","c"};
		 System.out.println(a1.getClass() == a2.getClass());
//		 System.out.println(a1.getClass() == a4.getClass());
//		 System.out.println(a1.getClass() == a3.getClass());
		 System.out.println(a1.getClass().getName());
		 System.out.println(a1.getClass().getSuperclass().getName());
		 System.out.println(a4.getClass().getSuperclass().getName());
		 
		 Object aObj1 =a1;
		 Object aObj2 =a4;//String[] 一维数组可以被当成Object类型使用
//		 Object[] aObj3 = a1;
		 Object[] aObj4 =a3;//基本类型的二维数组可以当成Object[]类型使用
		 Object[] aObj5 =a4;//String[] 一维数组可以被当成Object[]类型使用
		 System.out.println(a1);
		 System.out.println(a4);
		 //Arrays.asList()方法处理int[]和String[]时的差异。
		 System.out.println(Arrays.asList(a1));//打印结果为[[I@19ee1ac]
		 System.out.println(Arrays.asList(a4));//打印结果为[a, b, c]
		 
				 printObject(a1);
				 
				 printObject("xyz");
		
	}
	private static void printObject(Object obj) {
		Class clazz=obj.getClass();
		if(clazz.isArray()){
			int len = Array.getLength(obj);
			for(int i =0;i<len;i++){
				System.out.println(Array.get(obj,i));
			}
		}else{
			System.out.println(obj);
		}
	}


 

<八>反射的作用-->实现框架功能
1,框架与框架要解决的核心问题
我做房子卖给用户住,由用户自己安装门窗和空调,我做的房子就是框架,用户需要使用我的框架,把门窗插入进我提供的框架中。框架与工具类有区别,工具类被用户的类调用,而框架则是调用用户提供的类。

2,框架要解决的核心问题
我在写框架(房子)时,你这个用户可能还在上小学,还不会写程序呢?我写的框架程序怎样能调用到你以后写的类(门窗)呢?
因为在写才程序时无法知道要被调用的类名,所以,在程序中无法直接new 某个类的实例对象了,而要用反射方式来做。

3,代码演示:
首先eclipse自动生成 ReflectPoint类的equals和hashcode方法

@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + x;
		result = prime * result + y;
		return result;
	}



	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		ReflectPoint other = (ReflectPoint) obj;
		if (x != other.x)
			return false;
		if (y != other.y)
			return false;
		return true;
	}


 

public class ReflectTest2 {

 /**
  * @param args
  */
 public static void main(String[] args)throws Exception {
  // TODO Auto-generated method stub
  /*getRealPath();//金山词霸/内部
   * 一定要记住用完整的路径,但完整的路径不是硬编码,而是运算出来的。
   **/
//  InputStream ips =new FileInputStream("config.properties");
  /*一个类加载器能加载.class文件,那它当然也能加载classpath环境下的其他文件,既然它有如此能力,
   * 它没有理由不顺带提供这样一个方法。它也只能加载classpath环境下的那些文件。
   * 注意:直接使用类加载器时,不能以/打头。*/

  /*getClassLoader:返回该类的类加载器
   * public InputStream getResourceAsStream(String name)返回读取指定资源的输入流。 
  */
//  InputStream ips =ReflectTest2.class.getClassLoader().getResourceAsStream("cn/itcast/day1/config.properties");
  //Class提供了一个便利方法,用加载当前类的那个类加载器去加载相同包目录下的文件
  InputStream ips = ReflectTest2.class.getResourceAsStream("resource/config.properties");
  Properties props =new Properties();
  props.load(ips); //从输入流中读取属性列表(键和元素对)。
  ips.close();//ips对象还在 关闭的是关联的系统资源或物理资源 该对象由JVM管理
  String className =props.getProperty("className");
  Collection collections =(Collection)Class.forName(className).newInstance();
   
//  Collection collections =new HashSet();
  ReflectPoint pt1 =new ReflectPoint(3,3);
  ReflectPoint pt2 =new ReflectPoint(5,5);
  ReflectPoint pt3 =new ReflectPoint(3,3);
  collections.add(pt1);
  collections.add(pt2);
  collections.add(pt3);
  collections.add(pt1);
  
//  pt1.y = 7;
//  collections.remove(pt1);
  System.out.println(collections.size());
 }

}


HashSet和ArrayList两个集合的运行结果差异。
ArrayList是一个有序的集合,把每个对象的引用按照位置顺序放入集合中,不管是这个对象是否与前个对象相同也会按照顺序放入。所以每一个对象都能放进去,打印的结果为4.
HashSet集合是放入一个对象前会判断有没有这个对象会去比较两个对象的引用是否相等。如果该对象的引用存在就不放入集合中。因为这里生成了hashcode和覆盖了equls方法,所以两个对象的内容相等也不会放入集合。这里打印的结果为2.

二,泛型

1,引入泛型
Jdk 1.5以前的集合类中存在什么问题?

ArrayList collection = new ArrayList();
	collection.add(1);
	collection.add(1L);
	collection.add("abc");
	int i = (Integer) collection.get(1);//编译要强制类型转换且运行时出错!


没有使用泛型时,只要是对象,不管是什么类型的对象,都可以存储进同一个集合中。使用泛型集合,可以将一个集合中的元素限定为一个特定类型,集合中只能存储同一个类型的对象,这样更安全;并且当从集合获取一个对象时,编译器也可以知道这个对象的类型,不需要对对象进行强制类型转换,这样更方便。
泛型就是把原来的类名进行了延长!
Jdk 1.5的集合类希望你在定义集合时,明确表示你要向集合中装哪种类型的数据,无法加入指定类型以外的数据

ArrayList<Integer> collection2 = new ArrayList<Integer>();
	collection2.add(1);
	/*collection2.add(1L);
	collection2.add(“abc”);*///这两行代码编译时就报告了语法错误
	int i2 = collection2.get(0);//不需要再进行类型转换


 

2,泛型的定义
 泛型是提供给javac编译器使用的,可以限定集合中的输入类型,让编译器挡住源程序中的非法输入,编译器编译带类型说明的集合时会去除掉“类型”信息,使程序运行效率不受影响,对于参数化的泛型类型,getClass()方法的返回值和原始类型完全一样。由于编译生成的字节码会去掉泛型的类型信息,只要能跳过编译器,就可以往某个泛型集合中加入其它类型的数据,例如,用反射得到集合,再调用其add方法即可。

3,ArrayList<E>类定义和ArrayList<Integer>类引用中涉及如下术语:
整个称为ArrayList<E>泛型类型
ArrayList<E>中的E称为类型变量或类型参数
整个ArrayList<Integer>称为参数化的类型
ArrayList<Integer>中的Integer称为类型参数的实例或实际类型参数
ArrayList<Integer>中的<>念着typeof
ArrayList称为原始类型

4,参数化类型与原始类型的兼容性:
参数化类型可以引用一个原始类型的对象,编译报告警告,例如,Collection<String> c = new Vector();//可不可以,不就是编译器一句话的事吗?
原始类型可以引用一个参数化类型的对象,编译报告警告,例如,Collection c = new Vector<String>();//原来的方法接受一个集合参数,新的类型也要能传进去

5,参数化类型不考虑类型参数的继承关系:
Vector<String> v = new Vector<Object>(); //错误!///不写<Object>没错,写了就是明知故犯
Vector<Object> v = new Vector<String>(); //也错误!

6,编译器不允许创建泛型变量的数组。即在创建数组实例时,数组的元素不能使用参数化的类型,例如,下面语句有错误:
  Vector<Integer> vectorList[] = new Vector<Integer>[10];


7,泛型中的?通配符
定义一个方法,该方法用于打印出任意参数化类型的集合中的所有数据,该方法如何定义呢?
错误方式:

public static void printCollection(Collection<Object> cols) {
		for(Object obj:cols) {
			System.out.println(obj);
		}
		/* cols.add("string");//没错
		 cols = new HashSet<Date>();//会报告错误!*/
}


正确方式:

public static void printCollection(Collection<?> cols) {
		for(Object obj:cols) {
			System.out.println(obj);
		}
		//cols.add("string");//错误,因为它不知自己未来匹配就一定是String
		cols.size();//没错,此方法与类型参数没有关系
		 cols = new HashSet<Date>();
	}


 

总结:
Cols<Object> 中的Object只是说明Cols<Object> 实例对象中的方法接受的参数是Object
Cols<Object> 是一种具体类型,new HashSet<Date>也是一种具体类型,两者没有兼容性问题
Collection<?>  a可以与任意参数化的类型匹配,但到底匹配的是什么类型,只有以后才知道,所以,
a=new ArrayList<Integer>和a=new ArrayList<String>都可以, 但a.add(new Date())或a.add(“abc”)都不行,

使用?通配符可以引用其他各种参数化的类型,?通配符定义的变量主要用作引用,可以调用与参数化无关的方法,不能调用与参数化有关的方法。

8,泛型中的?通配符的扩展
限定通配符的上边界:
正确:Vector<? extends Number> x = new Vector<Integer>();
错误:Vector<? extends Number> x = new Vector<String>();
限定通配符的下边界:
正确:Vector<? super Integer> x = new Vector<Number>();
错误:Vector<? super Integer> x = new Vector<Byte>();
提示:
限定通配符总是包括自己。
?只能用作引用,不能用它去给其他变量赋值
 Vector<? extends Number> y = new Vector<Integer>();
 Vector<Number> x = y;
 上面的代码错误,原理与Vector<Object > x11 = new Vector<String>();相似,
 只能通过强制类型转换方式来赋值。

9,泛型的一个综合案例

HashMap<String,Integer> maps =new HashMap<String,Integer>();
		maps.put("zxx", 12);
		maps.put("huan", 22);
		maps.put("yy", 19);
		//HashMap不能进行遍历,所以将它存入一个set集合
		Set<Map.Entry<String,Integer>> entrySet =maps.entrySet();
		for(Map.Entry<String, Integer> entry :entrySet){
//			System.out.println(entry);
			System.out.println(entry.getKey()+":"+entry.getValue());
		}


 10,定义泛型方法
1.Java中的泛型类型(或者泛型)类似于 C++ 中的模板。但是这种相似性仅限于表面,Java 语言中的泛型基本上完全是在编译器中实现,用于编译器执行类型检查和类型推断,然后生成普通的非泛型的字节码,这种实现技术称为擦除(erasure)(编译器使用泛型类型信息保证类型安全,然后在生成字节码之前将其清除)。这是因为扩展虚拟机指令集来支持泛型被认为是无法接受的,这会为 Java 厂商升级其 JVM 造成难以逾越的障碍。所以,java的泛型采用了可以完全在编译器中实现的擦除方法。
例如,下面这两个方法,编译器会报告错误,它不认为是两个不同的参数类型,而认为是同一种参数类型。

private static void applyGeneric(Vector<String> v){
}

private static void applyGeneric(Vector<Date> v){
}


2,Java的泛型方法没有C++模板函数功能强大,java中的如下代码无法通过编译:
<T> T add(T x,T y) {
  return (T) (x+y);
  //return null;
 }
用于放置泛型的类型参数的尖括号应出现在方法的其他所有修饰符之后和在方法的返回类型之前,也就是紧邻返回值之前。按照惯例,类型参数通常用单个大写字母表示。
交换数组中的两个元素的位置的泛型方法语法定义如下:

static <E> void swap(E[] a, int i, int j) {
	E t = a[i];
	a[i] = a[j];
	a[j] = t;
}


3,只有引用类型才能作为泛型方法的实际参数,swap(new int[3],3,5);语句会报告编译错误。
这是因为编译器不会对new int[3]中的int自动拆箱和装箱了,因为new int[3]本身已经是对象了,你想要的有可能就是int数组呢?它装箱岂不弄巧成拙了。
代码演示:
编写一个泛型方法,自动将Object类型的对象转换成其他类型。

private static <T> T autoConvert(Object obj){
		return (T)obj;
	}


定义一个方法,可以将任意类型的数组中的所有元素填充为相应类型的某个对象。

private static <T> void fillArray(T[] a,T obj){
		for(int i=0;i<a.length;i++){
			a[i] = obj;
		}
	}


采用自定泛型方法的方式打印出任意参数化类型的集合中的所有内容。

public static void printCollection(Collection<?> collection){
		System.out.println(collection.size());
		for(Object obj :collection){
			System.out.println(obj);
		}
	}


或者这样写:

public static <T> void printCollection2(Collection<T> collection){
		System.out.println(collection.size());
		for(Object obj :collection){
			System.out.println(obj);
		}
	}


 

注意:在这种情况下,前面的通配符方案要比范型方法更有效,当一个类型变量用来表达两个参数之间或者参数和返回值之间的关系时,即同一个类型变量在方法签名的两处被使用,或者类型变量在方法体代码中也被使用而不是仅在签名的时候使用,才需要使用范型方法。


除了在应用泛型时可以使用extends限定符,在定义泛型时也可以使用extends限定符,例如,Class.getAnnotation()方法的定义。并且可以用&来指定多个边界,如<V extends Serializable & cloneable> void method(){}
普通方法、构造方法和静态方法中都可以使用泛型。
也可以用类型变量表示异常,称为参数化的异常,可以用于方法的throws列表中,但是不能用于catch子句中。
在泛型中可以同时有多个类型参数,在定义它们的尖括号中用逗号分,例如:
public static <K,V> V getValue(K key) { return map.get(key);}

11,泛型参数的类型推断
 编译器判断范型方法的实际类型参数的过程称为类型推断,类型推断是相对于知觉推断的,其实现方法是一种非常复杂的过程。
 根据调用泛型方法时实际传递的参数类型或返回值的类型来推断,具体规则如下:
 当某个类型变量只在整个参数列表中的所有参数和返回值中的一处被应用了,那么根据调用方法时该处的实际应用类型来确定,这很容易凭着感觉推断出来,即直接根据调用方法时传递的参数类型或返回值来决定泛型参数的类型,例如:
  swap(new String[3],3,4)       static <E> void swap(E[] a, int i, int j)
 当某个类型变量在整个参数列表中的所有参数和返回值中的多处被应用了,如果调用方法时这多处的实际应用类型都对应同一种类型来确定,这很容易凭着感觉推断出来,例如:
  add(3,5)    static <T> T add(T a, T b)
 当某个类型变量在整个参数列表中的所有参数和返回值中的多处被应用了,如果调用方法时这多处的实际应用类型对应到了不同的类型,且没有使用返回值,这时候取多个参数中的最大交集类型,例如,下面语句实际对应的类型就是Number了,编译没问题,只是运行时出问题:
  fill(new Integer[3],3.5f)    static <T> void fill(T[] a, T v)
 当某个类型变量在整个参数列表中的所有参数和返回值中的多处被应用了,如果调用方法时这多处的实际应用类型对应到了不同的类型, 并且使用返回值,这时候优先考虑返回值的类型,例如,下面语句实际对应的类型就是Integer了,编译将报告错误,将变量x的类型改为float,对比eclipse报告的错误提示,接着再将变量x类型改为Number,则没有了错误:
  int x =(3,3.5f)    static <T> T add(T a, T b)
 参数类型的类型推断具有传递性,下面第一种情况推断实际参数类型为Object,编译没有问题,而第二种情况则根据参数化的Vector类实例将类型变量直接确定为String类型,编译将出现问题:
 copy(new Integer[5],new String[5])  static <T> void copy(T[] a,T[]  b);
 copy(new Vector<String>(), new Integer[5])  static <T> void copy(Collection<T> a , T[] b);

12,定义泛型类型
 如果类的实例对象中的多处都要用到同一个泛型参数,即这些地方引用的泛型类型要保持同一个实际类型时,这时候就要采用泛型类型的方式进行定义,也就是类级别的泛型,语法格式如下:

public class GenericDao<T> {
		private T field1;
		public void save(T obj){}
		public T getById(int id){}
	}


 

类级别的泛型是根据引用该类名时指定的类型信息来参数化类型变量的,例如,如下两种方式都可以:
GenericDao<String> dao = null;
new genericDao<String>();
注意:
在对泛型类型进行参数化时,类型参数的实例必须是引用类型,不能是基本类型。
当一个变量被声明为泛型时,只能被实例变量、方法和内部类调用,而不能被静态变量和静态方法调用。因为静态成员是被所有参数化的类所共享的,所以静态成员不应该有类级别的类型参数。

问题:类中只有一个方法需要使用泛型,是使用类级别的泛型,还是使用方法级别的泛型?-->类级别

13,通过反射获得泛型的参数化类型
演示代码:

//通过反射获取到方法
		Method applyMethod =GenericTest.class.getMethod("applyVector", Vector.class);
		//得到方法的泛型参数类型
		Type[] types = applyMethod.getGenericParameterTypes();
		//得到第一个参数化类型
		ParameterizedType pType =(ParameterizedType)types[0];
		//打印原始参数类型
		System.out.println(pType.getRawType());
		//打印此类型实际类型参数
		System.out.println(pType.getActualTypeArguments()[0]);
		
	}
	
	public static  void applyVector(Vector<Date> v1){
		
	}
	


 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值