Java 泛型相关整理

1. 概述

  • Java 泛型(generics)是 JDK 5 中引入的一个新特性,泛型提供了 编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
  • 泛型,即 参数化类型。将类型由原来的具体的类型(类似于方法的变量参数,该变量定义了具体的类型),也定义成参数形式(可以称之为类型形参),然后在使用/调用时再传入具体的类型(类型实参)。
  • 泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。
  • 在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
  • 泛型机制将类型转换时的类型检查从运行时提前到了编译时。使用泛型编写的代码比杂乱的使用 Object 并在需要时再强制类型转换的机制具有更好的可读性和安全性。
  • 泛型程序设计意味着程序可以被不同类型的对象重用,类似 C++ 的模版。
  • 使用泛型时,在实际使用之前类型就已经确定了,不需要强制类型转换,适用于多种数据类型执行相同的代码。
  • 泛型只在编译阶段有效,编译之后程序会采取去泛型化的措施,在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法,泛型信息不会进入到运行时阶段,泛型类型在逻辑上看可以看成是多个不同的类型,实际上都是相同的基本类型
public static void main(String[] args) {
        List<String> stringList = new ArrayList<String>();
        List<Integer> integerList = new ArrayList<Integer>();

        Class classStringArrayList = stringList.getClass();
        Class classIntegerArrayList = integerList.getClass();

        System.out.println(classStringArrayList.getClass() == classIntegerArrayList.getClass());
}
// print true

2. 泛型的使用

  • 泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。

2.1 泛型类

  • 泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。例如各种容器类 List、Set、Map。
  • 泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。
public class Pair<T> {
    private T field;
}
  • 其中 <T> 是类型参数定义,使用时可以传入类型,Pair<String> p = new Pair<String>();,field 则被指定为 String 类型。
  • 如果引用多个类型,可以使用逗号分隔:<S, D>。
  • 类型参数名可以使用任意字符串,但建议使用有代表意义的单个字符,以便于和普通类型名区分,如:T 代表 type。源数据和目的数据就使用 S,D。子元素类型使用 E 等。
  • 可以不传入泛型类型实参,传入泛型类型实参,则会根据传入的泛型类型实参做相应的限制,如果不传入泛型类型实参,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。

2.2 泛型接口

  • Java 泛型接口的定义和 Java 泛型类基本相同。
  • 泛型接口常被用在各种类的生产器中。
public interface Generator<T> {
    public T next();
}
  • 泛型接口未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中。
/**
 * 即:class FruitGenerator<T> implements Generator<T> {
 * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}
  • 如果泛型接口传入类型参数时,实现该泛型接口的实现类,则所有使用泛型的地方都要替换成传入的实参类型。
/**
 * 传入泛型实参时:
 * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口 Generator<T>
 * 但是我们可以为 T 传入无数个实参,形成无数种类型的 Generator 接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:Generator<T>,public T next(); 中的的 T 都要替换成传入的 String 类型。
 */
public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

2.3 泛型方法

  • 泛型类,是在实例化类的时候指明泛型的具体类型,泛型方法,是在调用方法的时候指明泛型的具体类型 。
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
  IllegalAccessException{
        T instance = tClass.newInstance();
        return instance;
}
  • public 与 返回值中间 <T> ,可以理解为声明此方法为泛型方法。
  • 只有声明了 <T> 的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
  • <T> 表明该方法将使用泛型类型 T,此时才可以在方法中使用泛型类型 T。
  • 与泛型类的定义一样,此处 T 可以随便写为任意标识,常见的如 T、E、K、V 等形式的参数常用于表示泛型。
  • 如果类中的静态方法使用泛型,静态方法无法访问类上定义的泛型,必须要将泛型定义在方法上
public class StaticGenerator<T> {
    ....
    /**
     * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
     * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
     * 如:public static void show(T t){..},此时编译器会提示错误信息:
          "StaticGenerator cannot be refrenced from static context"
     */
    public static <T> void show(T t) {
    }
}
  • 泛型方法能使方法独立于类而产生变化。

