Java编程思想__泛型(三)

  • 下面是使用模板的C++示例, 你将注意到用于参数化类型的语法十分相似,因为Java是受C++的启发。
template class Manipulator{
    T obj;
 public:
    Manipulator(T x){ obj=x; }
    void maniput(){ obj.f(); }       
}

class HasF{
    public:
        void f(){
        cout << "HasF::f()" << endl;
    }
    
    int main(){
        HasF hf;
        Manipulator manipulator(hf);
        manipulator.maniput();
    }
}
  1.  Manipulator 类存储了一个 T 的对象,有意思的地方是 manipulator() 方法,它在调用 obj 上调用方法 f()。它怎么能知道 f() 方法是为类型参数 T 而存在的呢?当你实例化这个模板时,C++编译器将进行检查,因此在 Manipulator 被实例化的这一刻,它看到 HasF拥有一个方法 f() 。如果情况并非如此,就回到得到一个编译期错误,这样类型安全就得到了保障。
  2. C++ 编写这种代码很简单,因为当模板被实例化时,模板代码知道其模板参数的类型。Java泛型就不同了。如下 Java版本 HasF
class HasF{
    void f(){
        System.out.println("HasF f()");
    }
}

public class Manipulator{
    private T t;

    public Manipulator(T t) {
        this.t = t;
    }
    public void manipultor(){
        //error: 找不到符号方法f()
        t.f();
    }

    public static void main(String[] args) {
        HasF hasF = new HasF();
        Manipulator hasFManipulator = new Manipulator(hasF);
        hasFManipulator.manipultor();
    }
}
  1. 由于有了擦除,Java编译器无法将 manupulator() 必须能够在 t 上调用f() 这一需求映射到 HasF 拥有 f() 这一事实上。
  2. 为了调用 f() ,我们必须协助泛型类,给定泛型类的边界,以告知编译器只能接受遵循这个边界的类型。这里冲用了 extends 关键字。 由于有了边界,西面代码就可以编译了。
public class Manipulator2{
    private T t;

    public Manipulator(T t) {
        this.t = t;
    }
    public void manipultor(){
        t.f();
    }
}
  1. 边界 声明 T 必须具有类型 HasF 或者从 HasF 导出的类型。如果情况确实如此,那么就可以安全地在 obj上调用 f()。
  2. 我们说泛型类型参数将擦除到它的第一个边界(它可能会有多个边界,稍后你就会看到),我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例一样。T 擦除到了 HasF,就好像在类的声明中用 HasF 替换了T一样。
  3. 你可能已经正确地观察到 ,在Manipulator2类中,泛型没有贡献任何好处。只需很容易地自己去执行擦除,就可以创建出没有泛型的类:
public class Manipulator3{
    private HasF hasF;

    public Manipulator3(HasF hasF) {
        this.hasF = hasF;
    }
    public void manipultor() {
        hasF.f();
    }
}
  1. 这提出可很重要的一点: 只有当你希望使用的类型参数比某个具体类型(以及它的所有子类型) 更加 泛化 时___也就是说,当你希望代码能够跨多个类工作时,使用泛型才有所帮助。
  2. 因此,类型参数和它们在有用的泛型代码中的应用,通常比简单的类替换要更复杂。但是,不能因此而认为 形式是有所缺陷的。
  3. 例如,如果某个类有一个返回T 的方法,那么泛型就有所帮助,因为它们之后将返回准确的类型:
class ReturnGenericType{
    private T t;

    public ReturnGenericType(T t) {
        this.t = t;
    }
    public T getT(){
        return t;
    }
}
  1. 必须查看所有的代码,并确定它是否 足够复杂 到必须使用泛型的程序。

 

迁移兼容性

  • 为了减少潜在的关于擦除的混淆,你必须清除地认识到这不是一个语言特性。它是Java 的泛型实现中的一个折中,因为泛型不是Java语言出现时就有的组成部分,所以这种折中是必须的。这种折中会让你痛苦,因为你需要习惯它并了解为什么它会是这样。
  • 如果泛型在Java1.0中就已经是其一部分了,那么这个特性将不会擦除来实现___它将使用具体化,使用类型参数保持为第一类实体,因此你就能够在类型参数上执行基于类型语言操作和反射操作。你将在本章稍后看到,擦除减少了泛型的泛化性。泛型在Java中仍旧是有用的,只是不如它们设想的那么有用,而原因就是擦除
  • 在基于擦除的实现中,泛型类型被当做第二类类型处理,即不能再某些重要的上下文环境中使用的类型。泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如 List 这样的类型注解将被擦除为 List, 而普通的类型变量在未指定边界的情况下将被擦除为 Object。
  • 擦除的核心动机它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为 迁移兼容性。在理想状态下,当所有事物都可以同时被泛化时,我们就可以专注于此。在现实中,即使程序员只编写泛型代码,他们也必须处理在Java SE5之前编写的非泛型类库。那些类库的作者可能从没有想过要泛化它们的代码,或者可能刚刚开始接触泛型。
  • 因此Java泛型不仅必须支持向后兼容性,即现有的代码和类文件仍旧合法,并且继续保持其之前的含义。而且还要支持迁移兼容性,使得类库按照他们自己的步调变为泛型的,并且当某个类库变为泛型时,不会破坏依赖于它的代码和应用程序。在决定这就是目标之后,Java设计者们和从事此问题相关工作的各个团队决策认为擦除是唯一可行的解决方案。通过允许非泛型代码与泛型代码共存,擦除使得这种向着泛型的迁移称为可能。
  • 例如, 假设某个应用程序具有两个类库 X 和 Y,并且 Y 还要使用类库 Z。随着JavaSE5 的出现,这个应用程序和这些类库的创建者最终可能希望迁移到泛型上。但是,当进行这种迁移时,他们有着不同动机和限制。为了实现迁移兼容性,每个类库和应用程序都必须与其他所有的部分是否使用了泛型无关。这样,它们必须不具备探测其他类库是否使用了泛型的能力。因此,某个特定的类库使用了泛型这样的证据必须被擦除。
  • 如果没有某种类型的迁移途径,所有已经构建了很长时间的类库就需要与希望迁移到Java泛型上的开发者说再见了。但是,类库是编程语言无可争议的一部分,它们对生成效率会产生最重要的影响,因此这不是一种可以接受的代价。擦除是否是最佳的或者唯一的迁移途径,还需要时间来证明。

 

擦除的问题

  • 因此,擦除主要的正当理由是从非泛型代码到泛型代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入Java语言。

  • 擦除使得现有的非泛型客户端代码能够在不改变的情况下继续使用,直至客户端准备好使用泛型重写这些代码。这是一个崇高的动机,因为它不会突然间破坏所有现有的代码。

  • 擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操作之中,例如转型, instanceof 操作和new表达式。因为所有关于参数的类型信息都丢失了,无论何时,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来好像拥有有关参数的类型信息而已。

  • 因此,如果你便携了下面这样的代码段:

class Foo{
    T var;
}
  1. 那么,看起来当你在创建Foo 的实例时:
Foo f=new Foo();
  1. class Foo 中的代码应该知道现在工作于Cat之上,而泛型语法也在强烈暗示: 在整个类中的各个地方,类型 T 都在被替换。但事实并非如此,无论何时,当你在编写这个类代码时,必须提醒自己:  不 ,它只是一个 Object
  2. Java 中的泛型基本都是在编译器这个层次来实现的,在生成的 Java 字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器的时候去掉。这个过程就称为 类型擦除
public class ArrayList1{
    
    public static void main(String[] args) {
        List list = new ArrayList<>();
        list.add("str");
        List integers = new ArrayList<>();
        integers.add(123);

        System.out.println(list.getClass());
        System.out.println(integers.getClass());
        System.out.println(integers.getClass() == list.getClass());
    }
}

//运行结果为
true

 

  1. 我们定义两个ArrayList数组 ,不过一个是 List 泛型类型,只能保存字符串 ,一个是List 类型,只能保存 整型。
  2. 我们通过 list 和 integers 对象的getClass() 获取它们的类的信息,最后结果发现为 true。说明了泛型类型 String 和Integer 都被擦除掉了,只剩下原始类型。
class ArrayList2{
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        List list=new ArrayList<>();
        list.add(1);
        //利用反射进行添加数据asd
        list.getClass().getMethod("add",Object.class).invoke(list,"asd");

        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
}

//运行结果为
1
asd
  1. 在程序中定义一个 List 的集合,如果直接调用 add方法,那么只能存储 整型数值。
  2. 不过我们利用反射调用 add 方法的时候,却可以存储字符串。这就说明了 Integer 泛型实例在编译之后就被擦除了,只保留了原始类型。

 

类型擦除后保留的原始类型

  • 原始类型(raw type) : 擦除了泛型信息,最后在字节码中的类型变量的真正类型
  • 无论何时定义一个泛型,相应的原始类型都会被自动地提供。类型变量被擦除,并使用其限定类型(无限的变量用Object)替换。
class Person{

    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}

//原始类型为

class Person{

    private Object object;

    public Object getObject() {
        return object;
    }

    public void setT(Object object) {
        this.object = object;
    }
}
  • 在 Person 中,T 是一个无限定的类型变量,所以用 Object 替换。其结果就是一个普通的类,如同泛型加入 Java 编程语言之前已经实现的那样。
  • 在程序中可以包含不同类型的 Person,(Person , Person) 但是擦除类型后它们就成为原始的 Person类型了,原始类型都是Object。
  • 如果类型变量有限,那么原始类型就用第一个边界的类型变量来替换。例如
class ArrayList3{
    //...
}
  1. 那么原始类型就不是 Object 而是 ArrayList2。
  2. 注意: 如果 ArrayList 这样声明 class ArrayList ,那么原始类型就用 Serializable 替换, 而编译器在必要的时要向 ArrayList2 插入强制类型转换。为了提高效率,应该将标签接口放在边界限定列表的末尾。

 

要区分原始类型泛型类型变量的类型

  • 在调用泛型方法的时候,我们可以指定泛型,也可以不指定泛型。
  • 在不指定泛型的情况下,泛型变量的类型为该方法中的几种参数变量类型的同一个父级的最小级,直到 Object。
  • 在指定泛型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类。
class ArrayList4{

    //这是一个简单的泛型方法
    static  T add(T t,T k){
        return k;
    }

    public static void main(String[] args) {
        //不指定泛型的时候

        //两个Integer 所以T为 Integer
        Integer add = ArrayList4.add(1, 2);
        //一个Integer 一个Double T取同一父级最小级 Number
        Number number = ArrayList4.add(1, 1.2);
        //一个Integer 一格String T取同一父级为 Serializable
        Serializable ask = ArrayList4.add(1, "ask");

        //指定泛型的时候

        //指定了Integer 所以只能为 Integer 类型 或其子类 当你传入 Double 是就会编译出错
        Integer add1 = ArrayList4.add(1, 2);
        //指定为 Number 所以可以为 Integer 和Double
        Number number1 = ArrayList4.add(1, 2.2);
    }
}
  1. 其实在泛型类中,不指定泛型的时候,也差不多,只不过这个时候类型为 Object。比如ArrayList 中如果不指定泛型类型,那么这个ArrayList会默认为 Object 类型可以放置任意类型的对象。
class ArrayList5{
    public static void main(String[] args) {
        ArrayList objects = new ArrayList<>();
        objects.add(1);
        objects.add("123");
        objects.add(new Date());
    }
} 
  

 

类型擦除引起的问题及解决办法

  • 由于种种原因,Java不能实现真正的泛型,只能使用类型擦除来实现伪泛型,这样虽然不会有类型膨胀的问题,但是也引起了许多新的问题。所以,Sun对这些问题做出了许多限制,避免我们犯各种错误。

 

1).先检查,在编译,以及检查编译的对象和引用传递的问题

 

  • 既然说类型变量在编译的时候擦除掉,那为什么我们往 List str=new ArrayList<>(); 所创建的数组列表 str 中,不能使用 add 方法添加 整型数据呢? 不是说泛型变量 Integer 会在编译时候擦除变为 原始类型 Object ? 为什么不能存在别的类型呢? 既然类型擦除了 ,如何保证我们只能使用泛型变量限定的类型呢?
  • Java 是如何解决这个问题的呢?  Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,在进行编译的。
    public static void main(String[] args) {
        List objects = new ArrayList<>();
        objects.add("123");
        //编译失败
        objects.add(1);
    }
  1. 使用 add方法添加一个整型, 在 idea中,直接就会报错,说明这就是在编译之前的检查。因为如果是在编译之后检查,类型擦除后,原始类型为 Obejct ,是应该运行任意引用类型的添加的。可实际上却不是这样,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。
  2. 那么,这么类型检查是针对谁的呢? 我们先看看参数化类型与原始类型的兼容
