HashMap中使用ArrayList作为键

23

能否将 ArrayList 作为 HashMap 的键添加?我想要保留双词组的频率计数。双词组是键,其频率是值。

对于像“he is”这样的每个双词组,我都为其创建一个 ArrayList 并将其插入到 HashMap 中。但我没有得到正确的输出。

public HashMap<ArrayList<String>, Integer> getBigramMap(String word1, String word2) {
    HashMap<ArrayList<String>, Integer> hm = new HashMap<ArrayList<String>, Integer>();
    ArrayList<String> arrList1 = new ArrayList<String>();
    arrList1 = getBigram(word1, word2);
    if (hm.get(arrList1) != null) {
        hm.put(arrList1, hm.get(arrList1) + 1);
    } else {
        hm.put(arrList1, 1);
    }
    System.out.println(hm.get(arrList1));
    return hm;
}


public ArrayList<String> getBigram(String word1, String word2) {
    ArrayList<String> arrList2 = new ArrayList<String>();
    arrList2.add(word1);
    arrList2.add(word2);
    return arrList2;
}
10个回答

38

你可以将ArrayList作为哈希映射中的键,但这是一个非常糟糕的想法,因为它们是可变的

如果您以任何方式更改ArrayList(或其任何元素),映射将基本上丢失,因为该键在插入时的hashCode与现在不同。

经验法则是在哈希映射中仅使用不可变数据类型作为键。建议您按Alex Stybaev所建议的那样创建类似于Bigram的类:

final class Bigram {

    private final String word1, word2;

    public Bigram(String word1, String word2) {
        this.word1 = word1;
        this.word2 = word2;
    }

    public String getWord1() {
        return word1;
    }

    public String getWord2() {
        return word2;
    }

    @Override
    public int hashCode() {
        return word1.hashCode() ^ word2.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        return (obj instanceof Bigram) && ((Bigram) obj).word1.equals(word1)
                                       && ((Bigram) obj).word2.equals(word2);
    }
}

除了它是可变的之外,如果他实际上正在使用Bigram,引入一个“Bigram”类可能是个好主意。 - Joachim Sauer

3
为什么不能使用像这样的东西:
class Bigram{
    private String firstItem;
    private String secondItem;

    <getters/setters>

    @Override
    public int hashCode(){
        ...
    }

    @Override 
    public boolean equals(){
        ...
    }
}

不要使用动态集合来处理仅有的少量项目(两个)。


2
我甚至会省略掉设置器并使其成为不可变的。在构建之后,可能没有理由更改该类的对象。 - Joachim Sauer
+1 - 实际上,这可能会节省空间,因为Bigram类不需要32位的“length”字段开销。 - Stephen C

2

根据文档

注意:如果使用可变对象作为map的键,则必须非常小心。如果在对象作为map的键时以影响equals比较的方式更改对象的值,则不指定map的行为。这个禁令的一个特殊情况是,映射不能包含自身作为键。虽然允许映射将自身作为值包含,但应极度谨慎:在这样的映射上,equals和hashCode方法不再定义良好。

当您使用可变对象作为键时,必须小心处理以确保hashCodeequals的正确性。

总之,最好使用不可变对象作为键。


1
我想到了一个解决方案。显然,并非所有情况都可用,例如超过哈希码int容量或list.clone()复杂性(如果输入列表发生更改,则键保持与预期相同,但是当List的项目是可变的时,克隆的列表具有对其项目的相同引用,这将导致更改键本身)。
import java.util.ArrayList;

public class ListKey<T> {
    private ArrayList<T> list;

    public ListKey(ArrayList<T> list) {
        this.list = (ArrayList<T>) list.clone();
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;

        for (int i = 0; i < this.list.size(); i++) {
            T item = this.list.get(i);
            result = prime * result + ((item == null) ? 0 : item.hashCode());
        }
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        return this.list.equals(obj);
    }
}

---------
    public static void main(String[] args) {

        ArrayList<Float> createFloatList = createFloatList();
        ArrayList<Float> createFloatList2 = createFloatList();

        Hashtable<ListKey<Float>, String> table = new Hashtable<>();
        table.put(new ListKey(createFloatList2), "IT WORKS!");
        System.out.println(table.get(createFloatList2));
        createFloatList2.add(1f);
        System.out.println(table.get(createFloatList2));
        createFloatList2.remove(3);
        System.out.println(table.get(createFloatList2));
    }

    public static ArrayList<Float> createFloatList() {
        ArrayList<Float> floatee = new ArrayList<>();
        floatee.add(34.234f);
        floatee.add(new Float(33));
        floatee.add(null);

        return floatee;
    }

Output:
IT WORKS!
null
IT WORKS!

你看我是怎么测试它的,但出于某些原因我仍然觉得这个解决方案不够可靠。它有效吗? - Javo

1

试一下,这会起作用的。

 public Map<List, Integer> getBigramMap (String word1,String word2){
    Map<List,Integer> hm = new HashMap<List, Integer>();
    List<String> arrList1 = new ArrayList<String>();
    arrList1 = getBigram(word1, word2);     
    if(hm.get(arrList1) !=null){
        hm.put(arrList1, hm.get(arrList1)+1);
    }
    else {
        hm.put(arrList1, 1);
    }

    System.out.println(hm.get(arrList1));
    return hm;
}

问题在于,我无法将List参数化。您能否给我一些想法?或者我应该另开一个线程讨论它。 - thetna
使用这个方法,你可以将任何类型的列表传递给map。列表可以是字符串、整数或用户自定义对象。 - vikiiii

