Java HashSet与HashMap的区别

55

我知道HashSet基于HashMap实现,但是它用于需要唯一元素的集合。那么为什么在下面的代码中,当将相同的对象放入映射和集合时,两个集合的大小都等于1?难道映射大小不应该是2吗?因为如果两个集合的大小相等,我看不出使用这两个集合有任何区别。

    Set testSet = new HashSet<SimpleObject>();
    Map testMap = new HashMap<Integer, SimpleObject>(); 

    SimpleObject simpleObject1 = new SimpleObject("Igor", 1);
    SimpleObject simplObject2 = new SimpleObject("Igor", 1);
    testSet.add(simpleObject1);
    testSet.add(simplObject2);


    Integer key = new Integer(10);

    testMap.put(key, simpleObject1);
    testMap.put(key, simplObject2);

    System.out.println(testSet.size());
    System.out.println(testMap.size());

输出结果为 1 和 1。

SimpleObject code

public class SimpleObject {

private String dataField1;
private int dataField2;

public SimpleObject(){}

public SimpleObject(String data1, int data2){
    this.dataField1 = data1;
    this.dataField2 = data2;
}

public String getDataField1() {
    return dataField1;
}

public int getDataField2() {
    return dataField2;
}

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result
            + ((dataField1 == null) ? 0 : dataField1.hashCode());
    result = prime * result + dataField2;
    return result;
}

@Override
public boolean equals(Object obj) {
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    SimpleObject other = (SimpleObject) obj;
    if (dataField1 == null) {
        if (other.dataField1 != null)
            return false;
    } else if (!dataField1.equals(other.dataField1))
        return false;
    if (dataField2 != other.dataField2)
        return false;
    return true;
 }
}

8
只是好奇,为什么第一个前缀是“simple”,而第二个是“simpl”? :) - BalusC
1
我理解为什么映射的大小是1,但我不知道为什么集合的大小也是1?我们将2个对象放入集合中,为什么它的大小是1? - Hengameh
7个回答

130

该 Map 包含唯一的键。当您使用已存在于 Map 中的键调用 put 方法时,该键对应的对象将被新对象替换。因此,Map 的大小为 1。

两者之间的区别应该很明显:

  • 在 Map 中存储键值对。
  • 在 Set 中仅存储键。

实际上,HashSet 具有 HashMap 字段,每当调用 add(obj) 方法时,就会在底层 Map 上调用 put 方法,即 map.put(obj, DUMMY)——其中 dummy 对象是一个私有静态最终对象,即 private static final Object DUMMY = new Object()。因此,该 Map 中存储了您的对象作为键和一个无关紧要的值。


2
具有不同哈希码的键是被接受的。但是相互相等的键是不被接受的。 - Bozho
2
@Bart Kiers - 相等 - 是的,但是具有相同的哈希码是允许的。 - Bozho
2
@Bart,你可以使用以下代码:public int hashCode() { return 1; }。虽然我不建议这样做,但它是合法的,而且哈希映射将能够工作(尽管性能会下降)。 - corsiKa
1
完美的答案!我只用了2秒钟就理解了它!我注意到“在集合中,你只存储键”,而且不需要阅读这句话之前或之后的任何内容。 - Sergey Orshanskiy
在Java 8中,HashSet的DUMMY对象实际上是PRESENT。private static final Object PRESENT = new Object(); - Peter Perháč
显示剩余4条评论

7

Map中的键只能映射到一个值。因此,当您第二次使用相同的键将其放入映射中时,它会覆盖第一条条目。


6

对于HashSet而言,添加same对象将几乎没有任何作用。对于HashMap而言,在已存在的键上放置新的键值对将会覆盖现有的值,以设置该键的新值。下面我已经在你的代码中添加了equals()检查:

SimpleObject simpleObject1 = new SimpleObject("Igor", 1);
SimpleObject simplObject2 = new SimpleObject("Igor", 1);
//If the below prints true, the 2nd add will not add anything
System.out.println("Are the objects equal? " , (simpleObject1.equals(simpleObject2));
testSet.add(simpleObject1);
testSet.add(simplObject2);


Integer key = new Integer(10);
//This is a no-brainer as you've the exact same key, but lets keep it consistent
//If this returns true, the 2nd put will overwrite the 1st key-value pair.
testMap.put(key, simpleObject1);
testMap.put(key, simplObject2);
System.out.println("Are the keys equal? ", (key.equals(key));
System.out.println(testSet.size());
System.out.println(testMap.size());

2
这并不完全正确。如果你的对象上的 .equals() 方法是 return false;,那么你可以添加任意多次相同的对象。但我不建议这样做。 - corsiKa
是的,我假设他没有在SimpleObject中覆盖equals()或hashCode()方法。 - lobster1234
1
我已经在SimpleObject中重写了equals()或hashCode()方法。 - IgorDiy
3
我们应该始终假设equalshashCode方法都是按照规范实现的,并且是合理的。 - Paŭlo Ebermann

1

我只想补充一下这些出色的答案,回答你最后的困境。你想知道这两个集合的区别,如果在插入后它们返回相同的大小。嗯,你在这里看不到区别,因为你正在将具有相同键的两个值插入到映射中,从而用第二个值更改第一个值。你将看到真正的区别(在其他方面之间)应该是在地图中插入了相同的值,但使用了不同的键。然后,您会发现您可以在映射中拥有重复的值,但您不能拥有重复的键,并且在集合中您不能拥有重复的值。这就是主要区别。


1
答案很简单,因为HashSet的本质如此。HashSet内部使用名为PRESENT的虚拟对象作为值和此哈希映射的KEY,这个哈希映射将是您的对象。
hash(simpleObject1)和hash(simplObject2)将返回相同的int。那么呢?
当您将simpleObject1添加到hashset中时,它将使用simpleObject1作为键将其放入其内部哈希映射中。然后,当您添加(simplObject2)时,您将获得false,因为它已经作为键在内部哈希映射中可用。
额外的信息是,HashSet使用有效的哈希函数通过使用对象的equals()和hashCode()协定提供O(1)性能。这就是为什么HashSet不允许"null",因为它不能实现equals()和hashCode()到非对象。

0
我认为主要的区别在于,HashSet是稳定的,它不会替换重复的值(如果在插入第一个唯一键后发现重复,则丢弃所有未来的重复项),而HashMap将努力用新的重复值替换旧的。因此,在HashMap中插入新的重复项必须有开销。

0

public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable
这个类实现了Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证集合的迭代顺序;特别地,它不保证顺序会随时间保持不变。这个类允许null元素。

对于基本操作(添加、删除、包含和大小),这个类提供了常数时间性能,假设哈希函数将元素适当地分散在桶中。遍历这个集合需要的时间与HashSet实例的大小(元素数量)加上支持HashMap实例(桶的数量)的“容量”成比例。因此,如果迭代性能很重要,不要将初始容量设置得太高(或负载因子太低)。

请注意,此实现未进行同步。如果多个线程同时访问哈希集,并且其中至少一个线程修改了该集合,则必须在外部进行同步。通常通过对自然封装集合的某个对象进行同步来完成此操作。如果不存在这样的对象,则应使用Collections.synchronizedSet方法“包装”该集合。最好在创建时执行此操作,以防止意外的不同步访问该集合。 更多细节

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