简体   繁体   中英

Java Collection with generic method and subclasses

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.

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