Java泛型原理到实战一文通

一. 泛型的本质是什么:

  1. 是写给编译器使用的“语法糖”
        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<>();
  1. 以下运行结果为真:
        ArrayList<Number> list1=new ArrayList<>();
        ArrayList<Object> list2=new ArrayList<>();
        boolean b=list1.getClass()==list2.getClass();//rs: true

没有新的Class,所有的泛型信息,都会在编译后进行“型别擦除”。

  1. 结论:
    (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);

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,100评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,308评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,718评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,275评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,376评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,454评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,464评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,248评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,686评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,974评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,150评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,817评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,484评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,140评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,374评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,012评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,041评论 2 351

推荐阅读更多精彩内容