Java与Kt中的泛型

2022-05-11/2022-05-11

深扒Java与Kotlin中的泛型

为什么要有泛型?

1.泛型可以帮助我们在编译时检查类型安全

2.泛型可以帮助我们强制类型转换

3.泛型可以增加代码复用性

4.更加语义化,⽐如我们声明⼀个List,便可以知道⾥⾯存储的是String对象,⽽不是其他对象;

泛型类型的创建

泛型类的创建

public class Wrapper<T> {
 T instance;
 public T get() {
 return instance;
 }
 public void set(T newInstance) {
 instance = newInstance;
 }
}

泛型接⼝的创建

public interface Shop<T> {

 T buy();

 float refund(T item);

}

泛型方法的创建

<T> T fun(T parm){
    return T;
}

类型上界

Java使⽤extends关键字,⽽Kotlin使⽤“ :”,这种类型的泛型约束,我们称之为上界约束。

使用类型上界可以让泛型的实例化类型受到约束

例如

class FruitPlate<T: Fruit>(val t : T)

就只能使用Fruit的子类型去定义这个泛型的类型

可以通过where关键字对泛型参数类型添加多个约束条件

fun cut(t: T) where T: Fruit, T: Ground

⽐如这个例⼦中要求被切的东西是⼀种⽔果,⽽且必须是长在地上的⽔果。

泛型本质

在JDK5之前是没有泛型的,为了向下兼容,Java实现的泛型实际上数通过类型擦除来实现的,Java的泛型实际上是“伪泛型”,什么是类型擦除?

比如下面这段代码

public class Plate<T> {
    private T fruit;

    public void setFruit(T fruit) {
        this.fruit = fruit;
    }

    public T getFruit() {
        return fruit;
    }
}

将它编译成字节码文件后是这样的

// class version 51.0 (51)
// access flags 0x21
// signature <T:Ljava/lang/Object;>Ljava/lang/Object;
// declaration: Plate<T>
public class Plate {

  // compiled from: Plate.java

  // access flags 0x2
  // signature TT;
  // declaration: fruit extends T
  private Ljava/lang/Object; fruit

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 1 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LPlate; L0 L1 0
    // signature LPlate<TT;>;
    // declaration: this extends Plate<T>
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  // signature (TT;)V
  // declaration: void setFruit(T)
  public setFruit(Ljava/lang/Object;)V
   L0
    LINENUMBER 5 L0
    ALOAD 0
    ALOAD 1
    PUTFIELD Plate.fruit : Ljava/lang/Object;
   L1
    LINENUMBER 6 L1
    RETURN
   L2
    LOCALVARIABLE this LPlate; L0 L2 0
    // signature LPlate<TT;>;
    // declaration: this extends Plate<T>
    LOCALVARIABLE fruit Ljava/lang/Object; L0 L2 1
    // signature TT;
    // declaration: fruit extends T
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1
  // signature ()TT;
  // declaration: T getFruit()
  public getFruit()Ljava/lang/Object;
   L0
    LINENUMBER 9 L0
    ALOAD 0
    GETFIELD Plate.fruit : Ljava/lang/Object;
    ARETURN
   L1
    LOCALVARIABLE this LPlate; L0 L1 0
    // signature LPlate<TT;>;
    // declaration: this extends Plate<T>
    MAXSTACK = 1
    MAXLOCALS = 1
}

可以看到所有的泛型类型在字节码文件中实际上是Object类型,也就是说JVM实际上并不认识泛型类型

类型擦除说的是泛型类型在编译时会被擦除为上界类型,如果没有特别指定上界类型则擦除为Object类型

public class Plate<T extends Fruit> {
    private T fruit;

    public void setFruit(T fruit) {
        this.fruit = fruit;
    }

    public T getFruit() {
        return fruit;
    }
}

对应的字节码:

// class version 51.0 (51)
// access flags 0x21
// signature <T:LFruit;>Ljava/lang/Object;
// declaration: Plate<T extends Fruit>
public class Plate {

  // compiled from: Plate.java

