为什么使用泛型
首先,我们举个例子。
- 求和函数
针对开发中常见的数值求和需求,如int,long,double等类型。
public static int addInt(int x,int y){
return x+y;
}
public static float addFloat(float x,float y){
return x+y;
}
没有泛型的情况下,对不同的类型需要封装不同的方法。使用泛型则可以减少重复代码
public static double addNumber(T a, T b){
return a.doubleValue() + b.doubleValue();
}
此时,返回值选择double类型,因为其取值范围和精度相对其它Number都更合适。
2.List集合
List集合在没有使用泛型时,默认是Object元素,可以存放任意数据类型。
List list = new ArrayList();
list.add("mark");
list.add("OK");
list.add(100);
for (int i = 0; i < list.size(); i++) {
String name = list.get(i).toString();
System.out.println("name:" + name);
}
但是取出来使用的时候,仍需要知道元素类型,这就需要强制类型转换了。这种行为安全性不高,建议使用List时配合泛型。
List list = new ArrayList<>();
泛型机制的优点
泛型机制的优点有:
1.泛型可编写模版代码来适应多种类型,减少重复代码
2.泛型可避免强制类型转换,编译时进行类型检查,减少出错机会
泛型擦除
Java泛型是伪泛型,因在编译期间泛型信息会被擦除,也就是生成的字节码文件中不包含泛型中的类型信息。编码使用泛型时添加类型信息,编译器编译的时候去掉,这个过程就是泛型擦除。
ArrayList list1 = new ArrayList<>();
list1.add("abc");
ArrayList list2 = new ArrayList<>();
list2.add(123);
System.out.println("class:" + list1.getClass()); //class:class java.util.ArrayList
System.out.println(list1.getClass() == list2.getClass());//true
最终list1.getClass() == list2.getClass()的结果是true,说明泛型类型String和Integer被擦除了,只剩下原始类型java.util.ArrayList。
public void a(List list){
}
public void a(List list){
}
上述的代码会出现编译错误both methods have same erasure,因为泛型擦除后,二者不能构成重载。
综上,Java的泛型也被称为伪泛型。
- 真泛型:泛型中的类型是真实存在的。
- 伪泛型:仅在编译时类型检查,在运行时擦除类型信息。
Java泛型擦除的原因是向前兼容,把已有的类型(主要是Collections容器)泛型化,保证已经部署的程序可以继续运行。
泛型擦除问题
先了解下泛型擦除究竟擦除了什么,保留了什么信息。
问:泛型的信息不是被擦除了吗?
答:是被擦除了, 但是某些(声明侧的泛型,接下来解释) 泛型信息会被class文件 以Signature的形式 保留在Class文件的Constant pool中。但是使用侧泛型则不会。
声明侧泛型主要指以下内容
1.泛型类,或泛型接口的声明 2.带有泛型参数的方法 3.带有泛型参数的成员变量
使用侧泛型
也就是方法的局部变量,方法调用时传入的变量。
Gson解析时传入的参数属于使用侧泛型,因此不能通过Signature解析
如何获取泛型信息
通过class的getTypeParameters只能获取到声明泛型参数的占位符。
List list = new ArrayList<>();
Map map = new HashMap<>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters())); //E
System.out.println(Arrays.toString(map.getClass().getTypeParameters())); //K,V
但是开发中有些场景需要获取泛型的信息,如Retrofit接口,Gson序列化,这时该如何办呢。请看下面修改后的代码:
Map map1 = new HashMap() {};
Type type1 = map1.getClass().getGenericSuperclass();
ParameterizedType parameterizedType1 = ParameterizedType.class.cast(type1);
for (Type typeArgument : parameterizedType1.getActualTypeArguments()) {
System.out.println(typeArgument.getTypeName()); //class java.lang.String / class java.lang.Integer
}
示例代码获取了map1实例所对应的泛型信息,两端示例代码结果不同的关键就是map和map1的定义不同。其中变量map1是创建了一个HashMap的匿名内部类,其泛型参数限定为 String和Integer。通过定义类的方式,在类信息中保留泛型信息,进而在运行时获得这些泛型信息。
另一种获取field泛型类型的方法如下
// public Map memMap;
Field field = null;
try {
field = GenericDemo.class.getField("memMap");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
ParameterizedType parameterizedType2 = (ParameterizedType)field.getGenericType();
System.out.println("parameterizedType toString " + parameterizedType2); //java.util.Map
System.out.println("parameterizedType 的参数信息 " + Arrays.asList(parameterizedType2.getActualTypeArguments())); //[class java.lang.Integer, class java.lang.String]
Gson反序列化时如何解析泛型类型
当使用Gson库进行json的解析时,使用方式如下。可以看到也是使用了匿名内部类。
// Gson 常用的情况
public List parse(String jsonStr){
List topNews = new Gson().fromJson(jsonStr, new TypeToken>() {}.getType());
return topNews;
}
Gson反序列化原理
Class类提供了一个方法public Type getGenericSuperclass() ,可以获取到带泛型信息的父类Type。也就是说java的class文件会保存继承的父类或者接口的泛型信息。
TypeToken的部分代码如下:
public class TypeToken {
final Class super T> rawType;
final Type type;
final int hashCode;
@SuppressWarnings("unchecked")
protected TypeToken() {
this.type = getSuperclassTypeParameter(getClass());
this.rawType = (Class super T>) $Gson$Types.getRawType(type);
this.hashCode = type.hashCode();
}
/**
* Returns the type from super class's type parameter in {@link $Gson$Types#canonicalize
* canonical form}.
*/
static Type getSuperclassTypeParameter(Class> subclass) {
Type superclass = subclass.getGenericSuperclass();
if (superclass instanceof Class) {
throw new RuntimeException("Missing type parameter.");
}
ParameterizedType parameterized = (ParameterizedType) superclass;
return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);
}
/**
* Returns the raw (non-generic) type for this type.
*/
public final Class super T> getRawType() {
return rawType;
}
/**
* Gets underlying {@code Type} instance.
*/
public final Type getType() {
return type;
}
}
通过创建继承自TypeToken
PECS原则
PECS即Produce Extend Consumer Super,PECS是从集合的角度出发的,含义如下:
1.如果你只是从集合中取数据,那么它是个生产者,你应该用extend
2.如果你只是往集合中加数据,那么它是个消费者,你应该用super
3.如果你往集合中既存又取,那么你不应该用extend或者super
示例
public class Collections {
public static void copy(List super T> dest, List extends T> src) {
for (int i=0; i
解释如下:
在List extends Fruit>的泛型集合中,对于元素的类型,编译器只能知道元素是继承自Fruit,具体是Fruit的哪个子类是无法知道的。 所以「向一个无法知道具体类型的泛型集合中插入元素是不能通过编译的」。但是由于知道元素是继承自Fruit,所以从这个泛型集合中取Fruit类型的元素是可以的。
在List super Apple>的泛型集合中,元素的类型是Apple的父类,但无法知道是哪个具体的父类,因此「读取元素时无法确定以哪个父类进行读取」。 插入元素时可以插入Apple与Apple的子类,因为这个集合中的元素都是Apple的父类,子类型是可以赋值给父类型的。
? 无限定通配符。eg Pair>,既不能读也不能写,只能做一些null判定。
大多数情况下,可以引入泛型参数消除>通配符。>通配符有一个独特的特点,就是:Pair>是所有Pair 的超类,也就是可以安全的向上转型。
反射和泛型
Java的部分反射API也是泛型。例如:Class
Class clazz = String.class;
String str = clazz.newInstance();
调用Class的getSuperclass()方法返回的Class类型是Class super T>:
Class super String> sup = String.class.getSuperclass();
我们可以声明带泛型的数组,但不能用new操作符创建带泛型的数组。必须通过强制转型实现带泛型的数组:
Pair[] ps = null; // ok
Pair[] ps = new Pair[2]; // compile error!
@SuppressWarnings("unchecked")
Pair[] ps = (Pair[]) new Pair[2];
使用泛型数组时要特别注意,因为如果持有原强制转换对象的引用,该对象没有泛型的限制,编译器不对检查对其的修改操作。泛型数组对象和其指向同一对象,可能有不安全的类型转换。推荐上面的写法,避免持有原引用
带泛型的数组实际上是编译器的类型擦除,所以我们不能直接创建泛型数组T[],因为擦拭后代码变为Object[],必须借助Class
T[] createArray(Class cls) {
return (T[]) Array.newInstance(cls, 5);
}
还可以利用可变参数创建泛型数组T[],但是不推荐。
参考文档:
【知识点】Java泛型机制7连问
Java 的泛型擦除和运行时泛型信息获取
Java Type 类型详解
super通配符
extends通配符
泛型和反射