目录
什么是泛型
泛型与编译器
不要轻易使用原生态类型
可以通过通配符类型来替代原生态类型
几个适合原生态类型的场景
消除非受检的警告
什么是非受检警告
如果无法消除警告
本书27-33条主要介绍泛型。首先介绍什么是泛型,它的应用场景是什么。然后重点介绍27和28条对于泛型编程的两点意见。一是不要轻易使用原生态类型,二是尽量消除非受检警告。
什么是泛型
顾名思义“泛型”指的是“多种类型”,声明中具有一种或多种类型参数的类或者接口成为泛型类或者泛型接口,这两者统称为泛型。常见的泛型接口包括List<T>和Set<T>。
泛型与编译器
首先要明确一个概念,泛型的受用范围仅限于编译阶段,编译完成后通过类型擦除进入执行的程序是不带有泛型信息的,下面举一个例子:
//定义一个简单的泛型
public class Myclass<T> {
private T t;
public void setT(T t) {
this.t = t;
}
public T getT() {
return t;
}
}
通过javac将Myclass类编译后,由于类型擦除的原因当你将其反编译后相关的泛型信息会自动被替换成对应的实际类型,以下是通过javap反编译后的内容:
public class Myclass<T> {
public Myclass();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void setT(T);
Code:
0: aload_0
1: aload_1
2: putfield #7 // Field t:Ljava/lang/Object;
5: return
public T getT();
Code:
0: aload_0
1: getfield #7 // Field t:Ljava/lang/Object;
4: areturn
}
这里可以看到,泛型类中的两个泛型方法public void setT(T)和public T get()的输入和输出域原来的泛型T表示都被改换成了Ljava/lang/Object;(这里有的朋友会问为什么从还原的源码中仍然能够看到泛型,那是因为尽管类型擦除移除了泛型的具体类型信息,Java 编译器仍然会在字节码中添加一些额外的信息来支持运行时的类型安全性和泛型的正确使用。而反编译器会利用这些标注自动还原出原有的泛型信息)。
那么在编译阶段编译器会对于泛型做哪些操作来确保类型的安全性和正确性呢?
首先是类型检查:一是类型的兼容性检查,编译器会检查泛型类型的使用是否符合类型兼容性规则。例如,确保泛型参数的使用与上下文中的类型相匹配。二是类型参数限制,如果泛型参数有类型界限(如 T extends Number
),编译器会确保传递给泛型参数的实际类型符合这些界限。
其次是类型推断:编译器能够自动推断出泛型参数的具体类型,这使得开发者在某些情况下不必显式指定泛型参数的类型。比如对于泛型的初始化:
List<String> stringlist = new ArrayList<>();
这里ArrayList<>的初始化就不需要指定对应类型,编译器会自动推断。
类型擦除:编译器在编译阶段会去除泛型类型信息,将泛型类型转换为其对应的原始类型(例如,List<T>
变为 List
)。
插入类型转换:为了确保类型安全,编译器会在适当的位置插入必要的类型转换语句。比如要在一个List<Object>中插入一个Integer,编译器就会讲Integer类型强制转换成Object。
了解了编译器对于泛型进行的各种操作,就可以很好的理解书中的27、28两条。
不要轻易使用原生态类型
原生态类型即隐去了泛型参数信息的类或者接口,比如List是List<String>的原生态类型。这里需要强调的是原生态类型是对应泛型的超类。
很好理解,使用原生态类型以后,类或者接口失去了泛型的属性,自然而然就会绕过编译器的各种类型检测功能,也就失去了泛型在安全性和描述性上所有的优势。也就是说如果程序错误插入了一个不同类型的元素,这个错误会一直保留到运行中当调用这个元素的方法出现问题的时候才会暴露问题;另一方面,编译器无法进行自动的类型转换,因此如果有相关的需求需要靠手动转换。
下面给一个使用了原生态类型的案例,这个原本计划只有Integer的List中混入了String类型,虽然编译器会给出Application.java uses unchecked or unsafe operations.的告警,但是并未显示任何实质性的警告,只有当程序运行中调用到“30”这个元素时才会真正显示错误;另一方面由于编译器未进行插入类型转换,因此对于每一个元素都要进行Integer的强制类型转换,否则系统默认会当作Object来处理。
public class Application {
public static void main(String[] args) {
List list = new ArrayList();
list.add(21);
list.add(23);
list.add("30");
for(Iterator i = list.iterator(); i.hasNext();){
System.out.println((Integer)i.next()+1);
}
}
}
//编译警告
(base) chapter5$ javac Application.java
Note: Application.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
//运行异常
(base) chapter5$ java Application
22
24
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
可以通过通配符类型来替代原生态类型
我们已经知道了原生态类型是有缺陷的,那当需要表示无限制类型的泛型时要如果实现呢?答案是无限制的通配符类型。
通配符类型是泛型功能的扩展,泛型将类和接口的应用类型设置为参数,而通配符就是允许把这个参数设置为一个集合。
public class WildcardDemo {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
List<Number> numList = new ArrayList<>();
// 使用上界通配符
printNumbers(intList);
printNumbers(numList);
// 使用下界通配符
addElement(intList, 100); // intList 是 Number 的子类型
addElement(numList, 100); // numList 本身就是 Number 类型
}
public static void printNumbers(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n);
}
}
public static <T extends Comparable<T>> void addElement(List<? super T> list, T element) {
list.add(element);
}
}
这里printNumbers方法的输入参数是泛型接口List的上界通配符类型,代表该方法可以输入的泛型的类型参数可以是Number的子类。
这里要注意对于无限制通配符类型<?>,不能将任何元素(除null以外)放入,因为编译器无法确定相关元素是否符合类型要求(对于迭代器来说泛型的类型要求是一个未知类型)。
几个适合原生态类型的场景
在类文字中使用原生态类型:类文字是表示类的 Class
对象的一种特殊文字。List.class是合法的,而List<?>.class不合法。
通过instanceof操作符判断类型:用参数化类型使用instanceof操作符是非法的,但是无限制性通配符和原生态类型都是合法的。
消除非受检的警告
什么是非受检警告
首先警告是编译器在编译过程中发现的“异常”,编译过程中会有警告和错误两种“异常”,其中警告是对于运行过程中可能存在问题的提醒,但不影响编译的正常进行,而一旦存在错误将会提前终止编译。下面总结一下编译器的“异常”和程序运行过程中的“异常”。
非受检警告如果被无视就有可能在程序运行中出现各种异常或者错误情况,所以这也是为什么非受检异常无必要消除的原因(另外的弃用警告和未使用变量警告的影响会小一些,但最好还是也注意消除一下)。
如果无法消除警告
如果无法消除,同时证明引起警告的代码是类型安全的,才可以使用一个@SupressWarnings(“unchecked”)注解来禁止这条警告。
@SupressWarnings(“unchecked”)注解可以用在任何粒度的级别中,从单独的局部变量声明到整个类都可以,但是请尽量在尽可能小的范围内使用,否则可能会掩盖重要的警告(永远不要直接在类上使用)。
注意每次使用该注解时,都要添加一条注释,说明这么做为什么是安全的。