1

I was wondering if there exists any Map<K, V> implementation that takes a combination of 2 or more enums for its key.

I'll provide a simplified example.

Say I have two enums DiceEyes and CardSuit:

public enum DiceEyes {
    ONE, TWO, THREE, FOUR, FIVE, SIX
}

public enum CardSuit {
    HEARTS, DIAMONDS, SPADES, CLUBS
}

Now I have 6 * 4 = 24 images from which I would like to show exactly one, based on what a user selected in a GUI - he selects a DiceEyes and a CardSuit.

I have no control over the filenames of the images. So I cannot be smart by naming the images <DiceType>-<CardSuit>-.png or whatsoever. Therefore I need a mapping of the combination of a DiceType and a CardSuit to the filename.

I have already thought about a solution that operates on an EnumMap of EnumMaps:

EnumMap<DiceType, EnumMap<CardSuit, String>> ... = new ...;

But that looks overcomplicated to me. A colleague would then think that DiceType is more or less important than a CardSuit, looking at the order of the types.

Introducing a wrapper class for just this purpose also seems like overkill to me. When I have to use it in a Map, I would need to implement equals() and hashCode() which for this purpose seems just too much. With an enum you are already guaranteed of object equality, so I would like to stay with the enums.

How can this be achieved without introducing a specific class or too much overhead?

Timmos
  • 3,215
  • 2
  • 32
  • 40
  • You would have to create a wrapper class that combined the dice and cards together. Like a composite key in a database. – CodeChimp Mar 03 '14 at 14:36
  • 3
    `Introducing a wrapper class for just this purpose also seems like overkill to me` Wrapper class seems the easiest solution here. `equals` and `hashCode` would be very easy to implement. – Pshemo Mar 03 '14 at 14:36
  • 3
    "Introducing a wrapper class for just this purpose also seems like overkill to me. When I have to use it in a Map, I would need to implement equals() and hashCode() which for this purpose seems just too much." - but writing this long post is more suitable for you? – Smutje Mar 03 '14 at 14:37
  • 1
    @Smutje This post isn't getting committed to the VCS, deployed to production, or maintained by future programmers. – Marko Topolnik Mar 03 '14 at 14:37
  • 1
    It's quite horrible to do like that but maybe using a toString on your enum values could be enough (for example you would get the keys ONE-HEARTS, ONE-DIAMONDS and so on) – Julien Mar 03 '14 at 14:38
  • @Pshemo Indeed, given that I'm working on enum types, I can just rely on the `==` operator. – Timmos Mar 03 '14 at 14:39
  • I would go with wrapper class used as map key as well. Its purpose is clear for all code readers. I do not like any magic code. – Leos Literak Mar 03 '14 at 14:40
  • @MarkoTopolnik even worse! If one introduces a wrapper class `DiceEyesCartSuitPermutation` it nearly speaks for itself, even if one has one additional class (which can be immutable and therefore easy to understand), but with any fancy high-semantically difficult solution, future programmers have to understand strange ideas over and over again, wasting more time understanding then would took him writing this "overhead" wrapper. – Smutje Mar 03 '14 at 14:40
  • @Smutje I can learn something from what others think. It's also because my use case is working with 2 enums, so the set of possible combinations is limited by the multiplication of their sizes. That's still very different from working with 2 classes. – Timmos Mar 03 '14 at 14:41
  • Create only one enum? – Jonathan Drapeau Mar 03 '14 at 14:43
  • 3
    @Timmos this has *nothing* to do with the object/enum size you contain in a wrapper, but it has everything to do with readability and understandability. Programmers spend way more time understanding colleagues stuff then writing code. – Smutje Mar 03 '14 at 14:43
  • @Smutje You would really call your class `DiceEyesCartSuitPermutation`? Apart from being a mouthful, your claim that it "speaks for itself" is quite a stretch. – Marko Topolnik Mar 03 '14 at 14:47

6 Answers6

4

I would create a Pair<A, B> class and then you can use a map on that:

Map<Pair<DiceEyes, CardSuit>, String> = new HashMap<>();
skiwi
  • 66,971
  • 31
  • 131
  • 216
3

I've been known to implement an ArrayKey object which allows me to use an array as a key in maps etc.

public class ArrayKey {
   private Object[] array;
   public ArrayKey(Object... array) {
       this.array = array;
   }
   public boolean equals(Object other) { ... }
   public int hashCode() { ...}
   public Object[] getArray() { return array; }
}

Then you could do:

Map<ArrayKey, String> map = new HashMap();
map.put(new ArrayKey(DiceEyes.ONE,CardSuit.HEARTS), "/path/to/file.jpg");
map.get(new ArrayKey(DiceEyes.ONE,CardSuit.HEARTS));

Of course, this could be extended to create an ArrayKeyMap for convenience

public class ArrayKeyMap<T> {
   private Map<ArrayKey, T> map = new HashMap();
   public T put(Object... key, T value) {
      return map.put(new ArrayKey(key), value);
   }
   public T get(Object... key) {
      return map.get(new ArrayKey(key));
   }
}
lance-java
  • 25,497
  • 4
  • 59
  • 101
  • Basically, this is an untyped tuple. – Marko Topolnik Mar 03 '14 at 14:43
  • yes, in this instance. But it's a general purpose class where you can use an arbitrary number of arguments as a key. – lance-java Mar 03 '14 at 14:46
  • A tuple is a general-purpose immutable container of an arbitrary length. But a tuple is also typically *typed*. – Marko Topolnik Mar 03 '14 at 14:48
  • Ah, yes. I was considering a tuple to have 2 elements but you're meaning an n-tuple. – lance-java Mar 03 '14 at 14:50
  • Ah, I've seen 2-tuple and tuple used interchangably... Perhaps the definition means n-tuple but there's definitely some ambiguity out there when people discuss tuples. – lance-java Mar 03 '14 at 14:56
  • Usually not---2-tuples are called *pairs*---but you may have had exposure to some non-standard usage of the term. – Marko Topolnik Mar 03 '14 at 14:59
  • I'd be interested to see how a n-tuple can be typed in java since the number of generic types must be known at compile time. Obviously if they are all the same type then it can be done. – lance-java Mar 03 '14 at 15:03
  • Exactly: there's a type per tuple. Java would be much nicer if it had some convenient syntax for it. – Marko Topolnik Mar 03 '14 at 15:41
  • Of course, you could always [do as jOOQ does](http://www.jooq.org/javadoc/3.3.x/org/jooq/Record10.html) ;) – lance-java Mar 03 '14 at 15:49
  • Ah, you also use jOOQ? Yes, that's pretty much the standard way how this is done. Scala's implementation classes look the same, but you don't have to face that molass due to type inference. – Marko Topolnik Mar 03 '14 at 17:02
1

What about using the method Enum.name() to create Strings as keys?

String key=DiceEyes.ONE.name()+CardSuit.HEARTS.name(); //"ONEHEARTS".
Pablo Lozano
  • 10,122
  • 2
  • 38
  • 59
1

Unfortunately this kind of flexibility is not possible. Map is defined as <K, V> and basically that's that. The only ways are what you've presented: create a key object or use a map of maps. (Alternatively I suppose you could also write your own Map implementation.)

Since you are using enum it's actually convenient to make a wrapper object, but I'd recommend a slight twist on it. You can make it immutable and only have a single instance for each pair. This will avoid object creation. You can override equals if you want but since there will only be one of each pair, identity would be sufficient. You can use == to compare them which also means you could use IdentityHashMap.

public final class DiceCardPair {
    private final DiceEyes eyes;
    private final CardSuit suit;

    private DiceCardPair(
        final DiceEyes eyes,
        final CardSuit suit
    ) {
        this.eyes = eyes;
        this.suit = suit;
    }

    @Override
    public int hashCode() {
        // very compact hash code
        // make shift bigger if more constants
        return eyes.ordinal() | suit.ordinal() << 3;
    }

    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof DiceCardPair)) {
            return false;
        }

        DiceCardPair pair = (DiceCardPair)obj;
        return eyes == pair.eyes && suit == pair.suit;
    }

    private static final DiceCardPair[][] values;
    static {
        DiceEyes[] eyes = DiceEyes.values();
        CardSuit[] suits = CardSuit.values();

        values = new DiceCardPair[eyes.length][suits.length];
        for(int i = 0; i < values.length; i++) {
            for(int k = 0; k < values[i].length; k++) {
                values[i][k] = new DiceCardPair(
                    eyes[i], suits[k]
                );
            }
        }
    }

    public static DiceCardPair valueOf(DiceEyes e, CardSuit s) {
        return values[e.ordinal()][s.ordinal()];
    }
}

You don't even really have to use the instances of a class like this directly either, for example:

Value v = map.get(DiceCardPair.valueOf(DiceEyes.ONE, CardSuit.SPADES));
if(v == null) {
    map.put(DiceCardPair.valueOf(DiceEyes.ONE, CardSuit.SPADES), new Value());
}
Radiodef
  • 37,180
  • 14
  • 90
  • 125
0

The actual question

How can this be achieved without introducing a specific class or too much overhead?

was already answered: Not at all.

So although it was explicitly excluded from the question, I'd like to throw in a generic Pair class that can come in handy for cases like this one.

final class Pair<T>
{
    public static <T, TT extends T> Pair<T> of(TT t0, TT t1)
    {
        return new Pair<T>(t0, t1);
    }

    private final T first;
    private final T second;

    private Pair(T first, T second)
    {
        this.first = first;
        this.second = second;
    }


    @Override
    public String toString()
    {
        return "(" + first + "," + second + ")";
    }

    @Override
    public boolean equals(Object object)
    {
        if(object instanceof Pair)
        {
            Pair<?> other = (Pair<?>) object;
            return 
                equal(this.first, other.first) && 
                equal(this.second, other.second);
        }
        return false;
    }

    private static <U> boolean equal(U a, U b)
    {
        if (a == null)
        {
            return b == null;
        }
        return a.equals(b);
    }


    @Override
    public int hashCode()
    {
        return
            ((first == null) ? 0 : first.hashCode()) ^ 
            ((second == null) ? 0 : second.hashCode());
    }

    public T getFirst()
    {
        return first;
    }

    public T getSecond()
    {
        return second;
    }
}
Marco13
  • 53,703
  • 9
  • 80
  • 159
0

Use a Pair class for key. You can reuse it elsewhere, so it's not an overkill. Unfortunately, Java does not have built-in implementation like other languages (c++, c#), but there's a straight forward one in this post:

A Java collection of value pairs? (tuples?)

Community
  • 1
  • 1
Ali Cheaito
  • 3,746
  • 3
  • 25
  • 30