《Thinking in Java》中谈到有很多原因促成了泛型的出现,C++等泛型的存在等等,但是最引人注目的一个原因就是为了创造容器类。容器类按用途可以分创建容器类和使用容器类,创建可以是?
符号,因为你必须要求使用者知道里面需要装的东西是什么。?
的使用主要用来接受容器类返回值,所以包含用有关?作为方法参数以及返回参数。
一、简介
《Thinking in Java》中谈到有很多原因促成了泛型的出现,C++等泛型的存在等等,但是最引人注目的一个原因就是为了创造容器类。容器类按用途可以分创建容器类和使用容器类,创建可以是?
符号,因为你必须要求使用者知道里面需要装的东西是什么。?
的使用主要用来接受容器类返回值,所以包含用有关?作为方法参数以及返回参数。
图1 泛型继承关系
图2 向上界定
图三向下界定
1.1 基础知识补充
(1)、泛型擦除
Java 一开始就具有的特性,所以在后来想要增加泛型的时候,就必须要兼容以前的版本,Sun 他们想到的折中解决方案就是类型擦除;意思就是泛型的信息只存在于编译期,在运行时期所有的泛型信息都被擦除了,就想没有一样;
(2)、不能用基本类型实例化类型参数
1 | new ArrayList<int>(); // error |
(3)、不能用于运行时类型检查
1 | t instanceof T // error |
但是可以使用 clazz.isInstance();
进行补偿;
(4)、不能创建类型实例
1 | T t = new T(); // error |
同样可以使用 clazz.newInstance();
进行补偿;
(5)、不能静态化
1 | private static T t; // error |
(6)、不能抛出或捕获泛型类的实例
1 | catch (T t) // error |
因为在捕捉异常时候需要运行时类信息,并且判断异常的继承关系,所以不能抛出或捕获泛型类的实例;
(7)、不允许作为参数进行重载
1 | void test(List<Integer> list) |
因为在运行时期泛型信息被擦除,重载的两个方法签名就完全一样了;
(8)、不能创建泛型数组
对于一点我觉得是最重要的,关于数组的介绍可以参考,Array 相关 ;
1 | List<String>[] lists = new ArrayList<String>[10]; // error |
之所以不能创建泛型数组的主要原因:
- 数组是协变的,而泛型的不变的;
- 数组的
Class
信息是在运行时动态创建的,而运行时不能获取泛型的类信息;
根据上面的讲解可以看出所谓的擦除补偿或者擦除后的修正,其大体思路都是用额外的方法告知运行时的类型信息,可以是记录到局部变量,也可以是指定参数的确切类型(Array.newInstance(Class componentType, int length)
);
二、创建型
有关创建型我想从容器的用途来分析如何使用泛型class Holder
2.1、无所不容
这个容器我想所有东西都能装。此时有两种实现:(1)、通过属性Object[]数组。(2)、通过T泛型类
2.1.1、通过属性Object[]数组
1 | //这个容器啥都能装 |
2.1.2、通过T泛型类
1 | // 创建 |
两种方式比较:
对于第二种很明显具有更多的好处。原因1、因为第一种生来就无所不容,范围太广泛了。第二种是使用者决定是不是无所不容。例如第一种天生可以装水果也可以装动物,使用者没有办法加以限制。对于第二种使用者可以人为的在使用时候,进行加以限制,例如用来装水果或者用来装动物。
2.2、来自创建者的限制
有关于1的无所不容,创建者基于使用者很大的权限去使用。但是有时候对于创建者来说,我天生就要求你只能装水果,这个容器仅仅用来装水果,使用者也只能添加水果。此时依然有两种实现:
2.2.1 通过属性Fruit[]数组
1 | //这个容器啥都能装 |
2.2.2 通过创建泛型类class Holder
1 | // 创建 |
两者的比较:两种方式依然第二种更具有灵活性,原因:对于属性Fruit[]数组方式,天生的能力是可以装任何水果,Apple、Banana都可以,二、第二种使用者可以自己稍加限制,例如使用者想装所有水果只需要Holder
2.2.3 学习泛型的误解
创建个可以装Apple或者Fruit或者Object的容器类。(仅仅用来装父类,没有这个东西🙃)
😂class Holder
三、使用型
有关使用型如果从容器对象的创建也可分为,创建容器对象以及接收容器对象。前者是创建者自己创建使用,可能是自己使用也可能装完数据给别人使用。后者就是别人给我一个容器我怎么接收和使用。
3.1 创建容器对象及其使用容器
我们创建容器对象很显然我们应该清除容器对象内装载的是什么例如,List
3.2 容器接收及使用
容器对象有了,可是麻烦的是有时候我们并不知道容器内会装着什么,所以我们应该如何接收这个容器对象呢?
例如蕊蕊有个现在有个容器对象,她想把容器交给我,让我对容器进行一些可能的操作,我该怎么接受她给我的容器呢?
(1)、心蕊说她只装了水果:我可以这样来接受 public int count(List<? extends Fruit> list);
使用:
1 | public static double sumOfList(List<? extends Number> list) { |
蕊蕊可能传过来的容器:
1 | List<Apple> apples; |
(2)、心蕊说她也不知道这个容器里装了什么,因为这个容器别人给她的:public int count(List<?> list);
使用:
1 | public static void printList(List<?> list) { |
蕊蕊可能传过来的容器:
1 | List<Apple> appleList; |
(3)、她说她只装了苹果:public int count(List list);
1 | List<Apple> appleList; |
注意:
1 | public static void printList(List<Object> list) |
(4)、心蕊说她装了苹果的父类.:public int count(List<? super Apple> list);
使用:
1 | public static void addNumbers(List<? super Apple> list) { |
蕊蕊可能传过来的容器:
1 | //任何可以保存 Apple值的方法。 |
(5)、PECS原则
extends T> 不能往里存,只能往外取(被称作*协变*) super Fruit> 不影响往里存,但是往外取只能放在 `Object`(被称作*逆变*) PECS(Producer Extends Consumer Super) Producer Extends 你写的类是主要作为生产者向外提供数据,那么就用 extends Consumer Super 你写的类是主要作为消费者,需要吃进数据,那么就用 super # 四、泛型继承关系 ![genericspayloadListHierarchy.gif](https://vera.oss-cn-hangzhou.aliyuncs.com/markdown/img/20200820193740.gif) ![genericswildcardSubtyping.gif](https://vera.oss-cn-hangzhou.aliyuncs.com/markdown/img/20200925101546.gif) # 实例 例如现在我想要一个Class,无所谓他是哪个具体的Class,只要是Class就行,所以我就用?来接1 | Type type = Fish.class;//或者Type type = Apple.class; |
,是因为 Class
1 | //方法 |
四、类型转换
?不能定义参数只能用来接收不确定的泛型类。
Object: 所有类的基类,需要进行强制转换。但如果转换错误,只有在运行时才会抛出异常。
泛型T: 可以限定任何类型,在编译时就可以检查。不需要进行强制转化。
?: 可以接收不确定的类型,和Object类似,但可以缩小范围:? extends A
,这样就只能接收A类的子类。
五、自限定类型
1 | class SelfBounded<T extends SelfBounded<T>> |
声明了一个类型 T
,它是 SelfBounded
的子类型,这似乎是一个无限循环。其实自限定类型是为子类提供了一个模板,泛型 T
在子类中只能表示子类这个具体类,而不能是其他类型。
1 | interface SelfBoundSetter<T extends SelfBoundSetter<T>> { |
5.1 自限定类型的具体例子
如果你去看 Enum
的源码,就会发现,Enum
的声明是自限定的类型。
1 | public abstract class Enum<E extends Enum<E>> { |
一个具体的枚举类型 Color
在编译时就被翻译成了 Color extends Enum
。如我们前面所说,自限定类型主要目的是为子类提供一种模板,我们来看看枚举类 Enum
是怎么用这个模板的:
1 | public abstract class Enum< E extends Enum<E>> implements Comparable< E >, Serializable { |
如果我们声明一个具体的媒体类型 Color
:
1 | enum Color {RED, BLUE, GREEN} |
编译器会将它翻译成:
1 | public final class Color extends Enum<Color> { |
其中 Color
的 Color.compareTo
方法应该使用一个 Color
类型作为参数,自限定类型刚好就能满足这样的模板。
5.2 自限定类型的缺点
从名字上来看,自限定这个名字会让我们误以为像上面的 Color
类一样,其中的泛型方法只能接受子类自己,比如 Color.compareTo
只能接受 Color
类型的参数。其实不然,其实自限定类型还可以接受它的兄弟类。
1 | public interface SelfBound<E extends SelfBound<E>> {} |
SelfBound2
接受了它的兄弟类型 SelfBound1
,而不是像 SelfBound1
一样接受的自己。如果以为 SelfBound2
的声明只能接受它自己 SelfBound2
,那就完全错误了。因此自限定这个叫法有点名不符其实。
六、捕获转换
?
被称作无界通配符,它有一个特殊的应用场景叫做捕获转换。
1 | public class CaptureConversion { |
类型捕获的地方当然可以写已声明的类型 T
。但是这样会降低程序的可读性,因为在 f2()
中根本用不到泛型 T
相关的知识。而且,使 f2() 产生了协变,让方法 f2()中只能从 holder 中取数据,而不能写数据。但是将 holder 传给方法 f1() 后,由于方法 f1() 有声明了不变的泛型
,因此 holder
又被类型投影为了不变的类型。
六、参考
1 | https://www.jianshu.com/p/0808c0029b2d |
- 本文作者: 初心
- 本文链接: http://funzzz.fun/2021/01/04/泛型学习之详解object、T、?/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!