java 泛型的详解解

 Java泛型详解

Java泛型 (generics)是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数( type parameter)。 声明的类型参数在使用时用具体的类型来替换。 泛型最主要的应用是在JDK 5中的新 集合类框架 中。对于泛型概念的引入,开发社区的观点是 贬不一 。从 好的方面 来说,泛型的引入可以 解决之前的集合类框架在使用过程中通常会出现的运行时刻类型错误(安全性更好) 因为编译器可以在编译时刻就发现很多明显的错误。 而从不好的地方来说,为了保证与旧有版本的兼容性, Java泛型的实现上存在着一些不够优雅的地方。 当然这也是任何有历史的编程语言所需要承担的历史包袱。 后续的版本更新会为早期的设计缺陷所累。  

 
开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。 比如一个方法如果接收List<Object>作为形式参数, 那么如果尝试将一个List<String> 的对象作为实际参数传进去,却发现无法通过编译。 虽然从直觉上来说,Object是String的父类, 这种类型转换应该是合理的。 但是实际上这会产生隐含的类型转换问题, 因此编译器直接就禁止这样的行为。 本文试图对Java泛型做一个概括性的说明。

类型擦除

正确理解泛型概念的首要前提是理解 类型擦除(type erasure)  Java中的 泛型基本上都是在编译器这个层次来实现的 。在生成的 Java字节代码 不包含泛型中的类型信息 的。 使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。 这个过程就称为类型擦除。如在代码中定义的List< Object>和List<String>等类型, 在编译之后都会变成List。JVM看到的只是List, 而由泛型附加的类型信息对JVM来说是不可见的。 Java编译器会在编译时尽可能的发现可能出错的地方, 但是仍然无法避免在运行时刻出现类型转换异常的情况。 类型擦除也是Java的泛型实现方式与 C++模板机制 实现方式之 间的重要区别。
很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:
  • 泛型类并没有自己独有的Class类对象比如并不存在List<String>.class或是List<Integer>.class,而只有List.class。
  • 静态变量是被泛型类的所有实例所共享的对于声明为MyClass<T>的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过new MyClass<String>还是new MyClass<Integer>创建的对象,都是共享一个静态变量。
  • 泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException<String>和MyException<Integer>的。对于JVM来说,它们都是 MyException类型的。也就无法执行与异常对应的catch语句。

类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般是Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。同时去掉出现的类型声明,即去掉<>的内容。比如T get()方法声明就变成了Object get();List<String>就变成了List。接下来就可能需要生成一些桥接方法(bridge method)。这是由于擦除了类型之后的类可能缺少某些必须的方法。比如考虑下面的代码:

class MyString implements Comparable<String> {      public int compareTo(String str) {                  return 0;          }  } 


当类型信息被擦除之后,上述类的声明变成了class MyString implements Comparable。但是这样的话, 类MyString就会有编译错误, 因为没有实现接口Comparable声明的int compareTo(Object)方法。 这个时候就由编译器来动态生成这个方法。

实例分析

了解了类型擦除机制之后,就会明白编译器承担了全部的类型检查工作编译器禁止某些泛型的使用方式,正是为了确保类型的安全性以上面提到的List<Object>和List<String>为例来具体分析:

