对泛型深入浅出的认识

最近终于开始总结自己理解的东西,终于有时间写写自己对一些知识的认识。文笔不好,还望各位包含。以下知识点是我看了享学课堂的资料,对其中的内容进行汇总。

本预先资料来源于Oracle官方文档Java™ 教程-Java Tutorials

官方文档:https://docs.oracle.com/javase/tutorial/java/generics/index.html

中文翻译:https://pingfangx.github.io/java-tutorials/java/generics/types.html

下面我以问题的方式,阐明对泛型深入浅出的认识:

1.泛型是什么?

答:泛型是JDK5引入的一种参数化类型特性。JDK7及以上,泛型的菱形可以推断参数化类型:把类型当参数一样传递。数据类型只能是引用类型。

举个栗子:Plate<T>中的T是类型参数;Plate<Banana>的Banana是实际类型参数;

                   Plate<T>整个称为泛型类型;Plate<Banana>整个称为参数化的类型;

2.为什么使用泛型,使用泛型的好处?

答:1.代码更健壮(只要在编译器没有警告,运行期就不会出现ClassCastException);

        2.代码更简洁(不用强转)

         3.代码更灵活,可复用。

3.泛型包含哪些?

答:泛型只有三种情况:泛型接口,泛型类,泛型方法

//泛型接口

public interface Plate<T> {

      public void set(T t);

       public T get();

}


//泛型类

public class AiPlate<T> {

       private T t;

      public AiPlate(T t){ 

             this.t = t;

       }

       public void set(T t){ 

           this.t = t;

       }

       public T get(){

              return t;

        }

        //泛型方法

        public <U> void addFruit(U u) {

         }

}


4.泛型限定有哪些?

答:?无限定 ; extends 限定上界;super 限定下界        后面会以一个通熟易懂的口诀演示记住他

Class A {}

Class B {}

Interface C{}

Interface D{}

public class ClassTest<T extends A & C & D>{}  //this is ok  可以实现多个接口

public class ClassTest<T extends A & B & C & D> //error,  因为java 只支持单继承


在这个体系中,上界通配符 “Plate<? extends Fruit>” 覆盖下图中蓝色的区域。



Plate<? super Fruit>是Plate<Fruit>的基类,但是不是Plate<Apple>的基类,对应上面例子,Plate<? super Fruit>覆盖的下图中红色的区域

5.JAVA泛型的原理?什么是泛型擦除机制?

答:Java的泛型是JDK5新引入的特性,为了向下兼容,虚拟机其实是不支持泛型,所以Java实现的是一种伪泛型机制,也就是说Java在编译期擦除了所有的泛型信息,这样Java就不需要产生新的类型到字节码,所有的泛型类型最终都是一种原始类型,在Java运行时根本就不存在泛型信息。

泛型的擦除机制:如果有父类被擦除成父类,否则被擦除成Object。如果有继承,擦除之后会生成桥接方法,来解决类型擦除后保留泛型类型的多态性;桥方法在调用父类方法前,会将object进行强转成父类。

从 .java文件编译到.class文件会产生泛型擦除的残留,保留了定义的格式,方便分析字节码。类的常量池保留了泛型信息,可以通过反射API拿出type类型信息。


6.Java编译器具体是如何擦除泛型的?

答:step 1. 检查泛型类型,获取目标类型

        step 2. 擦除类型变量,并替换为限定类型

                    如果泛型类型的类型变量没有限定(<T>),则用Object作为原始类型

                    如果有限定(<T extends XClass>),则用XClass作为原始类型

                     如果有多个限定(T extends XClass1&XClass2),则使用第一个边界XClass1作为原始类

        step 3. 在必要时插入类型转换以保持类型安全

        step 4. 生成桥方法以在扩展时保持多态性

7.使用了泛型,后留下很多“后遗症”

7.1 泛型类型变量不能使用基本数据类型