//以前写法
List list=new ArrayList();

//现在写法
List lists=new ArrayList();

//如果是与以前的代码兼容,各种引用传值之间,必然会出现如下情况

List list1=new ArrayList<>();

和

List list2=new ArrayList();
  1. 这样是没有错误的,不过会有一个编译时警告。
  2. 不过在 list1 可以实现与完全使用泛型参数一样的效果, list2 则完全没有效果。
  3. 因为,本来类型检查就是编译时完成的。new ArrayList() 只是在内存中开辟一块存储空间,可以存储任何的类型对象。而真正涉及及类型检查的是它的引用,因为我们使用它引用 list1 来调用它的方法,比如说它调用 add() 方法。所以 list1 引用能完成泛型的类型检查。
    public static void main(String[] args) {
        List list1 = new ArrayList<>();
        list1.add("1234");
        //返回类型为 String
        String s = list1.get(0);
        
        List list2=new ArrayList();
        list2.add(112);
        list2.add("str");
        list2.add(new Date());
        //返回类型为 object
        Object o = list2.get(0);
    }
  1. 通过上面的例子,我们可以明白,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检查,而无关它真正引用的对象。

 

从这里,我们可以在讨论下,泛型中参数化类型为什么不考虑继承关系

List str1=new ArrayList(); //编译失败

List str2=new ArrayList(); //编译失败


//我们先看第一种情况,将第一种情况扩展成下面的形式:

List objs=new ArrayList<>();
objs.add(new Object());
objs.add(new Object());

List str3=objs; //编译失败


//第二种情况
List str4=new ArrayList<>();
str4.add(new String());
str4.add(new String());

