java 泛型

内部类+类型信息+泛型

内部类:

1.根据注释填写(1),(2),(3)处的代码

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. public class Test{  
  2.     public static void main(String[] args){  
  3.            // 初始化Bean1  
  4.            (1)  
  5.            bean1.I++;  
  6.            // 初始化Bean2  
  7.            (2)  
  8.            bean2.J++;  
  9.            //初始化Bean3  
  10.            (3)  
  11.            bean3.k++;  
  12.     }  
  13.     class Bean1{  
  14.            public int I = 0;  
  15.     }  
  16.    
  17.     static class Bean2{  
  18.            public int J = 0;  
  19.     }  
  20. }  
  21.    
  22. class Bean{  
  23.     class Bean3{  
  24.            public int k = 0;  
  25.     }  
  26. }  

从前面可知,对于成员内部类,必须先产生外部类的实例化对象,才能产生内部类的实例化对象。而静态内部类不用产生外部类的实例化对象即可产生内部类的实例化对象。

  创建静态内部类对象的一般形式为:  外部类类名.内部类类名 xxx = new 外部类类名.内部类类名()

  创建成员内部类对象的一般形式为:  外部类类名.内部类类名 xxx = 外部类对象名.new 内部类类名()

因此,(1),(2),(3)处的代码分别为:


[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. // 1  
  2. Test t=new Test();  
  3. Test.Bean1 b1=t.new Bean1();  
  4.   
  5.   
  6. // 2  
  7. Test.Bean2 b2=new Test.Bean2();  
  8.   
  9.    
  10. // 3  
  11. Bean b=new Bean();  
  12. Bean.Bean3 b3=b.new Bean3();  

2. 下面这段代码的输出结果是什么?


[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. public class Test {  
  2.     public static void main(String[] args)  {  
  3.         Outter outter = new Outter();  
  4.         outter.new Inner().print();  
  5.     }  
  6. }  
  7.    
  8.    
  9. class Outter  
  10. {  
  11.     private int a = 1;  
  12.     class Inner {  
  13.         private int a = 2;  
  14.         public void print() {  
  15.             int a = 3;  
  16.             System.out.println("局部变量:" + a);  
  17.             System.out.println("内部类变量:" + this.a);  
  18.             System.out.println("外部类变量:" + Outter.this.a);  
  19.         }  
  20.     }  
  21. }  
结果:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. 3  
  2. 2  
  3. 1  

内部类不是很好理解,但说白了其实也就是一个类中还包含着另外一个类

如同一个人是由大脑、肢体、器官等身体结果组成,而内部类相当于其中的某个器官之一,例如心脏:它也有自己的属性和行为(血液、跳动)

显然,此处不能单方面用属性或者方法表示一个心脏,而需要一个类

而心脏又在人体当中,正如同是内部类在外部内当中

 

实例1:内部类的基本结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//外部类
class  Out {
     private  int  age =  12 ;
     
     //内部类
     class  In {
         public  void  print() {
             System.out.println(age);
         }
     }
}
 
public  class  Demo {
     public  static  void  main(String[] args) {
         Out.In in =  new  Out(). new  In();
         in.print();
         //或者采用下种方式访问
         /*
         Out out = new Out();
         Out.In in = out.new In();
         in.print();
         */
     }
}

运行结果:12

从上面的例子不难看出,内部类其实严重破坏了良好的代码结构,但为什么还要使用内部类呢?

因为内部类可以随意使用外部类的成员变量(包括私有)而不用生成外部类的对象,这也是内部类的唯一优点

如同心脏可以直接访问身体的血液,而不是通过医生来抽血

 

程序编译过后会产生两个.class文件,分别是Out.class和Out$In.class

其中$代表了上面程序中Out.In中的那个 .

Out.In in = new Out().new In()可以用来生成内部类的对象,这种方法存在两个小知识点需要注意

  1.开头的Out是为了标明需要生成的内部类对象在哪个外部类当中

  2.必须先有外部类的对象才能生成内部类的对象,因为内部类的作用就是为了访问外部类中的成员变量

 

实例2:内部类中的变量访问形式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class  Out {
     private  int  age =  12 ;
     
     class  In {
         private  int  age =  13 ;
         public  void  print() {
             int  age =  14 ;
             System.out.println( "局部变量:"  + age);
             System.out.println( "内部类变量:"  this .age);
             System.out.println( "外部类变量:"  + Out. this .age);
         }
     }
}
 
public  class  Demo {
     public  static  void  main(String[] args) {
         Out.In in =  new  Out(). new  In();
         in.print();
     }
}

运行结果:

局部变量:14
内部类变量:13
外部类变量:12

从实例1中可以发现,内部类在没有同名成员变量和局部变量的情况下,内部类会直接访问外部类的成员变量,而无需指定Out.this.属性名

否则,内部类中的局部变量会覆盖外部类的成员变量

而访问内部类本身的成员变量可用this.属性名,访问外部类的成员变量需要使用Out.this.属性名

 

实例3:静态内部类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class  Out {
     private  static  int  age =  12 ;
     
     static  class  In {
         public  void  print() {
             System.out.println(age);
         }
     }
}
 
public  class  Demo {
     public  static  void  main(String[] args) {
         Out.In in =  new  Out.In();
         in.print();
     }
}

运行结果:12

可以看到,如果用static 将内部内静态化,那么内部类就只能访问外部类的静态成员变量,具有局限性

其次,因为内部类被静态化,因此Out.In可以当做一个整体看,可以直接new 出内部类的对象(通过类名访问static,生不生成外部类对象都没关系)

 

实例4:私有内部类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class  Out {
     private  int  age =  12 ;
     
     private  class  In {
         public  void  print() {
             System.out.println(age);
         }
     }
     public  void  outPrint() {
         new  In().print();
     }
}
 
public  class  Demo {
     public  static  void  main(String[] args) {
         //此方法无效
         /*
         Out.In in = new Out().new In();
         in.print();
         */
         Out out =  new  Out();
         out.outPrint();
     }
}

运行结果:12

如果一个内部类只希望被外部类中的方法操作,那么可以使用private声明内部类

上面的代码中,我们必须在Out类里面生成In类的对象进行操作,而无法再使用Out.In in = new Out().new In() 生成内部类的对象

也就是说,此时的内部类只有外部类可控制

如同是,我的心脏只能由我的身体控制,其他人无法直接访问它

 

实例5:方法内部类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class  Out {
     private  int  age =  12 ;
 
     public  void  Print( final  int  x) {
         class  In {
             public  void  inPrint() {
                 System.out.println(x);
                 System.out.println(age);
             }
         }
         new  In().inPrint();
     }
}
 
public  class  Demo {
     public  static  void  main(String[] args) {
         Out out =  new  Out();
         out.Print( 3 );
     }
}

运行结果:

3
12

在上面的代码中,我们将内部类移到了外部类的方法中,然后在外部类的方法中再生成一个内部类对象去调用内部类方法

如果此时我们需要往外部类的方法中传入参数,那么外部类的方法形参必须使用final定义

至于final在这里并没有特殊含义,只是一种表示形式而已



类型:
1.使用RTTI解决多态中的问题--知道某个泛化引用的确切类型
2.Java中的类加载是动态加载的,“当程序创建第一个对类的静态成员的引用时,就会加载这个类”,“使用new操作符创建类的新对象也会被当做对类的静态成员的引用”。类加载是就会执行static初始化,即为static变量赋值和执行static程序块。另,JDBC中用到的Class.forName("XXXX")就是为了加载类,使用.class不会引发初始化。
3.static final的编译期常量无需类初始化就可以读取,但如果不是常量,则需要类先初始化。
4.使用Class类的引用可以获取某个类的信息,其中需要注意的:
      a)newInstance()方法实现了“虚拟构造器”,但类必须要有无参构造器(可以不是默认的)。
      b)泛化Class引用中,Class<?>优于平凡的Class;Class<? extends XXXX>或Class<? super XXXX>可以强制类型检查;使用.getSuperclass()方法获取的超类后使用.newInstance()的返回值只是Object类型。
      c)转型前的检查,可以使用关键字instanceof,如:if(x instanceof Dog) ...,Class的isInsance()方法有同样效果,但后者的Class类型可以使用变量,而前者只能写死。
      d)类型检查时,使用==或.equals()方法只能判断类型是否相同,但使用instanceof或isInstance()则包含了继承关系。
