Java中的泛型
泛型是Java SE 1.5之后的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
一、为什么使用泛型?
Java语言引入泛型的主要目标是提高Java程序的类型安全。泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的另一个好处是,它可以提高代码的重用性。在泛型出现之前,如果要对不同类型的数据执行相同的操作,必须为每一种类型编写一个单独的方法。但使用了泛型之后,只需要编写一个方法,就可以应用于不同的数据类型。
二、泛型的概念
在Java编程语言中,可以使用“泛型”来创建可重用的组件,这些组件可以支持多种类型的数据。这样可以减少重复编码,并提高代码的可读性和可维护性。
泛型类是具有一个或多个类型参数的类。这些类型参数在实际使用泛型类时被实际的类型替换(例如,Integer或String)。这个过程被称为类型实参化。
泛型方法是在调用方法的时候指明泛型的具体类型。
泛型接口与泛型类的定义及使用基本相同。
三、泛型的使用
- 泛型类:泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。泛型类的类型参数声明部分可以包含一个或多个类型参数,类型参数之间使用逗号分隔。这些类型参数在类的体中被当作一种类型来使用。当创建一个泛型类的对象时,需要指定这些类型参数的实际类型。
例如,定义一个简单的泛型类Box<T>
,该类可以持有任何类型的对象:
java复制代码
public class Box<T> { | |
// T代表任何类型 | |
private T t; | |
public void set(T t) { | |
this.t = t; | |
} | |
public T get() { | |
return t; | |
} | |
} |
在这个例子中,T
是类型参数,当创建一个Box
对象时,可以指定T
的实际类型。例如:
java复制代码
Box<Integer> integerBox = new Box<Integer>(); | |
integerBox.set(new Integer(10)); | |
Integer value = integerBox.get(); |
在Java SE 7及更高版本中,可以使用菱形操作符<>
来推断类型参数:
java复制代码
Box<Integer> integerBox = new Box<>(); | |
integerBox.set(Integer.valueOf(10)); | |
Integer value = integerBox.get(); |
- 泛型方法:泛型方法使得方法可以独立于其所在的类进行参数化。泛型方法的语法是在方法的返回类型前面加上一个尖括号内包含类型参数的列表。然后,在方法体内部,可以像使用类的类型参数那样使用这个类型参数。
例如,定义一个打印不同数组元素的方法:
java复制代码
public class GenericMethods { | |
// 泛型方法打印数组的元素 | |
public static <E> void printArray(E[] inputArray) { | |
for (E element : inputArray) { | |
System.out.printf("%s ", element); | |
} | |
System.out.println(); | |
} | |
public static void main(String[] args) { | |
// 创建不同类型数组: Integer, Double 和 Character | |
Integer[] intArray = {1, 2, 3, 4, 5}; | |
Double[] doubleArray = {1.1, 2.2, 3.3, 4.4}; | |
Character[] charArray = {'H', 'E', 'L', 'L', 'O'}; | |
System.out.println("整型数组元素为:"); | |
printArray(intArray); // 传递整型数组 | |
System.out.println("\n双精度型数组元素为:"); | |
printArray(doubleArray); // 传递双精度型数组 | |
System.out.println("\n字符型数组元素为:"); | |
printArray(charArray); // 传递字符型数组 | |
} | |
} |
在这个例子中,printArray
方法被定义为一个泛型方法,可以接受任何类型的数组,并在控制台上打印出数组的元素。
- 泛型接口:泛型接口的定义和使用与泛型类非常相似。在接口声明中,类型参数在接口名之后以尖括号内的列表形式给出。然后,接口的方法和类型就可以使用这个类型参数。
例如,定义一个泛型接口GenericStack
:
java复制代码
public interface GenericStack<T> { | |
void push(T value); | |
T pop(); | |
boolean isEmpty(); | |
} |
在这个例子中,GenericStack
是一个泛型接口,它接受一个类型参数T
,然后使用该类型参数定义接口的方法。现在,任何实现了这个接口的类都必须为T
提供一个实际的类型,或者使用另一个类型参数来代替。
四、类型通配符
Java泛型提供了类型通配符的概念,用于创建更为灵活和可重用的泛型代码。类型通配符在方法签名、类、接口或字段的类型中使用,以表示未知的类型参数。
有两种主要类型的通配符:
- 无界通配符(Unbounded Wildcards):使用问号(?)表示无界通配符。例如,
List<?>
是任何类型的List
。这种无界通配符的主要用途是作为参数来接收任何类型的集合。但是,因为它的未知性,通常无法向其中添加新的元素(除了null)。 - 有界通配符(Bounded Wildcards):使用
extends
关键字表示有界通配符。有两种形式的有界通配符:? extends Type
和? super Type
。? extends Type
表示类型参数是给定类型的子类型(或该类型本身),? super Type
表示类型参数是给定类型的超类型(或该类型本身)。有界通配符主要用于在API中提供更为灵活的泛型方法。
五、泛型的限制
Java中的泛型并不是万能的,它有一些限制:
- 不能使用基本类型作为类型参数,例如
int
、char
等。但是,可以使用基本类型的包装类,如Integer
、Character
等。 - 不能创建泛型数组。但是,可以创建泛型对象数组(这种数组在创建时会收到编译器警告)。
- 不能实例化类型参数。也就是说,不能调用
new T()
来创建一个新的泛型对象(这里的T
是类型参数)。这是因为Java在编译时并不知道T
的实际类型是什么。 - 不能对泛型类型参数进行强制类型转换,除非这个转换是到
Object
类。例如,不能将T
强制转换为String
,即使在实际运行时T
是String
。 - 在静态上下文中,不能引用类型参数。例如,静态方法和静态字段都不能引用类型参数。这是因为静态上下文是属于类的,而不是属于对象的,而类型参数是随对象而定的。
- 不能在捕获的类型上进行运算,如在
instanceof
中使用类型参数或试图使用类型参数对数组进行索引。
这些限制主要是为了保护类型安全,防止在运行时出现类型错误。虽然这些限制可能会给使用泛型带来一些不便,但总体来说,泛型大大提高了Java程序的类型安全性和代码的可重用性。