Java泛型详解

本文深入讲解Java泛型的概念、使用方法及其实现原理。探讨泛型的定义与使用规范,包括类型参数赋值、通配符及数组泛型等内容。同时介绍了泛型与通配符的区别,类型擦除机制以及如何在类库升级中使用协变返回值。
1. 概述
泛型是Java 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
Java泛型被引入的好处是安全简单。
在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。
泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
泛型在使用中还有一些规则和限制:
    1、泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。
    2、同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
    3、泛型的类型参数可以有多个。
    4、泛型的参数类型可以使用extends语句,例如。习惯上成为“有界类型”。
    5、泛型的参数类型还可以是通配符类型
Class<?> classType = Class.forName(java.lang.String);  
泛型还有接口、方法等等。

2. 定义&使用
 类型参数的命名风格为:
 推荐你用简练的名字作为形式类型参数的名字(如果可能,单个字符)。最好避免小写字母,这使它和其他的普通的形式参数很容易被区分开来。
 使用T代表类型,无论何时都没有比这更具体的类型来区分它。这经常见于泛型方法。如果有多个类型参数,我们可能使用字母表中T的临近的字母,比如S。
 如果一个泛型函数在一个泛型类里面出现,最好避免在方法的类型参数和类的类型参数中使用同样的名字来避免混淆。对内部类也是同样。
 
 2.1 定义带类型参数的类
 在定义带类型参数的类时,在紧跟类命之后的<>内,指定一个或多个类型参数的名字,同时也可以对类型参数的取值范围进行限定,多个类型参数之间用,号分隔。
 定义完类型参数后,可以在定义位置之后的类的几乎任意地方(静态块,静态属性,静态方法除外)使用类型参数,就像使用普通的类型一样。
 注意,父类定义的类型参数不能被子类继承。
 public class TestClassDefine<T, S extends T> {
     ....  
 }
 
 2.2 定义待类型参数方法
 在定义带类型参数的方法时,在紧跟可见范围修饰(例如public)之后的<>内,指定一个或多个类型参数的名字,同时也可以对类型参数的取值范围进行限定,多个类型参数之间用,号分隔。定义完类型参数后,可以在定义位置之后的方法的任意地方使用类型参数,就像使用普通的类型一样。
 例如:
 public <T, S extends T> T testGenericMethodDefine(T t, S s){
     ...
 }
 注意:定义带类型参数的方法,骑主要目的是为了表达多个参数以及返回值之间的关系。例如本例子中T和S的继承关系, 返回值的类型和第一个类型参数的值相同。如果仅仅是想实现多态,请优先使用通配符解决。通配符的内容见下面章节。
 public <T> void testGenericMethodDefine2(List<T> s){
     ...
 }
 应改为
 public void testGenericMethodDefine2(List<?> s){
     ...
 }
 
3. 类型参数赋值
 当对类或方法的类型参数进行赋值时,要求对所有的类型参数进行赋值。否则,将得到一个编译错误。
 
 3.1 对带类型参数的类进行类型参数赋值
 对带类型参数的类进行类型参数赋值有两种方式
 第一声明类变量或者实例化时。例如
 List<String> list;
 list = new ArrayList<String>;
 第二继承类或者实现接口时。例如
 public class MyList<E> extends ArrayList<E> implements List<E> {...}
 
 3.2 对带类型参数方法进行赋值
 当调用范型方法时,编译器自动对类型参数进行赋值,当不能成功赋值时报编译错误。例如
 public <T> T testGenericMethodDefine3(T t, List<T> list){
     ...
 }
 public <T> T testGenericMethodDefine4(List<T> list1, List<T> list2){
     ...
 }
 
 Number n = null;
 Integer i = null;
 Object o = null;
 testGenericMethodDefine(n, i);//此时T为Number, S为Integer
 testGenericMethodDefine(o, i);//T为Object, S为Integer
 
 List<Number> list1 = null;
 testGenericMethodDefine3(i, list1)//此时T为Number
 
 List<Integer> list2 = null;
 testGenericMethodDefine4(list1, list2)//编译报错
 
 3.3 通配符
 在上面两小节中,对是类型参数赋予具体的值,除此,还可以对类型参数赋予不确定值。例如
 List<?> unknownList;
 List<? extends Number> unknownNumberList;
 List<? super Integer> unknownBaseLineIntgerList;
 注意: 在Java集合框架中,对于参数值是未知类型的容器类,只能读取其中元素,不能像其中添加元素,
 因为,其类型是未知,所以编译器无法识别添加元素的类型和容器的类型是否兼容,唯一的例外是NULL

 List<String> listString;
 List<?> unknownList2 = listString;
 unknownList = unknownList2;

 listString = unknownList;//编译错误

 3.4 泛型和通配符
 泛型是一种模版,在定义时使用,应用时在区域范围了确定后类型保持不变;通配符是未知类型定义,在实例应用时使用,在声明后应用任具有不确定性,未知类型声明不能赋值给确定类型声明(兼容问题,NULL除外),否则编译报错。

 
