从 Rxjava 看 Java 泛型(一)

最近在看 Rxjava 与 Retrofit ,发现它们都大量的使用到了泛型这个概念,之前我对泛型是有一点了解的,但是这点了解明显在学习它们的时候不够用,于是便花了点时间重新整体的学习了一遍泛型。

什么是泛型?###

泛型(generic)是指参数化类型的能力。

泛,广泛,按照字面意思可以简单的理解为它的含义是广泛的类型,事实也是如此,当我们为接口、方法或者类定义为泛型时,就说明它们可以接受的类型是广泛的,可以是 String ,int,date 等等。我们也可以指定泛型的具体类型,这样,当传入的对象类型与我们指定的类型不相符时编译器就会报错,能让代码更为健壮。

一个简单的例子:

public class GenericTest {
    public static void main(String[] args){
        ArrayList list = new ArrayList();
        list.add("张三");
        list.add("李四");
        list.add(new Integer(10));
        for(int i = 0; i < list.size(); i++){
            String name = (String) list.get(i);
            System.out.println(name);
        }
    }
}

我们在 list 数组中加入了两个 Sting 对象和 一个 int 对象,但在取出来时将它们全部强转成 String 类型的,所以毫无疑问代码会崩溃:

Exception in thread "main" 张三
李四
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    at GenericTest.main(GenericTest.java:10)

我们简单的修改一下代码:

 将   ArrayList list = new ArrayList();
 改为 ArrayList<String> list = new ArrayList<String>();

这时,你指定了 list 的类型为 String ,当你为它添加了除 String 外的类型对象时,IDE 在你写代码的时候就会提示你出错,而不会等到程序运行时才报错。

通常情况下,当我们不指定 ArrayList 泛型的具体类型时,可以把它的泛型类型视为 Object,即所有对象。也就是说,ArrayList 默认是泛型类型,即 ArrayList<E> ,这样我们就可以根据实际情况来指定 ArrayList 的具体类型。

从这里我们可以看出,当我们编写的代码实现了泛型后,这种代码就会变得更为通用,因为它可以在具体使用中根据实际要求来定义参数类型,这也正是泛型的定义:参数化类型。

泛型的使用###

  • 泛型类:

public class GenericTest <T>{
private T a;
public GenericTest(T a){
this.a = a;
}
public T get(){
return a;
}

public static void main(String[] args){
    GenericTest<String> string = new GenericTest<String>("hello,world");
    GenericTest<Integer> integer = new  GenericTest<Integer>(2016);
    String wordString = (String) string.get();
    int year = (Integer) integer.get();
    System.out.println(wordString);
    System.out.println(year);
}

}

这里,我们将 GenericTest 的类型设置为 <T>,即表示泛型,我们也可以用 E,或 V 来表示。可以看到,在主函数中,我们将 T 的类型分别指定为 String 和 Integer,然后再打印出来,结果如下:

hello,world
2016

从这我们可以得到这样一条通用语句:
`GenericTest<Xxx> xxx = new GenericTest<Xxx>(new Xxx());`
即 GenericTest 可以接受任何类型的对象。在这里,我们如果将 T 改为 Object 的话,GenericTest 同样能接受所有类型,但是如果假设 GenericTest 的作用是个集合呢?如 ArrayList?相信区别大家一眼便能看出来。所以,将类的类型指定为泛型而不是 Object 明显能更适用于多场景。

这里,我们需要注意的一点是,虽然在编译时 GenericTest<String> 与 GenericTest<Integer> 是两种类型,但实际上运行时只有一个 GenericTest 类会被加载到JVM 中,如下所示:

System.out.println(wordString +"->" + string.getClass().getName());
System.out.println(year + "->" + integer.getClass().getName());
运行结果为:
hello,world->GenericTest
2016->GenericTest

为什么呢?因为真正运行时泛型是不存在的,也就是说泛型只会在编译时存在,在运行时会被擦除。所以, GenericTest 类在加载到 JVM 中的方式为:

public class GenericTest{
private Object a;
public GenericTest(Object a){
this.a = a;
}
public Object get(){
return a;
}
public static void main(String[] args){
GenericTest string = new GenericTest("hello,world");
GenericTest integer = new GenericTest(2016);
String wordString = (String) string.get();
int year = (Integer) integer.get();
System.out.println(wordString);
System.out.println(year);
}
}

