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); } } }
相关文章推荐
- Java——其他对象
- Java-----IO读写操作
- java学习笔记(一)OutputStreams
- struts (三)
- 页面上动态编译及执行java代码
- java的测试
- 简单的ftp服务器实现 (java)
- 13.Java5条件阻塞Condition的应用
- java的myeclipse,java页面修改默认的javadoc方法
- Eclipse中设置ButterKnife进行注解式开发步骤
- java基础第四天
- java通过选择年月生成天下拉框
- 深入浅出的理解框架(Struts2、Hibernate、Spring)与 MVC 设计模式
- Eclipse搜索
- java之动态代理
- Java语言基础1--专题课 递归
- java io 装饰设计模式
- JDK 源码 阅读 - 2 - 设计模式 - 创建型模式
- java 数据类型转换
- Eclipse Ndk开发中的Method 'NewStringUTF' could not be resolved问题