4. 数组范型
 可以使用带范型参数值的类声明数组,却不可有创建数组
 List<Integer>[] iListArray;
 new ArrayList<Integer>[10];//编译时错误
 
5. 实现原理

5.1. Java泛型实现原理:类型擦出

Java的泛型是伪泛型。在编译期间,所有的泛型信息都会被擦除掉。正确理解泛型概念的首要前提是理解类型擦出(type erasure)。
Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器编译的时候去掉,这个过程就称为类型擦除。
如在代码中定义的List<object>和List<String>等类型,在编译后都会编程List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方法与C++模版机制实现方式(后面介绍)之间的重要区别。
5.2.类型擦除后保留的原始类型
原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。无论何时定义一个泛型类型,相应的原始类型都会被自动地提供。类型变量被擦除(crased),并使用其限定类型(无限定的变量用Object)替换。
class Pair<T> {  
  private T value;  
  public T getValue() {  
    return value;  
  }  
  public void setValue(T  value) {  
    this.value = value;  
  }  
}  
Pair<T>的原始类型为:
class Pair {  
  private Object value;  
  public Object getValue() {  
    return value;  
  }  
  public void setValue(Object  value) {  
    this.value = value;  
  }  
}
因为在Pair<T>中,T是一个无限定的类型变量,所以用Object替换。其结果就是一个普通的类,如同泛型加入java变成语言之前已经实现的那样。在程序中可以包含不同类型的Pair,如Pair<String>或Pair<Integer>,但是,擦除类型后它们就成为原始的Pair类型了,原始类型都是Object。
如果类型变量有限定,那么原始类型就用第一个边界的类型变量来替换。
比如Pair这样声明:
public class Pair<T extends Comparable& Serializable> { 
那么原始类型就是Comparable。
注意:
    如果Pair这样声明public class Pair<T extends Serializable&Comparable> ,那么原始类型就用Serializable替换,而编译器在必要的时要向Comparable插入强制类型转换。为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界限定列表的末尾。
    要区分原始类型和泛型变量的类型
    在调用泛型方法的时候,可以指定泛型,也可以不指定泛型。
    在不指定泛型的情况下,泛型变量的类型为 该方法中的几种类型的同一个父类的最小级,直到Object。
    在指定泛型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类。
public class Test{  
  public static void main(String[] args) {  
    /**不指定泛型的时候*/  
    int i=Test.add(1, 2); //这两个参数都是Integer,所以T为Integer类型  
    Number f=Test.add(1, 1.2);//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Number  
    Object o=Test.add(1, "asd");//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Object  
  
    /**指定泛型的时候*/  
    int a=Test.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类  
    int b=Test.<Integer>add(1, 2.2);//编译错误,指定了Integer,不能为Float  
    Number c=Test.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float  
  }  

