【Java】第二十六节 详述HashSet add方法

详述HashSet add方法
  • 调用HashSet无参构造方法创建HashSet对象底层是为HashSet中的全局变量map赋值了一个HashMap对象的引用,HashSet无参构造方法源码如下:
    public HashSet() {
       map = new HashMap<>();
    }
    
  • HashSet添加元素的add()方法底层调用了HashMap的put()方法,实质是添加在了HashMap的key,而value位置是一个全局常量,add()方法源码如下:
    public boolean add(E e) {
    	return map.put(e, PRESENT)==null;
    }
    
  • HashMap的put()方法内部又调用了putVal()方法,put()方法源码如下:
    public V put(K key, V value) {
    	return putVal(hash(key), key, value, false, true);
    }
    
  • putVal()方法源码分以下两种情况进行分析(部分注释为本人在此处添加,并非源码原有)
1、如果HashSet中存放的是String类型或者是基本数据类型的包装类
  • 例:
    public class Test {
    	public static void main(String[] args) {
    		HashSet<String> set = new HashSet<String>();	
    		set.add("Tom");
    		set.add(new String("Tom"));
    		System.out.println(set.size());		//1
    	}
    }
    
  • 则执行“set.add(“Tom”);”流程如下:
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
         //定义了几个局部变量
         Node<K,V>[] tab; Node<K,V> p; int n, i;
         
         if ((tab = table) == null || (n = tab.length) == 0)
    	   /**
    	     * 这里的table只进行了声明而没有赋值“transient Node<K,V>[] table;”
    	     * 因此这里是null,因此第一个逻辑表达式为真,第二个逻辑表达式不执行
    		 * 整个逻辑表达式为真,所以执行if语句内部的代码
    		 * resize()方法内给table赋值了一个Node<K,V>[]对象的引用,该语句且将该对象引用返回给了tab
    		 * Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    		 * table = newTab;
    		 * ...
    		 * return newTab;
    		 * 该语句又将赋值过的tab数组的长度赋值给了n,默认是16
    	     */
             n = (tab = resize()).length;
         if ((p = tab[i = (n - 1) & hash]) == null)
    	   	/**
    	   	 * 将传进来的字符串计算出来的hash值乘以15并赋值给i
    	   	 * 将tab数组中第i个位置的对象引用赋值给p
    	   	 * 判断p是否为空,此时返回真,所以执行if语句内部的代码
    	   	 * 创建调用newNode方法创建新的节点放在tab数组i的位置
    	   	 */
             tab[i] = newNode(hash, key, value, null);
         else {
         	//if执行了所以这里跳过
             Node<K,V> e; K k;
             if (p.hash == hash &&
                 ((k = p.key) == key || (key != null && key.equals(k))))
                 e = p;
             else if (p instanceof TreeNode)
                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
             else {
                 for (int binCount = 0; ; ++binCount) {
                     if ((e = p.next) == null) {
                         p.next = newNode(hash, key, value, null);
                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                             treeifyBin(tab, hash);
                         break;
                     }
                     if (e.hash == hash &&
                         ((k = e.key) == key || (key != null && key.equals(k))))
                         break;
                     p = e;
                 }
             }
             if (e != null) { // existing mapping for key
                 V oldValue = e.value;
                 if (!onlyIfAbsent || oldValue == null)
                     e.value = value;
                 afterNodeAccess(e);
                 return oldValue;
             }
         }
         ++modCount;
         if (++size > threshold)
             resize();
         afterNodeInsertion(evict);
         //返回null,add()方法收到null,判断是否为null,是则返回true,表示添加成功
         return null;
    }
    
  • 执行“set.add(new String(“Tom”));”流程如下:
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
     		//定义了几个局部变量
          Node<K,V>[] tab; Node<K,V> p; int n, i;
          
          if ((tab = table) == null || (n = tab.length) == 0)
          	/**
            * 这里的table执行“set.add("Tom");”的时候已经赋值,所以不为null
            * 因此第一个逻辑表达式为假同时将上次的table赋值给了tab
            * 第二个逻辑表达式执行为假同时将tab数组的长度赋值给了n
    	 	* 整个逻辑表达式为假,所以不执行if语句内部的代码
            */
              n = (tab = resize()).length;
          if ((p = tab[i = (n - 1) & hash]) == null)
          	/**
          	 * 将传进来的字符串计算出来的hash值乘以15并赋值给i
          	 * 将tab数组中第i个位置的对象引用赋值给p
          	 * 判断p是否为空,由于上次传入的字符串与这次传入的字符串内容相同
          	 * String类重写了hashCode()方法,内容相同则哈希值也相等,哈希值相同则计算出来的i也相等
          	 * 由于上次该位置已经添加了一个对象,所以此位置此时不为null,返回假,所以不执行if语句内部的代码
          	 */
              tab[i] = newNode(hash, key, value, null);
          else {
          	//if没执行所以这里执行
              Node<K,V> e; K k;
              if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
              	/**
              	 * 内容相同的字符串的哈希值相同,但由于这次传入的字符串是新创建的
              	 * 所以两者地址不同,这次传入的字符串不为null,所以此逻辑表达式返回true
              	 * 将p,也就是tab数组中i位置的对象引用赋值给e
              	 */
                  e = p;
              else if (p instanceof TreeNode)
              	//if执行此处不执行
                  e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
              else {
              	//if执行此处不执行
                  for (int binCount = 0; ; ++binCount) {
                      if ((e = p.next) == null) {
                          p.next = newNode(hash, key, value, null);
                          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                              treeifyBin(tab, hash);
                          break;
                      }
                      if (e.hash == hash &&
                          ((k = e.key) == key || (key != null && key.equals(k))))
                          break;
                      p = e;
                  }
              }
              if (e != null) { // existing mapping for key
              	/**
              	 * 由于此时e指向的是tab数组中i位置的对象,所以不为空,if中的代码将被执行
              	 * 将e的value赋值给oldValue
              	 */
                  V oldValue = e.value;
                  if (!onlyIfAbsent || oldValue == null)
                      e.value = value;
                  afterNodeAccess(e);
                  /**
                   * 返回oldValue,oldValue不为空,所以返回给putVal(hash(key), key, value, false, true)的值不为空
                   * 进而返回给map.put(e, PRESENT)的值也不为空,此时判断是否为空,结果为false,表明添加失败
                   * 此句结束该方法
                   */
                  return oldValue;
              }
          }
          ++modCount;
          if (++size > threshold)
              resize();
          afterNodeInsertion(evict);
          return null;
    }
    