答:比如没有ArrayList<int>,只有ArrayList<Integer>.当类型擦除后,ArrayList的原始类中的类型变量(T)替换成Object,但Object类型不能存放int值


7.2 不能使用instanceof 运算符

答:因为擦除后,ArrayList<String>只剩下原始类型,泛型信息String不存在了,所有没法使用instanceof

ArrayList<String> strings = new ArrayList<>();

if(strings instanceof ArrayList<?>){}  //可以   被擦除之后的ArrayList本来就是未知的,所以这个可以

if(strings instanceof ArrayList<String>) //不可以


7.3 泛型在静态方法和静态类中的问题

答:因为泛型类中的泛型参数的实例化在定义泛型类型对象(比如ArrayList<Integer>)的时候指定的,而静态成员是不需要使用对象来调用的,所有对象都没创建,编译器不知道如何确定这个泛型参数是什么。


7.4 泛型类型中的方法可能会冲突

答:因为擦除后两个equals方法变成一样的了。擦除之后T变成Object,而Object中的equals是默认被实现了的。所以重复了。

public boolean equals(T t){ 

       return super equals(t)

}

public boolean equals(Object obj){ 

       return super equals(obj)

}

7.5 没法创建泛型实例

答:因为类型不确定,没法直接new对象。

public static <E> void append(List<E> list){

          E elem = new E(); //不行,因为不知道E的具体类型

}

但是下面这种方式是可以的,通过反射来new对象;

public static <E> void append(List<E> list, Class<E> cls) throws Exception{

        E elem = cls.newInstance();  // OK

         list.add(elem);

}

7.6 没有泛型数组

答:因为数组可以协变,擦除后就没法满足数组协变的原则。

T[] arr = new T[10]    //不可以   不知道T代表那个类


另外:Apple extends Fruit

Apple[] 的父类是Fruit[] 这个叫数组的协变。

其中数组协变中:父类可以持有子类,子类持有父类可能会报转化异常。


List<Apple> 和 List<Fruit>在擦除之后运行期不知道是什么类型了,都是不满足协变原则了;数组是可以协变的,但是list是不会协变的。


另外假如:A extends B

Plate<B> = Plate<A>是不允许的,因为在泛型中,不管AB是什么关系,Plate<A>和Plate<B>没有任何关系。


8.通配符和泛型的关系?

答:通配符可以让泛型转化更灵活。


9.说出以下类型的区别?

Plate 普通的盘子类,编译时不会做类型检查

Plate<Object> 参数化的plate类,他在编译之后,字节码的类中的方法是Object

Plate<?> 非限定通配符下的泛型类,泛型?表示类型未知,等价于Plate<? extends Object>

Plate<T> 泛型Plate类,T是未知类型,编译后会被擦除成Object对象

Plate<? extends T> 限定上界的泛型类

Plate<? super T> 限定下界的泛型类


10.泛型边界的熟记口诀:

*extends 上界

* super  下界

* 说明:以下方便记忆的:“上”表示父类,“下”表示子类

* 口诀:

* 上界只可往上读,往上赋值;方法只能传递自己或者子类

* 下界只可向下写,向下赋值;方法只能传递自己或者父类


验证口诀的正确性:

import java.util.ArrayList;

import java.util.List;

/** * extends 上界

* super 下界

* 说明:以下方便记忆的:“上”表示父类,“下”表示子类

* 口诀:

* 上界只可往上读,往上赋值;方法只能传递自己或者子类

* 下界只可向下写,向下赋值;方法只能传递自己或者父类

* * 类关系: apple extends fruit extends food */

public class TestBoundary {

