Java那些事:泛型

         “让错误尽量在编译被发现”

         “你必须知道边界所在,才能成为高手”

                                                                                                                ---《Thinking in Java》

         错误在编译时被发现是十分让人向往的,Java泛型在JDK1.5中的引入目的就是“让编译器承担更多的工作,保证类型的正确”。但是随之而来的是开发社区褒贬不一的观点,就像在异常中说的那样----Java的泛型和异常一样,备受人们争议。

         Java泛型带来的优秀产物就是集合框架,相比于以前使用Java集合框架,使用泛型后的集合框架显得十分简洁和安全。随之而来的便是Java泛型给人们带来的种种疑惑,设计者曾说Java泛型的主要设计灵感是来自C++的模板,但是稍微知道C++的人也许都会对Java的泛型感到失望,如何优雅地使用Java泛型进行优秀的程序设计是值得探索的。

 

1.Java的泛型为什么会这样?

         Java的泛型是在Java出现几乎10年后才出现的特性,而此时已经存在大量的旧代码,作为一门广泛使用的生产语言,Java不得不考虑兼容性。为了让使用新特性的人能够逐步的迁移到新的平台,新代码必须和旧代码保持兼容,这是一个十分伟大的动机,不会在一夜之间破坏现有的所有代码!所以Java的设计者们在一个月黑风高的晚上决定使用“擦除机制”来设计泛型!

 

2.擦除机制

         正确理解泛型概念的首要前提是理解擦除机制(type erasure)。Java中的泛型基本上都在编译器这个层次上来实现的。在生成的Java字节代码中是不包含泛型中的类型信息(在运行时时候都是原生类型(raw type))。在编写泛型代码的时的任何类型信息都会被编译器在编译的时候去掉。这个过程称为“类型擦除”。在代码中如:List<Integer>与List<String>等,在编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的(术语:不可具体化)。

         擦除的整个过程也比较容易理解:首先找到用来替换类型参数的具体类。这个具体类不指名则默认Object,如果指定了参数类型的上界,那么就使用这个上界来替换类型参数。同时去掉类型声明,即去掉<>的内容,比如T get()方法声明就编程了Object get(),List<String>就变成了List。接下来有时候可能会生成一些桥接方法。

 

3.不可具体化的类型

         不可具体化的类型是指:运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型。泛型是典型的不可具体化类型,参考ArrayList<String>中放入取出对象时候的源代码

 

public boolean add(E e) {
       ensureCapacityInternal(size + 1); // Increments modCount!!
       elementData[size++] = e;
       return true;
    }
 
public E get(int index) {
       rangeCheck(index);
 
       return elementData(index);
    }
 
@SuppressWarnings("unchecked")
    EelementData(int index) {
       return (E) elementData[index];
    }


运行时实际持有Object数组,但是编译的时候是可以有效的判断放入的是否是String类型,从而避免ClassCastException异常。

         与之相反的便是”可具体化的类型“,数组便是一个例子。数组总是在运行时才检查他们元素的类型约束。比如

                 

  Object[]father = new Father[10];
                   father[0]= new Son();
                   father[1]= new Integer(100);

这里并没有编译错误,但是运行时会抛出ArrayStoreException异常,和泛型的特点相反。比如如下大家熟悉的代码:

                   

List<Object>list = new ArrayList<Long>();

这段代码是非法的。

         更加让人感到疑惑的是如下的代码:

                  

 		   Father[]father = new Son[10];
                   father[0]= new Son();
                   father[1]= new Father();

这段代码会在第三行抛出ArrayStoreException异常,是的,数组的类型是强制约束。按理来说,一个数组中放置的元素应该都是安全的,父类的数组是可以放置子类型的元素。子类的数组是可以向父类数组转型成功的(这里确实也转型成功了),但是运行时却抛出异常,最终原因就是实际运行的时候数组并不是Father类型,而是Son类型。这里的可具体化和泛型的不可具体化就产生了矛盾(如果你new了一个T并将其转型为Object类型,那么是可以放入任何类型的,编译时期是不能被编译器发现,但是运行时会很有可能会抛出ArrayStroeException或者是ClassCastException异常),于是又是一个月黑风高的晚上,Java的设计者们决定了另外一个一不做二不休的决定-----不能创建泛型数组。

         在Java中你不能new T[],也不能通过如这样看似合理的ArrayList<String> asd = new ArrayList<String>[]; 代码,因为我是可以将其转型为Object[]类型,然后便可以向其中放入任何类型的类型了,而运行使JVM却对你说了NO,这对Java泛型就是一种赤裸裸的嘲讽!

 

 

4.通配符

         泛型的通配符让Java的泛型变得更加的灵活和强大(虽然并不是那么强大)。参数化类型是不可变的,也就是说下面这样的代码是不能通过编译的:

                  

 		List<Object>f = new ArrayList<>();
                List<String>f1 = new ArrayList<>(); 
                f= f1;

第三行会报错!看似f1应该是f的子类,但是与直觉相悖。原因还是在”擦除“,由于List<Object>中的类都是被Object替代,而List<String>中均被String替换,这里面如何来协调就是一个问题。

         如果想写出更加强大的API,那么通配符就是你应该选择的。如下代码:

