Java基础系列二:Java泛型

 该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每一个Java知识点背后的实现原理,更完整地了解整个Java技术体系,造成本身的知识框架。java

 

1、泛型概述

一、定义:

所谓泛型,就是容许在定义类、接口、方法时使用类型形参,这个类型形参(或叫泛型)将在声明变量、建立对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。Java5改写了集合框架中的所有接口和类,为这些接口、类增长了泛型支持,从而能够在声明集合变量、建立集合对象时传入类型实参。面试

 

二、泛型初体验:一个被举了无数次的栗子

1
2
3
4
5
6
7
8
List arrayList =  new  ArrayList();
arrayList.add( "aaaa" );
arrayList.add( 100 );
 
for ( int  i =  0 ; i< arrayList.size();i++){
     String item = (String)arrayList.get(i);
     Log.d( "泛型测试" , "item = "  + item);
}

  

运行上述代码,咱们能够在控制台看到这样的错误信息:编程

1
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

  

ArrayList能够存听任意类型,例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,所以程序崩溃了。为了解决相似这样的问题(在编译阶段就能够解决),泛型应运而生。数组

 

三、泛型的特性:

先思考以下一段代码:缓存

1
2
3
List<String> sList= new  ArrayList<String>();
List<Integer> iList= new  ArrayList<Integer>();
System.out.println(sList.getClass()==iList.getClass());  

 

先不要看结果,本身思考一下。安全

 

结果:app

1
true

  

咱们看到输出的结果为true,经过上面的例子能够证实,在编译以后程序会采起去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程当中,正确检验泛型结果后,会将泛型的相关信息擦出,而且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。(泛型的这一特性在下述文字中会有详解介绍)框架

小结:泛型类型在逻辑上看以当作是多个不一样的类型,实际上都是相同的基本类型。

 

 

2、泛型的使用

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法dom

一、泛型类:

泛型类型用于类的定义中,被称为泛型类。经过泛型能够完成对一组类的操做对外开放相同的接口。最典型的就是各类容器类,如:List、Set、Map。ide

 

泛型的基本写法:

1
2
3
4
5
6
class  类名称 <泛型标识:能够随便写任意标识号,标识指定的泛型的类型>{
   private  泛型标识  /*(成员变量类型)*/  var;
   .....
 
   }
}

  

看不懂?看不懂就对了,下面咱们来看一个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public  class  Apple<T> {
     
     //使用T类型定义变量
     private  T info;
     public  Apple() {}
     
     //下面方法使用T类型定义构造器
     public  Apple(T info){
         this .info=info;
     }
 
     public  T getInfo() {
         return  info;
     }
 
     public  void  setInfo(T info) {
         this .info = info;
     }
     
     public  static  void  main(String[] args) {
         //因为传给T形参的是String,因此构造器参数只能是String
         Apple<String> apple= new  Apple<String>( "苹果" );
         System.out.println(apple.getInfo());
         
         //因为传给T形参的是Double,因此构造器参数只能是Double
         Apple<Double> apple2= new  Apple<Double>( 5.56 );
         System.out.println(apple2.getInfo());
         
     }
     
 
}

  

这里的T能够写成任意符合,经常使用的有以下几个符合:

  • ?:表示不肯定的 java 类型
  • T (type): 表示具体的一个java类型
  • K V (key value): 分别表明java键值中的Key Value
  • E (element) :表明Element

 

二、泛型接口:

泛型接口与泛型类的定义及使用基本相同。下面是Java5改写后的List接口,Map接口的代码片断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public  interface  List<E>{
     //在该接口中,E能够做为类型使用
     //下面方法可使用E做为参数类型
     void  add(E x);
     Iterator<E> iterator();
}
 
 
//定义该接口时指定了两个泛型形参,其参数名为K,V
public  interface  Map<K,V>{
     //在该接口中K,V彻底能够做为类型使用
     Set<K> keySet()
     V put(K key,V value);
}

  

下面咱们来看一个泛型案例:

定义一个泛型接口:

