基本概念
通常情况下集合可以存放不同类型的对象,是因为将所有对象多看作Object类型放入的.因此从集合中取出元素时它是Object类型.为了表达该元素真实的数据类型,则需要强制类型转换,而强制类型转换可能引发类型转换的异常.
为了避免上述情况的发生,从Java5开始增加了泛型机制,也就是在集合名称的右侧使用<数据类型>的方式来明确要求该集合中可以存放的元素类型,若放入其他类型的元素,则编译报错.
泛型只在编译时期有效,在运行时期不区分什么类型.
这个特点可以通过反射机制以验证.
package 泛型.泛型只在编译期间有效的验证;
import java.util.ArrayList;
public class Test {
public static void main(String[] args) throws Exception {
//第一个list1我们只创建了一个容器:可以输入任何类型
ArrayList list1=new ArrayList();
//第二个list2我们创建了一个泛型:只能输入String类型
ArrayList<String> list2=new ArrayList<String>();
//使用反射机制,获取Class
Class c1=list1.getClass();
Class c2=list2.getClass();
//疑问:在运行时,他们俩相等嘛?
System.out.print(c1==c2);
System.out.println(c1);
}
}
为了验证声明泛型的集合和没有声明泛型的集合时同种数据类型的,利用反射机制获得这两个集合的类型,输出c1 == c2,结果是输出true.它们的类型都是class java.util.ArrayList.
注意这里面应该这么理解,我原来的理解是运行时泛型声明的集合的元素自动转型成Object类型,后来发现这是错误的.在运行时集合的元素还是严格按照泛型的要求类型而存在.验证如下:
1.首先声明一个泛型接口(后面再解释,现在只要知道是一个泛型接口就可以了)
package 泛型.泛型类和泛型接口的声明;
/**
* 泛型接口
* @param <T>
*/
public interface Generator<T> {
public T next();
}
2.然后声明一个实现类继承这个接口,注意这个实现类里有一个成员变量,这里的泛型就是继承自那个接口的.写一个遍历的方法traverse(), 注意这个traverse()方法.
利用格式化输出printf,我们让它输出整数类型.
public class FruitGenerator<T> implements Generator<String>{
private T[] list;//泛型参数类型的成员变量不可以被new修饰,比如private T[] list = new T[5];是错误的写法
//构造器不可以是泛型方法
public FruitGenerator(T[] list){
this.list = list;
}
public void traverse(){
for(T ele : list){
System.out.printf("%5d", ele);
}
}
3.在主方法里分别new一个Integer类型的数组和Double类型的数组,然后声明两个FruitGenerator对象,将以上两个数组作为参数传入,再分别调用traverse()方法.观察结果.
public static void main(String[] args) {
Integer[] number = new Integer[]{11, 22, 33};
Double[] number2 = new Double[]{11.11, 22.22, 33.33};
FruitGenerator fg2 = new FruitGenerator(number);
fg2.traverse();
fg2 = new FruitGenerator(number2);
fg2.traverse();
}
4.结果如下:
我们看到整形数组的元素成功地遍历输出,但双精度类型数组的元素就不会被输出,因为这个元素并不是整数类型的.异常如下:
Exception in thread “main” java.util.IllegalFormatConversionException: d != java.lang.Double
这就说明了即使是在运行期间,泛型修饰的集合也是可以分辨其所包含的数据类型的.
泛型类
泛型类指的是在类名后面跟上尖括号<>,尖括号里用T, R, E等表示不同的数据类型.在声明一个泛型类时,形式如下:
class 类名 < T > {};
尖括号里可以包含其他字母,表示其他数据类型.这只是一个占位符而已,只起标记作用,用任何字母都可以,只是习惯上用T,R,E等表示.
T作为一个泛型参数可以用于类的任何地方,比如声明一个T类型的成员变量,声明一个T类型的成员方法等,但是不能用来声明一个构造方法(编译不通过),我指的是不能让构造方法成为一个泛型方法,但它可以在构造方法的参数列表及方法体内部的任何地方出现.
性质
泛型类需要注意以下几点:
1.当我们声明一个泛型类对象的时候,一般来说需要这么声明:
类名<数据类型1, 数据类型2,…> 引用名 = new 类名 <数据类型1, 数据类型2,…> ();
如果我们这样做,则需要在尖括号内部传入具体的数据类型,也就是实参,这样编译器才能识别这个泛型类对象接受什么类型的数据类型.当然我们也可以不这样做,比如这样声明:
类名 引用名 = new 类名 ();
这样就是声明一个普通类,它可以接受任何类型的数据,也就失去了泛型的意义.
2.泛型不支持基本数据类型,所以int,float,double等都不可以作为泛型参数传入,但支持包装类.
3.泛型类不可以使用instanceOf,理由如开篇所说,运行时泛型类不区分类型.
4.当我们定义并声明一个泛型接口的时候,它跟普通泛型类差别不大,也是可以不传入泛型实参,只要子类在继承这个接口的时候,这个接口后面不要跟上尖括号就可以了,同时因为子类必须重写接口的方法,这个方法也不能被泛型修饰,否则也报错,但是的确可以重写一个普通的方法.说了这么多,就是想说明一点,***泛型类或接口可以作为普通类和接口看待,只要在声明的时候不给它递实参即可.***但是一旦递了实参,原来定义的泛型类或接口中任何出现占位符T的地方都被实参表示的数据类型所替代,它只接受这种类型的数据,如果你执意传入其他类型的数据,则在编译阶段就通不过.
泛型方法
概念
泛型方法指的是这样一种方法
修饰符 <泛型> 返回值类型 方法名(泛型 参数名){};
泛型的尖括号要在方法的返回值类型之前,在修饰符之后,并且方法的参数列表中要包含前面定义的所有泛型,否则编译通不过.
下面是一个错误的泛型方法
public <T> T getName(Generic<E> e){
//错误原因是因为E未声明,我们不知道
}
需要注意的是只有在修饰符和返回值类型之间用尖括号表示的占位符才是真正的泛型方法,只要这个位置没有尖括号及占位符,那它们都不是泛型方法.
举个栗子
package 泛型.泛型方法注意;
public class GenericTest {
//这个类是个泛型类,在上面已经介绍过,用static修饰内部类,那么在建立内部类对象之前就省去了建立外部类对象的过程了
static class Generic<T>{
private T key;
private Integer i;
public Generic(T key) {
this.key = key;
}
public Generic(){
this.i = i;
}
//我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
//这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
//所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey(){
return key;
}
/**
* 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
* 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
public E setKey(E key){
this.key = keu
}
*/
}
/**
* 这才是一个真正的泛型方法。
* 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置.
* 泛型的数量也可以为任意多个
* 如:public <T,K> K showKeyName(Generic<T> container){
* ...
* }
*/
public <T> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
//当然这个例子举的不太合适,只是为了说明泛型方法的特性。
T test = container.getKey();
return test;
}
//这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
public void showKeyValue1(Generic<Number> obj){
System.out.println("泛型测试" + "key value is " + obj.getKey());
}
//这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
//同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
public void showKeyValue2(Generic<?> obj){
System.out.println("泛型测试" + "key value is " + obj.getKey());
}
/**
* 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
* 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
* 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
public <T> T showKeyName(Generic<E> container){
...
}
*/
/**
* 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
* 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
* 所以这也不是一个正确的泛型方法声明。
public void showkey(T genericObj){
}
*/
public static void main(String[] args) {
GenericTest gt = new GenericTest();
Generic<Integer> g = new Generic<>();//这里为什么Generic要用static修饰啊?
gt.showKeyName(g);
gt.showKeyValue1(new Generic<Number>());
gt.showKeyValue2(new Generic<Integer>());
}
}
这个代码基本上解释了所有导致误认的泛型方法,值得一读.但还有一个额外的知识点.注意到主方法中第二行的注释:
//这里为什么Generic要用static修饰啊?
这是因为Generic是类GenericTest的一个内部类.如果不用static修饰,那么必须先声明一个外部类对象,然后再利用这个外部类对象声明内部类对象,但static修饰内部类以后,内部类对象的声明可以直接被类名.内部类所实现.节省了声明外部类对象的步骤.而且还有一点很重要,那就是后面的方法调用基本上是要通过外部类去调用的.当然,如果只考虑声明的话,假设此时内部类没有用static修饰,则内部类的声明应该改成这样:
public static void main(String[] args) {
GenericTest gt = new GenericTest();
Generic<Integer> g = new Generic<>();//这里为什么Generic要用static修饰啊?
}
}
原声明(有static修饰)
public static void main(String[] args) {
Generic<Integer> g = new GenericTest().new Generic<>();//第一种声明方式
GenericTest gt = new GenericTest();
Generic<Integer> g1 = gt.new Generic<>();//第二种声明方式
}
现声明(无static修饰)
泛型类以及泛型方法存在的意义就是在创建或调用时被传入具体类型.
关于静态方法有一点需要注意,那就是在类中静态方法使用泛型,静态方法时无法访问类上定义的泛型的.如果静态方法操作的引用数据类型不确定时,必须要将泛型定义在方法上,也就是把静态方法定义成泛型方法.