5.动态代理,使用静态方法Proxy.newProxyInstance()可以创建动态代理的实例,三个参数分别为:类加载器、期望代理实现的接口列表、InvocationHandler接口的一个实现。在第三个参数中,可以实现方法过滤(使用代理的好处之一吧,感觉像是代理在操控情报。。。腹黑一下),也可以实现事务。
6.反射可以违反访问权限进行操作。


首先介绍一个本文后面会频繁提到的概念:RTTI(Runtime Type Information,或者,Run-Time Type Identification),运行时类型信息。简单来说,就是指程序能够在运行时发现和使用类型信息。



       RTTI能做什么??它解放了程序在编期间执行的面向类型的操作,不管是程序的安全性还是可扩展性和可维护性,都得到了大大的加强。



      我们一般使用二种方式来实现运行时识别对象和类的信息:“传统的”RTTI和“反射”机制。


、一个大家都熟悉的例子

传统的RTTI假定我们在编译时已经知道了所有的信息。


下面我们来看一看一个很熟悉的例子:


Java代码   收藏代码
  1. <span style="font-size: small;">package cn.OOP.Typeinfo;  
  2.   
  3. import java.util.Arrays;  
  4. import java.util.List;  
  5.   
  6. abstract class Student{  
  7.     void study(){ System.out.println(this+".study()");}  
  8.     abstract public String toString();  
  9. }  
  10.   
  11. class PrimaryStudent extends Student{  
  12.     public String toString(){ return "PrimaryStudent";}  
  13. }  
  14.   
  15. class HighSchoolStudent extends Student{  
  16.     public String toString(){ return "HighSchoolStudent";}  
  17. }  
  18.   
  19. class UniversityStudent extends Student{  
  20.     public String toString(){ return "UniversityStudent";}  
  21. }  
  22.   
  23.   
  24. public class Students {  
  25.   
  26.     public static void main(String args[]){  
  27.         List<Student>  studentList = Arrays.asList(  
  28.                 new PrimaryStudent(),new HighSchoolStudent(),new UniversityStudent()  
  29.             );  
  30.         for(Student s : studentList){  
  31.             s.study();  
  32.         }  
  33.     }  
  34. }  
  35. /* output: 
  36. PrimaryStudent.study() 
  37. HighSchoolStudent.study() 
  38. UniversityStudent.study() 
  39. *///  
  40. </span>  

  基类Studeng中包含study方法,通过传递this参数给System.out.println()方法,间接使用toString()来打印类标识符。这里,toString()被声明为abstract,以此强制子类覆写该方法,并可以防止无格式的Student的实例化。


输出结果反映,子类通过覆盖toString方法,study()方法在不同的情况下会有不同的输出(多态)。


而且,在将Student对象放入List<Student>的数组时,对象被向上转型为Student类,但同时也丢失了Student对象的具体类型信息:对于程序而言,如果我们不对数组内的对象进行向下转型,那么,他们“只是”Student对象。


在上述例子中,还有一个地方用到了RTTI。容器List将它持有的对象都看做Object对象来处理。当我们从数组中取出对象时,对象被转型回Student类型。这是最基本的RTTI形式,因为在Java中,所有的类型转换都是在运行才进行正确性检查的。


      还有一点,例子中的RTTI类型转换并不彻底:Object对象被转型为Student,而不是UStudent、PStudent、HStudent。这是因为程序只知道数组中保存的是Student,在编译时Java通过容器和泛型来确保这一点,而在运行时就由转型来实现。



例子很简单,但说明的东西很多。



二、一个特殊的编程问题

在现实当中,当我们能够知道某个泛化引用的确切类型的时候,我们可以方便快捷的解决它,怎么办??


比如:我们扫描某个区域的生物(animal),并将之装入一个数组。当用户要突出的显出其中的某一类,如人类,的时候,系统是并不容易判断的。因为,对于程序而言,数组中的对象都是aniaml。使用RTTI,可以查询animal引用的确切类型,然后选择或者剔除特例。




