views:

93

answers:

6

Hi,

I am trying to use a combination of wildcards in the type of the receiver and in the type of an argument to a method in Java. The context is that of defining a container. Now, the type Container should not admit insertions whatsoever, since this type does not specify the type of the contained objects. However, if the underlying data structure allows it, there should be a way for searching an object of type T, or of any other type that extends T.

Here is a code snippet that demonstrates the problem. Any ideas on how I can achieve this design goal in Java?

public class Main {
public static class Container<T extends Item> {
    public void insert(T t) {
        System.out.println("Inserting " + t);
    }

    public <R extends T> int find(R r) {
        return r.hashCode();
    }
}

public static class Item {
    // Nothing here
}

public static class ExtendedItem extends Item {
    // And nothing here...
}

public static class Client {
    public static void main(String[] args) {
        useContainerOfItem();
        useContainerWildCardOfItem(new Container<Item>());
        Container<? extends Item> c;
        c = new Container<Item>();         // OK. type of c, is a super type of Container<Item>
        c = new Container<ExtendedItem>(); // OK. type of c, is a super type of Container<ExtendedItem>
        useContainerWildCardOfItem(c);
    }

    private static void useContainerOfItem() {
        Container<Item> c = new Container<Item>();
        c.insert(new Item()); // OK. We can insert items
        c.insert(new ExtendedItem()); // OK. We can insert items
        c.find(new Item()); // OK. We can find items in here.
        c.find(new ExtendedItem()); // OK. We can also find derived items.
    }

    private static void useContainerWildCardOfItem(Container<? extends Item> c) {
        c.insert(new Item()); // Error: expected. We should not be able to insert items.
        c.insert(new ExtendedItem()); // Error: expected. We should not be able to insert anything!
        c.find(new Item()); // Error. Why??? We should be able to find items in here.
        c.find(new ExtendedItem()); // Error. Why??? We should be able to find items in here.
    }
}

}

+1  A: 

The error message is telling you precisely what is the issue. Your find method is using a generic R extends T, and the T in this case is ?, so the compiler has no way of checking your supplied R (an Item) to check if it extends "capture#6-of ?".

A: 

The key point is that the type of Container<? extends Item> means a container of something that extends Item, not anything that extends Item. Therefore it is possible that an object of type Item (the one being passed into the find method) may not be a compatible subclass of the something that extends Item. The compiler can not verify whether your code is correct, so it throws an error. The best you can do is allow a much broader parameter to find and restrict the return type:

public class Container<T> {

    public void insert(T item) {
        // insert...
    }

    public T find(Object o) {
        // Lookup using a map or something return null if not found
    }
}
Michael Barker
A: 

You must understand this, JavaRanch FAQs: Generics Super And Extends.

Adeel Ansari
A: 

I could understand the error message, but still, this does not answer my question, which was "Any ideas on how I can achieve this design goal in Java?" My current best solution is to use not one, but two generic parameters for the Collection class. The first designating what you can search for, and the other what you can insert. If this is cumbersome, you use a super class to capture the fact that you do not care about the type that "find" expects.

Still, I am not happy with this, and I hope a simpler solution would exist.

public class Main {

public static class Searchable<R extends Item> {
    public int find(R r) {
        return r.hashCode();
    }
}
public static class Container<T extends Item> extends Searchable<Item>{
    public void insert(T t) {
        System.out.println("Inserting " + t);
    }
}

public static class Item {
    // Nothing here
}

public static class ExtendedItem extends Item {
    // And nothing here...
}

public static class Client {
    public static void main(String[] args) {
        useContainerOfItem();
        useContainerWildCardOfItem(new Container<Item>());
        Container<? extends Item> c;
        c = new Container<Item>();         // OK. the type of c is a super type of Container<Item>
        c = new Container<ExtendedItem>(); // OK. the type of c is a super type of Container<ExtendedItem>
        useContainerWildCardOfItem(c);
    }

    private static void useContainerOfItem() {
        Container<Item> c = new Container<Item>();
        c.insert(new Item()); // OK. We can insert items
        c.insert(new ExtendedItem()); // OK. We can insert items
        c.find(new Item()); // OK. We can find items in here.
        c.find(new ExtendedItem()); // OK. We can also find derived items.
    }

