简体   繁体   中英

Java generics cast and usage

I have kind of problem with generics inheritance. Below is the dependency tree:

public class Attributes {
}

public abstract class Feature<T extends Attributes> {
    
    private T attributes;

    public T getAttributes() {
        return attributes;
    }

    public void setAttributes(T attributes) {
        this.attributes = attributes;
    }
}

public abstract class AbstractFeatureRepository<T extends Feature<? extends Attributes>> {
    public abstract String getType();

    public abstract boolean create(T feature);
}

And I have implementations of these feature repositories, like this:

public class NodeAttributes extends Attributes {
    
    private String startPoint;

    public String getStartPoint() {
        return startPoint;
    }

    public void setStartPoint(String startPoint) {
        this.startPoint = startPoint;
    }
}

public class Node extends Feature<NodeAttributes> {
}

public class NodeRepository extends AbstractFeatureRepository<Node> {
    public String getType() {
        return "Node";
    }
    public boolean create(Node node) {
        return true;
    }
}

public class LinkAttributes extends Attributes {
    
    private String uri;

    public String getUri() {
        return uri;
    }

    public void setUri(String uri) {
        this.uri = uri;
    }

}

public class Link extends Feature<LinkAttributes> {
}

public class LinkRepository extends AbstractFeatureRepository<Link> {
    public String getType() {
        return "Link";
    }
    public boolean create(Link link) {
        return true;
    }
}

I'm injecting these repositories with Spring to Controller via constructor (but in this example I'm manually creating in constructor for the sake of simplicity):

public class Controller {
    
    private final Map<String, AbstractFeatureRepository<? extends Feature>> featureRepositories;
    
    public Controller() {
        this.featureRepositories = Arrays.asList(new NodeRepository(), new LinkRepository()).stream()
                .collect(Collectors.toMap(AbstractFeatureRepository::getType, Function.identity()));
    }
    
    public Node createNode() {
        Node newNode = new Node();
        newNode.getAttributes().setStartPoint("Berlin");
        createFeature("Node", newNode);
        return newNode;
    }

    public Link createLink() {
        Link newLink = new Link();
        newLink.getAttributes().setUri("/home/local");
        createFeature("Link", newLink);
        return newLink;
    }
    
    
    private void createFeature(String type, Feature<? extends Attributes> feature) {
        featureRepositories.get(type).create(feature);
    }

}

All is good untill I want to call "create" method in generic "createFeature" where I'm getting compilation error that