泛型方法和可变参数

  • 这里的参数 T 可以同时传入不同类型。
public <T> void printMsg( T... args){
    for(T t : args){
         System.out.println(t);
    }
}

2.4 类型通配符

  • 同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的,因此有了通配符。

    • 类型通配符一般是使用 ? 代替具体的类型实参。此处 ? 是类型实参,而不是类型形参,且 Class<?> 在逻辑上是 Class<Integer>、Class<Number>...等所有 Class<具体类型实参> 的父类。
    • 当具体类型不确定的时候,可以使用通配符。
    • 当操作类型不需要使用类型的具体功能时,只使用 Object 类中的功能。那么可以用 ? 通配符(无限定通配符)来表示未知类型。
    • 无限定通配符只可读不可写。
    • 如果既想存又想取,就别用通配符。
    • Class<?> 不等于 Class<Object>,Class<Object> 是 Class<?> 的子类。
    • JDK 1.7 中,增加了泛型的类型推断机制。
// JDK 1.7 之前
Map<String, String> map = new HashMap<String, String>();
// JDK 1.7 类型推断
Map<String, String> map = new HashMap<>();

2.5 泛型上下边界

  • 使用泛型的时候,可以为传入的泛型类型实参进行上下边界的限制。
  • 不可同时声明上限和下限限定符,及 extends 和 super 只能出现一个。

上限(extends)子类型通配符

  • 指定的类必须是继承某个类,或者实现了某个接口(这里不是 implements)。

    • ? extends List

上限(extends)范围

public class Plate <? extends Fruit> {
}
  • 上界通配符是允许读取操作的,获取出来的对象都可以隐式的转为其基类(或者 Object 基类),上界描述符 extends 适合频繁读取的场景。

下限(super)超类型通配符

  • 即父类或本身。

    • ? super List

下限(super)范围

public class Plate <? super Fruit> {
}
  • 下界通配符规定了元素最小的粒度,必须是 T 及其基类,那么往里面存储 T 及其派生类都是可以的,因为都可以隐式的转化为 T 类型。但是往外读就不好控制了,里面存储的都是 T 及其基类,无法转型为任何一种类型,只有 Object 基类才能装下。

2.6 泛型数组

  • 不能创建一个确切的泛型类型的数组,错误的用例如下。
List<String>[] ls = new ArrayList<String>[10];  
  • 使用通配符创建泛型数组是可以的。
List<?>[] ls = new ArrayList<?>[10];  
  • 这样也是可以的。
List<String>[] ls = new ArrayList[10];
List<String>[] lsa = new List<String>[10]; // Not really allowed.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Unsound, but passes run time store check
String s = lsa[1].get(0); // Run-time error: ClassCastException.
  • 由于 Java 虚拟机泛型的擦除机制,在运行时虚拟机是不知道泛型信息的,所以可以给 oa[1] 赋上一个 ArrayList 而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。
  • 而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。
List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Correct.
Integer i = (Integer) lsa[1].get(0); // OK
  • 数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。

2.7 泛型的约束和限制

泛型的类型参数只能是类类型,不能是简单类型

  • 原因在于类型擦除,Object 不能存储基本类型(byte、char、short、int、long、float、double、boolean)。

类型检查不可使用泛型

  • 可以通过下面的代码来解决泛型的类型信息由于擦除无法进行类型检查的问题。
public class Test {

    Class<?> aClass;

    public Test(Class<?> aClass) {
        this.aClass = aClass;
    }

    public boolean isInstance(Object object) {
        return aClass.isInstance(object);
    }

    public static void main(String[] args) {
        Test test = new Test(A.class);
        System.out.println(test.isInstance(new A()));
        System.out.println(test.isInstance(new B()));
    }

    public static class A {
    }

    public static class B {
    }
}
/** print
true
false
**/

