
JavaSE(四)泛型程序设计
一、泛型的基本概念
泛型是 Java 中的一种特性,它允许在定义类、接口和方法时使用类型参数(type parameter),这些类型参数在使用时(即创建对象或调用方法时)被具体的类型(如 Integer、String 等)替换。泛型的主要目的是在编译时提供类型安全检查,避免运行时出现 ClassCastException 异常,同时减少代码中的强制类型转换。
例如,在没有泛型的情况下,使用 ArrayList 存储不同类型的对象,需要进行类型转换:
而使用泛型后:
二、泛型类
泛型类是带有类型参数的类。定义泛型类的语法是在类名后面加上 <T>(T 是类型参数,也可以用其他标识符,如 E 表示元素,K 表示键,V 表示值等)。
示例:
class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
使用泛型类时,指定具体的类型:
Box<String> stringBox = new Box<>();
//因为现在有了类型变量,在使用时同样需要跟上<>并在其中填写明确要使用的类型
//这样我们就可以根据不同的类型进行选择了
stringBox.setContent("Java泛型");
String content = stringBox.getContent();
Box<Integer> integerBox = new Box<>();
integerBox.setContent(123);
Integer num = integerBox.getContent();
只不过这里需要注意一下,我们在方法中使用待确定类型的变量时,因为此时并不明确具体是什么类型,那么默认会认为这个变量是一个Object类型的变量,因为无论具体类型是什么,一定是Object类的子类:

三、泛型接口
泛型接口与泛型类类似,在接口名后加上类型参数。
示例:
实现泛型接口时,可以指定具体类型:
public class Main {
public static void main(String[] args) {
A a = new A();
Integer i = a.test();
}
static class A implements Study<Integer> {
//在实现接口或是继承父类时,如果子类是一个普通类,那么可以直接明确对应类型
@Override
public Integer test() {
return null;
}
}
}
也可以继续使用泛型:
public class Main {
public static void main(String[] args) {
A<String> a = new A<>();
String i = a.test();
}
static class A<T> implements Study<T> {
//让子类继续为一个泛型类,那么可以不用明确
@Override
public T test() {
return null;
}
}
}
继承也是同样的:
四、泛型方法
泛型方法是在方法中使用类型参数,它可以在普通类或泛型类中定义。泛型方法的类型参数声明在方法的返回值类型之前。
示例:
class ArrayUtils {
public static <T> T getMiddleElement(T[] array) {
if (array == null || array.length == 0) {
return null;
}
return array[array.length / 2];
}
}
调用泛型方法:
Integer[] intArray = {1, 2, 3, 4, 5};
Integer middleInt = ArrayUtils.getMiddleElement(intArray);
String[] strArray = {"a", "b", "c"};
String middleStr = ArrayUtils.getMiddleElement(strArray);
五、类型通配符
在泛型中,有时需要处理不确定的类型,这时候就用到类型通配符。类型通配符用 ? 表示。
- 无界通配符(
?):表示未知类型,可以匹配任何类型。例如List<?>可以表示List<String>、List<Integer>等各种泛型列表。 - 上界通配符(
? extends T):表示未知类型,该类型必须是T或T的子类。例如List<? extends Number>可以匹配List<Integer>、List<Double>等,因为Integer和Double都是Number的子类。 - 下界通配符(
? super T):表示未知类型,该类型必须是T或T的父类。例如List<? super Integer>可以匹配List<Number>、List<Object>等,因为Number是Integer的父类,Object是Number的父类。
示例(上界通配符):
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number num : list) {
sum += num.doubleValue();
}
return sum;
}
可以传入 List<Integer>、List<Double> 等:
List<Integer> intList = Arrays.asList(1, 2, 3);
double sumInt = sumOfList(intList);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
double sumDouble = sumOfList(doubleList);
六、泛型的擦除
Java 中的泛型是伪泛型,在编译期间,泛型的类型信息会被擦除,这就是泛型擦除(Type Erasure)。也就是说,编译后的字节码中不包含泛型的类型参数信息,泛型类、泛型接口和泛型方法都会被擦除为原始类型(Raw Type)。例如,ArrayList<String> 在运行时会被擦除为 ArrayList,类型参数 String 相关的信息会被去掉。
泛型擦除会带来一些限制,比如不能直接创建泛型数组(new T[] 是不允许的),不能在静态方法或静态变量中使用类的泛型参数等。
实际上在Java中并不是真的有泛型类型(为了兼容之前的Java版本)因为所有的对象都是属于一个普通的类型,一个泛型类型编译之后,实际上会直接使用默认的类型:
当然,如果我们给类型变量设定了上界,那么会从默认类型变成上界定义的类型:
那么编译之后:
因此,泛型其实仅仅是在编译阶段进行类型检查,当程序在运行时,并不会真的去检查对应类型,所以说哪怕是我们不去指定类型也可以直接使用:
public static void main(String[] args) {
Test test = new Test(); //对于泛型类Test,不指定具体类型也是可以的,默认就是原始类型
}
只不过此时编译器会给出警告,同样的,由于类型擦除,实际上我们在使用时,编译后的代码是进行了强制类型转换的:
public static void main(String[] args) {
A<String> a = new B();
String i = a.test("10"); //因为类型A只有返回值为原始类型Object的方法
}
不过,我们思考一个问题,既然继承泛型类之后可以明确具体类型,那么为什么@Override不会出现错误呢?我们前面说了,重写的条件是需要和父类的返回值类型和形参一致,而泛型默认的原始类型是Object类型,子类明确后变为其他类型,这显然不满足重写的条件,但是为什么依然能编译通过呢?
我们来看看编译之后长啥样:
// Compiled from "B.java"
public class com.test.entity.B extends com.test.entity.A<java.lang.String> {
public com.test.entity.B();
java.lang.String test(java.lang.String);
java.lang.Object test(java.lang.Object); //桥接方法,这才是真正重写的方法,但是使用时会调用上面的方法
}
通过反编译进行观察,实际上是编译器帮助我们生成了一个桥接方法用于支持重写:
public class B extends A {
public Object test(Object obj) { //这才是重写的桥接方法
return this.test((Integer) obj); //桥接方法调用我们自己写的方法
}
public String test(String str) { //我们自己写的方法
return null;
}
}
类型擦除机制其实就是为了方便使用后面集合类(不然每次都要强制类型转换)同时为了向下兼容采取的方案。因此,泛型的使用会有一些限制:
首先,在进行类型判断时,不允许使用泛型,只能使用原始类型:
public static void main(String[] args) {
Test<String> test = new Test();
System.out.println(test instanceof Test<String>);
}
只能判断是不是原始类型,里面的具体类型是不支持的:
Test<String> test = new Test<>();
System.out.println(test instanceof Test); //在进行类型判断时,不允许使用泛型,只能使用原始类型
还有,泛型类型是不支持创建参数化类型数组的,要用只能用原始类型:
public static void main(String[] args) {
Test[] test = new Test[10]; //同样是因为类型擦除导致的,运行时可不会去检查具体类型是什么
}
只不过只是把它当做泛型类型的数组还是可以用的。
七、泛型的限制
- 不能使用基本类型作为泛型参数,必须使用对应的包装类,如
Integer而不是int。 - 运行时类型查询只适用于原始类型,无法查询泛型的具体类型参数,例如
list instanceof ArrayList<String>是不合法的,只能写成list instanceof ArrayList。 - 泛型类的静态成员不能使用类的泛型参数,因为静态成员在类加载时就已经存在,而泛型参数是在创建对象时才确定的。
- 不能创建泛型数组,如
T[] array = new T[10]是不允许的。