用多个键映射

时间:2013-10-04 08:46:04

标签: java collections multikey

我正在尝试实施像

这样的地图
Map<<key1, key2>, List<value>>

地图应包含2个键,相应的值将是一个列表。我想在同一个列表中添加记录,如果更改一个键值相等 例如,考虑以下记录

R1[key1, key2]
R2[key1, null/empty] - Key1 is equal
R3[null/empty, key2] - Key2 is equal
R4[key1, key2] - Key1 and Key2 both are equal.

所有应该插入相同的列表,如

Key = <Key1,Key2> 
Value = <R1, R2, R3, R4>

我无法使用Guava tablecommons MulitKeyMap(不要只为此包含整个库)。

我尝试实现一个类(我可以用作键),它将key1key2作为属性,但实现了一个不考虑key1和key2的有效哈希码似乎有点(或可能很多)棘手

public class Key {
    private int key1;
    private int key2;

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        // Cant include key1 and key2 in hashcode 
       /* result = prime * result + key1;
        result = prime * result + key2;*/
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Key other = (Key) obj;
        if(key2 and other.key2 both not blank/null){ // pseudo code
        if (key2 == other.key2)
            return true;
        }
        if(key1 and other.key1 both not blank/null){ //pseudo code
        if (key1 == other.key1)
            return true;
        }
        return true;
    }

}

如果我对所有人使用相同的哈希码,它会起作用,但是由于我有数千条记录,它会影响性能。


编辑:
我无法使用嵌套地图,如

Map<key1, Map< key2, List<value>>>

由于某些记录可能只有一个密钥。

  R1[key1, key2]     - Have both keys
  R2[key1, null/empty] - Key1 is equal
  R3[null/empty, key2] - Key1 is missing and key2 is equal

这里R3没有key1,因此无法插入与R1和R2相同的位置


编辑2:

我也希望维持互动顺序。

9 个答案:

答案 0 :(得分:4)

根据定义,地图每个值有1个键。

你可以有一张地图地图,或者你的钥匙可能是一个有2个字段的对象,但是这个地方尽可能接近。

地图地图:

Map myMap<key, Map<otherkey, value>>

自定义对象

public class MapKey {
    public Object keyFirstPart;
    public Object keySecondPart;

    // You'll need to implement equals, hashcode, etc
}

Map myyMap <MapKey, value>

答案 1 :(得分:1)

使用TreeMap,这样就可以为CustomKey类而不是Hashcode使用自定义比较器。

TreeMap<CustomKey, List<value>> map = new TreeMap<CustomKey, List<value>>(myComparator);

eta:您可以使CustomKey类实现Comparable

,而不是创建比较器类

答案 2 :(得分:1)

如果需要像HashMap一样的行为,我会创建两个地图,并且在处理集合时也能发挥作用(同样,我建议使用集合......):

public class MyMap<K1, K2, V> {
  Map<K1, Collection<V>> map1;
  Map<K2, Collection<V>> map2;

  //have to add to both lists
  put(K1 k1, K2 k2, V v) {
     addToCollection(map1, k1, v);
     addToCollection(map2, k2, v);
  }

  //notice T param
  <T> void addToCollection(Map<T, Collection<V>> map, T key, V value ) {
     Collection<V> collection= map.get(key);
     if(collection==null) {
       collection= new HashSet<V>();
       map.put(key, collection);
     }
     collection.add(value );
  }

  public Collection<V> get(K1 k1, K2 k2) {
     Collection<V> toReturn = new HashSet<V>();
     Collection<V> coll1 = map1.get(k1);
     if(coll1!=null) {
        toReturn.addAll(coll1);
     }

     Collection<V> coll2 = map2.get(k2);
     if(coll2!=null) {
        toReturn.addAll(coll2);
     }

     return toReturn;
  }
}

答案 3 :(得分:1)

试试这个......

为Map