public class Seven {
         publicstatic void main(String[] args) throws InstantiationException, IllegalAccessException{
                   Stack<Number>stack = new Stack<>();
                   List<Integer>list = new ArrayList<>();
                   list.add(10);
                   list.add(100);
                   list.add(1000);
                   Iterable<Integer>iter = list;
                   stack.pushAll(iter);
         }
}
class Stack<E>{
         privateE e;
         publicvoid push(E e){
         }
         /**
          * 这里是有通配符来提高API的灵活性
          * @param i
          */
         publicvoid pushAll(Iterable<? extends E> i){
                   for(Ee:i)
                            push(e);
         }
}


 

其中Stack类在创建实例的时候使用了Number类型作为其类型参数,那么在创建pushAll这个方法的时候,如果使用pushAll(Iterable<E> i)来作为签名,由于类型擦除机制,Java将在编译这一层面上对你SayNo!这里就引出PECS原则,即:如果参数化类型表示一个T生产者(Producer),就使用<? Extends T>,如果它是一个T的消费者(Comsumer),则使用<? Super T>。这一点对于设计出优秀强大的泛型代码是一个不错的原则!

         这里需要说明一下任意通配符----?,初看起来好像?和Object的区别不大(实际情况也是会被擦除为Object),但是如果你List<?>,那么将不可以插入任何值(除了null,但是这个没有什么意义)。这里的?是告诉编译器,我不知道将会接受什么类型,但是请使用Java泛型机制来处理它!但是,实际上有多大,只有合适的情况才能发挥它的才能了。

         与之相关的便是----类型捕获,参考下面的代码:

 

//这里是有任意通配符,函数调用后随即捕获了?的类型信息
         publicstatic void swap(List<?> list,int i,int j){
                   swapHelper(list,i, j);
         }
         //这里在由于helper知道E的类型,所以可以列表时安全的
         privatestatic <T> void swapHelper(List<T> list,int i,int j){
                   list.set(i,list.set(j, list.get(i)));
         }

         

Java通配符确实对Java的泛型的强壮起到了十分关键的作用,如何优雅的使用通配符,除了牢记上面的PECS原则,更加需要经验和眼光!

 

5.其他的一些忠告

         1.不要再新代码中使用原生类型

                   Java就是为了兼容才将泛型做成目前的状况,以后也不会再出现使用原生类型的代         码,最最重要的是:Java的泛型出现的一点原因是消除原生态可能产生的     ClassCastException异常,实现安全,如果你使用了原生,你就浪费了Java对你的一片心    意。

                   但是有些地方必须使用原生类型:

                            1.类文字

                            2.静态方法或者域的使用

 

       2.消除警告

                   Java对你的代码进行警告,表示这个代码是有可能出现问题,代码中警告越多,出现问题的可能性越大,想到解决警告的办法是最好,如果无法消除,那么可以使用注解@SuppressWarnings来消除,但是必须在那里证明自己消除的警告是类型安全的,是实际不会出现问题的。

                   对于@SuppressWarnings应该在尽可能小的范围内使用,千万别误将范围扩大而消  除了本来十分危险的警告。另外需要注意的一点:@SuppressWarnings是不能被注解在  return语句之上的。

 

         3.类表有限于数组

                   数组是在运行时确定信息,更容易出现问题,相对于类型安全的列表来说,有时候 牺牲一点性能而换来更多的安全和方便也是一个不错的选择。

        

         4.优先考虑泛型

                   泛型代码的好处之一就是模板,代码可复用,这正是设计者们所追求的,但是只有在有充分的理由(代码能够跨越多个类进行工作时)来使用泛型,否则不要使用泛型!     相对于泛型类,优先使用泛型方法则显得更加的灵活。

 

         5.优先考虑类型安全的异构容器

                   核心思想就是:将键(key)参数化而不是将容器参数化。

 

                  如下代码实现Map存放不同的key类型

                 

  publicclass Eight {
        
         publicstatic void main(String[] args) {
                   YiGouRongQiy = new YiGouRongQi();
                   y.put(String.class,"leon");
                   y.put(Integer.class,20);
                   System.out.println(y.get(String.class)+ " is " + y.get(Integer.class) + " years old!");
         }
}
 
class YiGouRongQi{
         privateMap<Class<?>,Object> hm = new HashMap<>();
         public<T> boolean put(Class<?> type,T instance){
                   if(type== null){
                            System.out.println("空指针异常");
                            returnfalse;
                   }
                   elseif(hm.containsKey(type)){
                            System.out.println("已经存在了键");
                            returnfalse;
                   }
                   hm.put(type,instance);
                   returntrue;
         }
        
//      @SuppressWarnings("unchecked")
         public<T> T get(Class<T> type){
                   //返回语句不能加注解
//               return(T)hm.get(type);
                   returntype.cast(hm.get(type));
                  
         }
}

                   集合API说明了泛型的一般应发,限制你每个容器只能有固定数目的类型参数。这 里展示了你可以将类型参数放在键上而不是容器上来避开这一限制。对于这种类型安全的异构容器,你可以使用Class对象作为键。

 

 

