泛型
1. 定义泛型
泛型程序设计意味着编写的代码可以对多种不同类型的对象重用。
所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。
1.1 泛型类
泛型类就是在类头上有一个或多个类型参数的类。类型参数在整个类定义中用于:
- 指定方法的返回类型;
- 字段的类型;
- 局部变量的类型。
以下为一个Pair
泛型类的示例,使用Pair<T>
时,就可以为T
形参传入实际类型,这样就可以生成如Pair<String>, Pair<Double>
等形式的多个逻辑子类了(物理上并不存在):
@Data
public class Pair<T> {
private T first;
private T second;
public Pair() {
first = null;
second = null;
}
// 当创建带泛型声明的自定义类并为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
}
对于泛型变量,常见的做法是类型参数使用大写字母,而且很简短。Java 库中,使用变量 E
表示集合的元素类型,K
和 V
分别表示键和值的类型,T
(必要时还可以使用相邻字母U
和S
)表示“任意类型”。
1.2 泛型方法
除了定义一个泛型类,还可以定义一个带有类型参数的泛型方法。泛型方法可以在普通类中定义,也可以在泛型类中定义。
定义泛型方法时,类型变量放在返回类型的前面、修饰符的后面,如下:
class ArrayAlg {
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
}
当调用一个泛型方法时,可以把具体类型包围在尖括号中,放在方法名前面:
String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public");
不过,大多数情况下的方法调用中可以省略泛型参数(上面的<String>
),因为编译器有足够的信息推断出你想要的方法。即,上述可以简单地调用:
String middle = ArrayAlg.getMiddle("John", "Q.", "Public");
与接口、类声明中定义的泛型不同的是:
- 方法声明中定义的泛型只能在该方法里使用,而接口、类声明中定义的泛型则可以在整个接口、类中使用;
- 方法中的泛型参数无须显式传入实际类型参数,因为编译器可以根据实参推断出泛型所代表的类型,它常推断出最直接的类型。
1.3 泛型构造器
正如泛型方法允许在方法签名中声明泛型形参一样,Java也允许在构造器签名中声明泛型形参,这样就产生了所谓的泛型构造器。
在调用泛型构造器时,不仅可以让Java根据实际参数的类型来“推断”泛型形参的类型,也可以显式地为构造器中的泛型形参指定实际的类型。如下:
class Foo {
public <T> Foo(T t) {
System.out.println(t);
}
}
class GenericConstructor {
public static void main(String[] args) {
// 由程序推断泛型形参的类型
new Foo("Crazy Java.");
new Foo(200);
// 显示指定泛型构造器中的T类型
new <String>Foo("Crazy Java.");
new <String>Foo(12.3); // 代码出错
}
}
Java7新增的“菱形”语法允许在调用构造器时在构造器后使用一对尖括号来代表泛型信息,但如果程序显式指定了泛型构造器中声明的泛型形参的实际类型,则不可以使用“菱形”语法。如下:
class MyClass<E> {
public <T> MyClass(T t) {
System.out.println(t);
}
}
class GenericDiamondTest {
public static void main(String[] args) {
// MyClass类声明中的E形参是String类型
// 泛型构造器中声明的T形参是Integer类型
MyClass<String> mc1 = new MyClass<>(5);
// 显式指定泛型构造器中声明的T形参是Integer类型
MyClass<String> mc2 = new <Integer>MyClass<String>(5);
// 在显式指定了泛型构造器中声明的T形参是Integer类型之后,不能使用菱形语法
// 以下代码错误
MyClass<String> mc3 = new <Integer>MyClass<>(5);
}
}
2. 泛型代码和虚拟机
对虚拟机而言,虚拟机没有泛型类型对象——所有对象都属于普通类。
2.1 类型擦除
Java语言的泛型实现方式是擦除法(Type Erasure)。
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型,即去掉类型参数后的泛型类型名,而类型参数会被擦除,并替换为其限定类型。
示例1
假设我们编写了一个泛型类Pair<T>
。
泛型定义:
源代码定义如下:
public class Pair<T> { private T first; private T last; public Pair(T first, T last) { this.first = first; this.last = last; } public T getFirst() { return first; } public T getLast() { return last; } }
经过编译器编译之后,在 JVM 中执行的代码实际变成了这样(也就是说 JVM 根本不知道泛型的存在,泛型信息在编译器中走了一遭后被擦除了):
public class Pair { private Object first; private Object last; public Pair(Object first, Object last) { this.first = first; this.last = last; } public Object getFirst() { return first; } public Object getLast() { return last; } }
泛型执行:
在使用泛型的时候,对于下述源代码:
Pair<String> p = new Pair<>("Hello", "world"); String first = p.getFirst(); String last = p.getLast();
编译之后的代码变成了这样:
Pair p = new Pair("Hello", "world"); String first = (String) p.getFirst(); String last = (String) p.getLast();
所以,Java的泛型其实是一种编译期假象,编译器内部永远把所有类型T
视为Object
处理,只是在需要转型的时候,编译器会根据T
的类型自动为我们实行安全的类型强转,在运行期是不存在泛型的。
这种擦除机制也意味着泛型类的所有实例在运行时具有相同的运行时类,例如Pair<String>
和Pair<LocalDate>
在擦除后都会变成原始的Pair
类型,并不存在所谓的“泛型类”。
示例2
原始类型用第一个限定来替换类型变量,或者,如果没有给定限定,就替换为Object
。
假定我们声明了一个稍有不同的类型:
public class Interval<T extends Comparable & Serializable> implemnts Serializable {
private T lower;
private T upper;
// ...
public Interval(T first, T second) {
if (first.compareTo(second) <= 0) {
// ...
} else {
// ...
}
}
}
原始类型Interval
如下所示:
public class Interval implemnts Serializable {
private Comparable lower;
private Comparable upper;
// ...
public Interval(Comparable first, Comparable second) {
// ...
}
}
而,如果Interval
的限定类型切换为class Interval<T extends Serializable & Comparable>
,原始类型会用Serializable
替换T
,而编译器在必要时要向Comparable
插入强制类型转换。因此,为了提高效率,在存在多个限定类型时要考虑一下它们的声明顺序。
2.2 转换泛型表达式
编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。例如:
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
getFirst
擦除类型后的返回类型是Object
,编译器会自动插入转换到Employee
的强制类型转换。也就是说,编译器把这个方法调用转换为两条虚拟机指令:
- 对原始方法
Pair.getFirst
的调用; - 将返回的
Object
类型强制转换为Employee
类型。
类似地,当访问一个泛型字段时,也要插入强制类型转换,如Employee buddy = buddies.first
(假设字段是public
的)。
2.3 转换泛型方法
类型擦除也会出现在泛型方法中。对于如下的泛型方法:
public static <T extends Comparable> T min(T[] a)
不能认为它是整个一组方法,实际上类型擦除之后,它只剩下一个方法:
public static Comparable min(Comparable[] a)
方法的擦除带来了一个复杂的问题——Java会合成*桥方法(bridge method)*来保持多态。其发生在继承中。考虑继承了Pair<T>
的如下DateInterval
类:
class DateInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {
if (second.compareTo(getFirst()) >= 0) {
super.setSecond(second);
}
}
// ...
}
这个DateInterval
类擦除后,变成:
class DateInterval extends Pair { // after erasure
public void setSecond(LocalDate second) {
// ...
}
}
然而,与此同时,事实上DateInterval
还有另一个从Pair
继承的setSecond
方法,即public void setSecond(Object second)
,这显然是一个不同的方法。考虑如下执行语句:
var interval = new DateInterval(...);
Pair<LocalDate> pair = interval; // OK--assignment to superclass
pair.setSecond(aDate);
上述语句中我们希望setSecond
调用具有多态性,会调用最合适的那个方法,由于pair
引用一个DateInterval
对象,所以应该调用DateInterval.setSecond
。问题在于,类型擦除与多态发生了冲突。为了解决这个问题,编译器在DateInterval
类中生成了一个桥方法:
public void setSecond(Object second) {
setSecond((LocalDate) second);
}
桥方法可能会变得很奇怪。假设DateInterval
类也覆盖了getSecond
方法:
class DateInterval extends Pair<LocalDate> {
public LocalDate getSecond() {
return (LocalDate) super.getSecond();
}
}
显然,会导致在DateInterval
类中有两个getSecond
方法:
LocalDate getSecond(); // defined in DateInterval
Object getSecond(); // overrides the method defined in Pair to call the first method
显然,当你自己编写代码时,不可能写出这样的Java代码(因为其方法签名相同,编译器不会通过)。但是,在虚拟机中,会由参数类型和返回类型共同指定一个方法。因此,编译器可以为两个仅返回类型不同的方法生成字节码,虚拟机能够正确地处理这种情况!
2.4 泛型继承
一个类可以继承自一个泛型类。例如,父类的类型是Pair<Integer>
,子类的类型是IntPair
,可以这么继承:
public class IntPair extends Pair<Integer> {
}
使用的时候,因为子类IntPair
并没有泛型类型,所以,正常使用即可:
IntPair ip = new IntPair(1, 2);
前面讲了,我们无法获取Pair<T>
的T
类型,即给定一个变量Pair<Integer> p
,无法从p
中获取到Integer
类型。
但是,在父类是泛型类型的情况下,编译器就必须把类型T
(对IntPair
来说,也就是Integer
类型)保存到子类的class文件中,不然编译器就不知道IntPair
只能存取Integer
这种类型。
在继承了泛型类型的情况下,子类可以获取父类的泛型类型。例如:IntPair
可以获取到父类的泛型类型Integer
。获取父类的泛型类型代码比较复杂:
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
public class Main {
public static void main(String[] args) {
Class<IntPair> clazz = IntPair.class;
Type t = clazz.getGenericSuperclass();
if (t instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) t;
Type[] types = pt.getActualTypeArguments(); // 可能有多个泛型类型
Type firstType = types[0]; // 取第一个泛型类型
Class<?> typeClass = (Class<?>) firstType;
System.out.println(typeClass); // Integer
}
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
class IntPair extends Pair<Integer> {
public IntPair(Integer first, Integer last) {
super(first, last);
}
}
因为Java引入了泛型,所以,只用Class
来标识类型已经不够了。实际上,Java的类型系统结构如下:
┌────┐
│Type│
└────┘
▲
│
┌────────────┬────────┴─────────┬───────────────┐
│ │ │ │
┌─────┐┌─────────────────┐┌────────────────┐┌────────────┐
│Class││ParameterizedType││GenericArrayType││WildcardType│
└─────┘└─────────────────┘└────────────────┘└────────────┘
向上转型
在Java标准库中的ArrayList<T>
实现了List<T>
接口,它可以向上转型为List<T>
:
public class ArrayList<T> implements List<T> {
...
}
// 向上转型成功
List<String> list = new ArrayList<String>();
即类型ArrayList<T>
可以向上转型为List<T>
。
然而,要特别注意的是:不能把ArrayList<Integer>
向上转型为ArrayList<Number>
或List<Number>
,这是为什么呢?
假设ArrayList<Integer>
可以向上转型为ArrayList<Number>
,那么观察以下代码:
// 创建ArrayList<Integer>类型:
ArrayList<Integer> integerList = new ArrayList<Integer>();
// 添加一个Integer:
integerList.add(new Integer(123));
// “向上转型”为ArrayList<Number>:
ArrayList<Number> numberList = integerList;
// 添加一个Float,因为Float也是Number:
numberList.add(new Float(12.34));
// 从ArrayList<Integer>获取索引为1的元素(即添加的Float):
Integer n = integerList.get(1); // ClassCastException!
我们把一个ArrayList<Integer>
转型为ArrayList<Number>
类型后,这个ArrayList<Number>
就可以接受Float
类型,因为Float
是Number
的子类。但是,ArrayList<Number>
实际上和ArrayList<Integer>
是同一个对象,也就是ArrayList<Integer>
类型,它不可能接受Float
类型, 所以在获取Integer
的时候将产生ClassCastException
。
因此,编译器为了避免这种错误,根本就不允许把ArrayList<Integer>
转型为ArrayList<Number>
,也即ArrayList<Integer>
和ArrayList<Number>
两者完全没有继承关系。
总结而言,可以把ArrayList<Integer>
向上转型为List<Integer>
(T
不能变!),但不能把ArrayList<Integer>
向上转型为ArrayList<Number>
(T
不能变成父类)。
3. Java泛型的局限
了解了Java用擦除法来实现泛型的方式,我们就知道了Java泛型的局限。
3.1 不能用基本类型实例化类型参数
<T>
不能是基本类型,例如int
,因为实际类型是Object
,Object
类型无法持有基本类型:
Pair<int> p = new Pair<>(1, 2); // compile error!
3.2 运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型,因此所有的类型查询只产生原始类型。
if (a instanceof Pair<String>) {} // ERROR
if (a instanceof Pair<T>) {} // ERROR
Pair<String> p = (Pair<String>) a; // warning--can only test that a is a Pair
Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if (stringPair.getClass() == employeePair.getClass()) {} // true--they are equal
3.3 不能创建参数化类型的数组
如下将报错:
var table = new Pair<String>[10]; // ERROR
因为,擦除之后,table的类型是Pair[]
,可以把它转换为Object[]
:
Object[] objArray = table;
显然,由于数组会记住它的元素类型,如下会抛出一个ArrayStoreException
异常:
objArray[0] = "Hello"; // ERROR--component type is Pair
然而,对于泛型类型,由于擦除机制的存在,如果要对数组元素赋一个泛型类型的话,在事实上会导致这种检查机制的失效:
objArray[0] = new Pair<Employee>(); // 虽然这其实能够通过数组存储的检查,但实际中仍会导致一个类型错误
出于这个原因,Java规定不允许创建参数化类型的数组。
注意,只是不允许创建这些数组,但声明类型为
Pair<String>[]
的变量仍是合法的,不过不能用new Pair<String>[10]
来初始化这个变量。特殊的是,可以声明通配类型的数组,然后进行强制类型转换,但这样做结果将是不安全的:
var table = (Pair<String>[]) new Pair<?>[10];
Varargs警告
由于Java不支持泛型类型的数组,对于向参数个数可变的方法中传递实参时显然会有相同的问题。
对于下面的方法,它的参数个数是可变的:
public static <T> void addAll(Collection<T> coll, T... ts) {
for (T t : ts) {
coll.add(t);
}
}
考虑如下调用,JVM为了调用这个方法,必须建立一个Pair<String>
数组,这显然就违反了前面的规则。不过对于这种情况,“幸运”的是规则有所放松,Java只会产生一个警告,而不是错误。
Collection<Pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
addAll(table, pair1, pair2);
可以采用两种方法来抑制这个警告:
- 为包含
addAll
调用的方法增加注解@SuppressWarnings("unchecked")
- 在Java7中,还可以用
@SafeVarargs
注解。注意这个注解只能用于声明为staic
、final
或(Java9中)private
的构造器和方法,所有的其他方法都可能被覆盖,使得这个注解没有什么意义。
3.4 不能实例化类型变量
不能实例化T
类型:
public class Pair<T> {
private T first;
private T last;
public Pair() {
// Compile error:
first = new T();
last = new T();
}
}
上述代码无法通过编译,因为构造方法的两行语句:
first = new T();
last = new T();
擦拭后实际上变成了:
first = new Object();
last = new Object();
这样一来,创建new Pair<String>()
和创建new Pair<Integer>()
就全部成了Object
,显然编译器要阻止这种类型不对的代码。
遗憾的是,也不能调用以下方法,因为T.class
是不合法的:
first = T.class.getConstructor().newInstance(); // ERROR
要实例化T
类型,必须适当地设计API以便得到一个Class
对象,比如:
借助额外的
Class<T>
参数:public class Pair<T> { private T first; private T last; public Pair(Class<T> clazz) { first = clazz.newInstance(); last = clazz.newInstance(); } }
在Java8之后,最好的解决方法是让调用者提供一个构造器表达式:
public static <T> Pair<T> makePair(Supplier<T> constr) { return new Pair<>(constr.get(), constr.get()); } Pair<String> p = Pair.makePair(String::new);
3.5 不能构造泛型数组
类似于不能实例化泛型实例一样,也不能实例化数组。不过原因有所不同,毕竟数组可以填充null
值,看上去好像可以安全地构造。不过,数组本身也带有类型,用来监控虚拟机中的数组存储。
如下会编译错误,因为本质上new T[n]
会变成new Object[n]
。
public class Tester<T> {
private T field1;
private T[] field2;
private T[] field3 = new T[10]; // 编译错误
}
结合使用lambda表达式的构造哭器引用能在一定的场景中解决这个问题。如
// 不好的方式,只能转换成Object[]
Object[] people = stream.toArray();
// 好的方式,可以转换成真实的类型
Person[] people = stream.toArray(Person[]::new);
3.6 泛型类的静态上下文中类型变量无效
不能在静态字段或方法中引用类型变量。例如,下面的方法是编译错误的:
public class Apple<T> {
T age;
static T info; // 错误
public void foo(T msg) {}
public static void bar(T msg) {} // 错误
}
因为不管为泛型形参传入哪一种类型,对于Java来说,该泛型依然被当成同一个类处理,在内存中也只占用一块内存空间,泛型形参只是在运行时绑定于具体的实例,因此,在静态方法、静态初始化块、静态变量的声明和初始化中不允许使用类型形参(否则,静态XX将不再是统一不变的)。
3.7 不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类的对象。实际上,泛型类扩展Throwable
甚至都是不合法的。例如,以下定义会编译错误:
public class Proble<T> extends Exception {} // ERROR--can't extend Throwable
catch子句中不能使用类型变量:
public static <T extends Throwable> void doWork(Class<T> t) {
try {
// ...
} catch (T e) { // ERROR--can't catch type variable
// ...
}
}
3.8 可以取消对检查型异常的限制
Java异常处理的一个基本原则是,必须为所有检查型异常提供一个处理器。不过可以利用泛型取消这个机制。关键在于以下方法:
// ...以后再说吧
使用这个技术能解决一个棘手的问题:要在一个线程中运行代码,需要把代码放在一个实现了Runnable
接口的类的run
方法中。不过这个方法不允许抛出检查型异常。我们将提供一个从Task
到Runnable
的适配器,它的run
方法可以抛出任意异常。
// ...以后再说吧
3.9 注意擦除后的冲突
当泛型类型被擦除后,不允许创建引发冲突的条件。下面来看一个示例,假定为Pair
类增加一个equals
方法,如下所示:
public class Pair<T> {
public boolean equals(T value) {
return first.equals(value) && second.equals(value);
}
}
那么,考虑一个Pair<String>
,从概念上讲,它有两个equals
方法:
boolean equals(String); // defined in Pair<T>
boolean equals(Object); // inherited from Object
但是,直觉把我们引入歧途,方法boolean equals(T)
擦除后就是boolean equals
,这会与Object.equals
方法发生冲突,唯一的补救方法就是重新命名引发冲突的方法。
4. 类型变量的限定
使用泛型的类型通配符,其中 ?
表示元素类型未知:
代码 | 解释 |
---|---|
Foo<?> fo = new Foo<>() | 通配符的上限为 Object |
Foo<? extends Number> fn = new Foo<>() | 通配符的上限为 Number |
Foo<? super Bar> fb = new Foo<>() | 通配符的下限为 Bar |
// 以下表示泛型参数T必须继承自Number或是Number本身
class Apple<T extends Number> {
T col;
public static void main(String[] args) {
Apple<Integer> ai = new Apple<>();
Apple<Double> ad = new Apple<>();
// 下面一行将引起编译错误
Apple<String> as = new Apple<>();
}
}
![]() | ![]() |
---|
带通配符的泛型List
仅能表示它是各种相应泛型List
的父类,并不能把元素加入到其中,如以下代码将发生编译错误:
List<?> c = new ArrayList<String>();
// null是所有引用类型的实例,故可添加
c.add(null);
// 编译错误,程序无法确定集合c的元素类型
c.add(new Object());
List<? extends Shape> shapes;
// 编译错误,无法确定通配符的具体类型
shapes.add(0, new Rectangle());
类似于Java的继承体系,还可以为泛型形参设定多个上限:
// 表明该泛型形参T必须是其父类的子类,并且实现多个上限接口
class Apple<T extends Number & java.io.Serializable> {}
- 但至多有一个父类上限;
- 可以有多个接口上限;
- 如果需要为泛型形参指定类上限,类上限必须位于第一位,接口位于其后。
协变与逆变???
直观地讲,带有超类型限定的通配符允许你写入一个泛型对象,而带有子类型限定的通配符允许你读取一个泛型对象。
一定要看廖雪峰关于
extends
和super
的讲解:extends通配符 - 廖雪峰的官方网站 (liaoxuefeng.com) 。
对于逆变的泛型集合来说,编译器只知道集合元素是下限的父类型,但具体是哪种父类型则不确定。因此,这种逆变的泛型集合能向其中添加元素(因为实际赋值的集合元素总是逆变声明的父类),从集合中取元素时只能被当成Object
类型处理(编译器无法确定取出的到底是哪个父类的对象)。
- 如果你想从一个数据类型里获取数据,使用
? extends
通配符; - 如果你想把对象写入一个数据结构里,使用
? super
通配符; - 如果你既想存,又想取,那就别用通配符。
口诀:
- 协变只读不取;
- 逆变只取不读。
无限定通配符
还可以使用根本无限定的通配符,例如,Pair<?>
,初看起来,这好像与原始的Pair类型一样,但实际上这两种类型有很大的不同。类型Pair<?>
有以下方法:
? getFirst()
:返回值只能赋给一个Object。void setFirst(?)
:此方法不能被调用,甚至不能用Object调用Pair<?>
和Pair
的本质不同在于:可以用任意Object
对象作为参数调用原始Pair
类的setFirst
方法,但不可能调用Pair<?>
的setFirst
方法(除了传入一个null
)。
那么为什么要使用这样一个脆弱的类型呢?这在很多简单操作的场景中非常有用,例如,下面这个方法可以来测试一个对组中是否包含一个null
引用,它不需要实际的类型:
public static boolean hasNulls(Pair<?> p) {
return p.getFirst() == null || p.getSecond() == null;
}
通过将hasNulls
转换成泛型方法,可以避免使用通配符类型:public static <T> boolean hasNulls(Pair<T> p)
,但是,显然带有通配的版本可读性更好。
通配符捕获
由于通配符不是类型变量,因此不能在编写代码中使用?
作为一种类型,也就是说,下面的代码是非法的:
public static void swap(Pair<?> p) {
? t = p.getFirst(); // ERROR
p.setFirst(p.getSecond());
p.setSecond(t);
}
对于这种情况,可以写一个辅助方法swapHelper
,如下所示:
public static <T> void swapHelper(Pair<T> p) {
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
此时,swapHelper
方法的参数T捕获通配符,它不知道通配符指示哪种类型,但好歹是一个明确的类型。
当然,从实用性角度来说,上面的例子不是很好,因为显然可以直接把swap
方法定义成swapHelper
方法,意会即可。如下是一个更实用性的例子:
public static void maxminBonus(Manager[] a, Pair<? super Manager> result) {
minmaxBonus(a, result);
PairAlg.swapHelper(result); // OK--swapHelper captures wildcard type
}
通配符捕获只有在非常限定的情况下才是合法,编译器必须能够保证通配符表示单个确定的类型。例如,ArrayList<Piar<T>>
中的T永远不能捕获ArrayList<Piar<?>>
中的通配符,数组列表可以保存两个Pair<?>
,其中的?
分别有不同的类型。
5. 反射和泛型
泛型Class类
java.lang.Class<T>
中包含了如下方法:
方法 | 作用 |
---|---|
T newInstance() | 返回无参构造器构造的一个新实例 |
T cast(Object obj) | 如果obj 为null 或有可能转换成类型T ,则返回obj ;否则抛出一个BadCastException 异常 |
T[] getEnumConstants() | 如果T 是枚举类型,则返回所有值组成的数组,否则返回null |
Class<? super T> getSuperclass() | 返回这个类的超类,如果T不是一个类或Object 类,则返回null |
Constructor<T> getConstructor(Class... parameterTypes) | 获得构造器 |
Constructor<T> getDeclaredConstructor(Class.. parameterTypes) |
java.lang.reflect.Constructor<T>
中包含了如下方法:
方法 | 作用 |
---|---|
T newInstance(Object... parameters) |
虚拟机中的泛型类型信息
尽管 Java 泛型的突出特性之一是在虚拟机中擦除泛型类型,但令人奇怪的是,擦除的类仍然保留原先泛型的微弱记忆。例如,原始的Pair
类知道它源于泛型类Pair<T>
,尽管一个Pair
类型的对象无法区分它是构造为Pair<String>
还是Pair<Employee>
。
以public static <T extends Comparable<? super T>> T min(T[] a)
为例,可以利用反射API来确定:
- 这个泛型方法有一 个名为
T
的类型参数 - 这个类型参数有一个子类型限定,其自身又是一个泛型类型
- 这个限定类型有一个通配符参数
- 这个通配符参数有一个超类型限定
- 这个泛型方法有一个泛型数组参数
换句话说,你可以重新构造实现者声明的泛型类和方法的所有有关内容,但是你不会知道对于特定的对象或方法调用会如何解析类型参数。
为了表述泛型类型声明,可以使用java.lang.reflect
包中的接口Type
,这个接口包含以下子类型:

Class
类:描述具体类型TypeVarabile
接口:描述类型变量,如T extends Comparable<? super T>
WildcardType
接口:描述通配符,如? super T
ParameterizedType
接口:描述泛型类或接口类型,如Comparable<? super T>
GenericArrayType
接口:描述泛型数组,如T[]
获取泛型信息:
Class<T> 的方法 | 说明 |
---|---|
TypeVariable[] getTypeParameters() | 如果这个类型被声明为泛型类型,则获得泛型类型变量,否则获得一个长度为0的数组 |
Type getGenericSuperclass() | 获得这个类型所声明超类的泛型类型;如果这个类型是Object 或不是类类型,则返回null |
Type[] getGenericInterfaces() | 获得这个类型所声明接口的泛型类型(按照声明的次序),否则,如果这个类型没有实现接口,则返回长度为0的数组 |
Method 的方法 | 说明 |
---|---|
TypeVariable[] getTypeParameters() | 如果这个方法被声明为一个泛型方法,则获得泛型类型变量,否则返回长度为0的数组 |
Type getGenericReturnType() | 获得这个方法声明的泛型返回类型 |
Type[] getGenericParameterTypes() | 获得这个方法声明的泛型参数类型。如果这个方法没有参数,则返回长度为0的数组 |
TypeVariable 的方法 | 说明 |
---|---|
String getName() | 获得这个类型变量的名字 |
Type[] getBounds() | 获得这个类型变量的子类限定,否则,如果该变量无限定,则返回长度为0的数组 |
WildcardTYPE 的方法 | 说明 |
---|---|
Type[] getUpperBounds() | 获得这个类型变量的子类(extends )限定,否则,如果没有子类限定,则返回长度为0的数组 |
Type[] getLowerBounds() | 获得这个类型变量的超类(super )限定,否则,如果没有超类限定,则返回长度为0的数组 |
ParameterizedType 的方法 | 说明 |
---|---|
Type getRawType() | 获得这个参数化类型的原始类型 |
Type[] getActualTypeArguments() | 获得这个参数化类型声明的类型参数 |
Type getOwnerType() | 如果是内部类型,则返回其外部类类型;如果是一个顶级类型,则返回null 。 |
GenericArrayType 的方法 | 作用 |
---|---|
Type getGenericComponentType() | 获得这个数组类型声明的泛型元素类型 |