Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Enums with interface - how to do so generically?

If I have a set of enums, and want to have them all implement an interface, is this the correct way to do generically?

Enum:

public enum MentalSkill implements SkillType {
    ACADEMICS,COMPUTER,CRAFTS,INVESTIGATION,MEDICINE,OCCULT,POLITICS,SCIENCE;
    private static final int UNTRAINED_PENALTY = -3;
    @Override
    public SkillType fromValue(String value) {
        return valueOf(value);
    }
    @Override
    public int getUntrainedPenalty() {
        return UNTRAINED_PENALTY;
    }
}

Interface:

public interface SkillType {

SkillType fromValue(String value);  
int getUntrainedPenalty();
}

Holding Class (where my suspcions lie that this is not quite right):

public class SkillsSet<T extends SkillType> {
    T t;
    Map<T,Integer> skills = new HashMap<>();
    public SkillsSet(String[] skills) {
        for (String string : skills) {
            addSkill(string,t.getUntrainedPenalty());
        }
    }
    private void addSkill(String skillString,Integer value) {
        skills.put((T) t.fromValue(skillString), 0);
    }
}

The issue comes in my T t which will obviously give a NPE as I don't instantiate my inferred type. The issue is that I can't, as it's an enum.

What's the Java way of saying 'Use this interface to access an enum method generically.'? Throwing in skills.put(T.fromValue(skillString), 0); doesn't work, which was my next guess.

I've looked over the tutorials (which is how I've gotten this far) and I couldn't seem to see how to get any further. How do I get this code to work?

like image 368
AncientSwordRage Avatar asked Aug 08 '14 07:08

AncientSwordRage


2 Answers

Your problem lies in SkillType - it should be generic:

public interface SkillType<T extends SkillType<T>> {

    T fromValue(String value);  

    int getUntrainedPenalty();
}

This is called a self referencing generic parameter.

Without this self reference, you could return an instance of a different class from the fromValue() method.

Here's how it would look:

public enum MentalSkill implements SkillType<MentalSkill> {
    public MentalSkill fromValue(...) {}
    ...
}

And

public class SkillsSet<T extends SkillType<T>> {
    ...
}

Once you make this change, it should all fall into place.

like image 141
Bohemian Avatar answered Sep 20 '22 09:09

Bohemian


One solution is through using reflection. I'm not sure if there are other solutions that might look cleaner though. The following does compile and run though.

public class SkillsSet<T extends SkillType> { 
    private Map<T,Integer> skills = new HashMap<>();

    // These don't have to be class members, they could be passed
    // around as parameters
    private T[] enumConstants;   // needed only for getEnumValue
    private Class<T> enumClass;  // needed only for getEnumValueAlternate

    public SkillsSet(String[] skills, Class<T> enumClass) {
        // Though all implementers of SkillType are currently enums, it is safer
        // to do some type checking before we do any reflection things
        enumConstants = enumClass.getEnumConstants();
        if (enumConstants == null)
            throw new IllegalArgumentException("enumClass is not an enum");

        for (String string : skills) {
            T t = getEnumValue(string)
            if (t == null) {
                // or use continue if you dont want to throw an exception
                throw new IllegalArgumentException();
            }
            this.skills.put(t, t.getPenalty());
        }
    }

    // These don't even need to be methods, but I separated them for clarity.
    // SuppressWarnings annotation is used since we checked types in the constructor
    @SuppressWarnings( "unchecked" )
    public T getEnumValue( String string ) {
        try {
            return (T) enumConstants[0].fromValue(string);
        }

        // If valueOf does not find a match, it throws IllegalArgumentExecption
        catch ( IllegalArgumentException e ) {
            return null;
        }
    }

    // An alternate version  of getEnumValue that does not require the 'fromValue'
    // method on the SkillType interface.
    @SuppressWarnings( "unchecked" )
    public T getEnumValueAlternate( String string ) {
        try {
            return (T) enumClass.getMethod("valueOf", String.class).invoke(null, string)
        }

        // Any reflection exceptions are treated as 'not found' on valueOf
        catch (Exception e) {
            return null; 
        }
    }


    ...
}

There are two different versions of getEnumValue. I'd recommend the version that uses the getEnumConstants method on the enumClass parameter. It also allows you to handle special cases in fromValue that might not typically match the enum's valueOf function.

Instantiating the above works with SkillsSet<MentalSkill> = new SkillsSet<MentalSkill>(new String[] {"COMPUTER", "CRAFTS"}, MentalSkill.class);

like image 21
Robert Bartlett-Schneider Avatar answered Sep 20 '22 09:09

Robert Bartlett-Schneider