三、Class对象

Class对象是RTTI在Java中工作机制的核心。


我们知道,Java程序是由一个一个的类组成的。而对于每一个类,都有一个class对象与之对应。也就是说,每编译一个新类都会产生一个class对象(事实上,是这个class对象时被保存在同名和.class文件当中的)。这个过程涉及到类的加载,不在本文的内容之内。

 

无论何时,只要我们想要在运行时使用类型信息,就必须获得恰当的class对象的引用。

 

 

常规来讲,class对象有三种获得方式,而且,它包含很多有用的方法。看下面的程序:

 

 

Java代码   收藏代码
  1. package cn.OOP.Typeinfo;  
  2.   
  3. interface Drinkable{}  
  4. interface Sellable{}  
  5.   
  6. class Coke{  
  7.     Coke(){}           //运行这个程序后,注释掉这个默认的无参构造器再试一试  
  8.     Coke(int i ){}  
  9. }  
  10.   
  11. class CocaCola extends Coke   
  12.     implements Drinkable,Sellable{  
  13.     public CocaCola() { super(1);}  
  14. }  
  15.   
  16.   
  17. public class TestClass {  
  18.     static void printinfo(Class c){  
  19.         System.out.println("Class Name:"+c.getName()+" is interface? ["+  
  20.                 c.isInterface()+"]");  
  21.         System.out.println("Simple Name:"+c.getSimpleName());  
  22.         System.out.println("Canonical Name:"+c.getCanonicalName());  
  23.     }  
  24.       
  25.       
  26.     public static void main(String args[]){  
  27.         Class c= null;  
  28.           
  29.         try{  
  30.             c = Class.forName("cn.OOP.Typeinfo.CocaCola");  
  31.               
  32.             //or we can init c in this way  
  33. //          CocaCola cc = new CocaCola();  
  34. //          c = cc.getClass();  
  35.               
  36.             //we can also init c in this way  
  37. //          c = CocaCola.class;  
  38.               
  39.         }catch(ClassNotFoundException e){  
  40.             System.out.println("Can't find CocaCola!!");  
  41.             System.exit(1);  
  42.         }  
  43.           
  44.         printinfo(c);  
  45.           
  46.         for(Class face : c.getInterfaces()){  
  47.             printinfo(face);  
  48.         }  
  49.     }  
  50. }/* output: 
  51. Class Name:cn.OOP.Typeinfo.CocaCola is interface? [false] 
  52. Simple Name:CocaCola 
  53. Canonical Name:cn.OOP.Typeinfo.CocaCola 
  54. Class Name:cn.OOP.Typeinfo.Drinkable is interface? [true] 
  55. Simple Name:Drinkable 
  56. Canonical Name:cn.OOP.Typeinfo.Drinkable 
  57. Class Name:cn.OOP.Typeinfo.Sellable is interface? [true] 
  58. Simple Name:Sellable 
  59. Canonical Name:cn.OOP.Typeinfo.Sellable 
  60. *///  

 

CocaCola类继承自Cola类并实现了Drinkable、Sellable接口。在mian方法中,我们用forName()方法创建了一个

Class对象的引用。需要注意的是,forName()方法传入的参数必须是全限定名(就是包含包名)

 

在printinfo()方法中,分别使用getSimpleName()和getCanonicalName()来打印出不含包名的类名和全限定的类名。isInterface()很明显,是得到这个class对象是否表示一个接口。虽然,我们这里只是演示了class对象的3种方法,但实际上,通过class对象,我们发现我们能够了解到类型的几乎所有的信息(之所以是“几乎”是因为我们有时候并不需要客户了解我们提供的类的某些信息,而选择性的屏蔽。大家可以试一试用class对象查询某些类的private属性!)

 

例子有出现了三种不同的获得class对象的方法:

Class.forName():最简单的,也是最快捷的方式,因为我们并不需要为了获得class对象而持有该类的对象实例。

obj.getClass():当我们已经拥有了一个感兴趣的类型的对象时,这个方法很好用。

Obj.class :  类字面常量,这种方式很安全,因为它在编译时就会得到检查(因此不需要放到try语句块中),而且高效。

我们可以根据我们的程序的条件和需要,选择上面三种方式中的任何一种来实现RTTI。

 

 

四、泛化的Class引用

通过上面的例子我们可以很容易的知道,class引用表示的是它所指向的对象的确切类型,并且,通过class对象我们

能获取特定类的几乎所有信息。这很容易理解。

 

但是,Java的设计者并不止步于此。通过泛型,我们可以让class引用所指向的类型更加具体。

 

package cn.OOP.Typeinfo;

Java代码   收藏代码
  1. public class GenericClassReference {  
  2.   
  3.     public static void main(String args[]){  
  4.         Class intClass = int.class;  
  5.         Class<Integer> genericIntClass = int.class;  
  6.           
  7.         genericIntClass = Integer.class;  //same thing  
  8.         intClass = double.class;  
  9. //      genericIntClass = double.class;   //Illegal,<span style="font-size: xx-small;"><span style="color: #333333; font-family: arial, sans-serif; white-space: normal; background-color: #f5f5f5;" class="hps">Can not</span><span style="color: #333333; font-family: arial, sans-serif; white-space: normal; background-color: #f5f5f5;"> </span><span style="color: #333333; font-family: arial, sans-serif; white-space: normal; background-color: #f5f5f5;" class="hps">compiled</span></span>  
  10.     }  
  11. }  

  看这个例子,普通的Class引用intClass能被随意赋值指向任意类型,但是,使用了泛型之后,编译器会强制对class引用的重新赋值进行检查。

 

但这种泛型的使用与普通的泛型又是不同的,比如下面这条语句:

Class<Number> c = int.class;

乍一看,没什么不对,Integer继承自Number类,不就是父类引用指向子类对象么。但实际上,这句代码会在编译时就出错。因为Integer的class对象引用不是Number的Class引用的子类。这看起来很诡异,但却是事实。

 

事实上,正确的做法是使用通配符?。看下面:

package cn.OOP.Typeinfo;

