【泛型】Generic 参数化类型 类型转换

时间:2021-01-25 19:26:05
参考: http://blog.csdn.net/lonelyroamer/article/details/7864531#comments http://blog.csdn.net/lonelyroamer/article/details/7868820#comments http://blog.csdn.net/LonelyRoamer/article/details/7927212#comments

关于泛型的一些重要知识点

泛型由来:早期Java版本(1.4及之前)如果要代指某个泛化类对象,只能使用Object,这样写出来的代码需要增加强转,而且缺少类型检查,代码缺少健壮性。在1.5之后,Java引入了泛型的概念,提供了一套抽象的类型表示方法。
简单来说,泛型是JDK1.5中出现的安全机制。
好处:将运行时期的ClassCastException问题转到了编译时期,避免了强制转换的麻烦。
什么时候用:当操作的引用数据类型不确定的时候,就使用<>,将要操作的引用数据类型传入即可。 其实<>就是一个用于接收具体引用数据类型的参数范围。 在程序中,只要用到了带有<>的类或者接口,就要明确传入的具体引用数据类型。

泛型技术是给编译器使用的技术,用于编译时期。确保了类型的安全。
运行时,会将泛型去掉,生成的class文件中是不带泛型的,这个称为泛型的擦除。
为什么擦除呢?因为为了兼容运行的类加载器。
泛型的补偿:在运行时,通过获取元素的类型进行转换动作。这样就不用再手动强制转换了。

泛型的通配符【?】未知类型。
泛型的限定:
  • 【? extends E】接收E类型或者E的子类型对象。上限。一般存储对象的时候用。比如 添加元素 addAll。
  • 【? super E】接收E类型或者E的父类型对象。下限。一般取出对象的时候用。比如比较器。

利用泛型,我们可以:
  • 1、表示多个可变类型之间的相互关系:HashMap<T,S>表示类型T与S的映射,HashMap<T, S extends T>表示T的子类与T的映射关系。
  • 2、细化类的能力:ArrayList<T> 可以容纳任何指定类型T的数据,当T代指人,则是人的有序列表,当T代指杯子,则是杯子的有序列表,所有对象个体可以共用相同的操作行为。
  • 3、复杂类型被细分成更多类型:List<People>和List<Cup>是两种不同的类型,这意味着List<People> listP = new ArrayList<Cup>()是不可编译的。这种检查基于编译时而非运行时,所以说是不可编译并非不可运行,因为运行时ArrayList不保留Cup信息。另外要注意,即使People继承自Object,List<Object> listO = new ArrayList<People>()也是不可编译的,应理解为两种不同类型。因为listO可以容纳任意类型,而实例化的People列表只能接收People实例,这会破坏数据类型完整性。

泛型的基本概念

泛型的定义:泛型是JDK 1.5的一项新特性,它的本质是参数化类型 ParameterizedType带有类型参数的类型。也就是说所操作的数据类型被指定为一个参数,在用到的时候再指定具体的类型。如:List<T>、Map<Integer, String>、List<? extends Number>。
public interface java.lang.reflect.ParameterizedType extends Type

GenericDeclaration接口是声明类型变量的所有实体的公共接口,也就是说,只有实现了该接口才能在对应的实体上声明类型变量。这些实体目前只有三个:Class、Construstor、Method。当这种参数化类型用在类、接口和方法的创建中时,分别称为泛型类、泛型接口和泛型方法。
注意:因为直接实现子类没有Field类,所以在属性上面不能定义类型变量。
public interface java.lang.reflect.GenericDeclaration所有已知实现类:Class、Constructor、Method

泛型思想早在C++语言的模板(Templates)中就开始生根发芽,在Java语言处于还没有出现泛型的版本时,只能通过 "Object是所有类型的父类" 和 "类型强制转换" 两个特点的配合来实现类型泛化。例如在哈希表的存取中,JDK 1.5之前使用HashMap的get()方法,返回值就是一个Object对象,由于Java语言里面所有的类型都继承于java.lang.Object,那Object转型为任何对象成都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个Object到底是个什么类型的对象。在编译期间,编译器无法检查这个Object的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多ClassCastException的风险就会被转嫁到程序运行期之中

泛型技术在C#和Java之中的使用方式看似相同,但实现上却有着根本性的分歧,C#里面泛型无论在程序源码中、编译后的IL中(Intermediate Language,中间语言,这时候泛型是一个占位符)或是运行期的CLR中都是切实存在的,比如 List<int> 与 List<String> 就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型被称为真实泛型

Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原始类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,ArrayList<int> 与 ArrayList<String> 就是同一个类型。所以说泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型被称为伪泛型

