泛型程序设计入门
大约 8 分钟
Java SE 5.0 增加泛型机制的主要原因是为了满足 1999年制定的 JSR 14 规范。专家组花费了5年左右的时间用来定义规范和测试实现。

泛型的诞生
泛型诞生之前
在没有泛型之前,从集合中读取到的每一个对象都必须进行转换。如果不小心插入了类型错误的对象,在运行时的转换处理就会出错。
java
package com.csthink.generic;
import java.util.ArrayList;
/**
* @author Mars
* @since 2024-01-04
* <p>
* 该类用于演示泛型编程出现之前使用集合类可能遇到的问题,注意:以下程序运行后会报错
* <p>
* {@throws ClassCastException class java.lang.Integer cannot be cast to class java.lang.String}
*/
public class CollectionWithoutGeneric {
public static void main(String[] args) {
ArrayList list = new ArrayList();
// 问题1:这里没有错误检查,可以向数组列表中添加任何类型对象
list.add("hello");
list.add(1);
// 运行这段没有问题
System.out.println(list);
// 问题2: 获取值时必须进行强制类型转换
String s1 = (String) list.get(0);
// 问题3:如果将 get 的结果强制类型转换为 String 类型,就会产生一个错误,但是编译阶段不会报错
String s2 = (String) list.get(1);
}
}

泛型诞生之后
有了泛型之后,泛型提供了 "类型参数(type parameters)", 本例中 ArrayList 类有一个类型参数用来指示元素的类型,这样集合中读取对象就不用进行转换,而且还能在编译期提起发现类型错误的对象。
java
package com.csthink.generic;
import java.util.ArrayList;
/**
* @author Mars
* @since 2024-01-04
* <p>
* 该类用于演示泛型编程出现之后,集合类中可以定义集合中对象的类型,这样在编译期就能提前进行错误检查
*/
public class CollectionWithGeneric {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("hello");
// 编译器可以进行检查,避免插入错误类型的对象, 下面这段语句是无法通过编译的
//list.add(1);
// 编译器可以利用类型参数这个信息,调用 get 时不需要进行强制类型转换,编译器就能知道返回值类型为 String
String s1 = list.get(0);
}
}
提示
Java SE 7 及以后的版本中,构造函数中可以省略泛型类型:
ArrayList<String> list = new ArrayList<>();
省略的类型可以从变量的类型推断得出。
- 泛型解决了容器类型在编译时安全检查的问题
- 类型参数使得程序具有更好的可读性和安全性
使用泛型的三个层次
- Level 1: 仅仅会使用JDK 提供的泛型类,像 ArrayList 这样的集合,不必考虑它们的工作方式与原理
- Level 2: 不同的泛型类混合在一起使用时,或者对类型参数一无所知的遗留代码进行衔接时,遇到错误时能解决问题,而不是瞎猜
- Level 3: 根据业务需要能实现自定义泛型类与泛型方法
泛型类 generic class
一个简单的泛型类
java
package com.csthink.generic;
/**
* @author Mars
* @since 2024-01-04
* <p>
* 定义一个简单的泛型类
*/
public class SimpleGenericClassExample<T> {
/**
* 成员变量的类型为 T
*/
private T field;
public SimpleGenericClassExample() {
}
public SimpleGenericClassExample(T field) {
this.field = field;
}
public T getField() {
return field;
}
public void setField(T field) {
this.field = field;
}
}
java
package com.csthink.generic;
/**
* @author Mars
* @since 2024-01-04
*
* 演示泛型类的使用
*/
public class SimpleGenericClassExampleTest {
public static void main(String[] args) {
SimpleGenericClassExample<String> stringExample = new SimpleGenericClassExample<>("abc");
SimpleGenericClassExample<Integer> integerExample = new SimpleGenericClassExample<>(123);
SimpleGenericClassExample<Number> numberExample = new SimpleGenericClassExample<>(234);
System.out.println(stringExample.getField().getClass().getName());
System.out.println(integerExample.getField().getClass().getName());
// 泛型类的类型约束只在编译期有效,以下二者打印的结果均是 com.csthink.generic.SimpleGenericClassExample
System.out.println(stringExample.getClass().getName());
System.out.println(integerExample.getClass().getName());
// sum result: 223
//handleField(integerExample);
//handleField(numberExample);
}
//private static void handleField(SimpleGenericClassExample<?> simpleGenericClassExample) {
// int result = 100 + (Integer) simpleGenericClassExample.getField();
// System.out.println("sum result: " + result);
//}
}

