软件构造学习(四)协变、类型擦除

本篇文章主要介绍Liskov替换原则,协变已经与泛型中的类型擦除的相关知识。

一、Liskov替换原则

  Liskov替换原则,以下简称Lsp,是一组用来创建层次结构的指导原则。当满足LSP时,可以用子类替换父类而不用担心对期望的行为产生影响。LSP的规则内容如下:

  • 前置条件不能强化
  • 后置条件不能弱化
  • 不变量要保持(不变量是指一个对象生命周期中永远保持为真的谓词)
  • 子类型的方法参数是可逆变的
  • 子类型的方法的返回值是可协变的
  • 异常的类型是可协变的(也就是子类型的异常不变或者更具体)

二、协变与逆变

  1.逆变

   逆变是指从 子类型到父类型越来越具体,参数类型不变或者越来越抽象。例如下面的例子:

Class T{
    void c(String s){…}
}
Class S extends T{
    @Override
    void c(Object s){…}
}

   但是目前Java并不支持逆变,当遇到这种情况时,会当作overload来对待。

  2.协变

  (1)协变定义

  指从父类型到子类型越来越具体,返回值的类型和异常的类型不变或者变得更加具体。比如说下面这个例子:

Class T{
    Object a(){……}
}
Class S extends T{
    @Override
    String a(){……}
}

   S是T的子类型,S返回的参数是String类型,是T返回的参数类型Object的子类型,也就是在方法重写时,子类返回类型是父类返回类型的子类型,更加具体。

   再看看下面的代码:

Class T{
    void b() throws Throwable{…}
}
Class S extends T{
    @Override
    void b() throws IOException{…}、
}
Class U extends S{
    @Override
    void b(){…}
}

   从T到S再到U,子类抛出的异常更加具体,也就是子类型中重写的方法不能抛出额外的异常。

   (2)数组协变

  数组是支持协变的,一个声明为T[]的数组实际包含的类型可能是T或者T的子类,也就是类          型取决于存储的元素的类型。示例如下:

Number[] numbers = new Number[2];
number[0] = new Integer(10);
number[1] = new Double(3.14);

  但是,这样可能会造成 声明对象类型和实际指向的类型不一致。比如下面这个例子 :

1 Interger[] myInts = {1,2,3,4};
2 Number[] myNumber = myInts;
3 myNumber[0] = 3.14;

  这个例子在运行到第三行时就会出现运行错误,这是因为Java直到数组被实例化成为Interger类型的数组,只是通过一个Number[]引用进行访问,所以当其被赋值成一个浮点类型时,当然会报错。

三、泛型中的LSP

  1.类型擦除

  Java在编译之后的文件不会包含泛型中的类型信息。也就是说使用泛型时加上的类型参数,会被编译器在编译时去掉,这种规则就成为类型擦除。比如List<Object>和List<String>在参数类型没有限定情况下,在编译后都会被视为一个List,也就代表下面的代码运行后控制台输出结果会是True

1 List<String> stringList = new ArrayList<String>();
2 List<Integer> integerList = new ArrayList<Integer>();
3 System.out.println(stringList.getClass() == integerList.getClass());

  类型擦除也可以限定类型参数,当没有参数类型限定时,会被全部替换成object。下面是一个泛型在编译时和运行时的差别,也就是类型擦除前后:

   如果参数类型有限制时,那么泛型擦除后参数类型被替换为限定的类型,这样就可以在类型内部执行与类型有关的方法。

  • 类型通配符上限:extends 例List <T ecxtends Number>代表T应该是Number的子类
  • 类型通配符下限:super 例List<? super Number>代表只能接受Number及其父类型

  2.泛型不支持协变

  泛型不支持协变,最主要的原因就在于类型擦除,List<String>和List<Object>之间没有任何父子关系,所以对于下列代码,在编译时就会报错:

1 List<Integer> myInts = new ArrayList<Integer>();
2 myInts.add(1);
3 myInts.add(2);
4 List<Number> myNums = myInts; //compiler error
5 myNums.add(3.14);

  与上面数组因支持协变,而运行时报错的方式相比,更早地报错,使其更加安全严谨。

posted @ 2022-06-04 22:11  叶绿体基质  阅读(118)  评论(1)    收藏  举报