  // access flags 0x2
  // signature TT;
  // declaration: fruit extends T
  private LFruit; fruit

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 1 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LPlate; L0 L1 0
    // signature LPlate<TT;>;
    // declaration: this extends Plate<T>
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  // signature (TT;)V
  // declaration: void setFruit(T)
  public setFruit(LFruit;)V
   L0
    LINENUMBER 5 L0
    ALOAD 0
    ALOAD 1
    PUTFIELD Plate.fruit : LFruit;
   L1
    LINENUMBER 6 L1
    RETURN
   L2
    LOCALVARIABLE this LPlate; L0 L2 0
    // signature LPlate<TT;>;
    // declaration: this extends Plate<T>
    LOCALVARIABLE fruit LFruit; L0 L2 1
    // signature TT;
    // declaration: fruit extends T
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1
  // signature ()TT;
  // declaration: T getFruit()
  public getFruit()LFruit;
   L0
    LINENUMBER 9 L0
    ALOAD 0
    GETFIELD Plate.fruit : LFruit;
    ARETURN
   L1
    LOCALVARIABLE this LPlate; L0 L1 0
    // signature LPlate<TT;>;
    // declaration: this extends Plate<T>
    MAXSTACK = 1
    MAXLOCALS = 1
}

因为Java使用类型擦除来实现伪泛型,导致了一些问题

①桥方法

public class Plate<T> {
    public T fruit;

    public void setFruit(T fruit) {
        this.fruit = fruit;
    }

    public T getFruit() {
        return fruit;
    }
}

public class GoldPlate<T extends Fruit> extends Plate<T>{

    @Override
    public void setFruit(T fruit) {
        super.setFruit(fruit);
    }
}

分析GoldPlate的字节码发现编译时为我们自动生成了一个桥方法

// class version 51.0 (51)
// access flags 0x21
// signature <T:LFruit;>LPlate<TT;>;
// declaration: GoldPlate<T extends Fruit> extends Plate<T>
public class GoldPlate extends Plate {

  // compiled from: GoldPlate.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 1 L0
    ALOAD 0
    INVOKESPECIAL Plate.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LGoldPlate; L0 L1 0
    // signature LGoldPlate<TT;>;
    // declaration: this extends GoldPlate<T>
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  // signature (TT;)V
  // declaration: void setFruit(T)
  public setFruit(LFruit;)V
   L0
    LINENUMBER 5 L0
    ALOAD 0
    ALOAD 1
    INVOKESPECIAL Plate.setFruit (Ljava/lang/Object;)V
   L1
    LINENUMBER 6 L1
    RETURN
   L2
    LOCALVARIABLE this LGoldPlate; L0 L2 0
    // signature LGoldPlate<TT;>;
    // declaration: this extends GoldPlate<T>
    LOCALVARIABLE fruit LFruit; L0 L2 1
    // signature TT;
    // declaration: fruit extends T
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1041
  public synthetic bridge setFruit(Ljava/lang/Object;)V
   L0
    LINENUMBER 1 L0
    ALOAD 0
    ALOAD 1
    CHECKCAST Fruit
    INVOKEVIRTUAL GoldPlate.setFruit (LFruit;)V
    RETURN
   L1
    LOCALVARIABLE this LGoldPlate; L0 L1 0
    // signature LGoldPlate<TT;>;
    // declaration: this extends GoldPlate<T>
    MAXSTACK = 2
    MAXLOCALS = 2
}

原因是Fruit类类型擦除后泛型类型变为Object类型,而GoldPlate类类型擦除后泛型类型擦除为Fruit类型,而GoldPlate类型是继承自Plate类型的并且重写了setFruit方法,那么在字节码文件中GoldPlate的setFruit方法的类型实际上是Fruit而不是Object,不符合继承的特性,因此需要生成一个setFruit的重载方法并且参数类型为Object,然后在这个方法对Object强制转型为Fruit后再调用我们的setFruit方法

②泛型类型变量不能使用基本数据类型

比如没有ArrayList,只有ArrayList.当类型擦除后,ArrayList的原始类中的类型变量(T)替换成Object,但Object类型不能存放int值

③不能使用instanceof 运算符

因为擦除后,ArrayList只剩下原始类型,泛型信息String不存在了,所有没法使用instanceof

