泛型

泛型

         泛型(Generic type或者 generics)是对Java语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样。

为什么要使用泛型

         使用泛型意味着编写的代码可以被很多不同类型的对象所重用。在JAVA 5.0之前,java泛型程序设计是用继承实现的,类可以维护一个Object类型,但是这样会涉及到类型的强制转换,且使用时没有错误检查。但是通过泛型的使用,使得程序具有了更好的可读性和安全性。

泛型类

         泛型类就是具有一个或多个类型变量的类。泛型类可以看作是普通类的工厂。可见ArrayList或是Map代码:

         

public class ArrayList<E> extendsAbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}

    

public interfaceMap<K,V> {}

 

    这里需要注意的是:

1、      类型变量使用大写形式且比较短,在Java库中,使用变量E表示集合的元素类型,KV分别表示表的关键字与值的类型。T(需要时可以使用临近的字母US)表示任意类型。

2、      ArrayList<Object>不是ArrayList<String>的父类,需要注意。

泛型方法

         泛型不光可以应用到类上,还可以定义一个带有类型参数的简单方法。如:

         

class Test{
       public <T> T method(){
           return null;
       }
      
       public <T> void methodWithParam(T t){
       }
}

    注意类型变量要放在修饰符的后面,返回类型的前面。

    在调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:

    

Testtest = new Test();
test.<Integer>methodWithParam(123);
test.<String>methodWithParam(“abc”);

    当然在大多数情况下,可以省略尖括号及其内容。编译器有足够的信息能够判断出所调用的方法,可以判别出T所代表的类型。可以这样用:

    

test.methodWithParam(123);
test.methodWithParam(“abc”);

      泛型重载

         见代码:

public class GenericsTest {
 
    public void method(String str){
       System.out.println("Stringmethod");
    }
   
    public <T> void method(T t){
       System.out.println("Tmethod");
    }
   
    public <T> void method(T...t){
       System.out.println("T...method");
    }
   
    /**
     * @param args
     */
    public static void main(String[] args) {
       GenericsTest gt = new GenericsTest();
       gt.method("");
       gt.method(123);
       gt.method(123,454);
       gt.method(123,454,3.14);
       gt.method(123,454,3.14,"");
    }
 
}


输出结果为:

String method

T method

T...method

T...method

T...method

由上可以总结,

1、             指定类型的方法是要优先于泛型方法的,究其原因在于虚拟机在加载类时进行了类型擦除(详见后续部分)。

2、             另外一个很有意思的地方是,method(T…t),可以放入任何我们想放入的类型,这是怎么回事?这是由于编译器会自动会对传入的基本类型数据打包,并寻找他们共同的超类,可能是Number或是Object。这么做没有错,但是实际应用中不建议这样使用,尤其是Integer,Double,String搅和在一起,在使用会造成极大的困扰,而且最终类型变成了Obejct也不满足我们使用泛型的初衷。

类型变量的限制

         有时,类或方法需要对类型变量加以限制。如一个方法内对象调用了compareTo方法,这时就需要确保类型变量都实现了Comparable接口:

             

public <T extendsComparable<T>> void method(T t){
           …
}

    限制类型可以是接口也可以是类,但都用extends来修饰。这是因为extends更接近子类的概念,而且java的设计者也不打算在语言中添加一个新的关键字。

    限定类型可以是多个,是“&”来分隔。

        

public <T extendsComparable<T> & Serializable>void method(T t){
           …
}

泛型代码和虚拟机

         虚拟机没有泛型类型对象——所有对象都属于普通类。

         无论何时定义一个泛型类型,都自动提供一个相应的原始类型。原始类型的名字就是删除类型参数后的泛型类型名。擦除类型变量,并替换为限定类型(无限定类型的变量用Object)。对于有多个限定类型的地方,原始类型用第一个限定的类型变量来代替,其余的变量类型会在代码中使用强制转换来处理。所以这里需要注意的是,为了提高效率,应该将标签接口(即没有方法的接口,如:Comparable)放在边界列表的末尾。

