java泛型

泛型类

如何定义一个泛型类呢?泛型类和普通类的区别是泛型类的类名后面需要加上一个<泛型符号>,如下图所示:
图1
当然,我们也可以使用多个泛型符号,并且泛型符号的可以为任意字母,如下图所示:
图2
但是最好按照大家约定俗成的命令规范来,即E表示element,T表示type,K表示key,V表示value。
但是注意,不能一个泛型符号都没有,例如下图这样会在编译阶段报错:
在这里插入图片描述
很多人也将泛型符号如E、T等视作和函数的形参一样,是一个占位符号,在实例化该类时通过传入的“实参”进行赋值。因此在使用泛型符号时,就把它当作一个已知类型使用即可。只有真正到该类被实例化的时候才知道E、T这些符号真正的类型。创建泛型类实例的方式如下所示,其中new test5<String>中的String可以省略。
在这里插入图片描述
泛型类中的方法也可以使用泛型符号,如下图所示:
在这里插入图片描述

注意,java编译器此时会检查你传入的对象是否真的是和“实参”一样,如果你创建泛型类实例时传入的是String类型,但是后面使用泛型方法时又传入其他类型如Integer,那么编译器会提示错误,如下所示:
在这里插入图片描述
如果泛型类的成员变量也是一个泛型类,那么该泛型类的泛型依旧可以使用该泛型,如下所示,ArrayList类是一个泛型类,它的泛型参数可以是E:
在这里插入图片描述

还有一个需要注意的点,不能用static关键词修饰泛型变量,不然会在编译阶段报错,例如下图这样。这里之所以会报错,是因为static定义的变量会在类加载阶段就被初始化,这个时候对象还没有被创建,也就是说此时还没有走到“实参”String给“形参”E赋值那步骤。也就是类的加载顺序决定了static关键词不能修饰泛型变量。静态方法也同理。
在这里插入图片描述

如果在创建泛型实例的时候不传入”实参“,那么其实这些没被传入的”实参“泛型符号都将被视作Object类型,如下图所示:
在这里插入图片描述

如果有多个泛型形参,要么都传参,要么都不传参,不能只传一部分,如下所示。有E和T两个泛型形参,但却只传入了一个实参String。
在这里插入图片描述
如果我要继承一个泛型类,我应该怎么写,此时有两种方式。第一种是在继承的时候就把父类中的泛型全部确定下来了,这样子类中就不再含有泛型,那么子类不是泛型类。如下图所示,下图中不能再使用父类中的泛型符号了,不然编译器就会报错,说不知道这个泛型符号的意思。
在这里插入图片描述
第二种是继续让子类使用和父类一样的泛型,此时子类也是一个泛型类,再次让泛型确定的时间推迟到实例化子类的时候,如下图所示:
在这里插入图片描述
其实这里Animal中的泛型符号不一定要和定义时的一样,但是要保证子类和父类的泛型符号一样,如下图所示。为什么要保证子类和父类的泛型符号一样呢?因为我们在实例化子类的时候,也会实例化其父类,此时的泛型实参也会传递给父类,若子类与父类的泛型符号不一致,那么父类中的和子类不一样的泛型将永远不会被赋值,这是编译器所不允许的。
在这里插入图片描述
注意,第二种情况下,子类的泛型符号不能少于父类的,如果少于,编译器会报错,如下图所示:
在这里插入图片描述
但是子类可以增加新的泛型符号,如下图所示:
在这里插入图片描述

泛型接口

使用方法和泛型类基本一致,不再赘述。

泛型方法

泛型方法并不是在泛型类中定义的方法,不管是泛型类还是普通类,都可以定义泛型方法。想要定义泛型方法,只需要在方法的返回值前面添加<泛型符号>即可。因此,泛型方法中的泛型符号只能在该泛型方法中使用,并且该泛型的确定是在调用该泛型方法时通过传入的实参确定,如下所示:
在这里插入图片描述
注意,普通类中是可以使用静态泛型方法的(前面提到的泛型类不行),如下所示。原因是泛型方法中的泛型是在使用该方法的时候就确定。
在这里插入图片描述
泛型方法和泛型类的泛型完全独立,两者没有任何关系,下面我故意将Test12类的泛型和test函数的泛型都设置成T,但是却发现对泛型类T传入String,但是对test泛型方法传入int,编译器不会报错,而泛型方法中的泛型的真正类型和调用该方法时的传入的参数有关,和他所属类,以及所属类的泛型完全无关,如下所示:
在这里插入图片描述
泛型方法与可变参数可以结合,此时的可变参数可以看作时一个数组,这个数组中存放着数量不确定的泛型符号。如下图所示,泛型可变参数方法的输出可以时各种类型都可以,此时的e变量需要当作一个数组对待,这个数组中的元素就是实际传入的实参。
在这里插入图片描述

通配符上下限

