精华内容
下载资源
问答
  • JAVA泛型擦除机制

    千次阅读 2015-09-27 17:03:22
    Java 泛型擦除机制
    public class TestF {
    
        public static void main(String[] args) {
            Class a1 = new ArrayList<Integer>().getClass();
            Class a2 = new ArrayList<String>().getClass();
            System.out.println(a1 == a2);
        }
    
    }

    如果仅凭直觉来看,该程序可能会输出 false 这个结果,但在具体的环境下运行时会输出 true 。这很令人费解,这是为什么呢?

    Java泛型是使用类型擦除来实现的。这表示在运行过程中任何和类型有关的信息都会被擦除,所有在运行中 ArrayList和ArrayList的具体信息都被擦除成它们的原生类型即ArrayList类型。

    由于擦除机制,泛型不能用于显示地引用运行时类型的操作之中,例如转型、new表达式和instanceof操作。

    若在泛型内部必须使用类型操作时,可以在运行时采用反射的方法将正在运行的类信息添加到泛型内部,这种方法称为补偿。

    public class Dog {
    
        public Dog() {
            System.out.println("Dog...");
        }
    }
    
    public class TestF2<T> {
        Class<T> type;
        public TestF2(Class<T> type) {
            this.type = type;
        }
    
        public boolean check(Object obj) {
            return type.isInstance(obj); //isInstance 和instanceof动态等价 
        }
    
        public void show(Class<T> type) {   //使用反射动态实现new表达式
            try {
                type.newInstance();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) {
            TestF2<Dog> t = new TestF2<Dog>(Dog.class);
            System.out.println(t.check(new Dog()));
            t.show(Dog.class);
        }
    
    }

    输出为:
    Dog…
    true
    Dog…

    展开全文
  • 不同语言对泛型的支持也不一样,Java 中的泛型类型在编译期会擦除,下面一个例子可以证明这一点: public static void main(String[] args) throws Exception { List<String> list = new ArrayList<>...

    我们都知道 Java 中的泛型可以在编译期对类型检查,避免类型强制转化带来的问题,保证代码的健壮性。不同语言对泛型的支持也不一样,Java 中的泛型类型在编译期会擦除,下面一个例子可以证明这一点:

    	public static void main(String[] args) throws Exception {
            List<String> list = new ArrayList<>();
            list.add("abc");
            Class<? extends List> listClass = list.getClass();
            Method addMethod = listClass.getMethod("add", Object.class);
            addMethod.invoke(list, 10);
            System.out.println(list.size());
        }
    

    上面这个例子中定义了一个 String 类型的集合,通过反射获取 add 方法并调用该方法添加了一个 int 类型的元素,代码执行过程中并不会出现异常,而且能正常输出 size = 2。

    因为泛型会被擦除,所以很多人都认为在代码运行期间是无法得知泛型参数类型的。最近重温 Java 基础语法的时候,Java Reflection - Generics 在文章开头认为这种结论不是很准确,因为通过反射可以获取到泛型的具体类型,就像下面这样:

    public class GenericsSamples {
        public static List<String> getStringList() {
            return new ArrayList<>();
        }
    
        public static void main(String[] args) throws NoSuchMethodException {
    
            Method method = GenericsSamples.class.getMethod("getStringList", null);
            Type returnType = method.getGenericReturnType();
            if (returnType instanceof ParameterizedType) {
                ParameterizedType type = (ParameterizedType) returnType;
                Type[] typeArguments = type.getActualTypeArguments();
                for (Type typeArgument : typeArguments) {
                    Class<?> typeArgClass = (Class<?>) typeArgument;
                    System.out.println("typeArgClass = " + typeArgClass);
                }
            }
        }
    }
    

    typeArgClass 输出的类型为 class java.lang.String,为什么编译期间被擦除的泛型,在代码运行期间能获取到呢?原因是为了让虚拟机解析泛型类型,在虚拟机规范里引入了 LocalVariableTypeTable 属性,我们可以通过 javap -v class 反编译方式查看。

    public class GenericsDemo {
        public static void main(String[] args) {
    
            List<String> stringList = new ArrayList<>();
            List<Integer> integerList = new ArrayList<>();
            System.out.println(stringList.getClass().equals(integerList.getClass()));
        }
    }
    

    反编译上面 GenericsDemo 的 class 文件,main 方法编译信息如下:

    在这里插入图片描述

    从上面的编译信息可以看出属性 LocalVariableTypeTableSignature 中保存了泛型的类型信息,因此我们可以通过反射在运行阶段获取到它。到这里可能会有人有个疑问:泛型不是被擦除了吗,为啥还能保存到 Signature 中,原因是它和我们所说的泛型擦除是两码事,下面再来看一个例子:

    public class GenericSamples<T> {
    
        private T param;
    
        public T getParam() { return param; }
    
        public void setParam(T param) { this.param = param; }
    
        public static void main(String[] args) {
            GenericSamples<Integer> integerGenericSamples = new GenericSamples<>();
            integerGenericSamples.setParam(10);
            System.out.println(integerGenericSamples.getParam());
        }
    }
    

    下面是 get 与 set 方法反编译的结果:

    在这里插入图片描述
    set 与 get 方法通过反编译后并不会保留类型信息,统一处理成了 Object,这个才是我们通常所说的泛型擦除,由于被编译器当作 Object 类型处理,那么我们就可以通过反射 set 任意类型的参数。

    LocalVariableTypeTable 类似的还有一个 LocalVariableTable,不同的地方在于前者的 Signature 相较于后者的 descriptor 保存了泛型信息,关于这两个属性更多的介绍可以查看官方的介绍:4.7.13. The LocalVariableTable Attribute

    参考:

    Java Reflection - Generics
    深入探索Java泛型的本质 | 泛型

    展开全文
  • 泛型擦除机制: 伪泛型机制就是说:在编译期间把泛型的信息全部擦除掉了,所以的泛型最终都变成了最原始的类型(Object);在运行期就不存在泛型的信息。 编译器怎么擦除泛型的? 检查泛型的类型,获取目标泛型 ...

    JAVA 泛型机制原理,泛型擦除机制

    • 泛型的原理:
      Java泛型是jdk5引入的一种机制。为了向下兼容,所以Java虚拟机是不支持泛型的,也就是说Java泛型是一种伪泛型机制。

    • 泛型的擦除机制:
      伪泛型机制就是说:在编译期间把泛型的信息全部擦除掉了,所以的泛型最终都变成了最原始的类型(Object);在运行期就不存在泛型的信息。

    • 编译器怎么擦除泛型的?

      1. 检查泛型的类型,获取目标泛型
      2. 擦除类型变量,并替换为限定类型
        a. 如果没有指定类型变量(),则用Object为原始类型
        b.如果限定类型变量(),则用A作为原始类型
        c.如果有多个限定类型(<T extends A & B & C >),则用第一个边界A作为原始类型
      3. 必要时插入类型转换以保持类型安全
      4. 生成桥方法用在扩展的时候保持多态性
    • 泛型的副作用?

      1. 使用泛型后,不能使用基本数据类型(byte,short,int ,long,float,double,boolean,char);
        a. 原因:因为擦除后变成Object,而Object无法存放int类型
      2. 不能使用 instanceof运算符
        a. 原因:因为擦除后只剩下原始类型,泛型信息不存在。
      3. 泛型在静态方法和静态类使用问题
        a. 泛型静态参数创建时候报错:泛型的静态参数在创建的时候就要确定参数
        b.泛型静态方法创建成功:方法在调用的时候在确定泛型类型
      4. 泛型类型中方法冲突
        a.比如equals方法:泛型擦除后变成了Object,object方法有equals方法,相当于重新定义equals方法

    题外话:谢谢大家观看,有不足之处欢迎大家一起讨论;码字不易,大家喜欢,麻烦点赞哦。


    灵魂三问:

    • 有没有觉得技术得不到系统的提升,技术成长慢?
    • 有没面试懵逼,升职加薪难?
    • 有没有想过去大一点的世界看看?

    有期望JAVA技术巩固的、Android知识进阶的、期望升职加薪的、Android面试技巧的大厂面试真题的;大家可以加我QQ哦:1070800492。我们一起学习,一起进步!

    重要的事情说三遍:

    • 学习、挣钱、自由
    • 学习、挣钱、自由
    • 学习、挣钱、自由

    疫情当下,唯有自强

    展开全文
  • 本文主要介绍Java泛型的一些语法,重点解释Java泛型擦除机制,以及擦除所带来的一些问题

    (本文主要帮助应用Java泛型,并不太深入,仅根据本人在阅读Java编程思想泛型这一章节遇到的问题给出一些解释,JAVA小菜,错误难免)
    一、泛型类:
    泛型类只需要在声明类名后面加即可,其中T可换成其他任意字符。如:class Test(){}

    二、泛型接口:
    声明形式与使用方法与泛型类相同。

    三、泛型方法:
    声明泛型方法,只需将泛型参数列表置于返回值之前即可,例如:public void f(T x){}。
    这里需要注意的是,泛型方法的返回值不能直接作为参数传递给另一个方法,例如有方法A,其声明为public int A(){}。另一泛型方法B,其声明为public void b(T x){}。那么不可以直接b(A())。这主要是因为泛型的擦除机制所引起的

    四、泛型擦除机制(本文的主要知识点。前三点可能看完会有点疑惑,这将在第4点给出答案。)
    1、问题引入,一个让人迷惑的泛型程序代码

    public class ErasedTypeEquivalence
    {
            public static void main(String[] args)
    {
            Class c1=new ArrayList<String>.getClass();
            Class c2=new ArrayList<Integer>.getClass();
            System.out.println(c1==c2);
    }
    }/*这段代码的输出结果为true*/

    这里让人疑惑的是ArrayList和ArrayList很容易让人认为是不同的类型。而实际上他们是相同的类型。原因就是下面要讲的Java泛型的擦除机制。

    2、擦除机制带来的问题:
    在Java中,无法在泛型代码内部获得任何有关泛型参数类型的信息(不过你可以获得它的类名)。

    import java.util.*;
    class Frob{}
    class Fnorkle{}
    class Quark<Q>{}
    class Particle<POSITION,MOMENTUM>{}
    public class Fanxing 
    {
        public static void main(String[] args) 
        {
            List<Frob> list=new ArrayList<Frob>();
            Map<Frob, Fnorkle> map=new HashMap<Frob, Fnorkle>();
            Quark<Fnorkle> quark=new Quark<Fnorkle>();
            Particle<Long, Double> p=new Particle<Long, Double>();
    
            System.out.println(Arrays.toString(list.getClass()  .getTypeParameters()));
            System.out.println(Arrays.toString(map.getClass(). getTypeParameters()));   
            System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));     
            System.out.println(Arrays.toString(p.getClass().getTypeParameters()));
        }
    }/*输出结果:
    [E]
    [K, V]
    [Q]
    [POSITION, MOMENTUM]*/

    这里输出的只是泛型在定义时候的那个字符,而没有输出泛型参数类型。

    3、extends关键字。
    问题引入,先看代码:

    class HasF()
    {
        public void f()
        {
           system.out.println() 
        }
    }
    
    class Manipulator<T>
    {
        private T obj;
        public Manipulator(T x){    Obj=x;  }
        public void manipulate(){   obj.f() ;}
    }
    
    public class Manipulation
    {
        public static void main(String[] args)
        {
            HasF hf=new HasF();
            Manipulator<HasF> manipulator=new Manipulator<HasF>(hf);
            manipulator.manipulate();
        }
    }
    /*这段代码将会产生错误,因为在类Manipulator 的manipulate 方法中,有obj.f();。如果在C++中,这段代码自然是不会报错,只要传入HasF即可(这被称为潜在类型机制),然而Java中有擦除机制,obj将被擦除为“Object”类型的,那么obj去调用f()自然是要报错。解决的办法就是使用extends关键字(当然,利用Java反射机制解决也可以,不过这里主要采用泛型的方法)。只要这么去定义Manipulator,这段程序便不会报错:class Manipulator<T extends HasF>。
    */

    4、擦除机制的内部实现:
    Java泛型擦除,分两个时间阶段:其一是编译期,也就是在你敲代码的时候;其二是编译后运行期,也就是你运行代码的时候。在编译期,编译器是可以知道泛型参数信息的(前面说没有办法获得泛型参数类型信息,这是因为获得泛型参数类型信息是需要函数的,函数只能在运行期才起作用),所以这段时间,Java的泛型就有点像C++的了,当然,只是有点。因为像下面这样的代码,Java无法通过编译,而C++可以(在3里说过C++有潜在类型机制,而Java没有)。如下:

    class HasF{ public void f(){} }
        class Test<T>
    {
        private T obj;
        public Test(T x){ obj=x; }
        public invokeF(){ obj.f(); }//这里报错,无法通过编译
    }
    class TestMain()
    {
        HasF hasF=new HasF();
        Test<HasF> test=new Test<HasF>(hasF);
        test.invokeF();
    }

    可能你会感到疑惑,既然编译器在编译期能够知道泛型参数的类型,那么理应向C++泛型那样给你编译通过才是,但为何这里不给编译通过,而是给你报了个编译错误?原因就是在运行期,将会发生泛型的擦除。发生擦除之后,上面的Test类代码将变成下面这样:

    class Test
    {
        private Object obj;
        public Test(Object x){ obj=x; }
        public invokeF(){ obj.f(); }    //这里必然报错
    }

    于是聪明的编译器在编译期就给你报了错,使你立即去修改你的代码,而不是等到代码运行的时候。
    擦除机制一定就是把泛型参数擦除为Object吗?不是的。泛型擦除遵循下面这条规则:有限定的擦除为限定类型,无限定的擦除为Object。上述情形就是无限定的情形。接下来看看有限定的情形,同样把上面的Test类的T修改为T extends HasF。擦除之后Test类的代码变为:

        class Test
        {
            private HasF obj;
            public Test(HasF x){ obj=x; }
            public invokeF(){ obj.f(); }    //这里obj是HasF类型,所以此时这里便正确了
        }

    最后再扩展一下,泛型多重限定的擦除,先在增加一个类:
    class HasG{ public g(){} }
    然后把Test类修改为多重限定,即: