• Java基础-Collection子接口之Set接口


                      Java基础-Collection子接口之Set接口

                                        作者:尹正杰

    版权声明:原创作品,谢绝转载!否则将追究法律责任。

      学习Collection接口时,记得Collection中可以存放重复元素,也可以不存放重复元素,那么我们知道List中是可以存放重复元素的。那么不重复元素给哪里存放呢?那就是Set接口,它里面的集合,所存储的元素就是不重复的。

    一.Set接口的特点

      一个不包含重复元素的 collection。更确切地讲,set 不包含满足 e1.equals(e2) 的元素对 e1e2,并且最多包含一个 null 元素。正如其名称所暗示的,此接口模仿了数学上的 set 抽象。在所有构造方法以及 addequalshashCode 方法的协定上,Set 接口还加入了其他规定,这些规定超出了从 Collection 接口所继承的内容。出于方便考虑,它还包括了其他继承方法的声明(这些声明的规范已经专门针对 Set 接口进行了修改,但是没有包含任何其他的规定)。 

      Set集合有多个子类,这么我们介绍其中的HashSet,LinkedHashSet这两个集合。Set结合取出元素的方式可以采用迭代器和增强for。

    二.HashSet集合介绍

      此类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持。它不保证 set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用 null 元素。 总结HashSet有以下结果特点:

      1>.无序集合,也就是说存取元素和取出元素的顺序不一定是相同的;

      2>.没有索引;

      3>.不存储重复元素;

     1 /*
     2 @author :yinzhengjie
     3 Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/
     4 EMAIL:y1053419035@qq.com
     5 */
     6 
     7 package cn.org.yinzhengjie.note;
     8 
     9 import java.util.HashSet;
    10 import java.util.Iterator;
    11 import java.util.Set;
    12 
    13 public class HashSetDemo {
    14     public static void main(String[] args) {
    15         Set<String> set = new HashSet<String>();
    16         set.add("yinzhengjie");
    17         set.add("尹正杰");
    18         set.add("2018");
    19         set.add("yinzhengjie");        //第二次添加同一个元素时,并不会放入HashSet集合中。
    20         set.add("java");
    21         set.add("Big Data");
    22 
    23         //用迭代器进行遍历
    24         System.out.println("第一种遍历方式");
    25         Iterator<String> it = set.iterator();
    26         while(it.hasNext()) {
    27             System.out.println("	"+it.next());
    28         }
    29         
    30         System.out.println("第二种遍历方式");
    31         for (String string : set) {
    32             System.out.println("	"+string);            
    33         }
    34     }
    35 }
    36 
    37 /*
    38 以上代码执行结果如下:
    39 第一种遍历方式
    40     尹正杰
    41     2018
    42     java
    43     Big Data
    44     yinzhengjie
    45 第二种遍历方式
    46     尹正杰
    47     2018
    48     java
    49     Big Data
    50     yinzhengjie
    51 */

    三.哈希表的数据结构

      哈希表其实就是链表和数组的结合体。哈希表底层使用的也是数组机制,数组中也存放对象,而这些对象往数组中存放时的位置比较特殊,当需要把这些对象给数组中存放时,那么会根据这些对象的特有数据结合相应的算法,计算出这个对象在数组中的位置,然后把这个对象存放在数组中。而这样的数组就称为哈希数组,即就是哈希表。

      当向哈希表中存放元素时,需要根据元素的特有数据结合相应的算法,这个算法其实就是Object类中的hashCode方法。由于任何对象都是Object类的子类,所以任何对象有拥有这个方法。即就是在给哈希表中存放对象时,会调用对象的hashCode方法,算出对象在表中的存放位置,这里需要注意,如果两个对象hashCode方法算出结果一样,这样现象称为哈希冲突,这时会调用对象的equals方法,比较这两个对象是不是同一个对象,如果equals方法返回的是true,那么就不会把第二个对象存放在哈希表中,如果返回的是false,就会把这个值存放在哈希表中。也就是说多个对象可能存放在同一块存储空间中,当第一个对象来到数组的存储空间时,这块空间存储的是第一个进入该区域的对象地址,当第二个对象再次进入这个空间时,这个对象的内存地址并不直接保存在数组中,而是保存在第一个存入的对象中。说道HashSet不得不说一下加载因子,所谓的加载因子就是一个触发数组扩容的指标。虚拟机默认是0.75,初始容量,数组长度默认是16。也就是说当数组的长度超过12时,就会扩容,而之前存在当前数组中的地址又会重新进行哈希。简图如下:

    2>.字符串对象的哈希值

     1 /*
     2 @author :yinzhengjie
     3 Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/
     4 EMAIL:y1053419035@qq.com
     5 */
     6 
     7 package cn.org.yinzhengjie.note;
     8 
     9 class Teacher {
    10     /*
    11      * 没有做重写父类,每次运行结果都是不同整数
    12      * 如果子类重写父类的方法,哈希值,自定义的
    13      * 存储到HashSet集合的依据
    14      * 
    15      */
    16     @Override
    17     public int hashCode() {
    18         return 100;
    19     }
    20 }
    21 
    22 public class HashDemo {
    23     public static void main(String[] args) {
    24         Teacher t = new Teacher();
    25         //调用自定义的类的hashCode方法
    26         int hashId = t.hashCode();
    27         System.out.println(hashId);
    28                 
    29         String s1 = new String("abc");
    30         String s2 = "abc";
    31         System.out.println(s1.hashCode());
    32         System.out.println(s2.hashCode());
    33     }
    34 }
    35 
    36 
    37 /*
    38 以上代码执行结果如下:
    39 100
    40 96354
    41 96354
    42 */

      我们可以明显看出自己定义的类调用hashCode()默认返回值为100,最终打印结果和我们预期的一样,而我们在创建一个字符串的时候,发现两个值相等的字符串他们的地址竟然也是一样的!这是怎么回事呢?这个时候我们就不得不去看一下String重写的hashCode源码啦。

    3>.哈希表的存储过程

     

    四.哈希表的存储自定义对象

     1 /*
     2 @author :yinzhengjie
     3 Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/
     4 EMAIL:y1053419035@qq.com
     5 */
     6 
     7 package cn.org.yinzhengjie.note2;
     8 
     9 public class Person {
    10     private String name;
    11     private int age;
    12     public String getName() {
    13         return name;
    14     }
    15     public void setName(String name) {
    16         this.name = name;
    17     }
    18     public int getAge() {
    19         return age;
    20     }
    21     public void setAge(int age) {
    22         this.age = age;
    23     }
    24     public Person(String name, int age) {
    25         super();
    26         this.name = name;
    27         this.age = age;
    28     }
    29     public Person() {
    30         super();
    31     }
    32     @Override
    33     public String toString() {
    34         return this.name + "---" + this.age;
    35     }
    36     @Override
    37     public int hashCode() {
    38         final int prime = 31;
    39         int result = 1;
    40         result = prime * result + age;
    41         result = prime * result + ((name == null) ? 0 : name.hashCode());
    42         return result;
    43     }
    44     @Override
    45     public boolean equals(Object obj) {
    46         if (this == obj)
    47             return true;
    48         if (obj == null)
    49             return false;
    50         if (getClass() != obj.getClass())
    51             return false;
    52         Person other = (Person) obj;
    53         if (age != other.age)
    54             return false;
    55         if (name == null) {
    56             if (other.name != null)
    57                 return false;
    58         } else if (!name.equals(other.name))
    59             return false;
    60         return true;
    61     }    
    62 }
    Person.java 文件内容
     1 /*
     2 @author :yinzhengjie
     3 Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/
     4 EMAIL:y1053419035@qq.com
     5 */
     6 
     7 package cn.org.yinzhengjie.note2;
     8 
     9 import java.util.HashSet;
    10 
    11 public class HashDemo {
    12     public static void main(String[] args) {
    13         HashSet<Person> set = new HashSet<>();
    14         set.add(new Person("尹正杰",18));
    15         set.add(new Person("尹正杰",28));
    16         set.add(new Person("尹正杰",38));
    17         set.add(new Person("尹正杰",48));
    18         set.add(new Person("尹正杰",108));
    19         set.add(new Person("桂  阳",20));
    20         set.add(new Person("张  杰",19));
    21         set.add(new Person("卜梦龙",20));
    22         set.add(new Person("李  洋",18));
    23         System.out.println(set);
    24     }
    25 }
    26 
    27 
    28 /*
    29 以上代码执行结果如下:
    30 [尹正杰---48, 张  杰---19, 尹正杰---18, 尹正杰---38, 卜梦龙---20, 李  洋---18, 桂  阳---20, 尹正杰---28, 尹正杰---108]
    31 */

    五.LinkedHashSet集合

      具有可预知迭代顺序的 Set 接口的哈希表和链接列表实现。此实现与 HashSet 的不同之外在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,即按照将元素插入到 set 中的顺序(插入顺序)进行迭代。注意,插入顺序 受在 set 中重新插入的 元素的影响。(如果在 s.contains(e) 返回 true 后立即调用 s.add(e),则元素 e 会被重新插入到 set s 中。)此实现可以让客户免遭未指定的、由 HashSet 提供的通常杂乱无章的排序工作,而又不致引起与 TreeSet 关联的成本增加。

      从后缀可以看出:其本质是HashSet只不过在内部维护了一个链表,可以记住元素放入的顺序,这样就保证了存取的顺序,但是由于多了个链表,所以他的效率低些。如果保证元素唯一呢?当然是基于hashCode和equals方法。那么如何保证顺序呢?当然是链表啦!

     1 /*
     2 @author :yinzhengjie
     3 Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/
     4 EMAIL:y1053419035@qq.com
     5 */
     6 package cn.org.yinzhengjie.note2;
     7 
     8 import java.util.LinkedHashSet;
     9 
    10 public class LinkedHashSetDemo {
    11     public static void main(String[] args) {
    12         LinkedHashSet<Integer> link = new LinkedHashSet<>();
    13         link.add(123);
    14         link.add(98);
    15         link.add(100);
    16         link.add(314);
    17         System.out.println(link);
    18     }
    19 }
    20 
    21 
    22 /*
    23 以上代码执行结果如下:
    24 [123, 98, 100, 314]
    25 */

     

    六.TreeSet类

      基于 TreeMapNavigableSet 实现。使用元素的自然顺序对元素进行排序,或者根据创建 set 时提供的 Comparator 进行排序,具体取决于使用的构造方法。此实现为基本操作(addremovecontains)提供受保证的 log(n) 时间开销。

    注意,如果要正确实现 Set 接口,则 set 维护的顺序(无论是否提供了显式比较器)必须与 equals 一致。(关于与 equals 一致 的精确定义,请参阅 ComparableComparator。)这是因为 Set 接口是按照 equals 操作定义的,但 TreeSet 实例使用它的 compareTo(或 compare)方法对所有元素进行比较,因此从 set 的观点来看,此方法认为相等的两个元素就是相等的。即使 set 的顺序与 equals 不一致,其行为也 定义良好的;它只是违背了 Set 接口的常规协定。

      注意,此实现不是同步的。如果多个线程同时访问一个 TreeSet,而其中至少一个线程修改了该 set,那么它必须 外部同步。这一般是通过对自然封装该 set 的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用 Collections.synchronizedSortedSet 方法来“包装”该 set。此操作最好在创建时进行,以防止对 set 的意外非同步访问: 

       SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));

      此类的 iterator 方法返回的迭代器是快速失败 的:在创建迭代器之后,如果从结构上对 set 进行修改,除非通过迭代器自身的 remove 方法,否则在其他任何时间以任何方式进行修改都将导致迭代器抛出 ConcurrentModificationException。因此,对于并发的修改,迭代器很快就完全失败,而不会冒着在将来不确定的时间发生不确定行为的风险。

      注意,迭代器的快速失败行为无法得到保证,一般来说,存在不同步的并发修改时,不可能作出任何肯定的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测 bug。 

     1 /*
     2 @author :yinzhengjie
     3 Blog:http://www.cnblogs.com/yinzhengjie/tag/Java%E5%9F%BA%E7%A1%80/
     4 EMAIL:y1053419035@qq.com
     5 */
     6 package cn.org.yinzhengjie.note2;
     7 
     8 import java.util.TreeSet;
     9 
    10 public class TreeSetDemo {
    11     public static void main(String[] args) {
    12         TreeSet<String> set = new TreeSet();
    13         set.add("d");
    14         set.add("a");
    15         set.add("c");
    16         set.add("b");
    17         for (String string : set) {
    18             System.out.println(string);            //自动进行排序
    19         }
    20         System.out.println(set);    
    21     }
    22 }
    23 
    24 
    25 /*
    26 以上代码执行结果如下:
    27 a
    28 b
    29 c
    30 d
    31 [a, b, c, d]
    32 */

    七.ArrayListm,HashSet判断对象是否重复的原因

    1>.ArrayList的contains方法判断元素是否重复原理

      ArrayList的contains方法会使用调用方法时,传入的元素的equals方法依次与集合中的旧元素所比较,从而根据返回的布尔值判断是否有重复元素。此时,当ArrayList存放自定义类型时,由于自定义类型在未重写equals方法前,判断是否重复的依据是地址值,所以如果想根据内容判断是否为重复元素,需要重写元素的equals方法。

    2>.HashSet的add/contains等方法判断元素是否重复原理

      Set集合不能存放重复元素,其添加方法在添加时会判断是否有重复元素,有重复不添加,没重复则添加。HashSet集合由于是无序的,其判断唯一的依据是元素类型的hashCode与equals方法的返回结果。规则如下:

      先判断新元素与集合内已经有的旧元素的HashCode值。

        a>.如果不同,说明是不同元素,添加到集合。

        b>.如果相同,再判断equals比较结果。返回true则相同元素;返回false则不同元素,添加到集合。

      所以,使用HashSet存储自定义类型,如果没有重写该类的hashCode与equals方法,则判断重复时,使用的是地址值,如果想通过内容比较元素是否相同,需要重写该元素类的hashcode与equals方法。

    八.hashCode和equals方法的面试题

      现有两个Person对象,p1和p2,那么存在两个问题:

    1>.如果两个对象的哈希值相同(p1.hashCode()==p2.hashCode()),两个对象的equals一定返回true吗?p1.equals(p2) 一定是true吗?

      正确答案:不一定!

    2>.如果两个对象的equals方法返回true(p1.equals(p2) == true),两个对象的哈希值一定相同吗?

      正确答案:一定!

     

  • 相关阅读:
    linux内核源码之基础准备篇
    GDB 自动化操作的技术-PYTHON
    GDB 调试PYTHON
    GCC onlinedocs
    深入CSS,让网页开发少点“坑”
    多款控件新版发布,新特性抢鲜知
    如何选择前端框架:ANGULAR VS EMBER VS REACT
    推荐10个很棒的AngularJS学习指南
    Top 15 不起眼却有大作用的 .NET功能集
    最全数据结构详述: List VS IEnumerable VS IQueryable VS ICollection VS IDictionary
  • 原文地址:https://www.cnblogs.com/yinzhengjie/p/8946998.html
Copyright © 2020-2023  润新知