泛型魔法:解码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》,含有少数本人修改补充痕迹。


相关文章
|
4天前
|
机器学习/深度学习 安全 Java
Java 泛型
5月更文挑战第17天
|
5天前
|
安全 Java 编译器
Java的泛型
Java的泛型
16 1
|
8天前
|
安全 Java 编译器
Java一分钟之——泛型方法与泛型接口
【5月更文挑战第20天】Java泛型提供编译时类型安全检查,提升代码重用和灵活性。本文探讨泛型方法和接口的核心概念、常见问题和避免策略。泛型方法允许处理多种数据类型,而泛型接口需在实现时指定具体类型。注意类型擦除、误用原始类型和泛型边界的理解。通过明确指定类型参数、利用通配符和理解类型擦除来避免问题。泛型接口要精确指定类型参数,适度约束,利用默认方法。示例代码展示了泛型方法和接口的使用。
34 1
Java一分钟之——泛型方法与泛型接口
|
8天前
|
存储 安全 Java
Java一分钟之-泛型擦除与类型安全
【5月更文挑战第20天】Java泛型采用类型擦除机制,在编译期间移除泛型信息,但在编译阶段提供类型安全检查。尽管需要类型转换且可能产生警告,但可以通过特定语法避免。使用泛型时应注意自动装箱拆箱影响性能,无界通配符仅允许读取。理解这些特性有助于编写更安全的代码。
35 4
|
1天前
|
Java
Java初识泛型 | 如何通过泛型类/泛型方法获取任意类型的三个数的最大值?
本文介绍了如何使用Java中的泛型来实现一个可以比较任意数值类型最大值的功能。。
11 2
|
3天前
|
存储 安全 Java
Java泛型:原理、应用与深入解析
Java泛型:原理、应用与深入解析
|
4天前
|
存储 Java 编译器
Java泛型类型擦除以及类型擦除带来的问题
Java中的泛型是伪泛型,编译时泛型信息会被擦除,例如ListString和ListInteger在JVM中都变为List。泛型擦除后,类型检查主要在编译时完成,针对的是引用而非实际对象。例如,ArrayListString的原始类型是ArrayList,但编译时会对引用调用的方法进行类型检查。类型转换由编译器自动处理,如PairDate的value在访问时会自动转换为`Date`。泛型不能用于基本类型,如ArrayListdouble应写作ArrayListDouble。静态方法和静态类不能使用泛型类的类型参数,但可以定义泛型静态方法。
140 0
|
5天前
|
存储 安全 Java
【JAVA学习之路 | 进阶篇】<-泛型->
【JAVA学习之路 | 进阶篇】<-泛型->
|
9天前
|
安全 Java API
Java一分钟之-泛型通配符:上限与下限野蛮类型
【5月更文挑战第19天】Java中的泛型通配符用于增强方法参数和变量的灵活性。通配符上限`? extends T`允许读取`T`或其子类型的列表,而通配符下限`? super T`允许向`T`或其父类型的列表写入。野蛮类型不指定泛型,可能引发运行时异常。注意,不能创建泛型通配符实例,也无法同时指定上下限。理解和适度使用这些概念能提升代码的通用性和安全性,但也需兼顾可读性。
31 3
|
13天前
|
Java 编译器
[java进阶]——泛型类、泛型方法、泛型接口、泛型的通配符
[java进阶]——泛型类、泛型方法、泛型接口、泛型的通配符