Java代码   收藏代码
  1. public class WildClassReference {  
  2.   
  3.     public static void main(String args[]){  
  4.         Class<?> intClass = int.class;  //? means  everything  
  5.         intClass = double.class;  
  6.           
  7.         Class<? extends Number> longClass = long.class;  
  8.         longClass = float.class;    //Compile Success  
  9.     }  
  10. }  
 

通配符?表示“任何类”,所以intClass能够重新指向double.class。同时? extends Number 表示任何Number类的子类。

 

五、反射:RTTI实现和动态编程

就上面的例子我们看到,RTTI可以告诉你所有的你想知道的类型信息,前提是这个类型是在编译时候已知的。

 

这好像没有什么不对吧??

 

将眼界放开点:假设程序获取了一个我们程序空间以外的对象的引用。即编译时并不存在的类。例如:从本地硬盘,从网络。要知道,Java的一大优势就是适于现在的WEB环境!!!

 

看下面:

package cn.OOP.Typeinfo;

Java代码   收藏代码
  1. import java.util.Scanner;  
  2.   
  3. public class Reflection {  
  4.   
  5.       
  6.     public static void main(String args[]){  
  7.         Class c = null;  
  8.         Scanner sc = new Scanner(System.in);  
  9.         System.out.println("Please put the name of the Class you want load:");  
  10.         String ClassName = sc.next();  
  11.           
  12.         try {  
  13.             c = Class.forName(ClassName);  
  14.             System.out.println("successed load the Class:"+ClassName);  
  15.         } catch (ClassNotFoundException e) {  
  16.             System.out.println("Can not find the Class ACommonClass");  
  17.             System.exit(1);  
  18.         }  
  19.           
  20.           
  21.     }  
  22. }  

当我们运行这个程序的时候,系统会阻塞在这一步:String ClassName = sc.next();

 

这时的输出是:

Please put the name of the Class you want load:

 

 

然后,由我们给定一个类名。假定,我们需要输入一个我们自定义的类:ACommonClass。注意,这是关键。这个时候,我们并没有开始写这个类,更没有编译这个类,也就没有对应的.class文件。

 

这个时候,写第二个程序(类)

package cn.OOP.Typeinfo;

Java代码   收藏代码
  1. public class ACommonClass {   
  2.     //I am just a generic Class  
  3. }  

  编译这个类,得到此类的.class 文件。然后,在程序一(注意,这个程序一直没有停止,一直在运行)输入类名:

cn.OOP.Typeinfo.ACommonClass,阻塞停止,打印输出。

 

最终结果:

Please put the name of the Class you want load:

Java代码   收藏代码
  1. cn.OOP.Typeinfo.ACommonClass  
  2. successed load the Class:cn.OOP.Typeinfo.ACommonClass  
 

在这个例子中,我们看到了一个和以前传统的编程截然不同的东西:在程序运行时,我们还能云淡风轻的写着程序必须的类,而且,这个类还能用于这个已经开始的程序!!!!神奇吧。

 

当然,这只是一个最简单的RTTI反射应用,通常的Java动态编程会更复杂,也更神奇!!

 

Class类与java.lang.reflect类库一起对反射机制提供了支持。当通过反射与一个未知类型的对象打交道时,JVM只是简单的检查这个对象,看他属于哪个特定的类(就像传统的RTTI一样)。当我们用反射机制做某些事情时,我们还是必须知道特定的类(也就是必须得到.class文件),要么在本地,要么从网络获取。所不同的是,由于设计体系的特殊,我们逃避了在编译期间的检查,知道运行时猜打开和检查.class文件。


泛型:

http://qiemengdao.iteye.com/blog/1525624


1. 泛型的主要目的之一,就是用来指定容器持有什么类型的对象,而且由编译器来保证其类型的正确性。由此,与其用Object指定为任何类型,不如暂时不指定类型,而到定义时决定类型,因为前者不会有类型检查。

2. 擦除。List<String>型的对象与List<Integer>型的对象,在运行时,其类型都被擦除为List。因此,在泛型机制下,无法获得有关泛型参数的运行时信息,像List<Integer>成为List,普通类型T则成为Object类型。这相比c++的模板机制,有很大的不足之处,弥补方法之一是使用泛型边界如<T extends ClassType>。

3. Java泛型没有其它语言的泛型那么有用,原因之一就是使用了擦除。擦除的核心动机是使得泛型化与非泛型化的代码之间能互相调用,是为了兼容。Java的设计者认为这是唯一可靠行的解决方案。如果Jdk1.0就引入了泛型,则不会有这个问题。

4. 擦除丢失了在泛型代码中执行某些操作的能力。任何在运行时需要知道确切类型信息的操作都将无法完成。而编译器会负责泛型代码之外的类型检查(即边界处),以确保插入容器的对象是正确的类型。

5. 边界可以有多个,如<T extends A&B&C>其中只能有一个是类,其它为接口。且第一个为类,后面的为接口。

6. Java的数组不允许放入子类型的对象,而泛型可以。

7. 泛型的一些限制:(1)不能用基本类型作为泛型参数;(2)一个类不能实现一个泛型接口的两种变体,由于擦除泛型无法识别;(3)无法在泛型代码内重载相同个数泛型参数的函数,因为各参数在擦除后没有区别。

8. 因为可向JDK1.5以前的代码传递泛型容器,所以旧式代码仍可能破坏你的容器。JDK1.5的java.util.Collections中有一组有用的工具,它们是静态方法checkedCollection,checkedList,checkMap等等,可以解决此类问题。使旧式代码中插入对象到容器时,也接受类型检查。

9. 使用数组的优势在于效率,如果需要变长度,或者需要一些除存取之外的特别操作,显然应该用容器。

10. Java中返回一个数组比c++中方便,c++中只能返回一个指针。

11. System.arrayCopy比用for循环要快得多。

12. 返回零长度的数组,而不是null是个好习惯。客户省去对null的判断。



1.泛型边界:

Java泛型编程时,编译器忽略泛型参数的具体类型,认为使用泛型的类、方法对Object都适用,这在泛型编程中称为类型信息檫除。

例如:

[java]  view plain  copy
  1. class GenericType{  
  2.     public static void main(String[] args){  
  3.         System.out.println(new ArrayList<String>().getClass());  
  4.         System.out.println(new ArrayList<Integer>().getClass());  
  5. }  
  6. }  

输出结果为:

java.util.ArrayList

java.util.ArrayList

泛型忽略了集合容器中具体的类型,这就是类型檫除。

但是如果某些泛型的类/方法只想针对某种特定类型获取相关子类应用,这时就必须使用泛型边界来为泛型参数指定限制条件。

例如:

[java]  view plain  copy
  1. interface HasColor{  
  2.     java.awt.Color getColor();  
  3. }  
  4. class Colored<T extends HasColor>{  
  5.     T item;  
  6.     Colored(T item){  
  7.         this.item = item;  
  8. }  
  9. java.awt.Color color(){  
  10.     //调用HasColor接口实现类的getColor()方法  
  11.     return item.getColor();  
  12. }  
  13. }  
  14. class Dimension{  
  15.     public int x, y, z;  
  16. }  
  17. Class ColoredDimension<T extends Dimension & HasColor>{  
  18.     T item;  
  19.     ColoredDimension(T item){  
  20.         this.item = item;  
  21. }  
  22. T getItem(){  
  23.     return item;  
  24. }  
  25. java.awt.Color color(){  
  26.     //调用HasColor实现类中的getColor()方法  
  27.     return item.getColor();  
  28. }  
  29. //获取Dimension类中定义的x,y,z成员变量  
  30. int getX(){  
  31.     return item.x;  
  32. }  
  33. int getY(){  
  34.     return item.y;  
  35. }  
  36. int getZ(){  
  37.     return item.z;  
  38. }  
  39. }  
  40. interface Weight{  
  41.     int weight();  
  42. }  
  43. class Solid<T extends Dimension & HasColor & Weight>{  
  44.     T item;  
  45.     Solide(T item){  
  46.         this.item = item;  
  47. }  
  48. T getItem(){  
  49.     return item;  
  50. }  
  51. java.awt.Color color(){  
  52.     //调用HasColor实现类中的getColor()方法  
  53.     return item.getColor();  
  54. }  
  55. //获取Dimension类中定义的x,y,z成员变量  
  56. int getX(){  
  57.     return item.x;  
  58. }  
  59. int getY(){  
  60.     return item.y;  
  61. }  
  62. int getZ(){  
  63.     return item.z;  
  64. }  
  65. int weight(){  
  66.     //调用Weight接口实现类的weight()方法  
  67.     return item.weight();  
  68. }  
  69. }  
  70. class Bounded extends Dimension implements HasColor, Weight{  
  71.     public java.awt.Color getColor{  
  72.         return null;  
  73. }  
  74. public int weight(){  
  75.     return 0;  
  76. }  
  77. }  
  78. public class BasicBounds{  
  79.     public static void main(String[] args){  
  80.         Solid<Bounded> solid = new Solid<Bounded>(new Bounded());  
  81.         solid.color();  
  82.         solid.getX();  
  83.         solid.getY();  
  84.         solid.getZ();  
  85.         solid.weight();  
  86. }  
  87. }  

Java泛型编程中使用extends关键字指定泛型参数类型的上边界(后面还会讲到使用super关键字指定泛型的下边界),即泛型只能适用于extends关键字后面类或接口的子类。

Java泛型编程的边界可以是多个,使用如<T extends A & B & C>语法来声明,其中只能有一个是类,并且只能是extends后面的第一个为类,其他的均只能为接口(和类/接口中的extends意义不同)。

使用了泛型边界之后,泛型对象就可以使用边界对象中公共的成员变量和方法。

2.泛型通配符:

泛型初始化过程中,一旦给定了参数类型之后,参数类型就会被限制,无法随着复制的类型而动态改变,如:

[java]  view plain  copy
  1. class Fruit{  
  2. }  
  3. class Apple extends Fruit{  
  4. }  
  5. class Jonathan extends Apple{  
  6. }  
  7. class Orange extends Fruit{  
  8. }  
  9. 如果使用数组:  
  10. public class ConvariantArrays{  
  11.     Fruit fruit = new Apple[10];  
  12.     Fruit[0] = new Apple();  
  13.     Fruit[1] = new Jonathan();  
  14.     try{  
  15.         fruit[0] = new Fruit();  
  16. }catch(Exception e){  
  17.     System.out.println(e);  
  18. }  
  19. try{  
  20.         fruit[0] = new Orange();  
  21. }catch(Exception e){  
  22.     System.out.println(e);  
  23. }  
  24. }  

编译时没有任何错误,运行时会报如下异常:

java.lang.ArrayStoreException:Fruit

java.lang.ArrayStoreException:Orange

为了使得泛型在编译时就可以进行参数类型检查,我们推荐使用java的集合容器类,如下:

[java]  view plain  copy
  1. public class NonConvariantGenerics{  
  2.     List<Fruit> flist = new ArrayList<Apple>();  
  3. }  

很不幸的是,这段代码会报编译错误:incompatible types,不兼容的参数类型,集合认为虽然Apple继承自Fruit,但是List的Fruit和List的Apple是不相同的,因为泛型参数在声明时给定之后就被限制了,无法随着具体的初始化实例而动态改变,为解决这个问题,泛型引入了通配符”?”。

对于这个问题的解决,使用通配符如下:

[java]  view plain  copy
  1. public class NonConvariantGenerics{  
  2.     List<? extends Fruit> flist = new ArrayList<Apple>();  
  3. }  

泛型通配符”?”的意思是任何特定继承Fruit的类,java编译器在编译时会根据具体的类型实例化。

另外,一个比较经典泛型通配符的例子如下:

public class SampleClass < T extendsS> {…}

假如A,B,C,…Z这26个class都实现了S接口。我们使用时需要使用到这26个class类型的泛型参数。那实例化的时候怎么办呢?依次写下

SampleClass<A> a = new SampleClass();

SampleClass<B> a = new SampleClass();

SampleClass<Z> a = new SampleClass();

这显然很冗余,还不如使用Object而不使用泛型,使用通配符非常方便:

SampleClass<? Extends S> sc = newSampleClass();