  //这是一个简单的泛型方法  
  public static <T> T add(T x,T y){  
    return y;  
  }  
}  

5.3.一个泛型类被其所有调用共享
下面的代码打印的结果是什么?
       List<String> l1 = new ArrayList<String>();
       List<Integer> l2 = new ArrayList<Integer>();
       System.out.println(l1.getClass() == l2.getClass());
或许你会说false,但是你想错了。它打印出true。因为一个泛型类的所有实例在运行时具有相同的运行时类(class),而不管他们的实际类型参数。事实上,泛型之所以叫泛型,就是因为它对所有其可能的类型参数,有同样的行为;同样的类可以被当作许多不同的类型。作为一个结果,类的静态变量和方法也在所有的实例间共享。这就是为什么在静态方法或静态初始化代码中或者在静态变量的声明和初始化时使用类型参数(类型参数是属于具体实例的)是不合法的原因。

5.4 转型和instanceof

泛型类被所有其实例(instances)共享的另一个暗示是检查一个实例是不是一个特定类型的泛型类是没有意义的。
       Collection cs = new ArrayList<String>();
       if (cs instanceof Collection<String>) { ...} // 非法
类似的,如下的类型转换
Collection<String> cstr = (Collection<String>) cs;
得到一个unchecked warning,因为运行时环境不会为你作这样的检查。

6. Class的范型处理
Java 5之后,Class变成范型化了。
JDK1.5中一个变化是类 java.lang.Class是泛型化的。这是把泛型扩展到容器类之外的一个很有意思的例子。
现在,Class有一个类型参数T, 你很可能会问,T 代表什么?它代表Class对象代表的类型。比如说,String.class类型代表 Class<String>,Serializable.class代表 Class<Serializable>。这可以被用来提高你的反射代码的类型安全。
特别的,因为 Class的 newInstance() 方法现在返回一个T, 你可以在使用反射创建对象时得到更精确的类型。比如说,假定你要写一个工具方法来进行一个数据库查询,给定一个SQL语句,并返回一个数据库中符合查询条件的对象集合(collection)。
一个方法是显式的传递一个工厂对象,像下面的代码:
interface Factory<T> {
      public T[] make();
}
public <T> Collection<T> select(Factory<T> factory, String statement) {
       Collection<T> result = new ArrayList<T>();
       /* run sql query using jdbc */
       for ( int i=0; i<10; i++ ) { /* iterate over jdbc results */
            T item = factory.make();
            /* use reflection and set all of item’s fields from sql results */
            result.add( item );
       }
       return result;
}
你可以这样调用:
select(new Factory<EmpInfo>(){
    public EmpInfo make() {
        return new EmpInfo();
        }
       } , ”selection string”);
也可以声明一个类 EmpInfoFactory 来支持接口 Factory:
class EmpInfoFactory implements Factory<EmpInfo> { ...
    public EmpInfo make() { return new EmpInfo();}
}
然后调用:
select(getMyEmpInfoFactory(), "selection string");
这个解决方案的缺点是它需要下面的二者之一:
调用处那冗长的匿名工厂类,或为每个要使用的类型声明一个工厂类并传递其对象给调用的地方
这很不自然。
使用class类型参数值是非常自然的,它可以被反射使用。没有泛型的代码可能是:
Collection emps = sqlUtility.select(EmpInfo.class, ”select * from emps”); ...
public static Collection select(Class c, String sqlStatement) {
    Collection result = new ArrayList();
    /* run sql query using jdbc */
    for ( /* iterate over jdbc results */ ) {
        Object item = c.newInstance();
        /* use reflection and set all of item’s fields from sql results */
        result.add(item);
    }
        return result;
}
但是这不能给我们返回一个我们要的精确类型的集合。现在Class是泛型的,我们可以写:
Collection<EmpInfo> emps=sqlUtility.select(EmpInfo.class, ”select * from emps”); ...
public static <T> Collection<T> select(Class<T>c, String sqlStatement) {
    Collection<T> result = new ArrayList<T>();
    /* run sql query using jdbc */
    for ( /* iterate over jdbc results */ ) {
        T item = c.newInstance();
        /* use reflection and set all of item’s fields from sql results */
        result.add(item);
    }
    return result;
}
来通过一种类型安全的方式得到我们要的集合。
这项技术是一个非常有用的技巧,它已成为一个在处理注释(annotations)的新API中被广泛使用的习惯用法。

7. 新老代码兼容

7.1. 为了保证代码的兼容性,下面的代码编译器(javac)允许,类型安全有你自己保证

List l = new ArrayList<String>();
List<String> l = new ArrayList();

7.2. 在将你的类库升级为范型版本时,慎用协变式返回值。
例如,将代码
public class Foo {
    public Foo create(){
        return new Foo();
    }
}

public class Bar extends Foo {
    public Foo create(){
        return new Bar();
    }
}
采用协变式返回值风格,将Bar修改为
public class Bar extends Foo {
    public Bar create(){
        return new Bar();
    }
}
要小心你类库的客户端。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值