1

Lets say I have two enums like so:

public enum foo {
    FOO1, FOO2, FOO3;
}

public enum bar {
    BAR1, BAR2, BAR3;
}

Now, I'd like to implement a lookup table that associates these enum values to int values using a Map and and have both enum values as keys so that I can interate over the map easily using Map utilities but I don't know how to proceed other than using a Map<Object, int> which is something that doesn't sound type safe AT ALL; I was thinking about either making a super class fooBar for both enums and use a Map<fooBar, int>, but AFAIK enums can't extend a class, or making an interface fooBarInterface with an empty body from which the two enums can implement and then use a Map<fooBarInterface, int>.

Any ideas for what would be a good approach for this problem? Merging the enums into one is not an option for me.

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
  • 3
    I don't think anyone will be able to give you a definitive answer here. The ideas you've had are all valid, and would be appropriate for some scenarios and less appropriate for others. You might get better answers if you can be more specific about your use-case, but at the moment I'd say this is a bit too abstract to be answered well. – DaveyDaveDave Mar 23 '21 at 15:18
  • 1
    Does this help? https://stackoverflow.com/questions/1414755/can-enums-be-subclassed-to-add-new-elements – Abra Mar 23 '21 at 15:24
  • @DaveyDaveDave Hey Dave, thanks for your answer! I'm working on a uni project for my graduation and being too specific may be a problem, but the fact that you're telling me that those ideas may be valid in their own scenarios reassures me; I wrote this question from my phone on the train omw back home so I don't have any way to test something until I get back home, so once I do I'll definitely do some tests and report back – blvck_mvgic_dot_exe Mar 23 '21 at 15:31
  • @Abra thanks for the pointer, will definitely read for ideas asap – blvck_mvgic_dot_exe Mar 23 '21 at 15:31
  • 1
    There is no one good solution to the problems you are describing, but there are workarounds, Specifcally, read around about the *Extensible Enum Pattern* in Java:: https://www.baeldung.com/java-extending-enums. This pattern doesn't solve everything. In particular, there is no simple way to iterate over all the enums in the classes that extend the common interface. Nevertheless, it may suit your needs. – scottb Mar 23 '21 at 18:48

1 Answers1

1
  1. Your idea of a common empty interface (FooBarInterface) is fine and solves the problem.
  2. If you want to accommodate more enums and possibly ones that don’t implement your interface, you may also use Enum as map key type: Map<Enum<?>, Integer> lookupTable = Map.of(Foo.FOO1, 1, Foo.FOO2, 3, Foo.FOO3, 5, Bar.BAR1, 6, Bar.BAR2, 8, Bar.BAR3, 11);.

In both cases, depending on circumstances I would probably still keep two separate maps and only make a union of them for iteration when needed.

To carry this idea a little further I might even consider developing a view onto the union of the two maps’ entry sets. You would need to develop your own Iterator implementation that first iterates the entry set of the first map, then the second map, then finally signals the end of the iteration (hasNext() returns false).

Type safe? What you say you want has an inherent “type unsafety” to it. Live with it. It’s only a small cost on top of the basic type problem with the java.util.Map interface: its get method accepts any object, not just an object of the declared key type (this is for historical reasons).

PS In the Java versions I have used int cannot be a value type of a Map or a type parameter for a generic type at all. It may have been added later. If not, you will have to use Integer.

Edit: the terse way of iterating two maps

One way to iterate two (or more) maps is through a stream operation:

    Map<Foo, Integer> fooLookupTable = new EnumMap<>(Map.of(Foo.FOO1, 2, Foo.FOO2, 3, Foo.FOO3, 8));
    Map<Bar, Integer> barLookupTable = new EnumMap<>(Map.of(Bar.BAR1, 4, Bar.BAR2, 6, Bar.BAR3, 11));

    Stream.of(fooLookupTable, barLookupTable)
            .flatMap(m -> m.entrySet().stream())
            .forEach(e -> System.out.format("%4s (%3s) %2d%n",
                    e.getKey(), e.getKey().getClass().getSimpleName(), e.getValue()));

Output:

FOO1 (Foo)  2
FOO2 (Foo)  3
FOO3 (Foo)  8
BAR1 (Bar)  4
BAR2 (Bar)  6
BAR3 (Bar) 11

