彻底搞懂泛型
因为JAVA的OOP特征实现得实在太经典了,以至于后来很多OOP语言都会借鉴它,所以我会先借用JAVA语言来阐释泛型这个概念,本文最后再用TypeScript来讲前端领域的泛型。
01.为什么要有泛型
没有泛型的年代,JAVA里面要定义好各种class,并为变量指明明确的类型,所以给变量赋值的时候经常会用到强制转换:
- /* JAVA */
-
- B b = (B) someObject;
这种强制转换在编译时不报错,但是在运行时可能抛出ClassCastException
异常。说个形象点的例子:
- /* JAVA */
-
- public class Coffe1891{
-
- class Water{
- }
-
- class Wine{
- }
-
- public static void main(String[] args) {
- Coffe1891 coffe1891= new Coffe1891();
- Map map = new HashMap();
- map.put("water", coffe1891.new Water());
- map.put("wine", coffe1891.new Wine());
-
- Wine wine = (Wine) map.get("water");
- System.out.println(wine);
- }
-
- }
这段代码的意思是:我们在map中放了一杯水(Water),又放了一杯酒(Wine),当我们想从map中取出酒的时候,却一不留神把水取了出来。 这段代码编译时是没有问题的,但运行的时候就会报ClassCastException
(水毕竟不是酒,不然岂不乱套了?):
- Exception in thread "main" java.lang.ClassCastException: com.coffe1891.java_demo.Coffe1891$Water cannot be cast to com.coffe1891.java_demo.Coffe1891$Wine
- at com.coffe1891.java_demo.Coffe1891.main(Coffe1891.java:12)
这个异常可能是程序员粗心造成的(实在太容易犯这个错了),但是影响是致命的(重则程序崩溃)。所以为了消灭这种异常,泛型出现了。当然,这只是重要原因之一,更多的还是在于让工程里的代码更灵活、可复用。
02.定义
所谓“泛型”,就是“宽泛的数据类型”。
泛型是这样一种特殊类型 ——它把类型明确的工作推迟到创建对象或调用方法的时候才去明确。说白了就是提供一种更高层次的OOP抽象/灵活性:
- 把类型当作参数一样传递;
- 这种类型参数只能用来表示引用类型,不能用来表示基本类型如 int、double、char 等。但是传递基本类型也不会报错,因为它们会自动装箱成对应的引用类型。
相关术语:
ArrayList<T>
中的T称为类型参数变量;ArrayList<Integer>
中的Integer称为实际类型参数;- 整个
ArrayList<T>
称为泛型类型; - 整个
ArrayList<Integer>
称为已参数化的类型(Parameterized Type)。
JAVA泛型设计原则:只要在编译时期没有出现警告,那么运行时期就不会出现ClassCastException
异常。
注:在Java中,经常用T、E、K、V等形式的参数来表示泛型参数。
T:代表一般的任何类。
E:代表 Element 的意思,或者 Exception 异常的意思。
K:代表 Key 的意思。
V:代表 Value 的意思,通常与 K 一起配合使用。
如果以上的“概念”不好理解,那我来说点通俗的。现在有一只玻璃杯,你可以让它盛一杯水,也可以盛一杯酒——泛型的概念就在于此:制造这只杯子的时候,没必要在说明书上定义死指明它只能盛水而不能盛酒。可以在说明书上指明它用来盛装液体,但最好也不要这样,弄不好用户想用它来盛几块冰糖呢 😁 。这么一说,你是不是感觉不那么抽象了?泛型其实就是在定义类、方法、接口的时候不局限地指定某一种特定类型参数,而让类、方法、接口的调用者来决定具体使用哪一种类型参数。就好比,玻璃杯的制造者说,我不知道使用者用这只玻璃杯来干嘛,所以我只负责造这么一只杯子;玻璃杯的使用者说,这就对了,我来决定这只玻璃杯是盛水还是酒,或者冰糖。
03.泛型类、泛型方法、泛型接口
1).泛型类
泛型类就是把泛型定义在类上,用户使用该类的时候,才把类型参数变量明确下来 。这样的话,用户明确了什么类型参数变量,该类就代表着什么类型。用户在使用的时候就不用担心转换异常ClassCastException
的问题了。
- /* JAVA */
-
- public class Coffe1891<T> {
- private T obj;
-
- public T getObj() {
- return obj;
- }
-
- public void setObj(T obj) {
- this.obj = obj;
- }
- }
用户最终想要使用哪种类型,就在创建的时候指定类型参数变量T,后续使用该类的时候,该类就会自动被转换成用户想要的类型了。
- /* JAVA */
- public static void main(String[] args) {
- //创建对象并指定元素类型
- Coffe1891<String> c = new Coffe1891<>();
-
- c.setObj(new String("coffe1891"));
- String s = c.getObj();
- System.out.println(s);//>> coffe1891
-
-
- //创建对象并指定元素类型
- Coffe1891<Integer> cc = new Coffe1891<>();
- /**
- * 如果在这个对象里传入的是String类型的,它在编译时期就通过不了.
- */
- cc.setObj(10);
- int i = cc.getObj();
- System.out.println(i);//>> 10
- }
2).泛型方法
某一个方法上需要使用泛型,外界仅仅是关心该方法,不关心类其他的属性。这样的话,我们在整个类上定义泛型,未免就有些大题小作了,这时候就需要用到泛型方法。
- //定义泛型方法..
- public <T> void show(T t) {
- System.out.println(t);
- }
用户传递进来的是什么类型参数变量,返回值就是什么类型了。
- public static void main(String[] args) {
- //创建对象
- ObjectTool tool = new ObjectTool();
-
- //调用方法,传入的参数是什么类型,返回值就是什么类型
- tool.show("coffe1891");
- tool.show(1891);
- }
3).泛型接口
在Java中也可以定义泛型接口,这里不再赘述,仅仅给出示例代码:
- //定义泛型接口
- interface Info<T> {
- public T getVar();
- }
-
- //实现接口
- class InfoImp<T> implements Info<T> {
- private T var;
-
- // 定义泛型构造方法
- public InfoImp(T var) {
- this.setVar(var);
- }
-
- public void setVar(T var) {
- this.var = var;
- }
-
- public T getVar() {
- return this.var;
- }
- }
04.通配符"?"
用一个问号?
代表匹配任意类型。
?和T的区别
?
和T
都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ?不行,比如如下这种 :
- /* JAVA */
-
- // 可以
- T t = operate();
-
- // 不可以
- ? car = operate();
T 是一个 确定的类型,通常用于泛型类和泛型方法的定义。
?是一个 不确定的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。
Class<T>
和 <Class<?>
又有什么区别呢?
最常见的是在反射场景下的使用,这里以用一段发射的代码来说明下。
- /* JAVA */
-
- // 通过反射的方式生成 multiLimit对象,这里比较明显的是,我们需要使用强制类型转换
- B b = (B)Class.forName("com.coffe1891.generic.B").newInstance();
对于上述代码,在运行期,如果反射的类型不是 B ,那么一定会报 java.lang.ClassCastException 错误。对于这种情况,则可以使用下面的代码来代替,使得在在编译期就能直接 检查到类型的问题:
- /* JAVA */
-
- public static <T> T createInstance(Class<T> clazz) throws InstantiationException, IllegalAccessException {
- return clazz.newInstance();
- }
-
- public static void main(String[] args) throws IllegalAccessException, InstantiationException {
- B b=createInstance(B.class);
- }
从上面的代码可以发现,Class<T>
在实例化的时候,T 要替换成具体类。
Class<?>
它是个通配泛型,? 可以代表任何类型,所以主要用于声明时的限制情况。比如,我们可以这样做声明:
- /* JAVA */
-
- class TEST{
- public Class<?> clazz;//可以
- public Class<T> clazzT;//报错,因为需要指定T
- }
如果想要上面的第5行不报错,就必须让当前的类也指定 T,可以这样写:
- /* JAVA */
-
- class TEST<T>{
- public Class<?> clazz;
- public Class<T> clazzT;
- }
05.泛型约束:PECS
PECS全称是 Producer Extends, Consumer Super 。
- /* JAVA */
-
- static class A extends B{} //子子类
- static class B extends C{} //子类
- static class C {} //父类
-
-
- static class Example<T>{}
-
- public static void main(String[] args) {
- {
- Example<? extends A> testAA = new Example<A>();
- Example<? extends A> testAB = new Example<B>();//报错
- Example<? extends A> testAC = new Example<C>();//报错
- Example<? extends B> testBA = new Example<A>();
- Example<? extends B> testBC = new Example<C>();//报错
- Example<? extends C> testCA = new Example<A>();
- Example<? extends C> testCB = new Example<B>();
- }
- {
- Example<? super A> testAA = new Example<A>();
- Example<? super A> testAB = new Example<B>();
- Example<? super A> testAC = new Example<C>();
- Example<? super B> testBA = new Example<A>();//报错
- Example<? super B> testBC = new Example<C>();
- Example<? super C> testCA = new Example<A>();//报错
- Example<? super C> testCB = new Example<B>();//报错
- }
-
- }
上面这段代码演示了PECS的基本特性。使用? extends T
之后,类型参数变量都被限定为T的子类(包含T)了;而使用? super T
的话,类型参数变量都会被限定为T的父类(含T),所以才有上面代码的表现。
Producer意指生产者,生产者用extends
关键字来限制类型参数变量的范围,这样类型参数变量的上限是指定的,也即约束是严格的;Consumer意指消费者,消费者用super
关键字来指定类型参数变量的下限,这样类型参数变量的上限是约束宽松的。
为什么这么设计?这是因为我们生产软件的时候,相对更容易知道软件的生产者具体是谁;但是不太容易明确到底有哪些具体的消费者使用这个软件。因此在Sun设计JAVA的时候采用了这种朴素的思想,那就是针对生产者Producer具体化(约束严格),针对消费者Consumer抽象化(约束宽松)。而生产者只产出不进,消费者只进不产出,所以为方便读者记忆理解,我总结为 “ 宽进严出 ” 。
熟悉OOP之后我们又都知道:JAVA是单继承,所有继承的类构成一棵树。子类可以强制转换为父类,父类强制转换为子类(向下转型)会有风险。具体的可以抽象化,抽象的不一定能具体化,这就是自然规律的映射。
- List<B> aList = new ArrayList<>();
- aList.add(new B());
-
- //这种向下转型(强制转换),编译时不报错,运行时报ClassCastException的错
- A a = (A)aList .get(0);
如上面代码所示,在JAVA中进行类型转换,向下转型存在着风险,而且编译期间不容易发现,只有在运行期间才会抛出异常,十分隐蔽,所以要尽量避免使用向下转型。接着往下看代码:
- /* JAVA */
-
- List<? extends B> extendsList = new ArrayList<>();
-
- extendsList.add(new A());//报错
- extendsList.add(new B());//报错
- extendsList.add(new C());//报错
-
- B b = extendsList.get(0);//可以
看来作为Producer的extendsList,我们不能往里插入对象,但是可以取出里面的项(也即只出不进) 。为什么呢?如果上面第5行可以通过编译,则extendsList应该是具体的类型List<A>
,那么后面第6行就无法通过了,因为B和A不是同一个类型,所以前后矛盾,那么第5行一开始就不能通过编译;至于第7行,C是B的父类,前面提到了“父类转换为子类有风险”,也即可能报ClassCastException错误,而泛型的目的就是要在编译阶段消灭ClassCastException,所以编译器也必须、不能让编译通过了。
其实,从另外一个角度而言,extendsList里放的类型是? extends B
,也即可能是B,也可能是B的子类A,但是具体是哪个类,并不清楚,所以如果往里添加任何B和A的实例,则会发生强制转换,促使extendsList就变成了具体的List或List,而这不是我们想要的。综上,编译器一定会禁止插入,对任何插入必须要报错 😎 。
至于第9行,从extendsList里面取出来的项,一定是B或者B的子类的实例,子类强制转换为父类是可行的,所以第9行可以安全地被通过编译。
- /* JAVA */
-
- List<? super B > superList = new ArrayList<>();
-
- superList .add(new A());//可以
- superList .add(new B());//可以
- superList .add(new C());//报错
-
- B b = superList.get(0);//报错
06.TypeScript的泛型
1).与JAVA的语法差别
TypeScript的泛型类、泛型方法、泛型接口语法上与JAVA有明显的区别:
- //泛型方法,在TypeScript里也可叫泛型函数
- function genericMethod<T>(arg: T): T {
- return arg;
- }
-
- //泛型类
- class GenericClass<T> {
- property: T;
- //add右侧第一个冒号右边部分,称为【泛型函数类型】。【函数类型】为TypeScript的基础概念,
- //读者可自行查阅文档。
- add: (x: T, y: T) => T;
- //下面这样写与上一行也是等效的
- //add: { (x: T, y: T): T };
- }
-
- //泛型接口
- interface GenericInterface<T> {
- (arg: T): T;//这里是【泛型函数类型】
- }
2).类型推导
TypeScript编译器会根据传入的参数自动地帮助我们确定T的类型:
- //明确指定类型T
- let a= genericMethod<string>("coffe1891"); // a的类型是 'string'
-
- //不指定类型T,由编译器推导类型
- let b= genericMethod("Bob Ma"); // b的类型是 'string'
类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面第2行那样明确的传入T的类型,在一些复杂的情况下是必须这么做的。
3).泛型约束
- function genericMethod<T>(arg: T): T {
- console.log(arg.length);//报错:类型“T”上不存在属性“length”
- return arg;
- }
为了解决这个问题,我们可以使用接口,在接口里约定必须具备length
属性:
- interface MyInterface{
- length:number;
- }
-
- function genericMethod<T extends MyInterface{>(arg: T): T {
- console.log(arg.length);
- return arg;
- }
现在这个泛型函数的参数arg
必须包含属性length
,因此这个函数它不再适用于任意类型,正所谓有得必有失。
4).约束嵌套
约束嵌套意思就是说泛型extends了另外一个泛型、或者类。这个和JAVA里的PECS有点区别,笔者发现TypeScript还没有引入JAVA的PECS那种深层次的约束能力,参见TypeScript官方Github页的issues#4704。
在TypeScript里,我们可以让一个类型参数被另外一个类型参数约束。比如,我要从一个Map对象里面获取某一个Key的值,并且要确保这个Key一定在Map里,在使用泛型之前我们可能会通过if来判断:
- function method(arg:Map,key:string){
- //todo
- }
使用泛型约束嵌套之后,在代码运行前编译器就能帮我们执行好这个约束,十分方便:
//todo
5).用TypeScript+Vue实现泛型组件
什么是泛型组件(Generic Components)?当组件支持传递泛型这种参数的时候,这个组件也具备了泛型的特征,而成为了泛型组件。
前端常用的框架React和Vue都有组件的概念,本篇以Vue为例,React+Typescript实现泛型组件可以看这篇网文。
//todo
免责声明
本站所有资源出自互联网收集整理,本站不参与制作,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站发布资源来源于互联网,可能存在水印或者引流等信息,请用户自行鉴别,做一个有主见和判断力的用户。
本站资源仅供研究、学习交流之用,若使用商业用途,请购买正版授权,否则产生的一切后果将由下载用户自行承担。