Java中的泛型
定义:泛型是一种参数化的类型机制。
使用泛型的好处:
- 在编译期进行类型检查,便于更早发现错误。
- 代码更简洁、可复用。
一、泛型的类型
1.1 泛型类
public class NormalGeneric<T,K> {
private T data;
private K result;
public NormalGeneric() { }
public NormalGeneric(T data, K result) {
this.data = data;
this.result = result;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public K getResult() {
return result;
}
public void setResult(K result) {
this.result = result;
}
}
1.2 泛型接口
/**
* 泛型接口
*/
public interface IGeneric<T> {
public T next();
}
/**
* 泛型接口1--未指定类型
*/
public class ImplGeneric<T> implements IGeneric<T>{
@Override
public T next() {
return null;
}
}
/**
* 泛型接口2--直接指定类型
*/
public class ImplGeneric2 implements IGeneric<String> {
@Override
public String next() {
return null;
}
}
1.3 泛型方法
指引入自己的类型参数的方法。但类型参数的范围仅限于声明它的方法。允许使用静态和非静态的泛型方法,也允许使用泛型类构造函数。
语法包括类型参数列表,在尖括号内,该列表出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。
public class GenericMethod {
//泛型方法标志:<T>,类型参数列表仅有一个T
public <T> T genericMethod(T...a){
return a[a.length/2];
}
//普通方法
public void test(int x,int y){
System.out.println(x+y);
}
public static void main(String[] args) {
GenericMethod genericMethod = new GenericMethod();
genericMethod.test(23,343);
System.out.println(genericMethod.<String>genericMethod("aaa","bbb","ccc","ddd"));
//该类型已明确提供,可以将其忽略,编译器将推断出所需的类型。此功能称为类型推断。
System.out.println(genericMethod.genericMethod(12,343));
}
}
/**
*泛型方法中 T 的意义
*/
public class GenericMethod3 {
static class Fruit{
@Override
public String toString() {
return "fruit";
}
}
static class Apple extends Fruit{
@Override
public String toString() {
return "apple";
}
}
static class Person{
@Override
public String toString() {
return "Person";
}
}
static class GenerateTest<T>{
//普通方法
public void show1(T t){
System.out.println(t.toString());
}
/**
* 在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。
* 类型可以与T相同,也可以不同。
*
* 由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,
* 编译器也能够正确识别泛型方法中识别的泛型。
*/
public <E> void show3(E t){
System.out.println(t.toString());
}
/**
* 在泛型类中声明了一个泛型方法,使用泛型T,
* 注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
*/
public <T> void show2(T t){
System.out.println(t.toString());
}
}
public static void main(String[] args) {
Apple apple = new Apple();
Person person = new Person();
GenerateTest<Fruit> generateTest = new GenerateTest<>();
generateTest.show1(apple);
// generateTest.show1(person);
generateTest.show2(apple);
generateTest.show2(person);
generateTest.show3(apple);
generateTest.show3(person);
}
}
1.4 类型参数命名约定
按照约定,类型参数名称是单个大写字母。(没有该约定,将很难分辨类型变量与普通类或接口名称之间的区别。)
最常用的类型参数名称是:
- E – Element (Java Collections Framework广泛使用)
- K – Key
- N – Number
- T – Type
- V – Value
- S,U,V etc. – 2nd, 3rd, 4th types
二、限定类型
有时你可能想限制可以在参数化类型中用作类型参数的类型。例如,对于compareTo方法可能只希望接受 Comparable 或其子类的实例。
2.1 类型变量的限定-泛型类
/**
* 限定类型可以是接口,也可以是类。在类中,extends表示扩展;在接口中,extends表示实现。
* 限定类型个数:可以是一个,可以是多个。但只能有一个类,且该类必须放在第一个。
*/
public class ClassBorder<T extends Comparable> {
public T min(T x,T y) {
if (x.compareTo(y) > 0) {
return x;
} else {
return y;
}
}
public static void main(String[] args) {
ClassBorder<String> classBorder = new ClassBorder<>();
System.out.println(classBorder.min("c","z"));
}
}
2.2 类型变量的限定-泛型方法
public class TypeLimit {
/**
* 限定类型:只有实现了Comparable方法的才可以使用
*/
public static <T extends Comparable> T min(T a, T b) {
if (a.compareTo(b) > 0) {
return a;
} else {
return b;
}
}
/**
* 限定类型可以是接口,也可以是类。
* 限定类型个数:可以是一个,可以是多个。但只能有一个类,且该类必须放在第一个。
*/
public static <T extends ArrayList & Comparable> T max(T a, T b) {
if (a.compareTo(b) > 0) {
return b;
} else {
return a;
}
}
public static void main(String[] args) {
TypeLimit.min("a","b");
}
}
三、泛型的约束和局限性
泛型的约束和局限性:
- 无法实例化具有基本类型的泛型类型。例如:double,但其包装类Double可以。在Java中基本类型不是对象。
- 无法创建类型参数的实例。
- 静态域或者方法里不能引用类型变量。但静态方法 本身是泛型方法就可以。
- 无法将Casts或instanceof与参数化类型一起使用。
- 泛型数组可以创建,但不能初始化。
- 泛型类不能extends Exception/Throwable。
- 无法重载每个重载的形式参数类型都擦除为相同原始(raw)类型的方法。不能捕获泛型类对象,但可以throws。
四、泛型类型的继承规则
泛型类型的继承规则:
- class Worker extends Employee,但 Pair< Employee > 和 Pair< Worker > 没有任何继承关系。
- 泛型类可以继承或者扩展其他泛型类。private static class ExtendPair< T > extends Pair< T >{}。
五、通配符
在泛型中使用” ?”(称之为通配符)表示未知类型。
通配符可以在多种情况下使用:作为参数,字段或局部变量的类型;有时作为返回类型。通配符从不用作泛
型方法调用,泛型类实例创建或超类型的类型参数。
/**
* ? extends
* 上界通配符:实例化对象的泛型的上界Apple
* kotlin中该过程称为协变
* 用途:可以突破「父类的泛型类型声明的实际值不能是子类的泛型类型的对象」的限制,
* 以此来扩大变量或者参数的接收范围,让程序更灵活。
* 限制:只能安全的访问数据。只能用,不能修改。
*/
GenericType<? extends Apple> appleExtendsGenericType = new GenericType<HongFuShi>();
appleExtendsGenericType.getData();
/**
* ? super
* 下界通配符:实例化对象的泛型的下界为Apple
* kotlin中该过程称为逆变或反变
* 用途:可以把父类的泛型类型对象赋值给子类的泛型类型的声明,以此来扩大变量或者参数的接收范围。
* 限制:只能安全的写入数据。只能输入,不能输出。
*/
GenericType<? super Apple> appleSuperGenericType = new GenericType<Fruit>();
appleSuperGenericType.setData(new Apple());
appleSuperGenericType.setData(new HongFuShi());
在决定是否使用通配符以及哪种类型的通配符时,可以使用“输入”和“输出”原理。以下列表提供了要遵循的准则:
- 使用上限通配符定义输入变量,使用 extends 关键字。
- 使用下限通配符定义输出变量,使用 super 关键字。
- 如果可以使用 Object 类中定义的方法访问输入变量,请使用无界通配符( ? )。
- 如果代码需要同时使用输入和输出变量来访问变量,则不要使用通配符。
这些准则不适用于方法的返回类型。应该避免使用通配符作为返回类型,因为这会迫使程序员使用代码来处理通配符。
六、类型擦除
类型擦除
:Java泛型的处理在编译器中运行,编译生成的字节码bytecode不包含泛型信息,泛型信息在编译处理时被擦除(erasure),这个过程即类型擦除。类型擦除可确保不会为参数化类型创建新的类;因此,泛型不会产生运行时开销。
Java泛型说明:
- 虚拟机中没有泛型,Java中的泛型在编译时会类型擦除。如果类型参数不受限制,则将泛型类型中的所有类型参数替换为其边界(上下限)或 Object 。因此,产生的字节码仅包含普通的类,接口和方法。
- 必要时插入类型转换,以保持类型安全。
- 生成桥接方法以在扩展的泛型类型中保留多态。
泛型变量有限定类型示例: <T extends Comparable & Serializable> ,因为Comparable包含实现方法CompareTo,而Serializable只是个标签(Tagging)接口,若二者对调位置,则原始类型将用Serializable来替换,这样在编译器必要时要向Comparable插入强制类型转换,所以为了提高效率,最好将标签(Tagging)接口放在边界列表的末尾。这也对应了上文中提到的类型变量的限定中多个限定类型个数时且包含类时,类放第一个。