重学泛型

2020-12-28  本文已影响0人  CallMeMrZ

什么是泛型?

什么是参数化类型?

把类型当参数一样传递

数据类型只能是引用类型(泛型的副作用)

举个例子:

为什么使用泛型,使用泛型的好处?

  1. 代码更健壮(只要编译期没有警告,那么运行期就不会出现ClassCastException)
//不使用泛型,运行期报错
List list = new ArrayList(); 
list.add("hello");
Integer s = (String) list.get(0);// Causes a ClassCastException to be thrown.
//使用泛型,编译期就检查
List<String> list = new ArrayList(); 
list.add("hello");
Integer s = (String) list.get(0);//编译器就会不通过
  1. 代码更简洁,不用强转
//不使用泛型,需要强转
List list = new ArrayList(); 
list.add("hello");
String s = (String) list.get(0);
//使用泛型,不需要强转
List<String> list = new ArrayList<String>();
list.add("hello"); 
String s = list.get(0); // no cast
  1. 代码更灵活,复用
 // java.util.List中的排序方法sort,只要实现了Comparator接口的都可以使用这个方法
 default void sort(@Nullable Comparator<? super E> c) {
        throw new RuntimeException("Stub!");
    }

Java是如何处理泛型的

  1. 通过运行时获取的类信息是完全一样的。泛型类型被擦除了,擦除后只剩下原始类型,如下面所示的只剩下ArrayList类型。
 ArrayList<String> strings = new ArrayList<>();
 ArrayList<Integer> integers = new ArrayList<>();
 System.out.println(strings.getClass() == integers.getClass());
 //result true

泛型擦除

用一个简单的例子看一下Java是怎么处理泛型的

  1. 定义一个泛型接口
public interface Box<T> {
    void set(T t);
    T get();
}
  1. 利用javac命令获取字节码文件
public interface Box<T> {
    void set(T var1);

    T get();
}
  1. 利用javap -c命令查看生成的字节码,我们的T变成了Object类型。
public abstract interface test3/Box {
  public abstract set(Ljava/lang/Object;)V
  public abstract get()Ljava/lang/Object;
}

  1. 我们定义一个类去实现这个接口
public class ConditionalBox<T> implements Box<T> {

    private List<T> items = new ArrayList<T>(10);

    public ConditionalBox() {
    }

    @Override
    public void set(T t) {
        items.add(t);
    }

    @Override
    public T get() {
        int index = items.size() - 1;
        if (index >= 0) {
            return items.get(index);
        } else {
            return null;
        }
    }
}

5.用javap -c命令查看生成的字节码。可以看到我们的set和get还是构造方法,T变成了Object类型。

public class test3/ConditionalBox implements test3/Box {

  // compiled from: ConditionalBox.java

  // access flags 0x2
  // signature Ljava/util/List<TT;>;
  // declaration: items extends java.util.List<T>
  private Ljava/util/List; items

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 10 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 8 L1
    ALOAD 0
    NEW java/util/ArrayList
    DUP
    BIPUSH 10
    INVOKESPECIAL java/util/ArrayList.<init> (I)V
    PUTFIELD test3/ConditionalBox.items : Ljava/util/List;
   L2
    LINENUMBER 11 L2
    RETURN
   L3
    LOCALVARIABLE this Ltest3/ConditionalBox; L0 L3 0
    // signature Ltest3/ConditionalBox<TT;>;
    // declaration: this extends test3.ConditionalBox<T>
    MAXSTACK = 4
    MAXLOCALS = 1

  // access flags 0x1
  // signature (TT;)V
  // declaration: void set(T)
  public set(Ljava/lang/Object;)V
   L0
    LINENUMBER 15 L0
    ALOAD 0
    GETFIELD test3/ConditionalBox.items : Ljava/util/List;
    ALOAD 1
    INVOKEINTERFACE java/util/List.add (Ljava/lang/Object;)Z (itf)
    POP
   L1
    LINENUMBER 16 L1
    RETURN
   L2
    LOCALVARIABLE this Ltest3/ConditionalBox; L0 L2 0
    // signature Ltest3/ConditionalBox<TT;>;
    // declaration: this extends test3.ConditionalBox<T>
    LOCALVARIABLE t Ljava/lang/Object; L0 L2 1
    // signature TT;
    // declaration: t extends T
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1
  // signature ()TT;
  // declaration: T get()
  public get()Ljava/lang/Object;
   L0
    LINENUMBER 20 L0
    ALOAD 0
    GETFIELD test3/ConditionalBox.items : Ljava/util/List;
    INVOKEINTERFACE java/util/List.size ()I (itf)
    ICONST_1
    ISUB
    ISTORE 1
   L1
    LINENUMBER 21 L1
    ILOAD 1
    IFLT L2
   L3
    LINENUMBER 22 L3
    ALOAD 0
    GETFIELD test3/ConditionalBox.items : Ljava/util/List;
    ILOAD 1
    INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object; (itf)
    ARETURN
   L2
    LINENUMBER 24 L2
   FRAME APPEND [I]
    ACONST_NULL
    ARETURN
   L4
    LOCALVARIABLE this Ltest3/ConditionalBox; L0 L4 0
    // signature Ltest3/ConditionalBox<TT;>;
    // declaration: this extends test3.ConditionalBox<T>
    LOCALVARIABLE index I L1 L4 1
    MAXSTACK = 2
    MAXLOCALS = 2
}
  1. IntelligentBox<T extends Comparable<T>>实现Box接口。代码如下
