热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

JAVAHashSet和TreeSet实现保证存入元素不会重复

这篇文章运用简单易懂的例子给大家介绍JAVAHashSet和TreeSet实现保证存入元素不会重复,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对

这篇文章运用简单易懂的例子给大家介绍JAVA HashSet和TreeSet 实现保证存入元素不会重复,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。

Set是一种数据集合。它与List同样继承与Collection接口。

它与Collection接口中的方法基本一致,并没有对Collection接口进行功能进行功能上的扩充,只是比Collection接口更严格了。与List不同的是,Set中的元素是无无需的,并且都以某种规则保证存入的元素不会出现重复。

它的特点也就是:

1. 元素不会出现重复。

2. 元素是无序的。(存取无序)

3. 元素可以为空。

每种类型的Set所使用的避免元素重复的规则都是不同的,今天我们主要还是看HashSet和TreeSet:

第一种是HashSet:

HashSet

我们先来看看HashSet的构造器是怎么样的:

static final long serialVersiOnUID= -5024744406713321676L;
 
  private transient HashMap map;
 
  // Dummy value to associate with an Object in the backing Map
  private static final Object PRESENT = new Object();
 
  /**
   * Constructs a new, empty set; the backing HashMap instance has
   * default initial capacity (16) and load factor (0.75).
   */
  public HashSet() {
    map = new HashMap<>();
  }

令人惊讶的是HashSet的结构里实际上就包含了一个HashMap,而初始化HashSet就是给这个对象的Map赋值一个空HashMap对象。

再让我们来看一看插入操作:

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

add操作实际上是向map中插入了一条记录,是以我们所要存的元素为key,以一个空对象为value的记录。

到了这不实际上我们已经能明白,set里的元素是不可能重复的,因为我们对hashMap同一个key进行put,并不会生成新的记录,而是对上一条记录进行覆盖而已。但是hashMap是如何判断Key是否是同一个的呢?让我们来看以下代码

public class SetTest {
 
 public class Obj {
 public String name;
 public Obj(String name) {
  this.name=name;
 }
 }
 
 public static void main(String[] args) {
 Set strSet = new HashSet();
 String str1 = new String("123");
 String str2 = new String("123");
 strSet.add(str1);
 strSet.add(str2);
 System.out.println(str1 == str2);
 for(String str : strSet) {
  System.out.println(str);
 }
 Set objSet = new HashSet();
 Obj o1 = new SetTest().new Obj("1");
 Obj o2 = new SetTest().new Obj("1");
 objSet.add(o1);
 objSet.add(o2);
 for(Obj str : objSet) {
  System.out.println(str.name);
 } 
 } 
}

结果为:

false
123
1
1

那让我们继续看看,在put方法中java代码又干了什么呢?(汗,感觉我从Set讲到HashMap去了)

 public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
  }

