开始
Java 泛型(generics)是 JDK 5 中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。作者印象中第一次接触泛型的时候应该是在学习集合的时候,今天再详细的的讨论下泛型。
package com.wlee.test;
import java.util.ArrayList;
import java.util.List;
public class GenericTest {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("123");
list.add("abc");
list.add(123); //这里明显会报错
}
}
上面很简单的例子,这里可以看出来在代码编写阶段就已经报错了,不能往 String 类型的集合中添加 int 类型的数据。那可不可以往 List 集合中添加多个类型数据呢,那肯定是可以的,其实我们可以把 List 集合当成普通的类也是没问题的:
package com.wlee.test;
import java.util.ArrayList;
import java.util.List;
public class GenericTest {
public static void main(String[] args) {
List list = new ArrayList();
list.add("123");
list.add("abc");
list.add(123); //这里就没有问题了
}
}
以上代码,就可以得知不定义泛型也是可以往集合中添加数据的,所以说泛型只是一种类型的规范,在代码编写阶段起一种限制。
下面我们通过例子来介绍泛型背后数据是什么类型:
package com.wlee.test;
public class BasePojo<T> {
T val;
public void setVal(T val) {
this.val = val;
}
public T getVal() {
return val;
}
}
上面定义了一个泛型的类,然后我们通过反射获取属性和 getValue 方法返回的数据类型:
package com.wlee.test;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class GenericTest {
public static void main(String[] args) {
//实例对象 并 赋值
BasePojo<String> basePojo = new BasePojo<String>();
basePojo.setVal("WorkerLee");
try {
//获取属性上的泛型类型
Field fieldVal = basePojo.getClass().getDeclaredField("val");
Class<?> type = fieldVal.getType();
String typeName = type.getTypeName();
System.out.println("type:" + typeName);
//获取方法上的泛型类型
Method getVal = basePojo.getClass().getDeclaredMethod("getVal");
Object objInvoke = getVal.invoke(basePojo);
String methodName = objInvoke.getClass().getName();
System.out.println("methodName" + methodName);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
type:java.lang.Object
methodNamejava.lang.String
从结果上看到通过反射获取到的属性是 Object 类型的,在方法中返回的是 String 类型,其实它在 getVal 方法里面实际是做了个强转的动作,将 Object 类型的 val 强转成 String 类型。说白了这里就是咱们经常说“装箱,拆箱”那些事。
泛型只是为了约束我们规范代码,而对于编译完之后的 class 交给虚拟机后,对于虚拟机它是没有泛型的说法的,所有的泛型在它看来都是 Object 类型。其实很好理解,我把上面定义的 BasePojo 稍微修改:
package com.wlee.test;
public class BasePojo<T extends String> {
T val;
public void setVal(T val) {
this.val = val;
}
public T getVal() {
return val;
}
}
这里我们将泛型加了个关键字 extends,extends 是约束了泛型是向下继承的,最后我们通过反射获取 val 的类型是 String 类型的,加上了 extends 关键字其实就是约束泛型是属于哪一类的。所以我们在编写代码的时候如果没有向下兼容类型,会警告报错。
//这里用Long肯定就直接报错了
BasePojo<Long> basePojo = new BasePojo<Long>();
既然泛型其实对于 JVM 来说都是 Object 类型的,咱们直接将类型定义成 Object 不就行了,这种做法是没问题的。但是您拿到 Object 类型值之后不是还得自己进行强转,定义了泛型减少了我们的强转工作,而将这些工作交给了虚拟机岂不是美滋滋。那么泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的。
泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
泛型用在哪
常见的泛型主要有作用在普通类上面,作用在抽象类、接口、静态或非静态方法上:。
方法上面的泛型:
public <T> T getData() {
return null;
}
类上面的泛型:
public class Result<T> {
public String code;
public String msg;
public T data;
}
抽象类或接口上的泛型:
//抽象类泛型
public abstract class TestService<T> {
public List<T> resultList;
}
//接口泛型
public interface TestService<T> {
public T delete();
}
//二级抽象类
public abstract class TestService<K extends Common, V> implements Base<K, V> {
}
//二级接口
public interface TestService<K extends Common, V> extends Base<K, V> {
}
多元泛型:
public interface TestService<K, V> {
public void setKey(K k);
public V getVal();
}
泛型中通配符
我们在定义泛型类,泛型方法,泛型接口的时候经常会碰见很多不同的通配符,比如 T,E,K,V,? 等等。本质上这些通配符没太大区别,只不过是编码时起一种代码约定的作用。一般情况下 T,E,K,V,?是这样约定的:
- ?表示不确定的 Java 类型
- T (type) 表示具体的一个 Java 类型,而且 T 完全可以用 A-Z 之间的任何一个字母代替
- K V (key value) 分别代表 Java 键值中的 Key Value
- E (element) 代表 Element
?无界通配符
通配符其实在声明局部变量时是没有太多的意义,但是当你为一个方法声明一个参数时,它是挺重要的。举个例子吧,比如有一个父类 Animal 和 两个子类 Bird 和 Dog:
public class Animal {
}
public class Bird extends Animal {
}
public class Dog extends Animal {
}
public class GenericTest {
//通配符用来定义变量时是没有太多意义
//List<Animal> listAnimals1;
//List<? extends Animal> listAnimals2;
//主要按一下两个方法 test1 和 test2
public static void test1(List<? extends Animal> animals) {
//方法内容略
}
public static void test2(List<Animal> animals) {
//方法内容略
}
public static void main(String[] args) {
List<Bird> birds = new ArrayList<Bird>();
//调用test1不报错
GenericTest.test1(birds);
//调用test2肯定是报错的
GenericTest.test2(birds); //报错
}
}
以上代码很好理解,test2 方法要求需要一个 Animal 的 List,传递 Bird 的 List 肯定报错。所以,对于不确定或者不关心实际要操作的类型,可以使用无界通配符(就是那个问号),表示可以持有任何类型。像 test1 方法中,限定了上届,但是不关心具体类型是什么,所以对于传入的 Animal 的所有子类都可以支持,并且不会报错。而 test2 就不行。
上界通配符与下界通配符
上界通配符与下界通配符其实很好理解。
上界通配符,其实就是用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类,用来限定泛型的上界。
下界通配符,就是用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object,用来限定泛型的下界。
代码示例:
//水果类 继承 食物
public class Fruit extends Food {
}
//桔子类 继承 水果
public class Orange extends Fruit {
}
//定义了一个 篮子 类
public class Basket<T>{
private T item;
public void set(T item) {
this.item = item;
}
public T get() {
return item;
}
}
现在我们定义一个"Fruit Basket 水果篮子",按照现实逻辑,Orange Basket 当然是 Fruit Basket 的一种。 按照 Java 中父类和子类的使用规范,子类 Orange Basket 当然可以赋值给父类 Fruit Basket。代码就是这样的:
Basket<Fruit> fruitBasket = new Basket<Orange>(); //ide报错,类型无法转换
以上代码肯定是报错的,其实很好理解,Orange 和 Fruit 是父子关系,但是相应的 Basket 却不是父子关系。那怎么解决这个问题?使用上界通配符来处理这种关系:
Basket<? extends Fruit> fruitBasket = new Basket<Orange>();
反过来思考,把 Fruit Basket 赋值给 Food Basket 是否可以?使用下界通配符来处理这种关系:
Basket<? super Fruit> orangeBasket = new Basket<Food>();
?和 T 的区别
简单总结下:T 是一个“确定的”类型,通常用于泛型类和泛型方法的定义。? 是一个“不确定”的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。通过 T 来 确保 泛型参数的一致性,类型参数可以多重限定而通配符不行,通配符可以使用超类限定而类型参数不行。