基础加强-3


泛型

没有使用泛型时,只要是对象,不管是什么类型的对象,都可以存储到同一个集合。使用泛型集合,可以将一个集合中的原始限定为有一个特定类型,集合中只能存储同一类型的对象,这样更安全,并且当从集合获取一个对象时,编译器也可以知道这个对象的类型,不需要对对象进行强制类型转换,这样更方便。

没使用泛型的集合简单实例:

ArrayList collection1 = new ArrayList();

collection1.add(1);

collection1.add(1L);

collection1.add("abc");

int i = (Integer)collection1.get(1);

使用泛型的集合简单实例:   

  ArrayList<String> collection2 = new ArrayList<String>();

   collection2.add("abc");//只能向集合里加入String类型的对象,别的任何类型都加不进去,包括它的父类的对象中的其他非字符串的对象,如Object中的非字符串对象,也加不进去,但如果集合参数类型是Object类型,什么对象都可以加进去,因为什么对象都是Object的对象,它是一切类的父类

     String element = collection2.get(0); 

 

泛型是给编译器使用的,可以限定集合中的输入类型,让编译器挡住源程序中的非法输入,编译器编译带类型说明的集合后会去掉“类型”信息,使程序运行效率不受影响,对于参数化的泛型类型,(如集合名后加上了<String>)其对象的getClass()方法的返回值同原始类型(集合名后不加<String>)对象getClass()后的字节码完全一样,由于编译器生成的字节码会去掉泛型的类型信息,只要能跳过编译器(利用反射,获取某个集合对象的Class字节码对象再调用自身的add()),就可以往某个泛型集合中加入其它类型的数据

 

ArrayList<E>类型定义和ArrayList<Integer>类引用中涉及如下术语:

ArrayList称为:原始类型

ArrayList<E>整个称为:泛型类型

ArrayList<Integer>整个称为:参数化的泛型类型

ArrayList<E>中的E称为:类型参数或类型变量

ArrayList<Integer>中的Integer称为:实际类型参数或类型参数的实例

参数化类型与原始类型的兼容性:

参数化类型可以引用一个原始类型的对象,编译报警告不报错,例如:

Collection<String> c=new Vector();

原始类型可以引用一个参数化类型的对象,编译报警告,不报错,例如:

Collection c=new Vector<String>();

参数化类型不考虑类型参数的继承关系:

Vector<String>v=new Vector<Object>();//错误!不写<Object>没错,写了就是明知故犯

Vector<Object>v= new Vector<String>();//错误!不写<Object>可以算对

下面的代码编译器不会报错:

Vector v1=new Vector<String>(); 原因:编译器只管一行一行的编译,不管

Vector<Object>v=v1; 运行,发现不了v1Vector <String>类型的,而认为他是Vector原始类型的,所以并不违法编译语法,运行报错

下面的代码编译器会报错:

 

Vector<String> v1=new Vector<String>();// 原因:编译器编译时

Vector<Object>v=v1; 发现v1Vector <String>类型,而参数化类型不考虑类型参数的继承关系:将报错这句等同于:

Vector<Object>v =new Vector<String>();//错误!

在创建数组实例时,数组的元素不能使用参数化的类型,例如:下面语句有错误:

Vector<Integer>vectorList[]=new Vector<Integer>[10];

泛型中的?通配符

Collection<String> collection1=new Vector<String>();

printCollection1(collection1);//报错,泛型里不考虑类型参数的继承关系,printCollection接收的参数类型是

Collection<Object>,所以编译通不过

printCollection2(collection1);//正确,printCollection2参数里的<>匹配任何对象类型

public static void printCollection1(Collection<Object> collection)

{

collection.add(1);//可以加入任何对象,因为泛型参数类型为Object,任何对象都是Object对象的

       System.out.println(collection.size());

       for(Object obj : collection)

           System.out.println(obj);

       collection =new HashSet<Date>();/*报错,原因:参数Cols的类型是Collection<Object>此句是个赋值语句,即:

Collection<Object> collection =new HashSet<Date>();

这是错误的 */

          

    }

                                        

