views:

663

answers:

3

Hi all,

I still have trouble with some corner cases in the java generics system.

I have this method (I'm only interested in the signature) :

 interface Extractor<RETURN_TYPE> {
    public <U extends Enum<U>> RETURN_TYPE extractEnum(final Class<U> enumType);
}

(think about an interface whose implementations sometimes extracts an EnumSet sometimes an implementation extract a JComboBox etc.)

and I want to call it with a class obtained at runtime, so I simply call it this way :

 public static <RETURN_TYPE> RETURN_TYPE extractField(final Extractor<RETURN_TYPE> extractor, final Field field) {
    final Class<?> type = field.getType();
    if (type.isEnum())
        return extractor.extractEnum(/* error here*/type.asSubclass(Enum.class));
    throw new RuntimeException("the rest of the visitor is not necessary here");
}

and I get a strange error message : incompatible types found : java.lang.Object required: RETURN_TYPE

the location of the message if just after the opening braket of the call, before the "t" of type.

if I call it from a non-generic context, it works :

    Integer extractField(final Extractor<Integer> extractor, final Field field) {
        final Class<?> type = field.getType();
        if (type.isEnum())
            return extractor.extractEnum(type.asSubclass(Enum.class));
        throw new RuntimeException("the rest of the visitor is not necessary here");
    }

Does anybody have an explanation and a solution to this problem please ?

Here is a complete file for people wanting to play with it :

public class Blah {
    interface Extractor<RETURN_TYPE> {
        public <U extends Enum<U>> RETURN_TYPE extractEnum(final Class<U> enumType);
    }

    public static <RETURN_TYPE> RETURN_TYPE extractField(final Extractor<RETURN_TYPE> extractor, final Field field) {
        final Class<?> type = field.getType();
        if (type.isEnum())
            return extractor.extractEnum(/* error here*/type.asSubclass(Enum.class));
        throw new RuntimeException("the rest of the visitor is not necessary here");
    }

    public static Integer extractField(final Extractor<Integer> extractor, final Field field) {
        final Class<?> type = field.getType();
        if (type.isEnum())
            return extractor.extractEnum(type.asSubclass(Enum.class));
        throw new RuntimeException("the rest of the visitor is not necessary here");
    }
}

thanks in advance,

Nico

+2  A: 

I haven't been able to infer the original problem.

Am I correct that Extractor.extract has two type parameters, U which must be an Enum and T which is an arbitrary type? In the generic call, VV is both T and U? If U is VV than the parameter has to be Class<VV>, not Class<Enum>. The following compiles for me, but as you can see the generic method needs to be provides instance of Class<VV>

class Outer {
  static class Extractor<T> {
    public <U extends Enum<U>> T extract(final Class<U> lala) {
      return null;
    }
    // two type parameters, T and U
    // U must be an enum
    // T is arbitrary class
  }

  static <VV extends Enum<VV>> VV extract(final Extractor<VV> extractor, Class<VV> vvClass) {
    final Class<?> type = null;
    return extractor.extract(vvClass);
    // Outer.extract returns VV 
    // T -> VV
    // it seems VV is also U
  }
}
Hemal Pandya
No, sorry for stating my problem in a difficult to understand way, I can't find any simplification yet.so, T is the return type of the extractor, and U is the static enum type. In my use point I create a method that's still generic on the extracted data type, but gets the enum type to be declared from Field.getType().
nraynaud
+1  A: 

Looks like your Field.getType( ) will only return a generic Class. Because you will try to put a type with already erased type information this function will have to emit an "unchecked" warning, and ALL type information on generic interface WILL get erased.

Remember, that with type erasure your interface looks like this:

interface Extractor {
    public Object extractEnum( final Class enumType );
}

So, because all type information is erased, the return type of extractEnum is java.lang.Object, so you have to add a specific cast. And this is precisely the error message that you've got.

Here is the modified example of your code.

@SuppressWarnings( "unchecked" )
public static <RETURN_TYPE> RETURN_TYPE extractField(
       final Extractor<RETURN_TYPE> extractor,
       final Field field
    )
{
    final Class type = field.getType(); // unchecked

    if (type.isEnum())
    {
        // needs cast
        return (RETURN_TYPE) extractor.extractEnum( type ); // unchecked
    }
    throw new RuntimeException("the rest of the visitor is not necessary here");
}

DISREGARD THIS COMMENT: To answher the original question on why you have a compile error. This is a square peg into a round hole kind of problem. type.asSubclass( Enum.class ) returns Class< ? extends Enum >, it is still not the same as Class< U > which the interface call expects.

Alexander Pogrebnyak
yes, I'll have to deal with a warning in this class anyway. But this doesn't give me an explanation of WHY I have this error.
nraynaud
BTW, in my environment ( Eclipse, JDK 1.6.0 ) the second method with explicit Integer type did not compile either. The specific error message might be different on your system, but the underlying problem is the same as I've described in the last paragraph.
Alexander Pogrebnyak
I can concentrate all the warning in one statement, thanks for the idea :(RETURN_TYPE)extractor.extractEnum((Class) type)But I don't think your square peg idea is right. look at this : http://stackoverflow.com/questions/1458779/how-to-cast-to-crtp-in-javaand it works, I use it 2-3 times in my code. Do you have those warnings raised as errors ?
nraynaud
I checked and type.asSubclass( Enum.class ) will pass just fine into the extractEnum call. So my square peg idea is indeed wrong. But the main problem here is that in your code the type erasure already happened. Because of this, using Generic vs. Raw type does not buy you anything ( you are still going to get an 'unchecked' warning ). However because you've already tested that 'type' is instance of Enum, the call to asSubclass( Enum.class ) is totally redundant. That's why you should use a raw Class type for 'type' variable and pass it directly to extractEnum
Alexander Pogrebnyak
One more comment. You SHOULD pay attention and not generate 'unchecked' warnings when possible, but sometimes you do need to erase type information. When you hit such a case remember that it's almost always better to erase to the Raw type ( if you are going to break the law anyway, do it in a big way ).Your use case looks totally legit to me to follow such a pattern.
Alexander Pogrebnyak
I have no choice anyway to have a warning. When you play with reflexivity, you have no choice than having static types warning.
nraynaud
+2  A: 

I would not be surprised if this is a bug in your compiler, actually. Through serious use of generics (the kind of thing you're doing, combining parameterised methods, bounded wildcards and other "advanced" uses of generics) I've encountered two or three issues in the last year in javac (annoyingly, the same unit often compiled fine in the IDE).

In your case I'm fairly sure it's a bug, since the part that the compiler is complaining about is that extractor.extractEnum is returning an Object rather than a RETURN_TYPE. And regardless of what crazy inference it does with your enum method arguments... it knows from the type signature that the Extractor is an Extractor<RETURN_TYPE>, so you should always be able to say return extractor.extractEnum(...);.

The damning evidence is that even if you call the method with a null argument (thus completely removing any potential complications from the enum generics in the argument), the compiler still complains. In particular, it now says that it thinks the return type from the Extractor is U<RETURN_TYPE> which is clearly rubbish.

In general the solution to working around these issues is throwing in some explicit casts. Is the compiler happy if you cast the output of extractEnum to RETURN_TYPE? Edit: no, it's really not - it complains that U<RETURN_TYPE> and RETURN_TYPE are inconvertible - eep...

If you're using a recent 1.6 compiler, I suggest you report this to Sun as this is quite a big problem with javac. Here's a very short test case that exercises it:

public class Test {
  interface Sub<O> {
    public <I extends Enum<I>> O method(final Class<I> enumType);
  }

  public static <O> O go(final Sub<O> sub) {
    return sub.method(null);
  }
}

P.S. it's general convention to use a single uppercase letter to designate generic type parameters. I'm not going to say "I'm right, you're wrong", but bear in mind that I found your code much harder to read and follow than if you had used Extractor instead. (And judging by Hemal's phrasing of his answer it's the same for him too.)

Andrzej Doyle
Ok, thank you for your deep analysis. But I have an apple, so I suppose they will throw me bug away. I buy your explanation without checking, it makes sense. I added the long variable type name for the sake of clarity because people might not understand what's the point of this variable, in my code I use one letters too, even if I'm not sure this convention is really good, it's widely accepted.
nraynaud