不能实例化泛型对象

T t= new T();//error
T.class.newInstance();//error
T.class;//error
  • 解决办法是传入 Class<T> t 参数,调用 t.newInstance()
public void sayHi(Class<T> c){
  T t=null;
  try {
    t=c.newInstance();
  } catch (Exception e) {
    e.printStackTrace();
  }
  System.out.println("Hi "+t);
}

不能在泛型类的静态域中使用泛型类型

  • 泛型类中,<T> 称为类型变量,实际上相当于在类中隐形的定义了一个不可见的成员变量 private T t,这是对象级别的,对于泛型类型变量来说在对象初始化时才知道其具体类型。

    • 而静态域中,不需要对象初始化就可以直接调用,因此这是矛盾的。
public class Singleton<T>{
    private static T singleton; //error
    public static T getInstance(){} //error
    public static void print(T t){} //error
}
  • 静态的泛型方法可以使用泛型类型。
public static <T> T getInstance(){return null;} //ok
public static <T> void print(T t){} //ok
  • 静态的泛型方法,是在方法层面定义的,在调用方法时,T 所指的具体类型已经明确。

不能捕获泛型类型的对象

  • Throwable 类不可以被继承,自然也不可能被 catch
public class GenericThrowable<T> extends Throwable{
  //The generic class GenericThrowable<T> may not subclass java.lang.Throwable
}
  • 由于 Throwable 可以用在泛型类型参数中,因此可以变相的捕获泛型的 Throwable 对象。
@Test
public void testGenericThrowable(){
  GenericThrowable<RuntimeException> obj=new GenericThrowable<RuntimeException>();
  obj.doWork(new RuntimeException("What did you do?"));
}
public static class GenericThrowable<T extends Throwable>{
  public void doWork(T t) throws T{
    try{
      Reader reader=new FileReader("notfound.txt");
      //这里应该是checked异常
    }catch(Throwable cause){
      t.initCause(cause);
      throw t;
    }
  }
}
  • FileReader 实例化可能抛出已检查异常,JDK 中要求必须捕获或者抛出已检查异常。这种模式把它给隐藏了。也就是说可以消除已检查异常,后果不可预料,慎用。

2.8 泛型嵌套

  • 执行顺序是从外向里取。
Student<String> student = new Student<String>();
student.setScore("优秀");
System.out.println(student.getScore());

//泛型嵌套
School<Student<String>> school = new School<Student<String>>();
school.setStu(student);

String s = school.getStu().getScore(); //从外向里取
System.out.println(s);

// hashmap 使用了泛型的嵌套
Map<String, String> map =  new HashMap<String,String>();
map.put("a", "张三");
map.put("b", "李四");
Set<Entry<String, String>> set = map.entrySet();
for (Entry<String, String> entry : set) {
     System.out.println(entry.getKey() + ":" + entry.getValue());
}

3. 泛型擦除

  • 泛型只在编译阶段有效,编译后类型被擦除了,也就是说 Java 虚拟机中没有泛型对象,只有普通对象。

3.1 擦除方式

  • 对于 <T> 的擦除,根据 T 在类中出现位置的不同,分 5 种情况。

    1. T 是成员变量的类型。
    2. T 是泛型变量(无论成员变量还是局部变量)的类型参数,常见如 Class<T>,List<T>。
    3. T 是方法抛出的 Exception(要求 <T extends Exception>)。
    4. T 是方法的返回值。
    5. T 是方法的参数。
  • 情况 1 的擦除不会有任何影响,因为编译器会在泛型被调用的地方加上类型转换。
  • 情况 2 的擦除也不会有问题,要实现不可变类,就要保证成员变量中引用指向的类型也是不可变的,是个递归定义。
  • 情况 3 用例。
class Parent<T extends SQLException>{
    public void test() throws T{}
}

