Skip to content

Latest commit

 

History

History
296 lines (204 loc) · 11.4 KB

6.2.4.md

File metadata and controls

296 lines (204 loc) · 11.4 KB

陆.2.4 彻底搞懂泛型

因为JAVA的OOP特征实现得实在太经典了,以至于后来很多OOP语言都会借鉴它,所以我会先借用JAVA语言来阐释泛型这个概念,本文最后再用TypeScript来讲前端领域的泛型。

陆.2.4.1 泛型出现的目的

//todo

陆.2.4.2 定义

所谓“泛型”,就是“宽泛的数据类型”。

泛型是这样一种特殊类型——它把类型明确的工作推迟到创建对象或调用方法的时候才去明确。说白了就是提供一种更高层次的OOP抽象/灵活性**:**

  • 把类型当作是参数一样传递;
  • 类型参数只能用来表示引用类型,不能用来表示基本类型,如 int、double、char 等。但是传递基本类型不会报错,因为它们会自动装箱成对应的包装类。

相关术语:

  • ArrayList<T>中的T称为类型参数变量;
  • ArrayList<Integer>中的Integer称为实际类型参数;
  • 整个ArrayList<T>称为泛型类型;
  • 整个ArrayList<Integer>称为已参数化的类型(Parameterized Type)。

**JAVA泛型设计原则:**只要在编译时期没有出现警告,那么运行时期就不会出现ClassCastException异常。

陆.2.4.3 泛型类、泛型方法、泛型接口

1.泛型类

泛型类就是把泛型定义在类上,用户使用该类的时候,才把类型参数变量明确下来。这样的话,用户明确了什么类型参数变量,该类就代表着什么类型。用户在使用的时候就不用担心转换异常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
}

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;
    }
}

陆.2.4.4 通配符"?"

用一个问号?代表匹配任意类型。

?和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;
}

陆.2.4.5 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<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,这个泛型你其实无法使用它,对吧!所以,对于把类型参数变量写成这样形态的泛型,无法读取,只能对这个泛型做插入操作,也即只进不出。

陆.2.4.6 TypeScript的泛型

//todo

参考文献

{% hint style="info" %} What is PECS (Producer Extends Consumer Super)? {% endhint %}