4

I have a class will contain a few different parser implementations for different objects. While I am able to store the parser implementations without any warnings, getting a parser from the map warns about an unchecked cast exception. Below is a simplified excerpt:

private Map<Class<?>, Parser<?>> parsers = new HashMap<>();

public <T> void addParser(Class<T> type, Parser<T> parser) {
    parsers.put(type, parser);
}

private <T> Parser<T> parserFor(Class<T> type) {
    // Compiler complains about unchecked cast below
    return (Parser<T>) parsers.get(type);
}

Is there another way to implement similar logic without causing an unchecked cast warning?

seeming.amusing
  • 1,179
  • 1
  • 8
  • 18
  • No there's not really a way to do this without the unchecked cast. It's fine as long as you don't do any funny business. – Radiodef Feb 10 '15 at 04:27

4 Answers4

2

Consider using a TypeToInstanceMap<Parser<?>> from Google Guava. This will let you do things like this with no compiler warnings or errors whatsoever:

TypeToInstanceMap<Parser<?>> parsers;

parsers.putInstance(new TypeToken<Parser<String>>(){},
                    makeStringParser());

Parser<Integer> intParser = parsers.getInstance(new TypeToken<Parser<Integer>>(){});

This is essentially a library that does something very similar to @ruakh's answer under the hood.

The developer who added the <T> to Class<T>, Neil Gafter, discussed the fundamental issue in his blog shortly after Java 5 was released. He calls Class<T> a "type token", and says:

[Y]ou simply can't make a type token for a generic type

... in other words, you can't make a Class<Parser<T>>.

Matt McHenry
  • 20,009
  • 8
  • 65
  • 64
  • That link you shared definitely is a good read and helped clarify the topic a bit. As I am looking for a solution that doesn't require a 3rd party library, I've decided to accept @ruakh's answer. Having said that, I think this is definitely a very good alternative as well. – seeming.amusing Feb 11 '15 at 01:53
  • Looks like it has to be `getInstance` instead of `get` - `get` seems to warn about the type. I'd use `putInstance` to put the values in as well, as it might catch other accidents. – Hakanai Mar 28 '19 at 05:04
  • @Trejkaz thanks, looks like the API changed since my original answer; updated. – Matt McHenry Mar 31 '19 at 01:27
2

There's no way to create a Map<Class<...>, Parser<...>> where the ...-s can both be anything but have to match between a key and its value; so there's no way that you can get the compiler to do the checking for you, where retrieving a Class<T> is guaranteed to give you a Parser<T>. However, your code itself is correct; you know that your cast is correct, even though the compiler does not.

So, when you know that your cast is correct, but Java doesn't know it, what can you do?

The best and safest approach is to craft a specific piece of your code, as small as possible, that is responsible for handling the translation between checked and unchecked logic, and for making sure that the unchecked logic doesn't cause any mistakes. Then, you just mark that code with the appropriate @SuppressWarnings annotation. For example, you can have something like this:

public abstract class Parser<T> {
    private final Class<T> mType;

    protected Parser(final Class<T> type) {
        this.mType = type;
    }

    public final Class<T> getType() {
        return mType;
    }

    @SuppressWarnings("unchecked")
    public final <U> Parser<U> castToParserOf(final Class<U> type) {
        if (type == mType) {
            return (Parser<U>) this;
        } else {
            throw new ClassCastException("... useful message ...");
        }
    }
}

This would allow you to safely write, in your example:

public <T> void addParser(final Parser<T> parser) {
    parsers.put(parser.getType(), parser);
}