使用泛型机制编写的程序代码要比那些杂乱的使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。泛型对于集合类来说尤其有用。
泛型程序设计(Generic Programming)意味着编写的代码可以被很多不同类型的对象所重用。

实例分析

在JDK1.5之前,Java泛型程序设计是用继承来实现的。因为Object类是所用类的基类,所以只需要维持一个Object类型的引用即可。就比如ArrayList只维护一个Object引用的数组:
public class ArrayList{      public Object get(int i){......}      public void add(Object o){......}      ......      private Object[] elementData;  } 
这样会有两个问题:
  • 没有错误检查,可以向数组列表中添加任何类的对象
  • 在取元素的时候,需要进行强制类型转换
这样,很容易发生错误,比如:
/**jdk1.5之前的写法,容易出问题*/  ArrayList arrayList1=new ArrayList();  arrayList1.add(1);  arrayList1.add(1L);  arrayList1.add("asa"); int i=(Integer) arrayList1.get(1);//因为不知道取出来的值的类型,类型转换的时候容易出错  
这里的第二个元素是一个长整型,而你以为是整形,所以在强转的时候发生了错误。

所以。在JDK1.5之后,加入了泛型来解决类似的问题。例如在ArrayList中使用泛型:
/** jdk1.5之后加入泛型*/  ArrayList<String> arrayList2=new ArrayList<String>();  //限定数组列表中的类型  //arrayList2.add(1); //因为限定了类型,所以不能添加整形//arrayList2.add(1L);//因为限定了类型,所以不能添加整长形arrayList2.add("asa");//只能添加字符串String str=arrayList2.get(0);//因为知道取出来的值的类型,所以不需要进行强制类型转换  

还要明白的是,泛型特性是向前兼容的。尽管 JDK 5.0 的标准类库中的许多类,比如集合框架,都已经泛型化了,但是使用集合类的现有代码(没有加泛型的代码)可以继续不加修改地在 JDK 1.5 中工作。

泛型的使用

泛型的参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。下面看看具体是如何定义的。

泛型类:类名后面

泛型类就是在声明时,定义了一个或多个类型变量的类。 泛型类中定义的类型变量的作用范围为当前泛型类中。 泛型类中定义的类型变量用于,在多个方法签名间实施类型约束。例如,当创建一个 Map<K, V> 类型的对象时,您就在方法之间宣称一个类型约束,您 put() 的值将与 get() 返回的值的类型相同。
public class HashMap<K,V> {    public V put(K key, V value) {...}    public V get(Object key) {...}    ...}