3.泛型下边界:

在1中大概了解了泛型上边界,使用extends关键字指定泛型实例化参数只能是指定类的子类,在泛型中还可以指定参数的下边界,是一super关键字可以指定泛型实例化时的参数只能是指定类的父类。

例如:

[java]  view plain  copy
  1. class Fruit{  
  2. }  
  3. class Apple extends Fruit{  
  4. }  
  5. class Jonathan extends Apple{  
  6. }  
  7. class Orange extends Fruit{  
  8. }  
  9. public superTypeWildcards{  
  10.     public static void writeTo(List<? super Apple> apples){  
  11.         apples.add(new Apple());  
  12.         apples.add(new Jonathan());  
  13. }  
  14. }  

通过? Super限制了List元素只能是Apple的父类。

泛型下边界还可以使用<?super T>,但是注意不能使用<Tsuper A>,即super之前的只能是泛型通配符,如:

[java]  view plain  copy
  1. public class GenericWriting{  
  2.     static List<Apple> apples = new ArrayList<Apple>();  
  3.     static List<Fruit> fruits = new ArrayList<Fruit>();  
  4.     static <T> void writeExact(List<T> list, T item){  
  5.         list.add(item);  
  6. }  
  7. static <T> void writeWithWildcards(List<? super T> list, T item){  
  8.     list.add(item);  
  9. }  
  10. static void f1(){  
  11.     writeExact(apples, new Apple());  
  12. }  
  13. static void f2(){  
  14. writeWithWildcards(apples, new Apple());  
  15.     writeWithWildcards(fruits, new Apple());  
  16. }  
  17. public static void main(String[] args){  
  18.     f1();  
  19.     f2();  
  20. }  
  21. }  

4.无边界的通配符:

泛型的通配符也可以不指定边界,没有边界的通配符意思是不确定参数的类型,编译时泛型檫除类型信息,认为是Object类型。如:

[java]  view plain  copy
  1. public class UnboundedWildcard{  
  2.     static List list1;  
  3.     static List<?> list2;  
  4.     static List<? extends Object> list3;  
  5.     static void assign1(List list){  
  6.         list1 = list;  
  7.         list2 = list;  
  8.         //list3 = list; //有未检查转换警告  
  9. }   
  10. static void assign2(List<?> list){  
  11.         list1 = list;  
  12.         list2 = list;  
  13.     list3 = list;  
  14. }  
  15. static void assign3(List<? extends Object> list){  
  16.         list1 = list;  
  17.         list2 = list;  
  18.     list3 = list;  
  19. }  
  20. public static void main(String[] args){  
  21.     assign1(new ArrayList());  
  22. assign2(new ArrayList());  
  23. //assign3(new ArrayList()); //有未检查转换警告  
  24. assign1(new ArrayList<String>());  
  25. assign2(new ArrayList<String>());  
  26. assign3(new ArrayList<String>());   
  27. List<?> wildList = new ArrayList();  
  28. assign1(wildList);  
  29. assign2(wildList);  
  30. assign3(wildList);   
  31. }  
  32. }  

List和List<?>的区别是:List是一个原始类型的List,它可以存放任何Object类型的对象,不需要编译时类型检查。List<?>等价于List<Object>,它不是一个原始类型的List,它存放一些特定类型,只是暂时还不确定是什么类型,需要编译时类型检查。因此List的效率要比List<?>高。

5.实现泛型接口注意事项:

由于泛型在编译过程中檫除了参数类型信息,所以一个类不能实现以泛型参数区别的多个接口,如:

[java]  view plain  copy
  1. interface Payable<T>{  
  2. }  
  3. class Employee implements Payable<Employee>{  
  4. }  
  5. class Hourly extends Employee implements Payable<Hourly>{  
  6. }  

类Hourly无法编译,因为由于泛型类型檫除,Payable<Employee>和Payable<Hourly>在编译时是同一个类型Payable,因此无法同时实现一个接口两次。

6.泛型方法重载注意事项:

由于泛型在编译时将参数类型檫除,因此以参数类型来进行方法重载在泛型中要特别注意,如:

[java]  view plain  copy
  1. public class GenericMethod<W,T>{  
  2.     void f(List<T> v) {  
  3. }  
  4. void f(List<W> v){  
  5. }  
  6. }  

无法通过编译,因为泛型檫除类型信息,上面两个方法的参数都被看作为Object类型,使用参数类型已经无法区别上面两个方法,因此无法重载。

7.泛型中的自绑定:

通常情况下,一个类无法直接继承一个泛型参数,但是你可以通过继承一个声明泛型参数的类,这就是java泛型编程中的自绑定,如:

[java]  view plain  copy
  1. class SelfBounded<T extends SelfBounded<T>>{  
  2.     T element;  
  3.     SelfBounded<T> set(T arg){  
  4.         Element = arg;  
  5.         return this;  
  6. }   
  7. T get(){  
  8.     return element;  
  9. }  
  10. }  
  11. class A extends SelfBounded<A>{  
  12. }  
  13. class B extends SelfBounded<A>{  
  14. }  
  15. class C extends SelfBounded<C>{  
  16.     C setAndGet(C arg){  
  17.         set(arg);  
  18.         return get();  
  19. }  
  20. }  
  21. public class SelfBounding{  
  22.     public static void main(String[] args){  
  23.         A a = new A();  
  24.         a.set(new A());  
  25.         a = a.set(new A()).get();  
  26.         a = a.get();  
  27.         C c = new C();  
  28.         C = c.setAndGet(new C());  
  29. }  
  30. }  

泛型的自绑定约束目的是用于强制继承关系,即使用泛型参数的类的基类是相同的,强制所有人使用相同的方式使用参数基类。


Java总结篇系列:Java泛型

一. 泛型概念的提出(为什么需要泛型)?

首先,我们看下下面这段简短的代码:

复制代码
 1 public class GenericTest {
 2 
 3     public static void main(String[] args) {
 4         List list = new ArrayList();
 5         list.add("qqyumidi");
 6         list.add("corn");
 7         list.add(100);
 8 
 9         for (int i = 0; i < list.size(); i++) {
10             String name = (String) list.get(i); // 1
11             System.out.println("name:" + name);
12         }
13     }
14 }
复制代码

