您的位置:首页 > 编程语言 > Java开发

java泛型,怎么这么难

2017-02-11 11:18 197 查看
泛型,就是参数化类型。好吧,这是我抄的定义,自己都觉得难以理解。还是举个简单例子吧。

public class SimpleJava<T> {
T t;
public static void main(String[] args) {
SimpleJava<Integer> sj = new SimpleJava<>();
sj.t = Integer.valueOf(1);
}
}


看见没,就是类型不确定,使用参数进行表示,那这么写有什么好处呢?我们继续讨论。

为什么使用泛型?

我们看段代码,来感受下泛型的便利。

public class SimpleJava<T> {
private T t;

public SimpleJava(T t) {
this.t = t;
}
public T getT() {
return t;
}
public static void main(String[] args) {
SimpleJava<Integer> sj = new SimpleJava<Integer>(1);
Integer result = sj.getT();

SimpleJava<String> sj1 = new SimpleJava<String>("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 "<init>":(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 "<init>":(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<Integer> list2 = new ArrayList<Integer>();
list2.add(1);
list2.add("2"); //编译期会直接报错
}
}


从上面可以看出,这是编译器对代码的优化,帮助进行类型转换,人会犯错,而编译器则不允许(不然谁用)。编译器既然能够进行正确的类型转换,那么必然的知道正确的类型,机制就是类型声明时<>中的具体类型信息。

由此看来,就编译期类型检查这一项的便利,就有足够的理由让我们使用泛型了。

泛型的不变性

不变性?说出来有些唬人,什么叫不变呢?来个比较,可能更明了,看代码:

public static void main(String[] args) {
List<Object> list = new ArrayList<Integer>(); //语法错误,编译器报错,泛型不变
Object[] arr = new Integer[10]; //正常
arr[0] = "aa"; //运行期报错ArrayStoreException
}


通过代码 ,我们可以看见List的构造会报错,如果不用泛型进行实例化,却又能正常运行,这是为什么呢?大家还记得不,前部分讲得,泛型的重要特性:编译期类型检查,如果上面的list能够成功实例化,那么由于List的参数化类型,任何对象都可以加入正确的加入到list中,而实际我们想要的却是Integer类型,从而破坏泛型的安全机制,所以,不变性换句话说,就是同一个类的不同参数化类型造成了不同的类型,直白点就是,List< A >,List< B >是完全不同的类型。有代码可知,数组是另一种行为,协变性,从代码中可以看到,是非常不安全的。

由此,我们有理由认为:非必要的场景(如:性能需要极致等),都不应该使用数组,而应该用list集合代替。

由于泛型的不变,使的有些情况,变的非常的复杂,举个例子:

interface A {}
class B implements A {}
public class SimpleJava{
static void fn(List<A> list) {
System.out.println(list);
}
public static void main(String[] args) {
List<B> list = new ArrayList<B>();
//      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<B> list = new ArrayList<B>();
fn(list);
List<A> list1 = new ArrayList<A>();
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<B>(); //合法,通配符特性
//      list.add(new B());//非法,因为泛型的不变性<? extends A>并不一定是<B>这种固定的类型
List<? super A> list2 = new ArrayList<A>(); ////合法,通配符特性
list2.add(new B());//合法,因为继承关系,B肯定是A类型,
}
}


现在,我们可以确定,变量的赋值操作,可以通配符,尤其是方法调用时候的赋值,可以提高编码的灵活性(不用面向具体的类),可以编写出更符合面向对象的代码。

同时为了保证类型安全,通配符有着严格的使用限制,例如:List< ? extends A >时,不允许写入除了null之外的任何类型,因为编译器并不能知道A的子类具体是哪个,为了类型安全考虑,不允许写入,使用List< ? super A >时,可以进行写入A的子类,因为A的子类肯定是A,但是取出时却会失去类型信息,编译器并不知道< ? super A >具体是哪个,唯一可以肯定的是Object。

这也是《Effective Java》中所说的生产者,消费者,有兴趣可以自行研读。

由上面的描述可以看出,java泛型中有着极其严格的限制,不过只要知道了,泛型使用的边界,也就不是太难了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: