分类 安全警示 下的文章

警告:不要将Java中的数组作为Map容器的Key类型

Java语言提供了泛型的支持。你可以将你能想得到的许多种类型作为HashMap等容器的Key类型使用(只要他们正确地实现了hashCode和equals之类的方法)。
你可能会想过把数组类型(例如String[]),作为Map容器的Key使用,例如写出这样的代码:

TreeMap<String[], String> map1;
HashMap<String[], String> map2;

第一行代码根本不能通过编译!这是因为数组类型根本没有实现Comparable接口,无法比较出“大小”,自然使得依赖于树的TreeMap无法工作。
第二行代码不会出现编译问题,但你很快就会发现代码的运行方式会与你所期望的相违背。你可能发现对于具有相同值的两个数组,会被HashMap当作两个不同的Key值,导致相同的Key对应不同的Value,实在是太糟糕了。
出现问题的是数组类型所实现的equals()方法。顾名思义,这个方法比较两个对象是否相等。在这里我们希望作为Key的对象是通过“值”来比较的。也就是说只要两个对象的“值”相同,不管它们是不是同一个对象,就应当认为他们是相等的。
但是与我们所期望的相反,数组类型所实现的equals()方法是基于引用进行比较的,如果两个引用指向的不是同一个对象,即使数组内部元素完全相同,这个方法也会返回false。
所以,请不要把数组作为Map容器的Key类型使用,有一些替代性的方法做参考:

  • 将数组转换为一个字符串。
  • 使用ArrayList,ArrayList中的equals方法是基于值进行比较的。

如果你真的非常想用数组作为Key,你可以考虑自己编写一个类,并正确实现它的equals和hashCode方法。
像这样:

import java.util.Arrays;
public class KeyArray {

    Object[] internalArray;

    KeyArray(Object[] arr) {
        internalArray = arr;
    }

    @Override
    public boolean equals(Object o) {
        /* 谨慎地编写equals方法,这里的代码没有考虑所有的情况 */
        if (o != null) {
            if (o instanceof KeyArray) {
                /* Arrays.equals会对两个数组进行基于值的比较 */
                return Arrays.equals(internalArray, ((KeyArray) o).internalArray);
            }
        }
        return false;
    }

    @Override
    public int hashCode() {
        /* Arrays.deepHashCode 会根据数组内的元素的值计算 hashCode */
        return Arrays.deepHashCode(internalArray);
    }
}

之后就可以将KeyArray作为HashMap的Key使用。
最后,我发现了一个很有趣的事情,Java中的List容器是这样计算hashCode的:

int hashCode = 1;
for (E e : list)
      hashCode = 31 * hashCode + (e==null ? 0 : e.hashCode());

我不清楚它使用31这个magic number的理由。如果你有什么想法,可以在评论里告诉我。