java核心基础之泛型

1,370 阅读6分钟

为什么要使用泛型

大家好,我是 jack xu,今天给大家讲的是 java 核心基础的第二篇,泛型,这个东西熟悉又陌生,熟悉是相信大家每天都在用,List< Long>这个就是泛型,陌生是泛型还有一些其他偏门冷门的知识,今天给大家查漏补缺一下。不喜勿喷,高手也可以略过,先看下面一段代码:

这段代码把 Integer 和 String 类型的数据都塞进去,然后打印出来,list 默认是可以存放任意类型的数据,代码是没毛病的,运行一下

报错了,Integer 不能转成 String 类型的,为了解决类似这样的问题,在编译阶段就可以解决,而不需要等到运行时才发现,泛型就应运而生了。我们只需要将第一行声明初始化 list 的代码改一下,加上一个< String>,编译器就曝出小红线了,从而让我们及时发现改正。

泛型的类型擦除

先来看一段代码

        List<String> stringArrayList = new ArrayList<>();
        List<Integer> integerArrayList = new ArrayList<>();

        Class classStringArrayList = stringArrayList.getClass();
        Class classIntegerArrayList = integerArrayList.getClass();

        System.out.println(classStringArrayList.equals(classIntegerArrayList));

运行一下,将 classStringArrayList 的类信息与 classIntegerArrayList 的类信息相比较,结果是 true,大家都很惊讶,啊,叹为观止,明明是两个类型的东西,怎么会这样呢。

因为泛型只在编译阶段有效,编译之后就被擦除了,擦除以后都是裸类型 list。我们再来看一个例子,定义一个 List< String>,防止别的类型的元素添加进来,那么真的是没有办法添加了吗,答案是否定的,这个泛型只能防君子,不能防小人,我们还可以通过万能的反射来添加,有关反射的相关知识请看《java核心基础之反射》

        List<String> list = new ArrayList();
        list.add("111");
        list.add("222");
        list.add("hello");

        Class<? extends List> aClass = list.getClass();
        Method method = aClass.getDeclaredMethod("add", Object.class);
        method.invoke(list, 333);
        System.out.println(list);

运行一下

小伙们,成功了!我通过反射的方式把 333 添加进去,成功通过编辑器的检查,并且运行得到了结果,这说明什么?在 Java 中泛型只在编译阶段有效,编译之后程序会采取去泛型化的措施。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。 对此总结成一句话:泛型类型在逻辑上可以看成是多个不同的类型,实际上都是相同的基本类型。

泛型的使用

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。泛型跟我们的成员属性一样,需要先声明才能使用。泛型的声明采用 <> 进行声明,声明可以随便写为任意标识,一般我们用大写的T、E、K、V等形式的参数。

泛型类

我举一个例子,这样的一个实体返回类大家应该都见过吧,这个就是一个泛型类,红框里面的就是声明。 使用的时候我们想返回什么类型的数据都可以,只要就往 data 里面塞就可以了,用泛型类的作用就是为了增加我们代码的灵活度,不用为每一种类型都建一个返回实体。

有一点需要注意的是,泛型的类型参数只能是类类型,不能是基本数据类型。比如 int,long 不可以,而要使用 Integer,Long 类型。

        Result<int> result3 = new Result<>();//错误
        Result<Integer> result4 = new Result<>();//正确

泛型接口

泛型接口与泛型类的定义及使用基本相同。我们来看一个例子:

public interface CalGeneric<T> {

    T add(T t1, T t2);

    T sub(T t1, T t2);

    T mul(T t1, T t2);

    T div(T t1, T t2);
    
}

定义了一个加减乘除的接口,定义泛型的作用也是灵活,我想计算 Integer 类型就计算 Integer 类型,我想计算 Long 类型就计算 Long 类型,不用为每个类型都写一个计算的类。

当实现泛型接口的类,未传入泛型实参时,未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中。

红框里的 T 必须要加,否则编译器会报错:"Unknown class"

当实现泛型接口的类,传入泛型实参时,我们可以为 T 传入无数个实参,形成无数种类型的 Generator 接口的实现类。在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型。

泛型方法

泛型方法有两种:

  • 实体方法,实体方法可以使用在类中定义的泛型或者方法中定义的泛型。
  • 静态方法,不可以使用在类中定义的泛型,只能使用在静态方法上定义的泛型。

我们先来看实体方法

  • public 与返回值中间的< T>非常重要,此处声明了的方法才是泛型方法,所以 method2 是泛型方法。这里为了给大家有个混淆,我用的也是 T,其实此处的 T 用 E、K、V来代替这样更好。

  • 泛型类中的使用了泛型的成员方法并不是泛型方法,method1 就是普通方法,它上面的 T 表明该方法使用的就是泛型类上的 T。

注意:区分清楚泛型方法和泛型类中使用了泛型的普通方法,在你能够做到的前提下,能使用泛型方法解决问题的,就尽量用泛型方法,不必使用泛型类。

下面来看静态方法,如果在泛型类中定义使用泛型的静态方法,需要添加额外的泛型声明,将这个方法定义成泛型方法。静态方法不能使用泛型类中已经声明过的泛型,method4 就是错误的写法,编译器报错了。

通配符

最后我们来讲一下通配符,老规矩说下为什么要使用通配符,通配符帮我们解决了什么问题。

大家看上面的例子,list1 和 list2 都要调用 loop 方法,可是 loop 的形参是 List< String> 类型的,list2 就传不进去,编辑器给出了红色的提示,为了解决上面的问题,我们就需要再定义一个新的方法 loop2(List< Number> list) 来专门给 list2 用,这显然与 java 中的多态理念相违背。因此我们需要一个可以同时处理 list1 和 list2 的方法,由此通配符就应运而生了。

无边界通配符

我们将上面的方法稍加改造下,把List< String> 改为List <?>

此时编辑器就不报错了,类型通配符一般是使用?代替具体的类型实参,此处的?和 Number、String、Integer 一样都是一种实际的类型,可以把?看成所有类型的父类,是一种真实的类型。

上界通配符

< ? extends T>:是指上界通配符,为什要有界,就像上面的例子,String 和 Number 是两种类型,这样不加限制就是一个大杂烩,阿毛阿狗都可以传进来,我这个方法也不是万金油,谁都可以用的,毕竟不是一个类别,一个门派的东西。

所以将上面的代码稍加改造下,变成了 List<? extends Number>

此时就只能放下 Number 类的子子孙孙,而 String 派别的你就一边去,你是放不进来的。

下界通配符

< ? super T>:是指下界通配符,这个也很简单,和上界通配符相反,这里代表的是泛型支持 Integer 及以上类型到 Object 的都可以放。

    public static void loop(List<? super Integer> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

最后掌握好泛型可以简化开发,且能很好的保证代码质量。原创不易,如果你觉得写的不错,请点一个赞!