即用 Object 来代替<T>,也就是说,泛型的意义在于能让你在编写的代码在编译时就检查它的类型安全,并且它的类型转换都是自动和隐式的,而用 Object 的类型转换则是显式转换,并且当真正的参数错误时不会提出警告,如第一个例子。当然,泛型的真正意义远不如此,还需要我们继续去深入学习。

- 泛型方法

public class GenericTest {
public static <T> void print(T t){
System.out.println(t + "->" + t.getClass().getName());
}
public static <T> T get(T t){
return t;
}
public static void main(String[] args){
GenericTest.print(2016);
GenericTest.print("hello,world");
System.out.println(GenericTest.get(2016));
System.out.println(GenericTest.get("hello,world"));
}
}

这是一个简单的泛型方法,能够做到和 GenericTest 泛型类一样的效果,它和泛型类的区别在于泛型类型`<T>`要放在方法返回类型之前,如:`<T> void print(T t)`,而泛型类要放在类名后面,如:`GenericTest<T>`。既然它们效果都是一样的,那具体使用时我们应该用泛型类还是方法呢?
> 无论何时,只要你能做到,你就应该尽量的使用泛型方法。

  为什么呢?因为泛型方法让问题变得更为具体,而不是一个整体的对象。另外,对于静态方法来说,它无法访问泛型类的类型参数,所以,它想拥有泛型的能力,就必须定义为泛型方法。

- 泛型接口

public interface Comparable<T>{
public int compareTo(T o);
}

这个接口是 JDK 1.5之后自带的一个比较接口,我们可以看出,它的定义方式和泛型类的定义相差无几,泛型类型`<T>`也是放在接口名后面,使用也基本没什么差别。

###通配泛型###

首先,什么是通配泛型?为什么要使用到通配泛型呢?不急,我们先介绍一下通配泛型,通配泛型的类型有三种形式:`<?>`、`<? extens T>`、`<? super T>`,我们分别来讲解一下它们的具体用法与意义。

- <?>

非受限通配符(unbounded wildcard),它与第二种方式其实可以归为一类,因为它的作用相当于是`? extends Object`,等于是指定 T 为 Object,所以它的应用场景不多,就算真的遇到,我们也可以把它看成`? extends Object`来分析处理。

- <? extends T>

上受限通配符(Upper Bounds Wildcards),到这里,我们可以来看看,到底什么情况下应该使用通配泛型,还是之前那个例子:

public class GenericTest <T>{
private T a;

public GenericTest(T a){
    this.a = a;
}
public void set(T t){
    this.a = t;
}
public T get(){
    return a;
}

public static int max(GenericTest<Number> a,GenericTest<Number> b,GenericTest<Number> c){
    int aInt = a.get().intValue(); 
    int bInt = b.get().intValue();
    int cInt = c.get().intValue();
    int max = aInt;
    if (max < bInt) {
        max = bInt;
    }
    if (max < cInt) {
        max = cInt;
    }
    return max;
}
  
public static void main(String[] args){
    int max = max(new GenericTest<Integer>(1),new GenericTest<Integer>(2),new GenericTest<Integer>(3));
    System.out.print(max);
}

}

在之前的基础上我们添加了一个判断大小的  `max()` 方法,它接受三个泛型指定类型为 Number 的类,然后取出类里面的值进行比较后返回最大值。仔细看下代码,Integer 是 Number 的子类,按照向上转型原则,这样写是允许的,似乎并没什么错误的地方。但在 IDE 上编写时,会在倒数第四行得到一个出错提示:

-> The method max(GenericTest<Number>, GenericTest<Number>, GenericTest<Number>)
in the type GenericTest<T> is not applicable for the arguments
(GenericTest<Integer>, GenericTest<Integer>, GenericTest<Integer>)

告诉我们 `GenericTest<Number>` 不适用于 `GenericTest<Integer>`,简单来说就是 `GenericTest<Number>` 不能转换成 `GenericTest<Integer>`。为什么呢?Integer 不是 Number 的子类吗?讲道理应该是可行的啊,其实道理很简单,虽然 Integer 是 Number 的子类,但是 `GenericTest<Integer>` 并不是 `GenericTest<Number>` 的子类,所以无法转换。

