java程序员从笨鸟到菜鸟之(二十六)集合之Set接口的子实现类HashSet,TreeSet

本文详细介绍了Set集合的概念、特点及其实现原理,包括HashSet如何保证元素唯一性及其子类LinkedHashSet的特点。并通过实例展示了如何使用这些集合。

Set接口

Set集合继承自Collection集合

类似数学中的集合概念;特点:集合中元素不能重复,且元素无序排列

无序排列理解:元素所在的位置与向集合中添加元素的顺序没有关系

元素不重复原因:底层数据结构是一个哈希表,能保证元素是唯一的,元素不重复!!!
它通过它的接口子实现类HashSet集合去实例化,而HashSet集合底层是HashMap集合的实例!!!

实例1  

package org.westos_01;

import java.util.HashSet;
import java.util.Set;

public class SetDemo {
	/**
	 * 说明问题:
	 * 1)Set集合元素不重复(从打印的内容以及集合元素个数)
	 * 2)集合元素无序(添加的顺序与打印的顺序)
	 * 3)添加、增、删、查询方法的使用---这四个方法的返回值类型都是boolean类型
	 * 4)Set集合存储字符串元素并遍历---for增强循环(jdk5后的版本)
	 */
	public static void main(String[] args) {
		
		//创建Set集合对象
		Set<String> set = new HashSet<String>() ;
		
		//添加元素(增)
		set.add("hello");
		set.add("java") ;
		set.add("java") ;
		set.add("world") ;
		set.add("world") ;
		set.add("world") ;
		System.out.println("集合中添加的元素:"+set);//打印的是内容(原因:传入参数类型是String,底层重写了toString()方法)
		System.out.println("集合中元素的个数:"+set.size());
		//增强for遍历
		for(String s :set){
			System.out.println(s);
		}
		//删除元素(删)
		System.out.println("是否成功删除:"+set.remove("hello"));//删除是否成功
		System.out.println("删除后集合中的元素:"+set);
		//查询
		System.out.println("是否包含此元素:"+set.contains("hello"));
		
	}
}

HashSet类

描述:按照哈希算法来存取集合的元素,此类实现Set 接口,由哈希表(实际上是一个 HashMap 实例)支持

特点:它不保证 Set 的迭代顺序;元素唯一(不重复)

注:HashSet有一个子类:LinkedHashSet类(下面会提到)

存取和查找的机理

机理:当向集合中添加一个元素时,HashSet会调用对象的hashCode()方法获取哈希码,然后根据这个哈希码进一步计算出对象在集合中存放的位置。

问题1  为什么能保证HashSet集合元素唯一?

当创建一个HashSet集合的对象时,会先调用其构造方法(蓝框标记)看源码1

即:

 

源码说明:可以看到实际上是创建了一个HashMap类对象的实例,map是HashMap类对象的实例

此时add()添加元素,看对应的源码2


源码说明:其实是调用了HashMap的put方法

看HashMap的put()方法的源码3


源码说明:调用了HashMap中的hash()方法和putVal()方法

看hash方法(重要)的源码4


源码说明:携带hashSet集合元素key,调用对象(传入参数类型)的hashCode()方法,进行位运算;涉及到Object类hashCode()方法重写

看putVal()方法(具体用处不知,后续会提到)源码5

//HashMap类中的putVal方法
public class Test1 {
	final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
		Node<K, V>[] tab;
		Node<K, V> p;
		int n, i;
		//1---首先判断hash表是否是空的,如果空,则resize扩容
		if ((tab = table) == null || (n = tab.length) == 0) 
			n = (tab = resize()).length;
		//2---通过key计算得到hash表下标,如果下标处为null,就新建链表头结点,在方法最后插入即可
		if ((p = tab[i = (n - 1) & hash]) == null) 
			tab[i] = newNode(hash, key, value, null);
		//3---如果下标处已经存在节点,则进入到这里
		else { 
			Node<K, V> e;
			K k;
			//4---先看hash表该处的头结点是否和key一样(hashcode和equals比较),一样就更新
			if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) 
				e = p;
			//5---hash表头结点和key不一样,则判断节点是不是红黑树,是红黑树就按照红黑树(jdk8的特性)处理
			else if (p instanceof TreeNode)//TreeNode(后续会提到)
				e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
			//6---如果不是红黑树,则按照之前的hashmap原理处理
			else { 
				//1)遍历链表
				for (int binCount = 0;; ++binCount) { 
					if ((e = p.next) == null) {
						p.next = newNode(hash, key, value, null);
						//2)显然当链表长度大于等于7的时候,也就是说大于8(由于插入一个元素)的话,
						//就转化为红黑树结构,针对红黑树进行插入(logn复杂度)
						if (binCount >= TREEIFY_THRESHOLD - 1)//TREEIFY_THRESHOLD常量字段为8								
							treeifyBin(tab, hash);//treeifyBin(后续会提到)
						break;
					}
				//3)如果hash码相同,则调用相应的集合元素(元素参数类型)的equals()方法判断,涉及到重写
					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;//如果没有找到该key(元素)的结点,则执行插入操作,需要对modCount增1。
		if (++size > threshold) // 在执行插入操作之后,如果size大于threshold容量,要扩容
			resize();
		afterNodeInsertion(evict);
		return null;
	}
}

源码说明:调用了元素类型的equals()方法,涉及equals()方法的重写

源码6----table 哈希表


源码说明:当new HashMap实例时,并没有初始化其成员变量table(也即并没有为table分配内存)。只有当put元素时才通过resize方法对table进行初始化

源码7----resize()方法(随后补充)

总结:如果自定义类型,必须重写hashCode()和equals()方法------快捷键生成---ctrl+alt+s+h

对问题1的解答

      HashSet集合的add()方法,底层是依赖于双列集合HashMap<K,V>的put(K key,V value)来实现的,而put(K key,V value)方法底层的实现又依赖于HashCode()和equals()方法,传递添加元素的时候,首先判断的是每一个元素对应的HashCode值是否一样,如果HashCode值一样,还比较他们的equals()方法,由于现在集合存储的是String类型,String类型本身重写了equals()方法,所以,默认比较的是内容是否相同,如果内容相同,这里最终返回的就是第一次存储的那个元素,由这两个方法保证元素唯一性!

相关链接:1 点击打开链接(比较好),2  点击打开链接,3  点击打开链接,4  点击打开链接,5  点击打开链接,6  点击打开链接

HashSet的子实现类LinkedHashSet

特点:具有可预知迭代顺序的Set接口的哈希表和链接列表实现。
原因:
        1  由哈希表保证元素的唯一性
        2  由链接列表来保证元素的有序性(保存插入时的顺序)

实例2

package org.westos_03;

import java.util.LinkedHashSet;

public class LinkedHashSetDemo {
	
	public static void main(String[] args) {
		
		//创建LinkedHashSet集合对象
		LinkedHashSet<String> link = new LinkedHashSet<String>();
		
		//给集合中添加元素
		link.add("hello") ;
		link.add("world") ;
		link.add("world") ;
		link.add("Java") ;
		link.add("Java") ;
		link.add("JavaWeb") ;
		link.add("JavaWeb") ;
		
		//遍历集合
		for(String s: link){
			System.out.println(s);
		}
	}
}




 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值