的键创建一个类
public class MapKey {
private Object key1;
private Object key2;

@Override
public boolean equals(Object object) {
    boolean equals = false;
    if (((MapKey) object).key1 == null && ((MapKey) object).key2 == null) {
        equals = true;
    }
    if (((MapKey) object).key1.equals(this.key1) && ((MapKey) object).key2.equals(this.key2)) {
        equals = true;
    }
    if (((MapKey) object).key1 == null && ((MapKey) object).key2.equals(this.key2)) {
        equals = true;
    }
    if (((MapKey) object).key1.equals(this.key1) && ((MapKey) object).key2 == null) {
        equals = true;
    }
    return equals;

}

@Override
public int hashCode() {
    return 1;
}

public Object getKey1() {
    return key1;
}

public void setKey1(Object key1) {
    this.key1 = key1;
}

public Object getKey2() {
    return key2;
}

public void setKey2(Object key2) {
    this.key2 = key2;
}
}

在上面的类中,您可以根据需要修改key1和key2的DataTypes。这是执行所需逻辑的主类

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MapWithTwoKeys {
private static final Map<MapKey, List<Object>> mapWithTwoKeys = new HashMap<MapKey,     List<Object>>();

public static void main(String[] args) {

// Create first map entry with key <A,B>.
MapKey mapKey1 = new MapKey();
mapKey1.setKey1("A");
mapKey1.setKey2("B");

List<Object> list1 = new ArrayList<Object>();
list1.add("List1 Entry");

put(mapKey1, list1);

// Create second map entry with key <A,B>, append value.
MapKey mapKey2 = new MapKey();
mapKey2.setKey1("A");
mapKey2.setKey2("B");

List<Object> list2 = new ArrayList<Object>();
list2.add("List2 Entry");

put(mapKey2, list2);

// Create third map entry with key <A,>.
MapKey mapKey3 = new MapKey();
mapKey3.setKey1("A");
mapKey3.setKey2("");

List<Object> list3 = new ArrayList<Object>();
list3.add("List3 Entry");

put(mapKey3, list3);

// Create forth map entry with key <,>.
MapKey mapKey4 = new MapKey();
mapKey4.setKey1("");
mapKey4.setKey2("");

List<Object> list4 = new ArrayList<Object>();
list4.add("List4 Entry");

put(mapKey4, list4);

// Create forth map entry with key <,B>.
MapKey mapKey5 = new MapKey();
mapKey5.setKey1("");
mapKey5.setKey2("B");

List<Object> list5 = new ArrayList<Object>();
list5.add("List5 Entry");

put(mapKey5, list5);

for (Map.Entry<MapKey, List<Object>> entry : mapWithTwoKeys.entrySet()) {
System.out.println("MapKey Key: <" + entry.getKey().getKey1() + ","
        + entry.getKey().getKey2() + ">");
System.out.println("Value: " + entry.getValue());
System.out.println("---------------------------------------");
}
}

/**
 * Custom put method for the map.
 * @param mapKey2 (MapKey... the key object of the Map).
 * @param list (List of Object... the value of the Map).
*/
private static void put(MapKey mapKey2, List<Object> list) {
if (mapWithTwoKeys.get(mapKey2) == null) {
    mapWithTwoKeys.put(mapKey2, new ArrayList<Object>());
}
mapWithTwoKeys.get(mapKey2).add(list);
}
}

代码非常简单易懂。如果这符合您的要求,请告诉我。

答案 4 :(得分:1)

我认为以下解决方案对您有用 - 我使用 MyKey.java 对象作为HashMap的键。它包含密钥和哈希码。哈希代码将用于标识您在问题中列出的不同密钥组合的值列表。首次注册两个密钥时会生成此哈希码。它存储在每个密钥中,因此即使其中任何一个密钥为空,您也将获得相同的哈希代码。

MultiKeyMap.java =&gt;扩展HashMap并覆盖'put'和'get'方法。 populateHashKey() - 此方法将为您想要的键的不同组合生成/返回相同的哈希码。

注意:插入顺序由Arraylist维护。此外,每个组合键的所有值都将存储在Map中的相同列表中。

package test.map;

public class MyKey {

    private String myKey1;
    private String myKey2;
    private int hashKey;

    public MyKey(String key1, String key2) {
       this.myKey1 = key1;
       this.myKey2 = key2;
    }
    /**
     * @return the myKey1
     */
    public String getMyKey1() {
        return this.myKey1;
    }
    /**
     * @param tmpMyKey1 the myKey1 to set
     */
    public void setMyKey1(String tmpMyKey1) {
        this.myKey1 = tmpMyKey1;
    }
    /**
     * @return the myKey2
     */
    public String getMyKey2() {
        return this.myKey2;
    }
    /**
     * @param tmpMyKey2 the myKey2 to set
     */
    public void setMyKey2(String tmpMyKey2) {
        this.myKey2 = tmpMyKey2;
    }
    /**
     * Returns the hash key.
     */
    @Override
    public int hashCode() {
        return this.hashKey;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        MyKey other = (MyKey) obj;
        if(checkEqual(this.myKey1, other.myKey1) 
                || checkEqual(this.myKey2, other.myKey2)) {
            return true;
        }

        return false;
    }
    /*
     * Checks whether key1 equals key2.
     */
    private boolean checkEqual(String key1, String key2) {
        if(key1 != null && key2 != null) {
            return key1.equals(key2);
        }
        return false;
    }
    /**
     * @return the hashKey
     */
    public int getHashKey() {
        return this.hashKey;
    }
    /**
     * @param tmpHashKey the hashKey to set
     */
    public void setHashKey(int tmpHashKey) {
        this.hashKey = tmpHashKey;
    }
}

MultiKeyMap.java -

  package test.map;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MultiKeyMap extends HashMap<MyKey, List<String>>{

    private static final long serialVersionUID = 3523468186955908397L;
    Map<String, Integer> hashKeyMap = new HashMap<String, Integer>();

    /*
     * Adds single value in the List of values against the key
     */
    public List<String> addValue(MyKey tmpKey, String tmpValue) {
        populateHashKey(tmpKey);
        List<String> orgValue = null;
        if(tmpKey.getHashKey() != -1) {
            orgValue = super.get(tmpKey);
            if(orgValue == null) {
                orgValue = new ArrayList<String>();
                super.put(tmpKey, orgValue);
            }
            orgValue.add(tmpValue);
        }
        return orgValue;
    }

    @Override
    public List<String> put(MyKey tmpKey, List<String> tmpValue) {
        populateHashKey(tmpKey);
        List<String> orgValue = null;
        if(tmpKey.getHashKey() != -1) {
            orgValue = super.get(tmpKey);
            if(orgValue == null) {
                orgValue = new ArrayList<String>();
                super.put(tmpKey, orgValue);
            }
            orgValue.addAll(tmpValue);
        }
        return orgValue;
    }

    @Override
    public List<String> get(Object tmpKey) {
        if(!(tmpKey instanceof MyKey)) {
            return null;
        }
        MyKey key = (MyKey) tmpKey;
        populateHashKey(key);
        return super.get(key);
    }
    /**
     * Populates the hashKey generated for the MyKey combination. If the both Key1 and Key 2 are not null and its hash key is not generated
     * earlier then it will generate the hash key using both keys and stores it in class level map 'hashKeyMap' against both keys.
     * @param tmpKey
     */
    public void populateHashKey(MyKey tmpKey) {
        int hashKey = -1;
        if(tmpKey.getMyKey1() != null && this.hashKeyMap.containsKey(tmpKey.getMyKey1()+"_")) {
            hashKey = this.hashKeyMap.get(tmpKey.getMyKey1()+"_");
        } else if(tmpKey.getMyKey2() != null && this.hashKeyMap.containsKey("_"+tmpKey.getMyKey2())) {
            hashKey = this.hashKeyMap.get("_"+tmpKey.getMyKey2());
        }
        /*
         * Assumption - While insertion you will always add first value with Key1 and Key2 both as not null. Hash key will be build only
         * when both keys are not null and its not generated earlier.
         */
        if(hashKey == -1 && tmpKey.getMyKey1() != null && tmpKey.getMyKey2() != null) {
            hashKey = buildHashKey(tmpKey);
            this.hashKeyMap.put(tmpKey.getMyKey1()+"_", hashKey);
            this.hashKeyMap.put("_"+tmpKey.getMyKey2(), hashKey);
        }
        tmpKey.setHashKey(hashKey);
    }

    public int buildHashKey(MyKey tmpKey) {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((tmpKey.getMyKey1() == null) ? 0 : tmpKey.getMyKey1().hashCode());
        result = prime * result + ((tmpKey.getMyKey1() == null) ? 0 : tmpKey.getMyKey1().hashCode());
        return result;
    }
}

测试课 -

import test.map.MultiKeyMap;
import test.map.MyKey;

public class TestMultiKeyMap {

    public static void main(String[] args) {

        System.out.println("=====Add values for each type of key==========");
        MyKey regKey = new MyKey("Key1", "Key2");
        MultiKeyMap myMap = new MultiKeyMap();
        //Register the MyKey having both keys as NOT null.
        System.out.println("Entry 1:"+myMap.addValue(regKey, "Key Reg"));

        MyKey key1 = new MyKey("Key1", null);
        //Add value against MyKey with only Key2
        System.out.println("Entry 2:"+myMap.addValue(key1, "Key1"));

        MyKey key2 = new MyKey(null, "Key2");
        //Add value against MyKey with only Key1
        System.out.println("Entry 3:"+myMap.addValue(key2, "Key2"));

        MyKey bothKey = new MyKey("Key1", "Key2");
        //Add value against MyKey with only Key1
        System.out.println("Entry 4:"+myMap.addValue(bothKey, "both keys"));

        System.out.println("=====Retrieve values for each type of key==========");
        MyKey getKey1 = new MyKey("Key1", null);
        System.out.println("Values for Key1:"+myMap.get(getKey1));

        MyKey getKey2 = new MyKey(null, "Key2");
        System.out.println("Values for Key2:"+myMap.get(getKey2));

        MyKey getBothKey = new MyKey("Key1", "Key2");
        System.out.println("Values for both keys:"+myMap.get(getBothKey));
    }

}

输出 -

=====Add values for each type of key==========
Entry 1:[Key Reg]
Entry 2:[Key Reg, Key1]
Entry 3:[Key Reg, Key1, Key2]
Entry 4:[Key Reg, Key1, Key2, both keys]
=====Retrieve values for each type of key==========
Values for Key1:[Key Reg, Key1, Key2, both keys]
Values for Key2:[Key Reg, Key1, Key2, both keys]
Values for both keys:[Key Reg, Key1, Key2, both keys]

答案 5 :(得分:0)

只检查key1的哈希码,如果匹配,那么我们无论如何都要测试equals方法。

@Override
    public int hashCode() {
        return key1.hashCode();
    }

@Override
public boolean equals(Object obj) {
Key k = (Key)obj;
// Generic equals code goes here
    if(this.key1.equals(k.key1) && this.key2.equals(k.key2) )
        return true;
    return false;
}

答案 6 :(得分:0)

基本上,实现这一目标的想法是通过值本身映射键。

所以你可以有一个内部地图来做这个(这里我有一组键而不是2)。

Map<V, Set<K>> keySetMap = new HashMap<V, Set<K>>();

所以你的Map实现看起来像这样:

public class MultiKeyMap<K, V> extends LinkedHashMap<K, V> {
    private static final long serialVersionUID = 1L;

    private Map<V, Set<K>> keySetMap = new HashMap<V, Set<K>>();

    @Override
    public V put(K key, V value) {
        V v = null;

        Set<K> keySet = keySetMap.get(value);
        if(keySet == null) {
            keySet = new LinkedHashSet<K>();
            keySetMap.put(value, keySet);
        }

        keySet.add(key);
        v = super.put(key, value);

        // update the old keys to reference the new value
        Set<K> oldKeySet =  keySetMap.get(v);
        if(oldKeySet != null) {
            for(K k : oldKeySet) {
                super.put(k, value);
            }
        }

        return v;
    }
}

这适用于简单(不可变)对象:

@Test
public void multiKeyMapString() {
    MultiKeyMap<String, String> m = new MultiKeyMap<String, String>();

    m.put("1", "A");
    m.put("2", "B");

    for(Entry<String, String> e : m.entrySet()) {
        System.out.println("K=" + e.getKey() + ", V=" + e.getValue().toString());
    }

    m.put("3", "A");

    System.out.println("----");
    for(Entry<String, String> e : m.entrySet()) {
        System.out.println("K=" + e.getKey() + ", V=" + e.getValue().toString());
    }

    m.put("4", "C");

    System.out.println("----");
    for(Entry<String, String> e : m.entrySet()) {
        System.out.println("K=" + e.getKey() + ", V=" + e.getValue().toString());
    }

    m.put("3", "D");

    System.out.println("----");
    for(Entry<String, String> e : m.entrySet()) {
        System.out.println("K=" + e.getKey() + ", V=" + e.getValue().toString());
    }

    System.out.println("----");
    System.out.println("values=" + m.values());

    System.out.println();
    System.out.println();
}

通过上面的测试,输出看起来像

K=1, V=A
K=2, V=B
----
K=1, V=A
K=2, V=B
K=3, V=A
----
K=1, V=A
K=2, V=B
K=3, V=A
K=4, V=C
----
K=1, V=D
K=2, V=B
K=3, V=D
K=4, V=C
----
values=[D, B, C]

正如您在上一个输出中看到的那样,键1现在映射了值D,因为3之前映射的值与1映射的值相同在之前的步骤。

但是当你想在地图中放置一个列表(或任何可变对象)时会变得棘手,因为如果你改变列表(添加/删除一个元素),那么列表将有另一个hashCode作为一个用于映射以前的键:

@Test
public void multiKeyMapList() {
    List<String> l = new ArrayList<String>();
    l.add("foo");
    l.add("bar");
    MultiKeyMap<String, List<String>> m = new MultiKeyMap<String, List<String>>();

    m.put("1", l);
    m.put("2", l);

    for(Entry<String, List<String>> e : m.entrySet()) {
        System.out.println("K=" + e.getKey() + ", V=" + e.getValue().toString());
    }

    m.get("1").add("foobar");
    m.put("3", l);

    System.out.println("----");
    for(Entry<String, List<String>> e : m.entrySet()) {
        System.out.println("K=" + e.getKey() + ", V=" + e.getValue().toString());
    }

    l = new ArrayList<String>();
    l.add("bla");

    m.put("4", l);

    System.out.println("----");
    for(Entry<String, List<String>> e : m.entrySet()) {
        System.out.println("K=" + e.getKey() + ", V=" + e.getValue().toString());
    }

    m.put("3", l);

    System.out.println("----");
    for(Entry<String, List<String>> e : m.entrySet()) {
        System.out.println("K=" + e.getKey() + ", V=" + e.getValue().toString());
    }

    System.out.println("----");
    System.out.println("values=" + m.values());
}

上面的测试将输出如下内容:

K=1, V=[foo, bar]
K=2, V=[foo, bar]
----
K=1, V=[foo, bar, foobar]
K=2, V=[foo, bar, foobar]
K=3, V=[foo, bar, foobar]
----
K=1, V=[foo, bar, foobar]
K=2, V=[foo, bar, foobar]
K=3, V=[foo, bar, foobar]
K=4, V=[bla]
----
K=1, V=[foo, bar, foobar]
K=2, V=[foo, bar, foobar]
K=3, V=[bla]
K=4, V=[bla]
----
values=[[foo, bar, foobar], [bla]]

