一. 泛型的本质是什么:
- 是写给编译器使用的“语法糖”
List<String> list=new ArrayList<String>(); //<String>会被编译器看到,但会在字节码中删除
list.add("abc");//编译成功:编译器对“abc” instaceof String检查,符合通过
// list.add(100); //编译失败:编译器对100 instaceof String检查,不符合失败
String s=list.get(0); //编程器将代码转换为:String s=(String)list.get(0); 再进行编译
而JDK6+的编译器将更加智能,第一行代码可以自动推测出对象实际类型的“泛型类型”
List<String> list=new ArrayList<>();
- 以下运行结果为真:
ArrayList<Number> list1=new ArrayList<>();
ArrayList<Object> list2=new ArrayList<>();
boolean b=list1.getClass()==list2.getClass();//rs: true
没有新的Class,所有的泛型信息,都会在编译后进行“型别擦除”。
- 结论:
(1) 泛型是在源代码中加入的语法,为编译器提供:(1)编译检查(2)源代码生成,这两大工作。
(2) 扩展认知:“注解技术(Annotation)”实质上是对泛型技术的扩展,通过我们对源代码的“修饰”(加入@xxx),从而让编译器(进一步:可以是执行器)获取更多的“糖”,从而可以做到:语法检查(@Override)、源代码生成(@Getter)、运行特殊生命周期(@Autowared)等一系列的工作,而以上这些工作,只需要在源代码中修改,而不在需要使用繁琐的XML,从而使用Java以一种全新的姿态展现给开发者,即:后Java时代。
(3) 回想下:junit,lombok,spring,mybatis,JPA,spirng-cloud等,都是在这种大技术背景下的产物。
二. 泛型的作用:
1. 可以让编译器提前检查 一些运行时异常:
List<String> list=new ArrayList<String>();
list.add(100);//编译失败:
2. 让“数据类型参数化”:
(1)如下代码完成了一个通用的“结果类型”设计:
public class CommonResult<T> {
private int code;
private String message;
//可以认为:T 是一种可变的数据类型(数据类型参数化)
private T entity;
public T getEntity(){
return entity;
}
public void setEntity(T entity){
this.entity=entity;
}
}
如此设计:CommonResult将不会加为entity的数据类型有多种可能,而去设计出相应的CommonResult。
(2)结论:变量名称的参数化,让“方法的调用”与“方法的实现”在【变量名称】达到解藕的目的;变量类型的参数化,让“方法的调用”与“方法的实现”在【变量类型】达到解藕的目的。
(3)联想:java认为“数据类型”是“算法+数据结构”的结合体,但FaaS时代的迫切任务是:把“算法”独立成为一种数据类型,“Java的Lamda”就是为此而诞生(以前是使用接口完成,但太笨重了!!!,此论点与泛型关系不大,但需要注意会有Lamda类型参数的存在,同样可以结合泛型,达到更灵活的目的)
三、泛型语法施加的主体有哪些:
1. 泛型类:
以下形式都是泛型类,T、E、V代表着可变的数据类型,class中任何出现变量类型的地方都可以使用(域,形参类型,返回类型、局部变量类型、异常类型), 以下是一个相对极端的例子:
public class CommonResult<T,E,V extends Throwable> {
private T entity;
private E e;
private V v;
public CommonResult(T entity,E e,V v){
this.entity=entity;
this.e=e;
this.v=v;
}
public E ProcessEntity(T entity) throws V{
this.entity=entity;
if(Math.random()>0.5) {
throw v;
}
return e;
}
...
要引起注意的是: T,E,V代表着未知的类型,所以在代码中不可以做任何有假设其类型的行为:如new(假设它是一个非抽象类),instanceof(假设它是某一类型)等操作,这也是使用泛型的副作用,没有银弹。
2. 泛型接口:
public interface WorkService<T> {
T produce();
void consume(T t);
}
3. 泛型方法:
public <M> M process(M m){
return m;
}
以上代码理解:
(1) 此方法可以位于泛型或非泛型类中
(2)<M> 的作用有两个:一是声明此方法是泛型方法;二是此方法中可以使用M做为参数化类型的符号。
(3)常见的错误:认为在泛型类,使用了带有参数化类型的方法,就是泛型方法:
public interface WorkService<T> {
T produce();//这是不是泛型方法,只是使用了参数化类型符号的方法
}
四、所谓的“形参”和“实参”问题:
在定义类、接口、方法时,相当于定义了数据类型的“形参”,传入方法总结如下:
1. 【声明变量类型】时传入实参:
List<String> list;
WorkService<Number> ws;
此时,泛型类或接口中所有的包含T(或其它)的变量声明将被Strring或Number替换,语法检查机制和代码生成机制将随之变化。
需要注意,并不需要在实例化时指定,目前的编译器可以智能的填入。
2. 【实现或继承时】传入实参:
public interface WorkService<T> {
T produce();
void consume(T t);
}
//=========实现或继承式传入"类型实参"==============//
class TomWrok implements WorkService<String>{
@Override
public String produce() {
return null;
}
@Override
public void consume(String t) {
}
}
注意几点:
(1)此时,所有的替换工作必须由硬编码方式完成,
(2)TomWork类并不是泛型类,当然也可以根据需要将之变成泛型类,但此时和它的泛型接口r 的泛型类型已经没有任何关系了,因为已经确定为String类型了。
/**============================================
* 实现或继承式传入"类型实参"
* 继承将Tomwork变为泛型类,T只是符号,可以是任何字母
* TomWrok确定了接口中实参,但又引入了新的形参
============================================**/
class TomWrok<T> implements WorkService<String>{
private T t;
@Override
public String produce() {
return null;
}
@Override
public void consume(String t) {
}
}
(3)以这种方式,只是将形参进行了传递,并没有实现,子类仍为泛型类
class Work<T> implements WorkService<T>{
@Override
public T produce() {
return null;
}
@Override
public void consume(T t) {
}
}
3. 【方法调用时】传入:
此方式只是针对“泛型方法”:
public static <M> M process(M m) {
return m;
}
public static void main(String[] args) {
String s = "abc";
String rs = process(s);//由传入实参的声明类型做为实参
Number m = new Integer(123);
Number rm = process(m);//由传入实参的声明类型做为实参
int i = process(123);//由传入实参的实际类型推断做为实参
}
四、对“参数化类型”的模糊限定:
1. 原理:
(1) 我们的需要是:全方面的“IS 关系”的检查,但是目前编译器做不到!!!
Java编译器,可以对变量类型进行"IS关系"进行检查,但无法对”泛型类型“的变量中的参数化类型进行”IS关系检查“。即:List<Number> :只能对ArrayList IS List
的检查,但是不能做Integer IS Number
的检查。
上述问题:就是本节要讨论的解决方法,我们要进行【准全面检查】
原因是:
List<String>
和List<Integer>‘是同一个List class类型,这也称为类型擦除,这样编译器在进行”参数化类型的检查时“,就无法获取有效的信息;但实际上编译器可以做的更加高级,从源代码中获取类型,再进行递归方式的检查,但这样就会把编译器做的足够的复杂,导致编译速度过慢,从而导致基于编译器的工具链不具备生产价值。
Java在运行时无法对传入“由泛型类”生成的对象,进行“泛型类型”检查。即:List<String>
和List<Integer>‘是同一个List class类型(这称为类型擦除)。
观察如下案例:
/*
* @param list:此时只能通过基于 "IS List"的检查,
* 而不能通过"IS LIST && IS Number"的检查
*/
public static void fn1(List<Number> list){
}
public static void main(String[] args) {
ArrayList<Number> list1=new ArrayList<>();
/**
* 此时编译器有足够的"类型信息",对"IS List"进行检查,此时编译检查通过
*/
fn1(list1);
ArrayList<Integer> list2=new ArrayList<>();
/**
* 此时编译器有足够的"类型信息",对"IS List"进行检查,
* 但没有足够的信息,对"IS Number"进行检查(因为型别擦除,同时现阶段编译器没有能力读到Number后进行检查)
* 基于安全角度,此时编译不通过(信息不足,任可错杀)
*/
//fn1(list2);
}
2. 通配符闪亮登场:
泛型体系中设计了“?extends”,原理是对“参数化类型”进地适当的限定,从而使编译器适当的介入,达到在编译期读取足够多的信息,完成“准全方面的”的“IS”检查。
观察如下案例:
public static void fn2(List<? extends Number> list){
}
public static void main(String[] args) {
ArrayList<Integer> list2=new ArrayList<>();
/**
* 此时编译器有足够的"类型信息",对"IS List"进行检查,
* 也有了足够的信息,对"IS Number"进行检查(不是从类信息查询,而是从源代码中读到了 extends Number后进行检查)
* 此时:ArrayList IS List && Integer IS Number,编译通过
*/
fn2(list2);
ArrayList<String> list3=new ArrayList<>();
//此时:ArrayList IS List 但是 String IS Not Number,编译不通过
// fn2(list3);
...
}
泛型体系又进而衍生出了一种更加“高级”的方式:“? super ”,完成了“参数化类型”的向上检查。
观察以下案例:
public static void fn3(List<? super Number> list){
}
public static void main(String[] args) {
ArrayList<Integer> list4=new ArrayList<>();
//list4 is List;但是 Integer不是Number的祖先,编译不通过
//fn3(list4);
ArrayList<Object> list5=new ArrayList<>();
//list5 is List && Object 是 Number的祖先,编译通过
fn3(list5);
...
}
五、使用通配符的副作用(建议把前面的熟悉之后再看):
没有银弹,在使用通配符时,我们会根据对参数的不同操作采用不同的?与 extends、super的组合,一般原则称为:PECS(Producer Extends Customer Super),案例如下:
1. Producer Extends :
public static void fn2(List<? extends Number> list){
/**
* 进行消费类型的操作(传入对象),是不安全的
* 由于使用extends,代表着"参数化类型"有上限,而无下限,
* 此时可以使用ArrayList<BigDecimal> 类型的实参,从而导致运行时的异常,编译器为预期的错误而禁止
*/
// list.add(100); //针对任何类型的参数,消费类型操作都无法进行,这是希望看到的结果
/**
* 进行生产类型的操作(产生该类型的对象),将是大体上安全的
* 如果获取的引用声明是Number或父类型,是绝安全的
* 如果是Number的子类型,需要做向下强转,但不一定安全
*/
Number m1=list.get(0);
Integer m2=(Integer) list.get(0);
Object o1=list.get(0);//多态引用:Object ref到Number,没有问题
/**
* String并不在Number的继承树之下,这会在编译期中被检查出来,而无法通过
* 这也是使用extends的原动力:即:Producer use Extends==PE
*/
//String s1=(String)list.get(0); 编译无法通过,是希望看到的结果
}
public static void main(String[] args) {
ArrayList<BigDecimal> list1=new ArrayList<>();
fn2(list1);//此时fn2中如果使用list.add(100),将发生错误,所以编译器不允许通过
...
}
2. Producer use Extends
public static void fn3(List<? super Number> list){
//传入的list可以是List<Number的祖先们>,所以无法直接由编译生成强转的代码,只能硬编码
// Number number= list.get(0);
Number number=(Number)list.get(0);//硬编码强转,提醒我们不应该在Producer中使用 super
/**
* 此时可以确认"参数化类型的下限是Number",此时传入fn3的list只能是:"IS LIST && ?是Number的祖先",
* 形如:ArrayList<Object> 这样的类型,
* 此时如下的操作都是安全的,
*/
list.add(100);
list.add(100.0);
Number n=new Float(2.4);
list.add(n);
//Number的父类型,只能强转,提醒我们安全的消费类型是Number
list.add((Number) new Object());
/**
* 由于String 并不是Number的祖先,所以编译器直接检出错误,
* 即: Consumer use Super
*/
// list.add("abc"); 这是我们希望看到的结果
}
public static void main(String[] args) {
ArrayList<Number> list1=new ArrayList<>();
fn3(list1);
ArrayList<Object> list2=new ArrayList<>();
fn3(list2);