public void inspect(List<Object> list) {          for (Object obj : list) {                  System.out.println(obj);          }          list.add(1); //这个操作在当前方法的上下文是合法的。   }  public void test() {          List<String> strs = new ArrayList<String>();          inspect(strs); //编译错误   } 

这段代码中,inspect方法接受List<Object>作为参数,当在test方法中试图传入List<String>的时候,会出现编译错误。假设这样的做法是允许的,那么在inspect方法就可以通过list.add(1)来向集合中添加一个数字。这样在test方法看来,其声明为List<String>的集合中却被添加了一个Integer类型的对象。这显然是违反类型安全的原则的,在某个时候肯定会抛出ClassCastException。因此,编译器禁止这样的行为。编译器会尽可能的检查可能存在的类型安全问题。对于确定是违反相关原则的地方,会给出编译错误。当编译器无法判断类型的使用是否正确的时候,会给出警告信息。 

通配符(?)与上下界(extends ,super )

使用泛型类的时候,既可以指定一个具体的类型,如List<String>就声明了具体的类型是String;也可以用通配符?来表示未知类型,如List<?>就声明了List中包含的元素类型是未知的。 通配符所代表的其实是一组类型,但具体的类型是未知的。List<?>所声明的就是所有类型都是可以的。但是List<?>并不等同于List<Object>。List<Object>实际上确定了List中包含的是Object及其子类,在使用的时候都可以通过Object来进行引用。而List<?>则其中所包含的元素类型是不确定。其中可能包含的是String,也可能是 Integer。如果它包含了String的话,往里面添加Integer类型的元素就是错误的。正因为类型未知,就不能通过new ArrayList<?>()的方法来创建一个新的ArrayList对象。因为编译器无法知道具体的类型是什么。但是对于 List<?>中的元素确总是可以用Object来引用的,因为虽然类型未知,但肯定是Object及其子类。考虑下面的代码:

如上所示,试图对一个带通配符的泛型类进行操作的时候,总是会出现编译错误。其原因在于通配符所表示的类型是未知的。
因为对于List<?>中的元素只能用Object来引用,在有些情况下不是很方便。在这些情况下,可以使用上下界来限制未知类型的范围。 如List<? extendsNumber>说明List中可能包含的元素类型是Number及其子类。而List<? super Number>则说明List中包含的是Number及其父类当引入了上界之后,在使用类型的时候就可以使用上界类中定义的方法。比如访问 List<? extends Number>的时候,就可以使用Number类的intValue等方法。

类型系统

在Java中, 大家比较熟悉的是通过继承机制而产生的类型体系结构。 比如String继承自Object。根据 Liskov替换原则 子类是可以替换父类的 。当需要Object类的引用的时候, 如果传入一个String对象是没有任何问题的。 但是反过来的话,即用父类的引用替换子类引用的时候,就 需要进行强制类型转换 编译器并不能保证运行时刻这种转换一定是合法的。 这种自动的子类替换父类的类型转换机制,对于数组也是适用的。 String[]可以替换Object[]。但是泛型的引入, 对于这个类型系统产生了一定的影响。正如前面提到的List< String>是不能替换掉List<Object>的。
引入泛型之后的类型系统增加了两个维度: 一个是类型参数自身的继承体系结构, 另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于 List<String>和List<Object> 这样的情况,类型参数String是继承自Object的。 而第二种指的是 List接口继承自Collection接口。 对于这个类型系统,有如下的一些规则:
  • 相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。即List<String>是Collection<String> 的子类型,List<String>可以替换Collection<String>。这种情况也适用于带有上下界的类型声明。
  • 当泛型类的类型声明中使用了通配符的时候, 其子类型可以在两个维度上分别展开。如对Collection<? extends Number>来说,其子类型可以在Collection这个维度上展开,即List<? extends Number>和Set<? extends Number>等;也可以在Number这个层次上展开,即Collection<Double>和 Collection<Integer>等。如此循环下去,ArrayList<Long>和 HashSet<Double>等也都算是Collection<? extends Number>的子类型。
  • 如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。
理解了上面的规则之后, 就可以很容易的修正实例分析中给出的代码了。只需要把List< Object>改成List<?>即可。List< String>是List<?>的子类型, 因此传递参数时不会发生错误。

开发自己的泛型类

泛型类与一般的Java类基本相同,只是在类和接口定义上多出来了用<>声明的类型参数。一个类可以有多个类型参数,如 MyClass<X, Y, Z>。 每个类型参数在声明的时候可以指定上界。所声明的类型参数在Java类中可以像一般的类型一样作为方法的参数和返回值,或是作为域和局部变量的类型。但是由于类型擦除机制,类型参数并不能用来创建对象或是作为静态变量的类型。考虑下面的泛型类中的正确和错误的用法。

class ClassTest<X extends Number, Y, Z> {          private X x;          private static Y y; //编译错误,不能用在静态变量中          public X getFirst() {          //正确用法                  return x;          }          public void wrong() {                  Z z = new Z(); //编译错误,不能创建对象 ,因为泛型类并没有自己独立的Class类对象         }  }

最佳实践

在使用泛型的时候可以遵循一些基本的原则,从而避免一些常见的问题。
  • 在代码中避免泛型类和原始类型的混用。比如List<String>和List不应该共同使用。这样会产生一些编译器警告和潜在的运行时异常。当需要利用JDK 5之前开发的遗留代码,而不得不这么做时,也尽可能的隔离相关的代码。
  • 在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念由于具体的类型是未知的,很多操作是不允许的。
  • 泛型类最好不要同数组一块使用。你只能创建new List<?>[10]这样的数组,无法创建new List<String>[10]这样的。这限制了数组的使用能力,而且会带来很多费解的问题。因此,当需要类似数组的功能时候,使用集合类即可。
  • 不要忽视编译器给出的警告信息

C++模板实现

    在c++中为每个模板的实例化产生不同的类型,这一现象被称为“模板代码膨胀”。

比如 vector<int>, vector<char>, vector<double>, 这里总共会生成3份不同的vector代码。


 

java 泛型方法类型推导

  • 编译器判断泛型方法的实际类型参数的过程叫做类型推断,类型推断的实现方法是一种非常复杂的过程.
  • 根据调用泛型方法时实际传递的参数类型或返回值类型来推断,具体规则如下:
  1. 如果某类型变量只在方法参数列表或返回值的一处被调用了,那根据调用该方法时该处的实际类型来确定,即直接根据调用方法时传递的实际类型或方法返回值的类型来确定泛型方法的参数类型.例如: swap(new String[3],3,4) --->static <E>  void swap(E[] a,int i,int t)
  2. 当某个类型变量在方法的参数列表和返回值中被多次利用了,而且在调用方法时这多处的实际类型又是一样的,那么这也可以很明显的知道此泛型方法的参数类型.例如: add(3,5) --> static <T> T add(T a,T b)
  3. 当某个类型变量在方法的参数列表和返回值中被多次利用了,而且在调用方法时这多处的实际类型又对应不同的类型,且返回值是void,那么这时取多处实际变量类型的最大交集.例如: fill(new Integer[3],3.5f) --> static <T> void fill(T[] a,T i) ,此时T为Number,编译不会报错,但运行有问题.
  4. 当某个类型变量在方法的参数列表和返回值中被多次利用了,且返回值不为空,在调用方法时这多处的实际类型又对应不同的类型,那么优先考虑返回值的类型.int x = add(3,3.5f) --> static <T> T add(T a,T b)
  5. 参数类型的类型推断具有传递性,
    copy(new Integer[5],new String[5]) --> static <T> void copy(T[] a, T[] b)  T为Object类型,没有问题
    copy(new Vector<String>(),new Integer[5]) --> static <T> void copy(Collection<T> a, T[] b)    在new Vector<String>()时决定了T为String类型,而new Integer[5]不是String类型,这样会报错
package all;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * java 泛型方法的类型推断
 * @author root
 *
 */
public class TypeInfer {
    /**
     * 1.类型变量只在方法参数列表或返回值中的一处被调用,
     * 那么类型推测的结点就是直接传递 的参数类型或返回的值的类型
     * @param <T>
     * @param value
     */
    public static<T> void onlyOneCall(T value){
	System.out.println(value);
    }
    /**
     *参数多次调用且传递参数类型相同,那么就会传入参数的实际类型
     * @param <T>
     * @param a
     * @param b
     * @return
     */
    public static<T> T sameParam(T a,T b){
	return a;
    }
    /**
     * 有多个参数,且传入实参的类型都不一样,推断的出的类型则是它们的交集,运行时可能会错误
     * 如: noSamePram(new Integer[2],new Float(2.0))//此时推断出来的类型为 Number,但会运行错误
     * @param <T>
     * @param array
     * @param b
     */
    public static<T> void noSameParam(T[]array,T b){
	int len = array.length;
	for(int i=0;i<len;i++)
	    array[i] = b;
    }
    /**
     * 有多个参数,传入值类型不同,而且有返回值,那么会优先考虑返回 值
     * @param <T>
     * @param a
     * @param b
     * @return
     */
    public static<T> T noSameParamWithReturn(T a,T b){
	return b;
    }
    /**
     * 类型推断具有传递性,如果参数中有需要推断的类型,如果list<T>此的T就是传递进来的参数的值的类型,如
     * copy(new List<String> list,new Integer(1)),T就为 string,而此时array为integer,两者不现,不会再取
     * 交集,会编译错误,因此,在参数中的泛型的泛型方法中,传递的参数一定保持一致
     * 如: copy(Collection<T> col,T[]array),copy(new List<String>,new Integer[]) 首先推断出 T 类型为  string,而后面的是 Integer,所以错误 
     * @param <T>
     * @param col
     * @param array
     */
    public static<T> void copy(Collection<T>col,T[]array){
	
    }
    public static<T> T copycopy(T[]array,Collection<T>col){
	return array[0];
    }
    public static<T> T copy(T a,T b){
	return a;
    }
    public static void main(String args[]){
	Integer a  = new Integer(1);
	Long b = new Long(2);
	//noSameParam(new Integer[3],new Float(1));
	List<String> list = new ArrayList<String>();
	Integer [] array = new Integer[]{1,2,3};
	copy(list,array);
	copy(array,list);
	copy(new String(),new Integer(2));
    }
}


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值