本文内容如下:
1、 什么是类型擦除
2、常用的 ?, T, E, K, V, N的含义
3、上界 通配符 < ?extends E>
4、下界通配符 < ?super E>
5、什么是PECS原则
6、通过一个案例来理解 ?和 T 和 Object 的区别
一、什么是类型擦除?
我们说 java 的 泛型 是伪泛型,那是因为泛型信息只存在于代码编译阶段,在生成的 字节码 中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程为类型擦除。
泛型是Java 1.5版本才引进的概念,在这之前是没有泛型的,但是因为类型擦除特性,让泛型代码能够很好地和之前版本的代码兼容。
我们来看个案例
(图1)
因为这里泛型定义为 Integer 类型集合,所以添加 String 的时候在编译时期就会直接报错。
那是不是就一定不能添加了呢?答案是否定的,我们可以通过Java泛型中的类型擦除特点及反射机制实现。
如下
public static void main(String[] args) throws Exception { | |
ArrayList<Integer> list = new ArrayList(); | |
list.add(); | |
// 反射机制 实现 | |
Class<? extends ArrayList> clazz = list.getClass(); | |
Method add = clazz.getDeclaredMethod("add", Object.class); | |
add.invoke(list, "欢迎关注:后端元宇宙"); | |
System.out.println("list = " + list); | |
} |
运行结果
list = [, 欢迎关注:后端元宇宙]
二、案例实体准备
这里先建几个实体,为后面举例用
Animal类
@Data | |
@AllArgs Constructor | |
public class Animal { | |
/** | |
* 动物名称 | |
*/ private String name; | |
/** | |
* 动物毛色 | |
*/ private String color; | |
} |
Pig类 :Pig是Animal的子类
public class Pig extends Animal{ | |
public Pig(String name,String color){ | |
super(name,color); | |
} | |
} |
Dog类 : Dog也是Animal的子类
public class Dog extends Animal { | |
public Dog(String name,String color){ | |
super(name,color); | |
} | |
} |
三、常用的 ?, T, E, K, V, N的含义
我们在泛型中使用通配符经常看到T、F、U、E,K,V其实这些并没有啥区别,我们可以选 A-Z 之间的任何一个字母都可以,并不会影响程序的正常运行。
只不过大家心照不宣地在命名上有些约定:
- T (Type) 具体的Java类
- E (Element)在集合中使用,因为集合中存放的是元素
- K V (key value) 分别代表java键值中的Key Value
- N (Number)数值类型
- ? 表示不确定的 Java 类型
四、上界通配符 < ? extends E>
语法:<? extends E>
举例:<? extends Animal> 可以传入的实参类型是Animal或者Animal的子类
两大原则
add | |
get |
示例
public static void method(List<? extends Animal> lists){ | |
//正确 因为传入的一定是Animal的子类 | |
Animal animal = lists.get(); | |
//正确 当然也可以用Object类接收,因为Object是顶层父类 | |
Object object = lists.get(); | |
//错误 不能用?接收 | |
? t = lists.get(); | |
// 错误 | |
lists.add(new Animal()); | |
//错误 | |
lists.add(new Dog()); | |
//错误 | |
lists.add(object); | |
//正确 除了null之外,不允许加入任何元素! | |
lists.add(null); | |
} |
五、下界通配符 < ? super E>
语法: <? super E>
举例:<? super Dog> 可以传入的实参的类型是Dog或者Dog的父类类型
两大原则
add | |
get |
示例
public static void method(List<? super Dog> lists){ | |
//错误 因为你不知道?到底啥类型 | |
Animal animal = lists.get(); | |
//正确 只能用Object类接收 | |
Object object = lists.get(); | |
//错误 不能用?接收 | |
? t = lists.get(); | |
//错误 | |
lists.add(object); | |
//错误 | |
lists.add(new Animal()); | |
//正确 | |
lists.add(new Dog()); | |
//正确 可以存放null元素 | |
lists.add(null); | |
} |
六、什么是PECS原则?
PECS原则:生产者(Producer)使用extends,消费者(Consumer)使用super。
原则
- 如果想要获取,而不需要写值则使用” ? extends T “作为数据结构泛型。
- 如果想要写值,而不需要取值则使用” ? super T “作为数据结构泛型。
示例-
public class PESC { | |
ArrayList<? extends Animal> exdentAnimal; | |
ArrayList<? super Animal> superAnimal; | |
Dog dog = new Dog("小黑", "黑色"); | |
private void test() { | |
//正确 | |
Animal a = exdentAnimal.get(0); | |
//错误 | |
Animal a = superAnimal.get(0); | |
//错误 | |
exdentAnimal.add(dog); | |
//正确 | |
superAnimal.add(dog); | |
} | |
} |
示例二
Collections集合工具类有个 copy 方法,我们可以看下源码,就是PECS原则。
public static <T> void copy(List<? super T> dest, List<? extends T> src) { | |
int srcSize = src.size(); | |
if (srcSize > dest.size()) | |
throw new IndexOutOfBoundsException("Source does not fit in dest"); | |
if (srcSize < COPY_THRESHOLD || | |
(src instanceof RandomAccess && dest instanceof RandomAccess)) { | |
for (int i=; i<srcSize; i++) | |
dest.set(i, src.get(i)); | |
} else { | |
ListIterator<? super T> di=dest.listIterator(); | |
ListIterator<? extends T> si=src.listIterator(); | |
for (int i=; i<srcSize; i++) { | |
di.next(); | |
di.set(si.next()); | |
} | |
} | |
} |
我们按照这个源码简单改造下来
public class CollectionsTest { | |
/** | |
* 将源集合数据拷贝到目标集合 | |
* | |
* @param dest 目标集合 | |
* @param src 源集合 | |
* @return 目标集合 | |
*/ public static <T> void copy(List<? super T> dest, List<? extends T> src) { | |
int srcSize = src.size(); | |
for (int i =; i < srcSize; i++) { | |
dest.add(src.get(i)); | |
} | |
} | |
public static void main(String[] args) { | |
ArrayList<Animal> animals = new ArrayList(); | |
ArrayList<Pig> pigs = new ArrayList(); | |
pigs.add(new Pig("黑猪", "黑色")); | |
pigs.add(new Pig("花猪", "花色")); | |
CollectionsTest.copy(animals, pigs); | |
System.out.println("dest = " + animals); | |
} | |
} |
运行结果
dest = [Animal(name=黑猪, color=黑色), Animal(name=花猪, color=花色)]
七、通过一个案例来理解 ?和 T 和 Object 的区别
1、实体转换
我们在实际开发中,经常进行实体转换,比如SO转 DTO ,DTO转DO等等,所以需要一个转换工具类。
如下示例
/** | |
* 实体转换工具类 | |
* | |
* TODO 说明该工具类不能直接用于生产,因为为了代码看去清爽点,我少了一些必要检验,所以如果直接拿来使用可以会在某些场景下会报错。 | |
*/public class EntityUtil { | |
/** | |
* 集合实体转换 | |
* | |
* @param target 目标实体类 | |
* @param list 源集合 | |
* @return 装有目标实体的集合 | |
*/ public static <T> List<T> changeEntityList(Class<T> target, List<?> list) throws Exception { | |
if (list == null || list.size() ==) { | |
return null; | |
} | |
List<T> resultList = new ArrayList<T>(); | |
//用Object接收 | |
for (Object obj : list) { | |
resultList.add(changeEntityNew(target, obj)); | |
} | |
return resultList; | |
} | |
/** | |
* 实体转换 | |
* | |
* @param target 目标实体class对象 | |
* @param baseTO 源实体 | |
* @return 目标实体 | |
*/ public static <T> T changeEntity(Class<T> target, Object baseTO) throws Exception{ | |
T obj = target.newInstance(); | |
if (baseTO == null) { | |
return null; | |
} | |
BeanUtils.copyProperties(baseTO, obj); | |
return obj; | |
} | |
} |
使用工具类示例
private void changeTest() throws Exception { | |
ArrayList<Pig> pigs = new ArrayList(); | |
pigs.add(new Pig("黑猪", "黑色")); | |
pigs.add(new Pig("花猪", "花色")); | |
//实体转换 | |
List<Animal> animals = EntityUtil.changeEntityList(Animal.class, pigs); | |
} |
这是一个很好的例子,从这个例子中我们可以去理解 ?和 T 和 Object的使用场景。
我们先以 集合转换 来说
public static <T> List<T> changeEntityListNew(Class<T> target, List<?> list);
首先其实我们并不关心传进来的集合内是什么对象,我们只关系我们需要转换的集合内是什么对象,所以我们传进来的集合就可以用 List<?> 表示任何对象的集合都可以。
返回呢,这里指定的是Class<T>,也就是返回最终是 List<T> 集合。
再以 实体转换 方法为例
public static <T> T changeEntityNew(Class<T> target, Object baseTO)
同样的,我们并不关心源对象是什么,我们只关心需要转换的对象,只需关心需要转换的对象为 T 。
那为什么这里用Object上面用?呢,其实上面也可以改成 List<Object> list ,效果是一样的,上面 List<?> list 在遍历的时候最终不就是用Object接收的吗
?和Object的区别
?类型不确定和Object作用差不多,好多场景下可以通用,但?可以缩小泛型的范围,如:List<? extends Animal>,指定了范围只能是Animal的子类,但是用 List<Object> ,没法做到缩小范围。
总结
- 只用于 读 功能时,泛型结构使用<? extends T>
- 只用于 写 功能时,泛型结构使用<? super T>
- 如果既用于 写 ,又用于 读 操作,那么直接使用<T>
- 如果操作与泛型类型无关,那么使用<?>