泛型介绍
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
(感觉和c++的模板很像,功能和写法都很类似)
假定我们有这样一个需求:写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,该如何实现?
答案是可以使用 Java 泛型。
使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。
▪ 以ArrayList<E>、ArrayList<Integer>为例1. ArrayList<E>定义了一个泛型类型,"E"称为"类型变量"或"类型参数"。
-
ArrayList<Integer>称为"参数化的类型","Integer"称为"实际类型参数"。
-
ArrayList称为泛型类型ArrayList<E>的"原始类型(raw type)"。
可以通过泛型对类型的指定,来避免出现给一个数组输入不同类型的情况出现,因为泛型会在编译阶段就不通过编译。
java
import java.lang.reflect.InvocationTargetException;
import java.util.*;
public class GenericList {
public static void main(String[] args) throws IllegalArgumentException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
//创建一个只想保存字符串的List集合
List<String> strList = new ArrayList<String>();
strList.add("One string");
strList.add("Two string");
strList.add("Three string");
//下面代码将引起编译错误
//strList.add(5);
//但使用反射可以绕开编译器的语法检查
//strList.getClass().getMethod("add", Object.class).invoke(strList, 5);
for (int i = 0; i < strList.size(); i++) {
//下面代码无需强制类型转换
String str = strList.get(i);
System.out.println(strList.get(i));
}
}
}
泛型方法
你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
下面是定义泛型方法的规则:
- 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔 ),该类型参数声明部分在方法返回类型之前(在下面例子中的**<E>**)。
- 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
- 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
- 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型 ,不能是原始类型(像 int、double、char 等)。因此,Pair<double>是错的,只能是Pair<Double>。
可以在普通类或泛型类中定义泛型方法:
java
class ArrayAlg {
public static <T> T getMiddle(T[] a) {
return a[a.length / 2];
}
}
使用泛型方法:(下面两个是等价的)
String[] names = ... ;
String middle = ArrayAlg.<String>getMiddle(names);
String middle = ArrayAlg.getMiddle(names);
泛型的类型自动推断
假设你有一个泛型方法,如下所示:
java
public class GenericMethod {
// 泛型方法
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
}
这个方法接受一个类型为T
的数组,并打印出数组的每个元素。
当你调用这个方法时,可以直接传入参数,Java编译器会根据你传入的参数类型来推断T
的具体类型。例如:
java
public class Main {
public static void main(String[] args) {
// 创建不同类型的数组
Integer[] intArray = {1, 2, 3, 4, 5};
String[] strArray = {"Hello", "World"};
// 自动推断类型
GenericMethod.printArray(intArray); // 输出: 1 2 3 4 5
GenericMethod.printArray(strArray); // 输出: Hello World
}
}
在这个例子中:
- 当调用
printArray(intArray)
时,编译器自动推断出T
是Integer
类型。 - 当调用
printArray(strArray)
时,编译器自动推断出T
是String
类型。
当然你也可以手动指定类型,不过在大多数情况下,Java会自动推断类型,手动指定是多余的。
数目可变的泛型方法参数
泛型方法支持使用"..."定义个数可变的参数,你可以定义一个接受可变数量参数的方法,并将这些参数视为数组。结合泛型,能够处理不同类型的可变参数。
可变参数的语法
可变参数使用三个点...
来表示,放在参数类型之后。它允许方法接受零个或多个参数,并将它们视为一个数组。
java
public class GenericVarargs {
// 可变参数的泛型方法
public static <T> void printElements(T... elements) {
for (T element : elements) {
System.out.print(element + " ");
}
System.out.println();
}
public static void main(String[] args) {
// 调用可变参数方法,传入不同类型的参数
printElements(1, 2, 3, 4, 5); // 输出: 1 2 3 4 5
printElements("Hello", "World"); // 输出: Hello World
printElements(1.1, 2.2, 3.3); // 输出: 1.1 2.2 3.3
}
}
-
泛型方法定义:
public static <T> void printElements(T... elements)
:这里T
是类型参数,elements
是一个可变参数,表示T
类型的数组。
-
调用方法:
- 可以直接传入多个参数,Java会自动将它们转换为数组。这个方法可以接受任意数量的参数(包括零个参数)。
-
输出结果:
- 在
main
方法中,调用printElements
方法传入不同类型的参数,显示了如何处理这些参数。
- 在
注意事项
-
可变参数只能在方法参数列表的最后一个位置:如果定义了多个参数,必须将可变参数放在最后。
-
性能考虑:虽然可变参数提供了灵活性,但在处理大量参数时,可能会产生数组的创建和销毁,因此在性能敏感的场景中要谨慎使用。
泛型类
定义了泛型参数的类成为泛型类,泛型参数可用于定义字段类型、方法参数类型和返回值类型。
泛型类的创建
java
//泛型参数为T
public class Pair<T> {
//用T定义字段类型
private T first;
private T second;
public Pair() {
first = null;
second = null;
}
//用T定义构造方法的参数类型
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
//用T定义函数返回值类型
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
//用T定义方法参数类型
public void setFirst(T newValue) {
first = newValue;
}
public void setSecond(T newValue) {
second = newValue;
}
}
泛型类的实例化
前面定义了泛型类Pair<T>,但在使用时,必须给T指定一个具体的类型(比如String)。"Pair<String>"就是"Pair<T>"的"实例化(instantiate )"。
java
class PairTest {
public static void main(String[] args) {
String[] strs = new String[]{"a", "b", "c"};
Pair<String> result = minmax(strs);
System.out.printf("Min:%1$s Max:%2$s\n",
result.getFirst(),
result.getSecond());
}
public static Pair<String> minmax(String[] a) {
if (a == null || a.length == 0) {
return null;
}
String min = a[0];
String max = a[0];
for (int i = 1; i < a.length; i++) {
if (min.compareTo(a[i]) > 0) {
min = a[i];
}
if (max.compareTo(a[i]) < 0) {
max = a[i];
}
}
return new Pair<String>(min, max);
}
}
从泛型类派生子类
从泛型基类派生子类时,应该给其指定一个具体的类型
如果MyClass是泛型类,在定义子类时不指定泛型参数,则 MyClass 的泛型参数默认为 Object 。
泛型使用须知
1.不能定义泛型化数组,如下这个语句是错误的:
Pair<String>[] table = new Pair<String>[10];
- 不能直接创建泛型类型的实例,这个语句也是错误的:
public class Pair<T> {
...
public Pair() {
first = new T();
second = new T();
}
...
}
- 泛型类型不能直接或间接继承自Throwable,这个语句还是错误的:
(Throwable 类是 Java 语言中所有错误或异常的超类,是对所有异常进行整合的一个普通类。它的作用就是能够提取保存在堆栈中的错误信息。)
public class Problem<T> extends Exception
- 我们无法抛出或捕获泛型类型的异常对象,这个语句是错误的:
catch (T e) {...}
- 不能定义静态泛型成员,以下代码将无法通过编译:
class MyClass<T> {
public static T value;
public static T f() { }
}
泛型标记符
当你设计自己的泛型类或泛型方法时,最好遵循以下惯例命名泛型参数:(不强制,属于一种技术规范)
- E - Element (在集合中使用,因为集合中存放的是元素)
- T - Type(Java 类)
- K - Key(键)
- V - Value(值)
- N - Number(数值类型)
- ? - 表示不确定的 java 类型(类型通配符)
泛型多态
在JDK中定义了大量的泛型类型(包容泛型类和泛型接口),并且这些类型之间存在着复杂的(基类)继承和(接口)实现关系。
▪ 基于泛型接口和抽象泛型类,在JDK中大量出现了基于泛型的"多态"代码,把握"泛型多态"特性,是用好它们的前提。
先回顾一下多态的两个特性:
- 父类变量,可以引用子类对象。
- 接口变量,可以引用实现了这一接口的类的实例。
JDK中泛型类型间的关联实例:
java
import java.util.ArrayList;
import java.util.List;
public class GenericPolymorphism {
public static void main(String[] args) {
List<Number> nums = new ArrayList<>();
nums.add(2);
nums.add(3.14);
for (Number number : nums) {
System.out.println(number.getClass().getName());
}
}
}
-
泛型限制 :指定了
List<Number>
,这意味着集合只能存储Number
及其子类的对象,不能存储其他非数字类型的对象,比如String
或Boolean
。这一点是通过泛型限制实现的。 -
多态性 :
Integer
和Double
是Number
的子类,通过多态性,Java允许将它们的实例存放在List<Number>
中。由于List<Number>
接收Number
类型的对象,任何继承自Number
的子类对象都可以存储进去。
但是这个多态性只是对于泛型所指的类型,而不是使用泛型的类本身。所以下面这个例子的语句是不合法的。
java
//定义一个类型为Integer的集合
List<Integer> ints = new ArrayList<Integer>();
ints.add(1);//装箱
ints.add(2);//装箱
//因为Integer派生自Number,所以尝试着将List<Integer>
//变量赋值给List<Number>变量
List<Number> nums = ints; // 出现编译时错误
即使Integer
是Number
的子类,但List<Integer>
和List<Number>
之间没有父子关系。换句话说,List<Integer>
不是List<Number>
的子类,也不能将List<Integer>
赋值给List<Number>
。
泛型约束
泛型约束(Generic Constraints)是在使用泛型时,通过限定泛型类型的范围,确保泛型可以操作特定类型及其子类型。Java中的泛型约束主要通过边界来实现,分为上界和下界。
上界约束
上界约束使用extends
关键字来限制泛型参数必须是某个类或者接口的子类或实现类。
java
class NumberBox<T extends Number> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
在这个例子中,T
必须是Number
类的子类(如Integer
、Double
、Float
等),因此你不能使用String
或其他非Number
类的对象来实例化NumberBox
。
java
public class Main {
public static void main(String[] args) {
NumberBox<Integer> intBox = new NumberBox<>();
intBox.setContent(123);
System.out.println(intBox.getContent()); // 输出: 123
NumberBox<Double> doubleBox = new NumberBox<>();
doubleBox.setContent(3.14);
System.out.println(doubleBox.getContent()); // 输出: 3.14
}
}
使用上界约束的好处
- 类型安全:确保泛型只能处理某个类或接口的子类,避免不必要的类型错误。
- 灵活性:你可以操作具有共同父类的对象,而不需要为每个具体类型单独写逻辑。
需要时,可以给泛型参数可以指定多个约束条件:
T extends Comparable & Serializable
多个约束条件中最多只能有一个是类,并且它必须放在第一位。
下界约束(下界通配符)
实际上,Java没有直接的下界约束 ,这与上界约束(T extends SomeClass
)不同。Java中的泛型定义不支持用T super SomeClass
这样的方式直接约束一个泛型类型必须是某个类的父类。
因为泛型中的上界约束常用于定义泛型类型的能力(如必须是某个类或接口的子类),而下界约束并没有类似的应用场景。下界的限制通常用于限制写入操作 ,而这在方法调用时已经可以通过下界通配符 (? super T
)来表达。
所以通过下界通配符来实现类似下界约束的效果类型通配符。
类型通配符
通配符约束(?
)用于表示未知的泛型类型,结合上界和下界,通配符可以在泛型代码中增加灵活性。必须使用?
来表示通配符,不能换成其他字母或符号。
无界通配符 <?>
无界通配符<?>
表示任意类型,适用于你不关心泛型参数类型的场景。例如,你可以使用无界通配符来读取或操作一个泛型集合,但无法向其中添加元素。
java
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
java
public class Main {
public static void main(String[] args) {
List<String> stringList = Arrays.asList("a", "b", "c");
List<Integer> intList = Arrays.asList(1, 2, 3);
printList(stringList); // 输出: a b c
printList(intList); // 输出: 1 2 3
}
}
注意事项:
- 你可以从
List<?>
中读取元素,但读取出来的元素类型是Object
。 - 你不能 向
List<?>
中添加元素,因为它可以接受任意类型,Java无法确保你添加的元素类型是安全的。
java
list.add(1); // 错误,不能添加任何元素
上界通配符 <? extends T>
上界通配符<? extends T>
表示某个类型的子类 或该类型本身。它限制了泛型参数的上界,即泛型参数必须是指定类型T
或它的子类。
java
public static void printNumbers(List<? extends Number> list) {
for (Number num : list) {
System.out.println(num);
}
}
只能读取,不能写入 :虽然你可以从List<? extends T>
中读取元素,但是你不能往里面添加元素(除了null
)。这是因为泛型参数可以是T
的任意子类,Java无法确定你要添加的元素是否与实际的泛型参数类型匹配。
java
public class Main {
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
printNumbers(intList); // 输出: 1 2 3
printNumbers(doubleList); // 输出: 1.1 2.2 3.3
}
}
java
list.add(new Integer(10)); // 错误,不能添加元素
适用情况:只读取泛型集合中的元素时,适合用上界通配符,因为你只需要知道元素的类型是某个类型的子类。并且读取到的类型是T
或其父类(通常是T
),这保证了类型安全。
下界通配符<? super T>
下界通配符<? super T>
表示某个类型的父类 或该类型本身。它限制了泛型参数的下界,即泛型参数必须是T
或T
的父类。下界通配符允许你向泛型中写入T
类型的对象或T
的子类对象。
java
public static void addNumbers(List<? super Integer> list) {
list.add(1); // 允许添加 Integer 类型的对象
list.add(2);
}
可以写入,有限制地读取 :你可以向List<? super T>
中添加T
类型或其子类的元素,但读取时只能得到Object
类型,因为列表中可能存储的是T
的父类对象。
java
public class Main {
public static void main(String[] args) {
List<Number> numList = new ArrayList<>();
addNumbers(numList);
List<Object> objList = new ArrayList<>();
addNumbers(objList);
System.out.println(numList); // 输出: [1, 2]
System.out.println(objList); // 输出: [1, 2]
}
}
在这里,? super Integer
表示list
接受Integer
类型或它的父类(如Number
、Object
),这意味着你可以向列表中添加Integer
及其子类,但不能向其中添加Double
或其他不相关的类型。
java
list.add(new Integer(10)); // 正确,允许添加 Integer
Object obj = list.get(0); // 返回 Object 类型
适用情况:当你需要向泛型集合中添加元素,并且关心集合可以接受某个类型的元素时,可以使用<? super T>
。因为它允许你向集合中写入元素,并保证类型安全。
上界通配符和上界约束的区别
上界通配符和上界约束从功能到写法上都很相似。但是两者其实不一样。
上界通配符(? extends T
)
上界通配符 是指在泛型方法或泛型类中,用? extends T
来表示某种未知类型 ,但这个类型必须是T
或T
的子类。
java
public static void printList(List<? extends Number> list) {
for (Number number : list) {
System.out.println(number);
}
}
在这个例子中,List<? extends Number>
表示可以接受任何Number
类型的子类,例如List<Integer>
或List<Double>
。通配符?
表示一个未知的类型 ,但我们知道这个类型是Number
或其子类。
- 主要作用 :允许使用
T
的所有子类,通常用于读取操作,因为我们不知道具体的类型,所以无法向列表中写入。 - 使用场景 :当我们关心的是泛型的读取行为,而不需要向其中添加元素时,使用上界通配符非常合适。
java
List<Integer> integers = Arrays.asList(1, 2, 3);
printList(integers); // 可以接受 List<Integer>
你可以从List<? extends T>
中读取元素,并且它们的类型至少是T
(比如Number
),但你不能 往其中添加新元素(除了null
),因为实际的类型可能是T
的子类。
上界约束 (T extends SomeClass
)
上界约束 是用于定义泛型类型时的约束条件,表示泛型类型参数必须是某个类的子类 或实现某个接口。通过T extends SomeClass
来限制泛型类型的范围。
java
class Box<T extends Number> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
在这个例子中,T extends Number
表示泛型参数T
必须是Number
类或其子类。这样做的好处是你可以保证T
一定具有Number
类的特性和方法(如doubleValue()
等)。
- 主要作用 :限制泛型类型只能是某个类及其子类,确保类型安全,可以对泛型进行读写操作。
- 使用场景:在设计一个泛型类或泛型方法时,想要确保类型参数具有某些特定的属性或行为(比如必须是某个类的子类)时使用上界约束。
java
Box<Integer> intBox = new Box<>();
intBox.setContent(123);
System.out.println(intBox.getContent()); // 输出: 123
上界约束用于定义泛型类型参数,而不是像上界通配符那样操作方法的参数或返回值。它明确了泛型参数的范围,并且允许你对这个泛型参数执行读写操作,因为你已经清楚地知道它的类型。
对比:上界通配符 vs 上界约束
特性 | 上界通配符(? extends T) | 上界约束(T extends SomeClass) |
---|---|---|
定义位置 | 用于方法参数或泛型类型中,用? extends T 表示 |
用于泛型类型定义时,用T extends SomeClass 表示 |
作用 | 限制操作的参数类型为T 或T 的子类,但泛型类型未知;主要用于读取操作 |
限制泛型参数必须是SomeClass 的子类,适用于泛型类或泛型方法的定义 |
典型用法 | 允许读取 泛型集合中的数据,但不能向其中添加数据(除null ) |
泛型类型的定义,允许对泛型进行读写操作 |
限制 | 只能从集合中读取类型为T 的元素,不能往集合中添加元素 |
泛型参数只能是T 或T 的子类,读写操作均可 |
示例 | List<? extends Number> 可以是List<Integer> 或List<Double> |
class Box<T extends Number> 限制T 只能是Number 的子类 |
使用场景分析
-
上界通配符 : 主要用于限制泛型的输入范围,在需要处理不确定类型但希望保证它是某个类或接口的子类时使用。它更适合只读的场景。
-
上界约束 : 适合用在定义类或方法时,当你需要确保泛型类型具有某些能力(比如实现某个接口或继承某个类)时使用。它适合既需要读取 又需要写入的场景,能够在泛型参数上进行更多操作。
上界通配符是用于泛型方法的参数或返回值中的灵活处理 ,而上界约束则是用于泛型类型定义中的类型限制。
泛型擦除
前面的内容都是在代码编写过程的操作,实际在Java文件编译时,会对泛型类型进行"擦除"。
因为Java虚拟机不直接支持泛型。
- Java泛型的类型擦除 是在编译时将泛型类型参数替换为其上界类型(如果未指定则为
Object
),并在需要的地方插入类型转换,以确保运行时与非泛型代码的兼容性。 - 擦除机制使得泛型在编译时是类型安全的,但在运行时类型信息会被移除,因此无法进行某些运行时操作(如类型检查和泛型数组创建)。
- 类型擦除的设计主要是为了保持Java的向后兼容,同时提供编译时的类型检查和安全性。