Java泛型

为什么使用泛型

首先,我们举个例子。

  1. 求和函数
    针对开发中常见的数值求和需求,如int,long,double等类型。
    public static int addInt(int x,int y){
        return x+y;
    }

    public static float addFloat(float x,float y){
        return x+y;
    }

没有泛型的情况下,对不同的类型需要封装不同的方法。使用泛型则可以减少重复代码

    public static <T extends Number> double addNumber(T a, T b){
        return a.doubleValue() + b.doubleValue();
    }

此时,返回值选择double类型,因为其取值范围和精度相对其它Number都更合适。

2.List集合
List集合在没有使用泛型时,默认是Object元素,可以存放任意数据类型。

        List list = new ArrayList();
        list.add("mark");
        list.add("OK");
        list.add(100);

        for (int i = 0; i < list.size(); i++) {
            String name = list.get(i).toString(); 
            System.out.println("name:" + name);
        }

但是取出来使用的时候,仍需要知道元素类型,这就需要强制类型转换了。这种行为安全性不高,建议使用List时配合泛型。

        List<String> list = new ArrayList<>();

泛型机制的优点

泛型机制的优点有:
1.泛型可编写模版代码来适应多种类型,减少重复代码
2.泛型可避免强制类型转换,编译时进行类型检查,减少出错机会

泛型擦除

Java泛型是伪泛型,因在编译期间泛型信息会被擦除,也就是生成的字节码文件中不包含泛型中的类型信息。编码使用泛型时添加类型信息,编译器编译的时候去掉,这个过程就是泛型擦除。

        ArrayList<String> list1 = new ArrayList<>();
        list1.add("abc");

        ArrayList<Integer> list2 = new ArrayList<>();
        list2.add(123);

        System.out.println("class:" + list1.getClass()); //class:class java.util.ArrayList
        System.out.println(list1.getClass() == list2.getClass());//true

最终list1.getClass() == list2.getClass()的结果是true,说明泛型类型String和Integer被擦除了,只剩下原始类型java.util.ArrayList。

    public void a(List<String> list){
    }

    public void a(List<Integer> list){
    }

上述的代码会出现编译错误both methods have same erasure,因为泛型擦除后,二者不能构成重载。

综上,Java的泛型也被称为伪泛型。

  • 真泛型:泛型中的类型是真实存在的。
  • 伪泛型:仅在编译时类型检查,在运行时擦除类型信息。

Java泛型擦除的原因是向前兼容,把已有的类型(主要是Collections容器)泛型化,保证已经部署的程序可以继续运行。

泛型擦除问题

先了解下泛型擦除究竟擦除了什么,保留了什么信息。

问:泛型的信息不是被擦除了吗?
答:是被擦除了, 但是某些(声明侧的泛型,接下来解释) 泛型信息会被class文件 以Signature的形式 保留在Class文件的Constant pool中。但是使用侧泛型则不会。

声明侧泛型主要指以下内容

1.泛型类,或泛型接口的声明 2.带有泛型参数的方法 3.带有泛型参数的成员变量

使用侧泛型

也就是方法的局部变量,方法调用时传入的变量。

Gson解析时传入的参数属于使用侧泛型,因此不能通过Signature解析

如何获取泛型信息

通过class的getTypeParameters只能获取到声明泛型参数的占位符。

        List<Integer> list = new ArrayList<>();
        Map<Integer, String> map = new HashMap<>();
        System.out.println(Arrays.toString(list.getClass().getTypeParameters())); //E
        System.out.println(Arrays.toString(map.getClass().getTypeParameters())); //K,V

但是开发中有些场景需要获取泛型的信息,如Retrofit接口,Gson序列化,这时该如何办呢。请看下面修改后的代码:

        Map<String, Integer> map1 = new HashMap<String, Integer>() {};
        Type type1 = map1.getClass().getGenericSuperclass();
        ParameterizedType parameterizedType1 = ParameterizedType.class.cast(type1);
        for (Type typeArgument : parameterizedType1.getActualTypeArguments()) {
            System.out.println(typeArgument.getTypeName()); //class java.lang.String / class java.lang.Integer
        }

示例代码获取了map1实例所对应的泛型信息,两端示例代码结果不同的关键就是map和map1的定义不同。其中变量map1是创建了一个HashMap的匿名内部类,其泛型参数限定为 String和Integer。通过定义类的方式,在类信息中保留泛型信息,进而在运行时获得这些泛型信息。

