简体   繁体   中英

Java generic: how to declare a generic parameter as the base type of another generic parameter?

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 {}

The code compiles. But when I do this:

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

It still compiles but gives a cast exception when running, since the second element in the pair is a car, not a tank.

So I changed it to this:

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

But this code still compiles and gives the same exception:

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

Seems like the declaration of T extends R is not working here.

How can I enforce type safety here?

More specifically, how can I make sure that java infers

twoCars.replaceFirst(new Tank())

to return a Pair of Vehicle instead of a Pair of tanks? So I would get compile time error when trying to assign its second element to a variable of the type Tank? And to reduce the chances of having run time exceptions?

Edit:

In Scala, we can do it like this:

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

[R >: T] makes sure that R is a base type of T, how do we do the same in Java?

how can I make sure that java infers

 twoCars.replaceFirst(new Tank()) 

to return a Pair of Vehicle instead of a Pair of tanks?

Provide a type argument explicitly

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

You'll get a compiler error when trying to invoke

Tank actuallyACar = twoCars.<Vehicle>replaceFirst(new Tank()).second();

First, pay attention to this form:

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

You're getting a compiler warning in which you're doing an unchecked assignment from PairJ to PairJ<Car> . You may run into gotchas with this later.

You can avoid this by providing the generic type, or letting Java (7.0+) infer it for you with the diamond operator.

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

The first problem is here:

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

You're getting another unchecked assignment here, but this one is worse - you're actively changing the type of one of your parameters to something completely different.

The reason that this is not possible is due to the way your generic type is bound to your class. You explicitly state that, in your constructor, you expect two arguments bound to some generic type T . What you're doing here is binding only one to R .

If you were to "fix" the issue by doing type inference as described above, you'd get a compilation failure (which is good - you want these to fail at compile time as opposed to run time).

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

The above fails due to it requiring parameters bound to R, R , not R, T .

The second problem is here:

class Vehicle {}

class Car extends Vehicle {}

class Tank extends Vehicle {}

Your hierarchy mandates that, while a Car is a Vehicle , and a Tank is a Vehicle , a Car is not a Tank . So the ClassCastException is appropriate; you really can't cast a Car to a Tank .

This would fail since they're not convertible between the two.

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

There's a solution here; if you want to keep this API, then you'll need two type parameters for your class. This means that you can now pass in a new type R freely when calling 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());
    }
}

The fun part now comes in calling the method. We can construct it pretty much the same way we did before, now with two Car type parameters.

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

As I said earlier, a Tank isn't a Car , so this just ain't gonna work:

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

...but this will :

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

The above more boiled down to the way you had your class hierarchy laid out. It would have never worked in Java, even if you got the generics on the class right, to cast from a Tank to a Car . While they share a common ancestor, one is not the other.

(Modern example: you can't cast a String to an Integer , even though they're both children of Object .)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM