Java 泛型 Generic机制实例详解
Airsaid 人气:0泛型是什么?
使用泛型可以指定类型变量,从而让代码可以对不同类型的对象进行重用。以及,还可以让编译器更好的了解类型,从而避免强制类型转换,提升代码的安全性。
类型变量就是尖括号 <>
中的变量,类型变量的命名规范是使用大写字母,例如 E 表示元素类型,K、V 分别表示键和值类型,T 和相邻的 U、S 表示任意类型。当然你也可以起其他的名字,编译器对此并没有强制限制,但是还是按照规范来。
泛型类
泛型类(generic class)就是有一个或多个类型变量的类。例如下面的 Pair 类,就有两个类型变量,分别是 T 和 U:
public class Pair<T, U> { public T first; public U second; public Pair(T first, U second) { this.first = first; this.second = second; } }
在使用时,就可以传入任意的类型。例如:
Pair<Integer, String> pair = new Pair<>(1, "A");
思考一下,如果没有泛型的存在,那么当我们要操作不同类型的对象时,要么为每种类型都创建一个类,但是这样就会存在大量的重复代码,不容易维护。要么就直接使用对象的顶级父类 Object 类,但是这样的话在使用时就需要进行强制转换,存在安全问题,即使每次强转前进行判断,也会存在重复代码和遗漏的风险。
说回泛型类,泛型类的类型变量是定义在类名后面,在整个类中都可以使用定义的类型变量。所以在选择使用泛型类时,泛型变量应该是和类所关联的。如果仅与方法关联,那么可以使用泛型方法。
泛型方法
泛型方法的类型参数是定义在方法返回类型的前面的,只在当前方法中可以使用。例如:
public static <T> T getMiddle(T... a) { return a[a.length / 2]; }
泛型方法可以在普通类中定义,也可以在泛型类中定义。如果泛型变量仅在方法内会用到,就可以考虑使用泛型方法。
类型变量的限制
默认情况下,类型变量可以是任何类型。但是有时候,我们需要限制类型变量的类型。此时可以通过 extends
关键字来限制:
<T extends UpperBoundType>
限制泛型类型为特定类型或者特定类型的子类
例如,假设我们有一个 Printer 类,我们只需要这个类打印动物的信息,那么就可以使用 <T extends Animal>
来限制只能接收 Animal 或其实现类:
public interface Animal { String getName(); } public static class Printer<T extends Animal> { public void print(T t) { System.out.println(t.getName()); } } public static class Dog implements Animal { @Override public String getName() { return "Woof"; } } public static class Cat implements Animal { @Override public String getName() { return "Meow"; } } public static void main(String[] args) { Printer<Animal> animalPrinter = new Printer<>(); animalPrinter.print(new Dog()); animalPrinter.print(new Cat()); animalPrinter.print(new People("Abc")); // 如果试图传入一个其他类型,则会编译失败 }
一个类型变量,可以指定多个限制类型。例如:
public class Printer<T extends Dog & Animal> { ... }
上面的限制的意思是,类型变量必须是 Dog 或其子类,并且实现了 Animal 接口。需要注意的是,在指定多个限制的类型时,除了第一个限制类型可以是类以外,其他的限制类型必须是接口类型。这是因为 Java 是不支持多重继承的。
类型擦除
由于泛型是在 Java 1.5 才推出的,所以 Java 为了保证在之前版本上的兼容性,泛型实际上在 JVM 中是没有的!
也就是说,编译器在编译时会擦除掉泛型的参数类型,并提供一个相应的原始类型(raw type),这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除(erased),并替换为其限定类型,没有限定类型则替换为 Object 类型。
举个例子,如果我们定义了一个泛型类:
public class Printer<T> { ... }
那么当类型擦除后,实际上就变为了 Printer<Object>
类型。
如果指定了限定类型,例如:
public class Printer<T extends Animal> { ... }
那么擦除后,就变为了 Printer<Animal>
类型。
如果指定了多个限定的类型,那么擦除后就会是第一个限定的类型。例如:
public class Printer<T extends Dog & Animal> { ... }
擦除后就变为了 Printer<Dog>
类型。
Java 泛型的很多限制都是由于泛型擦除导致的,下面会详细介绍下泛型的局限性。
通配符
通配符(Wildcard)就是 Java 泛型中的问号 ? 。要想知道通配符是要解决什么样的问题,我觉得首先需要知道什么是 Variance(不知道这个正确的译名是什么,如有知道请留言)?
提到 Variance 必须先提到子类型(subtyping),子类型是面向对象中类型多态的其中一种表现形式,主要用于描述 is-a 这样的关系,例如 S 是 T 的子类型,那么 S is a subtype of T。
Variance 指的是如何根据组成类型之间的子类型关系,来确定更复杂的类型之间的子类型关系。
上面这段话比较绕,举个实际的例子来说,Variance 指的就是当子类型在更复杂的场景下,例如 If S is a subtype of T,那 Generic<S> is subtype of Generic<T>
这种关系是否还能成立。
Variance 分为下面几种形式:
- 不变(Invariance):如果 B 是 A 的子类型,那么
Generic<B>
不是Generic<A>
的子类型。 - 协变(Covariance):如果 B 是 A 的子类型,那么
Generic<B>
是Generic<A>
的子类型。 - 逆变(Contravariance):如果 B 是 A 的子类型,那么
Generic<A>
是Generic<B>
的子类型(逆变就是逆转了子类型关系)。 - 双变(Bi-variance):Java 中不存在双变,因此不讨论。
在 Java 当中,泛型是不变的,这也就意味着下面这段看似没问题的代码会报错:
List<Dog> dogs = new ArrayList<>(); List<Animal> animals = dogs; // 编译报错
因为按照不变的说明,List<Dog>
根本就不是 List<Animal>
的子类型,所以自然就不能赋值了。
Java 中的泛型采用不变性是为了保证类型安全。不过不变性虽然保证了类型安全,但是也让灵活性大大降低。为此,Java 提供了通配符,可以解除部分这种限制,也不损失安全性。
上界通配符
上界通配符的形式为:? extends Type
,使用上限通配符可以将类型参数变为协变的,这就可以让原本无法赋值的类型可以正常赋值了。但是这在解除不变性限制的同时,也带来了新的限制,就是参数类型只能作为返回值类型,无法作为入参的类型。例如:
List<? extends Animal> animals = new ArrayList<Dog>(); // 可以正确赋值了 animals.add(new Dog()); // 编译错误,类型不匹配,无法写入,除非写入 null(也就是参数类型无法作为入参的类型) Animal animal = animals.get(0); // 可以获取(也就是参数类型可作为返回值类型)
这就叫当上帝给你关了一扇门,同时会帮你打开一扇窗。尽管窗户有个洞,但总比没有好吧?你说是不是?
下界通配符
下界通配符的形式为:? super Type
,使用下界通配符可以将类型参数变为逆变的,也就是说颠倒了类型关系,父类型可以赋值给子类型了!例如:
List<Animal> animals = new ArrayList<>(); List<? super Dog> dogs = animals; // 可以正确赋值!
看起来挺神奇的吧!这种方式同样也带来了新的限制,在使用这种变量的时候,不可以调用返回值包含类型参数的方法,也不能获得包含类型参数的字段值。简单来说,就是只能作为输入,但是不能输出。例如:
dogs.add(new Dog()); // 可以写入 Dog dog = dogs.get(0); // 编译报错,类型不匹配: Required type: Dog Provided: capture of ? super Dog
对于获取时编译报类型不匹配的问题,其实可以通过类型强制转换来解决。
无界通配符
无界通配符的形式就是一个单独的问号 ?。当我们对类型参数一无所知,但仍然希望以安全的方式使用它时,则可以考虑使用无界通配符。例如:
public static boolean hasNull(Pair<?> p) { return p.first != null && p.second != null; }
反射和泛型
因为泛型擦除,所以无法在反射时获取泛型类型。但是擦除的类依然保留原先泛型的微弱记忆。例如,如果定义了以下泛型方法:
public static <T extends Comparable<? super T>> T min(T[] a)
那么,可以通过反射可以获取到以下信息:
public static Comparable min(Comparable[] a)
也就是说,我们可以重新构造实现者声明的泛型类和方法的有关内容。但是,不会知道对于特定的对象或方法调用会如何解析类型参数。
为了表述泛型类型声明,可以使用 java.lang.reflect
包中的接口 Type
。这个接口包含以下子类型:
- Class 类:描述具体类型
- TypeVariable 接口:描述类型变量(如
T extends Comparable<? super T>
) - WildcardType 接口:描述通配符(如
? super T
) - ParameterizedType 接口:描述泛型类或接口类型(如
Comparable<? super T>
) - GenericArrrayType 接口:描述泛型数组(如
T[]
)
下面的这个示例程序,就是通过反射来打印指定类的信息:
public class GenericReflectionTest { public static void main(String[] args) { String className; try (Scanner scanner = new Scanner(System.in)) { System.out.println("Enter class name (e.g., java.util.Collections): "); className = scanner.next(); } try { Class<?> cl = Class.forName(className); ClassPrinter.print(cl); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static class ClassPrinter { public static void print(Class<?> cl) { System.out.print(cl); printTypes(cl.getTypeParameters(), "<", ", ", ">", true); final Type sc = cl.getGenericSuperclass(); if (sc != null) { System.out.print(" extends "); printType(sc, false); } printTypes(cl.getGenericInterfaces(), " implements ", ", ", "", false); System.out.print(" {"); System.out.println(); final Method[] methods = cl.getDeclaredMethods(); for (int i = 0; i < methods.length; i++) { final Method method = methods[i]; printMethod(method); if (i < methods.length - 1) { System.out.println(); } } System.out.println(); System.out.print("}"); } private static void printTypes(Type[] types, String pre, String sep, String suf, boolean isDefinition) { if (pre.equals(" extends ") && Arrays.equals(types, new Type[]{Object.class})) { return; } if (types.length > 0) { System.out.print(pre); } for (int i = 0; i < types.length; i++) { if (i > 0) { System.out.print(sep); } printType(types[i], isDefinition); } if (types.length > 0) { System.out.print(suf); } } private static void printType(Type type, boolean isDefinition) { if (type instanceof Class) { // 具体类型 Class<?> cl = (Class<?>) type; System.out.print(cl.getName()); } else if (type instanceof TypeVariable) { // 类型变量 TypeVariable<?> tv = (TypeVariable<?>) type; System.out.print(tv.getName()); if (isDefinition) { printTypes(tv.getBounds(), " extends ", " & ", "", false); } } else if (type instanceof WildcardType) { // 通配符 WildcardType wt = (WildcardType) type; System.out.print("?"); printTypes(wt.getUpperBounds(), " extends ", " & ", "", false); printTypes(wt.getLowerBounds(), " super ", " & ", "", false); } else if (type instanceof ParameterizedType) { // 泛型类或接口类型 ParameterizedType pt = (ParameterizedType) type; Type ownerType = pt.getOwnerType(); if (ownerType != null) { printType(ownerType, false); } printType(pt.getRawType(), false); printTypes(pt.getActualTypeArguments(), "<", ", ", ">", false); } else if (type instanceof GenericArrayType) { // 泛型数组 GenericArrayType gat = (GenericArrayType) type; printType(gat.getGenericComponentType(), isDefinition); System.out.print("[]"); } else { System.out.print("unknown type: " + type); } } private static void printMethod(Method m) { final String name = m.getName(); System.out.print(" "); System.out.print(Modifier.toString(m.getModifiers())); System.out.print(" "); printTypes(m.getTypeParameters(), "<", ", ", ">", true); printType(m.getGenericReturnType(), false); System.out.print(" "); System.out.print(name); System.out.print("("); printTypes(m.getGenericParameterTypes(), "", ", ", "", false); System.out.print(")"); } } }
以下面的这个测试类为例:
public class TestClass<T extends Comparable<? super T>> implements Cloneable { public void test(T t) { System.out.println(t); } }
运行上面的程序的输出结果是:
class com.airsaid.java.samples.generic.TestClass<T extends java.lang.Comparable<? super T>> extends java.lang.Object implements java.lang.Cloneable { public void test(T) }
可以看到,可以获取到该泛型类所定义的泛型信息。
类型字面量
有时候,我们确实需要在运行时获取泛型的参数类型,那么真的就没有办法了吗?
答案是有的,在上面反射的例子中我们可以看到,字节码当中其实保存了部分的泛型信息(签名信息)。
举一个实际场景的例子,Gson 这个框架相信大家都用过。在进行反序列化时,有多个重载方法:
fromJson(String json, Class cl)
fromJson(String json, TypeToken type)
当使用第一个方法时,如果第二个参数是一个泛型类的 class 对象,那么就会出现问题。例如下面的这段示例代码:
Foo<DataBean> foo = new Foo<DataBean>(); gson.fromJson(json, foo.getClass());
这是因为无法获取到 Foo 类的泛型参数 DataBean。
因此,Gson 提供了 TypeToken 类来处理这种问题。使用起来是这样的:
Foo<DataBean> foo = new Foo<DataBean>(); gson.fromJson(json, new TypeToken<Foo<DataBean>>(){});
下面是我简化过后的 TypeToken 源码:
public class TypeToken<T> { private final Type type; protected TypeToken() { this.type = getTypeTokenTypeArgument(); } public Type getType() { return type; } private Type getTypeTokenTypeArgument() { final Type superclass = getClass().getGenericSuperclass(); if (superclass instanceof ParameterizedType) { ParameterizedType parameterized = (ParameterizedType) superclass; if (parameterized.getRawType() == TypeToken.class) { return parameterized.getActualTypeArguments()[0]; } } else if (superclass == TypeToken.class) { throw new IllegalStateException("TypeToken must be created with a type argument: new TypeToken<...>() {}; " + "When using code shrinkers (ProGuard, R8, ...) make sure that generic signatures are preserved."); } throw new IllegalStateException("Must only create direct subclasses of TypeToken"); } }
TypeToken 的原理就是创建了一个匿名子类对象,然后通过 getGenericSuperclass()
方法获取其泛型父类,再获取其中定义的参数化类型的类型参数。通过这种方式,就可以获取到 TypeToken 所定义的泛型类型了:
public class TypeTokenTest { public static void main(String[] args) { TypeToken<?> typeToken = new TypeToken<List<String>>() {}; System.out.println(typeToken.getType()); // java.util.List<java.lang.String> } }
限制和局限性
不能使用基本类型实例化类型参数
泛型的类型参数不可以使用基本类型,例如 Pair<int>
,这是因为类型擦除后类型变为了 Object,而 Object 无法存储基本类型数值。对于这种情况,我们需要使用基本类型对应的包装类型。
运行时类型查询只适用于原始类型
同样也是由于类型擦除导致的问题,如果要在代码中进行判断是否是某个泛型类型,这是无法做到的。例如:
if (a instanceof Pair<String>) // 错误
不能创建参数化类型的数组
还是由于类型擦除导致的问题,无法实例化参数化类型的数组。例如:
Pair<String>[] table = new Pair<String>[10]; // 错误
不能创建 Throwable 的子类为泛型类
如果希望创建一个 Throwable 的子类,那么子类就不可以是泛型类。例如:
class Generic<T> extends Exception { }
此时,编译器会直接报错,提示:Subclass of 'Throwable' may not have type parameters
。
这还是因为泛型擦除,这就导致在 catch 时不知道该执行哪个代码分支:
try { ... } catch (Generic<String> e) { } catch (Generic<Integer> e) { }
加载全部内容