学习 Java,你不得不知的泛型知识

前言

泛型是 Java 5 新增的一项特性,可以理解为类型的参数,主要用于代码重用,语义化代码,避免运行时的强制类型转换异常。
在泛型出现之前,集合中的 List 存储的对象只能为 Object,示例代码如下

List list = new ArrayList(); 
list.add("str"); 
Integer num = (Integer)list.get(0);  

从 List 中获取 Integer 类型的对象,需要进行强制类型转换,如果不能保证存储的对象只为 Integer 类型,很容易出现 ClassCastException。泛型出现后上述代码可修改为如下。

List<Integer> list = new ArrayList<>(); 
list.add("str"); // 编译时报错 
Integer num = list.get(0);  

修改后的代码中,List 类型后携带了<Integer>,表示 List 中存储的只能为 Integer 类型,此时如果向 List 中添加其他类型,则会在编译时报错,将运行时的类型检查提前到编译期,避免的错误的产生,同时语义也相对清晰,一眼可以看出 List 中存储的是什么类型。

泛型的使用

泛型类及泛型方法

使用泛型,首先需要进行定义,泛型可以用在类上和方法上。

泛型在类上面的定义,只需要在类后面添加尖括号,然后在尖括号中为泛型取一个名字即可,一般为比较简短的大写英文字母,常用的 T 表示任意类型,E 表示集合中的元素,K 和 V 分别表示键和值。示例如下。

public class GenericClazz<T> {
    
    
}

如果一个类中存在多个泛型,泛型的名称之间可以用英文逗号分隔。示例如下。

public class GenericClazz<K,V> {
    
    
}

泛型可以用来表示任意类型,如果我们想要限制泛型的类型,则需要使用 extend 表示泛型只能为某个接口或类的子类。示例如下。

public class GenericClazz<T extend String> {
    
    
}

此时 T 只能用于表示字符串类型,如果想表示多个接口的子类,则可以在类型之间使用 & 符合连接。示例如下。

public class GenericClazz<T extend String & Serializable> {
    
    
}

泛型定义后,一般我们会在成员变量或方法中使用,如下所示。

public class GenericClazz<T extend String> {
    
    
	
	private T param;

	public T getParam(){
    
    
		return this.param;
	}

	public void setParam(T param){
    
    
		this.param = param;
	}
}

使用方式如下。

GenericClazz<String> clzss = new GenericClazz();
clazz.setParam("abc");
String param = clazz.getParam();

除了在类上定义泛型,还可以直接在方法上定义泛型。在方法上定义泛型需要在方法的修饰符后,返回值前定义泛型类型,如下所示。

public class Test {
    
    
    public static <T> T getParam(T param) {
    
    
        return param;
    }
}

调用泛型方法的示例如下。

public class Test {
    
    
    public static void main(String[] args) {
    
    
        String param = Test.getParam("param");
    }
}

类型擦除

每个泛型通过编译都会转换为一个原始类型,没有 extend 限制的泛型对应的原始类型是 Object,有 extend 限制的泛型类型为 extend 后面的第一个类型。如下

public class GenericClazz<T extend String> {
    
    
	private T param;
}

上述中的代码在编译后可能会转换为如下。

public class GenericClazz{
    
    
	private String param;
}

也就是说,泛型是通过类型擦除实现的,编译后的 class 文件中泛型已经转换为了具体的类型,由于存在类型擦除,编译器可能会插入强制类型转换的代码或生成桥接方法。
如下代码所示。

public class GenericClazz<T> {
    
    

    private T param;

    public T getParam() {
    
    
        return this.param;
    }

    public static void main(String[] args) {
    
    
        GenericClazz<Integer> clazz = new GenericClazz<>();
        Integer param = clazz.getParam();
    }
}

泛型类型 T 经过类型擦除,getParam 方法返回的类型会转换为 Object 类型,示例代码将其返回值赋值给 Integer 类型的变量,因此编译器会在赋值的指令中插入强制类型转换的代码。

如果类型擦除和多态发生冲突,编译器则会自动生成桥接方法,看下面的代码。

public class GenericClazz<T> {
    
    

    private T param;

    public T getParam() {
    
    
        return this.param;
    }

    public void setParam(T param) {
    
    
        this.param = param;
    }

    public static void main(String[] args) {
    
    
        SubGenericClazz subGenericClazz = new SubGenericClazz();
        subGenericClazz.setParam("str");
    }

}

class SubGenericClazz extends GenericClazz<String> {
    
    

    @Override
    public void setParam(String param) {
    
    
        return super.setParam(param);
    }
}

不带泛型的类 SubGenericClazz 继承了泛型类 GenericClazz<String>,然后实现其方法,然后将子类赋值给父类的引用,由于多态的存在,调用父类的方法时将会调用实际类型的方法,而父类由于类型擦除,最终调用的方法应该为 GenericClazz#setParam(Object param),而子类 SubGenericClazz 并不存在这样的方法,此时类型擦除和多态发生冲突,编译器自动生成桥接方法 SubGenericClazz#setParam(Object param),生成的代码可以理解如下。

