• Java 泛型


    泛型的作用

    使用泛型机制编写的程序代码要比那些杂乱地使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性.泛型对于集合类尤其有用.

    说白了就是减少类型转换,增加可读性,同时避免代码中大量的Objcet的使用带来的转换安全隐患。

    简单泛型类的定义

    一个泛型类(generic class)就是具有一个或多个类型变量的类.下面是Pair类的代码:

    1. public class Pair<T>  
    2. {  
    3.     private T first;  
    4.     private T second;  
    5.     
    6.     public Pair(){ first = null; second = null;}  
    7.     public Pair(T first, T second) { this.first = first; this.second = second; }  
    8.     public T getFirst() { return first; }  
    9.     public T getSecond() { return second; }  
    10.     public void setFirst(T newValue) { first = newValue; }  
    11.     public void setSecond(T newValue) { second = newValue; }  
    12. }  

    Pair类引入了一个类型变量T,用尖括号<>括起来,并放在类名的后面.泛型类可以有多个类型变量.例如,可以定义Pair,其中第一个域和第二个域使用不同的类型:

    1. public class Pair<T, U> { ... }  

    类定义中的类型变量指定方法的返回类型以及域和局部变量的类型.例如:

    1. private T first;    // uses the type variable  

    注释:类型变量使用大写形式,且比较短,这是很常见的.Java库中,使用变量E表示集合的元素类型,KV分别表示表的关键字与值的类型. T(需要时还可以用临近的字母US)表示"任意类型".
    用具体的类型替换类型变量就可以实例化泛型类型,例如:

    1. Pair<String>  

    可以将结果想象成带有构造器的普通类:

    1. Pair<String>()  
    2. Pair<String>(String, String)  

    和方法:

    1. String getFirst()  
    2. String getSecond()  
    3. void setFirst(String)  
    4. void setSecond(String)  

    换句话说,泛型类可以看做普通类的工厂.

    泛型方法

    1. class ArrayAlg  
    2. {  
    3.     public static <T> T getMiddle(T...a)  
    4.     {  
    5.         return a[a.length / 2];  
    6.     }  
    7. }  

    这个方法是在普通类中定义的,而不是在泛型类中定义的.然而,这是一个泛型方法,可以从尖括号和类型变量看出这一点.注意,类型变量放在修饰符(这里是 public static)的后面,返回类型的前面.
    泛型方法可以定义在普通类中,也可以定义在泛型类中.
    当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:

    1. String middle = ArrayAlg.<String>getMiddle("John""Q.""Public");  

    在这种情况下,方法调用中可以省略<String>类型参数.编译器有足够的信息能够推断出所调用的方法.它用names的类型(String[])与泛型类型T[]进行匹配并推断出T一定是String,也就是说,可以调用

    1. String middle = ArrayAlg.getMiddle("John""Q.""Public");  

    书上说在大多数情况下,对泛型方法的推断没有问题,但是,如果编译器无法推断出一个合适的类型,此时编译器就会报错。

    如下:

    1. package com.zjf;
    2.  
    3. public class Test {
    4.    public static void main(String[] args) {
    5.       test("zjf","xhj");
    6.       test("zjf",11);
    7.       test("zjf",11,3.14);
    8.       test(11,3.14,0);
    9.    }
    10.  
    11.    public static <T> void test(T... t)
    12.    {
    13.  
    14.    }
    15. }

    实际上,在我的环境下(java7),不仅编译没有报错,运行也没有问题。

    我猜想因为他们都是属于Object的子类,再怎么推断不出来,也可以使用Object。

    类型变量的限定

    使用extends 进行限制

    1. public static <T extends Comparable> void test(T t1,T t2)
    2.    {
    3.       t1.compareTo(t2);
    4.    }

    这样能保证,T必然是Comparable的子类或者接口实现,在方法里面就可以对变量a的内容使用Comparable具有的方法。

    一个类型变量或通配符可以有多个限定,例如:

    extends Comparable & Serializable  

    泛型代码和虚拟机

    虚拟机没有泛型类型对象--所有对象都属于普通类.无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type).原始类型的名字就是删除类型参数后的泛型类型名.擦除(erased)类型变量,并替换为限定类型(无限定的变量用Object).

    例如,Pair<T>的原始类型如下所示:

    1. public class Pair  
    2. {  
    3.     private Object first;  
    4.     private Object second;  
    5.     
    6.     public Pair(Object first, Object second)  
    7.     {  
    8.         this.first = first;  
    9.         this.second = second;  
    10.     }  
    11.     ...  
    12. }  

     因为T是一个无限定的变量,所以直接用Object替换.
     
    在程序中可以包含不同类型的Pair,例如,Pair<String>Pair<GregorianCalendar> .而擦除类型后就变成原始的Pair类型了.

    原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换.例如,Pair<T>中的类型变量没有显式的限定,因此,原始类型用Object替换T .假定声明了一个不同的类型.

    1. public class Interval<T extends Comparable & Serializable> implements Serializable  
    2. {  
    3.     private T lower;  
    4.     private T upper;  
    5.     ...  
    6.     public Interval(T first, T second)  
    7.     {  
    8.         if (first.compareTo(second) <= 0)  
    9.         {  
    10.             lower = first;  
    11.             upper = second;  
    12.         }  
    13.         else  
    14.         {  
    15.             lower = second;  
    16.             upper = second;  
    17.         }  
    18.     }  
    19. }  

        原始类型Interval如下所示:

    1. public class Interval implements Serializable  
    2. {  
    3.     private Comparable lower;  
    4.     private Comparable upper;  
    5.     ...  
    6.     public Interval(Comparable first, Comparable second)  
    7.     {...}  
    8. }  

    翻译泛型表达式

    当程序调用泛型方法时,如果擦掉返回类型,编译器插入强制类型转换.例如,下面这个语句序列

    1. Pair<Employee> buddies = ...;  
    2. Employee buddy = buddies.getFirst();  

    擦除getFirst的返回类型后将返回Object类型.编译器自动插入Employee的强制类型转换.也就是说,编译器把这个方法调用翻译为两条虚拟机指令:
       
    对原始方法Pair.getFirst的调用
       
    将返回的Object类型强制转换为Employee类型

    翻译泛型方法

     类型擦除也会出现在泛型方法中.程序员通常认为下述的泛型方法

    1. public static <T extends Comparable> T min(T[] a)  

        是一个完整的方法族,而擦除类型之后,只剩下一个方法:

    1. public static Comparable min(Comparable[] a)  

    方法擦除会导致方法参数的变化,所以在继承的时候直接使用方法擦除会导致问题:

    如下代码:

    1. class DateInterval extends Pair<Date>  
    2. {  
    3.     public void setSecond(Date second)  
    4.     {  
    5.         if (second.compareTo(getFirst()) >= 0)  
    6.             super.setSecond(second);  
    7.     }  
    8.     ...  
    9. }  

    如果执行完方法擦除:

    1. class DateInterval extends Pair  
    2. {  
    3.     public void setSecond(Date second) { ... }  
    4. }  

    注意,此时超类PairsetSecond中的T被擦除了,编程了setSecond(Object second),但是子类的方法中是Date,不会被擦除,依然是setSecond(Date second)

    这样,子类中的setSecond方法就不是超类中方法的重写(overwrite)。

    要解决这个问题,就需要编译器在DateInterval类中生成一个桥方法(bridge method):

    1. public void setSecond(Object second) { setSecond((Date)second); }  

    上面说的是方法参数擦除导致的问题。

    还有一种方法返回类型擦除导致的问题。

    如:

    1. class DateInterval extends Pair<Date>  
    2. {  
    3.     public Date getSecond() {  }  
    4.     ...  
    5. }  

        在擦除的类型中,有两个getSecond方法:

    1. Date getSecond();       // 定义在DateInterval  
    2. Object getSecond();     // 继承自Pair 

    在我们的代码中,是不允许出现这种代码的,方法的签名是方法名和参数,这两个可以说是一个方法。但是虚拟机做了特殊处理,可以应对这种情况。

     注释:桥方法不仅用于泛型类型.在一个方法覆盖另一个方法时可指定一个更严格的返回类型.例如:

    1. public class Employee implements Cloneable  
    2. {  
    3.     public Employee clone() throws CloneNotSupportedException { ... }  
    4. }  

        Object.cloneEmployee.clone方法被说成具有协变的返回类型(covariant return types).
       
    实际上,Employee类有两个克隆方法:

    1. Employee clone();       // defined above  
    2. Object clone();         // synthesized bridge method, overrides object.clone  

    合成的桥方法调用了新定义的方法.
       
    总之,需要记住有关Java泛型转换的事实:
        
    虚拟机中没有泛型,只有普通的类和方法.
       
    所有的类型参数都用它们的限定类型替换.
       
    桥方法被合成来保持多态.
       
    为保持类型安全性,必要时插入强制类型转换.

    调用遗留代码

    为了保证老的版本的兼容性。jdk有如下折中:

    如,在调用一个需要MyClass<String>类型作为参数的方法时,我们可以传入MyClass.

    反过来,在调用一个需要MyClass类型作为参数的方法时,我们可以传入MyClass<String>.

    乱入。。

    HashTable是Dictionary的子类,在Java1.2版本之后,已经被Map和HashMap替换,标注为废弃了。

    约束与局限性

    在下面几节中,将阐述使用Java泛型时需要考虑的一些限制.大多数限制都是由类型擦除引起的.

    • 不能用基本类型实例化类型参数

        不能用类型参数代替基本类型,因此,没有Pair<double>,只有Pair<Double>.当然,原因是类型擦除.擦除之后,Pair类含有Object类型的域,Object不能存储 double .
      

    • 运行时类型查询只适用于原始类型

        虚拟机中的对象总有一个特定的非泛型类型.因此,所有的类型查询只产生原始类型.例如:

    1. if (a instanceof Pair<String>)  // error  

        实际上仅仅测试a是否是任意类型的一个Pair .下面的测试同样如此:

    1. if (a instanceof Pair<T>)       // error  

         

    • 不能创建参数化类型的数组

        不能实例化参数化类型的数组,因为数组有存储检查,只能存入定义的类型,而泛型化类型会使用类型擦除。我们不能保证存储的是什么。如果你定义了一个Pair<String>[]的数组,在运行期间你即使存储了一个字符串都是可以的。因为它是Object型的。

     

     这一段书上讲的很浅,没有具体申深入探讨原因,我搜索了一下,整理如下。

    先假设Java可以创建泛型数组,由于java泛型的类型擦除和数组的协变。下面的代码将会编译通过。

    List<String>[] stringLists=new List<String>[1];

    List<Integer> intList = Arrays.asList(40);

    Object[] objects = stringLists;

    objects[0]=intList;//数组是协变的  在编译期间这是个object数组  可以存储任何类型  真正的类型检查是在运行时处理的。

    String s=stringLists[0].get(0);

         由于泛型的类型擦除,List<Integer>,List<String>与List在运行期并没有区别,所以List<String>放入List<Integer>并不会产生ArrayStoreException异常。但是String s=stringLists[0].get(0);将会抛出ClassCastException异常。如果允许创建泛型数组,就绕过了泛型的编译时的类型检查,将List<Integer>放入List<String>[],并在实际存的是Integer的对象转为String时抛出异常。

    • 不能实例化类型变量

        不能使用像 new T(...),new T[...]T.class这样的表达式中的类型变量.例如下面的Pair<T>构造器就是非法的:

    1. public Pair() { first = new T(); second = new T(); }    // error  

    类型擦除将T改变为Object,而且本意肯定不希望调用 new Object().

    而且不能调用: first = T.class.newInstance();//ERROR

    因为, 表达式 T.class 是不合法的,必须像下面这样设计 API 以便可以支配Class 对象:

    1. public static<T> Pair<T> makePair(Class<T> cl)
    2. {
    3.     try{ return new Pair<>(c1.newInstance(), c1.newInstance()) }
    4.     catch(Exception ex) {return null;}
    5. }
    • 不能抛出或捕获泛型类的实例

    既不能抛出也不能捕获泛型类对象。且泛型类扩展 Throwable 也是不合法的;

    如, public class Problem<T> extends Exception {} // ERROR--can't extend Throwable

    catch 子句中不能使用类型变量, 以下方法不能通过编译:

    public static <T extends Throwable> void dowork(Class<T> t)

    {

    ...

    catch(T e) // ERROR--can't catch type variable

    }

    不过, 在异常规范中适用类型变量是允许的,以下方法是合法的:

    public static <T extends Throwable> void dowork(T t) throws T // OK

    这个也不太清楚为什么,应该是异常框架的机制导致的。

    • 不能在静态域或方法中引用类型变量:

    public class Singleton<T>

    {

    private static T single; // ERROR

    private static getSingle() // ERROR

    {}

    }

    因为类型擦除后, 只剩下 Singleton 类, 它只包含一个 singleInstance 域;

    这个原则不太清楚,完全可以使用Object来消除,这个是可以跑起来的。不知道为什么不让这么用。

     

    泛型类型的继承规则

    无论ST有什么联系(甚至是父类与子类的关系),通常,Pair<S>Pair<T>没有什么联系.

    也就是说在一个定义了需要Pair<Person>参数的方法,不能传入Pair<Man>。反之也不行。

    Pair<Person> 和 Pair<Man>也不能相互赋值。

    之所以可以把一个Men作为Person使用,因为它可以代表Person做一切操作。

    但是如果你把一个List<Men>作为List<Person>使用,那么:

    1. List<Men> m = new ArrayList<Men>();
    2. List<Person> p = m;
    3. p.add(new Person());

    假设第二行可以执行,那么第三行就出事儿了。我们可以向一个List<Person>中add Person对象,但是其实p指向的是一个List<Men>对象,我们不能对一个List<Men>对象中add Person对象。

     

    泛型类可以扩展实现其他泛型类,这一点和普通的类没有区别。

    如List<T>和ArrayList<T>。

    ArrayList<T>实现了List<T>,它仍然是一个泛型类。

    还有一种情况,实现了一个具体的Comparable<Person>,Person类已经不是泛型类。如:

    1. public class Person implements Comparable<Person>{
    2.    public Person(Integer id) {
    3.       this.id = id;
    4.    }
    5.    private Integer id;
    6.  
    7.    public Integer getId() {
    8.       return id;
    9.    }
    10.    public void setId(Integer id) {
    11.       this.id = id;
    12.    }
    13.    @Override
    14.    public int compareTo(Person o) {
    15.       // TODO Auto-generated method stub
    16.       return this.id.compareTo(o.id);
    17.    }
    18. }

     

    通配符类型

    extends

    通配符类型Pair<? extends Employee>表示任何泛型Pair类型,它的类型参数是Employee的子类,Pair<Manager>,但不是Pair<String>.

    但是这样岂不是会出现我们说的对一个List<Men>对象中add Person对象的现象吗?

    所以,java做了限制:

    Pair<? extends Employee>里的所有T,如果是参数就变成了? extends Employee类型,返回类型也变成了? extends Employee

    对于参数? extends Employee,我们不确定他是Employee的哪一个子类(还是那个道理,我们不能对一个运行时TManager的方法,传入Employee),所以只能传入null

    对于返回类型是? extends Employee

    我们可以使用Employee = 来代表。因为他们都是Employee的子类。

    super 

    1. super Manager  

    这个通配符限制为Manager的所有超类型.

    super Manager如果是参数,那么可以向它传入所有Manager的子类。

    super Manager如果是返回值,那么只能把它赋给一个Object

    这基本和extends是相反的。

    也就是说,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取.

    无限定通配符

        可以使用无限定的通配符,例如,Pair<?>.初看起来,这好像与原始的Pair类型一样.实际上,有很大不同,类型Pair<?>有方法如下所示:

    1. ? getFirst()  
    2. void setFirst(?)  

    getFirst的返回值只能赋给一个Object, 可以调用setFirst(null).

    为什么要使用这样脆弱的类型?它对于许多简单的操作非常有用.例如,下面这个方法将用来测试一个Pair是否包含一个 null 引用,它不需要实际的类型

    1. public static boolean hasNulls(Pair<?> p)  
    2. {  
    3.     return p.getFirst() == null || p.getSecond() == null;  
    4. }  

        通过将hasNulls转换成泛型方法,可以避免使用通配符类型:

    1. public static <T> boolean hasNulls(Pair<T> p)  

    但是,带有通配符的版本可读性更强.

     

    List<?>和List和List<Object>的差别:

    通配符类型 List<?> 与原始类型 List 和具体类型 List<Object> 都不相同。如果说变量 x具有 List<?> 类型,这表示存在一些 T 类型,其中 x 是 List<T>类型,x 具有相同的结构,尽管我们不知道其元素的具体类型。这并不表示它可以具有任意内容,而是指我们并不了解内容的类型限制是什么 — 但我们知道存在 某种限制。另一方面,原始类型 List 是异构的,我们不能对其元素有任何类型限制,具体类型 List<Object> 表示我们明确地知道它能包含任何对象(当然,泛型的类型系统没有 "列表内容" 的概念,但可以从 List 之类的集合类型轻松地理解泛型)。

     

     

    通配符捕获

        编写一个交换一个Pair元素的方法:

    1. public static void swap(Pair<?> p)  

        通配符不是类型变量,因此,不能在编写代码中使用"?"作为一种类型.也就是说,下述代码是非法的:

    1. ? t = p.getFirst();     // error  
    2. p.setFirst(p.getSecond());  
    3. p.setSecond(t);  

        这是一个问题,因为在交换的时候必须临时保存第一个元素.幸运的是,这个问题有一个有趣的解决方法,可以写一个辅助方法法swapHelper,如下所示:

    1. public static <T> void swapHelper(Pair<T> p)  
    2. {  
    3.     T t = p.getFirst();  
    4.     p.setFirst(p.getSecond());  
    5.     p.setSecond(t);  
    6. }  

        注意,swapHelper是一个泛型方法,swap不是,它具有固定的Pair<?>类型的参数.
       
    现在可以由swap调用swapHelper:

    1. public static void swap(Pair<?> p) { swapHelper(p); }  

        在这种情况下,swapHelper方法的参数T捕获通配符.它不知道是哪种类型的通配符,但是,这是一个明确的类型,并且<T>swapHelper的定义只有在T指出类型时才有明确的含义.

  • 相关阅读:
    python实现指定目录下批量文件的单词计数:串行版本
    PythonPP+lambda:示例
    python面向对象编程基础
    《平凡的世界》读后感
    代码
    【转】提高沟通效果的十个技巧
    LODOP中page-break-before:always给div分页
    LODOP超文本简短问答和相关内容
    Lodop打印较大的超出纸张的图片
    Lodop打印设计矩形重合预览线条变粗
  • 原文地址:https://www.cnblogs.com/xiaolang8762400/p/7048560.html
Copyright © 2020-2023  润新知