浅谈Java泛型

使用泛型的目的

当我们第一次接触泛型时,第一个问题肯定会是,为什么要使用泛型?最直接的答案是为了避免转型,使得编译器能够在编译期就发现转型错误而不用等到运行时。

比如说,当我们声明了一个泛型为Integer的列表,那么该列表的元素就只能是Integer,当我们往里面放非Integer的元素时,编译器就能够发现,

List<Integer> intList = new ArrayList<>();
intList.add("abc"); //编译期错误,不能存放String对象到intList中

同理,当我们从列表中获取元素时,我们也不再需要使用强制转型将列表中的元素转型成Integer

Integer i = intList.get(0);
Integer i = (Integer)intList.get(0);//不再需要进行强制转型

然而,泛型却有一些容易令人困惑的地方,第一个就是关于泛型的带有通配符的上下界。

带有通配符的上下界

Java泛型现在支持两种带有通配符的上下界的表达方式,

  1. ? extends T - 这里的?表示类型T的任意子类型,包括类型T本身。
  2. ? super T - 这里的?表示类型T的任意父类型,包括类型T本身。

这两者的含义都很容易理解和区分,难点在于我们什么时候该用 ? extends T, 什么时候改用? super T.

比如说,由于? extends T表示类型T和它的任意子类型,那么我们可以说List<? extends Number> 实例可以添加任意为Number子类的元素吗?

    List<? extends Number> intList = new ArrayList<>();
    intList.add(1); //complier error
    intList.add(3.14); //compiler error

从上述的代码来看,它并不像我们所理解的那样工作。那么,我们在什么时候需要使用? extends T和? super T呢? 这里有一条简单的规则 (Get and Put Rule):

当你需要从一个数据结构中获取数据时(get), 那么就使用? extends T. 如果你需要存储数据(put)到一个数据结构时,那么就使用? super T. 如果你又想存储数据,又想获取数据,那么就不要使用通配符? . 即直接使用具体泛型T。

所以,根据Get&Put规则,在上面的例子中,我们是需要往数据结构里面存储数据的,所以需要使用? super T. 修改上面的例子,你可以发现程序可以工作了。

    List<? super Number> intList = new ArrayList<>();
    intList.add(1); //it works
    intList.add(3.14); //it works

第二个比较让人困惑的是关于泛型的类型系统。

泛型的类型系统

泛型的类型系统中最令人困惑的地方就是,Integer类是Number类的子类,而List<Integer>却不是List<Number>的子类。也就是说,你不能将List<Integer>实例对象直接赋值给List<Number>实例对象。 List<Integer>不是List<Number>子类的原因我们只能通过反证法推理出来:

List<Integer> ints = new ArrayList<Integer>();
ints.add(1);
ints.add(2);
List<Number> nums = ints; // compile-time error
nums.add(3.14);
assert ints.toString().equals("[1, 2, 3.14]"); // uh oh!

从上面的例子中,假设List<Integer>是List<Number>的子类,那么List<Integer>实例对象ints就可以赋值给List<Number>实例对象nums。接着我们可以往nums实例添加非Integer对象,如float对象3.14。但是这个是不允许的,因为nums实例实际上是List<Integer>实例对象,它不能接受任何非Integer对象。由此可证,List<Integer>并不是List<Number>的子类。但是,List<Integer>是Collection<Integer>的子类。

所以对于泛型的类型系统来讲,它应当遵循以下一些规则,摘抄自Java深度历险-Java泛型

引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于List<String>和List<Object>这样的情况,类型参数String是继承自Object的。而第二种指的是List接口继承自Collection接口。对于这个类型系统,有如下的一些规则:

  1. 相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。即List<String>是Collection<String> 的子类型,List<String>可以替换Collection<String>。这种情况也适用于带有上下界的类型声明。
  1. 当泛型类的类型声明中使用了通配符的时候,其子类型可以在两个维度上分别展开。如对Collection<? extends Number>来说,其子类型可以在Collection这个维度上展开,即List<? extends Number>和Set<? extends Number>等;也可以在Number这个层次上展开,即Collection<Double>和 Collection<Integer>等。如此循环下去,ArrayList<Long>和 HashSet<Double>等也都算是Collection<? extends Number>的子类型。
  2. 如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。

最后我们来看看在Java泛型参数是如何进行推断的。

泛型参数的自动推断

有两种方式可以指定泛型参数的类型,一种是显示的指定,比如对于下面的方法,

public class GenericUtil {
    public static <T> List<T> asList(T... array) {
        List<T> tlist = new ArrayList<>();
        for (T t : array) {
            tlist.add(t);
        }
        return tlist;
    }
}

我们可以显示的指定泛型参数的类型,

System.out.println(GenericUtil.<String>asList("a","b","c"));

除了显示的指定之外,我们还可以隐式的指定, 实际上就是让编译器自己去推断。

System.out.println(GenericUtil.asList("a","b","c"));

编译器可以通过asList方法接收的参数类型来推断出T实际上就是String。 如果从方法接收的参数类型推断不出来的话,那么编译器还会从方法赋值的目标参数来推断,比如说下面的newObject方法,

public class GenericUtil {

    public static <T> T newObject(String className) throws Exception {
        Class clz = Class.forName(className);
        return (T) clz.newInstance();
    }
}

从方法接收参数上编译器并不能推断出T是什么类型。这个时候可以从方法赋值的目标参数进行推断,

  Person p = GenericUtil.newObject("com.kevin.hibernate01.Person");

从方法赋值的目标参数p可以得知,newObject方法中定义的泛型参数T类型应该为Person类型。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容

  • 1. 泛型概述 泛型为JDK1.5之后sun公司推出的新功能,泛型可以消除源代码中的许多强制类型转换,泛型对于数据...
    yuan_dongj阅读 4,487评论 0 8
  • 第8章 泛型 通常情况的类和函数,我们只需要使用具体的类型即可:要么是基本类型,要么是自定义的类。但是在集合类的场...
    光剑书架上的书阅读 2,143评论 6 10
  • 开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收List作为形式参数,那么如果尝试...
    时待吾阅读 1,045评论 0 3
  • 我到底是爱你还是不爱你呢?是因为爱你才在乎你还是因为你不在乎我我才更在乎你?如果你不爱我或者没那么爱我最重要的是没...
    水也云云阅读 195评论 0 0
  • 风清扬,雪舞狂,这路上,我在! 每个人的路都有自己的走法,我们各行其道,各运常修,让线谱交织,让生命喝彩。 从来到...
    人泥阅读 127评论 0 1