private <T> Parser<T> parserFor(final Class<T> type) {
    return parsers.get(type).castToParserOf(type);
}
ruakh
  • 175,680
  • 26
  • 273
  • 307
  • Out of curiosity, how is suppressing a raw type warning be better / safer than suppressing the unchecked warning? – seeming.amusing Feb 10 '15 at 05:42
  • 1
    @fayerth: It isn't. Both are equally fine -- the important part is that you just localize the non-type-checked code as much as possible, so that you can be confident in its correctness. My example ended up with `@SuppressWarnings("raw")` because you can't cast directly from `Parser` to `Parser` -- that's not a compile-*warning* but a compile-*error* -- so I needed an intermediate cast, and `(Parser)` was the simplest one. (Otherwise I'd have needed to write *two* casts, e.g. something like `(Parser) (Parser>) this`.) – ruakh Feb 10 '15 at 05:46
  • @fayerth: Actually, sorry, correction: I just tested in Eclipse, and casting from `Parser` to `Parser` actually worked fine. Just an unchecked-cast warning. So I'll change my answer to that. – ruakh Feb 10 '15 at 05:51
1

Since your map parsers value type is Parser<?> and your method's return type is Parser<T>, it's clearly an error to cast the result of parsers.get(type) to T.

One way to remove compile error is to cast the type to Parser<T>:

private <T> Parser<T> parserFor(Class<T> type) {
  return (Parser<T>)parsers.get(type);
}

Also, you can change the return type to Parser<?> since you specified the parsers map as Map<Class<?>, Parser<?>>. This will clear the compilation error, too.

private <T> Parser<?> parserFor(Class<T> type) {
  return parsers.get(type);
}

Or you can add type parameter to your wrapping class.

public class YourClass<T> {
  private Map<Class<T>, Parser<T>> parsers = new HashMap<>();

  public void addParser(Class<T> type, Parser<T> parser) {
    parsers.put(type, parser);
  }

  private Parser<T> parserFor(Class<T> type) {
    return parsers.get(type);
  }
}

I'm not sure which one can be correctly applied, however, try not to use type casting. Consider why we use generic.

ntalbs
  • 28,700
  • 8
  • 66
  • 83
  • 1
    On the compile error, that was a typo on my part; I have edited the question accordingly. For your suggestion on returning a `Parser>`, I had tried that and initially thought it would work. However, I then realized that instead I am getting an unchecked cast warning when I use `parserFor()`, which essentially meant the warning is now just in a different place. Ideally, the class itself does not need a type parameter to do its work as it just wants to get the Parser for a specific type and use it. Would there still be some way to avoid the warning? – seeming.amusing Feb 10 '15 at 05:28
  • 1
    Returning a wildcard type, that too an unbounded one is a bad idea IMO. `Parser` should be fine. – Rohit Jain Feb 10 '15 at 19:32
0

I got this to work in a different way. I am experimenting with generics myself and would be happy to receive criticism :)

What I did was add a tagging interface for Parseable objects and then use that as an upper bound on the Parser.

public interface IParseable {}

public class Parser<T extends IParseable> {
    T paresableObj;
    // do something with parseableObject here
}

And now the Parser Factory does not have to use wildcards nor use casts.

public class ParserFactory {

    private Map<Class<?>, Parser<? extends IParseable>> parsers = new HashMap<Class<?>, Parser<? extends IParseable>>();

    public <T> void addParser(Class<T> type, Parser<? extends IParseable> parser) {
        if(parserFor(type) == null){
            parsers.put(type, parser);
        }else{
            //throw some excep
        }
    }

    private <T> Parser<? extends IParseable> parserFor(Class<T> type) {
        return parsers.get(type);
    }

}
ramp
  • 1,256
  • 8
  • 14
  • That doesn't help because you force clients to now deal with `IParseable` instead of the wildcard which would default to `Object`. However it might not be possible that everything that should be worked with by the parser will implement that interface. So this is basically just an extra burden. – SpaceTrucker Feb 10 '15 at 09:52
  • @SpaceTrucker - thank you, you are right. It does do that. I was thinking that this was more an internal library in which case it is not too difficult to have the class implement a tagged interface. Also I was thinking it would be a tall order to parse any object and an object that is Parseable should have certain characteristic. Not defending the answer, just explaining my line of thought. Thank you again for your feedback. – ramp Feb 10 '15 at 10:25