          public static void main(String[] args) {

            //演示extends赋值的上界

            List<Food> foods = new ArrayList<>();

            List<Apple> apples = new ArrayList<>();

            List<Fruit> fruits = new ArrayList<>();

            //fruits = apples; //error 往上赋值需要 extends

            List<? extends Fruit> fruits1 = apples; //向上赋值成功


            //演示向上读取

            Fruit fruit = fruits1.get(0); //ok 上界修饰的可以往上读

            Food food = fruits1.get(0); //ok 上界修饰的可以往上读

            Object object = fruits1.get(0); //ok 上界修饰的可以往上读

            //Apple apple = fruits1.get(0); //error 不能往下读


            //演示方法传递

            eat1(fruits); //ok 传递自己

            eat1(apples); //ok 传递子类

            //eat1(foods); //error 传递了父类


            //演示Super赋值的下界

            List<Food> foodsLower = new ArrayList<>();

            List<Apple> applesLower = new ArrayList<>();

            List<Fruit> fruitsLower = new ArrayList<>();

            //fruitsLower = foodsLower; //error 往下赋值需要super修饰

            List<? super Fruit> fruitList = foodsLower; // 向下赋值ok了


            //演示向下写

            fruitList.add(new Apple(1)); //用下写是ok的

            //fruitList.add(new Food()); //error 用上写是不行的;


            //演示下界方法只能传递自己或者父类

            eat2(fruitsLower); //传递自己ok

            eat2(foodsLower); // 传递父类ok

            //eat2(applesLower); //传递子类error

            }

            public static <T> void eat1(List<? extends Fruit> fruits){

            }

            public static <T> void eat2(List<? super Fruit> fruits){

            }

}


11.最后说了半天规则,用实例演示以下泛型有什么用处

import java.util.ArrayList;

import java.util.Collections;

import java.util.List;

/**

* 泛型的简单应用

* 从没用泛型慢慢演进 */

public class Test {

            //普通不使用泛型的方法。

            //有个需求需要把香蕉或者更多水果copy一下,难道需要重新写N个copy方法么?

            //答案不是的,出现了泛型copy2

            public static void copy1(List<Apple> dest,List<Apple> src){

                        Collections.copy(dest,src);

            }


            //现在可以把无数多相同的水果相互copy了,但是又有一个需求了

            //现在苹果是确定了的,我想想找一个水果list把苹果放进去,于是就出现了copy3

            public static <T> void copy2(List<T> dest,List<T> src){

                        Collections.copy(dest,src);

            }


            //现在找到一个水果list放苹果了。但是现在还有一个需求

            //就是现在水果list确定了,我要放的水果不确定,于是有了copy4

            public static <T> void copy3(List<? super T> dest,List<T> src){

                        Collections.copy(dest,src);

            }


           //这个就是Collections.copy的终极用法了。

           //public static <T> void copy(@RecentlyNonNull List<? super T> dest, @RecentlyNonNull List<? extends T> src)

           public static <T> void copy4(List<? super T> dest,List<? extends T> src){

                        Collections.copy(dest,src);

            }


            public static void main(String[] args) { 

                    //新建两个苹果list

                    List<Apple> apples = new ArrayList<>();

                    apples.add(new Apple(1));

                    List<Apple> apples1 = new ArrayList<>();

                    apples1.add(new Apple(2));

                    //新建两个香蕉list

                    List<Banana> bananas = new ArrayList<>();

                    bananas.add(new Banana(1));

                    List<Banana> bananas1 = new ArrayList<>();

                    bananas1.add(new Banana(2));

                    //新建水果list

                    List<Fruit> fruits = new ArrayList<>();

                    fruits.add(new Apple(10)) ;   // 先要告诉编译器fruit是放的苹果,否者copy3的dest参数编译器不知道放什么


                    //普通方法

                    copy1(apples,apples1);


                    //使用泛型方法

                    copy2(bananas,bananas1);    


                    //苹果往水果盘子放

                    Test.<Apple>copy3(fruits,apples);


                        //注意这里苹果是确认的了

                        //水果list确定了放任意水果

                        Test.<Fruit>copy4(fruits,apples);

                }

}


测试代码:https://github.com/oujie123/UnderstandingOfGeneric


最后非常感谢各位能看完,希望对大家有收获,欢迎留言一起交流

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