泛型相关的信息编译之后会被擦除不会保留到运行时阶段
手动编译成 class 字节码文件后,通过 Idea 查看会发现和泛型相关的信息都被擦除了,印证了同一个泛型类的不同实例的类类型都是同一个泛型类 com.csthink.generic.SimpleGenericClassExample ,说明泛型类的类型约束只在编译期有效`。

小结
- 泛型类可以看作普通类的工厂,通过传递不同的类型变量可以得出不同类型的类
- 通过打印泛型类的成员变量可以看出成员变量的类型取决于外界传入的类型变量的类型
- 同一个泛型类的不同实例的类类型都是同一个泛型类
com.csthink.generic.SimpleGenericClassExample,说明泛型类的类型约束只在编译期有效
泛型方法 generic method
一个简答的泛型方法使用
- 普通类中可以声明泛型方法
java
package com.csthink.generic;
/**
* @author Mars
* @since 2024-01-04
*
* <p>
* 带有泛型方法的普通类
*/
public class NormalClassWithGenericMethodExample {
public static <E> void print(E[] array) {
for (E e : array) {
System.out.printf("%s ", e);
System.out.print(" ");
}
System.out.println();
}
}
- 泛型类中声明的泛型方法,泛型方法的泛型标识符可以泛型类的标识符一样
java
package com.csthink.generic;
/**
* @author Mars
* @since 2024-01-04
*
* <p>
* 带有泛型方法的泛型类
*/
public class GenericClassWithGenericMethodExample<T> {
private T field;
/**
* 使用了泛型标识符<E>, 这是一个泛型方法,泛型标识符也可以和泛型类的标识符一样
*/
public static <E> void print(E[] array) {
for (E e : array) {
System.out.printf("%s ", e);
System.out.print(" ");
}
System.out.println();
}
/**
* 这只是一个普通的成员方法
*/
public T doSth(T target) {
return target;
}
}
java
package com.csthink.generic;
/**
* @author Mars
* @since 2024-01-04
*
* <p>
* 演示泛型方法的使用
*/
public class GenericMethodExampleTest {
public static void main(String[] args) {
Double[] doubleArray = {1.1, 2.2, 3.3, 4.4, 5.5};
Character[] charArray = {'A', 'B', 'C', 'D', 'E', 'F'};
NormalClassWithGenericMethodExample.print(doubleArray);
//GenericClassWithGenericMethodExample<String> stringExample = new GenericClassWithGenericMethodExample<>();
// 方法调用中可以省略<Character> 类型参数,编译器可以用 charArray 推断出 E 是 Character
// GenericClassWithGenericMethodExample.<Character>print(charArray);
GenericClassWithGenericMethodExample.print(charArray);
//stringExample.print(doubleArray);
}
}

小结
- 泛型方法的声明放在修饰符 (比如 public static) 的后面,返回类型的前面

- 泛型方法可以定义在普通类中,也可以定义在泛型类中
- 泛型方法的泛型标识符可以泛型类的标识符一样
类型变量 (type variable)
提示
泛型标识符也叫类型变量通常使用较短的大写字母
常用作类型变量的泛型字母的含义
- E - Element: 在集合中使用,因为集合中存放的是元素
- T - Type: 表示"任意类型"
- K - Key: 关键字
- V - Value: 值
- N - Number: 数值类型
- 需要时还可以使用临近的字母 U 和 S,表示 "任意类型"
类型变量的限定
有时候,类或方法需要对类型变量加以约束,我们不能在泛型里面使用具备继承关系的类
java
/**
* @author Mars
* @since 2024-01-04
*
* 研究泛型中类型变量的限定,即泛型的上下限
*/
public class GenericAndInheritanceExampleTest<T> {
public static void main(String[] args) {
// Number 是 Integer 的父类
SimpleGenericClassExample<Number> integerExample = new SimpleGenericClassExample<>(123);
// 编译不通过
//handleField1(integerExample);
}
public static void handleField1(SimpleGenericClassExample<Integer> simpleGenericClassExample) {
int result = 100 + (Integer) simpleGenericClassExample.getField();
System.out.println("result: " + result);
}
}
解决方法
java
package com.csthink.generic;
/**
* @author Mars
* @since 2024-01-04
*
* 研究泛型中类型变量的限定,即泛型的上下限
*/
public class GenericAndInheritanceExampleTest<T> {
public static void main(String[] args) {
// Number 是 Integer 的父类
SimpleGenericClassExample<Number> integerExample = new SimpleGenericClassExample<>(123);
// 编译不通过
//handleField1(integerExample);
// 使用通配符 ? ,编译能通过,但是会使得泛型的类型检查失去意义,不推荐使用,这样有违使用泛型的意义
handleField2(integerExample);
// 设置泛型的上边界,编译能通过,限定上限为 Number, Number 的所有子类都可以,最高的类型是 Number
handleField3(integerExample);
// 设置泛型的下边界,编译能通过,限定下限为 Integer, Integer 的所有父类都可以,最低类型只能是 Integer
handleField4(integerExample);
}
public static void handleField1(SimpleGenericClassExample<Integer> simpleGenericClassExample) {
int result = 100 + (Integer) simpleGenericClassExample.getField();
System.out.println("result: " + result);
}
/**
* 使用通配符 ? 来解决泛型里面不能使用继承关系的类
*/
public static void handleField2(SimpleGenericClassExample<?> simpleGenericClassExample) {
int result = 100 + (Integer) simpleGenericClassExample.getField();
System.out.println("result: " + result);
}
/**
* 给泛型加上上边界 ? extends E,来解决泛型里面不能使用继承关系的类
*/
public static void handleField3(SimpleGenericClassExample<? super Number> simpleGenericClassExample) {
int result = 100 + (Integer) simpleGenericClassExample.getField();
System.out.println("result: " + result);
}
/**
* 给泛型加上下边界 ? super E,来解决泛型里面不能使用继承关系的类
*/
public static void handleField4(SimpleGenericClassExample<? super Integer> simpleGenericClassExample) {
int result = 100 + (Integer) simpleGenericClassExample.getField();
System.out.println("result: " + result);
}
}

小结
- 使用通配符 ?,但是会使得泛型的类型检查失去意义,不推荐使用,这样有违使用泛型的意义
- 给泛型加上上边界 ? extends E,注意上边界不要设置的过大,比如 ? extends Object 这样就没法在编译期发现类型错误了
- 给泛型加上下边界 ? super E
小结
- 定义泛型时,对应的数据类型是不确定的
- 泛型方法被调用时,会指定具体类型
- 泛型的参数不支持基本类型,不能使用基本类型实例化类型参数\
- 类型擦除:虚拟机没有泛型类型对象----所有对象都属于普通类。泛型相关的信息编译之后会被擦除不会保留到运行时阶段