定义了一个List类型的集合,先向其中加入了两个字符串类型的值,随后加入一个Integer类型的值。这是完全允许的,因为此时list默认的类型为Object类型。在之后的循环中,由于忘记了之前在list中也加入了Integer类型的值或其他编码原因,很容易出现类似于//1中的错误。因为编译阶段正常,而运行时会出现“java.lang.ClassCastException”异常。因此,导致此类错误编码过程中不易发现。

 在如上的编码过程中,我们发现主要存在两个问题:

1.当我们将一个对象放入集合中,集合不会记住此对象的类型,当再次从集合中取出此对象时,改对象的编译类型变成了Object类型,但其运行时类型任然为其本身类型。

2.因此,//1处取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现“java.lang.ClassCastException”异常。

那么有没有什么办法可以使集合能够记住集合内元素各类型,且能够达到只要编译时不出现问题,运行时就不会出现“java.lang.ClassCastException”异常呢?答案就是使用泛型。

 

二.什么是泛型?

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

 看着好像有点复杂,首先我们看下上面那个例子采用泛型的写法。

复制代码
 1 public class GenericTest {
 2 
 3     public static void main(String[] args) {
 4         /*
 5         List list = new ArrayList();
 6         list.add("qqyumidi");
 7         list.add("corn");
 8         list.add(100);
 9         */
10 
11         List<String> list = new ArrayList<String>();
12         list.add("qqyumidi");
13         list.add("corn");
14         //list.add(100);   // 1  提示编译错误
15 
16         for (int i = 0; i < list.size(); i++) {
17             String name = list.get(i); // 2
18             System.out.println("name:" + name);
19         }
20     }
21 }
复制代码

采用泛型写法后,在//1处想加入一个Integer类型的对象时会出现编译错误,通过List<String>,直接限定了list集合中只能含有String类型的元素,从而在//2处无须进行强制类型转换,因为此时,集合能够记住元素的类型信息,编译器已经能够确认它是String类型了。

结合上面的泛型定义,我们知道在List<String>中,String是类型实参,也就是说,相应的List接口中肯定含有类型形参。且get()方法的返回结果也直接是此形参类型(也就是对应的传入的类型实参)。下面就来看看List接口的的具体定义:

复制代码
 1 public interface List<E> extends Collection<E> {
 2 
 3     int size();
 4 
 5     boolean isEmpty();
 6 
 7     boolean contains(Object o);
 8 
 9     Iterator<E> iterator();
10 
11     Object[] toArray();
12 
13     <T> T[] toArray(T[] a);
14 
15     boolean add(E e);
16 
17     boolean remove(Object o);
18 
19     boolean containsAll(Collection<?> c);
20 
21     boolean addAll(Collection<? extends E> c);
22 
23     boolean addAll(int index, Collection<? extends E> c);
24 
25     boolean removeAll(Collection<?> c);
26 
27     boolean retainAll(Collection<?> c);
28 
29     void clear();
30 
31     boolean equals(Object o);
32 
33     int hashCode();
34 
35     E get(int index);
36 
37     E set(int index, E element);
38 
39     void add(int index, E element);
40 
41     E remove(int index);
42 
43     int indexOf(Object o);
44 
45     int lastIndexOf(Object o);
46 
47     ListIterator<E> listIterator();
48 
49     ListIterator<E> listIterator(int index);
50 
51     List<E> subList(int fromIndex, int toIndex);
52 }
复制代码

我们可以看到,在List接口中采用泛型化定义之后,<E>中的E表示类型形参,可以接收具体的类型实参,并且此接口定义中,凡是出现E的地方均表示相同的接受自外部的类型实参。

自然的,ArrayList作为List接口的实现类,其定义形式是:

复制代码
 1 public class ArrayList<E> extends AbstractList<E> 
 2         implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
 3     
 4     public boolean add(E e) {
 5         ensureCapacityInternal(size + 1);  // Increments modCount!!
 6         elementData[size++] = e;
 7         return true;
 8     }
 9     
10     public E get(int index) {
11         rangeCheck(index);
12         checkForComodification();
13         return ArrayList.this.elementData(offset + index);
14     }
15     
16     //...省略掉其他具体的定义过程
17 
18 }
复制代码

由此,我们从源代码角度明白了为什么//1处加入Integer类型对象编译错误,且//2处get()到的类型直接就是String类型了。

 

三.自定义泛型接口、泛型类和泛型方法

从上面的内容中,大家已经明白了泛型的具体运作过程。也知道了接口、类和方法也都可以使用泛型去定义,以及相应的使用。是的,在具体使用时,可以分为泛型接口、泛型类和泛型方法。

自定义泛型接口、泛型类和泛型方法与上述Java源码中的List、ArrayList类似。如下,我们看一个最简单的泛型类和方法定义:

复制代码
 1 public class GenericTest {
 2 
 3     public static void main(String[] args) {
 4 
 5         Box<String> name = new Box<String>("corn");
 6         System.out.println("name:" + name.getData());
 7     }
 8 
 9 }
10 
11 class Box<T> {
12 
13     private T data;
14 
15     public Box() {
16 
17     }
18 
19     public Box(T data) {
20         this.data = data;
21     }
22 
23     public T getData() {
24         return data;
25     }
26 
27 } 
复制代码

在泛型接口、泛型类和泛型方法的定义过程中,我们常见的如T、E、K、V等形式的参数常用于表示泛型形参,由于接收来自外部使用时候传入的类型实参。那么对于不同传入的类型实参,生成的相应对象实例的类型是不是一样的呢?

复制代码
 1 public class GenericTest {
 2 
 3     public static void main(String[] args) {
 4 
 5         Box<String> name = new Box<String>("corn");
 6         Box<Integer> age = new Box<Integer>(712);
 7 
 8         System.out.println("name class:" + name.getClass());      // com.qqyumidi.Box
 9         System.out.println("age class:" + age.getClass());        // com.qqyumidi.Box
10         System.out.println(name.getClass() == age.getClass());    // true
11 
12     }
13 
14 }
复制代码

