因为JAVA的OOP特征实现得实在太经典了,以至于后来很多OOP语言都会借鉴它,所以我会先借用JAVA语言来阐释泛型这个概念,本文最后再用TypeScript来讲前端领域的泛型。
//todo
所谓“泛型”,就是“宽泛的数据类型”。
泛型是这样一种特殊类型——它把类型明确的工作推迟到创建对象或调用方法的时候才去明确。说白了就是提供一种更高层次的OOP抽象/灵活性**:**
- 把类型当作是参数一样传递;
- 类型参数只能用来表示引用类型,不能用来表示基本类型,如 int、double、char 等。但是传递基本类型不会报错,因为它们会自动装箱成对应的包装类。
相关术语:
ArrayList<T>
中的T称为类型参数变量;ArrayList<Integer>
中的Integer称为实际类型参数;- 整个
ArrayList<T>
称为泛型类型; - 整个
ArrayList<Integer>
称为已参数化的类型(Parameterized Type)。
**JAVA泛型设计原则:**只要在编译时期没有出现警告,那么运行时期就不会出现ClassCastException
异常。
泛型类就是把泛型定义在类上,用户使用该类的时候,才把类型参数变量明确下来。这样的话,用户明确了什么类型参数变量,该类就代表着什么类型。用户在使用的时候就不用担心转换异常ClassCastException
的问题了。
/* JAVA */
public class ObjectTool<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) {
//创建对象并指定元素类型
ObjectTool<String> tool = new ObjectTool<>();
tool.setObj(new String("coffe1891"));
String s = tool.getObj();
System.out.println(s);//>> coffe1891
//创建对象并指定元素类型
ObjectTool<Integer> objectTool = new ObjectTool<>();
/**
* 如果在这个对象里传入的是String类型的,它在编译时期就通过不了.
*/
objectTool.setObj(10);
int i = objectTool.getObj();
System.out.println(i);//>> 10
}
**某一个方法上需要使用泛型,外界仅仅是关心该方法,不关心类其他的属性。**这样的话,我们在整个类上定义泛型,未免就有些大题小作了,这时候就需要用到泛型方法。
//定义泛型方法..
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);
}
在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;
}
}
用一个问号?
代表匹配任意类型。
?
和T
都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ?不行,比如如下这种 :
/* JAVA */
// 可以
T t = operate();
// 不可以
? car = operate();
T 是一个 确定的类型,通常用于泛型类和泛型方法的定义。
?是一个 不确定的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。
最常见的是在反射场景下的使用,这里以用一段发射的代码来说明下。
/* 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;
}
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<A>或List<B>,而这不是我们想要的。综上,编译器一定会禁止插入,对任何插入必须要报错 😎 。
至于第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);//报错
可以发现,作为Consumer的superList,我们不能取出里面的任何项,但是可以往里面插入对象(也即只进不出)。这又是为什么呢?superList里放的类型是? super B
,也即可能是B,也可能是B的父类C。因为前面说到了“子类可以强制转换为父类”,所以第5、6行放一个子类进去,子类可以强制转换为父类,因此编译通过没毛病。第7行还是老毛病,superList里面的项可能为B类型,而C是B的父类,前面说到了“父类转换为子类有风险”,也即可能报ClassCastException错误,而泛型的目的就是要在编译阶段消灭ClassCastException,所以编译器也必须不能让编译通过了。
至于第9行为什么也报错?由于只规定了superList里的项必须是B或者B的超类,导致superList里的项没有明确统一的“根”(除了Object这个必然的根),所以除了把具体的项强制转成Object,这个泛型你其实无法使用它,对吧!所以,对于把类型参数变量写成这样形态的泛型,无法读取,只能对这个泛型做插入操作,也即只进不出。
//todo
{% hint style="info" %} What is PECS (Producer Extends Consumer Super)? {% endhint %}