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

简介: 泛型魔法:解码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 或 MyClass。而擦除类

型后就变成原始的 MyClass 类型了。

翻译泛型表达式

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

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

约束与局限性

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

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

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

不能实例化类型变置

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

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


不能构造泛型数组

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

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


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

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

public class MyClass<T> {
    // 错误用法
    private static T c;
    // 错误用法
    public static void test(T a) {
    }
}

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

泛型类型的继承规则

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

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

通配符类型

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

MyClass<? extends Person>

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

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

PECS原则

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

反射和泛型

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

泛型 Class 类

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

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

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

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

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

虚拟机中的泛型类型信息

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

可以使用反射 API来确定:

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

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

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

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


相关文章
|
19小时前
|
Java 编译器
【Java探索之旅】解密Java中的类型转换与类型提升
【Java探索之旅】解密Java中的类型转换与类型提升
8 0
|
3天前
|
安全 Java 程序员
Java 泛型类型:变幻中的不变性
【4月更文挑战第21天】
4 1
Java 泛型类型:变幻中的不变性
|
4天前
|
XML SQL 前端开发
【Java】实体字段传参类型线上问题记录
【Java】实体字段传参类型线上问题记录
17 1
|
6天前
|
存储 安全 Java
每日一道Java面试题:说一说Java中的泛型?
今天的每日一道Java面试题聊的是Java中的泛型,泛型在面试的时候偶尔会被提及,频率不是特别高,但在日后的开发工作中,却是是个高频词汇,因此,我们有必要去认真的学习它。
15 0
|
6天前
|
存储 Java 测试技术
一文搞清楚Java中的方法、常量、变量、参数
在JVM的运转中,承载的是数据,而数据的一种变现形式就是“量”,量分为:**常量与变量**,我们在数学和物理学中已经接触过变量的概念了,在Java中的变量就是在程序运行过程中可以改变其值的量。
14 0
|
6天前
|
存储 Java
JAVA变量类型
JAVA变量类型
11 0
|
12天前
|
存储 算法 安全
什么是Java泛型类型?
【4月更文挑战第13天】
13 0
什么是Java泛型类型?
|
17天前
|
Java
Java 16 新玩法:instanceof 升级版,让类型检查更精准
Java 16 新玩法:instanceof 升级版,让类型检查更精准
13 0
|
17天前
|
Java 程序员
编码新风潮:探索Java 10局部变量类型推断
编码新风潮:探索Java 10局部变量类型推断
13 0
|
4月前
|
存储 Java 编译器
【Java变量】 局部变量、成员变量(类变量,实例变量)、方法参数传递机制
【Java变量】 局部变量、成员变量(类变量,实例变量)、方法参数传递机制
41 0