在说泛型的上下限之前,先说一下类的父类变量可以引用子类对象,也就是我们常说的多态,如下图:
在这里插入图片描述
但是泛型并不能多态,你泛型符号指定的是什么,就必须传入什么,如果你的泛型符号是Animal,那么即使传入Person泛型就会报错,如下图所示。Zoo类的print方法的参数是一个指定类型为Animal的列表,此时我们传入指定类型为Person的列表,无法进行类似于前面提到的”多态“,编译器会进行报错,这两个泛型引用之间的类型不匹配。
在这里插入图片描述
那既然这两个列表中的元素满足父子关系,那么能不能用List<Person>中的元素赋值给List<Animal>中的元素呢?答案是当然可以,如下图所示:
在这里插入图片描述
但是如果我们非要让List<Animal>能够接受List<Peron>类型的变量的话,应该怎么做呢?此时,就需要我们的通配符<?>出场了,通配符表示任意的泛型类型都可以,下图的代码是可以成功运行的,其实这里还涉及到另一个知识点,其实java中真正的父类是不能强转成子类的,但是这里虽然类型是Object,但是里面实际的引用对象还是Person,因此这种强转方式是可以的。
在这里插入图片描述
通过上述分析我们也发现了,上图这段代码在强转部分可能会报错,如果我此时传入的不是List<Person>,那么就会出现强转错误。因此我们需要在通配符的基础上再加上限制,也就是规定该泛型的上限为Animal,那么传入的列表的范围就是List<Animal或Animal的子类>,如下所示。此时还有一个好处是,该方法中可以直接使用Animal这个泛型符号,不用再写死强转了,更加灵活。
在这里插入图片描述
这里注意,此时的animals列表无法进行元素的添加,如下图所示,如何理解呢?可以试想一下如果编译器允许我们添加元素,那我们添加的元素一定属于Animal的子类,Animal可以有多种不同的子类例如Dog和Cat。假设你在创建这个泛型列表的时候使用的是List<Cat>,你很容易就以为拿出来的都是Cat类型,但其实不然。如果拿到的是Dog子类,会发生错误,所以编译器干脆不让我们添加新的元素了。
在这里插入图片描述
与泛型上限对应的泛型下限也很好理解,下图中的Zoo类的print方法中传入的列表范围是List<Person或Person的父类>。但是在遍历这个列表时,列表中的元素都被当作Object对象来对待,因此调用实际方法时需要强转。
在这里插入图片描述
此时的列表是可以添加元素的,但是必须是Person或Person的子类,如下图所示。如果你创建一个List<Person>,你会期待拿到的也是Person对象,由于列表中能传入的元素都是Person或Person的子类,因此取出的元素肯定是能赋值给Person这个父类的。
在这里插入图片描述
总结一下就是:<? extends T> 不可以添加元素,但可以取出类型为T的元素。 <? super T> 可以添加T或者T的子类,取出的一定是object类。

泛型边界

在泛型方法中可以约束传入进来的泛型实参的边界,即属于某个类型的子类,如下图所示,此时传入
在这里插入图片描述
但是注意,这个和泛型上下界不一样,这个没有super关键词,如下所示的代码会报错:
在这里插入图片描述

类型擦除

Java中的泛型信息只存在于编译阶段,它的本质是将我们使用的数据类型参数化,类似于函数那样,弄个形参占个位,真正的类型在传参的时候才确定。在进入JVM之前,与泛型相关的信息会被擦除掉,也就是所谓的类型擦除。
下图是类型擦除的一个例子,List<String>和List<Integer>的类文件都一样。也就是在JVM面前,根本没有所谓的List<String>和List<Integer>对象,而只有List这一种对象,String和Integer好像被人擦掉了一样。
在这里插入图片描述
类型擦除的做法是将所有的泛型都变成其第一个上界,<T>会变成Object。<T extends Number>会被擦除成Number。

你可能会想,java为什么要把泛型给擦除掉,把泛型看作数据类型的一部分不好吗?之所以会将泛型擦除,是为了向JDK1.5版本之前的代码兼容,在JDK1.5之前没有泛型类,所有都是普通类。

你可能会想,这样擦除确实不会出错,但是所有的泛型符号都变成其第一个上界了,那我真正传入的”实参“泛型到底存在哪里的呢?答案就是class文件中的Signature属性表中。因此,我们可以可以通过反射拿到泛型符号代表的真正类型。但是并不是所有的泛型信息都会保存下来,知乎上的这个回答https://www.zhihu.com/question/346911525说局部变量中的泛型信息不会被记录下来。

桥接方法

假设一个子类Person继承了泛型Animal类,也重写父类的void getFriend(E e)为void getFriend(String e),根据泛型擦除,父类的这个方法会变成void getFriend(Object e),这违背了java重写规则,但是我们还是能编译通过,这是为什么呢?答案是编译器自动在子类Person种生成了一个void getFriend(Object e)方法,被称作桥接方法,在这个方法种中调用了子类真正重写的void getFriend(String e)方法。

总结

Java的泛型是JDK 5引入的一个特性,它提供了编译时类型安全检测机制,这使得代码可以灵活地适用于多种数据类型。

java泛型将类型参数化:允许在类、接口或方法创建时指定抽象的类型占位符(如E、T、K、V等),随后在实际使用时指定具体的类型。比如,List是一个通用的列表,使用时可以指定List、List等确定的类型。

编译时类型检查:泛型使得类型转换在编译时期就进行了检查。如果类型不匹配,编译器可以抛出错误,避免了运行时ClassCastException的风险,提高程序的稳定性。

泛型擦除:Java 的泛型是使用擦除来实现的,这意味着泛型类型信息只存在于编译阶段。在运行时,所有的泛型类型信息都会被擦除掉,并且替换为原始的裸类型(Raw Type,即擦除泛型信息前的基础类型,如List等),但是类型转换会根据泛型信息保留在字节码之中,确保了类型的正确性。

泛型边界:可以通过extends关键字为泛型类型指定边界,这意味着你可以限定使用泛型时的类型范围。例如<T extends Number>限定了T类型必须是Number类或其子类。

通配符:泛型还支持使用问号(?)作为通配符,表示未知的类型。通配符可以有上界(? extends Type)和下界(? super Type),分别表示匹配此类型或其子类型和此类型或其父类型。

  • 21
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值