2、如果HashSet中存放的是自定义数据类型
  • 例1,自定义类没有重写hashCode()方法和equals()方法:
    import java.util.HashSet;
    class Student{
    	private String id;
    	public Student(String id) {
    		this.id = id;
    	}
    }
    public class Test {
    	public static void main(String[] args) {
    		HashSet<Student> set = new HashSet<Student>();
    		set.add(new Student("110"));
    		set.add(new Student("110"));
    		System.out.println(set.size());
    	}
    }
    
  • 执行第一个“set.add(new Student(“110”));”语句时:与上面第一次添加字符串时流程相似
  • 执行第二个“set.add(new Student(“110”));”语句时:
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    	//定义了几个局部变量
          Node<K,V>[] tab; Node<K,V> p; int n, i;
          
          if ((tab = table) == null || (n = tab.length) == 0)
          	/**
            * 这里的table执行“set.add("Tom");”的时候已经赋值,所以不为null
            * 因此第一个逻辑表达式为假同时将上次的table赋值给了tab
            * 第二个逻辑表达式执行为假同时将tab数组的长度赋值给了n
    	 	* 整个逻辑表达式为假,所以不执行if语句内部的代码
            */
              n = (tab = resize()).length;
          if ((p = tab[i = (n - 1) & hash]) == null)
          	/**
          	 * 将传进来的字符串计算出来的hash值乘以15并赋值给i
          	 * 将tab数组中第i个位置的对象引用赋值给p
          	 * 判断p是否为空,由于上次传入的字符串与这次传入的字符串内容相同
          	 * Student类没有重写hashCode()方法,所以地址不同相同则哈希值也不相等,哈希值不相同则计算出来的i也不相等
          	 * 所以此位置此时为null,返回真,所以执行if语句内部的代码
          	 * 创建一个新的对象放在tab数组的i位置上
          	 */
              tab[i] = newNode(hash, key, value, null);
          else {
          	//if执行了所以这里不执行
              Node<K,V> e; K k;
              if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                  e = p;
              else if (p instanceof TreeNode)
                  e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
              else {
                  for (int binCount = 0; ; ++binCount) {
                      if ((e = p.next) == null) {
                          p.next = newNode(hash, key, value, null);
                          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                              treeifyBin(tab, hash);
                          break;
                      }
                      if (e.hash == hash &&
                          ((k = e.key) == key || (key != null && key.equals(k))))
                          break;
                      p = e;
                  }
              }
              if (e != null) { // existing mapping for key
                  V oldValue = e.value;
                  if (!onlyIfAbsent || oldValue == null)
                      e.value = value;
                  afterNodeAccess(e);
                  return oldValue;
              }
          }
          ++modCount;
          if (++size > threshold)
              resize();
          afterNodeInsertion(evict);
          //返回null,在add()方法中判断是null,并返回true表示添加成功
          return null;
    }
    
  • 例2,自定义类重写了hashCode()方法和equals()方法:
    import java.util.HashSet;
    class Student{
    		private String id;
    		public Student(String id) {
    			this.id = id;
    		}
    		@Override
    		public int hashCode() {
    			return id.hashCode();
    		}
    		@Override
    		public boolean equals(Object obj) {
    			if(obj instanceof Student) {
    				Student student = (Student)obj;
    				return this.id.equals(student.id);
    			}
    			return false;
    		}
    }
    public class Test {
    	public static void main(String[] args) {
    		HashSet<Student> set = new HashSet<Student>();
    		set.add(new Student("110"));
    		set.add(new Student("110"));
    		System.out.println(set.size());
    	}
    }
    
  • 执行第一个“set.add(new Student(“110”));”语句时:与上面第一次添加字符串时流程相似
  • 执行第二个“set.add(new Student(“110”));”语句时:
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    	//定义了几个局部变量
      Node<K,V>[] tab; Node<K,V> p; int n, i;
      
      if ((tab = table) == null || (n = tab.length) == 0)
      	/**
        * 这里的table执行“set.add("Tom");”的时候已经赋值,所以不为null
        * 因此第一个逻辑表达式为假同时将上次的table赋值给了tab
        * 第二个逻辑表达式为假同时将tab数组的长度赋值给了n
     	* 整个逻辑表达式为假,所以不执行if语句内部的代码
        */
          n = (tab = resize()).length;
      if ((p = tab[i = (n - 1) & hash]) == null)
      	/**
      	 * 将传进来的字符串计算出来的hash值乘以15并赋值给i
      	 * 将tab数组中第i个位置的对象引用赋值给p
      	 * 判断p是否为空,由于上次传入的字符串与这次传入的字符串内容相同
      	 * Student类没有重写了hashCode()方法,所以上次的对象和这次的对象哈希值相等,哈希值相同则计算出来的i也相等
      	 * 所以此位置此时不为null,返回假,所以不执行if语句内部的代码
      	 */
          tab[i] = newNode(hash, key, value, null);
      else {
      	//if没执行所以这里执行
          Node<K,V> e; K k;
          if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
        	  /**
        	   * 上次传入的对象和这次传入的对象哈希值相同,并且Student类重写了equals()方法
        	   * 两次传入的对象equals()比较后为真,所以整个表达式为真
        	   * 将p,也就是tab数组第i个位置上的对象引用赋值给e
        	   */
              e = p;
          else if (p instanceof TreeNode)
        	  //if执行此处不执行
              e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
          else {
        	  //if执行此处不执行
              for (int binCount = 0; ; ++binCount) {
                  if ((e = p.next) == null) {
                      p.next = newNode(hash, key, value, null);
                      if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                          treeifyBin(tab, hash);
                      break;
                  }
                  if (e.hash == hash &&
                      ((k = e.key) == key || (key != null && key.equals(k))))
                      break;
                  p = e;
              }
          }
          if (e != null) { // existing mapping for key
        	  /**
        	   * 经判断e确实不为null,所以返回真,执行下面的代码
        	   * 将e的value赋值给oldValue,此时oldValue不为null
        	   */
              V oldValue = e.value;
              if (!onlyIfAbsent || oldValue == null)
                  e.value = value;
              afterNodeAccess(e);
              /**
               * 返回的oldValue不为null因此add()方法返回"oldValue == null",即false,表示添加失败
               * 此句执行完毕结束当前方法
               */
              return oldValue;
          }
      }
      ++modCount;
      if (++size > threshold)
          resize();
      afterNodeInsertion(evict);
      return null;
    }
    
  • 注:由于HashSet的值是存储在HashMap的key位置上的,HashSet的add()方法底层调用的是HashMap()方法的put()方法,因此其值不允许重复。判断是否重复的两个重要的标准就是hashCode()和equals()两个方法。
    • 如果HashSet存储的是String类型:如果内容相同,则哈希值也相同;equals()比较两个内容相同的字符串返回true
    • 如果HashSet存储的是基本数据类型的包装类:如果值相同,则哈希值也相同;equals()比较两个相同的数值返回true
    • 如果HashSet存储的是自定义数据类型:如果没有重写hashCode()方法,则地址不同,哈希值也不同,可以重写hashCode()方法来规定自定义数据类型的哈希值;equals()比较两个自定义数据类型默认比较的是地址,而不同的对象有不同的地址,因此可以重写equals()方法来规定两个对象在满足什么条件时认定它们是同一个对象
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值