    private static void useContainerWildCardOfItem(Container<? extends Item> c) {
        c.insert(new Item()); // Error: expected. We should not be able to insert an item!
        c.insert(new ExtendedItem()); // Error: expected. We should not be able to insert anything!
        c.find(new Item()); // No error, we should be able to find an Item
        c.find(new ExtendedItem()); // No error, we should be able to find an ExtendedItem
}

}

Someone
What you've written is not equivalent - because you've provided an **explicit** bound of `<Item>` in your extends clause for `Container`, which is equivalent to writing your original `find` method with an explicit parameter type of `Item` too (which also passes your tests). Still - you should *probably* be allowing any object reference for `find` (don't make people downcast); have a look at the signature of `java.util.Collection.contains()` for example. It takes an `Object` instead of an `E` for good reason.
Andrzej Doyle
A: 

I think the goal is misdirected here. You concede that your Container#insert() method can't work on a reference to wildcard-bound Container, but you expect that your Container#find() method can work. It's the same problem in both cases; you're trying to use covariance in both cases, which can't be enforced in Java against a wildcard like this.

Your original signature for Container#find() was fine. It met your specification. The latter one involving Searchable#find() is too relaxed; it allows one to search on any type of Item, rather than just types equivalent to or derived from the lower type T of the container. If the specification says that one should only be able to search for entries that are possibly in the container, and we don't know the specific type of the entries in the container, than we can't enforce that contract from a call site like Container#find().

Instead, try avoiding the wildcards like this:

private static <C extends Item, U extends C>
void useContainerOfSpecificItem(Container<C> c, C key1, U key2) {
  c.find(key1);
  c.find(key2);
}

There, you can see that find() accepts covariant types of keys, though, in this limited use, it's not actually necessary to distinguish type U from type C.

seh
A: 

I think your find method is parameterized incorrectly. In particular, you almost certainly don't mean to use extends in the declaration R extends T.

You seem to accept that with the wildcarded generic parameter <? extends Item>, you are not going to be able to insert anything, because the compiler cannot assert that any particular object you pass in conforms to the bounds (with the single exception of the null literal). Remember that this is not because of any specials semantics of an insert-type method, but solely because of the interface.

Your find method cannot be called for the exact same reason. Bear in mind that declaring <R extends T> and then declaring a parameter as type R, is exactly the same as declaring the parameter of type T. (Think about it the allowed values in both cases). And as you've seen above, no non-null objects can be accepted as an instance of T in your wildcarded case.

I think you may have intended to write your find method as <R super T>. In this case, the compiler can know for sure that no matter actual type of T is, it's Item or a subtype - and so Item or any of its superclasses (including Object) will always be valid for R and thus can be passed in. However, in this case, since Object is a valid substitution for the bounds, and all objects can be accepted for an Object parameter, this method is then equivalent to

public int find(Object r) {
   return r.hashCode();
}

This is fact is entirely the semantics you're trying to capture - you don't need the generic bounding as they don't provide any bounds. Typically it's only ever worth using super in generic bounds when it's a nested generic parameter, e.g. you're accepting a collection as a parameter that you want to add objects of type T to (in which case you'd want a Collection<? super T>).

Alternatively, reading through your own answer to the question, my assessment in the above paragraph may be slightly incorrect. There are, then, three different restrictions you could try to apply to the type of the argument to the find method:

  1. Anything is allowed (i.e. Object).
  2. The argument must be an instance of the highest possible bound for the contained types (so if Container is defined as Container<T extends Item>, you declare the method to take a parameter of type Item).
  3. The types must match exactly (i.e. T).

Generally speaking I would recommend to go as general as is possible - if you need to call methods that are defined in the Item class in order to test the match, then your hands are tied and you'll have to go with the second option. However, if you don't need to do this, then accept arguments of type Object to give callers the most flexibility.

Along those lines, there is essentially no possible argument for ever taking option 3 - you won't get any extra functionality in your method (since you can't call any more specific methods than you could in the second case), and you're simply restricting clients. Consider the following:

MyItem a = new MyItem();
Container<MyItem> c = new Container<MyItem>();
c.insert(a);

// Much later, possibly passing through various layers of the stack/maps/etc.

Item i = a;
c.find(i); // Will not compile if the find method takes an argument of type T

There is no benefit at all in forcing callers to downcast their Item reference to T specifically, when by definition you will be able to make the required method calls within find on an Item object, and can return an appropriate response based on the actual state of the object rather than the reference it is currently held in.

Andrzej Doyle