1

I have an abstract class:

public abstract class AbstractPoint<PointType extends AbstractPoint<PointType>> {
    public abstract double getX();
    public abstract double getY();
    public abstract double rho();

    // Find the distance between two points
    public final <T extends AbstractPoint<T>> double distance(T other) {
        return vectorTo(other).rho();
    }

    // Find vector from one point to another.
    public abstract <T extends AbstractPoint<T>> PointType vectorTo(T other);

    // other methods ...
}

Classes OrthogonalPoint and PolarPoint extend the abstract class:

public class OrthogonalPoint extends AbstractPoint<OrthogonalPoint> {
    private double x;
    private double y;

    public OrthogonalPoint(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() { return this.x; }

    public double getY() { return this.y; }

    public double rho() { return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); }

    public double theta() { return Math.atan2(this.y, this.x); }

    public <T extends AbstractPoint<T>> OrthogonalPoint vectorTo(T other) {
        return new OrthogonalPoint(other.getX() - this.x, other.getY() - this.y);
    }
}
public class PolarPoint extends AbstractPoint<PolarPoint> {
    private double rho;
    private double theta;

    public PolarPoint(double x, double y) {
        this.rho = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
        this.theta = Math.atan2(y, x);
    }

    public double getX() { return this.rho() * Math.cos(this.theta()); }

    public double getY() { return this.rho() * Math.sin(this.theta()); }

    public double rho() { return this.rho; }

    public double theta() { return this.theta; }

    public <T extends AbstractPoint<T>> PolarPoint vectorTo(T other) {
        return new PolarPoint(other.getX() - this.getX(), other.getY() - this.getY());
    }
}

With this code, I can correctly calculate the distance between points no matter whether they are of same type (orthogonal or polar).
Now I add another class called Route:

public class Route {

    private final List<? extends AbstractPoint<?>> points = new ArrayList<>();

    // 'point' in 'this.points.add(point)' underlined red
    public <T extends AbstractPoint<T>> void appendPoint(T point) { this.points.add(point); }

    public <T extends AbstractPoint<T>> void removePoint(T point) { this.points.remove(point);  }

    // 'point' in '.distance(point)' underlined red
    public double routeDistance() {
        return this.points.stream().skip(1).mapToDouble(point ->
                this.points.get(this.points.indexOf(point) - 1).distance(point)).sum();
    }
}

A route is an ordered number of points whose length is defined as the sum of the distances of the points in the sequence. So, if a route consists of three points (p1, p2, and p3) the distance of the route is p1.distance(p2) + p2.distance(p3).
However, some places (look at the comments in Route) in my code are underlined red:

// in appendPoint function
Required type: capture of ? extends AbstractPoint<?>
     Provided: T

// in routeDistance function
Required type: T
     Provided: capture of ? extends AbstractPoint<?>

I want to be able to add any type of point to the points list and the call routeDistance function to calculate the length of the route. What is wrong with my solution - how should I change my generics (in any class)?

syydi
  • 119
  • 2
  • 13
  • Does this answer your question? [What is PECS (Producer Extends Consumer Super)?](https://stackoverflow.com/questions/2723397/what-is-pecs-producer-extends-consumer-super) – luk2302 Oct 03 '21 at 10:02
  • @luk2302 I do not think so. If the collection came in as a parameter to a function, then yes, but my collection is a class variable. As I understand from your link, my collection is both a producer and a consumer. Did I understand it correctly? – syydi Oct 03 '21 at 10:23
  • No, the way you got the data does not matter, variable, field, parameter, … - it has extends as part of the generic and therefore is a producer, can only be a producer, you cannot add stuff to it. – luk2302 Oct 03 '21 at 10:50
  • If you want it to be both a consumer and a producer the generic type parameter of the collection must not use extends or super. – luk2302 Oct 03 '21 at 11:03
  • So how can I create a Route, that has a class variable points that is a collection, and I can add both Orthogonal and Polar points to this collection and also iterate over it and do things with the items? If the collection would be just List, I would have to make Route generic and I can't add different types of points to the collection. – syydi Oct 03 '21 at 11:09

1 Answers1

3

First, note that you are both adding data to the points list, and taking data out of the list, so you should not use extends or super. (See also: PECS)

private final List<AbstractPoint<?>> points = new ArrayList<>();

Next, you will see that there is an error at the .distance(point) call. This is because there is no type that can be used as the type parameter T of the distance method, that also satisfies the constraint T extends AbstractPoint<T>. Note that the type of point is AbstractPoint<?>, and AbstractPoint<?> can't be T, because AbstractPoint<?> does not extend AbstractPoint<AbstractPoint<?>>. See the problem?

From what I can see, the way you have written distance does not allow you to find the distance between two AbstractPoints. One of the points must be a concrete type.

In fact, distance and vectorTo doesn't actually need the generic parameter. It can just take a AbstractPoint<?>:

public final double distance(AbstractPoint<?> other) {
    return vectorTo(other).rho();
}

public abstract PointType vectorTo(AbstractPoint<?> other);

This now allows you to pass in AbstractPoint<?> to distance.

Side note: your current implementation of routeDistance using indexOf is not very efficient, as it goes over the list once for every point in the list. You can do this with a zip operation instead. Using the zip method in the linked answer:

return zip(points.stream(), points.stream().skip(1), AbstractPoint::distance)
    .mapToDouble(x -> x).sum();
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • Thank you, I learned a lot from this. This was exactly what I wanted to achieve. However, I will keep my current implementation of routeDistance as performance is not that important for this project and I do not wish to add such a big and complicated method to my code. – syydi Oct 03 '21 at 11:45