public class IntelligentBox<T extends Comparable<T>> implements Box<T> {

    private List<T> items = new ArrayList<T>(10);

    @Override
    public void set(T t) {
        items.add(t);
        Collections.sort(items);
    }

    @Override
    public T get() {
        int index = items.size() - 1;
        if (index >= 0) {
            return items.get(index);
        } else {
            return null;
        }
    }
}

  1. 用javap -c命令查看IntelligentBox生成的字节码
public class test3/IntelligentBox implements test3/Box {

  // compiled from: IntelligentBox.java

  // access flags 0x2
  // signature Ljava/util/List<TT;>;
  // declaration: items extends java.util.List<T>
  private Ljava/util/List; items

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 9 L1
    ALOAD 0
    NEW java/util/ArrayList
    DUP
    BIPUSH 10
    INVOKESPECIAL java/util/ArrayList.<init> (I)V
    PUTFIELD test3/IntelligentBox.items : Ljava/util/List;
    RETURN
   L2
    LOCALVARIABLE this Ltest3/IntelligentBox; L0 L2 0
    // signature Ltest3/IntelligentBox<TT;>;
    // declaration: this extends test3.IntelligentBox<T>
    MAXSTACK = 4
    MAXLOCALS = 1

  // access flags 0x1
  // signature (TT;)V
  // declaration: void set(T)
  public set(Ljava/lang/Comparable;)V
   L0
    LINENUMBER 13 L0
    ALOAD 0
    GETFIELD test3/IntelligentBox.items : Ljava/util/List;
    ALOAD 1
    INVOKEINTERFACE java/util/List.add (Ljava/lang/Object;)Z (itf)
    POP
   L1
    LINENUMBER 14 L1
    ALOAD 0
    GETFIELD test3/IntelligentBox.items : Ljava/util/List;
    INVOKESTATIC java/util/Collections.sort (Ljava/util/List;)V
   L2
    LINENUMBER 15 L2
    RETURN
   L3
    LOCALVARIABLE this Ltest3/IntelligentBox; L0 L3 0
    // signature Ltest3/IntelligentBox<TT;>;
    // declaration: this extends test3.IntelligentBox<T>
    LOCALVARIABLE t Ljava/lang/Comparable; L0 L3 1
    // signature TT;
    // declaration: t extends T
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1
  // signature ()TT;
  // declaration: T get()
  public get()Ljava/lang/Comparable;
   L0
    LINENUMBER 19 L0
    ALOAD 0
    GETFIELD test3/IntelligentBox.items : Ljava/util/List;
    INVOKEINTERFACE java/util/List.size ()I (itf)
    ICONST_1
    ISUB
    ISTORE 1
   L1
    LINENUMBER 20 L1
    ILOAD 1
    IFLT L2
   L3
    LINENUMBER 21 L3
    ALOAD 0
    GETFIELD test3/IntelligentBox.items : Ljava/util/List;
    ILOAD 1
    INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object; (itf)
    CHECKCAST java/lang/Comparable
    ARETURN
   L2
    LINENUMBER 23 L2
   FRAME APPEND [I]
    ACONST_NULL
    ARETURN
   L4
    LOCALVARIABLE this Ltest3/IntelligentBox; L0 L4 0
    // signature Ltest3/IntelligentBox<TT;>;
    // declaration: this extends test3.IntelligentBox<T>
    LOCALVARIABLE index I L1 L4 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1041
  public synthetic bridge get()Ljava/lang/Object;
   L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKEVIRTUAL test3/IntelligentBox.get ()Ljava/lang/Comparable;
    ARETURN
   L1
    LOCALVARIABLE this Ltest3/IntelligentBox; L0 L1 0
    // signature Ltest3/IntelligentBox<TT;>;
    // declaration: this extends test3.IntelligentBox<T>
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1041
  public synthetic bridge set(Ljava/lang/Object;)V
   L0
    LINENUMBER 7 L0
    ALOAD 0
    ALOAD 1
    CHECKCAST java/lang/Comparable
    INVOKEVIRTUAL test3/IntelligentBox.set (Ljava/lang/Comparable;)V
    RETURN
   L1
    LOCALVARIABLE this Ltest3/IntelligentBox; L0 L1 0
    // signature Ltest3/IntelligentBox<TT;>;
    // declaration: this extends test3.IntelligentBox<T>
    MAXSTACK = 2
    MAXLOCALS = 2
}

 INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object; (itf)
