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


相关文章
|
1月前
|
Java
实现java执行kettle并传参数
实现java执行kettle并传参数
30 1
|
1月前
|
存储 Java 开发者
Java 中 Set 类型的使用方法
【10月更文挑战第30天】Java中的`Set`类型提供了丰富的操作方法来处理不重复的元素集合,开发者可以根据具体的需求选择合适的`Set`实现类,并灵活运用各种方法来实现对集合的操作和处理。
|
1月前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
76 2
|
1月前
|
存储 Java 编译器
Java泛型类型擦除以及类型擦除带来的问题
泛型擦除是指Java编译器在编译期间会移除所有泛型信息,使所有泛型类型在运行时都变为原始类型。例如,`List&lt;String&gt;` 和 `List&lt;Integer&gt;` 在JVM中都视为 `List`。因此,通过 `getClass()` 比较两个不同泛型类型的 `ArrayList` 实例会返回 `true`。此外,通过反射调用 `add` 方法可以向 `ArrayList&lt;Integer&gt;` 中添加字符串,进一步证明了泛型信息在运行时被擦除。
46 2
|
2月前
|
Java API
[Java]泛型
本文详细介绍了Java泛型的相关概念和使用方法,包括类型判断、继承泛型类或实现泛型接口、泛型通配符、泛型方法、泛型上下边界、静态方法中使用泛型等内容。作者通过多个示例和测试代码,深入浅出地解释了泛型的原理和应用场景,帮助读者更好地理解和掌握Java泛型的使用技巧。文章还探讨了一些常见的疑惑和误区,如泛型擦除和基本数据类型数组的使用限制。最后,作者强调了泛型在实际开发中的重要性和应用价值。
52 0
[Java]泛型
|
1月前
|
Java
在Java中定义一个不做事且没有参数的构造方法的作用
Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。
|
2月前
|
Java 编译器
Java“返回类型为 void 的方法不能返回一个值”解决
在 Java 中,如果一个方法的返回类型被声明为 void,那么该方法不应该包含返回值的语句。如果尝试从这样的方法中返回一个值,编译器将报错。解决办法是移除返回值语句或更改方法的返回类型。
260 5
|
2月前
|
设计模式 Java
Java“不能转换的类型”解决
在Java编程中,“不能转换的类型”错误通常出现在尝试将一个对象强制转换为不兼容的类型时。解决此问题的方法包括确保类型间存在继承关系、使用泛型或适当的设计模式来避免不安全的类型转换。
299 7
|
2月前
|
存储 安全 Java
🌱Java零基础 - 泛型详解
【10月更文挑战第7天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
22 1
|
2月前
|
Java
Java 中锁的主要类型
【10月更文挑战第10天】