1
2
3
4
//定义一个泛型接口
public  interface  Generator<T> {
     public  T next();
}

  

如今有一个类实现了这个泛型接口,类的代码以下:

1
2
3
4
5
6
class  FruitGenerator<T>  implements  Generator<T>{
     @Override
     public  T next() {
         return  null ;
     }
}

  

咱们看到了这个类中也使用了泛型,并未传入实际的参数

未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一块儿加到类中,即:class FruitGenerator<T> implements Generator<T>

若是不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"

 

1
2
3
4
5
6
7
8
9
10
public  class  FruitGenerator  implements  Generator<String> {
 
     private  String[] fruits =  new  String[]{ "Apple" "Banana" "Pear" };
 
     @Override
     public  String next() {
         Random rand =  new  Random();
         return  fruits[rand.nextInt( 3 )];
     }
}

  

这段代码也是实现了Generator接口,不一样的是传入了实际的类型String

传入泛型实参时:定义一个生产器实现这个接口,虽然咱们只建立了一个泛型接口Generator<T>可是咱们能够为T传入无数个实参,造成无数种类型的Generator接口。在实现类实现泛型接口时,如已将泛型类型传入实参类型,则全部使用泛型的地方都要替换成传入的实参类型即:Generator<T>,public T next();中的的T都要替换成传入的String类型。

 

三、泛型通配符:

为何要使用泛型通配符:

正如前面讲的,当使用一个泛型类时(包括声明变量和建立对象两种状况),都应该为这个泛型类传入一个类型实参。若是没有传入类型实际参数,编译器就会提出泛型警告。假设如今须要定义一个方法,该方法里有一个集合形参,集合形参的元素类型是不肯定的,那应该怎样定义呢?

考虑以下代码:

1
2
3
4
5
public  void  test(List c) {
     for ( int  i= 0 ;i<c.size;i++) {
         System.out.println(c.get(i));
     }
}

  

上面程序固然没有问题:这是一段最普通的遍历List集合的代码。问题是上面程序中List是一个有泛型声明的接口,此处使用List 接口时没有传入实际类型参数,这将引发泛型警告。为此,考虑为List 接口传入实际的类型参数——由于List集合里的元素类型是不肯定的

 

泛型通配符的使用:

为了表示各类泛型List的父类,可使用类型通配符,类型通配符是一个问号(?),将一个问号做为类型实参传给List集合,写做:List<?>(意思是元素类型未知的List)。这个问号(?)被称为通配符,它的元素类型能够匹配任何类型。能够将上面方法改写为以下形式:

1
2
3
4
5
public  void  test(List<?> c) {
     for ( int  i= 0 ;i<c.size;i++) {
         System.out.println(c.get(i));
     }
}

  

这样就不会出现警告了,但这种带通配符的List仅表示它是各类泛型List的父类,并不能将其余元素加入到其中,例如将String放入其中

List<?> c=new ArrayList<String>();//这段代码将报错

由于程序没法肯定c集合中元素的类型,因此不能向其中添加对象。根据前面的List<E>接口定义的代码能够发现:add0方法有类型参数E做为集合的元素类型,因此传给add的参数必须是E类的对象或者其子类的对象。但由于在该例中不知道E是什么类型,因此程序没法将任何对象“丢进”该集合。惟一的例外是null,它是全部引用类型的实例。

 

设置类型通配符的上限:

如今想实现一个简单的绘图程序,代码以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public  abstract  class  Shape{
     public  abstract  void  draw(Canvas c);
}
 
//定义Shape的子类Circle
public  class  Circle  extends  Shape{
     //实现画图方法,以打印字符串来模拟画图方法实现
     public  void  draw(Canvas c)
     {
         System.out.println( "在画布" +c+ "上画一个圆" );
     }
}
 
//定义Shape的子类Rectangle
public  class  Rectangle  extends  Shape{
     //实现画图方法,以打印字符串来模拟画图方法实现
     public  void  draw(Canvas c)
     {
         System.out.print1n( "把一个矩形画在画布" +c+ "上" );
     }
}

  