由此,我们发现,在使用泛型类时,虽然传入了不同的泛型实参,但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存上只有一个,即还是原来的最基本的类型(本实例中为Box),当然,在逻辑上我们可以理解成多个不同的泛型类型。

究其原因,在于Java中的泛型这一概念提出的目的,导致其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。

对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。

 

四.类型通配符

接着上面的结论,我们知道,Box<Number>和Box<Integer>实际上都是Box类型,现在需要继续探讨一个问题,那么在逻辑上,类似于Box<Number>和Box<Integer>是否可以看成具有父子关系的泛型类型呢?

为了弄清这个问题,我们继续看下下面这个例子:

复制代码
 1 public class GenericTest {
 2 
 3     public static void main(String[] args) {
 4 
 5         Box<Number> name = new Box<Number>(99);
 6         Box<Integer> age = new Box<Integer>(712);
 7 
 8         getData(name);
 9         
10         //The method getData(Box<Number>) in the type GenericTest is 
11         //not applicable for the arguments (Box<Integer>)
12         getData(age);   // 1
13 
14     }
15     
16     public static void getData(Box<Number> data){
17         System.out.println("data :" + data.getData());
18     }
19 
20 }
复制代码

我们发现,在代码//1处出现了错误提示信息:The method getData(Box<Number>) in the t ype GenericTest is not applicable for the arguments (Box<Integer>)。显然,通过提示信息,我们知道Box<Number>在逻辑上不能视为Box<Integer>的父类。那么,原因何在呢?

复制代码
 1 public class GenericTest {
 2 
 3     public static void main(String[] args) {
 4 
 5         Box<Integer> a = new Box<Integer>(712);
 6         Box<Number> b = a;  // 1
 7         Box<Float> f = new Box<Float>(3.14f);
 8         b.setData(f);        // 2
 9 
10     }
11 
12     public static void getData(Box<Number> data) {
13         System.out.println("data :" + data.getData());
14     }
15 
16 }
17 
18 class Box<T> {
19 
20     private T data;
21 
22     public Box() {
23 
24     }
25 
26     public Box(T data) {
27         setData(data);
28     }
29 
30     public T getData() {
31         return data;
32     }
33 
34     public void setData(T data) {
35         this.data = data;
36     }
37 
38 }
复制代码

这个例子中,显然//1和//2处肯定会出现错误提示的。在此我们可以使用反证法来进行说明。

假设Box<Number>在逻辑上可以视为Box<Integer>的父类,那么//1和//2处将不会有错误提示了,那么问题就出来了,通过getData()方法取出数据时到底是什么类型呢?Integer? Float? 还是Number?且由于在编程过程中的顺序不可控性,导致在必要的时候必须要进行类型判断,且进行强制类型转换。显然,这与泛型的理念矛盾,因此,在逻辑上Box<Number>不能视为Box<Integer>的父类。

好,那我们回过头来继续看“类型通配符”中的第一个例子,我们知道其具体的错误提示的深层次原因了。那么如何解决呢?总部能再定义一个新的函数吧。这和Java中的多态理念显然是违背的,因此,我们需要一个在逻辑上可以用来表示同时是Box<Integer>和Box<Number>的父类的一个引用类型,由此,类型通配符应运而生。

类型通配符一般是使用 ? 代替具体的类型实参。注意了,此处是类型实参,而不是类型形参!且Box<?>在逻辑上是Box<Integer>、Box<Number>...等所有Box<具体类型实参>的父类。由此,我们依然可以定义泛型方法,来完成此类需求。

复制代码
 1 public class GenericTest {
 2 
 3     public static void main(String[] args) {
 4 
 5         Box<String> name = new Box<String>("corn");
 6         Box<Integer> age = new Box<Integer>(712);
 7         Box<Number> number = new Box<Number>(314);
 8 
 9         getData(name);
10         getData(age);
11         getData(number);
12     }
13 
14     public static void getData(Box<?> data) {
15         System.out.println("data :" + data.getData());
16     }
17 
18 }
复制代码

有时候,我们还可能听到类型通配符上限和类型通配符下限。具体有是怎么样的呢?

在上面的例子中,如果需要定义一个功能类似于getData()的方法,但对类型实参又有进一步的限制:只能是Number类及其子类。此时,需要用到类型通配符上限。

复制代码
 1 public class GenericTest {
 2 
 3     public static void main(String[] args) {
 4 
 5         Box<String> name = new Box<String>("corn");
 6         Box<Integer> age = new Box<Integer>(712);
 7         Box<Number> number = new Box<Number>(314);
 8 
 9         getData(name);
10         getData(age);
11         getData(number);
12         
13         //getUpperNumberData(name); // 1
14         getUpperNumberData(age);    // 2
15         getUpperNumberData(number); // 3
16     }
17 
18     public static void getData(Box<?> data) {
19         System.out.println("data :" + data.getData());
20     }
21     
22     public static void getUpperNumberData(Box<? extends Number> data){
23         System.out.println("data :" + data.getData());
24     }
25 
26 }
复制代码

此时,显然,在代码//1处调用将出现错误提示,而//2 //3处调用正常。

类型通配符上限通过形如Box<? extends Number>形式定义,相对应的,类型通配符下限为Box<? super Number>形式,其含义与类型通配符上限正好相反,在此不作过多阐述了。

 

五.话外篇

本文中的例子主要是为了阐述泛型中的一些思想而简单举出的,并不一定有着实际的可用性。另外,一提到泛型,相信大家用到最多的就是在集合中,其实,在实际的编程过程中,自己可以使用泛型去简化开发,且能很好的保证代码质量。并且还要注意的一点是,Java中没有所谓的泛型数组一说。

对于泛型,最主要的还是需要理解其背后的思想和目的。

 

--------------------------------------------------------------------------------- 
笔者水平有限,若有错漏,欢迎指正,如果转载以及CV操作,请务必注明出处,谢谢!
分类:  Java
68
0
(请您对文章做出评价)
« 上一篇: Java总结篇系列:Java多线程(三)
» 下一篇: Android开发中的问题及相应解决(持续更新)
posted @  2014-07-12 23:39  Windstep 阅读( 115342) 评论( 45编辑  收藏


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值