泛型魔法:解码Java中的类型参数

泛型设计的意义

泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用。举个简单的例子,在没有泛型特性之前( JDK 5 以前),ArrayList 只维护一个 Object 引用的数组。

万物皆是 Object,所以啥都可以往里面放。但是这样会有两个问题:

获取值时必须进行强制类型转换:

ArrayList list = new ArrayList();
list.add("xw");
String str = (String) list.get(0);

编译器没有错误类型检查,可以向数组列表中添加任何类的对象。

list.add("xw");
list.add(20);
list.add(new Date());

添加对象的时候编译和运行都不会出错。然而在其他地方,如果将 get 的结果强制类型 转换为 String 类型,就会产生一个异常:ClassCastException

那么有了泛型之后,可以使用类型变量来明确指明元素的类型,完美的解决了上述问题。出现编译错误比类在运行时出现类的强制类型转换异常要好得多。

ArrayList<String> list = new ArrayList<String>();
// 从 JDK7 开始,构造函数中可以省略泛型类型,可以从变量的类型推断得出
ArrayList<String> list = new ArrayList<>();

还有另外一种情况,如果同一个泛型类的关于不同实例的元素直接存在关系呢?还是以 ArrayList 为例,如果对于存在的 Parent 类,和 Child 类,我们应该保证可以向父类的泛型类中添加子类的元素,但是不允许向子类的泛型类添加父类的元素,如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以很明显的看到,编译器发生了错误,究其根本,这是 Java 中引入了通配符类型。稍后我们会详细学习。

泛型类

泛型类就是具有一个或多个类型变量的类。对于这个类来说,我们只关注泛型,而不需要关注存储细节。

类型变量,用尖括号< >括起来,并放在类名的后面。泛型类可以有多个类型变量。类型变量可以用来指定方法的返回类型以及域和局部变量的类型。

public class MyClass<T> {
    private T a;
    private T b;

    public MyClass(T a, T b) {
        this.a = a;
        this.b = b;
    }
}

按照习惯和规范,类型变量使用大写形式, 且比较短,相当于实际类型的一个占位。通常使用 E 来表示集合的元素类型,K 和 V 表示键值对的元素类型,T用来表示任意类型,当然使用 A、B、C 这些也都可以。

实例化的时候可以用具体的类型替换掉类型变量,如下:

MyClass<Integer> myClass = new MyClass<>(1, 2);

泛型方法

泛型方法还可以定义在普通类中,同样使用类型变量的方式,用尖括号< >扩起来。不过注意类型变量的位置放在方法返回值的前面。

public static <A> A getMid(A ...values) {
  return values[values.length / 2];
}

调用的时候可以显示的声明出类型变量。也可以省略书写,由编译器推断。

String midStr = MyClass.<String>getMid("1", "2", "3");
Integer midStr = MyClass.getMid(1, 2, 3);

类型变量的限定类型

通过对类型变量设置限定来加以约束,方便内部逻辑的编写。

// T 需要同时 实现/继承 BoundingType1 和 BoundingType2
< T extends BoundingType1 & BoundingType2

表示 T 应该是限定类型的子类型。T 和绑定类型可以是类,也可以是接口。限定类型用& 分隔,逗号用来分隔类型变量。

<T extends Class1, U extends Class2 & Class3>

泛型代码与虚拟机

  1. 虚拟机中没有泛型,只有普通的类和方法
  2. 所有的类型变量都用它们的限定类型替换
  3. 桥方法被合成来保持多态
  4. 为保持类型安全性,必要时插人强制类型转换

类型擦除

无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删去类型变量后的泛型类型名。擦除类型变量,并替换为限定类型,如果有多个限定类型,则使用限定列表的第一个类型变量进行替换,如果无限定的变量则替换成 Object。

比如上面的 MyClass,因为 T 是一个无限定的变量,所以直接用 Object 替换,进行类型擦除后如下所示:

public class MyClass {
    private Object a;
    private Object b;

    public MyClass(Object a, Object b) {
        this.a = a;
        this.b = b;
    }
}

在程序中可以包含不同类型的 MyClass,例如, MyClass<String> 或 MyClass<LocalDate>。而擦除类
型后就变成原始的 MyClass 类型了。

翻译泛型表达式

