聊聊Java泛型

Posted 夜猫nightcat

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊聊Java泛型相关的知识,希望对你有一定的参考价值。

目录

引子

好处

泛型通配符

泛型位置

    出现在类或者接口上

    出现在方法中使用,包括返回值,参数

    出现在变量声明中

    注意事项

泛型边界

分类

无边界 

上界

下界

作用时机

类型擦除

部分擦除

哪些被擦除了,哪些没被擦除?

擦除的规则

泛型接口的多态实现

桥方法

可能的疑问


引子

Java在jdk 1.5引入泛型的概念,在1.5之前没有泛型.        

以 ArrayList 为例,它的底层是一个Object类型的数组,确保该容器可以放任何类型的元素. 比如:

 /**
  * 无泛型
  */
    @Test
    public void test01() {
        ArrayList list = new ArrayList();
        list.add("123");
        list.add(456);
        Student student = new Student();
        student.setAge(18);
        student.setName("夜猫");
        list.add(student);
        for (Object o : list) {
            System.out.println(o);
        }
    }

这段代码的输出是:

123
456
Student(age=18, name=夜猫)

到目前为止看起来还不错,我们放置了多种类型的元素,与数组只能放同一类型的元素,且必须提前指定数组长度相比,是一个巨大的进步. 

接下来你的同事告诉你:嘿,你要的学生信息我给你放到list里了,你要让他们开始写代码.

你看了一下Student的结构,正好有一个writeCode()方法

@Data
public class Student {
    private Integer age;
    private String name;

    public void writeCode() {
        System.out.println("写代码");
    }

 你心里想,这还不简单,我直接调用就好了.

for (Object o : list) {
    Student s = (Student) o;
    s.writeCode();
}

完美.可是运行之后发生了ClassCastException,提示你String没法转成Student.

原来是你的同事把很多其它的信息也放进去了. 

java.lang.ClassCastException: java.lang.String cannot be cast to com.baoly.generics.Student

 这当然难不倒你,你想那我判断一下元素类型,是student我再调用这个方法就可以了,于是

for (Object o : list) {
    if (o instanceof Student) {
        Student s = (Student) o;
        s.writeCode();
        }
    }

直到后来,你同事向list中放了Car,Teacher,File,City等类型,于是你每种类型都添加了手动的类型判断... 

这时你想:如果能在编写代码时就限制住List中存放的数据类型,那么就不用这么麻烦了。  

好处

后来你的同事把这段代码加了泛型,指定只能放Student类型,这次你终于不用手动判断类型然后再进行强转了.

ArrayList<Student> list = new ArrayList();
Student student = new Student();
student.setAge(18);
student.setName("夜猫");
list.add(student);
for (Student student1 : list) {
     student1.writeCode();
    }

可是新的问题也产生了,你现在一个Person类,一个Student类,一个Teacher类,它们之间的关系是这样的:

 这个时候我想放Person Teacher Student三种类型的数据,引入泛型之后,我是不是需要声明三种不同泛型的List来存放它们呢?如果声明不同类型的List,那么我们的容器数量会变得很多,我们常常有这样的需求,List中放置的是一类对象,以及它的子类或者父类,这时又该怎么做呢?

泛型通配符

泛型统配符实际上可以理解成一个占位符,它可以用任意大写字母表示,在实际开发过程中约定俗成的常见以下泛型(只是习惯,不是强制要求)

<K,V> 表示key,value键值对,常见于声明Map容器

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

<E> 表示element,比如ArrayList的add方法声明中

  public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

<T> 表示Type 即类

泛型位置

    出现在类或者接口上

public class Demo<K, V> {
    public K getKey(K k, V v) {
        return k;
    }
}

   出现在方法中使用,包括返回值,参数

 public <T> T getData(T t) {
        return t;
    }

如果你第一次见泛型方法,可能会对它的格式有疑问,T是返回值类型,<T>是什么?

<T>是泛型声明,泛型需要先声明,再使用,而出现在类上的例子,方法中的K在哪里声明的?

答案是在类中声明的泛型. 

出现在变量声明中

public class Demo<K, V> {
    public K data;

    public K getData() {
        return data;
    }

