Java 泛型通配符

1、常用的 ?, T, E, K, V, N的含义

2、什么是类型擦除

3、上界通配符 < ?extends E>

4、下界通配符 < ?super E>

5、什么是PECS原则

6、通过一个案例来理解 ?和 T 和 Object 的区别

一、 常用的 ?, T, E, K, V, N的含义

        在泛型中使用通配符经常看到T、F、U、E,K,V其实这些并没有啥区别,可以选 A-Z 之间的任何一个字母都可以,并不会影响程序的正常运行。
只不过大家心照不宣的在命名上有些约定:

通配符通配符说明说明
TType具体的Java类
EElement在集合中使用,因为集合中存放的是元素
Kkey分别代表java键值中的Key
Vvalue分别代表java键值中的Value
NNumber数值类型
?表示不确定的 Java 类型

二、什么是类型擦除?

       Java的泛型是伪泛型,是因为泛型信息只存在于代码编译阶段,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程为类型擦除。

       泛型是Java 1.5版本引进的概念,在这之前是没有泛型的,因为类型擦除特性,让泛型代码能够很好地和之前版本的代码兼容。

      如案例:泛型定义为Integer类型集合,添加String的时候在编译时期就会直接报错。

那是不是就一定不能添加了呢?答案是否定的,可以通过Java泛型中的类型擦除特点及反射机制实现。

如下

    public static void main (String[] args) >throws Exception {
        ArrayList<Integer> list = new ArrayList();
        list.add(6);
        //反射机制实现
        Class<? extends ArrayList> clazz = list.getClass();
        Method add = clazz.getDeclaredMethod("add", Object.class);
        add.invoke(list, "欢迎关注:后端元宇宙");
        System.out.println("list = " + list);
    }

运行结果

list = [6, 欢迎关注:后端元宇宙]

案例实体准备

这里先建几个实体,为后面举例用

Animal类

@Data
@AllArgsConstructor
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);
    }
}

三、上界通配符 < ? extends E>

语法:<? extends E>

举例:<? extends Animal> 可以传入的实参类型是Animal或者Animal的子类

两大原则

  • add:除了null之外,不允许加入任何元素!
  • get:可以获取元素,可以通过E或者Object接受元素!因为不管存入什么数据类型都是E的子类型

示例

public static void method(List<? extends Animal> lists){
        //正确 因为传入的一定是Animal的子类
        Animal animal = lists.get(0);
        //正确 当然也可以用Object类接收,因为Object是顶层父类
        Object object = lists.get(1);
        //错误 不能用?接收
        ? t = lists.get(2);
        // 错误
        lists.add(new Animal());
        //错误
        lists.add(new Dog());
        //错误 
        lists.add(object);
        //正确 除了null之外,不允许加入任何元素!
        lists.add(null);
    }

四、下界通配符 < ? super E>

语法: <? super E>

举例 :<? super Dog> 可以传入的实参的类型是Dog或者Dog的父类类型

两大原则

  • add:允许添加E和E的子类元素!
  • get:可以获取元素,但传入的类型可能是E到Object之间的任何类型,也就无法确定接收到数据类型,所以返回只能使用Object引用来接受!如果需要自己的类型则需要强制类型转换。

示例

public static void method (List<? super Dog> lists){
        //错误 因为你不知道?到底啥类型
        Animal animal = lists.get(0);
        //正确 只能用Object类接收</span>
        Object object = lists.get(1);
        //错误 不能用?接收
        ? t = lists.get( 2 );
        //错误
        lists.add(object);
        //错误
        lists.add(new Animal());
        //正确</span>
        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 a1 = exdentAnimal.get(0);
        //错误
        Animal a2 = 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= 0; i < srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di = dest.listIterator();
            ListIterator<? <span extends T> si=src.listIterator();
            for (int i = 0; i < srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

按照这个源码简单改造下

public class CollectionsTest {
    
    /**
     * 将源集合数据拷贝到目标集合
     *
     * @param</span> dest 目标集合
     * @param</span> src  源集合
     * @return</span> 目标集合
     */
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        for(int i = 0; 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() == 0) {
            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 List<T> changeEntityListNew(Class<T> target, List<?> list)</span>;

        首先不需关注传进来的集合内是什么对象,只关注需要转换的集合内是什么对象,所以传进来的集合就可以用List<?>表示任何对象的集合。

返回呢,这里指定的是Class<T>,也就是返回最终是List<T>集合。

再以实体转换方法为例

public static <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>
  • 如果操作与泛型类型无关,那么使用<?>
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

古龙飞扬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值