前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面试官问我:“泛型擦除是什么,会带来什么问题?”

面试官问我:“泛型擦除是什么,会带来什么问题?”

原创
作者头像
Android技术干货分享
修改2020-03-19 17:45:53
5400
修改2020-03-19 17:45:53
举报
文章被收录于专栏:Android技术分享Android技术分享

前言

这是我之前在抖音二面的时候自我感觉没有答好的一题。因为我的中心只是围绕在了TObject替换的问题上了,并没有去讲解他会带来的问题。

思维导图

什么是泛型擦除?

其实我们很常见这个问题,你甚至经常用,只是没有去注意罢了,但是很不碰巧这样的问题就容易被面试官抓住。下面先来看一段代码吧。

代码语言:javascript
复制
List list = new ArrayList();
List listString = new ArrayList<String>();
List listInteger = new ArrayList<Integer>();

这几段代码简单、粗暴、又带有很浓厚的熟悉感是吧。那我接下来要把一个数字1插入到这三段不一样的代码中了。

作为读者的你可能现在已经黑人问号了????你肯定有很多疑问,这明显不一样啊,怎么可能。

代码语言:javascript
复制
public class Main {
    public static void main(String[] args) {
        List list = new ArrayList();
        List listString = new ArrayList<String>();
        List listInteger = new ArrayList<Integer>();

        try {
            list.getClass().getMethod("add", Object.class).invoke(list, 1);
            listString.getClass().getMethod("add", Object.class).invoke(listString, 1);
            // 给不服气的读者们的测试之处,你可以改成字符串来尝试。
            listInteger.getClass().getMethod("add", Object.class).invoke(listInteger, 1);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("list size:" + list.size());
        System.out.println("listString size:" + listString.size());
        System.out.println("listInteger size:" + listInteger.size());
    }
}

不好意思,有图有真相,我就是插进去了,要是你还不信,我还真没办法了。

探索真相

上述的就是泛型擦除的一种表现了,但是为了更好的理解,当然要更深入了是吧。虽然List很大,但却也不是不能看看。

两个关键点,来验证一下:

  1. 数据存储类型
  2. 数据获取
代码语言:javascript
复制
// 先来看看画了一个大饼的List
// 能够过很清楚的看到泛型E
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{       
    // 第一个关键点    
    // 还没开始就出问题的存储类型
    // 难道不应该也是一个泛型E?
    transient Object[] elementData;

    public E get(int index) {
        rangeCheck(index);

        return elementData(index); // 1---->
    }

    // 由1直接调用的函数
    // 第二个关键点,强制转化得来的数据
    E elementData(int index) {
        return (E) elementData[index];
    }
}

我想,其实你也能够懂了,这个所谓的泛型T最后会被转化为一个Object,最后又通过强制转化来进行一个转变。从这里我们也就能够知道为什么我们的数据从前面过来的时候,String类型数据能够直接被Integer进行接收了。

带来什么样的问题?

(1) 强制类型转化

这个问题的结果我们已经在上述文章中提及到了,通过反射的方式去进行插入的时候,我们的数据就会发生错误。

如果我们在一个List<Integer>中在不知情的情况下插入了一个String类型的数值,那这种重大错误,我们该找谁去说呢。

(2)引用传递问题

上面的问题中,我们已经说过了T将在后期被转义成Object,那我们对引用也进行一个转化,是否行得通呢?

代码语言:javascript
复制
List<String> listObject = new ArrayList<Object>();
List<Object> listObject = new ArrayList<String>();

如果你这样写,在我们的检查阶段,会报错。但是从逻辑意义上来说,其实你真的有错吗?

假设说我们的第一种方案是正确的,那么其实就是将一堆Object数据存入,然后再由上面所说的强制转化一般,转化成String类型,听起来完全ok,因为在List中本来存储数据的方式就是Object。但其实是会出现ClassCastException的问题,因为Object是万物的基类,但是强转是为子类向父类准备的措施。

再来假设说我们的第二种方案是正确的,这个时候,根据上方的数据String存入,但是有什么意义存在呢?最后都还是要成Object的,你还不如就直接是Object

解决方案

其实很简单,如果看过一些公开课想来就见过这样的用法。

代码语言:javascript
复制
public class Part<T extends Parent> {

    private T val;

    public T getVal() {
        return val;
    }

    public void setVal(T val) {
        this.val = val;
    }
}

相比较于之前的Part而言,他多了<T extends Parent>的语句,其实这就是将基类重新规划的操作,就算被编译,虚拟机也会知道将数据转化为Parent而不是直接用Object来直接进行替代。

应用场景

该部分的思路来自于Java泛型中extends和super的区别?

上面我们说过了解决方案,使用<T extends Parent>。其实这只是一种方案,在不同的场景下,我们需要加入不同的使用方法。另外官方也是提倡使用这样的方法的,但是我们为了避免我们上述的错误,自然需要给出一些使用场景了。

基于的其实是两种场景,一个是扩展型super,一个是继承型extends。下面都用一个列表来举例子。

统一继承顺序
代码语言:javascript
复制
// 承载者
class Plate<T>{
    private T item;
    public Plate(T t){item=t;}
    public void set(T t){item=t;}
    public T get(){return item;}
}

// Lev 1
class Food{}

// Lev 2
class Fruit extends Food{}
class Meat extends Food{}

//Lev 3
class Apple extends Fruit{}
class Banana extends Fruit{}
class Pork extends Meat{}
class Beef extends Meat{}

//Lev 4
class RedApple extends Apple{}
class GreenApple extends Apple{}
<T extends Parent>

继承型的用处是什么呢?

其实他期待的就是这整个列表的数据的基础都是来自我们的Parent,这样获取的数据全部人的父类其实都是来自于我们的Parent了,你可以叫这个列表为Parent家族。所以也可以说这是一个适合频繁读取的方案。

代码语言:javascript
复制
Plate<? extends Fruit> p1=new Plate<Apple>(new Apple());
Plate<? extends Fruit> p2=new Plate<Apple>(new Beef()); // 检查不通过

// 修改数据不通过
p1.set(new Banana());

// 数据获取一切正常
// 但是他只能精确到由我们定义的Fruit
Fruit result = p1.get();
<T super Parent>

扩展型的作用是什么呢?

你可以把它当成一种兼容工具,由super修饰,说明兼容这个类,通过这样的方式比较适用于去存放上面所说的Parent列表中的数据。这是一个适合频繁插入的方案。

代码语言:javascript
复制
// 填写Food的位置,级别一定要大于或等于Fruit
Plate<? super Fruit> p1=new Plate<Food>(new Apple());
// 和extends 不同可以进行存储
p1.set(new Banana());
// get方法
Banana result1 = p1.get(); // 会报错,一定要经过强制转化,因为返回的只是一个Object
Object result2 = p1.get(); // 返回一个Object数据我们已经属于快要丢失掉全部数据了,所以不适合读取

以上就是我的学习成果,如果有什么我没有思考到的地方或是文章内存在错误,欢迎与我分享。

最后

最后我想说:对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 思维导图
  • 什么是泛型擦除?
    • 探索真相
    • 带来什么样的问题?
    • 解决方案
    • 应用场景
      • 统一继承顺序
        • <T extends Parent>
          • <T super Parent>
            • 最后
            相关产品与服务
            数据保险箱
            数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档