CHECKCAST java/lang/Comparable
CHECKCAST java/lang/Comparable
INVOKEVIRTUAL test3/IntelligentBox.set (Ljava/lang/Comparable;)V
  public synthetic bridge set(Ljava/lang/Object;)V
  public synthetic bridge get()Ljava/lang/Object;
public class test3/IntelligentBox implements test3/Box {
    public void set(Comparable t) { /* compiled code */ }

    public Comparable get() { /* compiled code */ }
    
    @Overide
    public synthetic bridge get(){
    }
    
    @Overide
    public synthetic bridge set(Object t){
        set((Comparable)t)
    }
}

泛型擦除的残留

看一下Box的字节码文件Box.class和查看生成的字节码

public interface Box<T> {
    void set(T var1);
    T get();
}

/**
 * ParameterizedType
 * 具体的范型类型, 如Map<String, String>
 * 有如下方法:
 *
 * Type getRawType(): 返回承载该泛型信息的对象, 如上面那个Map<String, String>承载范型信息的对象是Map
 * Type[] getActualTypeArguments(): 返回实际泛型类型列表, 如上面那个Map<String, String>实际范型列表中有两个元素, 都是String
 * Type getOwnerType(): 返回是谁的member.(上面那两个最常用)
 */
public class TestType {
    Map<String, String> map;
    //擦除 其实在类常量池里面保留了泛型信息
    public static void main(String[] args) throws Exception {
        Field f = TestType.class.getDeclaredField("map");
        System.out.println(f.getGenericType());                               // java.util.Map<java.lang.String, java.lang.String>
        System.out.println(f.getGenericType() instanceof ParameterizedType);  // true
        ParameterizedType pType = (ParameterizedType) f.getGenericType();
        System.out.println(pType.getRawType());                               // interface java.util.Map
        for (Type type : pType.getActualTypeArguments()) {
            System.out.println(type);                                         // 打印两遍: class java.lang.String
        }
        System.out.println(pType.getOwnerType());                             // null
    }
}

总结

使用泛型以及泛型擦除带来的影响(副作用)

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

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

//error报错,因为擦除后变成了Object,而Object是无法存放int
ArrayList<int> ints = new ArrayList<int>();
ArrayList<Integer> integerArrayList = new ArrayList<Integer>();

不能使用instanceof 运算符

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

ArrayList<String> stringArrayList = new ArrayList<String>();
//使用ArrayList<?>可以
if (stringArrayList instanceof ArrayList<?>){
            
}
//因为擦除ArrayList<String>后String丢失了
if (stringArrayList instanceof ArrayList<String>){

}

泛型在静态方法和静态类中的问题

因为泛型类中的泛型参数的实例化是在定义泛型类型对象
(比如ArrayList<Integer>)的时候指定的,而静态成员是不需要使用对象来调用的,所有对象都没创建,如何确定这个泛型参数是什么

//下面两个会报错,因为泛型参数是要创建对象时确定
public static T a;
public static T test1(T t) {
}

//这里不报错,因为这是一个泛型方法,此T非彼T test2(T t)的T
public static <T> T test2(T t) {
    return t;
}

泛型类型中的方法冲突

因为擦除后两个equals方法变成一样的了

//方法冲突,因为擦除后变一样了
@Override
public boolean equals(T obj) {
    return super.equals(obj);
}

@Override
public boolean equals(Object obj) {
    return super.equals(obj);
}

没法创建泛型实例

因为类型不确定

class Test02 {
   //无法创建一个类型参数的实例。下面会报错
   public static <E> void append(List<E> list) {
      //  E elem = new E();  // compile-time error
      //  list.add(elem);
   }
   //通过反射创建一个参数化类型的实例
   public static <E> void append(List<E> list, Class<E> cls) throws Exception {
       E elem = cls.newInstance();   // OK
       list.add(elem);
   }
}

