您的位置:首页 > 编程语言 > Java开发

Java中HashSet的存储原理

2015-07-15 23:14 525 查看

Java中HashSet的存储原理

1. 说明

HashSet,从字面意思我们大致可以看出它包含了两方面的内容:Hash和Set,即散列与集合。

实际确实如此,HashSet实现了Set接口,所以它符合Set集合使用的特征,集合中不允许有重复的元素,同时HashSet底层的实现是通过HashMap来实现元素保存操作的。

2. 分析

我们来分析一下HashSet的原码,下面这段是HashSet中存储对象的数据结构:

private transient HashMap<E,Object> map;

public HashSet() {
map = new HashMap<E,Object>();
}


这里可以看到,HashSet类维护了一个HashMap引用作为自己的成员变量,并在其构造方法中将其实例化。

HashMap表示的是关键字key到值value的映射,其中key唯一,即一个key某一时刻只能映射到一个value。

下面我们再来看HashSet中添加元素的方法:

private static final Object PRESENT = new Object();

public boolean add(E e) {
return map.put(e, PRESENT)==null;
}


我将add()方法中用到的一个参数PRESENT的定义也一并复制到一起来。大家可以看到,实际上add()方法的实现非常简单,直接调用HashMap的put()方法实现键-值对映射。在键-值对映射时,将e(待添加到集合中的元素)作为键,将PRESENT作为值传递到put()方法中。我们在调用add()方法的时候,其实不关心底层的实现,在这儿写的HashMap中保存的键值对映射,具体值是什么,其实是不重要的,所以定义了一个常量的Object,让所有的键映射到同一个值上,但这一点对于使用HashSet的人是不需要知道的。

那在add()方法中添加的元素是如何实现了唯一性(不重复)的呢,这还得从HashMap中去说。

和HashMap相关的一些基本概念,大家可以参考/article/9623851.html,这不再赘述。

下面我们再转到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;
}
}

modCount++;
addEntry(hash, key, value, i);
return null;
}


hash(key.hashCode())是获取key的散列码,indexFor(hash, table.length)是获取在底层存放数组中的索引位置,如果在该位置已经存放了元素,则会比较该位置上元素的散列码与关键字,如果这些都相同,则说明是同一个对象,那么不会重复添加,当然如果找到的位置上未存放有元素,或不是重复的元素,则调用addEntry()方法将元素保存起来。

那么我们仔细再来看下这段代码。在使用hash()方法获取散列码时,传递了key.hashCode()这样的参数,key是什么呢,key是在调用HashSet的add()方法时传递进来的要保存的对象。我们在判断是否重复元素时,会用到散列码,使用传递进来的对象的hashCode()方法计算出其散列码值,再使用hash()方法对该散列码重新运算得到一个新值,那么这个hashCode()方法是比较重要的。除了hashCode()这个方法外,还有一个方法:equals(),它主要是用来判断两个对象是否是相同的对象。为什么又会调用到这个方法呢?我们说,当两个对象散列码相同的时候,不一定是相同的两个对象,仅只能说明关键字使用散列函数运算后得到的存储位置一致,至于是否为相同对象,还得通过equals()方法判断。

如果我们要从HashSet中获取元素,如何来获取呢。因为HashSet底层是以HashMap的方式实现的,所以在HashSet中不会维护所存放元素的顺序,我们要获取所保存的元素,那就只能使用HashMap中的相应方法来实现了,好在调用什么方法,在HashSet中已经帮我们封装了实现细节。

public Iterator<E> iterator() {
return map.keySet().iterator();
}


这儿我们用到了迭代器(Iterator),在迭代器中遍历到的元素是些什么呢,看具体的实现,应该是map.keySet(),也就是在HashMap中保存的所有键集合,通过前面存储的分析,我们知道所有键的集合就是我们所在HashSet中保存的所有元素。

示例

学生类:

package com.hash.demo2;

/**
* 学生类
*
* @author 小明
*
*/
public class Student {
private int id; // 编号
private String name; // 姓名
private int age; // 年龄
private String sex; // 性别

public Student(int id, String name, int age, String sex) {
super();
this.id = id;
this.name = name;
this.age = age;
this.sex = sex;
}

// 省略getter与setter
// ……
}


测试:

package com.hash.demo2;

import java.util.HashSet;

/**
* HashSet测试
* @author 小明
*
*/
public class HashSetDemo {

public static void main(String[] args) {
HashSet<Student> students = new HashSet<Student>();

/* 创建两个属性值都相同的对象 */
Student stu1 = new Student(1, "张三", 28, "男");
Student stu2 = new Student(1, "张三", 28, "男");
/* 将两个对象添加到HashSet中保存 */
students.add(stu1);
students.add(stu2);
// 打印集合大小
System.out.println("集合中存放元素个数:" + students.size());
}
}


