最近在看 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](https://img.haomeiwen.com/i506482/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
网友评论