④没法创建泛型实例

因为类型不确定

image-20211023095739060

⑤没有泛型数组

因为数组是协变的,即Apple[]类型的值可以赋值给Fruit[]类型的值,擦除后就没法满足数组协变的原则

image-20211023095824567

kt中数组是支持泛型的,所以kt中的数组也不是协变的

泛型通配符

由于类型擦除,不能将 List 类型的变量赋值给 List 的变量,但可以使用泛型通配符来解决这个问题

比如这个方法就可以接收泛型类型为Plate的子类的,但是这样做会导致 plate 只能取数据不能拿数据

static int getPlateFruitPrice(Plate<? extends Fruit> plate) {
        return plate.getFruit().getPrice();
    }

这样做就让泛型是协变的了

也就是说如果在定义的泛型类和泛型⽅法的泛型参数前⾯加上out关键词,说明这个泛型类及泛型⽅法是协变,简单来说类型A是类型B的⼦类型,那么Generic也是Generic的⼦类型

此时这样写编译器会报错

static int getPlateFruitPrice(Plate<? extends Fruit> plate) {
        plate.setFruit(new Apple(12));
        return plate.getFruit().getPrice();
    }

super同理

而逆变则是指类型A是类型B的⼦类型,那么Generic是Generic的⼦类型,比如 Double是 Number 的子 类 型,反 过 来 Generic却是Generic的父类型

协变与逆变的应用

假设现在有个场景,需要将数据从⼀个Double数组拷贝到另⼀个Double数组,我们该怎么实现呢?

学过泛型的我们可能会这样写

image-20220511222221006

那么这种⽅式有没有什么局限呢?我们发现,使⽤copy⽅法必须是同⼀种 类型,那么假如我们想把Array拷贝到Array中将不允许,这时候我们就可以利⽤上⾯所说的泛型变形了

image-20220511222401949

image-20220511222415841

泛型与反射

泛型并没有被完全擦除,在类的常量池中还保存了泛型信息

可以通过反射拿到这些信息

public class main {
    Plate<Apple> plate = new Plate<>();
    public static void main(String[] args) throws Exception {
        Field f = main.class.getDeclaredField("plate");
        System.out.println(f.getGenericType()); //打印Plate<Apple>
        ParameterizedType pType = (ParameterizedType) f.getGenericType();
        System.out.println(pType.getActualTypeArguments()[0]);  //打印class Apple
    }

还可以通过反射突破通配符PECS的限制

public class main {

    public static void main(String[] args) throws Exception {
        Plate<Apple> plate = new Plate<>();
        plate.setFruit(new Apple(666));
        System.out.println(getPlateFruitPrice(plate));

    }

    static int getPlateFruitPrice(Plate<? extends Fruit> plate) throws Exception {
        Method method = plate.getClass().getMethod("setFruit", Object.class);
        method.invoke(plate,new Cherry(66));
        return plate.getFruit().getPrice();
    }

}

上面代码输入为66

怎么获取泛型的类型

通常情况下使⽤泛型我们并不在意它的类型是否是类型擦除,但是在有些场景,我们需要知道运⾏时泛型参数的类型,⽐如序列 化/反序列化的时候

可以通过以下方式获取泛型的类型

1.利用匿名内部类

image-20220511221238801

2.使用内联函数获取泛型

Kotlin中的内联函数在编译的时候编译器便会将相应函数的字节码插⼊调⽤的地⽅,也就是说,参数类型也会被插⼊字节码中,我们就可以获取参数的类型了。

image-20220511221449245

Kotlin中的泛型

Kotlin中的泛型和java中的很像

Java 的 <? extends> 在 Kotlin ⾥写作 ;Java 的 <? super> 在Kotlin ⾥写作

也很形象,out表明这个泛型类只能出数据而不能入数据,而in则表明这个泛型类只能入数据不能出数据

Kotlin 的 * 号相当于 Java 的 ? 号


标题:Java与Kt中的泛型
作者:OkAndGreat
地址:http://zhongtai521.wang/articles/2021/08/10/1652279120268.html

评论
发表评论
       
       
取消