Java 入门指南:Java 泛型(generics)

2024-08-25 18:36

本文主要是介绍Java 入门指南:Java 泛型(generics),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Java 泛型概要

Java 泛型(generics) 是 JDK 5 中引入的一个新特性。泛型的本质是参数化类型,也就是所操作的数据类型被指定为一个参数(可以称之为类型形参,然后在使用/调用时传入具体的类型。)

使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。

Java中的泛型,只在编译阶段有效

在编译之后程序会采取去泛型化的措施。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段

因此,泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型

泛型有三种使用方式:泛型类泛型接口泛型方法

泛型的优势

  1. 类型安全:泛型可以在编译时检查类型错误,减少运行时异常。

  2. 消除强制类型转换:使用泛型后,可以自动获得正确的类型,无需进行显式的类型转换,提高了代码的可读性和安全性。

  3. 提高代码重用性:泛型使得可以编写更加通用的代码,如泛型集合可以存储任何类型的对象,而无需为每种类型编写特定的集合类。

  4. 性能优化:通过消除运行时的类型检查和转换,泛型可以提高程序的性能。

泛型类

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:ListSetMap

泛型类的声明

class ClassName<T>{// 类成员的和方法定义private T item;public void setItem(T item){this.item = item;}public T getItem(){return item;}// 此方法有错误,因为类的声明中未声明泛型E,在使用E做形参和返回值类型时,编译器会无法识别。public E setKey(E key){this.key = key;}
}
  • T 是类型参数,它代表着一个占位符,表示在实例化泛型类时将传入的具体类型

  • 在泛型类的声明中,T可以被替换为任何合法的Java标识符,通常使用如下常见的命名约定:

    • T - 代表任意类型
    • E - 代表元素(通常在集合类中使用)
    • K - 代表键(通常在关联数据结构中使用)
    • V - 代表值(通常在关联数据结构中使用)
  • 类型参数声明部分可以包含一个或多个类型参数,参数间用逗号隔开。

  • 泛型的类型参数只能是类类型StringDoubleInteger),不能是简单类型(intfloatchardouble

  • 不能对确切的泛型类型使用 instanceof 操作。如下面的操作是非法的,编译时会出错:

if(ex_num instanceof Generic<Number>){...}

实例化是否需要传入实参

在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。

如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型

泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,它允许在接口的方法、成员变量和常量等位置使用一种或多种类型参数来增加灵活性和重用性。

public interface InterFaceName<T>{public T next();
}

未传入泛型实参

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

如果不声明泛型,如:

class FruitGenerator implements Generator<T>

编译器会报错:“Unknown class

public interface Pair<K,V>{K getKey();V getValue();
}public class OrderPair <K,V> implements Pair <K,V>{private K key;private V value;public OrderedPair(K key, V value) {this.key = key;this.value = value;}@Overridepublic K getKey() {return key;}@Overridepublic V getValue() {return value;}
}

传入泛型实参时

定义一个类(生产器)实现这个接口,虽然我们只创建了一个泛型接口
但是我们可以为 T 传入无数个实参,形成无数种类型的 Implement 接口。

在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型,所有的 T 都将换成传入的实参

public class OrdersPair implements Pair <Integer,String>{private Integer key;private String value;public OrderedPair(Integer key, String value) {this.key = key;this.value = value;}@Overridepublic Integer getKey() {return key;}@Overridepublic String getValue() {return value;}
}

类型擦除

虚拟机是没有泛型的,把泛型类的字节码进行反编译,用反编译工具(如 jad)将 class 文件反编译后,类型变量 <E> 消失了,取而代之的是 Object。

如果泛型类使用了限定符 extends,例如 <E extends TestClass>
类型变量 <E extends TestClass> 不见了,E 被替换成了 TestClass

Java 虚拟机会将泛型的类型变量擦除,并替换为限定类型(没有限定的话,为 Object

类型擦除会遇到的问题

![[Pasted image 20231103230114.png]]

在浅层的意识上,我们会想当然地认为 Arraylist<String> listArraylist<Date> list 是两种不同的类型,因为 StringDate 是不同的类。

但由于类型擦除的原因,以上代码是不会通过编译的——编译器会提示一个错误(这正是类型擦除引发的那些“问题”)
![[Pasted image 20231103230142.png]]

这两个方法的参数类型在擦除后是相同的。也就是说,method(Arraylist<String> list)method(Arraylist<Date> list) 是同一种参数类型的方法,不能同时存在。类型变量 StringDate 在擦除后会自动消失,method 方法的实际参数是 Arraylist list

泛型通配符

若传入的实参类型具有父子类关系,如 NumberInteger 类,能否在泛型中视为具有父子关系的泛型关系?

即在 class<Number>"作为形参的方法中,能否传入 class<Interger>的实例? 由于类型擦除的原因,编译器会报错

同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的

需要一个在逻辑上可以表示同时class<Integer>class<Number>父类的引用类型。由此类型通配符应运而生

在Java中,泛型通配符(Wildcard) 是一种特殊的类型参数,用于表示不确定的类型。通配符可以被用作泛型类、泛型接口和泛型方法中的类型参数,以增加代码的灵活性和重用性。

例如 List<?> 表示一个可以存储任何类型对象的 List,但是不能对其中的元素进行添加操作。通配符可以用来解决类型不确定的情况,例如在方法参数或返回值中使用。

泛型通配符有三种形式

通配符

问号 ? :表示未知类型,可以匹配任何类型。此处的“ ? ”是泛型实参,而不是泛型形参!!!即 NumberIntegerString… 都是同一种实际的类型

上限通配符

上限通配符 ? extends T表示类型参数是 T 或 T 的子类型。类型实参只准传入某种类型及其子类的对象。

public void processList(List<? extends Number> list) {for (Number element : list) {// 处理元素}
}

List<? extends Number> 表示接受的元素类型是 Number 或 Number 的子类型的List

下限通配符

下限通配符(Lower Bounded Wildcards) 用 super 关键字来声明,其语法形式为 <? super T> ,其中 T 表示类型参数。它表示的是该类型参数必须是某个指定类的超类(包括该类本身)。

当我们需要往一个泛型集合中添加元素时,如果使用的是上限通配符,集合中的元素类型可能会被限制,从而无法添加某些类型的元素。但是,如果我们使用下限通配符,可以将指定类型的子类型添加到集合中,保证了元素的完整性。

假设有一个类 Animal,以及两个子类 Dog 和 Cat。现在我们有一个 List<? super Dog> 集合,它的类型参数必须是 Dog 或其父类类型。可以向该集合中添加 Dog 类型的元素,也可以添加它的子类。但是,不能向其中添加 Cat 类型的元素,因为 Cat 不是 Dog 的子类。

虽然使用下限通配符可以添加某些子类型元素,但是在读取元素时,我们只能确保其是 Object 类型的,无法确保其是指定类型或其父类型。因此,在读取元素时需要进行类型转换

泛型方法

泛型方法是一种具有泛型类型参数的方法。通过在方法的声明中使用类型参数,可以在方法内部使用不特定的类型

![[Pasted image 20231103224842.png]]

方法返回类型和方法参数类型至少需要一个

public <T> returnType methodName(T parameter){// method code blockfor(T element : parameter){}
}
  • public 与 返回值中间 <T> 非常重要,表示类型参数的声明,可以是一个或多个类型参数。用于声明此方法为泛型方法。

  • 只有声明了 <T> 的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。

  • <T> 表明该方法将使用泛型类型 T,此时才可以在方法内部使用泛型类型T

  • 与泛型类的定义一样,此处 T 可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。

    • T - 代表任意类型
    • E - 代表元素(通常在集合类中使用)
    • K - 代表键(通常在关联数据结构中使用)
    • V - 代表值(通常在关联数据结构中使用)

泛型类,是在实例化类的时候指明泛型的具体类型;而泛型方法,是在调用方法的时候指明泛型的具体类型

如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法。

泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<T>上添加上下边界:

public <T extends Number> T showKeyName (Generic<T> container)

类中的泛型方法

public class ClassName<T>{public void test1(T t){System.out.println(t.toString);}public <E> void test2(E e){}public <T> void test3(T t){}
}
  • test2 中的 泛型 E 可以为任意类型。可以类型与 T 相同,也可以不同。由于泛型方法在声明的时候会声明泛型 <E>,即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。

  • test3 中的 泛型 T 是一个全新的类型,可以与类中使用的泛型 T 不同

泛型方法与可变参数

可变参数是一种特殊的参数形式,它允许方法接受可变数量的参数

使用可变参数和泛型方法的组合,可以更方便地处理具有不确定数量和类型的参数列表,并在方法内部使用泛型类型参数,从而实现更灵活和通用的代码。

class ClassName{public <T> void test(T...args){for(T arg : args){System.out.println(arg);}}
}ClassName cn = new ClassName();
cn.test("111",222,"aaaa","2323.4",55.55)
cn.test(true,false);

静态方法与泛型

在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型

如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。即如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法

泛型数组

java中是 不能创建一个确切的泛型类型的数组 的。因为Java中的数组是具体类型的集合,而泛型是在编译时进行类型擦除的。由于类型擦除的存在,无法在运行时创建具体类型的泛型数组。

可以创建一个泛型类型的数组引用,然后将其转换为指定类型的数组。这个转换被称为类型强制转换或类型安全的转换。

// 创建泛型类型的数组引用
Object[] genericArray = new Object[5];// 转换为指定类型的数组
String[] stringArray = (String[]) genericArray;

也可以使用通配符创建泛型数组:

List<?>[] lists = new ArrayList<?>[10];
// 转换为指定类型的数组
List<String>[] stringArray = (List<String>[]) genericArray;

最后取出数据是要做显式的类型转换的

或者直接指定类型:

List<String> lists = new ArrayList<?>[10];

在类型强制转换时,应确保转换是安全的,即转换后的类型与实际存储在数组中的对象类型相符。如果转换不是安全的,会在运行时抛出ClassCastException 异常。

尽管可以使用通配符创建泛型数组引用并进行类型转换,但在实际编程中,最好使用集合类型(如ArrayList)来代替泛型数组,以获得更好的类型安全和灵活性。

总结

泛型是Java中一种强大的特性,它允许我们编写类型安全且可重用的代码。通过使用泛型,我们可以创建灵活的组件,这些组件可以处理不同的数据类型,同时保持代码的简洁性和可读性。了解泛型的基本概念和高级用法对于编写高质量的Java程序至关重要。

这篇关于Java 入门指南:Java 泛型(generics)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1106331

相关文章

Java NoClassDefFoundError运行时错误分析解决

《JavaNoClassDefFoundError运行时错误分析解决》在Java开发中,NoClassDefFoundError是一种常见的运行时错误,它通常表明Java虚拟机在尝试加载一个类时未能... 目录前言一、问题分析二、报错原因三、解决思路检查类路径配置检查依赖库检查类文件调试类加载器问题四、常见

Java注解之超越Javadoc的元数据利器详解

《Java注解之超越Javadoc的元数据利器详解》本文将深入探讨Java注解的定义、类型、内置注解、自定义注解、保留策略、实际应用场景及最佳实践,无论是初学者还是资深开发者,都能通过本文了解如何利用... 目录什么是注解?注解的类型内置注编程解自定义注解注解的保留策略实际用例最佳实践总结在 Java 编程

Python中模块graphviz使用入门

《Python中模块graphviz使用入门》graphviz是一个用于创建和操作图形的Python库,本文主要介绍了Python中模块graphviz使用入门,具有一定的参考价值,感兴趣的可以了解一... 目录1.安装2. 基本用法2.1 输出图像格式2.2 图像style设置2.3 属性2.4 子图和聚

Java 实用工具类Spring 的 AnnotationUtils详解

《Java实用工具类Spring的AnnotationUtils详解》Spring框架提供了一个强大的注解工具类org.springframework.core.annotation.Annot... 目录前言一、AnnotationUtils 的常用方法二、常见应用场景三、与 JDK 原生注解 API 的

Java controller接口出入参时间序列化转换操作方法(两种)

《Javacontroller接口出入参时间序列化转换操作方法(两种)》:本文主要介绍Javacontroller接口出入参时间序列化转换操作方法,本文给大家列举两种简单方法,感兴趣的朋友一起看... 目录方式一、使用注解方式二、统一配置场景:在controller编写的接口,在前后端交互过程中一般都会涉及

Java中的StringBuilder之如何高效构建字符串

《Java中的StringBuilder之如何高效构建字符串》本文将深入浅出地介绍StringBuilder的使用方法、性能优势以及相关字符串处理技术,结合代码示例帮助读者更好地理解和应用,希望对大家... 目录关键点什么是 StringBuilder?为什么需要 StringBuilder?如何使用 St

使用Java将各种数据写入Excel表格的操作示例

《使用Java将各种数据写入Excel表格的操作示例》在数据处理与管理领域,Excel凭借其强大的功能和广泛的应用,成为了数据存储与展示的重要工具,在Java开发过程中,常常需要将不同类型的数据,本文... 目录前言安装免费Java库1. 写入文本、或数值到 Excel单元格2. 写入数组到 Excel表格

Java并发编程之如何优雅关闭钩子Shutdown Hook

《Java并发编程之如何优雅关闭钩子ShutdownHook》这篇文章主要为大家详细介绍了Java如何实现优雅关闭钩子ShutdownHook,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起... 目录关闭钩子简介关闭钩子应用场景数据库连接实战演示使用关闭钩子的注意事项开源框架中的关闭钩子机制1.

Maven中引入 springboot 相关依赖的方式(最新推荐)

《Maven中引入springboot相关依赖的方式(最新推荐)》:本文主要介绍Maven中引入springboot相关依赖的方式(最新推荐),本文给大家介绍的非常详细,对大家的学习或工作具有... 目录Maven中引入 springboot 相关依赖的方式1. 不使用版本管理(不推荐)2、使用版本管理(推

Java 中的 @SneakyThrows 注解使用方法(简化异常处理的利与弊)

《Java中的@SneakyThrows注解使用方法(简化异常处理的利与弊)》为了简化异常处理,Lombok提供了一个强大的注解@SneakyThrows,本文将详细介绍@SneakyThro... 目录1. @SneakyThrows 简介 1.1 什么是 Lombok?2. @SneakyThrows