举个更具体的例子来说,编译器的逻辑是这样的:学生(GenericTest)会使用笔(Number),钢笔(Integer)是笔的子类,编译器是允许的,但是“会使用钢笔的学生”这个整体对象是“会使用笔的学生”这个整体对象的子类这种逻辑编译器却是不允许的,这时候,我们就需要借助通配符来告诉编译器,你不要关心我和笔这个整体,你就只关心钢笔是不是笔的子类就可以了,这样就能进行正常的转换了:

public static int max(GenericTest<? extends Number> a,GenericTest<? extends Number> b,GenericTest<? extends Number> c)
结果为:
3

- <? super T>

下受限通配符(Lower Bounds Wildcards),一个上一个下,应该很容易联想理解,等于是告诉编译器只要关心? 这个类是不是 T 这个类的父类就可以了。

在这里,我们有一个特别需要注意的地方是上受限通配符`<? extends T>`只能从里面取东西而不能存,而下受限通配符`<? super T>`只能从里面存东西而不能取。我们可以简单的证明一下这个原则:

GenericTest<? extends Number> genericTest = new GenericTest<Integer>(10);
genericTest.set(10);//error:set(capture#4-of ? extends Number) in the type GenericTest<capture#4-of ? extends Number> is not applicable for the arguments (int)

错误提示似乎和之前没有用通配符进行 Integer 和 Number 的转换一样,但capture#4-of 这个是什么东西?它表示的意思是编译器捕获到了 Integer 这个类型,但是并没有把它设置成 Integer 而是取了个代号叫 #4-of。也就相当于 GenericTest 的类型被设置成了#4-of,而事实上并不存在这样的类型,我们自然就无法存东西进去。

可是编译器为什么要这样做呢?这是因为虽然我们 new 出来的是一个 Integer 类型的对象,但是持有这个对象的引用的却是 `GenericTest<? extends Number>`,而通配符`<? extends Number>`表示的是Number的所有子类的某一子类,也就是所只要是属于 Number 类子类的都能添加到`GenericTest<? extends T>`所持有的对象中,这样就很容易发生错误。

我们可以假设 Number 是苹果,它的子类可以有青苹果红苹果,青苹果又可以分为小青苹果大青苹果,这种子类的继承是没有下限的,所以它只有一个上限,也就是 T,所以如果我们 new 出来的是一个小青苹果类型的对象,但由于上限是苹果,我们也可以往这个对象里添加青苹果对象,这就相当于将一个父类赋值给子类了,明显是不行的,所以为了安全考虑,编译器就直接禁止了这种赋值。但是取出来就不受限制了,因为它有一个上限 T ,无论取出来的是小青苹果还是苹果都可以向上转型为苹果。

![未命名.png](http://upload-images.jianshu.io/upload_images/506482-910053ec9b8526a4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


同样的道理分析下受限通配符 <? super T>,我们也假设 T 是苹果,它的父类可以有水果,吃的,好吃的,特别好吃的,特特别好吃的……,这个父类延伸是没有上限的,它只有一个下限 T ,因此我们可以往里面任意存东西,因为都是 T 的父类,但是取就取不出来了,比如我们可以存水果进去,但取却允许取吃的,而里面却没有,这样程序就出错了。所以编译器就会禁止从里面取东西出来。

这一篇比较整体的介绍了一下泛型的用法与一些限制,因为有些部分是自己的见解,所以难免会有错误的地方,希望大家知道后能够指出来让我改正过来。:)下一篇将结合 Rxjava 来具体的分析一下泛型的实际应用,估计要比较久,因为我 Rxjava 还只看了一点点……

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

推荐阅读更多精彩内容

  • 开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收List作为形式参数,那么如果尝试...
    时待吾阅读 1,040评论 0 3
  • 在之前的文章中分析过了多态,可以知道多态本身是一种泛化机制,它通过基类或者接口来设计,使程序拥有一定的灵活性,但是...
    _小二_阅读 676评论 0 0
  • 第8章 泛型 通常情况的类和函数,我们只需要使用具体的类型即可:要么是基本类型,要么是自定义的类。但是在集合类的场...
    光剑书架上的书阅读 2,143评论 6 10
  • 得分后卫已经不再是科比麦迪时代能通过各种方式得分的球队当家,就现代联盟对得分后卫的“定义”来看,得分后卫更倾向于球...
    zoneball阅读 246评论 0 1
  • 九月份,校友合唱团要演出一场民国时期的学堂乐歌。其中,有一首是霞霞的太爷爷于1918年创办的敬德国民学校(位于宁波...
    林宏海阅读 517评论 0 0