上面定义了三个形状类,其中Shape是一个抽象父类,该抽象父类有两个子类:Circle和Rectangle。接下来定义一个Canvas类,该画布类能够画数量不等的形状(Shape子类的对象),那应该如何定义这个Canvas类呢?考虑以下的Canvas实现类。

1
2
3
4
5
//同时在画布上绘制多个形状
public  void  drawAll(List<Shape>shapes) {
     for (Shape s:shapes)
         s.draw( this );
}

  

注意上面的drawAll()方法的形参类型是List<Shape>,而List<Circle>并非List<Shape>的子类型,所以,下面代码将引发编译错误。

1
2
3
4
List<Circle> circleList= new  ArrayList<Circle>();
Canvas c= new  Canvas();
//不能把List<Circle>当成List<Shape>使用,因此下面代码引发编译错误
c.drawAll(circleList); 

  

这时咱们能够经过通配符的上限来解决这个问题:

1
List<?  extends  Shape>

  

List<? extends Shape>是受限制通配符的例子,此处的问号(?)表明一个未知的类型,就像前面看到的通配符同样。可是此处的这个未知类型必定是Shape的子类型(也能够是Shape自己),所以能够把Shape称为这个通配符的上限(upper bound)。

 

设置类型通配符的下限:

除能够指定通配符的上限以外,Java也容许指定通配符的下限,通配符的下限用<?super类型>的方式来指定,通配符下限的做用与通配符上限的做用刚好相反。

指定通配符的下限就是为了支持类型型变。好比Foo是Bar的子类,当程序须要一个A<? super Bar>变量时,程序能够将A<Foo>、A<Object>赋值给A<? super Bar>类型的变量,这种型变方式被称为逆变。

对于逆变的泛型集合来讲,编译器只知道集合元素是下限的父类型,但具体是哪一种父类型则不肯定。所以,这种逆变的泛型集合能向其中添加元素(由于实际赋值的集合元素老是逆变声明的父类),从集合中取元素时只能被当成Object类型处理(编译器没法肯定取出的究竟是哪一个父类的对象)。

 

四、泛型方法:

前面介绍了在定义类、接口时可使用泛型形参,在该类的方法定义和成员变量定义、接口的方法定义中,这些泛型形参可被当成普通类型来用。在另一些状况下,定义类、接口时没有使用泛型形参,但定义方法时想本身定义泛型形参,这也是能够的,Java5还提供了对泛型方法的支持。

 

(1)、泛型方法的基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
  * 泛型方法的基本介绍
  * @param tClass 传入的泛型实参
  * @return T 返回值为T类型
  * 说明:
  *     1)public 与 返回值中间<T>很是重要,能够理解为声明此方法为泛型方法。
  *     2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并非泛型方法。
  *     3)<T>代表该方法将使用泛型类型T,此时才能够在方法中使用泛型类型T。
  *     4)与泛型类的定义同样,此处T能够随便写为任意标识,常见的如T、E、K、V等形式的参数经常使用于表示泛型。
  */
     public  <T> T genericMethod(Class<T> tClass) throws  InstantiationException ,
       IllegalAccessException{
             T instance = tClass.newInstance();
             return  instance;
     }
 
Object obj = genericMethod(Class.forName( "com.test.test" ));

  

(2)、类中的泛型方法:

泛型方法能够出现杂任何地方和任何场景中使用。可是有一种状况是很是特殊的,当泛型方法出如今泛型类中时,咱们再经过一个例子看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//注意泛型类先写类名再写泛型,泛型方法先写泛型再写方法名
//类中声明的泛型在成员和方法中可用
class  A <T, E>{
     {
         T t1 ;
     }
     A (T t){
         this .t = t;
     }
     T t;
 
     public  void  test1() {
         System.out.println( this .t);
     }
 
     public  void  test2(T t,E e) {
         System.out.println(t);
         System.out.println(e);
     }
}
@Test
public  void  run () {
     A <Integer,String > a =  new  A<>( 1 );
     a.test1();
     a.test2( 2 , "ds" );
//        1
//        2
//        ds
}
 
