Java generic:如何将泛型参数声明为另一个泛型参数的基类型?

时间:2014-06-21 05:11:48

标签: java generics types type-inference

public class PairJ<T> {
    private T first;
    private T second;

    public PairJ(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T first() {
        return this.first;
    }

    public T second() {
        return this.second;
    }

    public <R> Pair<R> replaceFirst(R newFirst) {
        return new Pair(newFirst, second());
    }
}

class Vehicle {}

class Car extends Vehicle {}

class Tank extends Vehicle {}

代码编译。 但是当我这样做时:

    PairJ<Car> twoCars = new PairJ(new Car(), new Car());
    Tank actuallyACar = twoCars.replaceFirst(new Tank()).second();

它仍然可以编译但在跑步时会出现施法异常,因为该对中的第二个元素是汽车,而不是坦克。

所以我把它改成了这个:

public <R, T extends R> Pair<R> replaceFirst(R newFirst) {
    return new Pair(newFirst, second());
}

但是这段代码仍然会编译并给出相同的异常:

    PairJ<Car> twoCars = new PairJ(new Car(), new Car());
    Tank actuallyACar = twoCars.replaceFirst(new Tank()).second();

似乎T扩展R的声明在这里不起作用。

如何在此强制执行类型安全?

更具体地说,我如何确保java推断

twoCars.replaceFirst(new Tank())

返回一对车而不是一对坦克?因此,当尝试将其第二个元素分配给Tank类型的变量时,我会得到编译时错误?并减少运行时异常的可能性?

编辑:

在Scala中,我们可以这样做:

class Pair[T](val first: T, val second: T) {
  def replaceFirst[R >: T](newFirst: R): Pair[R] = new Pair[R](newFirst, second)
}

[R&gt;:T]确保R是T的基类型,我们如何在Java中做同样的事情?

2 个答案:

答案 0 :(得分:1)

  

如何确保java推断

twoCars.replaceFirst(new Tank())
     

返回一对车而不是一对坦克?

明确提供类型参数

twoCars.<Vehicle>replaceFirst(new Tank())

尝试调用

时,您将收到编译器错误
Tank actuallyACar = twoCars.<Vehicle>replaceFirst(new Tank()).second();

答案 1 :(得分:0)

首先,请注意这种形式:

PairJ<Car> twoCars = new PairJ(new Car(), new Car());

您收到编译器警告,其中您正在执行从PairJPairJ<Car>的未经检查的分配。你可能会在以后遇到问题。

您可以通过提供泛型类型或让Java(7.0+)使用菱形运算符为您推断它来避免这种情况。

PairJ<Car> twoCars = new PairJ<Car>(new Car(), new Car());
// or
PairJ<Car> twoCars = new PairJ<>(new Car(), new Car());

第一个问题在这里:

public <R> PairJ<R> replaceFirst(R newFirst) {
    return new PairJ(newFirst, second());
}

你在这里得到另一个未选中的作业,但是这个更糟 - 你正在积极地将你的一个参数的类型改为完全不同。< / p>

这是不可能的原因是由于您的泛型类型绑定到您的类的方式。您明确声明,在构造函数中,您期望两个参数绑定到某个泛型类型T。您正在做什么此处仅将一个绑定到R

如果您通过如上所述进行类型推断来“解决”问题,则会出现编译失败(这很好 - 您希望这些在编译时失败而不是运行时)。

public <R> PairJ<R> replaceFirst(R newFirst) {
    return new PairJ<R>(newFirst, second());
}

由于要求参数绑定到R, R而不是R, T,上述操作失败。

第二个问题在这里:

class Vehicle {}

class Car extends Vehicle {}

class Tank extends Vehicle {}

您的层次结构要求,CarVehicle,而TankVehicle a Car不是Tank所以ClassCastException是合适的;你真的无法Car投放到Tank

这会失败,因为它们不能在两者之间转换。

Tank actuallyACar = twoCars.replaceFirst(new Tank()).second();

这里有一个解决方案;如果你想保留这个API,那么你的类需要两个类型参数。这意味着您现在可以在调用R时自由传递新类型replaceFirst

class PairJ<S, T> {
    private S first;
    private T second;

    // accessors modified

    public <R> PairJ<R, T> replaceFirst(R newFirst) {
        return new PairJ<>(newFirst, second());
    }
}

现在有趣的部分是调用方法。我们可以像以前一样构造它,现在有两个Car类型参数。

PairJ<Car, Car> twoCars = new PairJ<>(new Car(), new Car());

正如我之前所说,Tank不是Car,所以这不会起作用:

Tank actuallyACar = twoCars.replaceFirst(new Tank()).second();

...但

Car actuallyACar = twoCars.replaceFirst(new Tank()).second();

以上更多归结为您的类层次结构的布局方式。即使您在课堂上使用了泛型,从Tank转换为Car,它也不会在Java中工作。虽然他们有共同的祖先,但其中一个不是另一个。

(现代例子:你不能将String投射到Integer,即使他们都是Object的孩子。)