简体   繁体   中英

Redesigning around unchecked cast warnings

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?

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>> .

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);
}

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.

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);
    }

}

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