前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >那些年我们在Java泛型上躺过的枪---万恶的泛型擦除【享学Java】

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

作者头像
YourBatman
发布2019-09-03 16:05:54
9250
发布2019-09-03 16:05:54
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦
前言

泛型(Generics),从字面的意思理解就是泛化的类型,即参数化类型。 我们都知道,泛型是JDK5提供的一个非常重要的新特性,它有非常多优秀的品质:能够把很多问题从运行期提前到编译器,从而使得程序更加的健壮。

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

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

初识泛型

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

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

泛型类

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

代码语言:javascript
复制
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;
    }
}

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

泛型接口
代码语言:javascript
复制
public interface Generator<T> {
    public T next();
}

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

代码语言:javascript
复制
public interface Iterable<T> {
    Iterator<T> iterator();
}
泛型方法

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

代码语言:javascript
复制
    public <T> T genericMethod(T t){
        return t;
    }

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

这里需要稍微注意一下:

代码语言:javascript
复制
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的任何方法(Object的方法除外),毕竟编译期它的类型还不确定
  3. 在能够使用泛型方法的时候,尽量避免使整个类泛化。粗细粒度需要控制好~
泛型擦除

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

代码语言:javascript
复制
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

代码语言:javascript
复制
    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语言。

泛型通配符

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

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

泛型使用示例(坑)

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

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

代码语言:javascript
复制
	 List list = new ArrayList();
     list.add("1");
     list.add(2);
     List<String> list2 = list; //不报错 因为类型一样都是List类型
	
	 //报错 类型转换异常   这时候里面的元素一碰就报错
     list2.forEach(x -> System.out.println(x)); 

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

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

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

如下示例:

代码语言:javascript
复制
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的子类而已)
附:关于Arrays.asList()使用陷阱、指南

Arrays.asList()在平时开发中还是比较常见的,我们可以使用它将一个数组转换为一个List集合。但是很多小伙伴对它有点滥用,它的使用还是存在一些坑的,这里借助泛型,稍微总结一下:

代码语言:javascript
复制
// @since  1.2
public class Arrays {
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }
    private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable {
    	...
    	// 来自父类  它自己并没有复写
	    public void add(int index, E element) {
	        throw new UnsupportedOperationException();
	    }
	    public E remove(int index) {
	        throw new UnsupportedOperationException();
	    }
	    ...
    }
}

这是asList()方法的一个申明,可以看到它接收的也是一个可变参数,这么看来它和我们上面定义的doSomething(T... values)方法何其相似,因此此处我就不再用代码示例了,总结出下面几条结论即可,具体缘由我建议小伙伴一定要做到心知肚明

  1. 传递的数组必须是对象数组,而不是基本类型。 1. 当传入一个源生数据类型数组时,asList真正得到的参数就不是数组的元素了,而是数组对象本身
  2. 使用集合的修改方法:add()、remove()、clear()会抛出异常(因为它本质上还是个数组,不可变

那么问题来了,如何正确的把数组Array转换为List呢???下面介绍几种常用方案: 方案一:自己for循环实现 代码略 方案二:最简便的方法(推荐)

代码语言:javascript
复制
List list = new ArrayList<>(Arrays.asList("a", "b", "c"))

方案三:使用 Java8 的Stream(推荐)

代码语言:javascript
复制
    public static void main(String[] args) {
        Integer[] myArray = {1, 2, 3};
        List<Integer> myList = Arrays.stream(myArray).collect(Collectors.toList());

        //基本类型也可以实现转换(依赖boxed的装箱操作) //boxed()是IntStream类的方法
        int[] myArray2 = {1, 2, 3};
        myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
        System.out.println(myList); //[1, 2, 3]
    }

方案四:使用 Apache Commons Collections

代码语言:javascript
复制
    public static void main(String[] args) {
        Integer[] myArray = {1, 2, 3};
        List<Integer> list = new ArrayList<>();
        CollectionUtils.addAll(list, myArray);
        System.out.println(list); // 
    }

它的addAll()是个重载方法,数组List都行。它内部其实就是把myArray遍历了一遍而已,因此int或者Integer数组都无所谓~

在这里插入图片描述
在这里插入图片描述

注意:java.util.Collections可以没有这个能力把Array转为List,但是它的singleton(T o)等方法也是非常好用的,但也是只读视图哦~

总结

Java的泛型一直是被吐槽的对象,它确实很渣不好用,容易出错。但是存在即合理,你不能改变它那请好好接受它。 编码的时候遵循一个原则:该写泛型的地方务必写上泛型,能使得你对泛型的理解更加的深刻。这是一个非常良好的编码习惯~

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019年06月29日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 初识泛型
  • 泛型的局限性
  • 泛型擦除
  • 泛型通配符
  • 泛型使用示例(坑)
    • 使用集合框架时请务必明确泛型
      • 泛型类型的可变参数作为入参的坑
        • 附:关于Arrays.asList()使用陷阱、指南
          • 总结
          相关产品与服务
          云数据库 Redis
          腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档