I've the following collection class that contains a method for grouping the elements in a map where each value has the type of the class invoking it
class TaskCollection<E extends Task> extends HashSet<E> {
<K> Map<K, ? extends TaskCollection<E>> groupBy(Function<E, K> groupingFunction) {
return this.stream()
.collect(Collectors.groupingBy(
groupingFunction,
Collectors.toCollection(this.collectionConstructor())
));
}
Supplier<? extends TaskCollection<E>> collectionConstructor() {
return TaskCollection::new;
}
}
What I want is to be able to create subclasses that use the groupBy
method that returns new istances of themselves as map values.
The following is an example
class AssertionCollection extends TaskCollection<Assertion> {
Map<Person, AssertionCollection> groupByPerson() {
return this.groupBy(Assertion::assignedPerson);
}
@Override
Supplier<AssertionCollection> collectionConstructor() {
return AssertionCollection::new;
}
}
The problem is in the groupByPerson
method. The compiler throws an error for the groupBy
call.
Error:(15, 28) java: incompatible types: no instance(s) of type variable(s) K exist so that java.util.Map<K,? extends TaskCollection<Assertion>> conforms to java.util.Map<Person,AssertionCollection>
I'm new in Java so I'm pretty sure there's something stupid I don't see
The intent is that for any class X
that extends TaskCollection
, when a groupBy operation is performed, the collection used for the map values are also instances of class X
.
In that case, the closest you can get to that is something like the following:
class Task {}
class Assertion extends Task {}
abstract class TaskCollection<E extends Task, C extends TaskCollection<E, C>> extends HashSet<E> {
<K> Map<K, C> groupBy(Function<E, K> groupingFunction) {
return this.stream()
.collect(Collectors.groupingBy(
groupingFunction,
Collectors.toCollection(this.collectionSupplier())
));
}
protected abstract Supplier<C> collectionSupplier();
}
class AssertionCollection extends TaskCollection<Assertion, AssertionCollection> {
@Override
protected Supplier<AssertionCollection> collectionSupplier() {
return AssertionCollection::new;
}
}
Notice that the definition of TaskCollection
above does not quite stop subclasses of using another TaskCollection
class for their groupBy map values. For example this would also compile:
class AssertionCollectionOther extends TaskCollection<Assertion, AssertionCollectionOther> {...}
class AssertionCollection extends TaskCollection<Assertion, AssertionCollectionOther> {...}
Unfortunately it is not possible to impose such a constraint, at least for now, as you cannot make reference to the class that is being declared in the C type-parameter wildcard.
If you can assume that descendants have a parameter free constructor as the collection supplier you can provide a default implementation for
collectionSupplier
. The price you pay is the need to silence a "unchecked" warning (not a real problem) and that not compliant classes (not providing the parameter-free constructor) won't fail at compilation time but at run-time which is less ideal:
import java.util.function.*;
import java.util.*;
import java.util.stream.*;
class Task {}
class Assertion extends Task {}
class TaskCollection<E extends Task, C extends TaskCollection<E, C>> extends HashSet<E> {
<K> Map<K, C> groupBy(Function<E, K> groupingFunction) {
return this.stream()
.collect(Collectors.groupingBy(
groupingFunction,
Collectors.toCollection(this.collectionSupplier())
));
}
@SuppressWarnings("unchecked")
protected Supplier<C> collectionSupplier() {
return () -> {
try {
return (C) this.getClass().newInstance();
} catch (Exception ex) {
throw new RuntimeException(String.format("class %s is not a proper TaskCollection", this.getClass()), ex);
}
};
}
}
class AssertionCollection extends TaskCollection<Assertion, AssertionCollection> {
// This override is not needed any longer although still could
// be included in order to produce a slightly faster
// customized implementation:
//@Override
//protected Supplier<AssertionCollection> collectionSupplier() {
// return AssertionCollection::new;
//}
}
If you declare collectionSupplier
as final
you would effectively force subclasses to always return instances of their own class with the caveat that a, then non-sense, declaration such as class AssertionCollection extends TaskCollection<Assertion, AssertionCollectionOther>
would still compile and produce run-time cast exceptions down the road.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With