The method create(capture#5-of? extends Feature) in the type AbstractFeatureRepository<capture#5-of? extends Feature> is not applicable for the arguments (Feature<capture#6-of? extends Attributes>)

What I'm doing wrong? Is this because potentially I can pass some other kind of "Feature" than particular "Repository" can work with? How then should I change my map Repositories in Controller so compiler doesn't complain? I though I should operate exact classes as a key for map but have no idea how to make all of this working. Any suggestions?

Thank you.

Update 1 . I changed Map to

private final Map<Class<?>, AbstractFeatureRepository<? extends Feature>> featureRepositories;

Changed AbstractFeatureRepository so it now looks this way:

public abstract class AbstractFeatureRepository<T extends Feature> {
    
    public abstract Class<T> getClazz();
    
    public abstract boolean create(T feature);
}

And changed the methods in controller:

    public Link createLink() {
        Link newLink = new Link();
        createFeature(Link.class, newLink);
        return newLink;
    }
    
    
    private <T extends Feature> void createFeature(Class<T> class1, T feature) {
        AbstractFeatureRepository<? extends Feature> abstractFeatureRepository = featureRepositories.get(feature.getClass());
        abstractFeatureRepository.create(abstractFeatureRepository.getClazz().cast(feature));
    }

It still doesn't allow me to do that.

This code:

featureRepositories.get(type)

returns an object whose type is the V in Map<K, V> , as per the docs of java.util.Map . In your code, that means that expression is of type AbstractFeatureRepository<? extends Feature<? extends Attributes>> AbstractFeatureRepository<? extends Feature<? extends Attributes>> AbstractFeatureRepository<? extends Feature<? extends Attributes>> .

Let's simplify matters a tad, and assume we have List<? extends Number> List<? extends Number> instead.

This is valid java code:

List<? extends Number> list = new ArrayList<Integer>();

that's sort of the point of ? extends ? extends , really. This does not compile:

List<Number> list = new ArrayList<Integer>();

Now, imagine you called, on your List<? extends Number> List<? extends Number> :

List<? extends Number> list = new ArrayList<Integer>();
Number n = Double.valueOf(5.0);
list.add(n);

uhoh. There is a non-integer in my list of integers. Whoops.

That's why you can't call add() here, at all. You cannot call add on a List<? extends whatever> List<? extends whatever> , at all . Any method that takes as argument a T where your type is Foo<? extends T> Foo<? extends T> cannot be invoked*.

Let's go back to your code now:

You have a AbstractFeatureRepository<? extends Feature<? extends Attributes>> AbstractFeatureRepository<? extends Feature<? extends Attributes>> AbstractFeatureRepository<? extends Feature<? extends Attributes>> - therefore any method that AbstractFeatureRepository has that takes a T cannot be invoked from this. at all. And create is such a method.

The solution is a bit tricky. You can use a type-safe container, if you somehow have a reference to a class representing T (careful; things can be a T that cannot be a Class: List<Integer> is a T, but only List.class exists, you can't write List<Integer>.class : - You can use that:

public <T extends Attribute> void createFeature(Class<T> typeDesired, Feature<T> feature) {
    featureRepositories.get(type).create(typeDesired.cast(feature));
}

is one way.

In general your method signature is problematic: There is just no guarantee that your stringly-typed String type implies that the kind of feature desired Feature<? extends Attribute> Feature<? extends Attribute> is handled by the repository matching that type.

A second option is to have each AbstractFeatureRepository responsible to deal with type mismatches. In that case, you can update the interface to read create(Object feature) throws TypeMismatchException instead. Or, have it return a type ( public Class<T> getType() ) and you can go back to the cast construct.

*) Well, you can invoke it, if you pass literally null , as null is all data types. But that clearly isn't what you intend to do here, thus, not relevant.

If you've only got 2 things in a Map (or N things, where N is a small number, you mention 4 in a comment), and you use fixed keys to indicate a particular type, using a Map is making it harder than necessary.

Maps are necessarily homogeneously typed, that is, all the keys have a common supertype, and all the values have a common supertype. The issue that you're facing is that you want a key's type to relate to the value's type: this can be done with a type-safe heterogeneous container (essentially: a map which you construct such that you can impose the constraints on the relationships between the types). But even this is overly-complex for the problem described.

Use two (N) fields instead:

public class Controller {
    private final NodeRepository nodeRepository = new NodeRepository();
    private final LinkRepository linkRepository = new LinkRepository();

This is still sort-of a map: the key is the field, the value is the field value.

But the advantage of this is that you've retained the concrete types of the repositories, so you can pass these to the method:

private <A extends Attributes> void createFeature(AbstractFeatureRepository<A> repo, Feature<A> feature) {
    repo.create(feature);
}

eg

public Node createNode() {
    Node newNode = new Node();
    newNode.getAttributes().setStartPoint("Berlin");
    // Or, easier, nodeRepository.create(newNode);
    createFeature(nodeRepository, newNode);
    return newNode;
}

public Link createLink() {
    Link newLink = new Link();
    newLink.getAttributes().setUri("/home/local");
    // Or, easier, linkRepository.create(newNode);
    createFeature(linkRepository, newLink);
    return newLink;
}

To arrive at a working solution that is as close to your original code as I could get it, I made three or four relatively minor refactors.

The most significant change though was in Controller.createFeature()

private <T extends Feature<?>> void createFeature(Class<T> class1, T feature) {
    AbstractFeatureRepository<T> abstractFeatureRepository = (AbstractFeatureRepository<T>)featureRepositories.get(feature.getClass());
    abstractFeatureRepository.create(feature);
}

The cast there is the simplest, most type safe solution in my opinion. The reason I'm convinced the cast is type safe is because of the compilation error you'd get if the cast weren't there:

Controller.java:31: error: incompatible types: AbstractFeatureRepository<CAP#1> cannot be converted to AbstractFeatureRepository<T>
    AbstractFeatureRepository<T> abstractFeatureRepository = featureRepositories.get(feature.getClass());

where T is a type-variable:
    T extends Feature<?> declared in method <T>createFeature(Class<T>,T)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Feature<?> from capture of ? extends Feature<?>
1 error

If you read the bottom part of the error message carefully, you'll see that the only difference between T extends Feature<?> and CAP#1 extends Feature<?> is the names of the two type variables. They both have the same upper bounds ( extends Feature<?> ). That tells me it's reasonable to infer that a cast would be type safe.

So, I annotated that method with SuppressWarnings("unchecked") .

To confirm that the solution is usable, I fleshed out Node and Link classes with toString() . Calling Controller.createNode() and Controller.createLink() in the solution demo gets you…

Node: [NodeAttributes - Start Point: 'Berlin']

Link: [LinkAttributes - URI: 'urn::foo::bar']

I have to admit that what problem you're trying to solve isn't crystal clear to me. So I've made some assumptions based on only my general Java knowledge. Please let me know if the solution meets your requirements?

Here is the approach I used:

public class Controller {

    private final Map<Class<?>, AbstractFeatureRepository<? extends Feature>> featureRepositories;

    public Controller3(List<AbstractFeatureRepository<? extends Feature>> featureRepositories) {
        this.featureRepositories = featureRepositories.stream()
                .collect(Collectors.toMap(AbstractFeatureRepository::getClazz, Function.identity()));
    }

    public Node createNode() {
        Node newNode = new Node();
        createFeature(Node.class, newNode);
        return newNode;
    }

    public Link createLink() {
        Link newLink = new Link();
        createFeature(Link.class, newLink);
        return newLink;
    }

    private <T extends Feature> void createFeature(Class<T> clazz, T feature) {
        AbstractFeatureRepository<T> repository = getRepository(clazz);
        repository.create(feature);
    }
    
    @SuppressWarnings("unchecked")
    private <T extends Feature, V extends AbstractFeatureRepository<T>> V getRepository(Class<T> clazz) {
        return (V) featureRepositories.get(clazz);
    }

    public static void main(String[] args) {
        Controller3 controller = new Controller3();
        controller.createLink();
    }
}

It doesn't satisfy completely no-cast approach(especially no @SuppressWarnings) but it is the least evil for me, since cast is done only in one method in controller, all the rest methods work no cast and no @SuppressWarnings.

Try

private static <T extends AbstractFeatureRepository> void createFeature(Class<T> clazz, Feature<? extends Attributes> feature) {
    ((T) featureRepositories.get(clazz)).create(feature);
}

You should modify the featureRepositories accordingly

private static final Map<Class<?>, AbstractFeatureRepository<? extends Feature>> featureRepositories

But I don't recommend using generics like this.

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