没有泛型数组

因为数组是协变,擦除后就没法满足数组协变的原则

//        Plate<Apple>[] applePlates = new Plate<Apple>[10];//不允许
//        T[] arr = new T[10];//不允许
       Apple[] apples = new Apple[10];
       Fruit[] fruits = new Fruit[10];
        System.out.println(apples.getClass());
        //class [Lcom.zero.genericsdemo02.demo02.Apple;
        System.out.println(fruits.getClass());
        //class [Lcom.zero.genericsdemo02.demo02.Fruit;
       fruits = apples;
       // fruits里面原本是放什么类型的? Fruit or Apple
        // Apple[]
       fruits[0] = new Banana();//编译通过,运行报ArrayStoreException
        //Fruit是Apple的父类,Fruit[]是Apple[]的父类,这就是数组的协变
        //如果加入泛型后,由于擦除机制,运行时将无法知道数组的类型
        Plate<?>[] plates = new Plate<?>[10];//这是可以的

泛型,继承和子类型

给定两种具体的类型A和B(例如Fruit和Apple),
无论A和B是否相关,
MyClass<A>与MyClass<B>都没半毛钱关系,
它们的公共父对象是Object

泛型PECS原则

在泛型编程时,使用部分限定的形参时,<? super T>和<? extends T>的使用场景容易混淆, PECS原则可以帮助我们很好记住它们:提供者(Provider)使用extends,消费者(Consumer) 使用super。通俗地说, Provider指的就是该容器从自己的容器里提供T类型或T的子类型的对象供别人使用; Consumer指的就是该容器把从别处拿到的T类型或T的子类型的对象放到自己的容器。

Kotlin的泛型

var textViews: List<out TextView>
var textViews: List<in TextView>

声明处的 out 和 in

Kotlin 提供了另外一种写法:可以在声明类的时候,给泛型符号加上 out 关键字,表明泛型参数 T 只会用来输出,在使用的时候就不用额外加 out 了。

class Producer<out T> {
    fun produce(): T {
        ...
    }
}

val producer: Producer<TextView> = Producer<Button>() // 👈 这里不写 out 也不会报错
val producer: Producer<out TextView> = Producer<Button>() // 👈 out 可以但没必要

where关键字

Java 中声明类或接口的时候,可以使用 extends 来设置边界,将泛型类型参数限制为某个类型的子集,同时这个边界是可以设置多个,用 & 符号连接:

//T 的类型必须同时是 B 和 C 的子类型
class A<T extends B & C>{ 
}

在Kotlin中

//T 的类型必须同时是 B 和 C 的子类型
class A<T> where T : B, T : C

reified关键字

inline fun <reified T> printIfTypeMatch(item: Any) {
    if (item is T) { // 👈 这里就不会在提示错误了
        println(item)
    }
}

Kotlin 泛型与 Java 泛型不一致的地方

  1. Java 里的数组是支持协变的,而 Kotlin 中的数组 Array 不支持协变。

这是因为在 Kotlin 中数组是用 Array 类来表示的,这个 Array 类使用泛型就和集合类一样,所以不支持协变。

  1. Java 中的 List 接口不支持协变,而 Kotlin 中的 List 接口支持协变。

Java 中的 List 不支持协变,原因在上文已经讲过了,需要使用泛型通配符来解决。

在 Kotlin 中,实际上 MutableList 接口才相当于 Java 的 List。Kotlin 中的 List 接口实现了只读操作,没有写操作,所以不会有类型安全上的问题,自然可以支持协变。

面试常问

  1. Array中可以用泛型吗?

答:不能

  1. 泛型类型引用传递问题

问:你可以把List<String>传递给一个接受List<Object>参数的方法吗?

ArrayList<String> arrayList1=new ArrayList<Object>();
ArrayList<Object> arrayList1=new ArrayList<String>();

答:不能。没有半毛钱关系

  1. Java中List<?>和List<Object>之间的区别是什么?
    答:
  1. 什么是泛型中的限定通配符和非限定通配符 ?

答:

5.泛型类型变量不能是基本数据类型

//error
ArrayList<double> arr1 = new ArrayList<>();
ArrayList<Double> arr2 = new ArrayList<>();
  1. 运行时类型查询
ArrayList<String> arrayList=new ArrayList<String>();
if( arrayList instanceof ArrayList<String>) //擦除
if( arrayList instanceof ArrayList<?>)  

  1. Java 的泛型本身是不支持协变和逆变的
  1. Java中数组是协变的
上一篇下一篇

猜你喜欢

热点阅读