Java教程

Java-泛型擦除

本文主要是介绍Java-泛型擦除,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();

System.out.println(l1.getClass() == l2.getClass());
输出为 true

上面的代码中涉及到了泛型,而输出的结果缘由是类型擦除。先好好说说泛型。

泛型的定义和使用

泛型按照使用情况可以分为 3 种。

  • 泛型类。

  • 泛型方法。

  • 泛型接口。

泛型类

我们可以这样定义一个泛型类。

public class Test<T> {
  T field1;
}

尖括号 <>中的 T 被称作是类型参数,用于指代任何类型。事实上,T 只是一种习惯性写法,如果你愿意。你可以这样写。

public class Test<Hello> {
  Hello field1;
}

但出于规范的目的,Java 还是建议我们用单个大写字母来代表类型参数。常见的如:

  • T 代表一般的任何类。

  • E 代表 Element 的意思,或者 Exception 异常的意思。

  • K 代表 Key 的意思。

  • V 代表 Value 的意思,通常与 K 一起配合使用。

  • S 代表 Subtype 的意思

如果一个类被 的形式定义,那么它就被称为是泛型类。

Test<String> test1 = new Test<>();
Test<Integer> test2 = new Test<>();

只要在对泛型类创建实例的时候,在尖括号中赋值相应的类型便是。T 就会被替换成对应的类型,如 String 或者是 Integer。你可以相像一下,当一个泛型类被创建时,内部自动扩展成下面的代码。

public class Test<String> {
  String field1;
}

当然,泛型类不至接受一个类型参数,它还可以这样接受多个类型参数。

public class MultiType <E,T>{
  E value1;
  T value2;
  
  public E getValue1(){
    return value1;
  }
  
  public T getValue2(){
    return value2;
  }
}

泛型方法

public class Test1 {
  public <T> void testMethod(T t){
    
  }
}

泛型方法与泛型类稍有不同的地方是,类型参数也就是尖括号那一部分是写在返回值前面的。中的 T 被称为类型参数,而方法中的 T 被称为参数化类型,它不是运行时真正的参数。

当然,声明的类型参数,其实也是可以当作返回值的类型的。

public  <T> T testMethod1(T t){
    return null;
}

泛型类与泛型方法的共存

public class Test1<T>{
​
  public  void testMethod(T t){
    System.out.println(t.getClass().getName());
  }
  public  <T> T testMethod1(T t){
    return t;
  }
}

上面代码中,Test1是泛型类,testMethod 是泛型类中的普通方法,而 testMethod1 是一个泛型方法。

而泛型类中的类型参数与泛型方法中的类型参数是没有相应的联系的,泛型方法始终以自己定义的类型参数为准。

所以,针对上面的代码,我们可以这样编写测试代码。

Test1<String> t = new Test1();
t.testMethod("generic");
Integer i = t.testMethod1(new Integer(1));

泛型类的实际类型参数是 String,而传递给泛型方法的类型参数是 Integer,两者不相干。

但是,为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。比如,Test1代码可以更改为这样

public class Test1<T>{
​
  public  void testMethod(T t){
    System.out.println(t.getClass().getName());
  }
  public  <E> E testMethod1(E e){
    return e;
  }
}

泛型接口

泛型接口和泛型类差不多

public interface Iterable<T> {
}

通配符 ?

除了用 表示泛型外,还有 <?>这种形式。? 被称为通配符。

class Base{}
​
class Sub extends Base{}
​
Sub sub = new Sub();
Base base = sub;      

上面代码显示,Base 是 Sub 的父类,它们之间是继承关系,所以 Sub 的实例可以给一个 Base 引用赋值,那么

    
List<Sub> lsub = new ArrayList<>();
List<Base> lbase = lsub;

最后一行代码成立吗?编译会通过吗?

答案是否定的。

编译器不会让它通过的。Sub 是 Base 的子类,不代表 List和 List有继承关系。

但是,在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。

所以,通配符的出现是为了指定泛型中的类型范围。

通配符有 3 种形式。

<?>被称作无限定的通配符。

<? extends T>被称作有上限的通配符。

<? super T>被称作有下限的通配符。

无限定通配符 <?>

无限定通配符经常与容器类配合使用,它其中的 ? 其实代表的是未知类型,所以涉及到 ? 时的操作,一定与具体类型无关。

public void testWildCards(Collection<?> collection){
}

上面的代码中,方法内的参数是被无限定通配符修饰的 Collection 对象,它隐略地表达了一个意图或者可以说是限定,那就是 testWidlCards() 这个方法内部无需关注 Collection 中的真实类型,因为它是未知的。

所以,你只能调用 Collection 中与类型无关的方法。

当 <?>存在时,Collection 对象丧失了 add() 方法的功能,编译器不通过。

List<?> wildlist = new ArrayList<String>();
wildlist.add(123);// 编译不通过
提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空

<? extends T>

<?>代表着类型未知,但是我们的确需要对于类型的描述再精确一点,我们希望在一个范围内确定类别,比如类型 A 及 类型 A 的子类都可以。

<? extends T> 代表类型 T 及 T 的子类
public void testSub(Collection<? extends Base> para){ } 

上面代码中,para 这个 Collection 接受 Base 及 Base 的子类的类型。 但是,它仍然丧失了写操作的能力。也就是说

para.add(new Sub()); 
para.add(new Base()); 

仍然编译不通过。 没有关系,我们不知道具体类型,但是我们至少清楚了类型的范围。

<? super T> 这个和 <? extends T>  相对应,代表 T 及 T 的超类。
public void testSuper(Collection<? super Sub> para){ } 
 <? super T>. 神奇的地方在于,它拥有一定程度的写操作的能力。
public void testSuper(Collection<? super Sub> para){ 
      para.add(new Sub());//编译通过 
      para.add(new Base());//编译不通过 
} 

通配符与类型参数的区别

一般而言,通配符能干的事情都可以用类型参数替换。 比如

public void testWildCards(Collection<?> collection){} 

可以被

public <T> void test(Collection<T> collection){}

取代

值得注意的是,如果用泛型方法来取代通配符,那么上面代码中 collection 是能够进行写操作的。只不过要进行强制转换。

public <T> void test(Collection<T> collection){
  collection.add((T)new Integer(12));
  collection.add((T)"123");
}

需要特别注意的是,类型参数适用于参数之间的类别依赖关系,举例说明。

public class Test2 <T,E extends T>{
  T value1;
  E value2;
}
public <D,S extends D> void test(D d,S s){
​
  }

E 类型是 T 类型的子类,显然这种情况类型参数更适合。
有一种情况是,通配符和类型参数一起使用。

public <T> void test(T t,Collection<? extends T> collection){
  
}

如果一个方法的返回类型依赖于参数的类型,那么通配符也无能为力。

public <T> T test1(T t){
  return value1;
}

类型擦除

泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。

这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。

通俗地讲,泛型类和普通类在 java 虚拟机内是没有什么特别的地方。回顾文章开始时的那段代码

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
    
System.out.println(l1.getClass() == l2.getClass());

打印的结果为 true 是因为 List和 List在 jvm 中的 Class 都是 List.class。

泛型信息被擦除了。

可能同学会问,那么类型 String 和 Integer 怎么办?

答案是泛型转译。

public class Erasure <T>{
  T object;
​
  public Erasure(T object) {
    this.object = object;
  }
}

Erasure 是一个泛型类,我们查看它在运行时的状态信息可以通过反射。

Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());

结果:

erasure class is:FinalizeEscapeGC$Erasure

Class 的类型仍然是 Erasure 并不是 Erasure这种形式,那我们再看看泛型类中 T 的类型在 jvm 中是什么具体类型。

        Erasure<String> erasure = new Erasure<String>("hello");
        Class eclz = erasure.getClass();
        Field[] fs = eclz.getDeclaredFields();
        for ( Field f:fs) {
            System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
        }

结果:Field name object type:java.lang.Object

那我们可不可以说,泛型类被类型擦除后,相应的类型就被替换成 Object 类型呢?

这种说法,不完全正确。

我们更改一下代码。

public class Erasure <T extends String>{
//  public class Erasure <T>{
  T object;
​
  public Erasure(T object) {
    this.object = object;
  }
}

结果:Field name object type:java.lang.String

我们现在可以下结论了,在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,

则会被转译成普通的 Object 类型,如果指定了上限如 则类型参数就被替换成类型上限。

所以,在反射中

public class Erasure <T>{
  T object;
​
  public Erasure(T object) {
    this.object = object;
  }
  
  public void add(T object){
    
  }
}

add() 这个方法对应的 Method 的签名应该是 Object.class。

Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());
​
Method[] methods = eclz.getDeclaredMethods();
for ( Method m:methods ){
  System.out.println(" method:"+m.toString());
}

打印结果是

method:public void FinalizeEscapeGC$Erasure.add(java.lang.Object)

也就是说,如果你要在反射中找到 add 对应的 Method,你应该调用 getDeclaredMethod("add",Object.class)否则程序会报错,提示没有这么一个方法,原因就是类型擦除的时候,T 被替换成 Object 类型了。类型擦除带来的局限性

类型擦除带来的局限性

类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。

理解类型擦除有利于我们绕过开发当中可能遇到的雷区,同样理解类型擦除也能让我们绕过泛型本身的一些限制。比如
在这里插入图片描述

正常情况下,因为泛型的限制,编译器不让最后一行代码编译通过,因为类似不匹配,但是,基于对类型擦除的了解,利用反射,我们可以绕过这个限制

public interface List<E> extends Collection<E>{
  
   boolean add(E e);
}

上面是 List 和其中的 add() 方法的源码定义。

因为 E 代表任意的类型,所以类型擦除时,add 方法其实等同于

boolean add(Object obj);

那么,利用反射,我们绕过编译器去调用 add 方法。

public class ToolTest {
​
​
  public static void main(String[] args) {
    List<Integer> ls = new ArrayList<>();
    ls.add(23);
//    ls.add("text");
    try {
      Method method = ls.getClass().getDeclaredMethod("add",Object.class);
      
      
      method.invoke(ls,"test");
      method.invoke(ls,42.9f);
    } catch (NoSuchMethodException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (SecurityException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (IllegalAccessException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (IllegalArgumentException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (InvocationTargetException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    
    for ( Object o: ls){
      System.out.println(o);
    }
  }
}

结果:

23 
test
42.9

可以看到,利用类型擦除的原理,用反射的手段就绕过了正常开发中编译器不允许的操作限制。

泛型中值得注意的地方

泛型类或者泛型方法中,不接受 8 种基本数据类型。

需要使用它们对应的包装类。

List<Integer> li = new ArrayList<>();
List<Boolean> li1 = new ArrayList<>();
这篇关于Java-泛型擦除的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!