在下一层的代码里,先对key本身进行了一个转化hash(key),这个方法的源码是:

 static final int hash(Object key) {
    int h;
    return (key == null) &#63; 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

判断key是否为空,如果为空就返回0,不然就对key值取hashCode并与h>>>16的值做异或操作,异或是一种位运算,在此就不做解释了。而>>>是一种位移操作, 在这个hash()方法里,实际上是生成了这个key值对应的hash值。这里做了什么计算,我准备放到另一篇博客里进行讨论,无论怎么样,我们都知道对hashmap put相同的key值,不会重复的,这个是由HashMap的机制由hashCode也就是Hash码解决的,关于HashMap的结构和具体方法,我会在另外一篇博客中单独列出。

TreeSet

当我们new 一个TreeSet的时候,实际上是创建了一个TreeMap,并将这个TreeMap赋值给了TreeSet对象的m.

 /**
   * The backing map.
   */
  private transient NavigableMap m;
 
  // Dummy value to associate with an Object in the backing Map
  private static final Object PRESENT = new Object();
 
  /**
   * Constructs a set backed by the specified navigable map.
   */
  TreeSet(NavigableMap m) {
    this.m = m;
  }
/**
   * Constructs a new, empty tree set, sorted according to the
   * natural ordering of its elements. All elements inserted into
   * the set must implement the {@link Comparable} interface.
   * Furthermore, all such elements must be mutually
   * comparable: {@code e1.compareTo(e2)} must not throw a
   * {@code ClassCastException} for any elements {@code e1} and
   * {@code e2} in the set. If the user attempts to add an element
   * to the set that violates this constraint (for example, the user
   * attempts to add a string element to a set whose elements are
   * integers), the {@code add} call will throw a
   * {@code ClassCastException}.
   */
  public TreeSet() {
    this(new TreeMap()); // 将一个新生成的TreeMap空对象赋值给m,也就是上一方法
  }

而用这个构造器定义的TreeMap是没有指定对比器的:

 public TreeMap() {
    comparator = null;
  }

让我们来看一下TreeSet的add方法的全过程:

public boolean add(E e) {
    return m.put(e, PRESENT)==null; // 如果返回值为空则表示我们插入了一个新的元素,如果返回值为非空,则表明我们插入的元素已经存在。
  }

实际上也就是向TreeMap以你的要放入的元素为key, 空对象为value做一次put。

 public V put(K key, V value) {
    Entry t = root; // 定义t为根节点
    if (t == null) { // 如果根节点为空
      compare(key, key); // type (and possibly null) check // 对自身做对比,如果有对比器就用对比器的规则进行对比,如果没有,就用元素自身对比的规则进行对比。为0则相等。我觉得这波其实没有意义,就是一个空的对比。
 
      root = new Entry<>(key, value, null); // 新建一个空的根节点
      size = 1; // 设置大小为1
      modCount++; //对0做+1
      return null; // 返回空值,表示插入成功。
    }
    int cmp;
    Entry parent;
    // split comparator and comparable paths
    Comparator<&#63; super K> cpr = comparator; // 用本treeMap的对比器对cpr赋值
    if (cpr != null) { // 如果定义的对比器不为空(在TreeSet里是为空的,我们之间说到过)
      do {
        parent = t; 
        cmp = cpr.compare(key, t.key);
        if (cmp <0)
          t = t.left;
        else if (cmp > 0)
          t = t.right;
        else
          return t.setValue(value);
      } while (t != null);
    }
    else { // 如果对比器为空(在这种情况下是为空的)
      if (key == null) // 如果key为空就抛出错误
        throw new NullPointerException();
      @SuppressWarnings("unchecked")
      Comparable<&#63; super K> k = (Comparable<&#63; super K>) key;// 生成可比较的对象Comparable
      do {
        parent = t; 将父节点(最初是根节点)赋值给parent
        cmp = k.compareTo(t.key); //对我们要插入的key与根节点的keyj进行对比
        if (cmp <0) // 对比后值小于0,则表示我们插入的key小于根节点的key,就让父节点往左走,并循环直至命中
          t = t.left;
        else if (cmp > 0) // 对比后值大于0,则表示我们插入的key小于根节点的key,就让父节点往右走,并循环直至命中
          t = t.right;
        else //当命中,用我们的值替换原有的值一次保证不插入重复的key,并返回替换后的对象
          return t.setValue(value);
      } while (t != null);
    }
    Entry e = new Entry<>(key, value, parent); // 如果没有在树中命中,则新生成一个树节点此时parent的父节点已经遍历到了某个叶子节点。
    if (cmp <0) // 如果你的这个值是小于叶子节点的,则插入左边,大于则插入右边
      parent.left = e;
    else
      parent.right = e;
    fixAfterInsertion(e); // 对整棵树做平衡修正
    size++; // size值加1表示我们插入了一个值
    modCount++; // modCount也加1
    return null;
  }

整个过程就是:

1. 先查看根节点是否存在,如果不存在就直接吧这个节点放在根节点上。

2. 如果根节点存在就依顺序向下查找,如果找到对应的节点,就把该节点的值替换。

3. 如果遍历到了叶子节点仍然没有命中,那么就向叶子节点插入一个子节点,小就在左边大就在右边。

因为TreeSet插入的值都是空对象,只有key是有效的,key又是相等就覆盖,所以不会重复

关于JAVA HashSet和TreeSet 实现保证存入元素不会重复就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。


推荐阅读
  • andr ... [详细]
  • 本文详细介绍如何在VSCode中配置自定义代码片段,使其具备与IDEA相似的代码生成快捷键功能。通过具体的Java和HTML代码片段示例,展示配置步骤及效果。 ... [详细]
  • 在 Flutter 开发过程中,开发者经常会遇到 Widget 构造函数中的可选参数 Key。对于初学者来说,理解 Key 的作用和使用场景可能是一个挑战。本文将详细探讨 Key 的概念及其应用场景,并通过实例帮助你更好地掌握这一重要工具。 ... [详细]
  • 深入理解Redis的数据结构与对象系统
    本文详细探讨了Redis中的数据结构和对象系统的实现,包括字符串、列表、集合、哈希表和有序集合等五种核心对象类型,以及它们所使用的底层数据结构。通过分析源码和相关文献,帮助读者更好地理解Redis的设计原理。 ... [详细]
  • 本文详细介绍了Java中org.w3c.dom.Text类的splitText()方法,通过多个代码示例展示了其实际应用。该方法用于将文本节点在指定位置拆分为两个节点,并保持在文档树中。 ... [详细]
  • 从 .NET 转 Java 的自学之路:IO 流基础篇
    本文详细介绍了 Java 中的 IO 流,包括字节流和字符流的基本概念及其操作方式。探讨了如何处理不同类型的文件数据,并结合编码机制确保字符数据的正确读写。同时,文中还涵盖了装饰设计模式的应用,以及多种常见的 IO 操作实例。 ... [详细]
  • 使用GDI的一些AIP函数我们可以轻易的绘制出简 ... [详细]
  • 深入了解 Windows 窗体中的 SplitContainer 控件
    SplitContainer 控件是 Windows 窗体中的一种复合控件,由两个可调整大小的面板和一个可移动的拆分条组成。本文将详细介绍其功能、属性以及如何通过编程方式创建复杂的用户界面。 ... [详细]
  • 实体映射最强工具类:MapStruct真香 ... [详细]
  • 本文探讨了在Java多线程环境下,如何确保具有相同key值的线程能够互斥执行并按顺序输出结果。通过优化代码结构和使用线程安全的数据结构,我们解决了线程同步问题,并实现了预期的并发行为。 ... [详细]
  • 本文介绍了Android开发中Intent的基本概念及其在不同Activity之间的数据传递方式,详细展示了如何通过Intent实现Activity间的跳转和数据传输。 ... [详细]
  • 并发编程:深入理解设计原理与优化
    本文探讨了并发编程中的关键设计原则,特别是Java内存模型(JMM)的happens-before规则及其对多线程编程的影响。文章详细介绍了DCL双重检查锁定模式的问题及解决方案,并总结了不同处理器和内存模型之间的关系,旨在为程序员提供更深入的理解和最佳实践。 ... [详细]
  • 本题探讨如何通过最大流算法解决农场排水系统的设计问题。题目要求计算从水源点到汇合点的最大水流速率,使用经典的EK(Edmonds-Karp)和Dinic算法进行求解。 ... [详细]
  • 本文探讨了在Java中实现系统托盘最小化的两种方法:使用SWT库和JDK6自带的功能。通过这两种方式,开发者可以创建跨平台的应用程序,使窗口能够最小化到系统托盘,并提供丰富的交互功能。 ... [详细]
  • Struts与Spring框架的集成指南
    本文详细介绍了如何将Struts和Spring两个流行的Java Web开发框架进行整合,涵盖从环境配置到代码实现的具体步骤。 ... [详细]
author-avatar
手机用户2502911627_202
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有