    public void setData(K data) {
        this.data = data;
    }
}

注意事项

  对于在类上声明的泛型,他们可以直接被使用在 

  • 成员变量类型
  • 实例方法返回值类型和参数类型中
  • 对于静态方法和静态成员变量不能直接使用

  对于静态方法,我们可以用静态方法自己声明的泛型类型,如:

 public static <E> E getData1() {
        return null;
 }

出现上面这种差异的原因是:当使用泛型类时,必须在创建对象时执行类型参数的值,对于静态方法,我们没有创建对象,它就生效了,所以静态方法不能使用类上声明的泛型.

而使用泛型方法时,我们通常不用指明参数类型,因为编译器会类型推断出具体的参数类型。所以虽然静态方法不能使用类上声明的泛型,但我们依然可以使用静态方法自己声明的泛型类型. 

泛型边界

了解了泛型通配符之后,回到我们最开始的问题:

我想声明一个容器List,它里面既可以放Person,也可以放Person和Teacher,或者Person和Student,那又该怎么做呢?这就需要了解泛型边界了.

分类

无边界 <?>

无边界的泛型,顾名思义,它不对类型做限制,它默认的上界就是Object.

看个例子:

  public void receiveList(ArrayList<?> list) {

}

 @Test
 public void test() {
   ArrayList<String> strList = new ArrayList<>();
   ArrayList<Integer> intList = new ArrayList<>();
   receiveList(strList);
   receiveList(intList);
}

receiveList()方法接收一个无边界泛型的ArrayList,观察发现,声明的intList和strList都可以接收

上界<? extends >

就是对上边界进行限制,你传入的类型不能超过extends的类型

看个例子:

    public void receiveList(ArrayList<? extends Number> list) {

    }

    @Test
    public void test() {
        ArrayList<String> strList = new ArrayList<>();
        ArrayList<Integer> intList = new ArrayList<>();
        ArrayList<Double> doubleList = new ArrayList<>();
        receiveList(strList); // 编译错误
        receiveList(intList); 
        receiveList(doubleList);
    }

上面的代码表示receiveList只能接收泛型为Number或Number的子类的ArrayList

下界<? super >

就是对下边界进行限制,你传入的类型不能低于super的类型

看个例子:

  public void receiveList(ArrayList<? super Integer> list) {

    }

    @Test
    public void test() {
        ArrayList<String> strList = new ArrayList<>();
        ArrayList<Integer> intList = new ArrayList<>();
        ArrayList<Number> numberList = new ArrayList<>();
        receiveList(strList);  //编译错误
        receiveList(intList);
        receiveList(numberList);
    }

作用时机

 泛型约束只有在编译期间有效,在运行期间都会被擦除,擦除规则请继续阅读.不过在此之前,我们先看一段代码.

  @Test
    public void test() throws Exception {
        List<Integer> intList = new ArrayList<>();
        intList.add(123);
        Class<? extends List> clazz = intList.getClass();
        Method addMethod = clazz.getDeclaredMethod("add",Object.class);
        addMethod.invoke(intList,"name");
        System.out.println(intList); // [123,name]
    }

 在上面的程序中,第二行我们正常添加了一个int类型的123,然后在运行期间,我们通过反射,向"intList"中添加了一个字面值为name的字符串.也就是说泛型Integer的限制并未在运行期生效.

类型擦除

先看以下的代码:

 @Test
    public void test() {
        ArrayList<String> strList = new ArrayList<>();
        ArrayList<Integer> intList = new ArrayList<>();
        System.out.println(strList.getClass() == intList.getClass()); // true

}

intList 和strList getClass()的结果都是java.util.ArrayList.

也就是说:泛型只有在静态类型检查时才生效,在此之后,部分泛型类型都会被擦除.即编译擦除.

部分擦除

注意,Java中并不是所有用到泛型的地方都会进行泛型擦除。

我们先来看一个例子

public class Person<T> {
}

 @Test
    public void test() throws Exception {
    Person<String> person = new Person<>();
    TypeVariable<? extends Class<? extends Person>>[] typeParameters = 
    person.getClass().getTypeParameters();
    System.out.println(Arrays.toString(typeParameters));//T
}

  

上面的代码运行结果为T,这也就是说,虽然我们传递的是String,但实际上拿到的只是一个占位符.到目前为止,我们无法取得与T绑定的泛型信息.

再看一个例子

public class Student extends Person<String> {
}