这时打印的结果为:

集合中存放元素个数:2


但是我们创建的两个Student对象所有属性都一致,一般我们会认为这是两个相同的对象,HashSet中不会存放重复元素,但为什么这两个相同的对象又都能存放到集合中呢?我们之前分析底层存储时说到,在HashMap中关键字会使用其hashCode()判断是否具有相同的散列码。因为两个Student对象都是独立实例化出来的,在堆中存放的位置不一样,所以hashCode()得到的结果也不一致(大家可以自行测试,打印出两个对象的hashCode值),那么在HashSet中保存这两个元素时,就会认为它们不是相同的元素,所以都能保存下来,集合长度为2。

那我们在Student类中重写hashCode()方法:

@Override
public int hashCode() {
/* 我们先自定义一个规则来生成对象的散列码 */
int result = 10;
result += this.age * 2 + this.id;
return result;
}


再来执行测试,结果为:

集合中存放元素个数:2


集合中存放元素的个数仍然为2。我们不是重写了hashCode()方法,经过hashCode()方法运算后返回的结果都为67,散列码都相同了,为什么还是存放了重复元素呢。前边已经解释过了,散列码相同,只能说明两个元素存放的位置是在同一个位置上面,而至于是不是相同的两个元素,还得通过equals()方法比较才知道。

那么接下来,我们再重写一下equals()方法吧:

@Override
public boolean equals(Object obj) {
Student stu = (Student) obj;
return id == stu.getId() && name.equals(stu.getName())
&& age == stu.getAge() && sex.equals(stu.getSex());
}


再来执行测试,结果为:

集合中存放元素个数:1


现在结果就是我们想要的结果了:HashSet中不重复保存元素。

每次如果我们都自定义hashCode()的实现与equals()的实现,会比较麻烦,并且在考虑比较条件的时候可能会不完全,所以可以使用Eclipse工具提供的方式,直接生成hashCode()和equals()方法。

关于hashCode有一个常规协定:

在Java应用程序执行期间,在对同一对象多次调用hashCode方法时,必须一致地返回相同的整数,前提是将对象进行equals比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。

如果根据equals(Object)方法,两个对象是相等的,那么对这两个对象中的每个对象调用hashCode方法都必须生成相同的整数结果。

如果根据equals(java.lang.Object)方法,两个对象不相等,那么对这两个对象中的任一对象上调用hashCode方法不要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。

所以我们要重写equals()方法,一般都有必要重写hashCode()方法,以维护hashCode()方法的常规协定。

总结

下面简要总结一下HashSet的存储原理

根据每个对象的哈希码值(调用hashCode()获得)用固定的算法算出它的存储索引,把对象存放在一个叫散列表的相应位置(表元)中,可能会有以下两种情况:

如果对应的位置没有其它元素,就只需要直接存入。

如果该位置有元素了,会将新对象跟该位置的所有对象进行比较(调用equals()),以查看是否已经存在了:还不存在就存放,已经存在就直接使用。

如果我们要取元素,则根据对象的哈希码值计算出它的存储索引,在散列表的相应位置(表元)上的元素间进行少量的比较操作就可以找出它。

完整示例

学生类:

package com.hash.demo2;

/**
* 学生类
*
* @author 小明
*
*/
public class Student {
private int id; // 编号
private String name; // 姓名
private int age; // 年龄
private String sex; // 性别

public Student() {
super();
}

public Student(int id, String name, int age, String sex) {
super();
this.id = id;
this.name = name;
this.age = age;
this.sex = sex;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getSex() {
return sex;
}

public void setSex(String sex) {
this.sex = sex;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + id;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((sex == null) ? 0 : sex.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;
Student other = (Student) obj;
if (age != other.age)
return false;
if (id != other.id)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (sex == null) {
if (other.sex != null)
return false;
} else if (!sex.equals(other.sex))
return false;
return true;
}

@Override
public String toString() {
return "Student [id=" + id + ", name=" + name + ", age=" + age
+ ", sex=" + sex + "]";
}
}


测试类:

package com.hash.demo2;

import java.util.HashSet;
import java.util.Iterator;

/**
* HashSet测试
*
* @author 小明
*
*/
public class HashSetDemo {

public static void main(String[] args) {
HashSet<Student> students = new HashSet<Student>();

/* 创建两个属性值都相同的对象 */
Student stu1 = new Student(1, "张三", 28, "男");
Student stu2 = new Student(1, "张三", 28, "男");
/* 将两个对象添加到HashSet中保存 */
students.add(stu1);
students.add(stu2);
// 打印集合大小
System.out.println("集合中存放元素个数:" + students.size());
/* 遍历HashSet中的元素 */
Iterator<Student> it = students.iterator();
while (it.hasNext()) {
Student stu = it.next();
System.out.println(stu);
}
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: