Java Generics对构造函数的困惑

时间:2018-06-16 13:59:57

标签: java generics

为什么以下代码示例无法编译?

试样-1

public class Generic5<T extends Comparable, Runnable> {
    T t;
    Comparable comparable;

    Generic5(T t) {
        this.t = t;
    }

    Generic5(Comparable comparable) {
        this.comparable = comparable;
    }
}

样品-2

public class Generic4<T extends Runnable, Comparable> {
    T t;
    Runnable runnable;

    Generic4(T t) {
        this.t = t;
    }

    Generic4(Runnable runnable) {
        this.runnable = runnable;
    }
}

以下代码示例编译成功?

试样-4

public class Generic4<T extends Runnable, Comparable> {
    T t;
    Comparable comparable;

    Generic4(T t) {
        this.t = t;
    }

    Generic4(Comparable comparable) {
        this.comparable = comparable;
    }
}

试样-5

public class Generic5<T extends Comparable, Runnable> {
    T t;
    Runnable runnable;

    Generic5(T t) {
        this.t = t;
    }

    Generic5(Runnable runnable) {
        this.runnable = runnable;
    }
}

样品-6

public class Generic6<T extends Number, Comparable, Runnable> {
    T t;
    Comparable comparable;

    Generic6(T t) {
        this.t = t;
    }

    Generic6(Comparable comparable) {
        this.comparable = comparable;
    }
}

样品-7

public class Generic6<T extends Number, Comparable, Runnable> {
    T t;
    Runnable runnable;

    Generic6(T t) {
        this.t = t;
    }

    Generic6(Runnable runnable) { 
        this.runnable = runnable; 
    }
}

我无法理解为什么在样本4,5,6和7的情况下擦除没有问题,而在样本1和2的情况下则没有。

我正在使用Java 8.我希望对每个样本单独解释。

1 个答案:

答案 0 :(得分:0)

阅读完评论后,这似乎只是对泛型的,符号的误解。

考虑以下两个陈述:

class A <T extends B, C> { ... }

class X <U extends Y & Z> { ... }

在第一种情况下,将指定2个类型参数:

  1. T,其中包含B
  2. C,其中包含java.lang.Object
  3. 因此,要使用A,必须指定类似的类型:

    A<B, Object> myA = new A<>();
    myA.myMethod();
    

    如您所见,有两种通用类型需要。

    对比是第二种情况,其中指定了1个类型参数:

    1. U,其中包含YZ
    2. 要使用它,必须指定类似的类型:

      class MyClass extends Y implements Z { ... }
      
      // ...
      
      X<MyClass> myX = new X<>();
      myX.myMethod();
      

      在这种情况下,只需要一种通用类型。该类型必须属于YZ类型,因此创建MyClass以满足此要求的原因。

      通过查看您的问题,我假设您打算指定第二种行为;但是,你不小心指定了第一个。

      因此,如果我们考虑Sample-1,应该清楚它失败的原因。通过删除泛型类型(类型擦除,编译器必须做的事情),样本1将有效地变为:

      public class Generic5 {
          Comparable t;
          Comparable comparable;
      
          Generic5(Comparable t) {
              this.t = t;
          }
      
          Generic5(Comparable comparable) {
              this.comparable = comparable;
          }
      }
      

      现在,如果我们调用一个类型为Comparable的构造函数,JVM应该执行哪个? Java编译器无法分辨。这就是为什么不能编译这个样本的原因。 Java编译器有一个如何指定它的问题。

      注意:在Sample-1中,Runnable类型不是 java.lang.Runnable。您不小心创建了具有相同名称的新泛型类型。这个Runnable实际上会成为java.lang.Object

      这实际上与指定类型的行为相同:

      public class Generic5<T extends Comparable, U> { ... }
      

      现在,如果我们考虑Sample-5并执行类型擦除,我们可以看到此示例的工作原理。 Sample-5将成为:

      public class Generic5 {
          Comparable t;
          Object runnable;
      
          Generic5(Comparable t) {
              this.t = t;
          }
      
          Generic5(Object runnable) {
              this.runnable = runnable;
          }
      }
      

      现在,编译器很乐意接受这段代码!

      与以前一样,Runnable是您创建的新类型,将替换为java.lang.Object不是 java.lang.Runnable

      因此,我们尝试修复Sample-1。立即解决方案是使用Multiple Type Parameter Bounds。这样就可以使类看起来像(注意第一行&的用法):

      public class Generic5<T extends Comparable & Runnable> {
          T t;
          Comparable comparable;
      
          Generic5(T t) {
              this.t = t;
          }
      
          Generic5(Comparable comparable) {
              this.comparable = comparable;
          }
      }
      

      现在,T必须是java.lang.Comparablejava.lang.Runnable类型。我们设法使T更加具体。我们还确保我们现在实际使用java.lang.Runnable

      然而,这仍然没有成功!类型擦除仍然与以前相同,除了现在增加了java.lang.Runnable的用法。

      那为什么会失败呢?嗯,它与类型的顺序有关。由于java.lang.Comparable首先出现,因此您通过说T的主要特征具有可比性来指导Java编译器。执行类型擦除时,它会将T替换为java.lang.Comparable。在我们的第一个解决方案中,我们重新排列顺序并创建主要特征java.lang.Runnable,编译器接受我们的帮助,一切都很好。在第二个中,我们用一个非常特定的新类型直接替换T,Java编译器将比第一个更喜欢这种变化。

      有两种方法可以解决这个问题:

      1。重新排列顺序:

      public class Generic5<T extends Runnable & Comparable> {
          T t;
          Comparable comparable;
      
          Generic5(T t) {
              this.t = t;
          }
      
          Generic5(Comparable comparable) {
              this.comparable = comparable;
          }
      }
      

      这里,类型擦除将有效地变为:

      public class Generic5 {
          Runnable t;
          Comparable comparable;
      
          Generic5(Runnable t) {
              this.t = t;
          }
      
          Generic5(Comparable comparable) {
              this.comparable = comparable;
          }
      }
      

      哪个会好的。

      注意:t仍然保证为java.lang.Comparable,因此java.lang.Comparable中的方法仍然可以在t上调用。

      2。指定新类型:

      可以放弃泛型以支持新类型:

      public class Generic5 {
          MyNewType t;
          Comparable comparable;
      
          Generic5(MyNewType t) {
              this.t = t;
          }
      
          Generic5(Comparable comparable) {
              this.comparable = comparable;
          }
      }
      
      class MyNewType implements Comparable, Runnable { ... }
      

      此处,泛型已完全删除。在我看来,这是更好的解决方案。它更接近于面向对象的编程,并消除了对类型的任何混淆。

      TL;博士

      如果要指定具有多个边界的泛型类型,则需要使用&运算符,而不是,运算符。

      此外,当涉及类型擦除时,边界的顺序很重要。将使用第一个界限。如果仍然存在冲突,请重新排列顺序,或者创建一个扩展边界的新类,并使用该类代替泛型。