 @Test
  public void test() throws Exception {
     Type type = Student.class.getGenericSuperclass();
     System.out.println(type.toString()); // com.baoly.Person<java.lang.String>
}

上面代码的运行结果是 com.baoly.Person<java.lang.String>,我们拿到了传递给Person的类型信息.

哪些被擦除了,哪些没被擦除?

以下信息将会被保留

  • 泛型类和泛型接口上的声明

  • 泛型方法的参数和返回值的声明 

其余的泛型信息被擦除了.

这也就是为什么我们可以通过用非泛型类继承泛型类之后取得泛型信息的原因.

擦除的规则

  • 对于未指定边界的泛型,擦除为Object
  • 对于指定边界的泛型,将擦除为它的非泛型上界

  比如List<T>擦除为List,List<T extends People> 擦除为People,等等.

泛型接口的多态实现

了解了泛型擦除,我们来思考一下它可能带来的问题,先来看一下代码

public interface Base<T> {
    void setItem(T t);

    T getItem();
}
public class BaseImpl implements Base<Integer> {
    @Override
    public void setItem(Integer integer) {

    }

    @Override
    public Integer getItem() {
        return null;
    }
}

我们先定义了一个泛型接口T,在子类(实现类)实现的时候,不通的子类会传递自己需要的类型参数,观察上面的BaseImpl声明,我们发现,由于子类实现父接口时给定了参数类型,使得子类中的参数和方法返回值都有了具体的类型,本例中是Integer.

我们知道,想要实现多态,其中一个必要条件是子类重写父类方法.可由于类型擦除,父接口中的T会被擦除成Object,而子类的类型参数是Integer,泛型是不是就破坏了多态了. 

桥方法

为了解决上面出现的问题,Java使用桥接方法去实现多态,它的思路是:

在编译阶段进行参数泛化,生成桥方法,既然接口T被擦除成Object,那么它就生成一个参数为Object 类型的setItem 方法,再生成一个返回值为Object类型的getItem方法,在生成的桥接方法内部,调用子类实际重写的setItem(Integer integer) 和 Integer getItem()来达到多态的目的,

看一下BaseImpl.class

 public com.baoly.BaseImpl();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/baoly/BaseImpl;

  public void setItem(java.lang.Integer);
    descriptor: (Ljava/lang/Integer;)V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/baoly/BaseImpl;
            0       1     1 integer   Ljava/lang/Integer;

  public java.lang.Integer getItem();
    descriptor: ()Ljava/lang/Integer;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aconst_null
         1: areturn
      LineNumberTable:
        line 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       2     0  this   Lcom/baoly/BaseImpl;

  public java.lang.Object getItem();
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #2                  // Method getItem:()Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/baoly/BaseImpl;

  public void setItem(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #3                  // class java/lang/Integer
         5: invokevirtual #4                  // Method setItem:(Ljava/lang/Integer;)V
         8: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/baoly/BaseImpl;
}
Signature: #24                          // Ljava/lang/Object;Lcom/baoly/Base<Ljava/lang/Integer;>;
SourceFile: "BaseImpl.java"

我们可以发现,BaseImpl自己写了两个方法,在编译阶段,又生成了两个桥接方法

观察

flags: ACC_BRIDGE

invokevirtual 调用的method是我们自己写的方法 

可能的疑问

生成桥接方法,不会导致类本身方法冲突么?

不会,因为在jvm层面,方法的签名包含方法返回值,从而避免了桥方法和本身方法的冲突问题.

以上是关于聊聊Java泛型的主要内容,如果未能解决你的问题,请参考以下文章

操作 Java 泛型:泛型在继承方面体现与通配符使用

Java泛型:类型擦除

用了这么多年的 Java 泛型,你对它到底有多了解?

201621123062《java程序设计》第九周作业总结

什么意思 在HashMap之前 ? Java中的泛型[重复]

聊聊 C# 和 C++ 中的 泛型模板 底层玩法