Java泛型

泛型概述


由来

泛型是JDK 1.5的一项新特性,在Java语言处于还没有出现泛型的版本时,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。例如在哈希表的存取中,JDK 1.5之前使用HashMap的get()方法,返回值就是一个Object对象,由于Java语言里面所有的类型都继承于java.lang.Object,那Object转型为任何对象成都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个Object到底是个什么类型的对象。在编译期间,编译器无法检查这个Object的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多ClassCastException的风险就会被转嫁到程序运行期之中。

伪泛型

许多人都认为c++模板template和java泛型generic这两个概念是等价的,不过,各种语言是怎么实现该功能,以及为什么这么做,却千差万别.

在C++中,模板本质上就是一套宏指令集,只是换了个名头,编译器会针对每种类型创建一份模板代码的副本。有个证据可以证明这一点:MyClass<Foo>不会与MyClass<Bar>共享静态变量。然而,两个MyClass<Foo>实例则会共享静态变量。但在Java中,MyClass类的静态变量会由所有MyClass实例共享,无论类型参数相与否。

Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原始类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,ArrayList<int>与ArrayList<String>就是同一个类。所以说泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型被称为伪泛型

泛型使用


泛型分类

  1. 泛型类
/**
 * 泛型类
 * @param <T> 此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 在实例化泛型类时,必须指定T的具体类型
 */
class Generic<T> {
    /**
     * key这个成员变量的类型为T,T的类型由外部指定
     */
    private T key;

    /**
     * @param key 形参key的类型也为T,T的类型由外部指定
     */
    public Generic(T key) {
        this.key = key;
    }

    /**
     * @return 返回值类型为T,T的类型由外部指定
     */
    public T getKey() {
        return key;
    }
}
  1. 泛型接口
/**
 * 泛型接口
 *
 * @param <T>
 */
interface Generator<T> {
    public T next();
}

/**
 * 1. 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
 */
class Fruit1Generator<T> implements Generator<T> {

    @Override
    public T next() {
        return null;
    }
}

/**
 * 2. 传入泛型实参时:
 * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
 * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
 */
class Fruit2Generator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}
  1. 泛型方法
class StaticGenerator<E> {

    /**
     * 泛型方法
     * 需要添加额外的泛型声明
     */
    public <T> void push(T data) {

    }

    /**
     * 泛型方法 静态方法使用泛型必须为泛型方法,如果定义
     * 如:public static void show(E t){..},此时编译器会提示错误信息:
     * "'StaticGenerator.this' cannot be referenced from a static context"
     */
    public static <T> void show(T t) {

    }


    /**
     * 泛型类中的包含泛型的方法,不是泛型方法
     */
    public E get() {
        return null;
    }
}

泛型通配符

看到Generic<Integer>不能被看作为Generic<Number>的子类,故无法使用多态,但可以使用如下形式接收不同泛型类型的Generic对象

public void showKeyValue1(Generic<?> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}

类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参 。再直白点的意思就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。

泛型限定

  1. ? extends SomeClass 这种限定,说明的是只能接收SomeClass及其子类类型,所谓的“上限”
  2. ? super SomeClass 这种限定,说明只能接收SomeClass及其父类类型,所谓的“下限”

限定有以下规则

  • 不管该限定是类还是接口,统一都使用关键字extends
  • 可以使用&符号给出多个限定
  • 如果限定既有接口也有类,那么类必须只有一个,并且放在首位置

原理


类型擦除

在JAVA的虚拟机中并不存在泛型,泛型只是为了完善java体系,增加程序员编程的便捷性以及安全性而创建的一种机制,在JAVA虚拟机中对应泛型的都是确定的类型,在编写泛型代码后,java虚拟中会把这些泛型参数类型都擦除,用相应的确定类型来代替,代替的这一动作叫做类型擦除,而用于替代的类型称为原始类型,在类型擦除过程中,一般使用第一个限定的类型来替换,若无限定则使用Object

class Test<? extends Comparable>
{
    private T t;
    public void show(T t)
    {

    }
}

虚拟机进行翻译后的原始类型:

class Test
{
    private Comparable t;
    public void show(Comparable t)
    {
        
    }
}

用反射来看泛型的机制(甚至可以破坏)

public static void main(String[] args) {
    ArrayList<Integer> list = new ArrayList<>();
    list.add(1);

    try {
        list.getClass().getMethod("add", Object.class).invoke(list, "hello");
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    }

    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}

在程序中定义了一个ArrayList泛型类型实例化为Integer的对象,如果直接调用add方法,那么只能存储整形的数据。不过当我们利用反射调用add方法的时候,却可以存储字符串。这说明了Integer泛型实例在编译之后被擦除了,只保留了原始类型。