static  class  B <T>{
     T t;
     public  void  go () {
         System.out.println(t);
     }
}

  

(3)、泛型方法和可变参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public  class  泛型和可变参数 {
     @Test
     public  void  test () {
         printMsg( "dasd" , 1 , "dasd" , 2.0 , false );
         print( "dasdas" , "dasdas" "aa" );
     }
     //普通可变参数只能适配一种类型
     public  void  print(String ... args) {
         for (String t : args){
             System.out.println(t);
         }
     }
     //泛型的可变参数能够匹配全部类型的参数
     public  <T>  void  printMsg( T... args){
         for (T t : args){
             System.out.println(t);
         }
     }
         //打印结果:
     //dasd
     //1
     //dasd
     //2.0
     //false
 
}

  

(4)、静态方法与泛型

静态方法有一种状况须要注意一下,那就是在类中的静态方法使用泛型:静态方法没法访问类上定义的泛型;若是静态方法操做的引用数据类型不肯定的时候,必需要将泛型定义在方法上。

即:若是静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。

1
2
3
4
5
6
7
8
9
10
11
12
13
public  class  StaticGenerator<T> {
     ....
     ....
     /**
      * 若是在类中定义使用泛型的静态方法,须要添加额外的泛型声明(将这个方法定义成泛型方法)
      * 即便静态方法要使用泛型类中已经声明过的泛型也不能够。
      * 如:public static void show(T t){..},此时编译器会提示错误信息:
           "StaticGenerator cannot be refrenced from static context"
      */
     public  static  <T>  void  show(T t){
 
     }
}

  

 

总结:

泛型方法能使方法独立于类而产生变化,如下是一个基本的指导原则:

不管什么时候,若是你能作到,你就该尽可能使用泛型方法。也就是说,若是使用泛型方法将整个类泛型化,那么就应该使用泛型方法。另外对于一个static的方法而已,没法访问泛型类型的参数。因此若是static方法要使用泛型能力,就必须使其成为泛型方法。

 

 

3、泛型的类型擦除:

一、什么是类型擦除:

还记得咱们在文章开始介绍的代码吗?咱们如今再来看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public  class  Test {
 
     public  static  void  main(String[] args) {
 
         ArrayList<String> list1 =  new  ArrayList<String>();
         list1.add( "abc" );
 
         ArrayList<Integer> list2 =  new  ArrayList<Integer>();
         list2.add( 123 );
 
         System.out.println(list1.getClass() == list2.getClass());
     }
 
}

  

在这个例子中,咱们定义了两个ArrayList数组,不过一个是ArrayList<String>泛型类型的,只能存储字符串;一个是ArrayList<Integer>泛型类型的,只能存储整数,最后,咱们经过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。这就是java的泛型擦除。

 

下面咱们再来看一个例子加深一下理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
public  class  Test001 {
     
     public  static  void  main(String[] args)  throws  Exception {
         ArrayList<Integer> list= new  ArrayList<Integer>();
         list.add( 1 );
         
         list.getClass().getMethod( "add" ,Object. class ).invoke(list,  "asd" );
         for ( int  i= 0 ;i<list.size();i++) {
             System.out.println(list.get(i));
         }
     }
 
}

  

上面这段代码首先建立了一个ArrayList,泛型类型实例化为Integer对象,若是咱们直接调用add()方法,那么只能添加Integer类型的值,可是如今咱们利用反射,发现能够往里面加入String类型的值,这也说明了java的泛型擦除。

 

定义:

Java在编译期间,全部的泛型信息都会被擦掉,这就是泛型擦除。

正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。

 

二、类型擦除后保留的原始类型

原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,不管什么时候定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。

例一、

1
2
3
4
5
6
7
8
9
class  Pair<T> { 
     private  T value; 
     public  T getValue() { 
         return  value; 
    
     public  void  setValue(T  value) { 
         this .value = value; 
    
}

  

Pair的原始类型为:Object

1
2
3
4
5
6
7
相关文章
相关标签/搜索