美文网首页深入浅出Android
《java基础》-胖菜鸟说泛型

《java基础》-胖菜鸟说泛型

作者: 半寿翁 | 来源:发表于2020-08-09 18:52 被阅读0次

    先扯两句

      转眼之间距离上次发类、抽象类、接口已经过去了不知道多少个日夜了,原以为在博客中已经暗示的不能再明显了。可结果呢,几篇博客发出去了,别说找女朋友了,就连赞都没得到几个,在孤寂的夜自己的眼泪还是得自己擦啊。还能咋办,接着写呗,万一啥时候被我真命天女看到了呢(虽然看技术博客的真命天女还是让我瑟瑟发抖)!

      之前说了,之所以写《java基础系列》并不是因为自己闲着没事来复习知识点来了,单纯的是因为想要写《设计模式》,自以为是的想要将一些内容解释出来让不太清楚的与我一样的菜鸟们了解,所以专门“封装”的博客,那么设计模式中,个人感觉泛型还是挺重要的,就写着玩了,为了女朋友!!!

    女朋友

      咳咳,就想着女朋友了,差点忘了鸣谢这位老哥Java3y 泛型就这么简单,大家如果想了解泛型正经一些的介绍可以去看看。

    正文

      之前为了找女朋友,专门学习了一下怎么做清蒸多宝鱼,既然留不住妹子的心,就只能迂回一下,想方设法留住妹子的胃了胖菜鸟说接口

    @Test
    public void girlfriendDate() {
        judge(new Lina(), new Tina(), new Shawn());
    }
    
    /**
     * 判断女孩是都满足做我女朋友的条件
     *
     * @param girls 相亲的女孩们
     */
    private void judge(IGirl... girls) {
        for (IGirl girl : girls) {
            if (girl instanceof ILikeSteamedTurbot) {
                System.out.println(MessageFormat.format("{0}喜欢吃清蒸多宝鱼,我要追她"
                        , girl.getClass().getSimpleName()));
            } else if (girl instanceof IDislikeSteamedTurbot) {
                System.out.println(MessageFormat.format("{0}讨厌吃清蒸多宝鱼,没机会了"
                        , girl.getClass().getSimpleName()));
            }
        }
    }
    

      虽然到现在还是没找到妹子,但是为了不让自己的努力白费,因此找女朋友的时候,必须添加一条要求,那就是必须喜欢吃清蒸多宝鱼。而之前的验证方式中,无论是否满足的条件女孩,我都要抽出时间按个去相亲,即便相亲第一句就问对方喜欢不喜欢吃清蒸多宝鱼,如果不喜欢就转身出门,还是要白白浪费掉大量的时间,有这大好青春,我舒舒服服的睡个回笼觉不好吗?

      因此,如果能够在约见相亲对象之前就能对于“是否喜欢吃清蒸多宝鱼”这一项做一个预判断,不喜欢吃的直接就不用去相亲岂不是美滋滋!说干就干!

    @Test
    public void girlfriendDate() {
        IGirl lina = new Lina();
        IGirl tina = new Tina();
        IGirl shawn = new Shawn();
        judge(new Lina());
        judge(new Tina());
        judge(new Shawn());
    }
    
    /**
     * 判断女孩是都满足做我女朋友的条件
     *
     * @param girls 相亲的女孩
     */
    private <SuitableGirl extends ILikeSteamedTurbot> void judge(SuitableGirl girl) {
        if (girl instanceof ILikeSteamedTurbot) {
            System.out.println(MessageFormat.format("{0}喜欢吃清蒸多宝鱼,我要追她"
                    , girl.getClass().getSimpleName()));
        } else if (girl instanceof IDislikeSteamedTurbot) {
            System.out.println(MessageFormat.format("{0}讨厌吃清蒸多宝鱼,没机会了"
                    , girl.getClass().getSimpleName()));
        }
    }
    

      上面就是调整好的代码,当天,这么看基本是看不出来什么的,我们来看一下IDE的截图:

    不是复核我要求的女孩 为啥不符合要求

      可以看到预判断给出的提示信息为:“Lina类不能被应用到judge方法中”。而Shawn却没有报错,这是为什么呢?

    /*
     * Lina
     *
     * @author 半寿翁
     * @date 2020-4-6
     */
    public class Lina implements IDislikeSteamedTurbot {
    }
    
    /*
     * Shawn
     *
     * @author 半寿翁
     * @date 2020-4-6
     */
    public class Shawn implements ILikeSteamedTurbot {
    }
    

      可以看到,这里Shawn有实现前面泛型规定的“SuitableGirl extends ILikeSteamedTurbot”限制条件,而Lina却没有实现ILikeSteamedTurbot接口,所以自然会报错了。而这个验证的条件,正是使用的泛型。

    泛型不是 一个T

      别打人啊!虽然我这里使用的是SuitableGirl指代泛型,不是传统博客中使用的“ T ”,或者是通配符“ ? ”,但可没有规定只有T才能指代泛型的,这里最好是依据实际的应用环境,大家能给出一个更加易于理解的名字,不然整改代码架构中满满的都是泛型的时候,看到这些“ T ”,在遇到几个不爱写注解的主,你会哭的!好吧,非要举一个官方泛型不用 T 的例子的话,请看:

    泛型不是 T

      显而易见,大家常用的 Map就是不用 T 做泛型的典范,而这里的“ K ”就是“key”,而“V”则是“value”,所以官方都这么用了,你还在等什么!

      什么?为什么Map会有两个泛型?当然人家充钱了呗!

    充钱

      信你个鬼!!!

    糟老头子坏得很

      诶诶诶,别打脸,别打脸!我说,其实Java类中并没有限制每一个封装的类或者接口中具体使用的泛型的数量,用多少完全看你的心情,只要你以后能看得懂。

    贱贱的笑

      啊啊啊,别打脸啊!

    我做错了什么

    啥是泛型

      定义:

    把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型

      设计原则:

    只要在编译时期没有出现警告,那么运行时期就不会出现ClassCastException异常

      还记得之前我们写的女朋友验证的方法吗?忘了?没关系,我再贴出来一份不就好了吗(顺便凑字数,虽然博客不按字数给打赏 T-T)

    /**
     * 判断女孩是都满足做我女朋友的条件
     *
     * @param girls 相亲的女孩
     */
    private <SuitableGirl extends ILikeSteamedTurbot> void judge(SuitableGirl girl) {
        if (girl instanceof ILikeSteamedTurbot) {
            System.out.println(MessageFormat.format("{0}喜欢吃清蒸多宝鱼,我要追她"
                    , girl.getClass().getSimpleName()));
        } else if (girl instanceof IDislikeSteamedTurbot) {
            System.out.println(MessageFormat.format("{0}讨厌吃清蒸多宝鱼,没机会了"
                    , girl.getClass().getSimpleName()));
        }
    }
    

      前面贴出来了女生判断方法调用时的截图,那么这个验证方法中截图后会有什么好玩的事发生吗?

    image.png image.png

      两张图显示的警告信息分别为:

    1. “girl instanceof ILikeSteamedTurbot”的判断条件是多余的,可以不做验证
    2. “girl instanceof IDislikeSteamedTurbot”的验证条件永远都不会满足

      然后连续两次Alt + Enter

    快捷键消除异常

      PS:针对我用的Android Studio,不知道这套快捷键是否适用于所有的idea IDE,如果是用的eclipse快捷键体系的这里就只能自力更生了。

      好吧,系统就是严谨,我一直都没发现自己竟然忘记了判空...好吧,这段掐掉别播,我们接着说正题。可以看到,经过了泛型的验证后,我们的代码简化了好多,这也就是泛型的好处之一。

    为什么要使用泛型

      前面说了这么多,都是直接上来就使用了泛型,并没有明确说明我们为什么要使用泛型。其实从泛型的定义就可以看出泛型的最重要的好处了:

    1. 代码更加简洁【不用强制转换】
    2. 程序更加健壮【只要编译时期没有警告,那么运行时期就不会出现ClassCastException异常】
    3. 可读性和稳定性【在编写集合的时候,就限定了类型】

    代码更加简洁【不用强制转换】

      为了验证这一点,让我们来看看没有泛型的世界:

    需要强转

      在实际的开发中,我们难免会遇到这样的工具类,他们的返回类型会随着传入参数的不同而改变,为了通用性,我们势必无法做到重载(返回值不同并不算重载),因此,就只能使用最简单的玩法,那就是使用最基本的父类Object来接收,而随之而来的,就是我们需要在获取结果的时候进行强转。

      而使用了泛型会有什么不同呢?

    使用了泛型

      显而易见,我们限制了返回值与传入的参数都必须是泛型“VALUE_TYPE”后,当传入的是String后,IDE会自动帮我们进行验证,要求返回值也是String,因此第一行直接使用String接收返回结果的,就是正确的。

      而当我们的传入参数变成String后,可以看到IDE就开始提示我们这么写是错误的了,接收方(result2)是String,而我们方法返回的是个Integer,那反过来呢,如果把接收和传参的类型换一下会怎么样?

    image.png

      可以看到,提示的接收方(result3)是int,而方法返回值是String,同样会报错,也就是说只要传参与接收方不是相同的VALUE_TYPE,就会提示报错。这样我们既省略了强转的步骤,也能避免类型不同导致的异常(这个下一条会进行分析)

    PS:这里需要额外解释说明两点

    1. 前面提到过,虽然我们看到的大多数博客泛型使用的都是 T,但是并不是只有 T 可以指代泛型,我这里就是为了方便这个方法被理解,而使用了“VALUE_TYPE”,也就是“值的类型”,因此大家在使用中,千万别被万能的 T 坑了,反正我个人如果接手了一个泛型全是 T 的项目,绝对会哭死的

    2. 有前面的IED提示我们可以发现,第一张截图中异常行是输入的Integer,result是String,IDE提示我们的是“Change variable ‘result2’ type to ‘Integer’”(请将变量result2的类型改为Integer);而第二张截图中异常行是输入的String,result是int,IDE提示我们的是“Change variable ‘result3’ type to ‘String’”(请将变量result2的类型改为String)。显而易见,他们有一个相同点,就是都是以传入参数为基准来提示我们如何修改的。而我们在实际编程的时候,需求是千奇百怪的,绝对不可能每次都是返回值这里出了问题,所以一定要具体问题具体分析,而不要图个便利,直接使用了IDE提供的便捷调整方案。

    程序更加健壮【只要编译时期没有警告,那么运行时期就不会出现ClassCastException异常】

      同样,我们还是先看看没有泛型的世界:

    不适用泛型

      可以看到我们成功使用addAll方法将一个String的集合添加到了Integer集合中,从而导致了我们取Integer值时出现了类转换异常(ClassCastException)。而再看看,我们为String集合添加上泛型之后呢?

    image.png

      显而易见,IDE会直接提示你,人家要的是一个Integer集合,不能应用String集合,这也就是为什么前面的女朋友验证类中能够进行“女孩是否喜欢清蒸多宝鱼”的验证。并且,验证后在judge方法中的执行操作可以得以简化的原因。

    可读性和稳定性【在编写集合的时候,就限定了类型】

      这条其实是基于前面的两条说的内容,例如集合的例子,我们已经明确了自己要使用的是String的集合(List<String>),大家也易于理解,一旦出现异常的时候,也可以很好的依据这个限制条件去查找,是否有人传了写什么奇奇怪怪的东西进来。

    有奇怪的东西混进去了

    泛型的应用环境

      前面说了那么多还是偏理论性质的,那具体我们的泛型应该怎么用呢?

    泛型方法

      比如我实在靠自己找不到女朋友了,就只能去相亲,相亲就得找人给我介绍,我就得提要求啊:

    public<GirlFriend extends Girl> void wanted(GirlFriend gf){
            
    }
    
    interface Girl{
            
    }
    

       别问我为什么用的“public”,能自产自销女朋友的就不用相亲了!!!

       而介绍人一看,那就赶快给介绍吧:

    public<GirlFriend extends Girl> GirlFriend introduce(GirlFriend gf){
        return gf;
    }
    
    interface Girl{
    
    }
    

       或者是介绍人也找人帮忙介绍的:

    public<GirlFriend extends Girl> GirlFriend introduce(){
        GfDelegate<GirlFriend> delegate = new GfDelegate<>();
        // 省略代理找女朋友的步骤
        return delegate.introduce();
    }
    
    interface Girl {
    
    }
    

      如上这三种情况都是泛型方法,也就是说泛型是在方法中进行设置的“<GirlFriend extends Girl> ”,特点就是泛型只作用于方法内部,与局部变量相似。一般是通用的工具方法封装的时候使用到,例如数组长度的测量:

    /**
     * 判断数组的长度
     *
     * @param array 索要获取长度的数组
     * @return 该数组的长度
     */
    public static <ITEM> int getArrayLength(ITEM[] array) {
        if (array == null) {
            return -99;
        } else {
            return array.length;
        }
    }
    

    泛型类(接口)

      前面说了泛型方法,可是其中却有这么一段代码:

     GfDelegate<GirlFriend> delegate = new GfDelegate<>();
    

      其实现是这样的:

    class GfDelegate<GirlFriend extends Girl> {
        private GirlFriend girlFriend;
    
        private LookForSingleGirl<GirlFriend> lookForOne;
    
        public void setLookForOne(LookForSingleGirl<GirlFriend> lookForOne) {
            this.lookForOne = lookForOne;
        }
    
        public GirlFriend introduce() {
            if (null == girlFriend) {
                girlFriend = lookForOne.lookForOne();
            }
            return girlFriend;
        }
    }
    
    interface LookForSingleGirl<GirlFriend extends Girl> {
        GirlFriend lookForOne();
    }
    

      GfDelegate就是泛型类,而LookForSingleGirl则是代理接口,两者的区别就在于一个是,一个是接口而已,除此之外,没有太大的差别,而我们需要做的就是如何利用他们来搞事情。

    搞事情

      对于泛型类与泛型接口的应用,其实主要遵从的还是类和接口的应用场景,只是添加了一个对于特定参数类型的限制而已,比如说,通用工具类封装和通用父类的封装。

    通用工具类封装——网络请求

      一般而言我们在使用网络请求的时候,接收的结构都是这样的:

    {
        "code":"0",
        "data":{
    
        },
        "message":"请求成功"
    }
    

      这个时候就可以封装通用的网络请求接收Bean(或者pojo或者其他的,看你们公司怎么要求了,也可以看看这篇文章,明确这哥几个都是干什么的:POJO,JAVABEAN,Entity区别

      我做的就是封装一个结果接收的类,然后封装一个接收结果回调的接口:

    /**
     * @author 半寿翁
     */
    public class ResultBean implements Parcelable {
        /**
         * code : 0
         * message : 请求成功
         */
    
        private String code;
        private String message;
    
        public String getCode() {
            return code;
        }
    
        public void setCode(String code) {
            this.code = code;
        }
    
        public String getMessage() {
            return message;
        }
    
        public void setMessage(String message) {
            this.message = message;
        }
    }
    
    public interface NetRequestCallBack<RESULT_BEAN extends ResultBean>{
        public abstract void success(RESULT_BEAN baseBean);
    }
    
    通用父类的封装——刚刚找女朋友的代理
    class MotherInLawDelegate extends GfDelegate<Girl>{
        @Override
        public Girl introduce() {
            return new Girl() {};
        }
    }
    

      如果我找女朋友的代理正好找到了我未来岳母,然后她把自己的女儿直接嫁给我了,那样就是已经很明确了,不需要从外部寻找对应的单身女孩了,这个时候,就可以直接明确对象就是Girl,然后返回的时候,直接返回她构造的女儿就可以了(咳咳,肯定不是现构造,大家领会精神就可以啊)。

    PS:这里new Girl() {}后面有个大括号是因为Girl是接口,实现接口或者抽象类的时候需要实现其中的抽象方法,虽然这里没有抽象方法,但是需要保留{}这个结构

    通配符

      前面说的那么多,都是找女朋友的事,到这里我们意淫一下,找到女朋友,结婚,生了个孩子,孩子上学要带饭(好吧,现在的年轻人都是外面买着吃,大家就领会精神吧)

    @Test
    public void packageFoodTest() {
        packageFood(new Box<>(new Fish()));
        packageFood(new Box<>(new Chicken()));
    }
    
    interface Food {
    
    }
    
    class Fish implements Food {
    
    }
    
    class Chicken implements Food {
    
    }
    
    /**
     * 打包盒
     */
    class Box<F extends Food> {
        private F f;
    
        public Box(F f) {
            this.f = f;
        }
    
        public void printFoodName() {
            System.out.println(f.getClass().getSimpleName() + "打包好了");
        }
    }
    
    private void packageFood(Box<?> box) {
        box.printFoodName();
    }
    

      可以看到,我们的打包盒是使用的泛型,按照正常的使用,我们的packageFood只能提前设定好我们想要打包的是什么,比如鸡肉或者是鱼肉,但是无奈,小孩太贪嘴,那就得两样都给他(她)带着。而我们基本的泛型对于这种情况就只能束手无策了。

      而我们封装的packageFood方法中却有了这么一个东西,让不可能变成了可能,那就是“ ? ”,它存在的意思就是通配符,那就是来者不拒,只要你敢给,我就敢收,并且能够保证正常运行不报错。同样也是用来方便我们在使用泛型时候的一些适配问题,就比如前面说到测量数组的长度,那么我们如果测量集合的长度呢:

    /**
     * 判断集合的长度
     *
     * @param list 索要获取长度的集合
     * @return 该集合的长度
     */
    public static int getListSize(List<?> list) {
        if (list == null) {
            return -99;
        } else {
            return list.size();
        }
    }
    

      这样添加了通配符以后,任何泛型的集合传入,就都可以测量出size了,而不用担心报错或者异常的情况发生。

    PS:

    1. 如果上面的方法,一不小心忘记了输入<?>,只是在传参处写了List,运行的时候发现了,抱着忐忑的心情等着程序崩溃,但是很遗憾,你会看到程序正常执行了,而且关键我写的时候竟然也没有异常警告,气不气人。作为一个菜鸟,暂时还不知道有没有<?>究竟有什么区别,强行解释的话,就只能引用了说这样写不优雅了。
    /**
     * 判断集合的长度
     *
     * @param list 索要获取长度的集合
     * @return 该集合的长度
     */
    public static int getListSize(List list) {
        if (list == null) {
            return -99;
        } else {
            return list.size();
        }
    }
    
    1. 这条原本是想加到上一条结尾的,但是奈何本身写的就已经很长了,这里就别再凑热闹了。使用“ ? ”通配符需要注意是(不添加泛型同样需要注意),在这个方法里面,可以对执行属性的查看操作和删除操作,但是尽量不要做编辑或者删除操作,因为在通配符的作用下,我们的泛型是无法进行严格的限制的,这个时候进行增加编辑很可能会添加错误的类型,而IDE没有泛型验证,也不会报错,为程序留下隐患
    2. 通配符的使用对象必须是对泛型类或者是泛型接口进行操作的时候,是无法直接通过泛型方法去定义,也无法在创建泛型类和泛型接口的时候使用的。
    泛型方法不能使用泛型通配符"?" 泛型类不能直接使用泛型通配符"?"

    泛型通配符上限

      这个通配符的上限的意思是从继承的角度出发的,大家都知道父类和子类的关系吧,那就是子类是继承自父类的。所以从这个角度来说,父类就是上,而子类就是下。

      既然是从继承的角度说明,那就显而易见了,我们之前写的打包食物的部分,如果我们在食物的基础上,再创建一个子类——Meat:

    上限设置

      可以看到当我们设置了上限Meat后,继承自Meat的fish是符合条件的,可以正常调用。而由于egg还是继承自Food,并不是Meat的子类,因此不符合上限的要求,因此会报错。

      在开发中,如果A是B的父类,B是C的父类,而某个方法是在B类中创建并可被继承的时候,我们就可以通过这种设置上限的方式,来实现约束,或者是没有必要查过深的层级的时候,减少资源消耗的时候,可以做上限限制。

    泛型通配符下限

      既然提到了上限,那么相对的,自然也会有下限限制,其使用的关键词就是super,例子如下:

    下限设置

      可以看到Meat自然不用说了,Food是Meat的父类,因此这里都可以正常调用,Fish是Meat的子类,Egg更是跟Meat一点关系都没有(叔伯关系就别来捣乱了),所以看到报错。

    PS: 之所以将Food和Meat的创建提到上面进行,而不是直接在Box的构造方法中直接new,是因为Food与Meat是接口,实例化的时候需要实现里面的抽象方法,即便没有也要保留结构(好像前面说过),并不会影响到结果,但是如果直接放到构造方法中,确实报错的内容与下面的Fish和Egg的一样,这个我这里没有找到对应的解释,如果有哪位大神知道欢迎一起讨论一下**

      使用环境的话,我这里能想到的就是对于父类的方法,我们总会有重写的时候,如果哪个子类将父类中的方法重写到父类都不认识它了,比如删掉了super,这个时候我们就需要通过设置通配符下限的方式,限制到这个重写了方法的子类前的所有父类可以调用,避免异常。

      例如:A是B的父类,B是C的父类,C将B中的父类复写到与原来完全相反的效果了(当然,规范是不允许的,但是规矩是死的人是活的,谁知道能干出来什么事),这个时候再使用这类通用方法的时候,就需要限制成<? super B>。

    通配符区间(同时设置上限和下限)——无法实现(个人的几种尝试均没有实现,如果的大家发现可以实现的方法,可以一起分享一下)

    直接写

      首先最容易想到的,那就是直接写到一起嘛:

    先设置上限,再设置下限
    先设置下限,再设置上限

      错误提示是“意外绑定”,就是IDE都没想到的意思。那既然这种方式行不通,我们换种方法嵌套的方式呢?

    方法嵌套
    1. 先验证下限,再验证上限
    先验证下限,再验证上限

      可以看到父类->子类的顺序是:Food->Meat->Fish->TurbotFish,packageFood的限制是设置的下限(传入的参数必须是TurbotFish的父类或TurbotFish自身),而packageFood2的限制是设置的上限(传入的参数必须是Meat的子类或Meat自身),从上面的父类到子类的关系中很显然可以看到,有符合条件的类的。但是验证的时候根本就没有考虑这个,而是验证下限了后,上限就是不行,别问我什么道理,我就是道理!

    听说有人要跟我讲道理
    1. 先验证上限,再验证下限

      好吧,我承认自己就是为了凑字数,不说了,直接上图吧,都是泪啊!

    先验证上限,再验证下限

    泛型擦除

      前面说的内容基本就是足够泛型的使用了,后面的这部分其实原本我也是不知道的,不过在部门培训中讲了一遍后,有个同事让我讲讲泛型擦除,很显然我被无情的挂在了讲台上,任由冷冷的冰雨在脸上胡乱的拍。

    冷冷的冰雨在脸上胡乱的拍

      其实对于泛型擦除,我现在能想到的也就是了解概念,在正常工作中,除非专门为了写bug,不太有机会用到,但是毕竟我也是个菜鸟,这里就不多哔哔了,反正多学点东西总是没有坏处的。

      前面说过了,我就是个小菜鸟,对于泛型擦除的理解还是比较浅显的,这里给大家推荐frank909大神的Java 泛型,你了解类型擦除吗?,有较深刻的分析。我这里只提取三点用来避坑的(也防止大神博客万一不小心404了,还有地方查看)

    泛型擦除的证明

    List<String> l1 = new ArrayList<String>();
    List<Integer> l2 = new ArrayList<Integer>();
            
    System.out.println(l1.getClass() == l2.getClass());
    

      感官上,一个是String的集合,一个是Integer的集合,很显然,两个Class应该是不同的,但是其结果竟然是true

    结果为true

      都不用运行,IDE就直接告诉我们答案了!!!

    泛型擦除后变成了什么?

      说到擦除,擦除后肯定不能直接就没有了啊,毕竟泛型不仅仅是用来做验证,很可能有一些私有变量就是泛型,不能直接把这些变量擦没了。

      通过反射可以得出,泛型擦除后,会变成他的上限,如果没有设置上限,则会变成Object:

    @Test
    public void packageFoodTest() {
        Box<Fish> box = new Box<>(new Fish());
        Field[] fs = box.getClass().getDeclaredFields();
        for (Field f : fs) {
            System.out.println("Field name " + f.getName() + " type:" + f.getType().getName());
        }
    }
    
    interface Food {
    
    }
    
    interface Meat extends Food {
    
    }
    
    class Fish implements Meat {
    
    }
    
    class Box<F extends Food> {
        private F food;
    
        public Box(F f) {
            this.food = f;
        }
    
        public void printFoodName() {
            System.out.println(food.getClass().getSimpleName() + "打包好了");
        }
    }
    
    擦除后的泛型类型

      可以看到,我们约束了<F extends Food>,所以通过反射可以看到,food的就变成了Food类型,去掉上限限制,我们再来看一下:

    @Test
    public void packageFoodTest() {
        Box<Fish> box = new Box<>(new Fish());
        Field[] fs = box.getClass().getDeclaredFields();
        for (Field f : fs) {
            System.out.println("Field name " + f.getName() + " type:" + f.getType().getName());
        }
    }
    
    interface Food {
    
    }
    
    interface Meat extends Food {
    
    }
    
    class Fish implements Meat {
    
    }
    
    class Box<F> {
        private F food;
    
        public Box(F f) {
            this.food = f;
        }
    
        public void printFoodName() {
            System.out.println(food.getClass().getSimpleName() + "打包好了");
        }
    }
    
    擦除后的泛型类型

      果然,将Box类的泛型换成<F>后,擦除后得到的food就是Object类型的了。

    泛型擦除了还有限制作用吗?

      这里厚着脸皮套用一下frank909大神的代码,可以看到,我们的List泛型是Integer,很显然正常通过add写入“test”这一项的时候肯定会报错,但是用反射直达泛型被擦除成为Object之后操作呢(List的泛型是没有设置上限的)

    @Test
    public void packageFoodTest() {
        List<Integer> ls = new ArrayList<>();
        try {
            Method method = ls.getClass().getDeclaredMethod("add", Object.class);
            method.invoke(ls, "test");
            method.invoke(ls, 42.9f);
        } catch (NoSuchMethodException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (SecurityException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        for (Object o : ls) {
            System.out.println(o);
        }
    }
    
    反射可以规避泛型的验证

      好吧,结果也很显然,反射确实成功的规避了泛型的验证,将String格式的“test”写入了泛型为Integer的集合中。这个例子除了提示我们java确实会将泛型擦除外,也提示了我们在写程序时需要注意的事,那就是慎用反射,尤其在有泛型的时候,尽量避免通过反射直接向泛型类中写入数据,不然会有悲剧发生的。

    List<Integer> ls = new ArrayList<>();
    try {
        Method method = ls.getClass().getDeclaredMethod("add", Object.class);
        method.invoke(ls, "test");
        method.invoke(ls, 42.9f);
    } catch (NoSuchMethodException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (SecurityException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (IllegalArgumentException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    int sum = 0;
    for (Integer integer : ls) {
        sum += integer;
    }
    System.out.println(sum);
    

      由于反射插入错误类型导致的崩溃

    由于反射插入错误类型导致的崩溃

    鸣谢(素材来自网络,如有疑问请联系博主)

    相关文章

      网友评论

        本文标题:《java基础》-胖菜鸟说泛型

        本文链接:https://www.haomeiwen.com/subject/pnqrdktx.html