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

目录
相关文章
|
1月前
|
XML Java 编译器
Java学习十六—掌握注解:让编程更简单
Java 注解(Annotation)是一种特殊的语法结构,可以在代码中嵌入元数据。它们不直接影响代码的运行,但可以通过工具和框架提供额外的信息,帮助在编译、部署或运行时进行处理。
88 43
Java学习十六—掌握注解:让编程更简单
|
18天前
|
Java 大数据 API
14天Java基础学习——第1天:Java入门和环境搭建
本文介绍了Java的基础知识,包括Java的简介、历史和应用领域。详细讲解了如何安装JDK并配置环境变量,以及如何使用IntelliJ IDEA创建和运行Java项目。通过示例代码“HelloWorld.java”,展示了从编写到运行的全过程。适合初学者快速入门Java编程。
|
1月前
|
存储 SQL 小程序
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
这篇文章详细介绍了Java虚拟机(JVM)的运行时数据区域和JVM指令集,包括程序计数器、虚拟机栈、本地方法栈、直接内存、方法区和堆,以及栈帧的组成部分和执行流程。
33 2
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
|
25天前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。
|
29天前
|
Java API
[Java]泛型
本文详细介绍了Java泛型的相关概念和使用方法,包括类型判断、继承泛型类或实现泛型接口、泛型通配符、泛型方法、泛型上下边界、静态方法中使用泛型等内容。作者通过多个示例和测试代码,深入浅出地解释了泛型的原理和应用场景,帮助读者更好地理解和掌握Java泛型的使用技巧。文章还探讨了一些常见的疑惑和误区,如泛型擦除和基本数据类型数组的使用限制。最后,作者强调了泛型在实际开发中的重要性和应用价值。
25 0
[Java]泛型
|
1月前
|
存储 安全 Java
🌱Java零基础 - 泛型详解
【10月更文挑战第7天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
11 1
|
1月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
45 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
1月前
|
存储 算法 Java
带你学习java的数组军队列
带你学习java的数组军队列
35 0
|
1月前
|
Java 大数据 开发工具
java学习——环境准备(1)
java学习——环境准备(1)
42 0
|
1月前
|
安全 Java 编译器
Java基础-泛型机制
Java基础-泛型机制
16 0
下一篇
无影云桌面