如您所见,12映射的值尚未更新,只需将密钥3转换为映射另一个值。原因是hashCode的{​​{1}}结果与[foo, bar]的{​​{1}}结果不同,导致[foo, bar, foobar]无法返回正确的结果。要克服这一点,您需要通过与实际值进行比较来获得一组键。

Map#get

现在再次运行两个测试会得到以下输出:

对于简单(不可变)对象

public class MultiKeyMap<K, V> extends LinkedHashMap<K, V> {
    private static final long serialVersionUID = 1L;

    private Map<V, Set<K>> keySetMap = new HashMap<V, Set<K>>();

    @Override
    public V put(K key, V value) {
        V v = null;

        Set<K> keySet = keySetMap.get(value);
        if (keySet == null) {
            keySet = new LinkedHashSet<K>();
            keySetMap.put(value, keySet);
        }

        keySet.add(key);
        v = super.put(key, value);

        // update the old keys to reference the new value
        for (K k : getKeySetByValue(v)) {
            super.put(k, value);
        }

        return v;
    }

    @Override
    public Collection<V> values() {
        // distinct values
        return new LinkedHashSet<V>(super.values());
    }

    private Set<K> getKeySetByValue(V v) {
        Set<K> set = null;
        if (v != null) {
            for (Map.Entry<V, Set<K>> e : keySetMap.entrySet()) {
                if (v.equals(e.getKey())) {
                    set = e.getValue();
                    break;
                }
            }
        }
        return set == null ? Collections.<K> emptySet() : set;
    }
}

对于可以更改的对象

K=1, V=A
K=2, V=B
----
K=1, V=A
K=2, V=B
K=3, V=A
----
K=1, V=A
K=2, V=B
K=3, V=A
K=4, V=C
----
K=1, V=D
K=2, V=B
K=3, V=D
K=4, V=C
----
values=[D, B, C]

我希望这可以帮助您找到实现Map的方法。您可以代替扩展现有实现实现K=1, V=[foo, bar] K=2, V=[foo, bar] ---- K=1, V=[foo, bar, foobar] K=2, V=[foo, bar, foobar] K=3, V=[foo, bar, foobar] ---- K=1, V=[foo, bar, foobar] K=2, V=[foo, bar, foobar] K=3, V=[foo, bar, foobar] K=4, V=[bla] ---- K=1, V=[bla] K=2, V=[bla] K=3, V=[bla] K=4, V=[bla] ---- values=[[bla]] 接口,以便您可以为其合同提供所有方法的实现,并将您选择的实现作为成员来处理实际映射。

答案 7 :(得分:0)

请使用Apache Commons Lang库中的优秀助手类EqualsBuilder和HashCodeBuilder。一个例子:

public class Person {
    private String name;
    private int age;
    // ...

    public int hashCode() {
        return new HashCodeBuilder(17, 31). // two randomly chosen prime numbers
            // if deriving: appendSuper(super.hashCode()).
            append(name).
            append(age).
            toHashCode();
    }

    public boolean equals(Object obj) {
        if (obj == null)
            return false;
        if (obj == this)
            return true;
        if (!(obj instanceof Person))
            return false;

        Person rhs = (Person) obj;
        return new EqualsBuilder().
            // if deriving: appendSuper(super.equals(obj)).
            append(name, rhs.name).
            append(age, rhs.age).
            isEquals();
    }
}

Source

答案 8 :(得分:-1)

创建另一个保存关系key-&gt; intermediateKey的Map。中间密钥可以是GUID或其他自动生成并保证唯​​一的东西。

  Map<String, GUID> first = new HashMap<String, GUID>();
  first.put(key1, guid1);
  first.put(key2, guid1);

  Map<GUID, ValueType> second = new HashMap<GUID, ValueType>();
  second.put(guid1, value1);

或者(虽然我发现它更复杂,灵活性更低),你可以玩钥匙。如果key1.equals(key2)(以及key2.equals(key1)&amp;&amp;(key1.hashCode()== key2.hashCode ) then Map.get(key1)will return the same value than Map.get (KEY2)`。