0

Having a collection of abstract objects: Set<Foo> foes;

I want to have a method like this:

List<? extends Foo> getFoesByType(TypeEnum type);

I've tried:

List<? extends Foo> result = new ArrayList<>();
for(Foo f : foes) {
        if(f.getType() == type) {
            switch(type) {
            case TYPE1:
                f = (FooType1) f;
                break;
            case TYPE2:
                /*...*/
            }

            result.add(f);  
            /*The method add(capture#1-of ?) in the type 
            List<capture#1-of ?> is not applicable for the arguments (Foo)*/
        } 
}  

return result;

But I get an error.

I want to be able to do this: List<FooType1> foesType1 = getFooesByType(TypeEnum.TYPE1); Which would be the correct way?

anat0lius
  • 2,145
  • 6
  • 33
  • 60
  • 2
    Why do you need to cast to a child class before inserting into the list? That doesn't make sense. – QBrute Jun 12 '17 at 07:56
  • @QBrute Maybe the wildcard is not what I should use. I just want to get a list of concrete classes. `type` is associated to the concrete class. – anat0lius Jun 12 '17 at 08:14

3 Answers3

6

The only safe way is by adding a Class parameter to support type checking at runtime:

private <T extends Foo> List<T> getFooesByType(EnumType type, Class<T> type) {
    List<T> result = new ArrayList<>();
    for(Foo f : foes) {
        if(f.getType() == type) {
            switch(type) {
                case TYPE1:
                    // do something, cast is not necessary
                    break;
                case TYPE2:
                    /*...*/
            }
            result.add(type.cast(f)); // safe cast
        }
    }
    return result;
}

to be called like

List<FooType1> foesType1 = getFooesByType(TypeEnum.TYPE1, FooType1.class);

This is the only way to ensure that the list elements are of the caller specified type. On the other hand, this makes the requirement that T is a subtype of Foo obsolete.

You could change <T extends Foo> to <T> raising the flexibility of the method,
e.g. List<Object> list = getFooesByType(TYPE1, Object.class); would be legal as well.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Why your line is a "safe cast" and mine is not? Have a look into `Class#cast` implementation: `return (T)paramObject;`. So, same question to you: "this method cannot guaranty that the elements are of the `T` the caller expects. [...] Now, how does your method hold its promise?" – Stefan Warminski Jun 12 '17 at 12:35
  • 2
    @Stefan Warminski: you have to look at [the lines before that](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/lang/Class.java#3368)… – Holger Jun 12 '17 at 12:59
  • 2
    @Stefan Holger is right. By using the second argument, the compiler won't allow i.e. `List foesType1 = getFooesByType(TypeEnum.TYPE2, FooType2.class);`, though `List foesType1 = getFooesByType(TypeEnum.TYPE2, FooType1.class);` will compile fine, which will have the effect of casting `Foo` objects to `FooType1` while taking the action for `FooType2`... So this all points to deeper design problems, IMO. Btw, Holger, you should rename the second argument to something like `clazz`... – fps Jun 12 '17 at 15:37
  • In fact, I don't need that `EnumType` anymore, it can work just with the class type: `if (type.isInstance(f)) result.add(type.cast(f));` – anat0lius Jun 13 '17 at 06:56
  • @LiLou_: indeed, if the only purpose of the `enum` was to denote the desired type, class literals are the better alternative. – Holger Jun 13 '17 at 06:57
3

You need to make your method generic:

private <T extends Foo> List<T> getFooesByType(EnumType type) {
    List<T> result = new ArrayList<>();
    for(Foo f : foes) {
        if(f.getType() == type) {
            switch(type) {
            case TYPE1:
                // do something, cast is not necessary
                break;
            case TYPE2:
                /*...*/
            }

            result.add((T) f); // cast here
        }
    }

    return result;
}
Stefan Warminski
  • 1,845
  • 1
  • 9
  • 18
  • 1
    How would you call this generic method? I suppose in the call you need to specify somehow what `T` is? – Ole V.V. Jun 12 '17 at 08:30
  • 1
    It is type inference. By calling `List foesType1 = getFooesByType(TypeEnum.TYPE1);` the compiler get the information from `List`. You can also specify the generic type by a 2nd parameter `Class fooClass` and invoke it like this: `List foesType1 = getFooesByType(TypeEnum.TYPE1, FooType1.class);` – Stefan Warminski Jun 12 '17 at 08:33
  • 5
    This is broken. You get an unchecked warning at the `(T)` cast for a good reason: this method cannot guaranty that the elements are of the `T` the caller expects. In fact, I can even write `List list = getFooesByType(TYPE1);` without a compiler error, as then, `T` will be `Foo&Runnable`. Now, how does your method hold its promise? – Holger Jun 12 '17 at 12:08
  • 1
    `` will not allow `Runnable`. But you're right, it is not sure that an instance of `Foo` with `EnumType.TYPE1` extending `FooType1`. This is not part of OP's question but could be easily solved if you add the parameter `Class fooClass` and check the `Foo`s with `fooClass.isAssignableFrom(f.getClass())` – Stefan Warminski Jun 12 '17 at 12:19
  • 5
    `` will not allow `Runnable`, but as said, it allows `Foo&Runnable`. I should have written `List extends Runnable> list = getFooesByType(EnumType.TYPE1);` to be precise, this *does* compile with Java 8. See “[Generic return type upper bound - interface vs. class - surprisingly valid code](https://stackoverflow.com/q/36402646/2711488)”. But stay at `List foesType1 = getFooesByType(TypeEnum.TYPE1);` if you wish and explain, how the compiler should distinguish that from `List foesType1 = getFooesByType(TypeEnum.TYPE2);`… – Holger Jun 12 '17 at 13:11
0

You can't do this. ? extends Foo means that there could be any child of Foo class. You don't know what exactly it is and that's why you can't add to List<? extends Foo>.

But you can create array list and assign it to List and then return.

List<? extends Foo> getFoesByType(TypeEnum type) {
    List<Foo> result = new ArrayList<>();
    // add something to result list
    return result;
}
solomkinmv
  • 1,804
  • 3
  • 19
  • 30
  • 1
    But then when I invoke the method like this: `List foesType1 = getFooesByType(TypeEnum.TYPE1);` I get the same error as before. – anat0lius Jun 12 '17 at 08:16
  • @LiLou_ in this case you don't need wildcards. You can use just `List`. You will be able to add Foo successors to such list. – solomkinmv Jun 12 '17 at 08:35