另一种获取field泛型类型的方法如下
        //    public Map<Integer, String> memMap;
        Field field = null;
        try {
            field = GenericDemo.class.getField("memMap");
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        ParameterizedType parameterizedType2 = (ParameterizedType)field.getGenericType();
        System.out.println("parameterizedType toString " + parameterizedType2); //java.util.Map<java.lang.Integer, java.lang.String>
        System.out.println("parameterizedType  的参数信息 " + Arrays.asList(parameterizedType2.getActualTypeArguments())); //[class java.lang.Integer, class java.lang.String]

Gson反序列化时如何解析泛型类型

当使用Gson库进行json的解析时,使用方式如下。可以看到也是使用了匿名内部类。

    // Gson 常用的情况
    public  List<String> parse(String jsonStr){
        List<String> topNews =  new Gson().fromJson(jsonStr, new TypeToken<List<String>>() {}.getType());
        return topNews;
    }

Gson反序列化原理

Class类提供了一个方法public Type getGenericSuperclass() ,可以获取到带泛型信息的父类Type。也就是说java的class文件会保存继承的父类或者接口的泛型信息。

TypeToken的部分代码如下:

public class TypeToken<T> {
  final Class<? super T> rawType;
  final Type type;
  final int hashCode;

@SuppressWarnings("unchecked")
  protected TypeToken() {
    this.type = getSuperclassTypeParameter(getClass());
    this.rawType = (Class<? super T>) $Gson$Types.getRawType(type);
    this.hashCode = type.hashCode();
  }

  /**
   * Returns the type from super class's type parameter in {@link $Gson$Types#canonicalize
   * canonical form}.
   */
  static Type getSuperclassTypeParameter(Class<?> subclass) {
    Type superclass = subclass.getGenericSuperclass();
    if (superclass instanceof Class) {
      throw new RuntimeException("Missing type parameter.");
    }
    ParameterizedType parameterized = (ParameterizedType) superclass;
    return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);
  }

  /**
   * Returns the raw (non-generic) type for this type.
   */
  public final Class<? super T> getRawType() {
    return rawType;
  }

  /**
   * Gets underlying {@code Type} instance.
   */
  public final Type getType() {
    return type;
  }
}

通过创建继承自TypeToken<T>的匿名内部类,并实例化泛型参数T。TypeToken的默认无参构造方法通过Class的public Type getGenericSuperclass()方法,获取了父类的泛型信息。即上述使用用例中的List<String>。最终Gson利用子类会保存父类class的泛型参数信息的特点,通过匿名内部类实现了泛型参数的解析。

PECS原则

PECS即Produce Extend Consumer Super,PECS是从集合的角度出发的,含义如下:
1.如果你只是从集合中取数据,那么它是个生产者,你应该用extend
2.如果你只是往集合中加数据,那么它是个消费者,你应该用super
3.如果你往集合中既存又取,那么你不应该用extend或者super

示例

public class Collections { 
  public static <T> void copy(List<? super T> dest, List<? extends T> src)   {  
      for (int i=0; i<src.size(); i++) { 
          dest.set(i, src.get(i)); 
      }
  } 
}

解释如下:

  • 在List<? extends Fruit>的泛型集合中,对于元素的类型,编译器只能知道元素是继承自Fruit,具体是Fruit的哪个子类是无法知道的。 所以「向一个无法知道具体类型的泛型集合中插入元素是不能通过编译的」。但是由于知道元素是继承自Fruit,所以从这个泛型集合中取Fruit类型的元素是可以的。

  • 在List<? super Apple>的泛型集合中,元素的类型是Apple的父类,但无法知道是哪个具体的父类,因此「读取元素时无法确定以哪个父类进行读取」。 插入元素时可以插入Apple与Apple的子类,因为这个集合中的元素都是Apple的父类,子类型是可以赋值给父类型的。

  • ? 无限定通配符。eg Pair<?>,既不能读也不能写,只能做一些null判定。
    大多数情况下,可以引入泛型参数<T>消除<?>通配符。<?>通配符有一个独特的特点,就是:Pair<?>是所有Pair<T>的超类,也就是可以安全的向上转型。

反射和泛型

Java的部分反射API也是泛型。例如:Class<T>就是泛型:

Class<String> clazz = String.class;
String str = clazz.newInstance();

调用Class的getSuperclass()方法返回的Class类型是Class<? super T>:

Class<? super String> sup = String.class.getSuperclass();

我们可以声明带泛型的数组,但不能用new操作符创建带泛型的数组。必须通过强制转型实现带泛型的数组:

Pair<String>[] ps = null; // ok
Pair<String>[] ps = new Pair<String>[2]; // compile error!

@SuppressWarnings("unchecked")
Pair<String>[] ps = (Pair<String>[]) new Pair[2];

使用泛型数组时要特别注意,因为如果持有原强制转换对象的引用,该对象没有泛型的限制,编译器不对检查对其的修改操作。泛型数组对象和其指向同一对象,可能有不安全的类型转换。推荐上面的写法,避免持有原引用

带泛型的数组实际上是编译器的类型擦除,所以我们不能直接创建泛型数组T[],因为擦拭后代码变为Object[],必须借助Class<T>来创建泛型数组。Java提供了Array类来动态创建数组,但是仍需要强制类型转换。提供的方法名为Array.newInstance。

T[] createArray(Class<T> cls) {
    return (T[]) Array.newInstance(cls, 5);
}

还可以利用可变参数创建泛型数组T[],但是不推荐。

参考文档:
【知识点】Java泛型机制7连问
Java 的泛型擦除和运行时泛型信息获取
Java Type 类型详解
super通配符
extends通配符
泛型和反射

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

推荐阅读更多精彩内容

  • 泛型机制是我们开发中的常用技巧,也是面试常见问题不过泛型机制这个知识点也比较繁杂又不成体系,学了容易忘本文从几个问...
    字节跳不动阅读 521评论 0 3
  • Java泛型,算是一个比较容易产生误解的知识点,因为Java的泛型基于擦除实现,在使用Java泛型时,往往会受到泛...
    三好码农阅读 617评论 1 4
  • 参数类型的好处 在 Java 引入泛型之前,泛型程序设计是用继承实现的。ArrayList 类只维护一个 Obje...
    杰哥长得帅阅读 861评论 0 3
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,471评论 28 53
  • 信任包括信任自己和信任他人 很多时候,很多事情,失败、遗憾、错过,源于不自信,不信任他人 觉得自己做不成,别人做不...
    吴氵晃阅读 6,133评论 4 8