我想在 HashMap 中存储大量对象。识别每个对象的关键是一个字符串,它总是由3个部分/子字符串组成,为简单起见,我将它们命名为A,B和C. A具有高可变性,B平均变异性和C低变异性 。组合各部分有多种方法:
key = A + "_" + B + "_" + C;
key = A + "_" + C + "_" + B;
key = B + "_" + A + "_" + C;
...
首先,我想知道如何从具有不同可变性/随机性的子串构建密钥,以获得最统一的哈希码分布。最随机的位应该是第一个,还是最后一个,或者......?
其次,我想知道长度如何影响从HashMap获取对象的时间。例如,如果我将密钥长度加倍,那么对象检索需要两倍的时间吗?或者哈希码的计算只占用了那个时间的一小部分,因为从HashMap的桶中获取对象的过程需要更长的时间?
答案 0 :(得分:1)
底线:您应该使用hashCode
类提供的标准String
方法...但不因为订单不是&# 39;无所谓。
(事实上,如果你说C具有最高的可变性且A具有最低的可变性,则java.lang.String.hashCode
的性能会非常糟糕!)
离开:鉴于有关Object
成员的附加信息,散列顺序会对密钥分配产生重大影响。
通常,如果没有任何特定领域的知识,最好选择可读性和完善的库的可靠性。但是,由于您对子字符串的分布有特定的了解,因此您可以对hashFunction做出更明智的决定。
为了演示,假设A部分可以采用任何字符值,B部分仅采用前15个字符,C部分仅采用前5个字符。并假设您以下列方式覆盖hashCode
方法:
@Override
public int hashCode(){
final int constant = 37;
final String partA = getPartA(myString);
final String partB = getPartB(myString);
final String partC = getPartC(myString);
int total = 17;
total= total * constant + partA;
total= total * constant + partB;
total= total * constant + partC;
return total;
}
我们期望这种方法的字符串几乎均匀随机分布。但是,如果我们要颠倒以下几行:
total= total * constant + partC; //formerly part A
total= total * constant + partB;
total= total * constant + partA; //formerly part C
我们只会在值范围的前半部分生成代码。这里有一些实验结果在15,000个符合我上述假设的随机字符串上进行了测试。
答案 1 :(得分:1)
您是否仅为了在HashMap
中使用密钥而制作密钥?如果是这样,那么你甚至不必去做。您可以将对象直接放在HashMap
中,但必须覆盖方法hashCode()
和equals()
。
好消息是 - 您的IDE(例如Eclipse
)可以为您生成hashCode()
和equals()
的建议代码。 (在Eclipse中,Source
> Generate hashCode() and equals() ...
)。你可以从那里得到它的建议。
请参阅下面的示例代码。
我倾向于认为计算速度非常快。但是如果您对速度有所顾虑,并且三个字段/部分/子字符串是不可变的,那么您可以在构造函数中计算hashCode,就像我在示例代码中所做的那样。
从散列映射访问元素的速度取决于负载因子(即散列映射的有效程度)。如果散列图被轻载(大多数桶中有零个或一个元素),则几乎可以获得持续时间O(1)进行访问。如果散列图负载很重(大多数存储桶有很多元素),那么性能会显着降低。
示例代码
package StringKeyForHashMap;
import java.util.HashMap;
import java.util.Map;
public class Thing {
private final String a;
private final String b;
private final String c;
private final int hashCode;
public Thing(String a, String b, String c) {
super();
this.a = a;
this.b = b;
this.c = c;
this.hashCode = computeHashCode();
}
@Override
public int hashCode() {
return this.hashCode;
}
private int computeHashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((a == null) ? 0 : a.hashCode());
result = prime * result + ((b == null) ? 0 : b.hashCode());
result = prime * result + ((c == null) ? 0 : c.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Thing other = (Thing) obj;
if (a == null) {
if (other.a != null)
return false;
} else if (!a.equals(other.a))
return false;
if (b == null) {
if (other.b != null)
return false;
} else if (!b.equals(other.b))
return false;
if (c == null) {
if (other.c != null)
return false;
} else if (!c.equals(other.c))
return false;
return true;
}
public static void main(String[] args) {
/*
* Below I assume that the value of interest is
* an integer
*/
Map<Thing, Integer> map = new HashMap<>();
map.put(new Thing("AAA", "BBB", "CCC"), 0);
}
}
答案 2 :(得分:1)
String
在字符串开头与字符串末尾的变异性是否相关无关紧要。
为了测试这一点,下面的代码模拟了Java 8的HashMap
类的哈希表逻辑。方法tableSizeFor
和hash
是从JDK源代码中复制的。
代码将创建60个字符串,这些字符串仅由第一个或最后7个字符区分。然后,它将构建一个具有适当容量的哈希表,并计算哈希桶冲突的数量。
从输出中可以看出,无论被散列的字符串的前导或尾随变化如何,冲突计数都是相同的(在统计边界内)。
<强>输出强>
Count: 1000 Collisions: 384 By collision size: {1=240, 2=72}
Count: 1000 Collisions: 278 By collision size: {1=191, 2=30, 3=3, 4=3, 6=1}
Count: 100000 Collisions: 13876 By collision size: {1=12706, 2=579, 3=4}
Count: 100000 Collisions: 15742 By collision size: {1=12644, 2=1378, 3=110, 4=3}
Count: 10000000 Collisions: 2705759 By collision size: {1=1703714, 2=381705, 3=65050, 4=9417, 5=1038, 6=101, 7=3}
Count: 10000000 Collisions: 2626728 By collision size: {1=1698957, 2=365663, 3=56156, 4=6278, 5=535, 6=27, 7=4}
测试代码
public class Test {
public static void main(String[] args) throws Exception {
//
test(1000, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_%07d");
test(1000, "%07d_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
test(100000, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_%07d");
test(100000, "%07d_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
test(10000000, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_%07d");
test(10000000, "%07d_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
}
private static void test(int count, String format) {
// Allocate hash-table
final int initialCapacity = count * 4 / 3 + 1;
final int tableSize = tableSizeFor(initialCapacity);
int[] tab = new int[tableSize];
// Build strings, calculate hash bucket, and increment bucket counter
for (int i = 0; i < count; i++) {
String key = String.format(format, i);
int hash = hash(key);
int bucket = (tableSize - 1) & hash;
tab[bucket]++;
}
// Collect collision counts, i.e. counts > 1
// E.g. a bucket count of 3 means 1 original value plus 2 collisions
int total = 0;
Map<Integer, AtomicInteger> collisions = new TreeMap<>();
for (int i = 0; i < tableSize; i++)
if (tab[i] > 1) {
total += tab[i] - 1;
collisions.computeIfAbsent(tab[i] - 1, c -> new AtomicInteger()).incrementAndGet();
}
// Print result
System.out.printf("Count: %-8d Collisions: %-7d By collision size: %s%n", count, total, collisions);
}
static final int MAXIMUM_CAPACITY = 1 << 30;
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
}
答案 3 :(得分:0)
订单不会影响散列键的分发。所有字符都具有相同的&#34;权重&#34;。
密钥越长,计算散列所需的时间就越多,BUT一旦创建了hashCode就重用了hashCode,因此如果重用相同的String,hashCode只会生成一次。
话虽如此,我建议你改变你的实施:
即使您没有重复使用该对象,它也是一种更好的方法,因为它封装了哈希逻辑。但如果对象被重用,真正的好处就来了。