class Son extends Parent<BatchUpdateException>{
    @Override
    public void test() throws BatchUpdateException{} //这里必须与参数类型保持一致,否则编译不通过。
}
  • Parent 参数类型被擦除之后。
class Super<SQLException>{
    public void test() throws SQLException{}
}
  • 与 Son 类对比,发现并没有违背 Java 中方法重写(Override)的规则。

    • 子类方法的方法名和参数列表与父类方法的相同。
    • 子类方法的返回类型是父类方法返回类型的子类(协变返回类型,范围更窄)。
    • 子类方法抛出的异常少于父类方法抛出的异常(范围更窄)。
    • 子类方法的访问控制权限大于父类方法(访问范围更宽)。
  • 情况 4 中 T 作为返回类型时被擦除,因为协变返回类型的存在,所以不会有问题。
class Parent<T>{
    T test(){}
}
class Son extends Parent<String>{
    @Override
    protected String test(){}  //protected 拥有比 package 更高的访问权限,可以被同一包内的类访问
}
  • 类型擦除后的 Parent,同样没有违背重写规则。
class Super{
    Object test(){}
}
  • 情况 5 在 3.4 擦除冲突中说明。

3.2 擦除原理

  • 非泛型版本用例。
public class Test {

    private Object obj;

    public Object getObj() {
        return obj;
    }

    public void setObj(Object obj) {
        this.obj = obj;
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.setObj("test");
        String testString = (String) test.getObj();
    }
}
  • 通过查看字节码。
public class Test
{
  public Test();
    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 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LTest;

  public java.lang.Object getObj();
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field obj:Ljava/lang/Object;
         4: areturn
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LTest;

  public void setObj(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2                  // Field obj:Ljava/lang/Object;
         5: return
      LineNumberTable:
        line 10: 0
        line 11: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   LTest;
            0       6     1   obj   Ljava/lang/Object;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #3                  // class Test
         3: dup
         4: invokespecial #4                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: ldc           #5                  // String test
        11: invokevirtual #6                  // Method setObj:(Ljava/lang/Object;)V
        14: aload_1
        15: invokevirtual #7                  // Method getObj:()Ljava/lang/Object;
        18: checkcast     #8                  // class java/lang/String
        21: astore_2
        22: return
}
  • 泛型版本用例。
public class Test<T> {

    private T obj;

    public T getObj() {
        return obj;
    }

    public void setObj(T obj) {
        this.obj = obj;
    }

    public static void main(String[] args) {
        Test<String> test = new Test<String>();
        test.setObj("test");
        String string = test.getObj();
    }
}
  • 通过查看字节码。
public class Test<T extends java.lang.Object> extends java.lang.Object
{
  public Test();
    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 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LTest;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LTest<TT;>;

  public T getObj();
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field obj:Ljava/lang/Object; 运行期为 Object 类型
         4: areturn
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LTest;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LTest<TT;>;
    Signature: #25                          // ()TT;

  public void setObj(T);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2                  // Field obj:Ljava/lang/Object;
         5: return
      LineNumberTable:
        line 10: 0
        line 11: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   LTest;
            0       6     1   obj   Ljava/lang/Object;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   LTest<TT;>;
            0       6     1   obj   TT;
    Signature: #28                          // (TT;)V

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #3                  // class Test
         3: dup
         4: invokespecial #4                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: ldc           #5                  // String test
        11: invokevirtual #6                  // Method setObj:(Ljava/lang/Object;)V
        14: aload_1
        15: invokevirtual #7                  // Method getObj:()Ljava/lang/Object;
        18: checkcast     #8                  // class java/lang/String 类型转换为编译器自动添加
        21: astore_2
        22: return
}
Signature: #37                          // <T:Ljava/lang/Object;>Ljava/lang/Object;
  • 在编译过程中,类型变量的信息可以拿到。

    • set() 方法,在编译器可以做类型检查,非法类型不能通过编译。
    • get() 方法,由于擦除机制,运行时的实际引用类型为 Object 类型。
  • 为了 " 还原 " 返回结果的类型,编译器在 get 之后添加了类型转换。
    • Test.class 文件 main() 方法主体第 18 行有类型转换的逻辑,这是编译器自动添加的。
  • 编译器在泛型类对象读取和写入的位置做了处理,为代码添加了约束。

3.3 擦除残留

  • 通过查看字节码,发现还有一些泛型的信息未擦除,这些就是擦除的残留。

    • descriptor(对方法参数和返回值进行描述)。
    • Signature(JDK 5 加入,标记了定义时的成员签名,包括定义时的泛型参数列表,参数类型,返回值等)。
  • 最后一行是类的签名,可以看到 T 后面跟了擦除后的参数类型,这样的机制,对于分析字节码是有意义的。
Signature: #37                          // <T:Ljava/lang/Object;>Ljava/lang/Object;

3.4 擦除冲突

重载与重写

  • 定义一个普通的子类与父类,子类重载了父类的 setName() 方法。
public class Parent {

    private Object obj;

    public void setName(Object name) {
        System.out.println("Parent:" + name);
    }

    public Object getName() {
        return obj;
    }
}
public class Son extends Parent {
    private String obj;

    public void setName(String name) {
        System.out.println("Son:" + name);
    }

    public String getName() {
        return obj;
    }

