Unchecked cast warnings in generic method on param

2019-08-10 22:19发布

问题:

In the code below, the type parameter D can be either a List<Byte> or a List<List<Byte>> (it is the third generic parameter in the Fields<?, ?, D> interface but still I might omit it there - but it is present also in the return type of the method). Can't seem to find a way to tell the compiler this - get Unchecked cast warnings in the lines marked //* :

public static <D, K, T extends Enum<T> & Fields<?, ?, D>> List<EnumMap<T, D>> 
        getEntries(InputStream is, Class<T> fields) throws IOException {
    final List<List<Byte>> entries = new ArrayList<List<Byte>>();
    // populate "entries"
    final boolean hasLists = hasLists(fields);
    List<K> daBytes;
    if (hasLists) {
        daBytes = (List<K>) new ArrayList<EnumMap<T, List<List<Byte>>>>(); //*
    } else {
        daBytes = (List<K>) new ArrayList<EnumMap<T, List<Byte>>>(); //*
    }
    final int numOfEntries = entries.size();
    for (int currentEntry = 0; currentEntry < numOfEntries; ++currentEntry) {
        // add an element in daBytes for this currentEntry
        if (hasLists) {
            daBytes.add((K) new EnumMap<T, List<List<Byte>>>(fields)); //*
        } else {
            daBytes.add((K) new EnumMap<T, List<Byte>>(fields)); //*
        }
        for (T daField : fields.getEnumConstants()) {
            List<Byte> field = new ArrayList<Byte>();
            // populate "field"
            D map = (D) daBytes.get(currentEntry);
            if (hasLists) {
                List<List<Byte>> fieldEntries = new ArrayList<List<Byte>>();
                // populate "fieldEntries"
                ((EnumMap<T, List<List<Byte>>>) map).put(daField,
                    fieldEntries); //*
            } else {
                ((EnumMap<T, List<Byte>>) map).put(daField, field); //*
            }
        }
    }
    return (List<EnumMap<T, D>>) daBytes; //*
}

If hasLists is false then I need D to be a List<Byte> else a List<List<Byte>>. The daList variable is a List<EnumMap<T, D>>. Now it would seem natural (to me) to define :

List<EnumMap<T, D>> daBytes;

But as soon as I do this and change :

if (hasLists) {
    daBytes = (List<EnumMap<T, D>>) new ArrayList<EnumMap<T, List<List<Byte>>>>();
}

I get an error :

Cannot cast from ArrayList<EnumMap<T,List<List<Byte>>>> to List<EnumMap<T,D>>

Been going round in circles making daBytes an Object, a List<?> etc but always getting to use either outright casts or generic casts that lead to warnings. There must a way to have this compile cleanly with no casts

回答1:

I did a bit of refactoring, extracting a "method object" as suggested by other posters. This is an instance of the so called "strategy class" design pattern.

Wherever you have a check on hasLists I introduced an abstract method. It turns out that you do not need the K type parameter anymore.

The code produces one unchecked warning at the top, where I have put a check on hasLists to choose an implementation of the abstract class.

public static <X, T extends Enum<T> & Fields<?, ?, List<X>>>
List<EnumMap<T, List<X>>> getEntries(InputStream is, Class<T> fields) throws IOException {
    final List<List<Byte>> entries = new ArrayList<List<Byte>>();
    // populate "entries"

    FieldsStrategy<X, T> strategy = selectStrategy(fields);
    return strategy.getEntries(entries);
}

private static <X, T extends Enum<T> & Fields<?, ?, List<X>>>
FieldsStrategy<X, T> selectStrategy(Class<T> fields) {
    final boolean hasLists = hasLists(fields);
    return hasLists
            ? (FieldsStrategy<X, T>) new ByteListFieldsStrategy(fields) //* this is the only unchecked warning
            : (FieldsStrategy<X, T>) new ByteFieldsStrategy(fields);    //* this is the only unchecked warning
}

private abstract static class FieldsStrategy<X, T extends Enum<T> & Fields<?, ?, List<X>>> {
    private Class<T> fields;

    public FieldsStrategy(Class<T> fields) {
        this.fields = fields;
    }

    public List<EnumMap<T, List<X>>> getEntries(List<List<Byte>> entries) {

        List<EnumMap<T, List<X>>> daBytes = new ArrayList<EnumMap<T, List<X>>>();
        final int numOfEntries = entries.size();
        for (int currentEntry = 0; currentEntry < numOfEntries; ++currentEntry) {
            // add an element in daBytes for this currentEntry
            daBytes.add(new EnumMap<T, List<X>>(fields));
            for (T daField : fields.getEnumConstants()) {
                EnumMap<T, List<X>> map = daBytes.get(currentEntry);
                map.put(daField, getFieldData(daField));
            }
        }
        return daBytes;
    }

    protected abstract List<X> getFieldData(T daField);

}

public static class ByteFieldsStrategy<T extends Enum<T> & Fields<?, ?, List<Byte>>>
        extends FieldsStrategy<Byte, T> {
    public ByteFieldsStrategy(Class<T> fields) {
        super(fields);
    }

    protected List<Byte> getFieldData(T daField) {
        ArrayList<Byte> field = new ArrayList<Byte>();
        // populate "field"
        return field;
    }
}

public static class ByteListFieldsStrategy<T extends Enum<T> & Fields<?, ?, List<List<Byte>>>>
        extends FieldsStrategy<List<Byte>, T> {
    public ByteListFieldsStrategy(Class<T> fields) {
        super(fields);
    }

    protected List<List<Byte>> getFieldData(T daField) {
        List<List<Byte>> fieldEntries = new ArrayList<List<Byte>>();
        // populate "fieldEntries"
        return fieldEntries;
    }
}

You probably can remove the sole remaining unchecked warning by moving hasLists() logic and the switch to choose which strategy class to use to the Fields interface and implementing it in the enum classes.

UPDATE: here is the updated definition of selectStrategy and Fields which does not raise any warnings:

public static interface Fields<T extends Enum<T> & Fields<T, D, K>, D extends Data, K> {
    // ....

    GetEntries<K, T> selectStrategy();
}

private static <K, T extends Enum<T> & Fields<T, ?, K>>
GetEntries<K, T> selectStrategy(Class<T> fields) {
    for (T field : fields.getEnumConstants()) {
        return field.selectStrategy();
    }
    throw new IllegalArgumentException("Enum type has no instances: " + fields);
}

You need to implement selectStrategy() in your enum types and return either an appropriate GetByteEntries or GetByteListEntries.

You can remove hasLists() now.