那些年我们在Java泛型上躺过的枪---万恶的泛型擦除【享学Java】(上)

简介: 那些年我们在Java泛型上躺过的枪---万恶的泛型擦除【享学Java】(上)

前言


泛型(Generics),从字面的意思理解就是泛化的类型,即参数化类型。

我们都知道,泛型是JDK5提供的一个非常重要的新特性,它有非常多优秀的品质:能够把很多问题从运行期提前到编译器,从而使得程序更加的健壮。


但是因为Java5要保持良好的向下兼容性,所以从推出之际一直到现在,它都是个假东西:只在编译期存在,编译成.class文件后就不存在了,这就是所谓的泛型擦除。


C++里的泛型是真实的,它通过类模版的概念去实现。


初识泛型

泛型(generics),从字面的意思理解就是泛化的类型,即参数化类型。


请注意参数化类型和方法参数类型的区别~


泛型类

对比下面两个类,一个是普通类,一个是泛型类:

class Generics {
    Object k;
    Object v;
    public Generics(Object k, Object v) {
        this.k = k;
        this.v = v;
    }
}
class Generics<K, V> {
    K k;
    V v;
    public Generics(K k, V v) {
        this.k = k;
        this.v = v;
    }
}


泛型类的声明一般放在类名之后,可以有多个泛型参数,用尖括号括起来形成类型参数列表。


泛型接口
public interface Generator<T> {
    public T next();
}


这种泛型接口设计,是声明的是一个工厂设计模式常用的生成器接口。比如我们常见的迭代器接口Iterable就是这样一个接口


public interface Iterable<T> {
    Iterator<T> iterator();
}


泛型方法


分为实例泛型方法和静态泛型方法


    public <T> T genericMethod(T t){
        return t;
    }
  // 注意静态方法<T>必须是在static的后面~
    public static <T> T genericStaticMethod(T t){
        return t;
    }


这里需要稍微注意一下:


public class Main<T> {
    // 静态方法不能直接使用类的泛型参数T  而需要自己声明的
    // 形如这样书写才正确:public static <T> T genericStaticMethod(T t) { ... }
    public static T genericStaticMethod(T t) {
        return t;
    }
    // 实例方法可以直接使用类声明的泛型参数
    public T genericMethod(T t) {
        return t;
    }
}


静态方法使用泛型参数需要自己单独声明,否则编译报错。


泛型方法的声明和泛型类的声明略有不同,它是在返回类型之前用尖括号列出类型参数列表(也可以有多个泛型类型),而函数传入的形参类型可以利用泛型来表示。


泛型的局限性

总结出如下几种情况,使用泛型的时候务必注意:


  1. 基本类型无法作为类型参数。如ArrayList<int>这样是非法的,而只能ArrayList<Integer>1. 请注意:数组表示中int[]和Integer[]都是可以的
  2. 在泛型代码内部,无法获得任何有关泛型参数类型的信息。比如你传入的泛型参数为T,而在方法内部你无法使用T的任何方法,毕竟编译期它的类型还不确定1. 对此句话我做个备注:并不是什么方法都不能使。如果你的泛型<T extends BaseDBEntity>的话,你是可以使用BaseDBEntity方法的,但如果你没有任何继承,那你只能使用Object的方法(毕竟任何类型都是它的子类嘛~)
  3. 在能够使用泛型方法的时候,尽量避免使整个类泛化。粗细粒度需要控制好~1. 当你大部分方法都用了泛型的时候,建议将类泛化


泛型擦除


前面指出了,Java的泛型是假的,它是编译期的。下面通过一个示例证明这一点:


public class Main {
    public static void main(String[] args) {
        List<String> c1 = new ArrayList<>();
        List<Integer> c2 = new ArrayList<>();
        Class<? extends List> class1 = c1.getClass();
        Class<? extends List> class2 = c2.getClass();
        System.out.println(class1 == class2);
    }
}

输出结果为:true

其实这里可能有不少小伙伴不能理解了,泛型中明明给它传的类型是不一样的,为何Class还是一样的呢???这里就要说到Java语言实现泛型所独有的——泛型擦除。


本例说明了:当我们声明List<String>和List<Integer>时,在运行时实际上是相同的,都是List,而具体的类型参数信息String和Integer被擦除了。(具体有兴趣的小伙伴可以去查看它的.class文件内容(可反编译后查看对比))


由上可知,下面结果是:false


    public static void main(String[] args) {
        List<String> c1 = new ArrayList<>();
        List<Integer> c2 = new LinkedList<>();
        Class<? extends List> class1 = c1.getClass();
        Class<? extends List> class2 = c2.getClass();
    // 一个是ArrayList  一个是LinkedList 所以返回false
        System.out.println(class1 == class2); // false
    }


为什么Java要擦除泛型?


从上例可以知道,java的泛型擦除确实给实际编程中带来了一些不便(特别是运行时反射时,有时候无法判断出真实类型)。那Java的设计者为什么要这么干呢?


