一、泛型的定义
先来看一段代码
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); // 1
System.out.println("name:" + name);
}
}
}
这段代码首先是定义了一个List类型集合,然后向里面添加了两个String类型的值,然后添加了一个 integer类型的值,这是被允许的,因为List默认为Object类型,可以往里面添加任意类型的值,而在后面的获取元素的时候,因为第三次添加加的是integer类型的值,而又没有进行对应的强制类型转换,所以在运行时期会出现一个java.lang.ClassCastException”异常,所以这个时候泛型就起作用了。
泛型的定义:
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
可以这么理解,如果把集合,类,方法等等这些看成一个容器,那么泛型就相当于是一种限制,限制某种容器只能存放某些类型的值,还有注意一点,java中没有泛型数组,这一点的原因后面会提到。
如果把上面的一段代码添加泛型的话,在1处就会提示编译错误,因为通过泛型已经限制了添加进list的值只能是String,而不能是其他类型的值,否则在编译期间就会报错,而不是在运行时报错,因此在2处就不需要进行强制类型转换,编译器已经知道了返回值的类型为String,提高了程序的安全性,避免插入了错误类型的对象,也使得程序具有更好的可读性
public class GenericTest {
public static void main(String[] args) {
/*
List list = new ArrayList();
list.add("qqyumidi");
list.add("corn");
list.add(100);
*/
List<String> list = new ArrayList<String>();
list.add("qqyumidi");
list.add("corn");
//list.add(100); // 1 提示编译错误
for (int i = 0; i < list.size(); i++) {
String name = list.get(i); // 2
System.out.println("name:" + name);
}
}
}
二、泛型的特性
泛型只在编译时期有效。在使用泛型类时,虽然传入了不同的泛型实参,但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存上只有一个,即还是原来的最基本的类型,但是泛型不可以是基本数据类型,这两者是有区别的,泛型只能是一个对象类型(包括自己定义的类型),但是基本数据类型都有包装类型,比如Number,integer等,当然,在逻辑上我们可以理解成多个不同的泛型类型。在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。为了验证上述所说的,对于不同传入的类型实参,生成的相应对象实例的类型是不是一样的
先看一个例子
public class GenericTest {
public static void main(String[] args) {
Box<String> name = new Box<String>("corn");
Box<Integer> age = new Box<Integer>(712);
System.out.println(name.getClass() == age.getClass()); // true
}
}
由这个例子可以验证上面的说法是正确的,他们实际上都是相同的Box类型,所以运行结果为true
另外还要提一点的是,如果没有限定类型,即Box
三、泛型的使用和继承规则
泛型一般有三种使用方式,泛型类,泛型接口,泛型方法,首先看一个简单的泛型类和泛型方法的定义
其中T表示类型形式参数,用于接受外部传来的类型实参,在本例中,传来的类型实参为String,一般用T表示任意类型,E表示集合类型,K,V表示关键字和映射键的类型
public class GenericTest {
public static void main(String[] args) {
Box<String> name = new Box<String>("corn");
System.out.println("name:" + name.getData());
}
}
class Box<T> {
private T data;
public Box() {
}
public Box(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
泛型的继承规则
假设有两个类,比如Employee类和Manager类,且两者为父类和子类的关系,那么Box
四、类型通配符
当一个类型无法确认时,就可以用类型通配符,类型通配符一般用 ? 表示,例如 Box<?>,用类型通配符代表具体的类型实参,**注意! ?是代表类型实参,不是类型形参!, ?是代表类型实参,不是类型形参! , ?是代表类型实参,不是类型形参**!, 意思就是此处的 ? 和 Number,Integer一样都是实际的类型,
且逻辑上?是所有Box
类型通配符的限定:
在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,表示类型实参只准传入某种类型的父类或某种类型的子类,为泛型添加边界,表示传入的类型实参必须是指定类型的子类型。
上限:?extends E:可以接收E类型或者E的子类型对象。
下限:?super E:可以接收E类型或者E的父类型对象。
上限什么时候用:往集合中添加元素时,既可以添加E类型对象,又可以添加E的子类型对象。为什么?因为取的时候,E类型既可以接收E类对象,又可以接收E的子类型对象。
下限什么时候用:当从集合中获取元素进行操作的时候,可以用当前元素的类型接收,也可以用当前元素的父类型接收。
示例代码
public void showKeyValue1(Generic<? extends Number> obj){
//此处表示传入的类型只能是Number的子类型
Log.d("泛型测试","key value is " + obj.getKey());
}
Generic<String> generic1 = new Generic<String>("11111");
Generic<Integer> generic2 = new Generic<Integer>(2222);
Generic<Float> generic3 = new Generic<Float>(2.4f);
Generic<Double> generic4 = new Generic<Double>(2.56);
//这一行代码编译器会提示错误,因为String类型并不是Number类型的子类
//showKeyValue1(generic1);
showKeyValue1(generic2);
showKeyValue1(generic3);
showKeyValue1(generic4);
五、泛型类和泛型方法
在了解了泛型的具体定义之后看看什么是泛型类和泛型方法,泛型类和泛型方法:
- 泛型类的定义:一个泛型类就是具有一个或者多个类型变量的类,
public class Box<T>{...}
Box类引入了一个类型变量T,一个泛型类可以有多个类型变量,泛型类中的类型变量指定方法的返回类型和局部变量的类型,接下来看一个泛型类的具体实例:
public class Box<T>{
private T first;
private T second;
public Box() {....}
public Box(T first,T second) {}
public T getFirst() {}
public T getsecond() {}
public void setFirst(T first) {}
public void setsecond(T second ) {}
}
-
泛型方法:带有类型参数的方法,
定义形式:publicT method() {……}类型变量放在修饰符public static等的后面,返回类型前面,泛型方法可以定义在普通类,也可以定义在泛型类,当调用一个泛型方法时,在方法名的前面加上尖括号并放入具体的类型,例如clas是一个类,method是一个泛型方法,调用方式可以是class. method,但是大多数情况下,方法调用可以省略尖括号 ,即可以直接调用,因为编译器会自动推断出调用方法的类型。 -
最后提一下泛型数组,在前面说到,java里面不允许创建一个确切的泛型数组。
即这样是不被允许的
List[] ls = new ArrayList [10];
但是这样是可以的
List[] ls = new ArrayList[10];
原因:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。