5

I want to create collections which may contain duplicate values, in no particular order.

In other words:

{ 1, 1, 2 } == { 2, 1, 1 } == { 1, 2, 1 }

In fact, I want to have a Set of these collections, so if I try to add both { 1, 1, 2 } and { 2, 1, 1 }, the second .add() will not actually do anything.

Is there a standard collection that already behaves this way?

If I understand correctly:

  • ArrayList allows for duplicate values, but has a fixed order
  • HashSet allows for the order to be arbitrary but no duplicate values
  • TreeSet ensures that the order is constant, but allows no duplicate values

Is there a collection that I have overlooked that allows for both duplicate values and either an arbitrary or a fixed order, so that two collections with the same elements are consider equal?


@asteri asked about my use case. In a game, I have blocks of different lengths which can be laid end to end to fill a certain distance. For example, if the distance is 10, it can be filled with 2-3-5 or 5-2-3 or 3-3-4 or 3-4-3, or any number of other permutations. Depending on what blocks are available, I want to make a list of all the possible collections that would solve fill the gap.


CUSTOM SOLUTION
@sprinter suggested creating a subclass of ArrayList. @dasblinkenlight and @Dici suggested using a Map to store { Element : Count } entries. I have chosen to combine these two suggestions. Below is a subclass of TreeMap. The keys are always stored in the same order, to ensure that the hashCode() method produces the same value for instance with the same keys and values.

I've used an increment method to make it easy to add a new occurrence of a particular integer "value".

package com.example.treematch;

import java.util.Map;
import java.util.TreeMap;

public class TreeMatch<K> extends TreeMap<K, Integer> {

  @Override
  public boolean equals(Object other) {
    if (this == other) {
      return true;
    }

    if (!(other instanceof TreeMatch)) {
      return false;
    }

    TreeMatch otherMatch = (TreeMatch) other;
    if (size() != otherMatch.size()) {
      return false;
    }

    for (Object key : this.keySet()) {
        if (!otherMatch.containsKey(key)) {
            return false;
        }
    }

    for (Object key : otherMatch.keySet()) {
      if (!this.containsKey(key)) {
        return false;
      }

      if (this.get(key) != otherMatch.get(key)) {
        return false;
      }
    }

    return true;
  }

  public void increment(K key) {
    Integer value;

    if (this.containsKey(key)) {
      value = (this.get(key)) + 1;
    } else {
      value = 1;
    }

    this.put(key, value);
  }


  @Override
  public int hashCode() {
    int hashCode = 0;

    for (Map.Entry entry : this.entrySet()) {
      hashCode += entry.getKey().hashCode();
      hashCode = hashCode << 1;
      hashCode += entry.getValue().hashCode();
      hashCode = hashCode << 1;
    }

    return hashCode;
  }
}
James Newton
  • 6,623
  • 8
  • 49
  • 113
  • Mmm... interesting question. Not that I can think of, though there might be something that helps in Apache Commons or Guava. Can I ask your use case for this, out of curiosity? – asteri Jan 08 '15 at 01:49
  • Not an answer to your question, but an easy workaround would be to have them as lists, then just sort them and compare. – asteri Jan 08 '15 at 01:50
  • Interesting: [Is there a way to check if two Collections contain the same elements, independent of order?](http://stackoverflow.com/a/1565262/1762224) `->` `HashMultiset.create(c1).equals(HashMultiset.create(c2));` – Mr. Polywhirl Jan 08 '15 at 01:50
  • @asteri If you set up a class whose `equals()` method compares the sorted lists, make sure it also overrides `hashCode()` which probably should also use sorted lists to do the computation. – ajb Jan 08 '15 at 01:56
  • @ajb I wasn't even saying to encapsulate this process into any kind of class. Was just talking about having two `List`s as variable somewhere that he wants to compare. – asteri Jan 08 '15 at 02:05
  • http://www.wikiwand.com/en/Composition_over_inheritance: better implement Map instead of extending TreeMap. – sp00m Jan 08 '15 at 16:36
  • @sp00m: Will implementing Map ensure that the hashCode() method treats identical entries in different instances in the same order? – James Newton Jan 08 '15 at 16:41

6 Answers6

6

There's nothing in the Java built-in libraries, but Guava's Multiset does this.

Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
3

This type of collection is commonly known as a multiset. There are no implementations of multisets included in the standard library, but the external libraries Guava does include multisets.

user253751
  • 57,427
  • 7
  • 48
  • 90
3

You can use a Bag from Eclipse Collections, also known as a Multiset.

MutableBag<Integer> bag1 = Bags.mutable.with(1, 1, 2);
MutableBag<Integer> bag2 = Bags.mutable.with(1, 2, 1);
Assert.assertEquals(bag1, bag2);

Since your collection will only contain ints, it's better to use a primitive bag. IntHashBag is backed by int[] arrays, not Object[] arrays, uses less memory, and may be faster.

MutableIntBag intBag1 = IntBags.mutable.with(1, 1, 2);
MutableIntBag intBag2 = IntBags.mutable.with(1, 2, 1);
Assert.assertEquals(intBag1, intBag2);

Note: I am a committer for Eclipse collections.

Donald Raab
  • 6,458
  • 2
  • 36
  • 44
Craig P. Motlin
  • 26,452
  • 17
  • 99
  • 126
2

I think your better option is to wrap a Map this way :

  • keys are your values
  • values are the number of occurrences of the keys
  • equal method based on Map.keySet() equality if the number of occurrences matters, Map.equals() otherwise
Dici
  • 25,226
  • 7
  • 41
  • 82
  • I think the OP would prefer equals implementations just based on `Map.equals`, not on the `keySet`, since they appear to want the number of occurrences of each element to matter? – Louis Wasserman Jan 08 '15 at 02:02
  • @LouisWasserman Actually, reeading the question again, I think I was right since `{ 1, 1, 2 } == { 2, 1, 1 } == { 1, 2, 1 }` – Dici Jan 08 '15 at 02:10
  • Comparing with keySet would make {1,1,2}={1,2}, which I don't think is right? – Louis Wasserman Jan 08 '15 at 02:14
  • @LouisWasserman I included both of them haha, thanks again for your remark – Dici Jan 08 '15 at 02:18
2

HashMap<Integer,Integer> would be able to represent your collection if you store the value as the key and the number of times that value appears as the value in the map. For example, your collection {1, 1, 2} would be represented like this:

{{ 1 : 2 }, { 2 : 1 }}

meaning that there are two items with the value of 1 (the first pair of integers), and one item with the value of 2 (the second pair of integers).

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
1

I would suggest creating a subclass of ArrayList that overrides the equals method:

class MultiMemberList extends ArrayList {
    public boolean equals(Object other) {
        if (this == other)
            return true;
        if (!(other instanceof MultiMemberList))
            return false;
        MultiMemberList otherList = (MultiMemberList)other;
        if (size() != otherList.size())
            return false;
        return stream().distinct().allMatch(element -> countElement(element) == otherList.countElement(element));
    }

    private int countElement(Object element) {
        return stream().filter(element::equals).count();
    }
}

I haven't made this generic to keep it simple but obviously you should do that.

You also should override the hash function: the simplest thing to do would be to sort the list before calling the super class's hash.

Once you've implemented this then Set that has MultiMemberList objects added will work as you expect: it won't add a second list that equals the first according to this definition.

sprinter
  • 27,148
  • 6
  • 47
  • 78