学习泛型的理由
首先明确为什么需要学习泛型?个人觉得至少有三个理由:1、使用泛型可以让你在声明类(或者创建方法)的时候不着急立即去指定它的类型,而是等到你实例化对象(或者方法调用)的时候才明确它的类型;2、避免通过使用Object类型来泛指java对象时,因类型强制向下转型时发生错误;3、可以毫无障碍的阅读Java相关源码。你经常遇到诸如Comparator super E> comparator和List extends Number>此类的代码,可能不太明白其中的含义,如果你学会了泛型,就毫无压力可以从容地面对那些代码了。
什么是泛型
泛型不光是在java,在很多面向对象语言及各种设计模式中有广泛的应用。所谓的泛型,其实就是把类型“参数化”。
一提到参数,大家最熟悉的就是定义方法时的形参和调用方法时的实参。那么类型“参数化”到底怎么理解呢?顾名思义,类型“参数化”就是将类型由原来的具体类型,变成参数化的“类型”,有点类似于方法中的变量参数,不过此时是类型定义成参数形式(你可以理解为类型形参),然后在使用时传入具体的类型(也就是类型实参)。为什么这样操作呢?因为它能让类型"参数化",也就是在不创建新的类型的情况下,通过泛型可以指定不同类型来控制形参具体限制的类型。
举一个你可能在实际编程过程中遇到过的例子:
public static void main(String[] args){ List arrayList = Arrays.asList("hello","world",2018); for(int i=0;i
猜猜运行结果,可以发现输出信息为:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
为什么会这样呢?我们知道ArrayList可以存放任意类型,在本例中仅仅是添加了两个String类型和一个Integer类型,只不过在获取对象的时候都变成String类型,由于Integer类型无法强制转为String类型,所以程序报出异常。问题就在于程序在编译时期并没有报错,而是在运行期报错了。我们希望有这么一个类型机制,就是只要程序在编译时期没有出现警告,那么运行时期就不会出现上述的ClassCastException异常,其实这就是Java泛型设计的原则。泛型其实就是将类型明确的工作推迟到创建对象或方法调用的时候才去明确的特殊类型。
为了解决上面那个问题,你需要将声明arrayList的代码修改一下,这样编译器在编译阶段就能发现类似的问题:
List arrayList = Arrays.asList("hello","world",2020);// Incompatible types.在编译阶段,编译器就会报错,无需运行
通过"泛型"这种语法糖,我们就能在编译阶段发现由于类型异常而导致的程序运行问题。
泛型只存在于编译阶段
泛型只存在于编译阶段,到了程序运行时刻就不存在了,这一现象称为“泛型擦除”。当你使用javac编译器将.java源文件转为字节码的.class文件时,泛型就不存在了。接下来通过一个例子来验证泛型只存在于编译阶段这一论据:
public static void main(String[] args){ List stringList = new ArrayList<>(); List integerList = new ArrayList<>(); Class classStringList = stringList.getClass(); Class classIntegerList = integerList.getClass(); System.out.println(classStringList==classIntegerList); }
猜猜运行结果,可以发现输出信息为:
true
这里我们使用了java反射机制,这样可以在运行时动态获取类的相关信息,这从侧面验证了论据的正确性。程序会编译后去除泛型,即Java中的泛型只在编译阶段有效。在编译过程正确检验泛型结果后,会将泛型的相关信息擦除,且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。泛型类型在逻辑上看似乎是多个不同的类型,但其实都是相同的类型。(注意泛型不能使用基本数据类型,必须是其对应的包装类。)
由于你在声明stringList对象的时候,使用泛型确定了它存储的元素类型为String,因此可以使用增强for循环来遍历stringList对象。
Java泛型中的标记符
在使用Java泛型前,了解其中一些标识符的含义,有助于提升开发效率。常用的标识符及含义如下:
E - Element (集合使用,因集合中存放元素) T - Type(Java 类) K - Key(键) V - Value(值) N - Number(数值类型) ? - 表示不确定的java类型 S、U、V - 2nd、3rd、4th types
你可能会有疑问,弄这么多标识符干嘛,直接使用万能的Object难道不香么?我们知道Object是所有类的基类(任何类如果没有指明其继承类,都默认继承于Object类),因此任何类的对象都可以设置Object的引用,只不过在使用的时候可能要类型强制转换。但是如果设置了泛型E、T等这些标识符,那么在实际使用之前类型就已经确定,因此不再需要类型强制转换。
泛型的使用
在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口和泛型方法。
泛型类
顾名思义,泛型类就是把泛型定义在类上,用户使用该类的时候,才把类型确定下来。用户确定什么类型,该类就代表什么类型,就不用担心在使用的时候需要类型强转及运行时转换异常等问题。泛型使用最多的就是各种容器类,如List、Set、Map等,通过泛型可以对一组类的操作提供对外相同的接口。请注意,在类上定义的泛型,在类的方法中同样也能使用(普通静态方法除外)。
泛型类的基本写法:
class 类名称 { private 泛型标识 variable; ..... }}
这么看你可能会感到困惑,这里结合一个实例来进行说明。新建一个泛型类:
package com.envy.parameter;public class ParameterClass { private T type; public T getType(){ return type; } public void setType(T type){ this.type = type; } //无参的构造方法 public ParameterClass(){ } //有参的构造方法 public ParameterClass(T type){ this.type =type; }}
接下来开始进行测试,前面说过用户需要哪种类型,就可以在实例化对象的时候指定哪种类型:
public static void main(String[] args){ ParameterClass stringParameterClass = new ParameterClass<>("envy"); String result = stringParameterClass.getType(); System.out.println(result)