6.泛型的反思

         经常提起Java泛型,人们大多数时候总是在提它的诟病,暂且搁置开Java的历史原因,说说到底什么是泛型。就像C++中泛型又被称为模板,是的,泛型是对普通方法更加泛化的实现,这种泛型类是适用于所有类或者部分类,一般情况下是使用与部分类。对这些类会存在某些限制,而泛型类并不关心类的具体类型是什么,而在乎它是否可以执行某个方法!

这便是“潜在的类型机制”。所以,一般泛型类会要求参数类型实现了某个方法或者存在某个方法,使用实现接口或者反射式完全可以反映泛型的特点,也就是说实现“类型潜在机制”。泛型,泛型,其内部本来就不应该保存类的具体信息,因为一旦是针对某个具体类进行泛型类的编写,那么这个泛型类就完全没有必要写出泛型类。所以说,虽然Java泛型机制给开发者带来了诸多的不便,比较遗憾的一点是没有支持泛型数组,但是反过来看,擦除内部参数类型信息,利用接口和反射实现潜在类型机制,到也不失为一种还算优雅的方式。

简单代码如下:

 

public class Four {
         publicstatic void main(String[] args) {
                   //客户类,调用泛型方法,传入实现规定接口的类
                   GenericTest.doIt(newTestClass());
                   System.out.println("------------------------------");
                   GenericTest.doIt(newTestClass2());
         }
}
interface testInterface {
 
         /**
          * 这个接口规定了需要做什么事情
          */
         publicvoid doSomething();
}
 
/**
 * 以下两个类是使用了接口的特点,针对接口进行编程 要实现泛型的潜在类型机制----“我不关心这里使用的类型,只要它具有这些方法即可”
 * 也可以使用反射进行实现,不需要依赖接口的方式
 *@author leon
 *
 */
class TestClass implements testInterface {
         @Override
         publicvoid doSomething() {
                   System.out.println("I'mLeon,I'll do some thing here!");
         }
}
 
class TestClass2 implements testInterface {
 
         @Override
         publicvoid doSomething() {
                   System.out.println("I'mDiana,I'll do other thing here!");
         }
}
 
class GenericTest {
 
         /**
          * 这个泛型类中的泛型需要执行某些方法,但是必须 是实现了testInterface接口
          *
          * @param t
          */
         static<T extends testInterface> void doIt(T t) {
 
//               //一旦T确定了,那么编译器便可以确定在类中或者方法中使用的类型的内部一致性
//               //换句话说,就是这个类所有的T都被编译前替换了
                  
                   System.out.println("Dobefore!");
 
                   t.doSomething();
 
                   System.out.println("Doafter!");
         }
}


 

7.类型系统

         在Java中,大家比较熟悉的是通过继承机制而产生的类型体系结构。比如String继承

自Object。根据Liskov替换原则,子类是可以替换父类的。当需要Object类的引用的

时候,如果传入一个String对象是没有任何问题的。但是反过来的话,即用父类的引

用替换子类引用的时候,就需要进行强制类型转换。编译器并不能保证运行时刻这

种转换一定是合法的。这种自动的子类替换父类的类型转换机制,对于数组也是适

用的。String[]可以替换Object[]。但是泛型的引入,对于这个类型系统产生了一定的

影响。正如前面提到的List<String>是不能替换掉List<Object>的。

引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,

另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于 List<String>和

List<Object>这样的情况,类型参数String是继承自Object 的。而第二种指的是 List

接口继承自Collection 接口。对于这个类型系统,有如下的一些规则:

 

         1.相同类型 参数的泛型类的关系取决于泛型类自身的继承体系结构。即

List<String> 是Collection<String> 的子类型, List<String> 可以替换

Collection<String>。这种情况也适用于带有上下界的类型声明。

         2.当泛型类的类型声明中使用了通配符的时候,其子类型可以在两个维度上分别

展开。如对Collection<? extends Number>来说,其子类型可以在Collection 这个

维度上展开,即List<? extends Number>和Set<? extendsNumber>等;也可以在

Number 这个层次上展开,即Collection<Double>和Collection<Integer>等。如此

循环下去,ArrayList<Long>和HashSet<Double>等也都算是Collection<?extends

Number>的子类型。

         3.如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。

 

理解了上面的规则之后,就可以很容易的修正实例分析中给出的代码了。只需要把

List<Object>改成List<?>即可。List<String>是List<?>的子类型,因此传递参数时不会

发生错误。

注:上面这一点引用自《java深度历险》

 

 

8.闲话

         Java的泛型已经是Java本身不可分割的一部分了,就像其异常,在未来改动的可能性十分的渺茫,在历史问题上面编写代码更多的时候需要更多的思考,Java的泛型在JDK中的主要应用就是集合框架,而事实证明集合框架确实不错,虽然Java中的泛型没有想象的那么强大。所以,泛型与异常一样,如何写出优雅的泛型是值得追求的艺术。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值