    public static void main(String[] args) {
        Son son = new Son();
        son.setName("abc");
        son.setName(new Object());
    }
}
/** print
Son:abc
Parent:[email protected]
** /
  • 修改 Parent 为泛型类。
public class Parent<T> {

    public void setName(T name) {
        System.out.println("Parent:" + name);
    }
}
  • 从擦除的机制得知,擦除后 class 文件应该为。
public class Parent {
        public void setName(Object name) {
            System.out.println("Parent:" + name);
        }
}
  • 如果修改 Son 为用例,发现编译器 son.setName(new Object()) 提示了错误,造成了重载无效。
public class Son extends Parent<String> {
        public void setName(String name) {
              System.out.println("Son:" + name);
        }

        public static void main(String[] args) {
              Son son = new Son();
              son.setName("abc");
              son.setName(new Object());//The method setName(String) in the type Son is not applicable for the arguments (Object)
        }
}
  • 重载无效是泛型擦除造成的,无论 setName(String) 是否标注为 @Override 都将是重写而不是重载。
  • 通过查看字节码发现编译器内部编译了两个 setName() 方法。
    • 编译器会自动生成一个桥方法(bridge method),使用桥方法覆盖了泛型父类的 setName(Object) 方法,解决了多态问题。
    • 方法 setName(java.lang.Object) 有关键字 ACC_BRIDGE 指定为桥方法,关键字 ACC_SYNTHETIC 表示这个方法是由编译器自动生成。
    • 从常量池中也可以看出,桥方法调用了 Son 类中原有的重写方法。
public void setName(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=2
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        10: ldc           #5                  // String Son:
        12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: aload_1
        16: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        22: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        25: return
public void setName(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     #15                 // class java/lang/String
         5: invokevirtual #13                 // Method setName:(Ljava/lang/String;)V
         8: return
  • 同时也出现了两个方法签名一样的 getName() 方法,只是返回类型不同。
public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #9                  // Field obj:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LSon;
public java.lang.Object getName();
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #14                 // Method getName:()Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LSon;
  • Java 虚拟机使用参数类型和返回类型确定一个方法。

    • 一旦编译器通过某种方式自己编译出方法签名一样的两个方法(程序员不能人为编写这种代码,因为编译器无法确定调用哪个 getName(),所以禁止出现这种情况),Java 虚拟机是能够分清楚这些方法的,前提是需要返回类型不一样(在运行期,Java 虚拟机有足够的方法去区分这种二义性,比如用 ACC_BRIDGE 或 ACC_SYNTHETIC,所有就允许了这种情况出现)。
  • 为 Parent 增加方法 equals()
public boolean equals(T value){
        return (obj.equals(value));
}
  • 会提示错误:‘equals(T)‘ in ‘Parent‘ clashes with ‘equals(Object)‘ in ‘java.lang.Object‘; both methods have same erasure, yet neither overrides the other。
  • 似乎没有问题的代码连编译器都通过不了。
    • 子类方法要覆盖,必须与父类方法具有相同的方法签名(方法名 + 参数列表),而且必须保证子类的访问权限 >= 父类的访问权限。
    • 当编译器发现 Parent<T> 中的 equals(T) 方法时,第一反应是 equals(T) 没有覆盖住父类 Object 中的equals(Object) 方法。
    • 紧接着,编译器将泛型代码中的 T 用 Object 替代(擦除),发现与 Object 的 equals() 方法一致,造成了冲突(两个方法都有相同的擦除,但都不重写另一个)。
  • 在继承泛型类型的时候,桥方法的合成是为了避免类型变量擦除所带来的多态灾难

继承泛型的参数化

  • 一个泛型类的类型参数不同,称之为泛型的不同参数化。
  • 泛型有一个原则:一个类或类型变量不可成为两个不同参数化的接口类型的子类型。
  • 普通类用例没有问题。
import java.util.Comparator;
public class Parent implements Comparator {

    @Override
    public int compare(Object o1, Object o2) {
        return 0;
    }
}

import java.util.Comparator;
public class Son extends Parent implements Comparator {
}
  • 增加了泛型参数化。

    • 提示错误:‘java.util.Comparator‘ cannot be inherited with different type arguments: ‘Parent‘ and ‘Son‘。
    • 原因是 Son 实现了两次 Comparator<T>,擦除后均为 Comparator<Object>,造成了冲突。
import java.util.Comparator;
public class Parent implements Comparator<Parent> {

    @Override
    public int compare(Parent o1, Parent o2) {
        return 0;
    }
}

import java.util.Comparator;
public class Son extends Parent implements Comparator<Son> {
}

3.5 边界类型的协变性与逆变性

  • 对于 <? extends A> 和 <? super A> 的擦除,因为保留上界,所以擦除后并没有破坏 里氏替换原则
List<? extends Parent> list = new ArrayList<Son>();  //协变
List<? super Son> list2 = new ArrayList<Parent>();   //逆变
  • 类型擦除后,等价于。
List<Parent> list = new ArrayList<Son>();
List<Object> list2 = new ArrayList<Parent>(); 

里氏替换原则

  • 子类完全拥有父类的方法,且具体子类必须实现父类的抽象方法。
  • 子类中可以增加自己的方法。
  • 当子类覆盖或实现父类的方法时,方法的形参要比父类方法的更为宽松。
  • 当子类覆盖或实现父类的方法时,方法的返回值要比父类更严格。

协变与逆变的定义

  • 逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义。

    • 如果 A、B 表示类型,f(⋅) 表示类型转换,≤ 表示继承关系(比如,A ≤ B 表示 A 是由 B 派生出来的子类),那么。

      • f(⋅) 是逆变(contravariant)的,当 A ≤ B 时有 f(B) ≤ f(A) 成立。
      • f(⋅) 是协变(covariant)的,当 A ≤ B 时有 f(A) ≤ f(B) 成立。
      • f(⋅) 是不变(invariant)的,当 A ≤ B 时上述两个式子均不成立,即 f(A) 与 f(B) 相互之间没有继承关系。
  • 令 f(A) = ArrayList<A>,那么 f(⋅) 时是不变的。
    • 如果是逆变,则 ArrayList<Integer> 是ArrayList<Number> 的父类型。
    • 如果是协变,则 ArrayList<Integer> 是 ArrayList<Number> 的子类型。
    • 如果是不变,二者没有相互继承关系。
  • 令 f(A)=[]A,证明数组是协变的。
  • 在 Java 1.4 中,子类覆盖(override)父类方法时,形参与返回值的类型必须与父类保持一致。
class Parent {
    Number method(Number n) { ... }
}

class Son extends Parent {
    @Override
    Number method(Number n) { ... }
}
  • 从 Java 1.5 开始,子类覆盖父类方法时允许协变返回更为具体的类型。
class Parent {
    Number method(Number n) { ... }
}

class Son extends Parent {
    @Override
    Integer method(Number n) { ... }
}

PECS 原则

  • 上界 <? extends T> 不能往里存,只能往外取,适合频繁往外面读取内容的场景。
  • 下界 <? super T> 不影响往里存,但往外取只能放在 Object 对象里,适合经常往里面插入数据的场景。

4. 泛型与继承

  • 继承泛型类时,必须对父类中的类型参数进行初始化,或者说父类中的泛型参数必须在子类中可以确定具体类型。
  • 用具体类型初始化。
public class Son extends Parent<String> {}
  • 用子类中的泛型类型初始化父类。
public class Son<T> extends Parent<T> {}
  • 无论 Parent 和 Son 有什么继承关系,一般 Pair<Parent> 和 Pair<Son> 也没什么关系。
Pair<Son> s = new Pair<>();
Pair<Parent> p = s; //error
  • 泛型类自身可以继承其他类或实现接口,如 List<T> 的实现 ArrayList<T>。
  • 泛型类可以扩展泛型类或接口,如 ArrayList<T> 实现了 List<T>,此时 ArrayList<T> 可以转换为 List<T>,这是安全的。
  • Parent<T> 随时都可以转换为原生类型 Parent,但需要注意类型检查的安全性。
    • 需要注意,以下代码运行是没有异常的。
public class Parent<T> {

    private T name;

    public T getName() {
        return name;
    }

    public void setName(T name) {
        this.name = name;
    }

    public static void main(String[] args) {
        Parent<String> p1 = new Parent<>();
        p1.setName("abc");
        System.out.println(p1.getName());
        Parent p2 = p1;
        p2.setName(new File("1.txt")); //error
        System.out.println(p2.getName());
    }
}
/** print
abc
1.txt
**/

5. 泛型与反射