翻译泛型表达式

         当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。例如:

         

Pair<Employee>buddies = …;
Employeebuddy = buddies.getFirst();

         执行时,编译器把这个方法调用翻译为两条虚拟机指令:

1)  对原始方法Pair.getFirst的调用

2)  将返回的Object类型强制转换为Employee类型

同理,泛型域也会做同样的处理。

翻译泛型方法

         类型擦除也会出现在泛型方法中。

         

public <T extendsComparable<T>> void method(T t){
           …
}

    擦除后变为:

         

public Comparable void method(Comparablet){
           …
}

         类型参数T被擦除,只留下了Comparable。方法

         

public <T extends String> void method(T t){
           …
}

    和方法

public String void method(String t){
           …
}


    是不能共存的,因为这已经不是overload了,<T extends String>擦除后只剩下String类型,两个方法就产生了冲突,因为其实两个方法是一样的。但是需要注意的是:

        

public <T extendsComparable<T> & Serializable>void method(T t){
           …
}
public void method(Serializable t){
           …
}

    并不冲突,原因在于擦除时,需要用限制类型转换时使用的是第一个限制类型。擦除后为:

        

public void method(Comparablet){
           …
}

    符合多态的特性。加载时要多注意,需要多多注意。

         在泛型重载中,提到了<T>void method(T t)和void method(String str)两个方法的区别,当时看的不是很清楚,但是在编译期对类型参数擦除后,就可以刻清楚地看到,这两个方法是method(Object t)和method(String str)的问题,输出那样的结果就很正常了。

         在编写代码时要注意,类型擦除后与多态发生冲突。当然可以庆幸的一点就是IDE可智能的帮你识别这些情况,但是我们要知其然知其所以然。关键一点在于擦除,多考虑一下擦除后的代码就会发现擦除没什么复杂的。

         总之要记住Java泛型转换的事实:

1、  虚拟机中没有泛型,只用普通的类和方法

2、  所有的类型参数都用它们的限定类型(最先出现的)替换

3、  桥方法被合成来保持多态

4、  为报出类型安全性,必要时要插入强制类型转换

使用泛型的约束

1、  不能使用基本类型实例化类型参数。因为基本类型不是Object的子类,这样就无法进行擦除操作了,虚拟机会不高兴的,呵呵。好在可以使用基本类型的封装类来进行处理。而且在实际处理中,编译器也会将基本类型自动打包为对应的封装类的。

2、 运行时类型查询只适用于原始类型。虚拟机中的对象总是一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。

Pair<String> stringPair = …;

Pair<Double> doublePair = …;

这里,stringPair.getClass() == doublePair.getClass()返回true。

3、 不能抛出也不能捕获泛型类实例。

a)        泛型类是无法扩展Throwable的,这是不合法的

class Problem<T> extends Exception{
…
}


编译时是无法通过的。

b)       不能在catch子句中使用类型变量。

public <T extends Throwable> void doWork(T t){
try{
//do work
}catch(T e){}
}


会返回“Cannot use the type parameter Tin a catch block”的编译错误。

c)        在异常声明中可以使用类型变量。

public <T extends Throwable> void doWork(T t) throws T{
try{
//do work
}catch(Throwable e){}
}


这么做是合法的。

4、  参数化类型的数组不合法。

Pair<String>[] arr = newPair<String>[10];//ERROR

是不合法的。为什么呢?这里又要考虑到擦除这个特性了。擦除后数组类型变为Pair[],可以转换为Object[]:

Object[] objarray = arr;

数组可以记住原始的数据类型,所以存入其他类型是不可以的,如

Objarray[0] = “Hello”会返回一个类型异常。但是如果赋值是:

Objarray[0] = new Pair<Double>();则不会有任何问题,What?看看约束2,所有的类型查询只会对应到原始类型上。