定义一个泛型类十分简单,只需要在类名后面加上<>,再在里面加上类型参数:
public class Pair<T> {private T value;public Pair(T value) {this.value = value;}public T getValue() {return value;}public void setValue(T value) {this.value = value;}}
现在我们就可以使用这个泛型类了:
public static void main(String[] args) throws ClassNotFoundException {Pair<String> pair = new Pair<String>("Hello");//注意,"="号左边和右边都要使用<>指定泛型的实际类型String str = pair.getValue();pair.setValue("World");}
泛型类可以有多个类型变量,例如:
class Pair<T, S, P, U, E> { }
注意:类型变量使用大写形式,且比较短,这是很常见的。在Java库中,使用变量E表示集合的元素类型,K和V分别表示关键字与值的类型。需要时还可以用临近的字母U和S表示“任意类型”。

泛型接口

泛型接口和泛型类差不多:
interface Show<T,U>{      void show(T t,U u);  } 
实现类
public class ShowTest implements Show<String, Date> {@Overridepublic void show(String t, Date u) {System.out.println(t + "  " + u.getTime());}}
测试一下:
Show<String, Date> show = new ShowTest();show.show("包青天", new Date());

泛型方法:返回值之前

泛型方法就是在声明方法时,定义了一个或多个类型变量的方法。
泛型方法中定义的类型变量的作用范围为当前泛型方法中。
泛型方法中定义的类型变量用于,在该方法的多个参数之间,或在该方法的参数与返回值之间,宣称一个类型约束。
class Person<S> {public <W> void show(W w) {//这里的【W】完全等价于Objectif (w != null) System.out.println(w.toString());}public static <Y> void staticShow(Y y) {if (y != null) System.out.println(y.toString());//静态方法不能访问在类声明上定义的类型变量//S s;//错误提示:Cannot make a static reference to the non-static type S}}

泛型变量的类型限定

对于上面定义的泛型变量,因为在编译之前,也就是我们还在定义这个泛型方法的时候,我们并不知道这个泛型类型 T 到底是什么类型,所以,只能默认T为原始类型Object,所以它只能调用来自于Object的那几个方法。 如果我们想限定类型的范围,比如必须是某个类的子类,或者某个接口的实现类,这时可以使用类型限定对类型变量T设置限定(bound)来实现。

类型限定在泛型类、泛型接口和泛型方法中都可以使用,不过要注意下面几点:
  • 无限定的泛型变量等价于Object(白哥添加)
  • 不管该限定是类还是接口,统一都使用关键字 extends
  • 可以使用 & 符号给出多个限定
  • 如果限定既有接口也有类,那么类必须只有一个,并且放在首位置
比如:
public static <T extends Comparable> T get(T t1,T t2)  //继承或实现都用extendspublic static <T extends Comparable & Serializable> T get(T t1,T t2)  //使用 & 符号给出多个限定public static <T extends Object & Comparable & Serializable> T get(T t1,T t2)  //继承的类Object必须放在首位

通配符?的使用

通配符有三种:
  • 无限定通配符  形式<?>
  • 上边界限定通配符 形式< ? extends Number>
  • 下边界限定通配符    形式< ? super Number>

1、泛型中的?通配符
如果定义一个方法,该方法用于打印出任意参数化类型的集合中的所有数据,如果这样写
public static void main(String[] args) throws Exception {List<Integer> listInteger = new ArrayList<Integer>();printCollection(listInteger);//报错 The method printCollection(Collection<Object>) in the type Test is not applicable for the arguments (List<Integer>)}public static void printCollection(Collection<Object> collection) {for (Object obj : collection) {System.out.println(obj);}}
语句printCollection(listInteger);报错,这是因为泛型的参数是不考虑继承关系的,就直接报错。 这就得用?通配符
public static void printCollection(Collection<?> collection) {...}
在方法 printCollection 中不能出现与参数类型有关的方法,比如:
collection.add(new Object());//The method add(capture#1-of ?) in the type Collection<capture#1-of ?> is not applicable for the arguments (Object)
因为程序调用这个方法的时候传入的参数不知道是什么类型的。 但是可以调用与参数类型无关的方法比如 collection.size(); 总结:使用?通配符可以引用其他各种参数化的类型,?通配符定义的变量的主要用作引用,可以调用与参数化无关的方法,不能调用与参数化有关的方法。

2、?通配符的扩展:界定通配符的上边界
List<? extends S> x = new ArrayList<T>();
类型S指定一个数据类型,那么类型T就只能是类型S或者是类型S的子类
List<? extends Number> x = new ArrayList<Integer>();//正确List<? extends Number> y = new ArrayList<Object>();//错误  Type mismatch: cannot convert from ArrayList<Object> to List<? extends Number>

3、?通配符的扩展:界定通配符的下边界
List<? super S> x = new ArrayList<T>();
类型S指定一个数据类型,那么类型T就只能是类型S或者是类型S的父类
List<? super Number> y = new ArrayList<Object>();//正确List<? super Number> x = new ArrayList<Integer>();//错误  Type mismatch: cannot convert from ArrayList<Integer> to List<? super Number>
提示:限定通配符总是包括自己

类型擦除

前面已经说了,Java的泛型是伪泛型。为什么说Java的泛型是伪泛型呢?因为,在编译期间,所有的泛型信息都会被擦除掉。正确理解泛型概念的首要前提是理解类型擦出(type erasure)。

Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。

如在代码中定义的List<object>和List<String>等类型,在编译后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方法与C++模版机制实现方式之间的重要区别。

可以通过两个简单的例子,来证明java泛型的类型擦除。 案例一:
ArrayList<String> list1 = new ArrayList<String>();ArrayList<Integer> list2 = new ArrayList<Integer>();System.out.println((list1.getClass() == list2.getClass()) + "  " + (list1.getClass() == ArrayList.class));//true  true
在这个例子中,我们定义了两个ArrayList集合,不过一个是ArrayList<String>泛型类型,只能存储字符串。一个是ArrayList<Integer>泛型类型,只能存储整形。最后,我们通过两个ArrayList对象的getClass方法获取它们的类的信息,最后发现两者相等,且等于ArrayList.class。说明泛型类型String和Integer都被擦除掉了,只剩下了原始类型。

案例二:
List<Integer> list = new ArrayList<Integer>();list.add(10086);Method method = list.getClass().getMethod("add", Object.class);//运行时利用反射机制调用集合的add方法,跳过编译时的泛型检查method.invoke(list, "虽然集合中对元素限定的泛型是Integer,但是也能通过反射把字符串添加到集合中");Object object = list.get(1);System.out.println(object.getClass().getSimpleName() + "  " + (object.getClass() == String.class));//String  truetry {    System.out.println(((Object) list.get(1)).getClass());//class java.lang.String    System.out.println(list.get(1).getClass());//如果不指定list.get(1)的类型,则会默认将其强制转换为集合上指定的泛型类型} catch (Exception e) {    e.printStackTrace();//java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer}
因为泛型只在编译的时候起作用,在运行的时候,你得ArrayList已经不受泛型的控制了,也就是说跟已经没有泛型限定的ArrayList没有任何区别了。而反射直接获得了add方法的字节码,跳过编译层在运行时直接添加,这样就骗过了编译。

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

在上面,两次提到了原始类型,什么是原始类型?原始类型(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。 从上面的那个例2中,我们也可以明白ArrayList<Integer>被擦除类型后,原始类型也变成了Object,所以通过反射我们就可以存储字符串了。

如果类型变量有限定,那么原始类型就用第一个边界的类型变量来替换。
比如Pair这样声明:
public class Pair<T extends Comparable& Serializable> { ... } 
那么原始类型就是Comparable
如果Pair这样声明
public class Pair<T extends Serializable & Comparable> 
那么原始类型就用Serializable替换,而编译器在必要的时要向 Comparable 插入强制类型转换。为了提高效率,应该将标签接口(即没有方法的接口)放在边界限定列表的末尾。


要区分原始类型和泛型变量的类型
在调用泛型方法的时候,可以指定泛型,也可以不指定泛型。
  • 在不指定泛型的时候,泛型变量的类型为 该方法中的几种类型的同一个父类的最小级,直到Object。
  • 在指定泛型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类。
public class Test {public static void main(String[] args) {/**不指定泛型的时候,泛型变量的类型为 该方法中的几种类型的同一个父类的最小级,直到Object*/int i = Test.add(1, 2); //这两个参数都是Integer,所以T为Integer类型  Number f = Test.add(1, 1.2);//这两个参数一个是Integer,一个是Float,所以取同一父类的最小级,为NumberObject 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;}}
其实在泛型类中,不指定泛型的时候也差不多,只不过这个时候的泛型类型为Object,就比如ArrayList中,如果不指定泛型,那么这个ArrayList中可以放任意类型的对象。

附加:GenericDeclaration 接口

public interface java.lang.reflect.GenericDeclaration
所有已知实现类:Class、Constructor、Method
声明类型变量的所有实体的公共接口。

可以声明类型变量的实体的公共接口,也就是说,只有实现了该接口才能在对应的实体上声明(定义)类型变量,这些实体目前只有三个:Class、Construstor、Method。注意:因为直接实现子类没有Field类,所以属性上面不能定义类型变量

方法
  • TypeVariable<?>[]  getTypeParameters() 返回声明顺序的 TypeVariable 对象的数组,这些对象表示由此 GenericDeclaration 对象表示的一般声明声明的类型变量。
    • 返回:表示由此一般声明声明的类型变量的 TypeVariable 对象的数组
    • 如果底层的一般声明未声明任何类型变量,则返回一个 0 长度的数组。
public static <T extends Person, U> void main(String[] args) throws Exception {    Method method = Test.class.getMethod("main", String[].class);    TypeVariable<?>[] tvs = method.getTypeParameters();//返回声明顺序的 TypeVariable 对象的数组    System.out.println("声明的类型变量有:" + Arrays.toString(tvs));//[T, U]    for (int i = 0; i < tvs.length; i++) {        GenericDeclaration gd = tvs[i].getGenericDeclaration();        System.out.println("【GenericDeclaration】" + gd);//public static void com.bqt.Test.main(java.lang.String[]) throws java.lang.Exception        System.out.println(gd.getTypeParameters()[i] == tvs[i]);//true。    GenericDeclaration和TypeVariable两者相互持有对方的引用        System.out.println(tvs[i] + "  " + tvs[i].getName() + "  " + Arrays.toString(tvs[i].getBounds()));//T  T  [class com.bqt.Person] 和 U  U  [class java.lang.Object]    }}
2017-9-4

来自为知笔记(Wiz)