泛型,就是参数化类型。好吧,这是我抄的定义,自己都觉得难以理解。还是举个简单例子吧。
public class SimpleJava {
T t;
public static void main(String[] args) {
SimpleJava sj = new SimpleJava<>();
sj.t = Integer.valueOf(1);
}
}
看见没,就是类型不确定,使用参数进行表示,那这么写有什么好处呢?我们继续讨论。
我们看段代码,来感受下泛型的便利。
public class SimpleJava {
private T t;
public SimpleJava(T t) {
this.t = t;
}
public T getT() {
return t;
}
public static void main(String[] args) {
SimpleJava sj = new SimpleJava(1);
Integer result = sj.getT();
SimpleJava sj1 = new SimpleJava("a");
String result1 = sj1.getT();
}
}
看上面代码,我们发现T和直接用object类型没什么两样。仔细看,发现没,sj.getT()和sj1.getT()的返回值能够自动进行正确的类型转换。我们知道,所有的类型转换都是在运行期进行,如果出错,会抛出类型转换异常:ClassCastException,而使用泛型则不会,我们看下编译后的汇编:
public static void main(java.lang.String[]);
Code:
0: new #1 // class com/esy/rice/tool/simple/SimpleJava
3: dup
4: iconst_1
5: invokestatic #29 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
8: invokespecial #35 // Method "":(Ljava/lang/Object;)V
11: astore_1
12: aload_1
13: invokevirtual #37 // Method getT:()Ljava/lang/Object; //返回的还是Object
16: checkcast #30 // class java/lang/Integer //编译器自动插入类型转换指令
19: astore_2
20: new #1 // class com/esy/rice/tool/simple/SimpleJava
23: dup
24: ldc #39 // String a
26: invokespecial #35 // Method "":(Ljava/lang/Object;)V
29: astore_3
30: aload_3
31: invokevirtual #37 // Method getT:()Ljava/lang/Object;//返回的还是Object
34: checkcast #41 // class java/lang/String //编译器自动插入类型转换指令
37: astore 4
39: return
}
}
我们进行有泛型和没泛型编码的对比:
public class SimpleJava {
public static void main(String[] args) {
List list = new ArrayList();
list.add(1);
list.add("2");
List list2 = new ArrayList();
list2.add(1);
list2.add("2"); //编译期会直接报错
}
}
从上面可以看出,这是编译器对代码的优化,帮助进行类型转换,人会犯错,而编译器则不允许(不然谁用)。编译器既然能够进行正确的类型转换,那么必然的知道正确的类型,机制就是类型声明时<>中的具体类型信息。
由此看来,就编译期类型检查这一项的便利,就有足够的理由让我们使用泛型了。
不变性?说出来有些唬人,什么叫不变呢?来个比较,可能更明了,看代码:
public static void main(String[] args) {
List
通过代码 ,我们可以看见List的构造会报错,如果不用泛型进行实例化,却又能正常运行,这是为什么呢?大家还记得不,前部分讲得,泛型的重要特性:编译期类型检查,如果上面的list能够成功实例化,那么由于List的参数化类型,任何对象都可以加入正确的加入到list中,而实际我们想要的却是Integer类型,从而破坏泛型的安全机制,所以,不变性换句话说,就是同一个类的不同参数化类型造成了不同的类型,直白点就是,List< A >,List< B >是完全不同的类型。有代码可知,数组是另一种行为,协变性,从代码中可以看到,是非常不安全的。
由此,我们有理由认为:非必要的场景(如:性能需要极致等),都不应该使用数组,而应该用list集合代替。
由于泛型的不变,使的有些情况,变的非常的复杂,举个例子:
interface A {}
class B implements A {}
public class SimpleJava{
static void fn(List list) {
System.out.println(list);
}
public static void main(String[] args) {
List list = new ArrayList();
// f(list); //直接语法报错
}
}
看上面的fn方法,应该是我们编程的规范吧,接口与实现分离,利用接口进行编程。然而,泛型的不变性,使的list< B >类型,也就是接口的具体实现的泛型无法使用。难道使用泛型后,就只能编写处理具体类的代码了吗?可想而知,肯定不是,不能对接口进行编程,那泛型也太失败了,如何解决呢?通过限制的通配符,看下面代码:
interface A {}
class B implements A {}
public class SimpleJava{
static void fn(List extends A> list) {
for (A a : list) {
}
}
static void fn1(List super A> list) {
list.add(new B());
}
public static void main(String[] args) {
List list = new ArrayList();
fn(list);
List list1 = new ArrayList();
fn1(list1);
}
}
我们利用< ? extends A >和< ? super A >进行泛型边界的限定,进行安全控制同时提升了泛型的灵活性,这到底是什么原因呢,看点简单点的代码,看下面:
interface A {}
class B implements A {}
public class SimpleJava{
public static void main(String[] args) {
List extends A> list = new ArrayList(); //合法,通配符特性
// list.add(new B());//非法,因为泛型的不变性 extends A>并不一定是这种固定的类型
List super A> list2 = new ArrayList(); ////合法,通配符特性
list2.add(new B());//合法,因为继承关系,B肯定是A类型,
}
}
现在,我们可以确定,变量的赋值操作,可以通配符,尤其是方法调用时候的赋值,可以提高编码的灵活性(不用面向具体的类),可以编写出更符合面向对象的代码。
同时为了保证类型安全,通配符有着严格的使用限制,例如:List< ? extends A >时,不允许写入除了null之外的任何类型,因为编译器并不能知道A的子类具体是哪个,为了类型安全考虑,不允许写入,使用List< ? super A >时,可以进行写入A的子类,因为A的子类肯定是A,但是取出时却会失去类型信息,编译器并不知道< ? super A >具体是哪个,唯一可以肯定的是Object。
这也是《Effective Java》中所说的生产者,消费者,有兴趣可以自行研读。
由上面的描述可以看出,java泛型中有着极其严格的限制,不过只要知道了,泛型使用的边界,也就不是太难了。