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

简介: 前言泛型是 Java 5 新增的一项特性,可以理解为类型的参数,主要用于代码重用,语义化代码,避免运行时的强制类型转换异常。

前言


泛型是 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 extend String> {
}



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

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 类作为类的原始类型抽象,然后又添加了一些其他的表示泛型的类型。如下图所示。


image.png

- 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 中泛型的简化。

目录
相关文章
|
7天前
|
安全 Java 程序员
Java 泛型
Java 泛型
10 0
|
1天前
|
数据采集 安全 Java
Java并发编程学习12-任务取消(上)
【5月更文挑战第6天】本篇介绍了取消策略、线程中断、中断策略 和 响应中断的内容
14 4
Java并发编程学习12-任务取消(上)
|
2天前
|
存储 算法 搜索推荐
滚雪球学Java(27):从零开始学习数组:定义和初始化
【5月更文挑战第2天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
7 3
|
4天前
|
缓存 Java 数据库
Java并发编程学习11-任务执行演示
【5月更文挑战第4天】本篇将结合任务执行和 Executor 框架的基础知识,演示一些不同版本的任务执行Demo,并且每个版本都实现了不同程度的并发性。
24 4
Java并发编程学习11-任务执行演示
|
4天前
|
存储 安全 Java
掌握8条泛型规则,打造优雅通用的Java代码
掌握8条泛型规则,打造优雅通用的Java代码
掌握8条泛型规则,打造优雅通用的Java代码
|
5天前
|
数据库连接
java+ssm+vue代码视频学习讲解
java+ssm+vue代码视频学习讲解
7 0
|
12天前
|
Java
JAVA难点包括异常处理、多线程、泛型和反射,以及复杂的分布式系统知识
【5月更文挑战第2天】JAVA难点包括异常处理、多线程、泛型和反射,以及复杂的分布式系统知识。入坑JAVA因它的面向对象特性、平台无关性、强大的标准库和活跃的社区支持。
36 2
|
14天前
|
算法 Java 大数据
Java从入门到精通学习报告
Java从入门到精通学习报告
20 1
|
14天前
|
消息中间件 监控 安全
【JAVAEE学习】探究Java中多线程的使用和重点及考点
【JAVAEE学习】探究Java中多线程的使用和重点及考点
|
15天前
|
Java
【专栏】Java 8 的 Streams 提供了一种处理数据集合的新方式,增强了代码的可读性和可维护性
【4月更文挑战第28天】Java 8 的 Streams 提供了一种处理数据集合的新方式,增强了代码的可读性和可维护性。本文介绍了 Streams 的基本概念,如从数据源创建 Stream,以及中间和终端操作。通过过滤、映射、归并、排序、分组等案例,展示了 Streams 的使用,包括并行 Streams 提高效率。学习 Streams 可以提升代码质量和效率,文章鼓励读者在实际开发中探索更多 Streams 功能。