0
当然可以。我猜问题出在你的put操作上。尝试获取bigram的键,将其增加,删除该bigram对应的条目,然后插入更新后的值即可。

0

Array不同,List可以用作HashMap的键,但这并不是一个好主意,因为我们应该始终尝试使用不可变对象作为键。

.toString()方法获取字符串表示形式在许多情况下是一个很好的键选择,因为String是一个不可变对象,可以完美地代表数组或列表。


0

是的,您可以扩展HashMap,然后只需要修改get()以在匹配时返回值,例如。

class KeyArrayHashMap extends HashMap <ArrayList<String>, ArrayList<String>> { 

 public ArrayList<String> get(String findKey) { 
    ArrayList<String> retVal = null; 
    for ( ArrayList<String> key : this.keySet() ) {
        if (key.contains(findKey)) {           
             System.out.printf( "Found key %s it's %s.\n", findKey, key.toString());
            retVal = key; 
            break; 
            }
    }

return this.get(retVal);
}
}

这段代码获取键集并迭代搜索每个键ArrayList中的字符串。代码应该运行大约是 O(n)(我们在第一次匹配时离开,但取决于你有多少键)。


-1
请查看下面的代码,以了解键是否为Map中的ArrayList,以及JVM如何处理输入: 在这里,我为TesthashCodeEquals类编写了hashCode和equals方法。
package com.msq;

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

class TesthashCodeEquals {
    private int a;
    private int b;

    public TesthashCodeEquals() {
        // TODO Auto-generated constructor stub
    }



    public TesthashCodeEquals(int a, int b) {
        super();
        this.a = a;
        this.b = b;
    }



    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }

    public int getB() {
        return b;
    }

    public void setB(int b) {
        this.b = b;
    }

    public int hashCode() {

        return this.a + this.b;
    }

    public boolean equals(Object o) {

        if (o instanceof TesthashCodeEquals && o != null) {

            TesthashCodeEquals c = (TesthashCodeEquals) o;

            return ((this.a == c.a) && (this.b == c.b));

        } else
            return false;
    }
}

public class HasCodeEquals {
    public static void main(String[] args) {

        Map<List<TesthashCodeEquals>, String> m = new HashMap<>();

        List<TesthashCodeEquals> list1=new ArrayList<>();
        list1.add(new TesthashCodeEquals(1, 2));
        list1.add(new TesthashCodeEquals(3, 4));

        List<TesthashCodeEquals> list2=new ArrayList<>();
        list2.add(new TesthashCodeEquals(10, 20));
        list2.add(new TesthashCodeEquals(30, 40));


        List<TesthashCodeEquals> list3=new ArrayList<>();
        list3.add(new TesthashCodeEquals(1, 2));
        list3.add(new TesthashCodeEquals(3, 4));



        m.put(list1, "List1");
        m.put(list2, "List2");
        m.put(list3, "List3");

        for(Map.Entry<List<TesthashCodeEquals>,String> entry:m.entrySet()){
            for(TesthashCodeEquals t:entry.getKey()){
                System.out.print("value of a: "+t.getA()+", value of b: "+t.getB()+", map value is:"+entry.getValue() );
                System.out.println();
            }
            System.out.println("######################");
        }

    }
}

.

output:

value of a: 10, value of b: 20, map value is:List2
value of a: 30, value of b: 40, map value is:List2
######################
value of a: 1, value of b: 2, map value is:List3
value of a: 3, value of b: 4, map value is:List3
######################

这将检查List中对象的数量和对象中变量的值。如果对象的数量相同且实例变量的值也相同,则它将视为重复键并覆盖该键。

现在,如果我只更改list3上对象的值

list3.add(new TesthashCodeEquals(2, 2));

那么它将打印:

 output
    value of a: 2, value of b: 2, map value is:List3
    value of a: 3, value of b: 4, map value is:List3
    ######################
    value of a: 10, value of b: 20, map value is:List2
    value of a: 30, value of b: 40, map value is:List2
    ######################
    value of a: 1, value of b: 2, map value is:List1
    value of a: 3, value of b: 4, map value is:List1
######################

这样它就可以始终检查列表中对象的数量和对象实例变量的值。

谢谢


-3

ArrayList.equals()是从java.lang.Object继承而来的 - 因此,ArrayList上的equals()与列表内容无关。

如果您想将ArrayList用作映射键,则需要覆盖equals()hashcode(),以使具有相同内容和顺序的两个ArrayList在调用equals()时返回true,并在调用hashcode()时返回相同的哈希码。

是否有任何特定原因,您必须使用ArrayList而不是简单的字符串作为键?

编辑:请忽略我,正如Joachim Sauer在下面指出的那样,我是如此错误,甚至不好笑。


8
实际上,ArrayList 使用了实现得恰当的 AbstractList.equals()。事实上,每个正确的 List 实现都要求具有符合规范的 equals()hashCode() 实现。 - Joachim Sauer
1
啊,谢谢你的纠正。我只是快速浏览了ArrayList源代码,没有往上继续查看——这是我犯下的大错误。 - mcfinnigan
提示:在Eclipse中,可以使用Ctrl-O打开大纲对话框,输入“equals”,如果没有定义,则再次按Ctrl-O查看继承成员,可以看到实际上有4个继承成员(Object,Collection,List和AbstractList)。我相信其他IDE中也有类似的快捷方式。 - Joachim Sauer
我使用 IDEA - ^N <classname> - 我有点懒 :-) - mcfinnigan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接