How do you go about using an instance of a (possibly further) inherited generic type within a method declaration in the generic type itself being declared? This is one of the problems you will face when you dig a bit deeper into generics, but still a fairly common one.
Assume we have a DataSeries<T>
type (interface here), which defines a generic data series containing values of type T
. It is cumbersome to work with this type directly when we want to perform a lot of operations with e.g. double values, so we define DoubleSeries extends DataSeries<Double>
. Now assume, the original DataSeries<T>
type has a method add(values)
which adds another series of the same length and returns a new one. How do we enforce the type of values
and the type of the return to be DoubleSeries
rather than DataSeries<Double>
in our derived class?
The problem can be solved by adding a generic type parameter referring back to and extending the type being declared (applied to an interface here, but the same stands for classes):
public interface DataSeries<T, DS extends DataSeries<T, DS>> {
DS add(DS values);
List<T> data();
}
Here T
represents the data type the series holds, e.g. Double
and DS
the series itself. An inherited type (or types) can now be easily implemented by substituting the above mentioned parameter by a corresponding derived type, thus, yielding a concrete Double
-based definition of the form:
public interface DoubleSeries extends DataSeries<Double, DoubleSeries> {
static DoubleSeries instance(Collection<Double> data) {
return new DoubleSeriesImpl(data);
}
}
At this moment even an IDE will implement the above interface with correct types in place, which, after a bit of content filling may look like this:
class DoubleSeriesImpl implements DoubleSeries {
private final List<Double> data;
DoubleSeriesImpl(Collection<Double> data) {
this.data = new ArrayList<>(data);
}
@Override
public DoubleSeries add(DoubleSeries values) {
List<Double> incoming = values != null ? values.data() : null;
if (incoming == null || incoming.size() != data.size()) {
throw new IllegalArgumentException("bad series");
}
List<Double> newdata = new ArrayList<>(data.size());
for (int i = 0; i < data.size(); i++) {
newdata.add(this.data.get(i) + incoming.get(i)); // beware autoboxing
}
return DoubleSeries.instance(newdata);
}
@Override
public List<Double> data() {
return Collections.unmodifiableList(data);
}
}
As you can see the add
method is declared as DoubleSeries add(DoubleSeries values)
and the compiler is happy.
The pattern can be further nested if required.