一、 什么是泛型
泛型,即“参数化类型”,顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用时传入具体的类型(类型实参)
二、泛型类、泛型接口、泛型方法
- 2-1 简单的泛型类
public class Test {
public static void main(String[] args) {
Box integerBox = new Box();
Box stringBox = new Box();
integerBox.add(new Integer(10));
stringBox.add(new String("菜鸟教程"));
System.out.printf("整型值为 :%d\n\n", integerBox.get());
System.out.printf("字符串为 :%s\n", stringBox.get());
}
}
public class Box {
private T data;
public Box() {
}
public Box(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
看完上面的例子应该有一个疑问:在泛型接口、泛型类和泛型方法的定义过程中,我们常见的如T、E、K、V等形式的参数常用于表示泛型形参,来接收来自外部使用时候传入的类型实参。相同的泛型类传入不同的类型实参,生成的实例的类型是不是一样的呢?这句翻译一下就是问,上面例子中的integerBox 和 stringBox 这两个实例对象的类型是不是一样呢?
↓↓↓↓↓我们来验证一下↓↓↓↓↓
public class Test {
public static void main(String[] args) {
Box name = new Box("dog");
Box age = new Box(521);
System.out.println("name class:" + name.getClass()); // com.jowney.Box
System.out.println("age class:" + age.getClass()); // com.jowney.Box
System.out.println(name.getClass() == age.getClass()); // true
}
}
由此,我们发现,在使用泛型类时,虽然传入了不同的泛型实参(上例传入了String和Integer),但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类的实例的类型还是原来的最基本的类型(本实例中为Box),当然,在逻辑上我们可以理解成多个不同的泛型类型。
究其原因在于,Java中的泛型这一概念提出的目的导致其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。
对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的类型。
↓↓↓↓↓问题来喽↓↓↓↓↓
public class Test {
public static void main(String[] args) {
Box name = new Box(66);
Box age = new Box(521);
getData(name);
getData(age); // 1
}
public static void getData(Box data){
System.out.println("data :" + data.getData());
}
}
如上例,Number是Integer的父类,这个时候注释1的代码会报错吗?会,错误提示是:
The method getData(Box
) in the type Test is not applicable for the >arguments (Box )。 显然,通过提示信息,我们知道Box
在逻辑上不能视为Box 的父类。这也证实了上面的总结(编译过程中泛型类型在逻辑上可以看成是多个不同的类型,编译完成后实际上都是相同的类型)。
这怎么办,怎么让它编译通过
↓↓↓↓↓解决一下↓↓↓↓↓
public class Test {
public static void main(String[] args) {
Box name = new Box("dog");
Box age = new Box(512);
Box number = new Box(114);
getData(name);
getData(age);
getData(number);
}
public static void getData(Box> data) {
System.out.println("data :" + data.getData());
}
}
这就需要类型通配符来解决了。类型通配符一般是使用 ? 代替具体的类型实参。注意了,此处是类型实参,而不是类型形参!且Box>在逻辑上是Box
、Box ...等所有Box<具体类型实参>的父类。 类型通配符上限通过形如Box extends Number>形式定义,相对应的,类型通配符下限为Box super Number>形式,其含义与类型通配符上限正好相反,在此不作过多阐述了。
- 2-2泛型方法
(1)
注意 上例中的public static void getData(Box> data)不是泛型方法。
如果你定义了一个泛型(类、接口),那么Java规定,你不能在静态方法、静态初块等所有静态内容中使用泛型类的类型形参 。例如:
public class A {
public static void func(T t) {
//报错,编译不通过
}
}
(2) 什么是泛型方法:定义泛型方法,其格式是:修饰符 <类型参数列表> 返回类型 方法名(形参列表) { 方法体 }。例如:public static
int func(List list, Map map) { ... },其中T和S是泛型类型参数。
/**
* 泛型方法的基本介绍
* @param tClass 传入的泛型实参
* @return T 返回值为T类型
* 说明:
* 1)public 与 返回值中间非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
*/
public T genericMethod(Class tClass)throws InstantiationException ,
IllegalAccessException{
T instance = tClass.newInstance();
return instance;
}
(3)泛型的作用域:
- class A
{ ... }中T的作用域就是整个A; - public
func(...) { ... }中T的作用域就是方法func; - 类型参数也存在作用域覆盖的问题,可以在一个泛型模板类/接口中继续定义泛型方法,例如:
class A {
// A已经是一个泛型类,其类型参数是T
public static void func(T t) {
// 再在其中定义一个泛型方法,该方法的类型参数也是T
}
}
//当上述两个类型参数冲突时,在方法中,方法的T会覆盖类的T,即和普通变量的作用域一样,内部覆盖外部,外部的同名变量是不可见的。
//除非是一些特殊需求,一定要将局部类型参数和外部类型参数区分开来,避免发生不必要的错误,因此一般正确的定义方式是这样的:
class A {
public static void func(S s) {
}
}
(4)泛型方法类型参数范围限定
泛型方法的类型参数可以指定上限,类型上限必须在类型参数声明的地方定义上限,不能在方法参数中定义上限。规定了上限就只能在规定范围内指定类型实参,超出这个范围就会直接编译报错。
void func(List list){ ... },正确 void func(T t){ ... },正确 void func(List list){ ... } ,编译错误 void func(List dest, List extends T> src) { ... },编译正确
(5)泛型方法调用
显式指定方法的类型参数,类型参数要写在尖括号中并放在方法名之前。例如:object.
func(...),这样就显式指定了泛型方法的类型参数为String,那么所有出现类型参数T的地方都将替换成String类型。 隐式地自动推断,不指明泛型参数,编译器根据传入的实参类型自动推断类型参数。例如:
void func(T t){ ... }隐式调用object.func("name"),根据"name"的类型String推断出类型参数T的类型是String 避免歧义,例如:
void func(T t1, T t2){ ... }如果这样调用的话object.func("name", 15); 虽然编译不会报错,但是仍然会有很大隐患,T到底应该是String还是Integer存在歧义; 有些歧义Java是会直接当成编译错误的,即所有和泛型参数有关的歧义,例如:
void func(List l1, List l2){...}如果这样调用的话,object.func(new List (), new List ()); 这里会有歧义,编译器无法知道T到底应该是String还是Integer,这种歧义会直接报错的,编译无法通过。即泛型方法中,如果类型参数刚好就是泛型参数的类型实参,那么这个类型实参不得有歧义,否则直接编译报错。
(6) 避免泛型方法与其他方法混淆
在java中,泛型类的定义非常简单,但是泛型方法就比较复杂了。
尤其是我们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法,这样在初学者中非常容易将泛型方法理解错了。
public class Test {
public static void main(String[] args) {
}
//这个类是个泛型类
public class Generic{
private T key;
public Generic(T key) {
this.key = key;
}
//我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
//这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
//所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey(){
return key;
}
/**
* 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
* 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。 */
public E setKey(E key){
this.key = key
}
}
/**
* 这才是一个真正的泛型方法。
* 首先在public与返回值之间的必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置.
* 泛型的数量也可以为任意多个
* 如:public K showKeyName(Generic container){
* ...
* }
*/
public T showKeyName(Generic container){
System.out.println("container key :" + container.getKey());
//当然这个例子举的不太合适,只是为了说明泛型方法的特性。
T test = container.getKey();
return test;
}
//这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic这个泛型类做形参而已。
public void showKeyValue1(Generic obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
//这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
//同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
public void showKeyValue2(Generic> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
/**
* 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
* 虽然我们声明了,也表明了这是一个可以处理泛型的类型的泛型方法。
* 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
*/
public T showKeyName(Generic container){
...
}
}
public class GenericFruit {
class Fruit{
@Override
public String toString() {
return "fruit";
}
}
class Apple extends Fruit{
@Override
public String toString() {
return "apple";
}
}
class Person{
@Override
public String toString() {
return "Person";
}
}
class GenerateTest{
public void show_1(T t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
//由于泛型方法在声明的时候会声明泛型,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
public void show_3(E t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
public void show_2(T t){
System.out.println(t.toString());
}
}
public static void main(String[] args) {
Apple apple = new Apple();
Person person = new Person();
GenerateTest generateTest = new GenerateTest();
//apple是Fruit的子类,所以这里可以
generateTest.show_1(apple);
//编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
//generateTest.show_1(person);
//使用这两个方法都可以成功
generateTest.show_2(apple);
generateTest.show_2(person);
//使用这两个方法也都可以成功
generateTest.show_3(apple);
generateTest.show_3(person);
}
}