当程序调用泛型方法的时候,如果擦除返回类型,编译器插入强制类型转换。当存取一个泛型域时也要插人强制类型转换。类型擦除也会出现在泛型方法中。

若擦除后泛型方法返回 Object 类型,那么编译器会在字节码中自动插入实际类型的强制类型转换。

约束与局限性

不能用基本类型实例化类型变量

运行时类型查询只适用于原始类型

不能创建参数化类型的数组

不能实例化类型变置

不能使用像 new T(…),newT[…] 或 T.class 这样的表达式中的类型变量。

最好的解决办法是让调用者提供一个构造器表达式(可以使用反射)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

不能构造泛型数组

就像不能实例化一个泛型实例一样,也不能实例化数组。不过原因有所不同,毕竟数组会填充 null 值,构造时看上去是安全的。不过,数组本身也有类型,用来监控存储在虚拟机中的数组。这个类型会被擦除。

最好的解决办法是让调用者提供一个数组构造器表达式(可以使用反射)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

泛型类的静态上下文中类型变量无效

不能在静态域或方法中引用类型变量。

public class MyClass<T> {
    // 错误用法
    private static T c;

    // 错误用法
    public static void test(T a) {

    }
}

不能抛出或捕获泛型类的实例

泛型类型的继承规则

泛型类可以扩展或实现其他的泛型类。就这一点而言,与普通的类没有什么区别。例如, ArrayList<T>类实现 List<T>接口。

这意味着, 一个 ArrayList<Manager> 可以被转换为一个 List<Manager>。但是,如前面所见,一个 ArrayList<Manager> 不是一个 ArrayList <Employee> 或 List<Employee>。

通配符类型

使用通配符类型可以允许类型变量的变化。例如:

MyClass<? extends Person>

表示上面的泛型的类型变量需要是 Person 的子类。

  • <? extends Class1>上界通配符,类型变量需要是 Class1 的子类,不能往里存,能往外取。
  • <? super Class1>下界通配符,类型变量需要是 Class1 的父类,不能往外取,能往里存。
  • <?>一种无限的符号,代表任何类型都可以

PECS原则

  • 频繁往外读取内容的,适合用上界 Extends。
  • 经常往里插入的,适合用下界 Super。

反射和泛型

反射允许你在运行时分析任意的对象。如果对象是泛型类的实例,关于泛型类型变量则得不到太多信息,因为它们会被擦除。

泛型 Class 类

类型变量十分有用 ,这是因为它允许 Class<T> 方法的返回类型更加具有针对性。

newlnstance 方法返回一个实例,这个实例所属的类由默认的构造器获得。它的返回类型目前被声明为 T,其类型与 Class<T> 描述的类相同,这样就免除了类型转换。

如果给定的类型确实是 T 的一个子类型,cast 方法就会返回一个现在声明为类型 T的对象,否则,抛出一个 BadCastException 异常。

如果这个类不是 enum 类或类型 T 的枚举值的数组,getEnumConstants 方法将返回 null。

getConstructor 与 getDeclaredConstructor 方法返回一个 Constructor<T>对象。Constructor 类也已经变成泛型,以便 newlnstance 方法有一个正确的返回类型。

虚拟机中的泛型类型信息

Java 泛型的卓越特性之一是在虚拟机中泛型类型的擦除。令人感到奇怪的是,擦除的类仍然保留一些泛型祖先的微弱记忆。

可以使用反射 API来确定:

  • 这个泛型方法有一个叫做 T 的类型变量。
  • 这个类型变量有一个子类型限定,其自身又是一个泛型类型
  • 这个限定类型有一个通配符参数
  • 这个通配符参数有一个超类型限定
  • 这个泛型方法有一个泛型数组参数

为了表达泛型类型声明,使用 java.lang.reflect 包中提供的接口 Type。这个接口包含下列子类型:

  • Class类,描述具体类型
  • TypeVariable 接口,描述类型变量(如 T extends Comparable<? super T> )
  • WildcardType 接口,描述通配符(如?super T )
  • ParameterizedType 接口,描述泛型类或接口类型(如 Comparable<? super T> )
  • GenericArrayType 接口,描述泛型数组(如 T[ ] )

笔记大部分摘录自《Java核心技术卷I》,含有少数本人修改补充痕迹。

  • 24
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值