public static void printCollection2(Collection<?> collection)

{

//collection.add(1);不可以调用add(),因为collection是个参数,不确定传递进来的是什么参数化泛型类型对象,由此也不确定add()什么类型的对象,如果传递过来的是泛型参数类型为Object的参数化泛型类型对象,则什么对象都可以加,但不一定传进来的就是参数类型为Object的泛型,如传递过来的是其他参数类型的泛型,则只能add()相应的对象。

 

           System.out.println(collection.size());

            for(Object obj : collection) /不管collection里面存的是什么具体类型的对象,都是Object对象,这是确定的,因为子类的对象也就是父类的*/

           System.out.println(obj);

            collection =new HashSet<Date>();/*可以,这是给参数赋值的语句,

相当于:

            Collection<?> collection= new HashSet<Date>();

这是完全可以的,?的作用就是为了这个目的。和在方法外部调用此方法,给形参collection传递一个实参的性质是一样的  */

       }

总结:

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

泛型中的?通配符的扩展

限定通配符的上边界:

正确:Vector<?extends Number> x=new Vector<Integer>();//Vector中加入对象的类型必须是NumberNumber的子类类型

错误:Vector<?extends Number> x=new Vector<String>();//String不是Number的子类

限定通配符的下边界:

正确:Vector<? super Integer> x=new Vector<Number>();//Vector中加入对象的类型必须是IntegerInteger父类类型的,NumberInteger的父类

错误:Vector<? Super Integer> x=new Vector<Byte>();// Byte不是Integer的父类,它和Integer平级

注:限定通配符总是包括自己。

泛型的综合应用

public static void main(String[] args) {

Map<String,Integer> maps=new HashMap<String,Integer>();/*Map是一个接口,内部结构是一条条的记录,记录由两个字段组成keyvalue.

                     <String,Integer> 就是对key,value的类型进行限制,而在接口Map里面定义了一个内部类EntryEntry将一条记录的key,value封装成一个整体,即一个Map.Entry类的对象即是实现接口Map类的对象里的一条记录,HashMap是实现Map接口的一个类,*/

       maps.put("one", 1);

       maps.put("two", 2);

        maps.put("three",3);

Set<Map.Entry<String, Integer>>entrySet=maps.entrySet();/*Set是一个集合类接口,它实现了Iterator接口, 其子类对象可以用增强 for遍历, 实现Set接口的类是一个集合类,这个集合类的对象是一个集合,集合内部所装的对象是Map.Entry类型的。HashMap里有个方法entrySet() ,返回一个对应对象 maps 、实现Set接口类的对象*/

   

        for(Map.Entry<String, Integer> m:entrySet)

        {System.out.println(m.getKey()+"="+m.getValue());}

MapEntry的关系是:Entry是接口Map里面的一个内部类,用于将Map内部的一条记录key,value封装成一个整体

MapSet的区别在于:Map内部结构里面的每条记录分成两部分,keyvalue,Set内部结构里的每条记录都是一个Map.Entry对象,即keyvalue的封装体,Set可以用增强for循环遍历

        */

       // TODO Auto-generated method stub

    }

定义泛型方法

Private static <T> T addT x,T y//T必须是类,不能是基本类型如int

{

    Return x+y;

}

    add(3,5); //会自动装箱,将35装成对应的Integer对象

    add(3.5,3); //会将3.53转换成floatInteger的共同的父类Number对象

add(3,"abc");//会将3”abc”转换成IntegerString的共同的父类Object对象

 

private static <T> void swap(T[] a,int i,int j){

       T tmp = a[i];

       a[i] = a[j];

       a[j] = tmp;

                       }

swap(new String[]{"abc","xyz","itcast"},1,2);

//swap(new int[]{1,3,5,4,5},3,4);//编译通不过,T不能是基本类型,此时不会装箱

用于放置泛型的类型参数的尖括号放在方法返回值之前,紧邻返回值,类型参数通常用单个大写字母表示

除了在应用泛型时可以使用extends限定符,在定义泛型时也可以使用extends限定符如:

JDK文档中的 Class.getAnnotation()方法的定义。并且可以用&来指定多个边界,如 <V extends Serializable&cloneable>void method(){}

普通方法,构造方法和静态方法中都可以使用泛型。编译器也不允许创建类型变量的数组。

也可以用类型变量表示异常,称为参数化的异常,可以用于方法的throws列表中,但是不能用于catch子句中。

Private static <T extends Exception> sayHello()throws T

{

try{}

 catch(Exception e)

 {throw(T)e;}

}

 

在泛型中可以同时有多个类型参数,在定义它们的尖括号中用逗号分开,例如:

Public static<K,V>V getValue(K key){return map.get(key)}

例子;

编写一个泛型方法,自动将Object类型的对象转换成其他类型。

private static <T> T autoConvert(Object obj){

       return (T)obj;

    }

Object obj=23;

Integer x=autoConvert(obj);

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

private static <T> void fillArray(T[] a,T obj){

       for(int i=0;i<a.length;i++){

           a[i] = obj;

       }

    }

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

public static <T> void printCollection2(Collection<T> collection){

       //collection.add(1);

       System.out.println(collection.size());

       for(Object obj : collection){

           System.out.println(obj);

       }

 

    }

定义两个方法,copy1把任意参数类型的集合中的数据安全的复制到相应类型的数组中

Copy2把一个数组中的内容复制到另一个数组中

public static <T> void copy1(Collection<T> dest,T[] src){

      

    }

   

    public static <T> void copy2(T[] dest,T[] src){

      

    }  

copy1(new Vector<String>(),new String[10]);

copy2(new Date[10],new String[10]); //传播,把DateString都转换成Object//copy1(new Vector<Date>(),new String[10]);//传递,不可转换类型

定义泛型类型

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

Public calss GenericDao <E>{

 Private T field1;

 Public void save(T obj) {}

   Public T getBytd(int id){}

}

类级别的泛型是根据引用类名时指定的类型信息来参数化类型变量的如下两种方式都可以:

 1GenericDao<String >dao=null;

2new genericDao<String>();

   Dao (data access object) 数据访问对象,是用于数据访问的,数据访问就是操作数据库crud

    import java.util.Set;

 

//dao data access object--->crud

public class GenericDao<E>  {

    public void add(E x){

      

    }

   

    public E findById(int id){

       return null;

    }

   

    public void delete(E obj){

      

    }

   

    public void delete(int id){

      

    }  

   

    public void update(E obj){

      

    }

   

    public static <E> void update2(E obj){

      

    }

   

    public E findByUserName(String name){

       return null;

    }

    public Set<E> findByConditions(String where){

       return null;

    }

}

 

}

GenericDao<ReflectPoint> dao = new GenericDao<ReflectPoint>();

       dao.add(new ReflectPoint(3,3));   

       //String s = dao.findById(1);

注意:

在对泛型类型进行参数化时,类型参数必须是引用类型,不能是基本类型。

在定义泛型类型时,泛型变量,只能被实例变量和方法调用(还有内嵌类型),不能被静态变量和静态方法调用。因为静态成员时被所有参数化的类所共享的,所以静态成员不应该有类级别的类型参数

通过反射获得泛型的实际类型参数

Vector<Date> v1 = new Vector<Date>();//通过对象v1,是没法知道类型参数的类型的,但是当把这个变量当做一个方法去使用的时候,通过这个方法,是可以知道这个对象的泛型类型的类型变量的类型,如下:

Method applyMethod = GenericTest.class.getMethod("applyVector", Vector.class);

Type[] types = applyMethod.getGenericParameterTypes();//获得方法的泛型类型参数的类型,因为有可能有多个参数,所以返回一个数组,数组元素都是一个个Type对象

ParameterizedType pType = (ParameterizedType)types[0];//因为我们知道applyVector方法只有一个参数,所以第一个元素即是我们想要获得的方法参数的类型

System.out.println(pType.getRawType());//得到方法参数的原始类型:

Class java.util.Vector

System.out.println(pType.getActualTypeArguments()[0]);