所以我们要禁止参数化类型的数组。在使用时可以考虑使用相应的集合,如ArrayList<String>。

5、 不能实例化类型变量。

不能使用像new T(…),new T[…]或是T.class这样的表达式中的类型变量。因为类型擦除后可能就变为Object,显然初始打算不是要new Object()的。

6、  泛型类的静态上下文中类型变量无效。

不能在静态域或方法中引用类型变量。

public class StaticGenericsTest<T> {
 
    private static T singleIstance;
    public static TgetSingleIstance(){
       if(singleIstance == null){}
      
       return singleIstance;
    }
}


    编译器提示的错误是“Cannot make a static reference tothe non-static type T”,但是假设它是可以执行。我们可以传入不同的类型,然后不同的类型都会有其各自的单例,这绝对是让人欣喜的,代码得到了极大的重用,多好。但是实际情况是怎么回事呢?由于擦除,对,又是擦除的存在,类型变量直接被编译器转换为Object,可见我们只可能有一个单例,而不是我们所想的一堆。因此,要禁止使用带有类型变量的静态域和方法。

7、  注意擦除后的冲突。上面说了很多擦除导致的悲剧,多注意了。

泛型类型的继承规则

         类型继承时要考虑原始类型。即Pair<List<String>>和Pair<LinkedList<String>>不是父子关系,而且它们的getClass返回的是同样的值。

而且把Pair<LinkedList<String>>实例,赋值给Pair<List<String>>是非法的。这点不像是数组。

         泛型类可以扩展或实现其他泛型类。如:List<String>是LinkedList<String>的父类。关注原始类型。

通配符类型

         固定的泛型类型系统使用起来不是特别的方便,所以java的设计者发明了一种很巧妙且安全的解决方案——通配符类型。

         Pair<?extends Employee>

         表示任何泛型Pair类型,它的类型参数是Employee的子类。

    

public void test(){
       ArrayList<ArrayList<String>> al_str = newArrayList<>();
       ArrayList<? extends List<String>> al = al_str;
       //The methodadd(capture#1-of ? extends List<String>) in the type
//ArrayList<capture#1-of? extends
         //List<String>> is notapplicable for the arguments (LinkedList<String>)
       al.add(new LinkedList<String>());
}

    会有问题,编译错误信息如上。使用通配符没有通过ArrayList<? extends List<String>>引用破坏ArrayList<ArrayList<String>>。

通配符的超类型限定

         通配符限定和类型变量限定相似,但是它可以指定超类型限定。

         ArrayList<? super LinkedList<String>>

    对一个超类型限定的域采的get方法,无法确定具体的类型,只能知道是指定限定类型的超类,一般就只能返回Object类,编译器无法知道超类型限定域的set方法的具体类型。对于带有子类型限定的来说,正好相反。

    所以说,待用超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。

无限定通配符

         对于无限定通配符,如:Pair<?>。它与Pair是不一样的。为什么呢?类型Pair<?>的方法:

         

?getFirst();
voidsetFirst(?);

         其中getFirst的返回值只能赋值给一个Object。setFirst()方法不能被调用,甚至不能被Object调用。而Pair类中的setFirst()方法时可以调用的。

捕获通配符

    

public void test(ArrayList<?> p){
       ? t = p.get(0);
}

    我们显然不能用“?”来作为一种类型,上面的代码一定是非法的。那么实际中我们应该怎么用呢?怎么处理类型?

    我们可以考虑使用一个辅助方法。

    

public <T> void testHelper(ArrayList<T> p){
       T t = p.get(0);
}
public void test(ArrayList<?> p){
       testHelper(p);
}

         这样通过testHelper的T来捕获通配符。注意testHelper是泛型方法,而test不是泛型方法,因为它具有固定的ArrayList<?>类型参数。注意。

题外话

         Class类也是一个泛型类。String.class是一个Class<String>类的对象,也会唯一的对象。



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值