class SubGenericClazz extends GenericClazz<String> {
    
    

	// 生成的桥接方法
	public void setParam(Object param){
    
    
		return this.setParam((String)param);
	}

    @Override
    public void setParam(String param) {
    
    
        return super.setParam(param);
    }
}

通配符类型

相同的类型,如果其泛型类型不同,则赋值会编译失败,如下所示。

GenericClazz<Number> genericClazz = new GenericClazz<Integer>();

这里 GenericClazz<Number>GenericClazz<Integer>,虽然都是 GenericClazz,但由于编译时对泛型类型的检查,因此会编译失败,为了解决这个问题,可以使用通配符类型。

通配符类型使用 ? 表示,对其类型的限制除了使用 extends,还可以使用 super。上述代码修正后如下。

GenericClazz<? extends Number> genericClazz = new GenericClazz<Integer>();

extends 后面的表示通配符的上界,super 表示通配符的下界,如GenericClazz<? super Integer> 表示类型只能为 Integer 的父类,通配符如果存在上界或下界,将会影响包含通配符的对象的赋值,方法可传入的参数类型、方法的的返回值类型等。

通配符设置上界示例代码如下。

        GenericClazz<? extends Number> genericClazz = new GenericClazz<Integer>();
        Number param = genericClazz.getParam();
        genericClazz.setParam(Integer.valueOf("1")); //编译失败

为通配符提供上界,则泛型类型作为返回值时只能返回上界的类型,而泛型类型则无法作为参数调用方法。

通配符设置下界示例代码如下。

        GenericClazz<? super Integer> genericClazz = new GenericClazz<>();
        Object param = genericClazz.getParam();
        genericClazz.setParam(1);

为通配符设置下界后,泛型类型作为方法的返回类型只能返回 Object 类型,同时也只能使用通配符的下界类型作为方法参数的类型。

可以使用一个无上界和下界的通配符类型,此时和普通的泛型类型相比,泛型方法的返回值只能为 Object 类型,而通配符类型则无法作为方法的参数调用。

泛型与反射

虽然泛型通过类型擦除实现,但是编译后的 class 文件中仍保留着类或方法的泛型信息,在前面的文章 Java 基础知识之 Java 反射 主要将重点放在反射对类型的抽象上,反射同样提供了获取类的泛型信息的能力。

泛型自 Java 5 诞生,为了描述泛型信息,Java 将 Class 类作为类的原始类型抽象,然后又添加了一些其他的表示泛型的类型。如下图所示。
Java 类型- Type:表示 Java 的某一种类型。

  • WidcardType:通配符类型,如 GenericClazz<? super Integer> 中的 ? super Integer
  • Class:不包含泛型信息的原始类型。
  • ParameterizedType:参数化类型,如public class GenericClazz<T extend Number> {} 中的 GenericClazz<T extend Number>
  • GenericArrayType:泛型数组类型,如T[]
  • TypeVariable:类型变量,如public class GenericClazz<T extend Number> {} 中的 T extend Number

关于反射中有关泛型的 API ,使用示例如下所示。

        Class<?> clazz = String.class;
        // 获取类的类型变量
        TypeVariable<? extends Class<?>>[] typeParameters = clazz.getTypeParameters();
        // 获取类的泛型父类
        Type genericSuperclass = clazz.getGenericSuperclass();
        // 获取类的泛型接口
        Type[] genericInterfaces = clazz.getGenericInterfaces();

        for (Field field : clazz.getDeclaredFields()) {
    
    
            // 获取成员变量的泛型类型
            Type genericType = field.getGenericType();
        }
        
        for (Method method : clazz.getDeclaredMethods()) {
    
    
            // 获取方法返回的泛型类型
            Type genericReturnType = method.getGenericReturnType();
            // 获取参数的泛型类型
            Type[] genericParameterTypes = method.getGenericParameterTypes();
        }

        TypeVariable<?> typeVariable = null;
        // 获取类型参数的子类限定
        Type[] bounds = typeVariable.getBounds();

        WildcardType wildcardType = null;
        // 获取通配符类型的上界
        Type[] upperBounds = wildcardType.getUpperBounds();
        // 获取通配符类型的下界
        Type[] lowerBounds = wildcardType.getLowerBounds();

        ParameterizedType parameterizedType = null;
        // 获取参数化类型的原始类型
        Type rawType = parameterizedType.getRawType();
        // 获取参数化类型中泛型的真实类型
        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();

        GenericArrayType genericArrayType = null;
        // 获取泛型数组的元素类型
        Type genericComponentType = genericArrayType.getGenericComponentType();

总结

泛型是 Java 中的基础知识,日常开发中,定义泛型类的场景相对较少一些,在集合中使用相对较多,泛型是学好 Java 必须掌握的技能,后面将介绍 Spring 对 Java 中泛型的简化。

猜你喜欢

转载自blog.csdn.net/zzuhkp/article/details/107749136