java泛型,怎么这么难

泛型,就是参数化类型。好吧,这是我抄的定义,自己都觉得难以理解。还是举个简单例子吧。

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 = new ArrayList(); //语法错误,编译器报错,泛型不变
        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 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());//非法,因为泛型的不变性并不一定是这种固定的类型
        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泛型中有着极其严格的限制,不过只要知道了,泛型使用的边界,也就不是太难了。

你可能感兴趣的:(java基础)