Java 泛型概要
Java 泛型(generics) 是 JDK 5 中引入的一个新特性。泛型的本质是参数化类型,也就是所操作的数据类型被指定为一个参数(可以称之为类型形参,然后在使用/调用时传入具体的类型。)
使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。
Java中的泛型,只在编译阶段有效。
在编译之后程序会采取去泛型化的措施。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
因此,泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
泛型有三种使用方式:泛型类、泛型接口、泛型方法
泛型的优势
-
类型安全:泛型可以在编译时检查类型错误,减少运行时异常。
-
消除强制类型转换:使用泛型后,可以自动获得正确的类型,无需进行显式的类型转换,提高了代码的可读性和安全性。
-
提高代码重用性:泛型使得可以编写更加通用的代码,如泛型集合可以存储任何类型的对象,而无需为每种类型编写特定的集合类。
-
性能优化:通过消除运行时的类型检查和转换,泛型可以提高程序的性能。
泛型类
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List
、Set
、Map
。
泛型类的声明
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
- 代表值(通常在关联数据结构中使用)
-
类型参数声明部分可以包含一个或多个类型参数,参数间用逗号隔开。
-
泛型的类型参数只能是类类型(
String
、Double
、Integer
),不能是简单类型(int
、float
、char
、double
) -
不能对确切的泛型类型使用
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;
}
@Override
public K getKey() {
return key;
}
@Override
public 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;
}
@Override
public Integer getKey() {
return key;
}
@Override
public String getValue() {
return value;
}
}
类型擦除
虚拟机是没有泛型的,把泛型类的字节码进行反编译,用反编译工具(如 jad)将 class 文件反编译后,类型变量 <E>
消失了,取而代之的是 Object。
如果泛型类使用了限定符 extends
,例如 <E extends TestClass>
类型变量 <E extends TestClass>
不见了,E 被替换成了 TestClass
Java 虚拟机会将泛型的类型变量擦除,并替换为限定类型(没有限定的话,为 Object
)
类型擦除会遇到的问题
在浅层的意识上,我们会想当然地认为 Arraylist<String> list
和 Arraylist<Date> list
是两种不同的类型,因为 String
和 Date
是不同的类。
但由于类型擦除的原因,以上代码是不会通过编译的——编译器会提示一个错误(这正是类型擦除引发的那些“问题”)
这两个方法的参数类型在擦除后是相同的。也就是说,method(Arraylist<String> list)
和 method(Arraylist<Date> list)
是同一种参数类型的方法,不能同时存在。类型变量 String
和 Date
在擦除后会自动消失,method 方法的实际参数是 Arraylist list
泛型通配符
若传入的实参类型具有父子类关系,如
Number
和Integer
类,能否在泛型中视为具有父子关系的泛型关系?即在 class<Number>"作为形参的方法中,能否传入 class<Interger>的实例? 由于类型擦除的原因,编译器会报错
同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
需要一个在逻辑上可以表示同时是 class<Integer>
和 class<Number>
父类的引用类型。由此类型通配符应运而生
在Java中,泛型通配符(Wildcard) 是一种特殊的类型参数,用于表示不确定的类型。通配符可以被用作泛型类、泛型接口和泛型方法中的类型参数,以增加代码的灵活性和重用性。
例如
List<?>
表示一个可以存储任何类型对象的 List,但是不能对其中的元素进行添加操作。通配符可以用来解决类型不确定的情况,例如在方法参数或返回值中使用。
泛型通配符有三种形式:
通配符
问号 ?
:表示未知类型,可以匹配任何类型。此处的“ ? ”是泛型实参,而不是泛型形参!!!即 Number
、Integer
、String
… 都是同一种实际的类型
上限通配符
上限通配符 ? 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 类型的,无法确保其是指定类型或其父类型。因此,在读取元素时需要进行类型转换。
泛型方法
泛型方法是一种具有泛型类型参数的方法。通过在方法的声明中使用类型参数,可以在方法内部使用不特定的类型。
方法返回类型和方法参数类型至少需要一个
public <T> returnType methodName(T parameter){
// method code block
for(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程序至关重要。