泛型详解与解析总结


  • tags:泛型
  • categories:笔记
  • date: 2017-06-10 11:24:24

在java代码开发中,泛型的使用是非常普遍的,特别是在集合中。那么,其实java中的泛型并不是真正意义上的新的类型,只是java语法中的语法糖已。便于java编译器在编译源代码的时候进行类型校验,今早发现程序错误而已。但是对于我们程序员来说的话,有了泛型,确实会对我们程序开发,无论是代码可读性,还是开发效率都有一定的积极作用。所以,就打算来分析分析泛型内容,是怎么来的,有什么内容,该如何使用。


image

泛型Type与Class

下面可以来讨论看看泛型-Type接口-Class类型之间的关系?

  • 泛型出现之前的类型
    在JDK1.5之前,没有引入泛型类型的概念的时候,只有所谓的原始类型(相对于泛型参数化的List<String>来说,List就是原始类型),所有的原始类型都是通过字节码文件类Class类进行抽象。java.lang.Class类的一个具体对象就代表一个指定的原始类型。(eg: String原始类型抽象类即为:String.class....)

  • 泛型出现后的类型
    泛型<>类型出现之后,就扩充了数据类型。即在原始类型的基础上,添加泛型的特性,就能代表或者说表达更多的数据类型。从只有原始类型扩充了参数化类型(ParameterizedType),类型变量类型(TypeVariable),泛型限定的参数化类型(含通配符+通配符限定表达式。即 ? extends , super等),泛型数组类型(GenericArrayType)。

  • 3. 与泛型有关的类型不能和原始类型统一到Class的原因(泛型类型与对应的原始类型对应相同的Class)
    上面这个原因要说的意思是:泛型类型所代表的Class对象与对应的原始类型对应的Class是同一个对象,例如:List<String>,List<Integer>..等等与原始类型List,所代表的Class类都是java.util.List。因为会有"类型擦除"原因。

【1】产生泛型擦除的原因
本来新产生的类型+原始类型都应该统一成各自的字节码文件Class类型对象,但是由于泛型不是最初的Java的基础部分,如果真的要加入的真正的泛型,会涉及到JVM指令集的修改,这样代价就太大了。多说一句:在java中的泛型并不是代表着真正的新的类型,只是一个语法糖,便于类型检查和编译器优化的手段而已

【2】Java中如何引入泛型
为了使用泛型的优势而又不真正引入泛型,java采用“泛型擦除”的机制来引入泛型这个语法糖。所以Java中的泛型仅仅是给编译器javac使用的,用来确保数据的安全性和免去强制类型转换的麻烦。结果就是,一旦含有泛型声明和定义使用的代码地方,经过编译完成之后,所有和泛型有关的类型就会被全部擦除,也就是被清理掉。(List<String> list = new ArrayLIst<String>() 会编译成 List list = new ArrayList( ))

【3】Class不能表达与泛型有关的类型
由于编译器的"擦除"机制,就造成了与泛型有关的如参数化类型,类型变量类型,泛型限定的参数化类型,数组类型,都会被全部打回原形,在字节码文件中中全部都是泛型擦除后的原始类型,并不存在和自身类型一直的字节码文件。所以,和泛型相关的新扩充进行的类型不能被统一到Class类中,即List<String>并不能用类似List<String>.class这个Class类文件来表示类型。

【4】 与泛型有关的类型在java中的表示
为了通过反射操作这些类型已迎合实际的开发需要,Java就新增加了ParameterizedType,GenericArrayType,TypeVariable,WildcardType这几种类型来代表不能被归一到Class类中的类型,但是又是和原始类型不同且齐名同等级的类型。

【5】 Type接口的引入:统一与泛型有关的类型和原始类型Class

  • 引入Type的原因
    为了程序的扩展性,最终引入了Type接口作为Class,ParameterizedType,GenericArrayType,TypeVariable,WildcardType这几种类型的公共父接口。这样实现了Type接口类型参数,并可以接收以上五种子类的实参或者返回值类型就是Type型的参数。
  • Type接口中没有方法的原因
    从上面看到,Type接口出现仅仅起到了通过多态来达到程序类型扩展性提高的作用,并没有其他的作用。所有,没有必要提供任何方法。

