
Java中泛型机制
图片来源于网络
写在前边:
博主今年虽然都大三了😂,但是Java基础并不是很好,尽管可以做一点微小的项目,但是其中的道理依旧不怎么懂。最近一直在补基础知识,博主会把学习过程中的心得分享到这里哦,也会把自己走过坑分享到这,供大家参考。好啦,现在让我们步入正题吧。
今天在做笔试题的时候,突然遇到泛型的问题,简单的题还会一点,可是难一点就不会了啊。好头疼,泛型是jdk1.5的新增功能,在课堂上并没有教的太透彻。只能各种查阅资料,《深入理解Java虚拟机》、《Java从入门到精通》(真是本神书)、大佬的博客、老刘的公众号能查的资料都查了,终于是明白了一点,然后博主就总结了一下,写了这篇文章。可是仍有不足之处。欢迎大家在评论区补充哦~如果你觉得有帮助就帮我点个小心心呦~
正文:
为什么要有泛型?
在泛型出现之前,集合类里可以放任意类型的元素。就像这样
List list=new ArrayList(); list.add(new Double(2.9)); list.add(new Person()); list.add(new Dog()); list.add(new Girl());
那么问题来了。我们要怎么遍历呢?如果用for(Object obj:list),类型不就丢失了吗?小仙女灵机一动想出了个办法:可以强制转换啊,那不就好了吗?可是到底遍历到的是什么类型,我们没法确定啊,一旦转错了,运行时又要报异常,程序员哥哥又要通宵改Bug了。如果你想按顺序强制转换的话,那…….就算List是有序的,那还有Set、Map呢,到底遍历到什么类型真的不知道,也没有必要知道,于是泛型闪亮登场!
List<Person> list=new ArrayList<Person>(); list.add(new Double(2.9));//编译失败 list.add(new Dog());//编译失败 list.add(new Girl());//编译失败 list.add(new Person()); list.add(new Person());
这样就可以在编译时确定类型了,避免了很多运行时异常,这样做这个list里就只有Person对象了。我们在遍历的时候就可以顺利的这样for(Person person:list)遍历集合中的元素。
泛型通配符“?”到底是什么?
我现在有5个类分别是BabyGirl,BabyBoy,Father,Grandfather和Test
他们的派生关系如下(自己画的,略丑~),其中Test是测试类:
好啦,现在我们可以在测试类里使用类型通配符定义2个集合,就像这样
List<? super Father> listsuper; List<? extends Father> listextends;
第一个是使用<? super T>表示限制类型是T或T的父类型
第二个是使用<? extends T>表示限制类型是T或T的子类型
<? super T>又叫下届通配符(刚开始我也不懂为什么这样叫)
<? extends T>又叫上届通配符
然后呢,使用普通的泛型定义5个集合
List<Grandfather> listgrandfather = new ArrayList<Grandfather>(); List<Father> listfarher = new ArrayList<Father>(); List<BabyGirl> listgirl = new ArrayList<BabyGirl>(); List<BabyBoy> listboy = new ArrayList<BabyBoy>(); List<Object> listobj = new ArrayList<Object>();
重点:类型通配符只能限定类型的范围,而且在创建对象时,运行时类型必须指定一个类型
下面我们来为listsuper和listextends这两个集合初始化八~
listsuper = listgirl;//编译失败 listsuper = listboy;//编译失败
为什么第一行和第二行就编译失败了呢?
<? super Father>为它初始化的时候,运行时类型必须得是Father或者Father的确定的父类(注意是或)
如下代码就可以编译成功
listsuper = listfather; listsuper = listgrandfather; listsuper = listobj;
那么listextends这样赋值会怎样呢?
listextends = listgirl; listextends = listboy; listextends = listfather; listextends = listgrandfather;//编译失败 listextends = listobj;//编译失败
这是因为<? extends Father>为他初始化的时候,运行时类型必须得是Father或者Father确定的子类型
下面,我们来看看add方法的编译效果八~
listsuper.add(new Father()); listsuper.add(new BabyBoy()); listsuper.add(new BabyGirl()); listsuper.add(new Grandfather());//编译失败 listsuper.add(new Object());//编译失败
我试过了不管listsuper的运行时类型不管是Father、Object还是Grandfather都是一样的结果,对于add方法List<? super Father>这种类型,用它定义的变量是不能向里边添加Father的父类型的对象的,你可能现在有点懵,因为刚才说了List<? super Father>里边限定的类型是Father或其父类,可是为什么只能往listsuper里边add子类型的对象呢?前方高能
根据面向对象的多态特性,我们已经规定了listsuper的运行时类型只能Father或它的父类,可是到底是那个父类?编译器并不知道啊,编译器只知道是它的父类,所以没有办法确定往里边放的具体是哪一个的父类的类型,但是根据面向对象的多态性可以往里边加入Father或它的子类对象。比如Father boy=new BabyBoy();就是多态创建对象
listextends.add(new Father());//编译失败 listextends.add(new BabyBoy());//编译失败 listextends.add(new BabyGirl());//编译失败 listextends.add(new Grandfather());//编译失败 listextends.add(new Object());//编译失败
listextends的add()方法竟然没有编译通过的,这是因为List<? extends Father>会让add方法失效,这种方法定义的变量表示只能放Father或Father的子类型。但是到底是哪一个子类型编译器根本并不知道,要知道每一个子类都是不一样的类型,不管是往里边添加哪一个都有可能会在运行时出现异常,编译器为了避免这个异常,干脆就不让add了。而且如果集合里本来就有元素的话,也是只能取,不能add确定类型的元素。
如果你还不懂,可以这样理解,listextends的运行时类型编译器并不知道到底是那个类型,如果是Father的话,集合里可以放入Father、BabyGir和BabyBoy,如果是BabyGirl就只能放BabyGirl及其子类(尽管它没有子类)这时候如果再往里放入一个BadyBoy或Father都是不对的,因为他们根本就是同一个类型,你不可以说男宝宝是女宝宝,也不可以说女宝宝可以生一个爸爸吧!而且编译器也不知道里边放的到底该是Father、BabyGirl还是BabyBoy,编译器干脆就不让add了。
补充:评论区有大神指出listextends可以add(null),我试了一下,确实是可以的。null可以转化成任意类型的元素,所以才可以使用add()方法。
接下来我们看一下get()方法的编译效果
BabyBoy babyBoy0 = listsuper.get(0);//编译失败 BabyGirl babyGril0 = listsuper.get(0);//编译失败 Grandfather grandfather0 = listsuper.get(0);//编译失败 Father father0 = listsuper.get(0);//编译失败 Object object0 = listsuper.get(0) ;
get方法只可以用Object接收,这是为什么呢??
原因很简单!编译器只知道List<? super Father> 定义的类型里边只能放Father以及它的父类,但是究竟是哪一个父类编译器并不知道啊,用哪一个父类去接收这个值都可能出现异常,所以就只能是Object(最大的父类)这个值。
但是对于listextends就不会这样请看下面一段代码
BabyBoy babyBoy = listextends.get(0);//编译失败 BabyGirl babyGirl = listextends.get(0);//编译失败 Grandfather grandfather = listextends.get(0); Father father = listextends.get(0); Object object = listextends.get(0);
我们发现List<? extends Father>的get方法还是能用的,由于是里边装的全是Father或者Father子类型,可以使用它和它的父类也就是Father,GrandFather,Object接收。但到底是Father、BabyGirl还是BabyBoy,编译器根本不知道,那就不能用Father的子类型接收了。还有一点要强调一下为什么这种方式定义的变量不能add却能取呢?那是因为这种方式定义的集合可能本来就有元素。
listgirl.add(new BabyGirl()); listextends = listgirl;//这样集合里就有元素了吧
突然发现教别人真的是可以帮助到自己的,哈哈,我现在也明白了~
总结:
Java里的泛型和C++还不一样呢。在Java中其实不是真正的泛型,Java里泛型其实就是伪泛型。只在编译时检查,运行时擦除了,有人说反射可以跳过泛型检查,不过我还没有尝试过,就不详细说到底怎么操作啦~😜
还有就是<? super T>是下届通配符,表示限定其类型为T或T的父类
<? extends T>是上届通配符,表示限定其类型为T或T的子类
<? extends Father>就表示限定范围是父亲和父亲的孩子们限定了类型的上限所以叫上届通配符,<? super Father>就表示限定范围是父亲和父亲的父亲们限定了类型的下限所以叫下届通配符(我的个人理解)
<? super T>只能用于传参(add),不能用于返回(get),返回会类型丢失
<? extends T>只能用于返回(get),不能用于传参数(add)