HashSet:
不可重复的字典
- 实现了Set接口
- HashSet依赖的数据结构是哈希表
- 因为实现的是Set接口,所以不允许有重复的值
- 插入到HashSet中的对象不保证与插入的顺序保持一致。对象的插入是根据它的hashcode
- HashSet中允许有NULL值
- HashSet也实现了Searlizable和Cloneable两个接口
拓展:
LinkedHashSet:具有按照插入顺序排序的HashSet,是HashSet 的子类。如果在映射中重新插入键,则插入顺序是不受影响的。
HashSet方法摘要
boolean add(E e) :如果不存在则添加,存在则返回false。
void clear() :移除Set中所有的元素
boolean contains(Object o) :如果这个元素在set中存在,那么返回true。
boolean remove(Object o) :如果这个元素在set中存在,那么从set中删除。
Iterator iterator() :返回set中这个元素的迭代器。
int size() :返回此集合中的元素的数量(集合的容量)。
isEmpty() :如果此集合不包含任何元素,则返回 true。
简单使用:
HashSet<String> al = new HashSet();
al.add("data1");
al.add("data2");
al.add("data3");
al.add("data4");
// 建立迭代器,输出里面的元素
Iterator it = al.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
// 也可以使用 foreach 方式输出
for(String s: al){
System.out.println(s);
}
HashSet内部是如何工作的?
所有Set接口的类内部都是由Map做支撑的。HashSet用HashMap对它的内部对象进行排序。你一定好奇输入一个值到HashMap,我们需要的是一个键值对,但是我们传给HashSet的是一个值。
那么HashMap是如何排序的?
实际上我们插入到HashSet中的值在map对象中起的是键的作用,因为它的值Java用了一个常量。所以在键值对中所有的键的值都是一样的。
如果我们在Java Doc中看一下HashSet的实现,大致是这样的:
private transient HashMap map;
// Constructor - 1
// All the constructors are internally creating HashMap Object.
public HashSet()
{
// Creating internally backing HashMap object
map = new HashMap<>();
}
// Constructor - 2
public HashSet(int initialCapacity)
{
// Creating internally backing HashMap object
map = new HashMap<>(initialCapacity);
}
// Dummy value to associate with an Object in Map
private static final Object PRESENT = new Object();
如果我们看下HashSet中的add方法:
public boolean add(E e)
{
return map.put(e, PRESENT) == null;
}
我们可以注意到,HashSet类的add()方法内部调用的是HashMap的put()方法,通过你指定的值作为key,常量“PRESENT”作为值传过去。
remove()也是用类似的方法工作。它内部调用的是Map接口的remove。
public boolean remove(Object o)
{
return map.remove(o) == PRESENT;
}
HashSet操作的时间复杂度:
HashSet底层的数据结构是哈希表,所以HashSet的add,remove与查询(包括contain方法)的分摊(平均或者一般情况)时间复杂度是O(1)。
HashSet中是如何保证元素唯一性呢?
HashSet是通过元素的两个方法,hashCode和equals来完成。
如果元素的hashCode值相同,才会判断equals是否为true,当equals为true时,元素不会被加入
如果元素的hashCode值不同,不会调用equals,顺利添加元素
对于判断元素是否存在,以及删除等操作,依赖的方法是元素的hashCode和equals方法。
具体参见HashSet的部分代码:
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<E,Object>();
}
public boolean contains(Object o) {
return map.containsKey(o);
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
}
发现HashSet是用HashMap来实现的,利用HashMap中Key的唯一性,保证元素不重复。
由此可见,HashSet中的元素实际上是作为HashMap中的Key存放在HashMap中的。下面是HashMap类中的put方法:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
}
从这段代码中可以看出,HashMap中的Key是根据对象的hashCode() 和 euqals()来判断是否唯一的。
为了保证HashSet中的对象不会出现重复值,在被存放元素的类中必须要重写hashCode()和equals()这两个方法。
实现筛选元素重复的例子:
(创建一个Person对象,有年龄和姓名。如果对象的年龄和姓名一致时,则认为是同一个元素。)
import java.util.*;
class Person{
private String name;
private int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
public Object getName(){
return this.name;
}
public Object getAge(){
return this.age;
}
public int hashCode(){
// 返回name的hashCode 加上 年龄的数值 * 39
// 为什么后面还要 乘一个数? 答:为了确保每个hashCode不同
// 如果两个数直接相加,有可能不同组合加起来的值是一样的。例:2+6=8 1+7=8
return this.name.hashCode() + this.age * 39;
}
public boolean equals(Object obj){
Person temp = (Person)obj;
/*
* 判断年龄和姓名相等才返回真
* */
return(this.age == temp.age && this.name.equals(temp.name));
}
}
public class test{
public static void main(String[] args)
{
HashSet al = new HashSet();
al.add(new Person("li1",12));
al.add(new Person("li1",12)); // 这个元素是重复的
al.add(new Person("li2",13));
al.add(new Person("li3",14));
Iterator it = al.iterator();
while(it.hasNext()){
Person each = (Person)it.next();
System.out.println(each.getName() + ".." + each.getAge());
}
}
}
输出结果:
li2..13
li1..12
li3..14