泛型种类与使用

在Java中,接口Type用于表示所有类型的高级公共接口,也是Class类也实现了该接口。Type类型的所有子类型包括:原始类型,参数化类型,数组类型,类型变量和基本类型。
Type中包括了以下几种实现:

public interface Type {
}
//说明class类可以转化为Type类型
public final
    class Class<T> implements  java.lang.reflect.Type...
    
Type(所有子接口)
   -GenericArrayType
   -ParameterizedType
   -TypeVariable<D extends GenericDeclaration>
   -WildcardType

下面分别总结每种类型的含义和使用场景。

参数化类型-原始类型

在Java中,参数化类型使用java.lang.reflect.ParameterizedType表示。其源代码如下,比较重要的或者说通常使用的是前两个:

public interface ParameterizedType extends Type {
    //获取参数化类型中的实际参数类型(eg. String.class..)
    Type[] getActualTypeArguments();
    //获取参数化类型中的原始类型
    Type getRawType();

下面通过一段示例代码来说明:

import Java.util.*;
//声明有泛型的类
public class Pair<T> {
    private final T first;
    private final T second;

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T first() {
        return first;
    }

    public T second() {
        return second;
    }

    //申明返回List<String>的集合
    public List<String> stringList() {
        return Arrays.asList(String.valueOf(first), String.valueOf(second));
    }

    public static void main(String[] args) {
        Pair p = new Pair<Object>(23, "skidoo");
        for (String s : p.stringList())
            System.out.print(s + " ");
    }
}

上述程序会报错,错误在声明p变量时候,没有指明参数类型。这段程序没多大实用效果,仅仅用来说明。
若是在程序中有如下声明:List<String> list = new ArrayList<String>()的时候,对于List<String>来说,List集合类型就是原始类型,集合内部元素类型被参数固定的List<String>就是参数化类型,即参数化类型ParameterizedType也是一种类型,但是是包含内部元素类型被固化的一种类型,通常都是基于泛型<>来声明的。关于代码中泛型类型使用,有以下几点需要说明:

