Java泛型的介绍与使用

来源:互联网 发布:烈焰战车10级升级数据 编辑:程序博客网 时间:2024/05/21 05:43

一、基本概念


    “泛型编程”这个概念最早就是来源于C++当初设计STL时所引入的模板(Template),而为什么要引入模板呢,因为STL要完成这样一个目标:设计一套通用的,不依赖类型的,高效的的算法(例如std::sort)和数据结构(例如std::list)。关于通用性,运行时多态可以做到(例如很多高级语言的继承(机制,接口机制),但是C++作为一门相对底层的语言,对运行效率的要求是很严格的,而运行时多态会影响效率(例如成员函数只有在运行时才知道调用哪个),所以设计STL的人就创造了一种编译时多态技术,即模板

    那什么又是编译时多态呢,简单点说就是让编译器帮我确定类型,我写程序时只要标记下这里我要用“某种类型”的对象,至于具体是什么类型我不关心,编译器帮我确定,编译完成后,在运行时类型是绝对确定的,这样就大大提高了运行效率,反之对编译器来说,就增加了很多工作,而且生成的目标代码也会大大增加。所以对C++来说,所谓“泛型(Generics)”,并不是说编译器不知道类型,而是针对程序员来说的,这也正是通用性的体现。


二、为什么Java需要泛型


        首先,我们看下下面这段简短的代码:

public class GenericTest {public static void main(String[] args) {List list = new ArrayList();list.add("qqyumidi");list.add("corn");list.add(100);for (int i = 0; i < list.size(); i++) {String name = (String) list.get(i);// 1System.out.println("name:" + name);}}}

    代码定义了一个List类型的集合,先向其中加入了两个String类型的值,随后加入一个Integer类型的值。这是完全允许的,因为此时list默认的类型为Object类型。在之后的循环中,由于忘记了之前在list中加入了Integer类型的值或其他编码原因,很容易出现类似于//1中的错误。这段语句在编译阶段正常,而运行时会出现“Java.lang.ClassCastException”异常。因此,导致此类错误编码过程中不易发现。在如上的编码过程中,我们发现主要存在两个问题:

    1)当我们将一个对象放入集合中,集合不会记住此对象的类型,当再次从集合中取出此对象时,该对象的编译类型变成了Object类型但运行时类型转换为其本身类型

    2)取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现“Java.lang.ClassCastException”异常。

    那么有没有什么办法可以使集合能够记住集合内元素各类型,且能够达到只要编译时不出现问题,运行时就不会出现Java.lang.ClassCastException”异常呢?答案就是使用泛型。


三、Java泛型怎么用


(1)定义泛型类

    我们定义一个Person1类,包含三个属性xyz。在我们开始定义地时候,我们也不知道这三个属性是用来干什么的,所以我们定义为Object类型。但是在使用的时候,我们分别对xyz赋予了intdoubleString类型,所以在取出的时候,我们需要把这三个类型值进行强制转换。同时,再定义一个泛型类Person2,定义三个属性xyz,在测试类中,我们设置属性的值,并打印属性的值,代码如下:

class Person1 {private Object x;private Object y;private Object z;//使用Object类型。可以转化为任何类型public Object getX() {return x;}public void setX(Object x) {this.x = x;}public Object getY() {return y;}public void setY(Object y) {this.y = y;}public Object getZ() {return z;}public void setZ(Object z) {this.z = z;}}class Person2<T> {private T x;private T y;private T z;public T getX() {return x;}public void setX(T x) {this.x = x;}public T getY() {return y;}public void setY(T y) {this.y = y;}public T getZ() {return z;}public void setZ(T z) {this.z = z;}}public class PersonTest {public static void main(String[]args){Person1 boy1 = new Person1();boy1.setX(20);boy1.setY(22.2);boy1.setZ("帅哥TT");//取出属性值时,需要进行类型转化System.out.println((Integer)boy1.getX());System.out.println((Double)boy1.getY());System.out.println((String)boy1.getZ());//下面是使用泛型后的代码Person2 boy2=new Person2();boy2.setX(20);boy2.setY(22.2);boy2.setZ("帅哥TT");//这里程序员不需要手工类型转化//但实际上,由于类型擦除,类型转换将由编译器自动执行System.out.println(boy2.getX());System.out.println(boy2.getY());System.out.println(boy2.getZ());}}

(2)类型变量的限定

    如下代码,我们在方法min中定义了一个变量smallest类型为T,这说明了smallest可以是任何一个类的对象,我们在下面的代码中需要使用compareTo方法, 但是我们没有办法确定我们的T中含有CompareTo方法,所以我们需要对T进行限定,在代码中我们让T继承Comparable类。如下:

public static<T extends Comparable>T min(T[]a)//泛型方法

    测试代码:
public class Person{public static<T extends Comparable>T min(T[]a){if(a==null||a.length==0){return null;}T smallest=a[0];for(int i=1;i<a.length;i++){if(smallest.compareTo(a[i])>0){smallest=a[i];}}return smallest;}public static void main(String [] args){Integer[]num={20,25,30,10};Integer smallest =Person.<Integer>min(num);System.out.println(smallest);}}

(3)定义泛型接口

public interface Person<T1,T2> {public T1 getX();public T2 getY();}

(4)定义泛型方法

    必须在方法的返回值前边加一个<T>,来声明这是一个泛型方法,持有一个泛型T,然后才可以用泛型T作为方法的返回值。

public class Person{public static<T>T getMiddle(T[]a){return a[a.length >> 1];}public static void main(String [] args){String[]name={"帅哥TT","帅哥TT1","帅哥TT2"};String middle=Person.<String>getMiddle(name);System.out.println(middle);Integer[]num={20,22,25};Integer middle1=Person.<Integer>getMiddle(num);System.out.println(middle1);}}

(5)类型通配符

    我们知道,由于类型擦除,Box<Number>Box<Integer>实际上都是Box类型,ArrayList<Manager>ArrayList<Employee>实际上都是ArrayList类型,现在需要继续探讨一个问题,那么在逻辑上,类似于Box<Number>Box<Integer>ArrayList<Manager>ArrayList<Employee>是否可以看成具有父子关系的泛型类型呢?

    为了弄清这个问题,我们继续看下面这个例子:

class Box<T> {private T data;public Box(T data){this.data = data;}public T getData(){return data;}}public class BoxTest {public static void main(String[] args){Box<Number> name = new Box<Number>(99);Box<Integer> age = new Box<Integer>(712);System.out.println("name class:" + name.getClass());System.out.println("age class:" + age.getClass());System.out.println("Box class:" + Box.class);getData(name);getData(age);//1编译错误}public static void getData(Box<Number> data){System.out.println("data:" + data.getData());}}

    这个例子中,显然//1处会出现错误提示的。我们知道Box<Number>逻辑上不能视为Box<Integer>的父类。那么如何解决呢?总不能再定义一个新的函数吧。这和Java中的多态理念显然是违背的,因此,我们需要一个在逻辑上可以用来表示同时是Box<Integer>Box<Number>的父类的一个引用类型,由此,类型通配符应运而生。

    类型通配符一般是使用?代替具体的类型实参。注意了,此处是类型实参,而不是类型形参!且Box<?>在逻辑上是Box<Integer>Box<Number>...等所有Box<具体类型实参>的父类。由此,我们将getData函数的代码修改成如下即可。

public static void getData(Box<?> data){System.out.println("data:" + data.getData());}

    有时候,我们还可能听到类型通配符上限和类型通配符下限。具体有是怎么样的呢?在上面的例子中,如果需要定义一个功能类似于getData()的方法,但对类型实参又有进一步的限制只能是Number类及其子类。此时,需要用到类型通配符上限。

public static void getUpperData(Box<? extends Number> data){System.out.println("data:" + data.getData());}

    类型通配符上限通过形如Box<? extends Number>形式定义,相对应的,类型通配符下限为Box<? super Number>形式,其含义与类型通配符上限正好相反,在此不作过多阐述了。


(6)类型擦除

    Java中的泛型与C++泛型(模板)有很大不同,那就是“类型擦除”。Java中的类型擦除并不是一个语言特性。泛型不是Java语言出现时就有的组成部分,擦除只是Java的泛型实现中的一种折中。《Thinking in Java》一书中的例子很好的展示了擦除是什么:

import java.util.*;public class Erase {public static void main(String[] args) {// TODO code application logic hereClass c1 = new ArrayList<String>().getClass();Class c2 = new ArrayList<Integer>().getClass();System.out.println(c1 == c2);}}

    结果输出是true

    ArrayList<String>和ArrayList<Interger>很容易认为是不同的类型。不同的类型在行为方面肯定不同,但上面的程序认为它们是相同的类型。原因就是Java的泛型是使用擦除来实现的,这意味着当你使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此List<String>和List<Interger>在运行时事实上是相同的类型,这两种形式都被擦除成它们的“原生”类型,即List。类型擦除导致的残酷现实是:在泛型代码内部,无法获得任何有关泛型参数类型的信息(Thinking in Java)。擦除丢失了在泛型代码中执行某些操作的能力,任何在运行时需要知道确切类型信息的操作都将无法工作。

public class Erased<T> {private static  final int SIZE = 100;public void test(){//编译错误1if(arg instanceof T) {}  //编译错误2T var = new T();      //编译错误3T[] array = new T[SIZE];      // 编译会发生警告T[] array1 = (T[])new Object[SIZE];}} 

    现在我们都知道擦除会丢失了在泛型代码中执行某些操作的能力,这将会使任何确切类型信息的操作都将会无法工作,但有时必须通过引入类型标签对擦除进行补偿,这意味着你需要显示的传递你的类型的Class对象,以便你可以在类型表达式中使用它。 

    对于以上代码中的“编译错误1”,解决方案如下:

public class ClassTypeCapture<T> {Class<T> kind;public ClassTypeCapture(Class<T> kind) {this.kind = kind;}public boolean f(Object arg) {//只要对象是kind的自身实例或其子类实例,就返回truereturn kind.isInstance(arg);} public static void main(String[] args) {ClassTypeCapture<String> ctt1 = new ClassTypeCapture<String>(String.class);System.out.println(ctt1.f(new String()));System.out.println(ctt1.f(new Integer(2)));ClassTypeCapture<Integer> ctt2 = new ClassTypeCapture<Integer>(Integer.class);System.out.println(ctt2.f(new String()));System.out.println(ctt2.f(new Integer(2)));}}

    运行结果如下:

truefalsefalsetrue

    “编译错误2”表明创建一个new T()的尝试将无法实现,部分原因是因为擦除,另一部分原因是因为编译器不能验证T具有默认(无参)构造函数。但是在C++中,这种操作很自然、很直观,并且很安全(它是在编译期受到检查的)。Java中的解决方案是传递一个工厂对象,并使用它来创建新的实例。最便利的工厂对象就是Class对象,因此如果使用类型标签,那么就可以使用newInstance()来创建这个类型的新对象:

class ClassAsFactory<T>{T x;public ClassAsFactory(Class<T> kind){try{x = kind.newInstance();} catch(Exception e) {throw new RuntimeException(e);}}}class Employee {}public class InstantiateGenericType{public void main(String[] args){ClassAsFactory<Employee> fe = new ClassAsFactory<Employee>(Employee.class);print(“ClassAsFactory<Employee> succeeded”);try{ClassAsFactory<Integer> fi = new ClassAsFactory<Integer>(Integer.class);} catch(Exception e) {print(“ClassAsFactory<Integer> failed”);}}}
    运行结果如下:

ClassAsFactory<Employee> succeededClassAsFactory<Integer> failed

    因为Integer没有任何默认的构造器,这个错误是在运行期捕获的。

    “编译错误3”表明不能创建泛型数组,一般的解决方案是在任何想要创建泛型数组的地方都使用ArrayList:

import java.util.*;public class ListOfGenerics<T> {private List<T> array = new ArrayList<T>();public void add(T item) { array.add(item); }public T get(int index) { return array.get(index); }}

    看一个更复杂的示例。考虑一个传递一个类型标记的泛型数组包装器:

public class GenericArrayWithTypeToken<T> {private T[] array;@SuppressWarnings("unchecked")public GenericArrayWithTypeToken(Class<T> type, int sz) {array = (T[])Array.newInstance(type, sz);}public void put(int index, T item) {array[index] = item;}public T get(int index) { return array[index]; }public T[] rep() { return array; }    public static void main(String[] args) {GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<Integer>(Integer.class, 10);Integer[] ia = gai.rep();}} 
    类型标记Class<T>被传递到构造器中,以便从擦除中恢复,使得我们可以创建需要的实际类型的数组,尽管从转型中产生的警告必须用@Suppresswarnings压制住。一旦我们获得了实际类型。就可以返回它,并获得想要的结果,就像在main()中看到的那样。


四、Java泛型与C++模板的区别


    C++的模板会对针对不同的模板参数静态实例化,目标代码体积会稍大一些,运行速度会快很多

    C++ template是具体化的泛型,Java的泛型靠的是类型擦除,目标代码只会生成一份,牺牲的是运行速度。

    C++在运行时没有额外的开销,Java运行时有类型转换的开销

    C++的每个具体的泛型类型都有一份独立的代码,Java只有一份类型擦除之后的代码,这意味着C++ template编译后会产生代码膨胀而Java泛型不会。

    C++的类型检查是在编译时做的,Java的类型检查在编译期和运行期都要做一些工作。

    总的来说C++template会生成更大的二进制代码,但会执行的比较快,但大个的二进制代码可能会导致更多的I/O,所以也不一定完全是优势。

    Java生成的代码只有一份,运行时会有一些类型转换的开销,但可以在运行时支持新类型,比如用ClassLoader动态加载进来的类。

    Java泛型的实现植根于“类型擦除”这一概念。当源代码被转换成Java虚拟机字节码时,这种技术会消除参数化类型。例如,假设有如下Java代码:

Vector<String> vec = new Vector<String>();vec.add(new String(“hello”));String str = vec.get(0);

    编译时,上面的代码会被改写为:

Vector vec = new Vector();vec.add(new String("hello"));String str = (String)vec.get(0);//编译器自动加上类型转换

    有了Java泛型,我们可以做的事情也没有真正改变多少;它只是让代码变得漂亮些。这点跟C++截然不同。在C++中,模板本质上是一套宏指令集,只是换了个名头,编译器会针对每种类型创建一份模板代码的副本。有项证据可以证明这一点:MyClass<Foo>不会与MyClass<Bar>共享静态变量。然而,两个MyClass<Foo>实例则会共享静态变量。看了下面的代码,应该会更清楚一点:

/*MyClass.h*/template<typename T>class MyClass{public:static int val;MyClass() {}static void increment(){val += 1;}};template<typename T>int MyClass<T>::val = 0;/*main.cpp*/int main(void){MyClass<int> foo1;MyClass<int> foo2 ;foo1.increment();foo2.increment();cout << "foo1.val = " << foo1.val << endl;//2cout << "foo2.val = " << foo2.val << endl;//2MyClass<double> *bar1 = new MyClass<double>();MyClass<double> *bar2 = new MyClass<double>();bar1->increment();bar2->increment();cout << "barr->val = " << bar1->val << endl;//2cout << "bar2->val = " << bar2->val << endl;//2return 0;}

    JavaMyClass类的静态变量会由所有MyClass实例共享,不论类型参数相同与否。由于架构设计上的差异,在Java中,不管类型参数是什么MyClass的所有实例都是同一类型,类型参数不能用于静态方法和变量,因为它们会被MyClass<Foo>MyClass<Bar>所共享。在C++中,这些类都是不同的,因此类型参数可以用于静态方法与静态变量。

    C++模板中,编译器根据程序员提供的类型参数来扩充模板,因此,为List<A>生成的C++代码不同于为List<B>生成的代码,List<A>List<B>实际上是两个不同的类。而Java中的泛型是用类型擦除实现的语法糖,编译器仅仅对这些类型参数进行擦除和替换。类型ArrayList<Integer>ArrayList<String>的对象共享相同的类,并且只存在一个ArrayList,在编译期类型检查以外,生成目标代码的过程中根本不区分泛型的类型参数。因此在C++中存在为每个模板的实例化产生不同的类型,这一现象被称为“模板代码膨胀”,而Java则不存在这个问题的困扰。Java中虚拟机中没有泛型,只有基本类型和类类型,泛型会被擦除,一般会修改为Object,如果有限制,例如T extends Comparable,则会被修改为Comparable,同时在必要处插入从Object或者Comparable到给定的类型参数的类型转换而已(因此类型参数不能是int这样的基本类型,C++模板就没这个限制)。而在C++中不能对模板参数的类型加以限制,如果程序员用一个不适当的类型实例化一个模板,将会在模板代码中报告一个错误信息。

    C++的模板在刚出来的时候并没有想到会演化成今天这样,其他高级语言(如JavaC#)也是看到C++模板在使用的时候带给了程序员极大的便利,就考虑支持这样一种功能,但是也仅仅是借用了C++的模板理念,而没有完全照抄模板的实现方法,所以对于大部分程序员来说,只要使用起来差不多,并不关心实现。

    最后总结下,泛型是只是一个概念,具体实现有C++的模板,Java的泛型等,但实现方法大不相同,只是提供给语言使用者相同的使用方法而已。


五、Java泛型总结


    正确理解Java泛型概念的首要前提是理解类型擦除(type erasure)。Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉,这个过程就称为类型擦除。如在代码中定义的List<Object>List<String>等类型,在编译之后都会变成ListJVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。

    很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:

    1)泛型类并没有自己独有的Class类对象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class

    2)静态变量是被泛型类的所有实例所共享的。对于声明为MyClass<T>的类,访问其中的静态变量的方法仍然是MyClass.myStaticVar。不管是通过new MyClass<String>还是new MyClass<Integer>创建的对象,都是共享一个静态变量。

    3泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException<String>MyException<Integer>的。对于JVM来说,它们都是MyException类型的,也就无法执行与异常对应的catch语句。

    4)由于擦除丢失了在泛型代码中执行某些操作的能力。任何在运行时需要知道确切类型信息的操作将无法工作。

    5Java的泛型是停留在编译层的,也就是说JVM在对待泛型数据的时候,依然会把它们看成是Object类型。只不过在使用这些元素的时候,JVM会自动帮助开发者进行相应的类型转换

    在使用Java泛型的时候可以遵循一些基本的原则,从而避免一些问题。

    1)在代码中避免泛型类和原始类型的混用。比如List<String>List不应该共同使用。这样会产生一些编译器警告和潜在的运行时异常。当需要利用JDK 5之前开发的遗留代码,而不得不这么做时,也尽可能的隔离相关的代码。

    2)在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念。由于具体的类型是未知的,很多操作是不允许的。

    3)泛型类最好不要同数组一块使用。你只能创建new List<?>[10]这样的数组,无法创建new List<String>[10]这样的。这限制了数组的使用能力,而且会带来很多费解的问题。因此,当需要类似数组的功能时候,使用容器类即可

    4)不要忽视编译器给出的警告信息。

    5)不能用基本类型实例化类型参数。因此没有Pair<double>,只有Pair<Double>。当然其原因是类型擦除。擦除之后,Pair类含有Object类型的域,而Object不能存储double值。

0 0