原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。

类型擦除引起的问题及解决办法


先检查、再编译

public static void main(String[] args) {
    ArrayList<Integer> list = new ArrayList<>();
    list.add(1);
    list.add("hello"); // 编译报错

}

因为类型擦除是在编译期完成的,在运行的时候就会忽略泛型,为了保证在运行的时候不出现类型错误,就需要在编译时检查是否满足泛型要求(类型检查)。

类型检查的依据

public static void main(String[] args) {
    // 1. 方式1
    ArrayList<Integer> list1 = new ArrayList<>();
    // 2. 方式2
    ArrayList list2 = new ArrayList<Integer>();
    list1.add(1);
    list1.add("hello"); // 该句编译报错
    list2.add(1);
    list2.add("hello"); // 该句编译正常
}

注释1和2都没有编译错误:第一种情况,在使用list1时与完全使用泛型参数一样的效果,因为new ArrayList()只是在内存中新开辟一个存储空间,它并不能判断类型,而真正涉及类型检查的是它的引用,所以在调用list1的时候会进行类型检查。同理,第二种情况,就不会进行类型检查。

泛型参数化类型没有继承关系

public static void main(String[] args) {
    ArrayList<String> list1 = new ArrayList<>();
    push(list1); // 编译报错

}
public static void push(ArrayList<Object> list) {

}

可以通过泛型通配符解决

public static void main(String[] args) {
    ArrayList<String> list1 = new ArrayList<>();
    push(list1);

}

public static void push(ArrayList<?> list) {

}

类型擦除与多态的冲突和解决方法


class Pair<T> {
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

class DateInter extends Pair<Date> {
    @Override
    public void setValue(Date value) {
        super.setValue(value);
    }

    @Override
    public Date getValue() {
        return super.getValue();
    }
}

可以看到,父类和子类的方法中参数类型不同,如果是在普通的继承关系中,这完全不是重写,而是重载;但是如果在泛型中呢?

public static void main(String[] args) {
    DateInter dateInter = new DateInter();
    dateInter.setValue(new Date());
    dateInter.setValue(new Object()); // 编译报错
}

无法接收Object类型参数,可见在泛型中确实是重写了,而不是重载。具体原理如下,编译class后再通过jad工具反编译出代码如下

class Pair
{

    public Pair()
    {
    }

    public Object getValue()
    {
        return value;
    }

    public void setValue(Object obj)
    {
        value = obj;
    }

    private Object value;
}

class DateInter extends Pair
{

    DateInter()
    {
    }

    public void setValue(Date date)
    {
        super.setValue(date);
    }

    public Date getValue()
    {
        return (Date)super.getValue();
    }

    public volatile void setValue(Object obj)
    {
        setValue((Date)obj);
    }

    public volatile Object getValue()
    {
        return getValue();
    }
}

由于DateInter继承Pair<Date>,但是Pair在类型擦除后还有一个public volatile void setValue(Object obj)方法,这和那个public void setValue(Date date)出现重载,但是程序本意却是不需要public volatile void setValue(Object obj)的,故通过桥方法调用了public void setValue(Date date)方法,达到了重写的效果。

泛型数组


看到了很多文章中都会提起泛型数组,经过查看sun的说明文档,在java中是”不能创建一个确切的泛型类型的数组”的。

也就是说下面的这个例子是不可以的:

List<String>[] ls = new ArrayList<String>[10];  

而使用通配符创建泛型数组是可以的,如下面这个例子:

List<?>[] ls = new ArrayList<?>[10]; 

这样也是可以的,但是仍存在ClassCastException问题

List<String>[] ls = new ArrayList[10];

假如泛型数组允许创建,代码如下

// Not really allowed.
List<String>[] lsa = new List<String>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Unsound, but passes run time store check
oa[1] = li;

// Run-time error: ClassCastException.
String s = lsa[1].get(0);

这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。

解决办法:

  1. 采用通配符方式
    下面采用通配符的方式是被允许的,对于通配符的方式,最后取出数据是要做显式的类型转换的
// OK, array of unbounded wildcard type.
List<?>[] lsa = new List<?>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Correct.
oa[1] = li;
// Run time error, but cast is explicit.
String s = (String) lsa[1].get(0);
  1. 采用List方式
List<List<String>> lists = new ArrayList<>();

错误方式

// 编译不会报错,但存在潜在的运行时ClassCastException
List<String>[] ls = new ArrayList[10];

参考


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

推荐阅读更多精彩内容