1.简述
泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
泛型的意义:
- 封装及代码复用,适用于多种数据类型执行相同的代码(比如Request,Response),这样就可以通过对外开放相同的接口来完成对一组类的操作。
- 在编译时期进行类型安全检查 (比如ArrayList)。
- 控制数据安全访问,即PECS法则。
常用的泛型类型变量:
- E:元素(Element)或者异常(Exception),多用于java集合框架。
- K:关键字(Key)。
- N:数字(Number)。
- T:类型(Type)。
- V:值(Value),通常与 K 一起配合使用。
- ?:不确定的Java类型。
注意:Java的泛型是伪泛型,在编译期间,所有的泛型信息都会被擦除掉。
2.泛型类
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。
声明泛型类的语法:
class className<type-param-list> {}
泛型类示例代码如下:
public class Test { public static void main(String[] args) { Info<Integer> integerInfo = new Info<Integer>(); Info<String> stringInfo = new Info<String>(); integerInfo.add(new Integer(10)); stringInfo.add(new String("Hello World")); System.out.println("Integer Value :"+ integerInfo.get()); System.out.println("String Value :"+ stringInfo.get()); } } //泛型类 class Info<T> { private T t; public void add(T t) { this.t = t; } public T get() { return t; } }
3.泛型接口
声明泛型接口的语法:
interface interfaceName<type-param-list> {}
泛型接口示例代码如下:
public class Test { public static void main(String[] args) { Info<Integer> integerInfo = new Info<Integer>(); Info<String> stringInfo = new Info<String>(); integerInfo.add(new Integer(10)); stringInfo.add(new String("Hello World")); System.out.print("Integer Value :"); integerInfo.PrinterInfo(); System.out.print("String Value :"); stringInfo.PrinterInfo(); } } //泛型接口 interface infoInterface<T> { void PrinterInfo(); } //实现泛型接口 class Info<T> implements infoInterface<T> { private T t; public void add(T t) { this.t = t; } public T get() { return t; } @Override public void PrinterInfo() { System.out.println(get()); } }
注意:如果一个类实现了一个泛型接口,那么该类也必须是泛型的。
4.泛型方法
声明泛型方法的语法:
[访问权限修饰符][static][final]<类型参数列表>返回值类型方法名([形式参数列表]) public static List<T> find(Class<T>class,int userId){}
泛型方法示例代码如下:
public class Test { public static void main(String[] args) { List<String> stringlist = new ArrayList<String>(); stringlist.add("A");stringlist.add("B");stringlist.add("C"); List<Integer> integerlist = new ArrayList<Integer>(); integerlist.add(1);integerlist.add(2);integerlist.add(3); printInfo(stringlist); printInfo(integerlist); } public static void printInfo(List<?> datas) { // 定义泛型方法 for (int i = 0; i < datas.size(); i++) { System.out.println(datas.get(i)); } } }
该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
5.泛型的高级用法
(1)限制泛型可用类型
Java中默认可以使用任何类型来实例化一个泛型类对象、实现泛型接口、定义泛型方法,当然也可以对泛型类实例、接口、方法的类型进行限制。
语法格式:
class className<T extends anyClass>{} interface interfaceName<T extends anyClass>{} [访问权限修饰符][static][final]<类型参数列表>返回值类型方法名([形式参数列表]) public static List<? extends anyClass> find(List<? extends anyClass> list,int userId){}
anyClass指某个接口或类。使用泛型限制后,泛型类的类型必须实现或继承anyClass这个接口或类。无论anyClass是接口还是类,在进行泛型限制时都必须使用extends关键字。
示例代码如下:
public class Test <T extends List> { public static void main(String[] args) { // 实例化使用ArrayList的泛型类ListClass,正确 Test<ArrayList> lc1 = new Test<ArrayList>(); // 实例化使用LinkedList的泛型类LlstClass,正确 Test<LinkedList> lc2 = new Test<LinkedList>(); // 实例化使用HashMap的泛型类ListClass,错误,因为HasMap没有实现List接口 // Test<HashMap> lc3=new Test<HashMap>(); } }
当没有使用extends关键字限制泛型类型时,其实是默认使用Object类作为泛型类型。
(2)类型通配符
Java中的泛型还支持使用类型通配符,它的作用是在创建一个泛型类对象时限制这个泛型类的类型必须实现或继承某个接口或类。
语法格式:
泛型类名称<? extends List>a = null;
"<? extends List>"作为一个整体表示类型未知,当需要使用泛型对象时,可以单独实例化。
示例代码如下:
A<? extends List>a = null; //ArrayList类实现了List接口,所以正常 a = new A<ArrayList> (); //LinkedList类实现了List接口,所以正常 b = new A<LinkedList> (); //HashMap类没有实现List接口,所以会报错 c = new A<HashMap> ();
(3)继承泛型类和实现泛型接口
定义为泛型的类和接口也可以被继承和实现。
继承泛型类的示例代码如下:
public class FatherClass<T1>{} public class SonClass<T1,T2,T3> extents FatherClass<T1>{}
SonClass类继承FatherClass类时保留父类的泛型类型,需要在继承时指定,否则直接使用extends FatherClass语句进行继承操作,此时T1、T2 和 T3都会自动变为Object,所以一般情况下都将父类的泛型类型保留。
实现泛型接口的示例代码如下:
interface interface1<T1>{} interface SubClass<T1,T2,T3> implements interface1<T1>{}
6.类型擦除
(1)类型擦除说明
Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。
示例1:
public class Test2 { public static void main(String[] args) { List<String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); System.out.println(l1.getClass() == l2.getClass()); } }
上面的例子中,定义了两个ArrayList数组,不过一个是ArrayList<String>泛型类型的,只能存储字符串。一个是ArrayList<Integer>泛型类型的,只能存储整数,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下原始类型。
示例2:
public class Test { public static void main(String[] args) throws Exception{ List<Integer> l1 = new ArrayList<Integer>(); l1.add(1); l1.getClass().getMethod("add", Object.class).invoke(l1, "A"); for (int i = 0; i < l1.size(); i++) { System.out.println(l1.get(i)); } } }
上面的例子中,定义了一个ArrayList泛型类型实例化为Integer对象,如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型。
(2)类型擦除后保留的原始类型
原始类型就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。
示例1:
class Pair<T> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } }
因为在Pair<T>中,T是一个无限定的类型变量,所以用Object替换,其结果就是一个普通的类,如同泛型加入Java语言之前的已经实现的样子。在程序中可以包含不同类型的Pair,如Pair<String>或Pair<Integer>,但是擦除类型后他们的就成为原始的Pair类型了,原始类型都是Object。
示例2:
public class Pair<T extends Comparable> {}
如果类型变量有限定,那么原始类型就用第一个边界的类型变量类替换。上面示例中的原始类型就是Comparable。
在调用泛型方法时,可以指定泛型,也可以不指定泛型:
- 在不指定泛型的情况下,泛型变量的类型为该方法中的几种类型的同一父类的最小级,直到Object。
- 在指定泛型的情况下,该方法的几种类型必须是该泛型的实例的类型或者其子类。
(3)类型擦除引起的问题及解决方法
因为Java不能实现真正的泛型,只能使用类型擦除来实现伪泛型,这样虽然不会有类型膨胀问题,但是也引起来许多新问题,所以SUN对这些问题做出了种种限制,避免我们发生各种错误。
(1)编译的对象和引用传递问题
Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。
示例1:
public class Test { public static void main(String[] args) throws Exception{ List<String> list = new ArrayList(); list.add("1"); //编译通过 list.add(1); //编译错误 String str1 = list.get(0); //返回类型就是String List list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String } }
上面的例子,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
(2)自动类型转换
ArrayList.get()方法示例:
public E get(int index) { RangeCheck(index); return (E) elementData[index]; }
可以看到,在return之前,会根据泛型变量进行强转。假设泛型类型变量为String,虽然泛型信息会被擦除掉,但是会将(E) elementData[index],编译为(String)elementData[index]。所以我们不用自己进行强转。当存取一个泛型域时也会自动插入强制类型转换。
(3)类型擦除与多态的冲突和解决方法
示例1:
public class Test { public static void main(String[] args) throws Exception{ StringInter dateInter = new StringInter(); dateInter.setValue(new String()); dateInter.setValue(new Object()); //编译错误 } } class Pair<T> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } } class StringInter extends Pair<String> { @Override public void setValue(String value) { super.setValue(value); } @Override public String getValue() { return super.getValue(); } }
由于种种原因,虚拟机并不能将泛型类型变为String,只能将类型擦除掉,变为原始类型Object。我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。类型擦除就和多态有了冲突。那我们怎么去重写我们想要的String类型参数的方法啊。于是JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法来实现。
(4)泛型类型变量不能是基本数据类型
不能用类型参数替换基本类型。就比如没有List<int>
,只有List<Integer>。因为当类型擦除后,List的原始类型变为Object,但是Object类型不能存储int值,只能引用Integer的值。
(5)运行时类型查询
示例1:
public class Test { public static void main(String[] args) throws Exception{ ArrayList<String> list = new ArrayList<String>(); if(list instanceof ArrayList<String>){ System.out.println("111"); } } }
因为类型擦除之后,ArrayList只剩下原始类型,泛型信息String不存在了。运行时进行类型查询的时候使用下面的方法是错误的。
(6)泛型在静态方法和静态类中的问题
泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数。
示例1:
class Test<T> { public T one; //编译错误 public static T show(T one){ //编译错误 return null; } }
因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。
示例2:
class Test<T> { public T one; //编译错误 public static <T> T show(T one){ //编译错误 return null; } }
因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的 T,而不是泛型类中的T,所以不会报错。