  • 有了泛型机制,JDK 的 reflect 包中增加了几个泛型有关的类。

    • Class<T>.getGenericSuperclass() 获取泛型超类。
    • ParameterizedType 类型参数实体类。

参考资料

http://www.sohu.com/a/245549100_796914
https://www.imooc.com/article/18159
https://blog.csdn.net/tyrroo/article/details/80930938
https://blog.csdn.net/wang__qin/article/details/81415223
https://segmentfault.com/a/1190000014824002
https://www.runoob.com/java/java-generics.html
https://www.cnblogs.com/coprince/p/8603492.html
https://www.cnblogs.com/lwbqqyumidi/p/3837629.html

原文地址:https://www.cnblogs.com/youngao/p/12576440.html

时间: 2024-11-10 06:53:36

Java 泛型相关整理的相关文章

Java泛型相关总结(上)

最近在看<Java核心技术>泛型相关的部分,总结下. 泛型程序设计是什么? 泛型编程(generic programming)是计算机编程中的一种风格,类型通过参数指定.意味着编写的代码可以被不同类型的对象所使用. 类型参数(type parameters),指示类型.ArrayList用类型参数来指示包含元素的类型.使程序有更好的可读性和安全性. 解决什么问题? 使代码具有更好的可读性和安全性. 如何用 泛型类(generic class)具有一个或多个类型参数的类.如下所示,用具体的类型替

Java泛型相关

字节码对象的三种获取方式 以String为例 Class<? extends String> strCls = "".getClass(); Class<String> strCls2 = String.class; Class strCls3 = Class.forName("java.lang.String"); System.out.println(strCls.equals(strCls2)); // true System.out.

Java 反射相关整理

1. Class 类 Class 是一个类,封装了当前对象所对应的类的信息,一个类中有属性,方法,构造器等. 对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对象.一个 Class 对象包含了特定某个类的有关信息. Class 对象只能由系统建立对象,一个类(而不是一个对象)在 Java 虚拟机中只会有一个 Class 实例. Class 对象的由来是将 class 文件读入内存,并为之创建一个 Class 对象. 获取 Class 类对象的三种方法 使用 Class.forNa

Java泛型的实现:原理与问题

很久没写博客了,因为项目和一些个人原因.最近复习找工作,看书+回想项目后有一些心得,加上博客停更这么长时间以来的积累,很是有些东西可写.从今儿开始,慢慢把之前积累的东西补上来,方便以后查漏补缺. 先从最近的开始.昨天看到Java泛型相关的内容,有些疑惑,查资料之后发现这部分很有些有意思的东西,比如类型擦除带来的重写问题等等,一并记录在这篇文章里. 1. 泛型定义 看了很多泛型的解释百度百科,解释1,解释2,都不是我想要的"以用为本"答案(没讲明白泛型的作用或者说设计目的),这里我自己总

详解Java泛型type体系整理

一直对jdk的ref使用比较模糊,早上花了点时间简单的整理了下,也帮助自己理解一下泛型的一些处理. java中class,method,field的继承体系 java中所有对象的类型定义类Type 说明: Type : Type is the common superinterface for all types in the Java programming language. These include raw types, parameterized types, array types,

Java集合相关面试问题和答案

Java集合相关面试问题和答案 面试试题 1.Java集合框架是什么?说出一些集合框架的优点? 每种编程语言中都有集合,最初的Java版本包含几种集合类:Vector.Stack.HashTable和Array.随着集合的广泛使用,Java1.2提出了囊括所有集合接口.实现和算法的集合框架.在保证线程安全的情况下使用泛型和并发集合类,Java已经经历了很久.它还包括在Java并发包中,阻塞接口以及它们的实现.集合框架的部分优点如下: (1)使用核心集合类降低开发成本,而非实现我们自己的集合类.

C++泛型 &amp;&amp; Java泛型实现机制

C++泛型 C++泛型跟虚函数的运行时多态机制不同,泛型支持的静态多态,当类型信息可得的时候,利用编译期多态能够获得最大的效率和灵活性.当具体的类型信息不可得,就必须诉诸运行期多态了,即虚函数支持的动态多态. 对于C++泛型,每个实际类型都已被指明的泛型都会有独立的编码产生,也就是说list<int>和list<string>生成的是不同的代码,编译程序会在此时确保类型安全性.由于知道对象确切的类型,所以编译器进行代码生成的时候就不用运用RTTI,这使得泛型效率跟手动编码一样高.

Java Reflection 相关及示例

Java Reflection 相关及示例 前言: 代码有点长.贴出github地址:https://github.com/andyChenHuaYing/scattered-items/tree/master/items-java-reflection 测试目标类:TargetClass.自定义的辅助类比较多.在这里不贴了.篇幅有限.并且测试也简单.因此测试类也没有提及. 一:简介 Java Reflection是针对Class也就是我们平常说的类而言的.用于操作Java中的Class.在Ja

泛型相关知识

一.基本信息 泛型是程序设计语言的一种特性.允许程序员在强类型程序设计语言中编写代码时定义一些可变部分,那些部分在使用前必须作出指明.各种程序设计语言和其编译器.运行环境对泛型的支持均不一样.将类型参数化以达到代码复用提高软件开发工作效率的一种数据类型.泛型类是引用类型,是堆对象,主要是引入了类型参数这个概念. 二.定义分类 泛型的定义主要有以下两种: 1.在程序编码中一些包含类型参数的类型,也就是说泛型的参数只可以代表类,不能代表个别对象.(这是当今较常见的定义) 2.在程序编码中一些包含参数