Edit: code for an iterator iterating multiple collections

For keeping two separate lookup maps and iterate over both at once, you may want to have code like the following:

    Map<Foo, Integer> fooLookupTable = new EnumMap<>(Map.of(Foo.FOO1, 2, Foo.FOO2, 3, Foo.FOO3, 8));
    Map<Bar, Integer> barLookupTable = new EnumMap<>(Map.of(Bar.BAR1, 4, Bar.BAR2, 6, Bar.BAR3, 11));
    
    IterableUnion<Entry<? extends Enum<?>, Integer>> union
            = new IterableUnion<>(fooLookupTable.entrySet(), barLookupTable.entrySet());
    for (Map.Entry<? extends Enum<?>, Integer> e : union) {
        System.out.format("%4s (%3s) %2d%n",
                e.getKey(), e.getKey().getClass().getSimpleName(), e.getValue());
    }

Output is:

FOO1 (Foo)  2
FOO2 (Foo)  3
FOO3 (Foo)  8
BAR1 (Bar)  4
BAR2 (Bar)  6
BAR3 (Bar) 11

I am using an IterableUnion class like the following. I think it has got some generality to it: you can iterate more than two collections or other things implementing Iterable, and the element type may vary.

public class IterableUnion<E> implements Iterable<E> {

    private class UnionIterator implements Iterator<E> {
        private int index = 0;
        private Iterator<? extends E> iteratorForIndex;

        UnionIterator() {
            if (iterables.length > 0) {
                iteratorForIndex = iterables[0].iterator();
            }
        }
        
        @Override
        public boolean hasNext() {
            if (index == iterables.length) {
                return false;
            }
            while (true) {
                if (iteratorForIndex.hasNext()) {
                    return true;
                }
                index++;
                if (index == iterables.length) {
                    return false;
                }
                iteratorForIndex = iterables[index].iterator();
            }
        }

        @Override
        public E next() {
            if (! hasNext()) {
                throw new NoSuchElementException();
            }
            return iteratorForIndex.next();
        }
        
    }
    
    private final Iterable<? extends E>[] iterables;

    @SafeVarargs
    public IterableUnion(Iterable<? extends E>... iterables) {
        this.iterables = iterables;
    }
    
    @Override
    public Iterator<E> iterator() {
        return new UnionIterator();
    }

}
Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
  • 1
    Thanks a lot for your anwer, I didn't think of using Enum as a map key type and even though I don't need to use more enums as keys in this specific use case I really like the idea and I reckon that it will definitely come in handy for future projects Also, the idea of keeping maps separate and implementing an Iterator really gives food for thought, I think it's the solution I'm going for – blvck_mvgic_dot_exe Mar 23 '21 at 15:53
  • @blvck_mvgic_dot_exe "I didn't think of using Enum as a map key type" - Your original question says "using a Map and and have both enum values as keys" which sounds like you wanted to use an Enum as a map key. – Jeff Scott Brown Mar 23 '21 at 16:23
  • @JeffScottBrown yes and no, I wanted a way to constrain the key type to those two specific enums instead of using ANY enum as a key but that's definitely an improvement from the Object key type which is not type safe at all – blvck_mvgic_dot_exe Mar 23 '21 at 19:09
  • " I wanted a way to constrain the key type to those two specific enums instead of using ANY enum as a key" - Without writing your own Map implementation to impose that, I don't think that is possible in Java. I don't think you will be able to create a situation where that constraint can be imposed using generics, even if you introduce a common interface. – Jeff Scott Brown Mar 23 '21 at 19:24
  • @JeffScottBrown A common interface would prevent an enum not implementing the interface to be added to the map (but wold bit prevent it being looked up there). You may say that someone wanting to add a third enum could trivially implement the interface, but no one would be likely to do it by simple mistake. Tape safety is there to prevent accidental errors, not to prevent fraud. – Ole V.V. Mar 23 '21 at 19:31
  • " A common interface would prevent an enum not implementing the interface to be added to the map " - No, it wouldn't, and the scenario you describe in the following sentence is one example of why. I was only pointing out that there is no way to use generics to impose the constraint, and I believe that is correct. Using a common interface has benefits, but it doesn't impose a constraint that only these 2 enums could be used as keys. That is all I was saying. – Jeff Scott Brown Mar 23 '21 at 19:45