List objts= str4; 
  
  • 实际上,在运行 List str=objs 代码的时候,就会有编译错误。那么我们先假设它编译没错。那么当我们使用 str3 引用 get() 方法取值的时候,返回的都是 String 类型的对象(上面提到了,类型监测是根据引用来决定的),可是它里面实际上已经被我们存放了 Object 类型的对象,这样,就会有 ClassCastException 异常,所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。(这也是泛型出现的原因,就是为了解决类型转换的问题,我们不能违背它的初衷)。
  • List objts= str4; 这种情况比上一种好的多,最起码,在我们使用 objts 取值的时候不会出现 ClassCastException ,因为是从String 转换为 Object。可是,这样做有什么意义呢?  泛型出现的原因,就是为了解决类型转换的问题。我们使用泛型,到头来,还是要自己强转,违背了泛型设计的初衷。所以Java 不允许这么干。在说,你如果又用 objts 的 add() 添加新对象,那么取值的时候,我们怎么知道取出来的是String类型还是Object类型?。

     

    2).自动类型转换

    • 因为类型擦除的问题,所以所有泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么我们再获取的时候,不需要进行强制类型转换呢?
    //arrayList 源码
    
      /**
         * Returns the element at the specified position in this list.
         *
         * @param  index index of the element to return
         * @return the element at the specified position in this list
         * @throws IndexOutOfBoundsException {@inheritDoc}
         */
        public E get(int index) {
            rangeCheck(index);
    
            return elementData(index);
        }
    • 可以看到,在return 之前会根据泛型变量进行强转。
    //写了个简单的程序
    
    public class ArrayList5 {
    
        public static void main(String[] args) {
            List list1 = new ArrayList<>();
            list1.add("1234");
            String s = list1.get(0);
        }
    }
    
    //反编译如下
    public class generic.ArrayList5 {
      public generic.ArrayList5();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."":()V
           4: return
    
      public static void main(java.lang.String[]);
        Code:
           0: new           #2                  // class java/util/ArrayList
           3: dup
           4: invokespecial #3                  // Method java/util/ArrayList."":()V
           7: astore_1
           8: aload_1
           9: new           #4                  // class java/util/Date
          12: dup
          13: invokespecial #5                  // Method java/util/Date."":()V
          16: invokevirtual #6                  // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
          19: pop
          20: aload_1
          21: iconst_0
          22: invokevirtual #7                  // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
          25: checkcast     #4                  // class java/util/Date
          28: astore_2
          29: return
    }
    1. list.get(0) 方法 方法返回值是一个Object 类型 说明类型擦除了 
    2. 然后在 checkcast  #4 操作之后跳转到 #4  如 new #4 class java/util/Date 是一个Date类型,即做了Date 类型转换。 所以它不是在get 方法强转的 是在你调用的地方强转的。

    附一个checkcast解释

    checkcast checks that the top item on the operand stack (a reference to an object or array) can be cast to a given type. For example, if you write in Java:
    
    return ((String)obj);
    
    then the Java compiler will generate something like:
    
    aload_1 ; push -obj- onto the stack
    checkcast java/lang/String ; check its a String
    areturn ; return it
    
    checkcast is actually a shortand for writing Java code like:
    
    if (! (obj == null || obj instanceof )) {
    throw new ClassCastException();
    }
    // if this point is reached, then object is either null, or an instance of
    //  or one of its superclasses.
    

     

    3),类型擦除与多态冲突和解决方案

    //现在有一个泛型类
    
    public class Pair {
        private T t;
    
        public T getT() {
            return t;
        }
    
        public void setT(T t) {
            this.t = t;
        }
    }
    
    //我们想要一个子类继承它
    
    public class DataClass extends Pair{
        @Override
        public Date getT() {
            return super.getT();
        }
    
        @Override
        public void setT(Date date) {
            super.setT(date);
        }
    }
    1. 在这个子类中,我们假设父类泛型类型为 Pair 在子类中,我们覆盖了父类的两个方法,我们的意愿是这个样的。
    2. 将父类的泛型类型限定为 Date , 那么父类里面的两个方法的参数都为 Date 类型。
        public Date getT() {
            return t;
        }
    
        public void setT(Date t) {
            this.t = t;
        }
    1. 所以,我们再子类中重写这两个方法一点问题也没有,实际上,从它们的 @ Override 标签中可以看到,一点问题也没有,实际上是这样?

    分析:

    • 实际上,类型擦除后,父类的泛型类型全部变为原始类型 Object,所以父类编译之后会变成下面这样
    public class Pair {
        private Object t;
    
        public Object getT() {
            return t;
        }
    
        public void setT(Object t) {
            this.t = t;
        }
    }
    1. 再看子类两个重写的方法类型:
        @Override
        public Date getT() {
            return super.getT();
        }
    
        @Override
        public void setT(Date date) {
            super.setT(date);
        }
    1. 先来分析 setT() 方法,父类的类型是 Object ,而子类的类型为 Date ,参数类型不一样,如果实在普通的继承关系中,根本就不会重写,而是重载。
    2. 我们如下进行测试:
        public static void main(String[] args) {
            DataClass aClass = new DataClass();
            aClass.setT(new Date());
            //编译错误
            aClass.setT(new Object());
        }
    1. 如果是重载,那么子类中两个 setT() 方法,一个参数是 Object类型, 一个是参数Date类型,可是我们发现,根本就没有这样的一个子类继承自父类的Object类型参数的方法。所以说,确实是重写了,而不是重载了。
    2. 为什么会这样呢?
    3. 原因: 我们传入父类的泛型类型是Date ,Pair 我们的本意是将泛型类变为如下
    class Pair {
    	private Date t;
    	public Date getValue() {
    		return t;
    	}
    	public void setValue(Date t) {
    		this.t= t;
    	}
    }
    1. 然后,我们重写参数类型为 Date的那两个方法,实现继承中的多态。
    2. 可是由于种种原因,虚拟机并不能将泛型类型变为 Date ,只能将类型擦除掉,变为原始类型Object 。这样,我们的本意识进行重写,实现多态。可是类型擦除后,只能变为重载。这样,类型擦除就和多态有了冲突。
    3. JVM 知道你的本意? 知道!!! 可是它能直接实现? 不能!!! 如果真的不能的话,那我们怎么去重写我们想要的Date 类型的方法呢?
    • JVM 采用了一种特殊的方法,来完成这项功能,那就是桥方法。
    • 首先我们 javap -c className 的方式反编译 DateClasss 子类的字节码如下
    public class generic.DataClass extends generic.Pair {
      public generic.DataClass();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method generic/Pair."":()V
           4: return
    
      public java.util.Date getT();
        Code:
           0: aload_0
           1: invokespecial #2                  // Method generic/Pair.getT:()Ljava/lang/Object;
           4: checkcast     #3                  // class java/util/Date
           7: areturn
    
      public void setT(java.util.Date);
        Code:
           0: aload_0
           1: aload_1
           2: invokespecial #4                  // Method generic/Pair.setT:(Ljava/lang/Object;)V
           5: return
    
      public static void main(java.lang.String[]);
        Code:
           0: new           #5                  // class generic/DataClass
           3: dup
           4: invokespecial #6                  // Method "":()V
           7: astore_1
           8: aload_1
           9: new           #3                  // class java/util/Date
          12: dup
          13: invokespecial #7                  // Method java/util/Date."":()V
          16: invokevirtual #8                  // Method setT:(Ljava/util/Date;)V
          19: return
    
      public void setT(java.lang.Object);
        Code:
           0: aload_0
           1: aload_1
           2: checkcast     #3                  // class java/util/Date
           5: invokevirtual #8                  // Method setT:(Ljava/util/Date;)V
           8: return
    
      public java.lang.Object getT();
        Code:
           0: aload_0
           1: invokevirtual #9                  // Method getT:()Ljava/util/Date;
           4: areturn
    }
    1. 从编译结果来看,我们本意是重写 setT 和 getT 方法的子类,竟然有4个方法,其实不用惊奇,最后两个方法,就是编译器自己生成的桥方法
    2. 可以看到 桥方法 的参数类型都是 Object ,也就是说,子类中真正覆盖父类两个方法就是这两个我们看不到的桥方法。
    3. 而打在我们自己定义的 setT 和 getT 方法上面的 @Overrride  只不过是假象。而桥方法的内部实现,就只是调用我们自己重写的那两个方法。
    4. 所以,Java 虚拟机巧妙地使用了桥方法,来解决了类型擦除和多态的冲突
    5. 不过要提到一点,这里面的 setT 和 getT 这两个桥方法的意义又有不同。
    • setT 方法是为了解决类型擦除与多态之间的冲突。
    • getT 方法是为了解决类型擦除与多态之间的冲突。

     

      //那么父类的setT 方法如下
      public Object getT() {
            return t;
        }
    
    
    //子类重写如下
        @Override
        public Date getT() {
            return super.getT();
        }
    1. 其实这在普通类继承中也是普遍存在重写,这就是协变。
    2. 关于协变......还有有点也许会有疑问,子类中的 桥方法 Object getT() 和 Date getT() 是同时存在的,可是如果是常规的两个方法,它们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。
    3. 如果使我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来 不合法的事情,然后交给虚拟器去区别。

     

    4),泛型类型变量不能是基本数据类型

    • 不能用类型参数替换基本类型,就比如 ,没有 ArrayList ,只有ArrayList
    • 因为当类型擦除后,ArrayList的原始类型变为Object ,但是Object类型不能存储double,只能引用Double的值。

     

    5),运行时类型查询

    List str=new ArrayList();
    1. 当类型擦除之后, ArrayList<>(); 只剩下原始类型,泛型String 就不存在了。
    2. 那么再进行类型查询的时候使用下面方法是错误的
    if (str instanceof ArrayList){}
    1. Java限定了这种类型查询的方式
     if (str instanceof ArrayList){}
    1. ? 是通配符的形式

     

    6),异常中使用泛型的问题

    • 不能排除也不能捕获类的对象。事实上,泛型类扩展 Throwable都不合法。
    • 如下定义将不会通过编译:
    class Problem extends Exception{}
    1. 为什么不能扩展 Throwable ,因为异常都是在运行时捕获和抛出的,而在编译的时候,泛型信息全都会被擦除掉,那么,假设上面的编译可行,那么,在看下面的定义:
    try{
    
    }catch(Problem e1){
        //...
    }catch(Problem e2){
        //...
    }
    1. 类型信息被擦除后,那么两个地方的catch都变味原始类型 Object,那么也就是说,这两个地方的 catch一模一样就相当于下面的这样
    try{
    
    }catch(Problem e1){
        //...
    }catch(Problem e2){
        //...
    } 
      
    1. 这个当然就是不行的。就好比,catch两个一模一样的普通异常,不能通过编译一样。
    try{
    
    }catch(Exception e1){
        //...
    }catch(Exception e2){  //编译失败
        //...
    }
    1. 不能在catch 子句中使用泛型变量
    public static  void doWork(Class t){
            try{
                ...
            }catch(T e){ //编译错误
                ...
            }
       }
    1. 因为泛型信息在编译的时候已经变为原始类型,也就是说上面的T 会变为原始类型 Throwable,那么如果可以在 catch子句中使用泛型变量,那么,下面的定义呢:
    public static  void doWork(Class t){
            try{
                ...
            }catch(T e){ //编译错误
                ...
            }catch(IndexOutOfBounds e){
            }                         
     }
    1. 根据异常捕获的原则,一定是子类在前面,父类在后面,那么上面违背了这个原则。
    2. 即使你在使用该静态方法的使用 T 是 ArrayIndexOutBounds ,在编译之后还是会变成 Throwable ,ArrayIndexOutBounds 是IndexOutOfBounds 的子类,违背了异常捕获的原则。所以Java为了避免这种的情况,禁止在 catch 子句中使用泛型变量。
    3. 但是在异常声明中可以使用类型变量。下面方法是合法的。
       public static void doWork(T t) throws T{
           try{
               ...
           }catch(Throwable realCause){
               t.initCause(realCause);
               throw t; 
           }
      }
    1. 这个是没有任何问题的。

     

    7),数组(这个不属于类型擦除引起的问题)

    • 不能声明参数化类型的数组。如:
    Pair [] pairs=new Pair[10];  //error
    1. 这是因为擦除后, pairs 的类型变为 Pair[] ,可以转化成一个Object[]
    Object[] objs=pairs;
    1. 数组可以记住自己的元素类型,下面的赋值会抛出一个 ArrayStoreException 异常信息。
    objs="hello";
    1. 对于泛型而言,擦除降低了这个机制的效率。下面的赋值可以通过数组存储的监测,但任然会导致类型错误。
    objs=new Pair[];
    1. 提示: 如果需要收集参数化类型对象,直接使用 ArrayList : ArrayList> 最安全且有效。

     

    8),泛型类型的实例化

    • 不能实例化泛型类型,如 index = new T(); 会报错误。类型擦除会使这个操作做成 new Object()。
    //不能建立一个泛型数组 
       public  T minMax(T t){
            T t=new T[2]; //error
            //....
        }
    1. 类似的,擦除会使这个方法总是靠一个 Object[2] 数组。但是,可以利用反射构造泛型对象和数组。利用反射,调用 Array.newInstance:
        public static  T[] minMax(T[] t){
            return (T[]) Array.newInstance(t.getClass().getComponentType(),2);
        }
    
    //替换掉如下代码
        Object[] objs=new Object[2];
        return (T[])objs;
    
    

     

    9),类型擦除后的冲突

    • 当泛型类型被擦除后,创建条件不能产生冲突。如果在 Pair类中添加下面的equals方法。
    public class Pair {
        
        public boolean equals(T t) {
            return null;
        }
    }
    1. 考虑一个Parir 。从概念上,它有两个 equals 方法:
    • boolean equals(String);   //从Pair 中定义
    • boolean equals(Object);  //从Object 中继承
    1. 但是,这只是一种错觉,实际上,擦除后方法 boolean equals(T) 变味了 boolean equals(Object) 这就是Object.equals方法是冲突的! 当然,补救的办法是重新命名引发错误的方法。
    2. 泛型规范说明提及另一个原则 要支持擦除的转换,需要强行制一个类或者类型变量不能同时成为两个接口的子类,而这两个子类是同一接口的不同参数化
    class Calendar implements Comparable{ ... }
    
    class GregorianCalendar extends Calendar implements Comparable{...} //ERROR
    
    GregorianCalendar 会实现 Comparable  和Comparable这是同一接口的不同参数实现。
    
    这一限制与类型擦除的关系并不很明确。非泛型版本
    class Calendar implements Comparable{ ... }
    
    class GregorianCalendar extends Calendar implements Comparable{...} //ERROR
    
    是合法的
    

     

    10),泛型在静态方法和静态类中的问题

    • 泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数
    public class Pair {
        
        private static T t; //编译失败
    
        public static T getT() {//编译失败
            return t;
        }
    
        public static void setT(T t) {//编译失败
            this.t = t;
        }
    }
    1. 因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。
    2. 对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。

    但是要区分下面这一种情况

    public class Pair {
        
        public static  T setT(T t) {//这个是正确的
            return null;
        }
    }
    1. 因为这是一个泛型方法,在泛型方法中使用的 T 是自己在方法中定义的 T ,而不是泛型类中的 T 。

    你可能感兴趣的:(java编程思想)