  • Java参数化类型相对于C++的参数化类型而言,有本质区别。前者只是编译器的类型检查的一种手段,而后者则是真正的不利于原始类型的新的类型。
  • 一个原生类型很像其对应的参数化类型,但是它的所有实例成员都要被替换掉,而替换物就是这些实例成员被擦除掉对应部分之后剩下的东西。具体地说,在一个实例方法声明中出现的每个参数化的类型都要被其对应的原生部分所取代。我们程序中的变量p是属于原生类型Pair的,所以它的所有实例方法都要执行这种擦除。这也包括声明返回List<String>的方法stringList。编译器会将这个方法解释为返回原生类型List。
//源代码中声明
List<String> list = new ArrayList<Stirng>();
list.add("test");

//编译器编译后class字节反汇编得到如下:
List list = new ArrayList();
list.add(String.valueOf("test");
//也就说明了类型擦出和参数化类型实际上,仅仅参数化类型与原始类型差不多,仅仅是参数化类型可以在我们写代码时候,在编译期间进行类型检查而已,仅仅是编译器类型安全检查的一种机制。
  • 不同的类型参数组成的泛型类型,其class的类型都和原始类型的class的类型完全相同。也说明了不同类型参数组成的参数化类型之间可以进行强制类型转换。

接下来,再看看如何在程序中使用ParameterizedType。可以通过该类获取参数化类型中类型参数的实际类型,也就是对应的Class对象。

public class GenericParamType { 
public static void applyMethod(List<Date> list) throws Exception  
    {  
        Method m = GenericParamType.class.getMethod("applyMethod",List.class);  
        // 其返回是参数的类型
        Type[] t1 = m.getParameterTypes();
        //其返回的是参数的参数化的类型,里面的带有实际的参数类型
        Type[] t2 = m.getGenericParameterTypes();  
        Method m2 = GenericParamType.class.getMethod("main",String[].class);  
        //参数里面如果不是参数化类型的话,那么 getGenericParameterTypes就返回与 getParameterTypes 一样   
        Type[] t3 = m2.getGenericParameterTypes();
        Type[] t4 = m2.getParameterTypes();  
        out.println(t1[0]);//interface java.util.List    
        out.println(t2[0]);//java.util.List<java.util.Date>  
        //我们通过 getGenericParameterTypes 得到的是 List<Date>,那么我们怎么能得到它的参数化类型的实例呢  
        // type 还有好多子接口,我们通过子接口来操作  
        out.println(t2.getClass());  
        ParameterizedType t = (ParameterizedType)t2[0];//将类型向参数化类型转换  
        out.println(t.getClass());  
        out.println(t.getActualTypeArguments()[0]);// 可以得到参数化类型的参数实例  
          
    }  

}

若是复合的参数化类型,还可以迭代里面的参数化类型出来:

public class ReflectDemo {

    
    public static void main(String[] args) throws Exception{
        Proxy target = new ReflectDemo.Proxy();
        
        Method m = target.getClass().getDeclaredMethod("parameter", List.class,String[].class,Integer.class);
        Type[] types = m.getGenericParameterTypes();
        Assert.assertEquals(Modifier.PUBLIC, m.getModifiers());
        Class<?>[] parameterTypes = m.getParameterTypes();
        System.out.println("method's parameter class object>>"+parameterTypes.length);
        for(Type t : types) {
            if(t instanceof ParameterizedType) {
                ParameterizedType pt = (ParameterizedType) t;
                Type[] actypes = pt.getActualTypeArguments();
                for(Type tt : actypes) {
                    if(tt instanceof ParameterizedType){
                        ParameterizedType ptt = (ParameterizedType) tt;
                        Type[] pttype = ptt.getActualTypeArguments();
                        for(Type pttt : pttype) {
                            System.out.println(pttt.toString());
                        }
                    }
                    System.out.println(tt.toString());
                }
            }
        }
        
    }
    
    static class Proxy{
        private Map<String,Object> map;
        public void parameter(List<Map<String,Object>> list,String[] str,Integer it){
            
        }
    }
}

数组类型

在java中,代表数组类型的类是GenericArrayType.只有诸如Type[][]...形式的至少有一个[]即代表为数组的类型才能转化成GenericArrayType。内部只有一个方法,用于查询数组中的元素类型:

//数组类接口定义
public interface GenericArrayType extends Type {
    Type getGenericComponentType();
}

public class GenType<E> {

    public String[] str;
    public E[] e1;
    public ArrayList<E>[] ale;
    public E[][] e2;
    //不可转成GenericArrayType,class [[Ljava.lang.Integer;
    public Integer[][] ins;
    //不可转成GenericArrayType,class [Ljava.lang.String;
    public String[] str;

    public static void main(String[] args) throws Exception{
        Type type = GenType.class.getDeclaredField("ale").getGenericType();
        System.out.println(type.toString());
        //从上一个输出可以看到是一个参数化类型或者类型变量数组类型,才可以强转
        //并且,基本类型对象数组也不能转为数组类型,因为代表固定class对象
        System.out.println(((GenericArrayType)type).getGenericComponentType());
    }

}
//输出
java.util.ArrayList<E>[]
java.util.ArrayList<E>

关于这个数组类型GenericArrayType需要说明的是:

  • 无论从左向右有几个[]并列,这个方法仅仅脱去最右边的[]之后剩下的内容就作为这个方法的返回值.
  • 在通过反射获取得到类型是参数化类型数组或者是类型变量数组类型,才可以强转换为GenericArrayType数组类型对象,对内部的实际元素类型进行查询和处理;而主类型是基本数据类型对象或者String类型的数组,是不能转成数组类型的,因为他们已经是原子类型。

类型变量

在java中类型变量用类TypeVariable表示。类型变量接口中有个方法getBounds()是用来获取上边界类型,原因是在类型变量定义时候只能使用extends关键字进行多边界限定,而不能使用super,所以关键字extends都代表的是类型变量的上边界。
若是将类型变量具体化,例如定义一个类Person<E>,那么E就代表这个Person类的类型变量,变量变量,即为可以变的类型,可以变就代表我们在创建Person实例的时候,可以传入不同类型的变量来进行设置。eg: Person<String>, Person<Integer>..等等。传入的类型不同,那么,容器元素的类型也就固化了,在编译期就能检查错误。

关于类型边界上界说明看例子:

public static <E extends Map<String, Date>& Cloneable&Serializable> E methodVI(E e){
        return null;
    }

  • E的第一个上边界就是Map<String,Date>,是参数化ParameterizedType类型
  • E的第二个上边界是Cloneable,是Class类型
  • 因为类型变量可以通过&进行多个上边界限定,因此上边界有多个,因此返回值类型是数组类型[ ]

看看使用例子:

public class GenType<E> {

    public Map<String,Object>[] map;
    public E[] e1;
    public ArrayList<String>[] ale;
    public E[][] e2;
    public Integer[][] ins;//不可转成GenericArrayType
    public String[] str;//不可转成GenericArrayType

    public static <E extends Map<String, Date>& Cloneable&Serializable> E methodVI(E e){
        return null;
    }

    public static void main(String[] args) throws Exception{
        Type type = GenType.class.getDeclaredField("e1").getGenericType();
        System.out.println(type.toString());
        System.out.println(((GenericArrayType)type).getGenericComponentType());
        System.out.println(((GenericArrayType)type).getGenericComponentType().getClass());
        //类型变量TypeVariable.getBounds()获取类型边界
        System.out.println(((TypeVariable)((GenericArrayType)type).getGenericComponentType()).getBounds()[0]);
    }

}
//输出
E[]
E
class sun.reflect.generics.reflectiveObjects.TypeVariableImpl
class java.lang.Object

通配符类型

表示通配符类型的类型,表示为wildcardType,通常用符号?来表示匹配。通常该类用来限定泛型类型的边界,包括上界和下界。

public interface WildcardType extends Type {
    //上界,? extends ...
    Type[] getUpperBounds();
    //下界,? super ....
    Type[] getLowerBounds();
}

至于边界的返回值为什么声明为Type类型,可以从下面例子中看到:

  • public static void printColl(ArrayList<? extends ArrayList<String>> al){},其中通配符表达式是?extends ArrayList<String>,这样extends后面是?的上边界,这个上边界是参数化类型ParameterizedType。
  • public static <E> void printColl(ArrayList<? extends E> al){},其中通配符表达式是? extends E>,这样extends后面是?的上边界,这个上边界是类型变量TypeVariable类型。
  • public static <E> void printColl(ArrayList<? extends E[]> al){},其中通配符表达式是? extends E[]>,这样extends后面是?的上边界,这个上边界是类型变量GenericArrayType类型。
  • public static <E> void printColl(ArrayList<? extends Number> al){},其中通配符表达式是? extends Number>,这样extends后面是?的上边界,这个上边界是类型变量Class类型。

所以,综上的不同类型,得到的都是Type接口的子实现类。

extends与super

  • <? extends T> : 是指上界通配符(Upper Bounds Wildcards)
  • <? super T>: 是指下界通配符(Lower Bounds Wildcards)
  • 子类转换为父类(小转大)是隐式的,而父类转换成子类,需要显示手动强制转换。
  • PECS(Producer Extends Consumer Super)原则:
  1. 频繁往外读取内容的,适合用上界Extends。
  2. 经常往里插入的,适合用下界Super。
  • List<? extends E>表示该list集合中存放的都是E的子类型(包括E自身),由于E的子类型可能有很多,但是我们存放元素时实际上只能存放其中的一种子类型(这是为了泛型安全,因为其会在编译期间生成桥接方法<Bridge_Methods>该方法中会出现强制转换,若出现多种子类型,则会强制转换失败),例子如下:
List<? extends Number> list=new ArrayList<Number>();
list.add(4.0);//编译错误
list.add(3);//编译错误

上例中添加的元素类型不止一种,这样编译器强制转换会失败,为了安全,Java只能将其设计成不能添加元素。虽然List<? extends E>不能添加元素,但是由于其中的元素都有一个共性--有共同的父类,因此我们在获取元素时可以将他们统一强制转换为E类型,我们称之为get原则

  • 对于List<? super E>其list中存放的都是E的父类型元素(包括E),我们在向其添加元素时,只能向其添加E的子类型元素(包括E类型),这样在编译期间将其强制转换为E类型时是类型安全的,因此可以添加元素,例子如下:
 List<? super Number> list=new ArrayList<Number>();
 list.add(2.0);
 list.add(3.0);

但是,由于该集合中的元素都是E的父类型(包括E),其中的元素类型众多,在获取元素时我们无法判断是哪一种类型,故设计成不能获取元素,我们称之为put原则实际上,我们采用extends,super来扩展泛型的目的是为了弥补例如List<E>只能存放一种特定类型数据的不足,将其扩展为List<? extends E> 使其可以接收E的子类型中的任何一种类型元素,这样使它的使用范围更广

    public static void main(String[] args) {
        //装苹果的盘子不是装水果的盘子,不等价
//      Plate<Fruit> fp = new Plate<Apple>(new Apple());
        //Plate<? extends Fruit> 是Plate<Fruit> 以及Plate<Apple>的基类
        Plate<? extends Fruit> fp = new Plate<Apple>(new Apple());
//      fp.setItem(new Apple()); //error
//      fp.setItem(new Fruit()); //error
        Fruit f = fp.getItem();
        f.outf();
        Object of = fp.getItem();
        Apple apple = (Apple) fp.getItem();//父类强制转化为子类
        apple.outa();
        System.out.println(apple);
        
//      Plate<? super Fruit> sf = new Plate<Apple>(new Apple()); //error
        //sf,getItem() >> generic.Fruit@xxx
        Plate<? super Fruit> sf = new Plate<Fruit>(new Fruit());
        //sf,getItem() >> generic.Apple@xxx
        //强制转换 sf1 = new Plate<Fruit>((Fruit)new Apple())
        Plate<? super Fruit> sf1 = new Plate<Fruit>(new Apple());
        ((Fruit)sf.getItem()).outf();
        System.out.println(sf1.getItem().getClass());
        ((Apple)sf1.getItem()).outa();
        sf.setItem(new Apple());
    }
//输出
fruit
apple
generic.Apple@4eb09321
fruit
class generic.Apple
apple

设计这玩意,除了能让给程序员加点语法糖,在处理这些集合类型时候,能够灵活多变的存取不同类型的对象;另一方面,也为了程序安全,在编译器编译过程中就要对这些容器中元素类型进行类型检查,尽早发现错误并反馈给程序员。

原生类型 和 参数化类型
反射得到参数化类型中的类型参数
Java 泛型 <? super T> 中 super 怎么 理解?与 extends 有何不同?

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

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,293评论 18 399
  • 开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收List作为形式参数,那么如果尝试...
    时待吾阅读 990评论 0 3
  • 一、基本数据类型 注释 单行注释:// 区域注释:/* */ 文档注释:/** */ 数值 对于byte类型而言...
    龙猫小爷阅读 4,213评论 0 16
  • 整体Retrofit内容如下: 1、Retrofit解析1之前哨站——理解RESTful 2、Retrofit解析...
    隔壁老李头阅读 4,450评论 2 12
  • 郑老师好样的,这让我想起2015年一个下午的情景,那天阳光明媚,跟着几位领导走市场,来到一个感觉不食人间烟火的世界...
    心境意境阅读 162评论 0 1