//得到方法参数的泛型类型的实际类型参数,getActualTypeArguments()返回的是一个数组,因为泛型类型的类型参数有可能不止一个如:

Map<String,Integer> maps=new HashMap<String,Integer>();就有2个类型参数

而现在我们知道,类型参数只有一个所以第一个元素,即是我们要的:

                                         Class Java .util.Date

public static void applyVector(Vector<Date> v1)

{  }

类加载器

Java虚拟机中可以安装多个类加载器,系统默认3个主要的类加载器,每个加载器负责加载特定位置的类:

1BootStrap(加载:jdk…:/JRE/lib/rt.jar中的类,主要是系统类,如System)

2ExClassLoader(加载:jdk…:JRE/lib/ext/*.jar中的类)

3AppClassLoaderCLASSPATH指定的所有jar或目录)

类加载器其实也是Java类,正因为它也是类,所以当它加载别的类的时候,必定得有其他的加载器加载它,显然必须有一个类加载器不是java类,这正是BootStart,它不是一个Java类,它是嵌套在JAVA虚拟机内核里面的一个类加载器,是用C++语言写的一段二进制代码,java虚拟机一启动,它就自动运行,不需要加载,其他类加载器正是先被他加载进虚拟机,然后其他加载器再去加载别的类

这三个加载器的继承关系及管辖范围图如下所示:

 

高新技术—3 - 雪天 - 张晴的博客

  

 

 

 

 

例子代码:

public class ClassLoaderTest {

 

    public static void main(String[] args) throws Exception {

       System.out.println(

       ClassLoaderTest.class.getClassLoader().getClass().getName()

              );

       System.out.println(System.class.getClassLoader());     

       System.out.println("xxx");

ClassLoader loader = ClassLoaderTest.class.getClassLoader();

//获得加载ClassLoaderTest这个类的加载器类对象

while(loader != null){  System.out.println(loader.getClass().getName());//获得加ClassLoaderTest类的加载器类的名称

       loader = loader.getParent();//获得获得加载ClassLoaderTest这个类的加载器类父类的对象

       }

     System.out.println(loader);

结果:

sun.misc.Launcher$AppClassLoader

null

xxx

sun.misc.Launcher$AppClassLoader

sun.misc.Launcher$ExtClassLoader

null

      

如将ClassLoaderTest类打成jar包放入jdk…:JRE/lib/ext/目录下

结果:sun.misc.Launcher$ExtClassLoader

null

xxx

sun.misc.Launcher$ExtClassLoader

null

类加载器的委托机制

java虚拟机要加载一个类时,到底派出哪个类加载器去加载呢?

首先当前线程的类加载器去加载线程中的第一个类

如果类A中引用了类BJava虚拟机将使用加载A的类加载器去加载类B

还可以直接调用ClassLoader.loadClass()方法来指定某个加载器去加载某个类

每个类加载器加载类时,又先委托给其上级类加载器。当所有祖宗类加载器都没有加载到类,则回到发起者类加载器去加载,还加载不了,则抛出ClassNotFoundException.不再去找,而不是再委托给他的子类加载器

试题:我们能不能自己写个类叫java.lang.System?

不可以,因为我们在调用这个类的时候,当前线程的类加载器会首先把这个加载任务委托给最上层的父类,最上层的父类即是BootStrap类会在自己的加载范围(jre/lib/rt.jar)找这个类,而在这个范围正好有这个类,就将系统的System类加载进来了,而我们写的那个类,永远也加载不进来,白写了

编写对class文件进行加密的类

import java.io.*;

public class encrypt

 {

 

    /**

     *将通过参数传递进来的class文件加密后存入目标文件

     * @param args 同过参数给该程序传递一个class源文件和一个目标文件

*/

     

    public static void main(String[] args) throws Exception {

       // TODO Auto-generated method stub

       String srcPath = args[0];

       String destDir = args[1];

       FileInputStream fis = new FileInputStream(srcPath);

       String destFileName = srcPath.substring(srcPath.lastIndexOf('\\')+1);

       String destPath = destDir + "\\" + destFileName;

       FileOutputStream fos = new FileOutputStream(destPath);

       cypher(fis,fos); //调用加密算法,将class文件的输入流和目标文件的输出流传递进去

       fis.close();

       fos.close();

    }

   

    private static void cypher(InputStream ips ,OutputStream ops) throws Exception{

       int b = -1;

       while((b=ips.read())!=-1){

           ops.write(b ^ 0xff);

       }

    }

注:Eclipse默认的文件根目录是工程:\,默认的ClassPath路径是:工程:\bin\

默认的当前路径:工程:\src\ ,Eclipse中,有包名的类不能调用无包名得类

 

加密后,用到这个类时,系统默认的类加载器再去加载加密后的类,就会出错,加密后的类内部已是乱码,需用自定义的类加载器,经解密后再将它转化成内存中的字节码

 

编写自己的类加载器

编写自己的类加载器,需要继承一个类:ClassLoader

defineClass 方法用于将一个 byte 数组转换为 Class 类的实例对象

编写自己的类加载器,加载用上面代码已加密的并存于工程:\itcastlib 文件夹下ClassLoaderAttachment类(原classpath路径下的此class文件已删除)

import java.io.*;

public class MyClassLoader extends ClassLoader{

 

    private String classDir;

    public MyClassLoader(){

          

       }

      

    public MyClassLoader(String classDir){

           this.classDir = classDir;

       }

    /**

     * 加密算法,也可作为解密算法只是将01兑换

     * @param ips

     * @param ops

     * @throws Exception

     */

    private static void cypher(InputStream ips ,OutputStream ops) throwsException{

       int b = -1;

       while((b=ips.read())!=-1){

           ops.write(b ^ 0xff);

       }

    }

 

    /**

     * findClass(String name)是重写的父类ClassLoader的方法,作用是:加载name.class这个类

     * 此类会在loadClass( String name)方法<父类的>内部自动调用

     * 在这个自定义的加载器类的对象调用loadClass方法时,loadClass方法内部:

     * {

     * 首先将这个加载任务传递给最上层的父类即BootStrap,让它在它的范围去寻找这个类,如找到则返回,

     * 找不到再将这个任务依次传递给它的子类加载器ExClassLoaderAppClassLoader

     * 如都没找到这才将这个name传递给方法findClass(String name),去加载这个类

     * }

     *

     * @param  name  loadClass( String name)方法传递过来的待加载类名

     */

    @Override

    protected Class<?> findClass(String name) throws ClassNotFoundException {

       // TODO Auto-generated method stub

       String classFileName = classDir + "\\"  + name.substring(name.lastIndexOf('.')+1) + ".class";

       try {

           FileInputStream fis = new FileInputStream(classFileName);

           ByteArrayOutputStream  bos = new ByteArrayOutputStream();

           cypher(fis,bos);

           fis.close();

           System.out.println("aaa");

           byte[] bytes = bos.toByteArray();

           return defineClass(bytes, 0, bytes.length);// defineClass

于将一个 byte 数组转换为 Class 类的实例对象

       } catch (Exception e) {

           // TODO Auto-generated catch block

           e.printStackTrace();

       }

       return null;

    }

   

   

}

import java.util.Date;

public class ClassLoaderAttachment extends Date {

    public String toString(){

       return "hello,itcast";

    }

}

 

public class MyClassLoaderTest{

 public static void main(String args[])

{

    Class clazz = newMyClassLoader("itcastlib").loadClass("ClassLoaderAttachment");//

Itcastlib为路径名,ClassLoaderAttachment为类名

       Date d1 = (Date)clazz.newInstance();//编译时如果是ClassLoaderAttachment类型将通不过,这个类不在默认Classpath路径下,我们已将它加密后存在了itcastlib文件夹中,加密后的乱码类,且将原class文件删了

      System.out.println(d1); }

}

 

类加载器的一个高级问题

TomCat是一个WEB服务器,他也是一个JAVA程序,它有自己的类加载器,这些类加载器是我们上面所说的系统默认的类加载的子类,或子类的子类。当运行TomCat的时候,这些加载器类就会被BootStrap加载到虚拟机中准备去加载应用程序的类,如当运行类HttpSerlet及其子类的时候,它就会用自己的类加载器去加载

但是如果我们把HttpSerlet子类打成JAR包放到了目录jdk…:JRE/lib/ext/*.jar

则这个类在运行的时候就会用ExClassLoader类加载器加载,因为在子类中用到了父类(extends),而我们之前说过,如在类A中用到了类B,则用加载类A的加载器去加载类B,但是此时,其父类HttpSerlet并没有相应的JAR包在那个目录下,所以就会报错,解决办法:HttpSerletJAR包也放入上面的目录中

 

高新技术—3 - 雪天 - 张晴的博客

 

Ctrl+shift+/ 对选中的文字注释

代理

要为已存在的多个具有相同接口的目标类的各个方法增加一些系统功能,例如,异常处理,日志,计算方法的运行时间,事物管理等

编写一个与目标类具有相同接口的代理类,代理类的每个方法调用目标类的相同方法,并在调用方法时加上协调功能的代码

如果采用工程模式和配置文件的方式进行管理,则不需要修改客户端程序,在配置文件中配置是使用目标类还是代理类,这样以后很容易切换,譬如;想要日志功能时就配置代理类,否则配置目标类,这样,增加系统功能很容易,以后运行一段时间后,又想去掉系统功能也很容易

交叉业务的编程问题即为面向方面的编程(Aspect oriented program 简称AOP)AOP

的目标就是要使交叉业务模块化,可以采用将切面代码移动到原始方法的周围,这与直接在方法中编写切面代码的运行效果是一样的如下:

 

高新技术—3 - 雪天 - 张晴的博客

 

动态代理技术

要为系统中的各种接口的类增加代理功能,那将需要太多的代理类,全部采用静态代理方式,工作量极大,是不可取的

JVM可以在运行期动态生成出类的字节码,这种动态生成的类往往被用作代理,即动态代理

CGIB库可以动态生成一个类的子类,一个类的子类也可以用作该类的代理,所以,如果要为一个没有实现借口的类生成动态代理类,那么可以使用CGLIB库。

代理类的各个方法中通常除了要调用目标的相应方法和对外返回目标返回的结果外,还可以在代理方法中的如下几个位置上加上系统功能代码:

1、 在调用目标方法之前

2、 在调用目标方法之后

3、 在调用目标方法前后

4、 在处理目标方法异常的catch块中

程序代码如下:

import java.lang.reflect.Method;

public interface Advice {

    void beforeMethod(Method method);

    void afterMethod(Method method);

}

 

public class MyAdvice implements Advice { //MyAdvice相当于要插入的日志功能

 

    long beginTime = 0;

    public void afterMethod(Method method) {

       // TODO Auto-generated method stub

       System.out.println("从传智播客毕业上班啦!");    

       long endTime = System.currentTimeMillis();

       System.out.println(method.getName() + " running time of " + (endTime -beginTime));

 

    }

 

    public void beforeMethod(Method method) {

       // TODO Auto-generated method stub

       System.out.println("到传智播客来学习啦!");

       beginTime = System.currentTimeMillis();

    }

 

}

import java.lang.reflect.Proxy;

public class ProxyTest {

final ArrayList target = new ArrayList();    

 

    private static Object getProxy(final Object target,final Advice advice) {

       Object proxy3 = Proxy.newProxyInstance(

              target.getClass().getClassLoader(),

              target.getClass().getInterfaces(),

              new InvocationHandler(){

             

                  public Object invoke(Object proxy, Method method, Object[] args)

                         throws Throwable {

 

                     advice.beforeMethod(method);

                     Object retVal = method.invoke(target, args);

                     advice.afterMethod(method);

                     return retVal;                    

                    

                  }

              }

              );

       return proxy3;

    }

    Public static  final void main(String args [])

       Collection proxy3 = (Collection)getProxy(target,new MyAdvice());

       proxy3.add("zxx");

       proxy3.add("lhm");

       proxy3.add("bxd");

       System.out.println(proxy3.size());

       System.out.println(proxy3.getClass().getName());

 

    }

 

 

}


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值