这是一个历史问题,Java在版本1.0(1.5之前)中是不支持泛型的,这就导致了很大一批原有类库是在不支持泛型的Java版本上创建的。而到后来Java逐渐加入了泛型,为了使得原有的非泛化类库能够在泛化的客户端使用,Java开发者使用了擦除进行了折中(保持向下兼容)。


所以Java使用这么具有局限性的泛型实现方法就是从非泛化代码到泛化代码的一个过渡,以及不破坏原有类库的情况下,将泛型融入Java语言。


Java泛型擦除是擦除所有吗?


泛型擦除其实是分情况擦除的,不是完全擦除,一定要消除这个误区。

Java 在编译时会在字节码里指令集之外的地方保留部分泛型信息,泛型接口、类、方法定义上的所有泛型、成员变量声明处的泛型都会被保留类型信息,其他地方的泛型信息都会被擦除。


这就是为什么上面这些地方的泛型,你在运行期还是可以通过反射获取的原因(如果是真的全部擦除了,运行期就找不到了)

当然喽,你也可以理解是全部擦除了–> 只是找了另外一个地方存放而已~


泛型通配符


<? super T><? extends T><?>

关于通配符的使用,本文略~



泛型使用示例(坑)


使用集合框架时请务必明确泛型


如下使用案例,存在非常大的风险

public static void main(String[] args) {
    List list = new ArrayList();
    list.add("1");
    list.add(2);
    List<String> list2 = list; //不报错 因为类型一样都是List类型
    System.out.println(list2); //[1, 2] 这样是不抱错的哦
    //报错 类型转换异常   这时候里面的元素一碰就报错
    list2.forEach(x -> System.out.println(x));
}


如上由于第一个List没有加上泛型,使得它里面既装了String,又装了Integer,所以最后只要一get出来就报错了(迭代也不行)。


这个也是通过反射完成一些封装的框架,比如MyBatis、Redis序列化值、SpringMVC等处理入参的值普遍会遇到但是无法解决的问题


泛型类型的可变参数作为入参的坑


如下示例:

public class Main {
    public static void main(String[] args) {
        Integer[] ints1 = new Integer[]{1, 2, 3};
        int[] ints2 = new int[]{1, 2, 3};
        // 注意下面两种输出结果是不一样的
        doSomething(ints1); //输出1,2,3
        doSomething(ints2); //输出[I@1f32e575
    }
    // 静态泛型方法  需要自己申明泛型T
    // 静态泛型方法  需要自己申明泛型T
    private static <T> void doSomething(T... values) {
        //boolean b = values instanceof Object; // 编译不抱错
        //boolean b = values instanceof List; // 编译报错:不能转换为List类型
        for (T value : values) {
            System.out.println(value);
        }
    }
}


这个结果是由于int[]这个数组最终就被当作一个数值作为入参了。

通过此例可以总结出如下两点:


  1. 泛型的类型参数只能是类类型,不能是简单类型
  2. 不能对不确切的泛型类型使用instanceof操作(如上例子泛型类型若没指定上限,都是Object的子类而已)





相关文章
|
8天前
|
JavaScript Java 编译器
Java包装类和泛型的知识点详解
Java包装类和泛型的知识点的深度理解
|
29天前
|
Java
java中的泛型类型擦除
java中的泛型类型擦除
13 2
|
1月前
|
存储 Java fastjson
Java泛型-4(类型擦除后如何获取泛型参数)
Java泛型-4(类型擦除后如何获取泛型参数)
33 1
|
7天前
|
存储 监控 安全
泛型魔法:解码Java中的类型参数
泛型魔法:解码Java中的类型参数
26 0
泛型魔法:解码Java中的类型参数
|
9天前
|
Java API
Java基础—笔记—内部类、枚举、泛型篇
本文介绍了Java编程中的内部类、枚举和泛型概念。匿名内部类用于简化类的创建,常作为方法参数,其原理是生成一个隐含的子类。枚举用于表示有限的固定数量的值,常用于系统配置或switch语句中。泛型则用来在编译时增强类型安全性,接收特定数据类型,包括泛型类、泛型接口和泛型方法。
9 0
|
1月前
|
存储 安全 Java
JAVA泛型
JAVA泛型
11 0
|
1月前
|
Java 编译器
[java进阶]——泛型类、泛型方法、泛型接口、泛型的通配符
[java进阶]——泛型类、泛型方法、泛型接口、泛型的通配符
|
1月前
|
存储 Java 编译器
Java——泛型
Java——泛型
15 0
|
1月前
|
存储 安全 Java
JAVA的泛型
JAVA的泛型
9 0
|
4月前
|
Java Go
【Java 泛型方法】泛型方法的定义和使用,泛型的通配符和泛型的上下限,泛型的注意事项
【Java 泛型方法】泛型方法的定义和使用,泛型的通配符和泛型的上下限,泛型的注意事项