一、Set接口介绍 点击此处返回总目录 二、HashSet 三、哈希表的数据结构 四、对象的哈希值 五、哈希表的存储过程 六、自定义重写hashCode()方法和equals()方法 七、一道关于hashCode()方法和equals()方法的面试题 八、LinkedHashSet 一、Set接口介绍 Set接口中不予许存储重复元素。 Set集合的取值方式,目前只有两种:迭代器和增强for。Set是没有索引的,不能通过索引的方式来获取数据。 Set接口中的方法与Collection接口的方法一模一样。 二、HashSet 是Set接口的一个实现类。HashSet的底层本质是HashMap。 HashSet集合的自身特点: 底层数据结构:哈希表。哈希表是数组和链表的结合体。 存储、取出都比较快。 线程不安全,运行速度比较快 例:
package cn.itcast.demo07; import java.util.HashSet; import java.util.Iterator; public class Test { public static void main(String[] args) { HashSet<String> set = new HashSet<String>(); set.add("aaa"); set.add("bbb"); set.add("ccc"); set.add("bbb"); //重复的不会存 //迭代器方式遍历 Iterator<String> it = set.iterator(); while(it.hasNext()){ System.out.println(it.next()); //aaa ccc bbb } //增强for方式遍历 for(String s:set){ System.out.println(s); //aaa ccc bbb } } } |
三、哈希表的数据结构 自己翻翻数据结构。一个数组,数组的每个元素挂一个链表。 四、对象的哈希值 Object类的方法:public int hashCode() ,返回对象的哈希值,是一个整数,不知道怎么算的,运行多次可能得到多个结果。【例1】 String对象重写了hashCode()方法,有自己的一个算法,见下面。【例2】 例1:得到对象的哈希值。 //Person.java
package cn.itcast.demo08; public class Person { } |
//Test.java
package cn.itcast.demo08; import java.util.HashSet; public class Test { public static void main(String[] args) { Person p = new Person(); int i = p.hashCode(); System.out.println(i); //26542488、24763620等。 } } |
例2:String类重写了Object类的hashCode()方法。
package cn.itcast.demo08; import java.util.HashSet; public class Test { public static void main(String[] args) { String s1 = new String("abc"); String s2 = new String("abc"); System.out.println(s1.hashCode()); //96354 System.out.println(s2.hashCode()); //96354。得到的哈希值是一样的。 } } |
解释: 查看String的源代码可以发现,String类重写了Object类的hashCode()方法,如下:
public int hashCode() { int h = hash; //hash初始为0 if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; } |
计算过程如下: step1:h = 0 step2:h == 0 && value.length=3>0,为true step3:val[] = value; step4:h = 31*0 + 'a' = 0+97 = 97 step5:h = 31*97 + 'b' = 31*97 + 98 = 3105 step6:h = 31*3105 +'c' = 31* 3105 + 99 = 96354 因此s1与s2调用hasCode()方法得到的值相等。 五、哈希表的存储过程 对象调用hashcode()方法; if(集合中没有出现过该哈希值){ 是一个新的对象,存储; }else if(集合中出现过该哈希值){ if(新对象.equals(旧对象) == true){ 元素重复,不存储; }else{ 不重复,挂在旧对象下面; } } 例1:
package cn.itcast.demo08; import java.util.HashSet; public class Test { public static void main(String[] args) { HashSet<String> set = new HashSet<String>(); set.add(new String("abc")); set.add(new String("abc")); set.add(new String("bbc")); set.add(new String("bbc")); System.out.println(set); //[abc, bbc]。明明是四个对象,为什么set中只存了两个元素呢? } } |
上面明明是四个不同的对象,但是只存储了两个。因此需要知道是怎么存储的。 当存储new String("abc")时: step1:首先调用对象的哈希值。new String("abc").hashCode()方法,得到96354。 step2:在容器中找有没有和96354一样的哈希值,没有,把该对象存在数组的一个索引上。 当又存储new String("abc")时: step1:首先调用对象的哈希值。new String("abc").hashCode()方法,得到96354。 step2:在容器中找有没有和96354一样的哈希值。找到了一个对象也有同样的哈希值。 step3:集合让后来的对象调用equals()方法,与已经有了的对象作比较,得到true。 step4:集合判定元素重复,不再存储。 "bbc"是一样的,不再解释。 假设又来了一个new String("adc"),调用hashCode(),假设也得到了96354。在调用equals()方法,得到了false。那么集合判断元素没有重复,会采用桶的存储方式,把"adc"存储在"abc"的链表中。 六、自定义重写hashCode()方法和equals()方法 通过五,可以知道存储元素的时候会调用hashCode()和equals()方法。如【例1】: 例1:
package cn.itcast.demo09; import java.util.HashSet; public class Test { public static void main(String[] args) { HashSet<Person> set = new HashSet<Person>(); Person p1 = new Person("aaa",23); Person p2 = new Person("aaa",23); Person p3 = new Person("bbb",22); set.add(p1); set.add(p2); set.add(p3); System.out.println(set); //[aaa--23, bbb--22, aaa--23]。因为p1和p2是两个不同的对象,hash值不同,所以存了两次。 } } |
如果想将Person对象中,姓名和年龄相同的数据看做同一个对象,在set中只存储一份,就需要改写hashCode()方法和equals()方法。 例2: //Person.java
package cn.itcast.demo09; public class Person { private String name; private int age; public Person(){} public Person(String name, int age) { this.name = name; this.age = age; } 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 toString(){ return name + "--"+age; } public int hashCode(){ //重写了hashCode()方法。 String s = name +age; return s.hashCode(); } public boolean equals(Object b){ //重写了equals()方法。 if(this == b){ return true; } if(b == null){ return false; } if(b instanceof Person){ Person p = (Person)b; return name.equals(p.name) && age == p.age; } return false; } } |
//Test.java
package cn.itcast.demo09; import java.util.HashSet; public class Test { public static void main(String[] args) { HashSet<Person> set = new HashSet<Person>(); Person p1 = new Person("aaa",23); Person p2 = new Person("aaa",23); Person p3 = new Person("bbb",22); set.add(p1); set.add(p2); set.add(p3); System.out.println(p1.equals(p2)); //true System.out.println(p1.hashCode()); //92566082 System.out.println(p2.hashCode()); //92566082 System.out.println(set); //[bbb--22, aaa--23]。不再输出3个了,因为哈希值相等,equals()结果也相等。 } } |
七、一道HasCode()和equals()相关的面试题 1.如果两个对象的哈希值相等:p1.hashCode() == p2.hashCode(); 那么两个对象的equals一定返回true么?即,p1.equals(p2) 一定是true么? 答:不一定。 2.如果两个对象的equals返回true,即p1.equals(p2) 是true, 那么两个对象的哈希值一定相等么? 答:一定。 解释:参考API文档,对Object类的hashCode()方法的描述如下:
hashCode 的常规协定是:
- 在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
- 如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用
hashCode 方法都必须生成相同的整数结果。 - 如果根据
equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不 要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。
|
而,对Object类的equals()方法的描述如下:
注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。 |
综上,一定要维护hashCode方法的常规协定。当equals时,一定要hashCode()相等。 八、LinkedHashSet 基于链表的哈希表实现,继承HashSet。 具有顺序,存储和取出的顺序相同。有序的Set集合。【例1】 例1:
package cn.itcast.demo10; import java.util.LinkedHashSet; public class Test { public static void main(String[] args) { LinkedHashSet<Integer> link = new LinkedHashSet<Integer>(); link.add(3); link.add(2); link.add(2); link.add(1); System.out.println(link); //[3, 2, 1]。不存储重复元素,而且有序。 } } |
|