java泛型详解
目录
一、泛型类
二、泛型方法
三、类型变量限定
四、类型擦除
4.1与4.2:虚拟机中没有泛型
4.3:翻译泛型表达式
五、继承规则(通配符的上下限)
捕获通配符。
六、约束与局限性
大部分的泛型文章只涉及到泛型类与泛型方法等一些部分,在这一篇文章中,尽量对泛型有一个详细全面的基础描述,介绍实现自己的泛型需要了解的各种知识。
为什么使用泛型?
泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。使用泛型机制编写的程序代码要比那些杂乱地使用Object变量,然后再进行类型强制转换的代码具有更好的安全性和可读性。泛型程序设计意味着代码可以被很多不同类型的对象所重用。
一、泛型类
一个泛型类就是一个具有一个或多个类型变量的类。
例如,定义一个名为Pair的泛型类
public class Pair<T>{private T first;private T second;}
Pair类引入一个类型变量T,用<>括起来,并放在类型后面。
例如,当你要声明一个Strng类型变量的Pair类时可以:
Pair<String> p;
代表:
public class Pair<String>{private Sting first;private Sting second;}
同样,当first与second的类型不一样时,也可以设计如下一个泛型类。
public class Pair<T,U>{private T first;private U second;}
声明时,Pair<Sting,Date>p;
注:在java库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字与值,T(有时使用U和S)表示“任意类型”
二、泛型方法
除了定义一个泛型类之外,我们也可以定义一个泛型方法。泛型方法可以定义在普通类与泛型类当中。
在Pair类原有的基础上,我们再增加getter与setter方法。
public class Pair<T>{private T first;private T second;public <T> T getFirst(){return first;}public <T> setFirst(T first){this.first = first;}}
在泛型方法中,类型参数<T>放在修饰符(此处是public)后面,返回类型前面(此处是T)。
三、类型变量限定
有时候,我们需要对类型T做一些限定,比如传入的类型需要是能够进行比较排序的类型。
例如,我们定义一个找出最小值的泛型方法min,类型T是继承了Comparable接口的类型。
class sort{public static <T extendsComparable> T min(T[] a){T smallest = a[0];for(int i = 0;i < a.length;i++){//如果T不限定成是继承了comparable接口的类,则不能够保证传入的类型变量拥有compareTo方法,在程序运行时容易出错。If(smallest.compareTo(T[i]) > 0)Smallest = T[i];}}}
在java继承中,可以根据需要限定多个接口超类型,但最多限定一个类,限定类型用“&”分隔。
例如限定一个继承Comparable接口、Serializable接口与Pair类接口的子类类型,则min方法应该这么定义:
public static <T extends Pair & Comparable & Serializable>T min(T[] a){…}
注意:限定类时,类应该放在限定列表的第一个
四、类型擦除
首先,记住以下四点:
1、虚拟机(JVM)中,没有泛型,只有普通的类与方法;
2、所有的类型参数都用它们的限定类型替换;
3、为保持类型安全性,必要时插入强制类型转换(虚拟机自动完成此项工作);
4、桥方法被合成来保持多态(虚拟机自动完成此项工作);
下面将分别阐述以上四点
4.1与4.2:虚拟机中没有泛型
在虚拟机中,所有的类与方法都是普通类。无论何时定义一个泛型,在虚拟机中,都自动提供一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数(<T>)后的泛型类型名。
例如前面的Pair<T>的原始类型名就是Pair。而类型变量T 就会被擦除,替换成Object。
public class Pair<T>{private T first;private T second;}
类型擦除后:
public class Pair{private Object first;private Object second;}
当T是有单个或者多个限定类型时,T被替换成限定列表的第一个:
class sort{
public static <T extendsComparable> T min(T[] a)
{
T smallest = a[0];
for(int i = 0;i < a.length;i++)
{
If(smallest.compareTo(T[i]) > 0)
Smallest = T[i];
}
}
}
类型擦除后:
class sort{
public static Comparable min(Comparable [] a)
{
Comparable smallest = a[0];
for(int i = 0;i < a.length;i++)
{
If(smallest.compareTo(Comparable [i]) > 0)
Smallest = [i];
}
}
}
4.3:翻译泛型表达式
给出如下的一个泛型类
class Pair<T>{
private Tfirst;
private T second;
public Pair(T first,T second)
{
this.first = first;
this.second = second;
}
public T getFIrst()
{
return first;
}
}
当类型擦除后变成了
class Pair{
private Objectfirst;
private Objectsecond;
public Pair(Object first,Object second)
{
this.first = first;
this.second = second;
}
public Object getFIrst()
{
return first;
}
}
考虑如下代码:
Pair<String> p = new Pair(“hello”,”world”);
String p1 = p.getFirst();
记住,由于类型擦除,Pair类里面的getFirst
public Object getFIrst()
{
return first;
}
返回的是Object对象,编译器自动插入String的强制类型转换,类似于
String p1 = (Strng)p.getFirst();
4.4、桥方法
桥方法的存在是为了解决泛型方法类型擦除后产生的问题。
假设有一个类继承自Pair<String>
class PairChild extends Pair<String>{
public void setFirst(String first)
{
//如果first是字符串“hello”,则调用父类的setFirst()方法
If(first == “hello”)
super.setFirst(first);
}
}
这段代码在类型擦除后变成了:
class PairChild extends Pair{
//注意此处的setFirst(String first)方法
public void setFirst(String first)
{
//如果first是字符串“hello”,则调用父类的setFirst()方法
If(first == “hello”)
super.setFirst(first);
}
}
此时注意父类Pair
public class Pair<T>
{
private T first;
private T second;
public <T> setFirst(T first){
this.first = first;
}
}
类型擦除后变成:
public class Pair
{
private Objectfirst;
private Object second;
public void setFirst(Objectsecond){…}
}
这里有一个public voidsetFirst(Object second){…}方法。
子类PairChild继承了父类的public void setFirst(Object second)方法,同时自身又有一个public void setFirst(String first),此时,setFirst方法有了多态。
那么当有以下代码时:
PairChlid pc = new PairChild();
pc.setFirst(“world”);
由于拥有父类参数的方法能传入子类,此时的pc.setFirst(“world”)应该调用public void setFirst(Object second)还是publicvoid setFirst(String first)方法呢?
为了避免这种疑惑,虚拟机在PairChlid类中生成一个桥方法
class PairChild extends Pair<String>{
//覆盖了父类的setFirst(Object first)方法的同时,调用了setFirst(String first)方法
public void setFirst(Object first){
setFirst((String)first);
}
public void setFirst(String first)
{
//如果first是字符串“hello”,则调用父类的setFirst()方法
If(first == “hello”)
super.setFirst(first);
}
}
五、继承规则(通配符的上下限)
可以说,通配符的存在是为了解决泛型中的继承问题。
假设有一个父类Father和一个子类child
有一个方法public static void min(father f){..}
此时这样调用方法
Child c = new Child();
min(c);//编译通过
但是如果是泛型方法:public static voidmin(Pair<father> f){..}
这样调用
Pair<child> c = new Pair<child>();
min(c);//编译错误
Child是Father的子类,但是Pair<Child>不是Pair<Father>的子类。
要解决这一点很简单,引入一个通配符“?”。
泛型方法min可以这样设计:
public static void min(Pair<? extends Father> f){..}
表示传入的参数类型“?”是Father的某个子类类型或者Father本身
此时
Pair<child> c = new Pair<child>();
min(c);//编译通过
另外一种情况,当传入的参数类型不要求是father的子类而要求是father类的父类时,泛型方法可以这样定义:
public static void min(Pair<? extends Father> f){..}
表示传入的类型参数“?”应该是father的父类。
如果传入的类型参数无类型要求时,min可以这样定义:
public static void min(Pair<? > f){..}
表示任何的Pair参数对象都可以传递进min方法。
注:可能会有人疑惑参数类型T与通配符?有何不同?T一般是在定义泛型类与泛型方法时使用,并且没有参数类型是继承自…或者是…的父类的意思,与?并不相同,?也不是类型变量。
当然 public static <T> void min(pair<T> f){…} 与public static void min(Pair<? > f){..}作用相同。
捕获通配符。
考虑有一个swap方法用于交换Pair类型的first与second属性,怎么交换?。
public static void swap(Pair<? > f){
.. ? temp = f.getFirst();//编译错误,?并不是类型变量
}
此时应该用T先写一个方法swapTemp来捕获通配符。
public static <T> void swapTemp(Pair<T> f){
.. T temp = f.getFirst();
f.setFirst(f.getSecond());
f.setSecond(temp);
}
再将swap改写成:
public static void swap(Pair<? > f){
.. swapTemp (f);
}
六、约束与局限性
终于来到最后一个部分,这里简单阐述一下泛型的一些局限。
1、不能用基本类型实例化类型参数,没有Pair<int>。
2、运行时的类型检查只适应于原始类型查询。(原因参见类型擦除的4.1)
例如:Pair<String>s = new Pair<String>();
s instanceof Pair<String> 是错误的.
无论何种Pair,只能使用s instanceof Pair。GetClass方法返回的也是Pair。
3、不能创建参数化类型的数组。例如:Pair<String>[] ss = new Pair<String>[10]是错误的。当然,声明Pair<String>[] ss是可以的,只不过不能用new初始化。
4、不能实例化类型变量。如new T(…)是错误的,一般用getter与setter设置泛型属性。
5、禁止使用带有类型变量的静态域与方法。
如public static T first 与publicstatic T getFirst()是禁止的。
6、注意类型擦除后的冲突
参考:《java核心技术卷I》