资讯专栏INFORMATION COLUMN

Java泛型和类型擦除

el09xccxy / 2639人阅读

摘要:对每个泛型类只生成唯一的一份目标代码该泛型类的所有实例都映射到这份目标代码上,在需要的时候执行类型检查和类型转换。参考文章的模板是典型的实现,而泛型则是实现,将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除实现的。

一 前言:初识泛型

废话不说,先来看一段代码:

public class Holder {
    private Object data;
    
    public Holder(Object data ){
        this.data = data;
    }

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

    public Object getData() {
        return data;
    }
    
    public static void main(String[] args){
        Holder holder = new Holder(new SomeNode());
        SomeNode someNode = holder.getData();
        
    }
}

class SomeNode{}

Holder类是一个容器,它的作用是用来保存其他类的,这里我们用它来保存SomeNode类,随后把它取出来,编译运行,结果如下:

Error:(21, 43) java: incompatible types
  required: SomeNode
  found:    java.lang.Object

意思是,需要的是SomeNode,取出来的却是Object,如此看来,如果我想保存SomeNode类,就只能把data声明为SomeNode:

private SomeNode data;

这就意味着我们需要为每一个类创造一个Holder,这肯定是不行的,于是泛型的作用来了,泛型,可以理解为任何类型,意思是我可以声明一个可以容纳任何类型的容器:

public class Holder {
    private T data;

    public Holder(T data ){
        this.data = data;
    }

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

    public T getData() {
        return data;
    }

    public static void main(String[] args){
        Holder holder = new Holder(new SomeNode());
        SomeNode someNode = holder.getData();

    }
}

class SomeNode{}

注意写法,在类声明后面加个就行了,你也可以加,只是一个占位符,形参而已。然后我们再把它取出来:

Process finished with exit code 0

程序没有报错,如果这时候我们使用holder的set()方法去插入设置一些非SomeNode类型的值,代码如下:

 public static void main(String[] args){
        Holder holder = new Holder(new SomeNode());
        SomeNode someNode = holder.getData();
        holder.setData("AAAA");

    }

看结果:

Error:(22, 15) java: method setData in class Holder cannot be applied to given types;
  required: SomeNode
  found: java.lang.String
  reason: actual argument java.lang.String cannot be converted to SomeNode by method invocation conversion

泛型机制就自动为我们报错,很方便。

二 泛型类:元组(Tuple),返回多个对象

熟悉python的同学都知道元组的概念,它是一个只读列表,在返回多个结果时是很有用的,我们利用泛型特性来创造一个包含两个对象的元组:

public class Tuple {
    public static void main(String[] args){
        TwoTuple t = new TwoTuple("Monkey",12);
        System.out.println(t.toString());
    }
}

class TwoTuple{
    final A first;
    final B second;

    public TwoTuple(A a,B b){
        first = a;
        second = b;
    }

    public String toString(){
        return "("+first+","+second+")";
    }
}

来看结果:

(Monkey,12)

是不是很方便:)如果想要一个长度为3的元组可以这么写:

public class Tuple {
    public static void main(String[] args){
        ThreeTuple t = new ThreeTuple("Dog",12,true);
        System.out.println(t.toString());
    }
}

class TwoTuple{
    final A first;
    final B second;

    public TwoTuple(A a,B b){
        first = a;
        second = b;
    }

    public String toString(){
        return "("+first+","+second+")";
    }
}

class ThreeTuple extends TwoTuple{
    final C three;

    public ThreeTuple(A a,B b,C c){
        super(a,b);
        three = c;
    }

    public String toString(){
        return "("+first+","+second+","+three+")";
    }
}

结果如下:

(Dog,12,true)
三 泛型接口

泛型接口的定义和泛型类的定义类似,我们来定义一个生成器接口:

public interface Generator {
    T next();
}

接着我们实现这个接口,来生成斐波拉契数:

public class Fib implements Generator {
    private int count = 0;

    @Override
    public Integer next() {
        return fib(count++);
    }

    private int fib(int n){
        if (n<2)
            return 1;
        else
            return fib(n-2) + fib(n-1);
    }

    public static void main(String[] args){
        Fib f = new Fib();
        for (int i=0;i<100;i++){
            System.out.println(f.next());
        }
    }

}
四 泛型方法

比起泛型类,我们更推荐去使用泛型方法,泛型方法定义起来也很简单,我们只需将泛型参数放在返回类型前面即可:

public class GenericMethods {
    public  void f(T x){
        System.out.println(x.getClass().getName());
    }

    public static void main(String[] args){
        GenericMethods g = new GenericMethods();
        g.f("Hello");
        g.f(100);
        g.f(true);
    }
}

这里我们定义了一个泛型方法f(),并使用getClass获取类的相关信息(关于Class对象的知识点这里),来看结果:

java.lang.String
java.lang.Integer
java.lang.Boolean

这里还要注意一下Varargs(变长参数)机制和泛型的结合:

public class GenericVarargs {
    public static  List makeList(T...args){
        List list = new ArrayList();
        for (T item : args){
            list.add(item);
        }

        return list;
    }

    public static void main(String[] args){
        List list = makeList("A","B","C","D");
        System.out.println(list);
    }
}

结果如下:

[A, B, C, D]
六 类型擦除

在认识类型擦除之前,我们首先要明白编译器对泛型的处理有两种方式:
1.Code specialization
在实例化一个泛型类或者泛型方法是都生成一份新的字节码,比如对于List,List,List产生三份不同的字节码。
2.Code sharing
对每个泛型类只生成唯一的一份目标代码;该泛型类的所有实例都映射到这份目标代码上,在需要的时候执行类型检查和类型转换。参考文章
C++的模板是典型的Code specialization实现,而Java泛型则是Code sharing实现,将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的。对擦除更通俗的理解就是:编译器生成的bytecode是不包涵泛型信息的。我们看下面的代码:

public class ErasedType {
    public static void main(String[] args){
        Class c1 = new ArrayList().getClass();
        Class c2 = new ArrayList().getClass();

        System.out.println(c1 == c2);
    }
}

结果如下:

true

也就是说我们在实例化ArrayList和实例化ArrayList时是共享一份目标代码的,泛型类类型信息在编译的过程中被擦除了。对于JVM来说,它只看到一份ArrayList(原始类型)而已。我们还可以从反射的角度来理解类型擦除:

public class ErasedType {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        List list = new ArrayList();
        list.add("ABC");
        list.getClass().getMethod("add",Object.class).invoke(list,123);
        System.out.println(list);
    }

}

看结果:

[ABC, 123]

我们很顺利的把Integer型的123插入到了String的List里:)

七 后记

由于类型擦除的存在,我们往往会在使用泛型特性的时候遇到一些诡异的问题,由于篇幅原因,这里不展开了:)我将在另外一篇文章中集中的总结一下这方面的问题。

我的微信号是aristark,欢迎交流指正!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/65687.html

相关文章

  • Java 泛型总结(二):泛型与数组

    摘要:总结数组与泛型的关系还是有点复杂的,中不允许直接创建泛型数组。本文分析了其中原因并且总结了一些创建泛型数组的方式。 简介 上一篇文章介绍了泛型的基本用法以及类型擦除的问题,现在来看看泛型和数组的关系。数组相比于Java 类库中的容器类是比较特殊的,主要体现在三个方面: 数组创建后大小便固定,但效率更高 数组能追踪它内部保存的元素的具体类型,插入的元素类型会在编译期得到检查 数组可以持...

    Vultr 评论0 收藏0
  • Java语法糖的编译结果分析(一)

    摘要:操作对应字节码中的个字节我们可以看到最关键的操作其实就是调用的其实是类的方法,此方法的入参类型是,返回值类型是,翻译过来就是类的方法,执行完后将获得的结果做了,检查返回的对象类型是否是。 语法糖(Syntactic Sugar)的出现是为了降低我们编写某些代码时陷入的重复或繁琐,这使得我们使用语法糖后可以写出简明而优雅的代码。在Java中不加工的语法糖代码运行时可不会被虚拟机接受,因此...

    zhangxiangliang 评论0 收藏0
  • Java 泛型总结(一):基本用法与类型擦除

    摘要:然而中的泛型使用了类型擦除,所以只是伪泛型。总结本文介绍了泛型的使用,以及类型擦除相关的问题。一般情况下泛型的使用比较简单,但是某些情况下,尤其是自己编写使用泛型的类或者方法时要注意类型擦除的问题。 简介 Java 在 1.5 引入了泛型机制,泛型本质是参数化类型,也就是说变量的类型是一个参数,在使用时再指定为具体类型。泛型可以用于类、接口、方法,通过使用泛型可以使代码更简单、安全。然...

    Java_oldboy 评论0 收藏0
  • Java系列之泛型

    摘要:总结泛型的类型必须是引用类型,不能是基本类型,泛型的个数可以有多个,可以使用对创建对象时的泛型类型以及方法参数类型进行限制,如使用关键字和对泛型的具体类型进行向下限制或向上限制,最后一点,可以声明泛型数组,但是不能创建泛型数组的实例。 自从 JDK 1.5 提供了泛型概念,泛型使得开发者可以定义较为安全的类型,不至于强制类型转化时出现类型转化异常,在没有反省之前,可以通过 Object...

    MadPecker 评论0 收藏0

发表评论

0条评论

el09xccxy

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<