泛型
泛型(Generics)是Java SE 5中引入的一个新特性,它使得在定义类和方法时可以指定一个或多个类型参数,从而可以在不考虑具体类型的情况下直接使用这些类型参数。泛型可以增强代码的安全性、可读性和可重用性。
在Java中,泛型的语法是通过在类名或方法名后面加上尖括号,然后在尖括号里指定类型参数。
查看代码
class ClassName<T1, T2, ..., Tn> {
// 这里可以使用类型参数
}
// 可以通过泛型实例化一个泛型对象
ClassName<T1, T2, ..., Tn> variableName; // 定义一个泛型类引用
new ClassName<T1, T2, ..., Tn>(constructorArgs); // 实例化一个泛型类对象
// 当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写
ClassName<T1, T2, ..., Tn> variableName = new ClassName<>();
需要注意的是,泛型只能接受类,所有的基本数据类型必须使用包装类。
泛型通配符
Java泛型通配符是指在使用泛型时使用的一种特殊符号,用于表示未知类型或者不确定类型的情况。通配符使用?
表示,有三种形式:? extends T
、? super T
和?
。
? extends T
:表示通配符所代表的类型是T的子类型(包括T本身),通常用于限制泛型类型为某个范围内的子类型。例如List<? extends Number>
表示泛型类型是Number或者Number的子类。? super T
:表示通配符所代表的类型是T的父类型(包括T本身),通常用于向泛型集合中添加元素。例如List<? super Integer>
表示泛型类型是Integer或者Integer的父类。?
:表示通配符可以匹配任意类型,通常用于读取数据而不需要向集合中添加新元素的情况。
通配符的使用可以使代码更加灵活,但需要注意的是,使用通配符会使得对泛型参数的操作受到限制,因为编译器无法确定通配符代表的具体类型,所以只能进行有限的操作。
在Java中,泛型通配符用于表示未知类型,通常使用?
表示。泛型通配符可以用在参数类型、方法返回类型以及变量类型上。
上界通配符:
<? extends T>
表示类型必须是T或者T的子类。javaList<? extends Number> list1 = new ArrayList<Integer>(); // 合法的,Integer是Number的子类 List<? extends Number> list2 = new ArrayList<String>(); // 非法的,String不是Number的子类
下界通配符:
<? super T>
表示类型必须是T或者T的父类。javaList<? super Integer> list3 = new ArrayList<Number>(); // 合法的,Number是Integer的父类 List<? super Integer> list4 = new ArrayList<String>(); // 非法的,String不是Integer的父类
无界通配符:
<?>
表示未知类型,相当于<? extends Object>
。javaList<?> list5 = new ArrayList<Integer>(); // 合法的 List<?> list6 = new ArrayList<String>(); // 合法的
泛型擦除
泛型擦除(Type Erasure)指的是在编译过程中,编译器会移除(擦除)所有的泛型类型信息,将泛型代码中的类型参数替换为它们的限定类型(如果没有指定限定类型,则替换为Object)。这样做的目的是为了保持与Java 5之前的版本的兼容性,因为泛型是在Java 5中引入的。
在Java 5之前,所有的集合类和方法都是使用Object类型来存储和操作数据的,这样做的缺点是集合中可以存储任何类型的对象,但在取出对象时需要强制类型转换,这个过程中可能会出现类型错误,而这种错误只能在运行时被发现。
为了解决这个问题,Java 5引入了泛型,但是为了保证旧的代码仍然可以在新版本中运行,Java设计者选择了类型擦除这种方式来实现泛型。
泛型信息仅存在于编译阶段,Java编译器在编译时会检查泛型的类型,确保类型安全,但在生成的字节码中,所有的泛型信息都会被擦除。
泛型擦除的过程
泛型擦除的过程主要发生在Java编译器的编译阶段,这个过程可以分为以下几个步骤:
- 类型检查:编译器首先检查泛型代码的类型正确性。它会确保类型参数的使用是合法的,例如,确保类型参数符合指定的边界,以及确保类型参数在需要时被正确地转换。
- 类型参数替换:如果泛型代码没有类型错误,编译器会将所有的类型参数替换为它们的限定类型或者Object类型(如果没有指定限定类型)。例如,如果一个泛型类
Box<T>
没有指定边界,那么类型参数T
会被替换为Object。 - 擦除类型参数:编译器会移除所有的泛型类型信息,包括类型参数和泛型方法签名中的类型参数。这意味着在生成的字节码中不会包含任何泛型类型的信息。
- 插入类型转换:为了保持类型安全,编译器会在必要时插入类型转换代码。例如,当从Object类型转换为具体的泛型类型时,编译器会插入相应的强制类型转换代码。
- 生成桥接方法:在某些情况下,为了保持多态性,编译器会生成桥接方法(bridge methods)。这些方法用于确保子类中的方法能够正确地覆盖父类中的方法,即使在类型擦除后也能够保持多态性。
- 修正泛型签名:在类型擦除后,编译器会修正方法签名,以确保它们与Java的类继承结构保持一致。这通常涉及到修改方法的参数类型和返回类型。
泛型擦除后,泛型代码在运行时看起来就像是普通的非泛型代码,所有的类型参数都已经被替换为它们的限定类型或者Object类型。这意味着在运行时,泛型类型的信息是不可用的,所有的泛型操作都是基于Object类型进行的,然后在必要时通过类型转换来保持类型安全。
桥接方法
桥接方法(Bridge Method)是Java编译器在类型擦除过程中自动生成的一种特殊方法,它的主要作用是在泛型类型的继承和多态中保持类型安全,并保持与旧版本Java代码的兼容性。
桥接方法的生成是为了解决类型擦除后可能出现的多态冲突问题。
在Java中,当一个子类继承了一个带有泛型参数的父类,并且子类对泛型参数进行了具体化(即指定了具体的类型),或者子类重写了父类的泛型方法时,编译器可能会生成桥接方法。
查看代码
class GenericClass<T> {
T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
class SubClass extends GenericClass<String> {
@Override
public String getValue() {
return super.getValue();
}
@Override
public void setValue(String value) {
super.setValue(value);
}
}
在这个例子中,SubClass
继承了 GenericClass<String>
,并且重写了 getValue
和 setValue
方法。在类型擦除后,GenericClass
的方法会被擦除为 Object
类型的参数和返回值。然而,SubClass
重写的方法是 String
类型的参数和返回值。为了保持多态性,编译器会为 SubClass
生成两个桥接方法,这两个方法会调用子类重写的方法,但是参数和返回值类型擦除为 Object
类型。
生成的桥接方法可能看起来像这样:
查看代码
class SubClass extends GenericClass<String> {
// ... 其他代码 ...
// 桥接方法
public Object getValue() {
return getValue(); // 调用子类的 String 类型方法
}
// 桥接方法
public void setValue(Object value) {
setValue((String) value); // 调用子类的 String 类型方法
}
}
这样,当通过 GenericClass
的引用调用 getValue
或 setValue
方法时,如果引用指向的是 SubClass
的实例,那么调用的将是桥接方法,桥接方法内部会调用子类具体化的方法,从而保持类型安全。
桥接方法的生成是编译器的内部机制,通常开发者不需要直接与桥接方法交互。但是了解桥接方法的存在有助于理解Java泛型的类型擦除和多态行为。
泛型擦除的问题
泛型擦除虽然使得Java能够在保持向后兼容的同时引入泛型,但它也带来了一些问题和限制:
由于泛型信息在运行时被擦除,因此无法在运行时反射地获取泛型类型参数的具体信息。这意味着不能使用
instanceof
运算符来检查泛型类型,也不能创建泛型数组。由于类型擦除,无法创建具体泛型类型的数组。例如,
List<String>[]
是不合法的。这是因为数组会保留其元素类型的运行时信息,而泛型类型的运行时信息已经被擦除。泛型方法在调用时可以提供具体的类型参数,但是方法体内部的类型参数仍然会被擦除。这意味着泛型方法在运行时不会保留类型参数的信息。尽管在代码中定义了不同类型的泛型集合(如
List<Object>
和List<String>
),但它们在编译后都会被擦除为原始类型List
。为了保持多态和类型擦除的兼容,编译器可能会生成额外的桥接方法(bridge methods)。这些方法可能会增加代码的复杂性,并在某些情况下影响性能。
由于类型擦除,不能捕获泛型类型的异常。例如,不能捕获
List<String>
类型的异常,因为擦除后它变成了List<Object>
。类型擦除可能会导致泛型继承中的问题。例如,不能创建一个继承自
List<String>
的List<Integer>
,因为擦除后它们都变成了List<Object>
。如果一个类实现了一个泛型接口,那么在实现接口的方法时,必须提供具体的类型参数。这可能会导致代码冗余,因为可能需要为不同的类型参数实现相同的方法。
不能创建泛型枚举,因为枚举类型在运行时需要保留类型信息。