13_Set集合

  • Post author:
  • Post category:其他




Set集合



1.基础介绍

属于Collection的子类接口
Set集合存储数据的特点:无序,不可重复。取决于hash后,再确定索引的结果。
1.为什么重写了equals必须重写hashcode方法?
    Set集合在判断2个对象是否相等的时候
    <1>.先检查hashcode值是否相等(默认情况下一个唯一的值,内存地址就是由hashcode的16进制组成)
    不相等,那么说明2个对象不相等。(不会去判断equals方法了)
    如果相等才回去判断equals是否相等
    <2>如果equals相等则2个对象就是相等的。
    
结论:
<1>.equals相等的2个对象 hashCode一定相等
<2>.hashCode相等的2个对象,equals不一定相等
    
    
问题一:如果我只重写equals方法? Set 如何去执行add方法的?
    equals(Object o){
    	属性全部相等return true...
	}
	没有重写hashcode(hashcode是系统分配唯一)
    set.add(new User(18,"jack"));//分配一个唯一hashcode值
 	set.add(new User(18,"jack"));//分配一个唯一hashcode值
 因为只重写了equals没有重写hashcode方法,导致2个对象hashcode值不一样,判断的时候直接认为2个对象不相等,equals方法不会执行了。
  
Set添加元素的过程:
    1.先获取元素的哈希值(hashCode方法)
    2.对哈希值进行运算,得出一个索引值即为要存放在哈希表中的位置号
    3.如果该位置上没有其他元素,则直接存放;如果有其他元素,则需要equals进行判断,如果相等,则不再添加,如果不相等,则以链表的方式添加。
  
 无序:指的是不会按照添加的顺序排列。
     (其实set集合是有顺序的,HashSet会默认按照数据的hash值进行顺序排列) ;//跟数据大小无关
	(TreeSet也有默认顺序的,会按照大小顺序进行排列顺序排列);//跟数据的大小是由关系的
	如果使用TreeSet存储数据(自定义类型 Student,User)可以存方法吗? 比较器
        
不可重复:不能添加重复的元素
    执行添加的时候先判断hashcode 相等了 才会去判断equals 如果都相等,则判断是重复,不会添加,如果不相等才会添加。
    
有关hashSet扩容:
    我们都明白扩容因子是0.75,当数组大小为16时,存放12个元素后,再次存放下一个元素时,就需要扩容了。
    这是的12个元素不是指数组16个格子用了12个,而是真的指存放了12个元素,哪怕这12个元素都接在一个链表上。



2.HashSet的应用

HashSet:底层是由HashMap实现(HashMap中的Key)
	public static void main(String[] args) {
		HashSet<String> set=new HashSet<>();
		//1.添加,插入数据(自动去除重复的元素)
		set.add("jack");
		set.add("apple");
		set.add("china");
		set.add("jack");
		set.add("apple");
		set.add("china");
		set.add("rouse");
		//2.根据元素从集合中删除
		set.remove("jack");
		//3.修改元素
		set.remove("apple");
		set.add("Apple");
		System.out.println("获取集合的长度:"+set.size());//返回元素的个数
		//4.遍历元素(只能根据增强for循环以及迭代器遍历集合)
		System.out.println("-----------------增强for循环------------------");
		for(String str:set) {
			System.out.println(str);
		}
		System.out.println("---------------迭代器---------------");
		Iterator<String> it=set.iterator();
		while(it.hasNext()) {
			System.out.println("元素为:"+it.next());
		}
		System.out.println("------------------------------");
		System.out.println(set);
		
		System.out.println("--------------------------");
		
		//5.批量添加数据至集合
		List<String> list=new ArrayList<String>();
		list.add("赵云");
		list.add("关羽");
		list.add("赵云");
		list.add("赵云");
		list.add("关羽");
		list.add("关羽");
		set.addAll(list);
		System.out.println(set);
	}



2.1分析HashSet源码:

public static void main(String[] args) {
    HashSet hashSet = new HashSet(); hashSet.add("java");//到此位置,第 1 次 add 分析完毕. 
    hashSet.add("php");//到此位置,第 2 次 add 分析完毕 
    hashSet.add("java"); 
    System.out.println("set=" + hashSet);
}

1. 执行 HashSet() 
public HashSet() { 
    map = new HashMap<>(); 
}

2. 执行 add() 
public boolean add(E e) {//e = "java" 
    //(static) PRESENT = new Object();
    哨兵value,只是占位map(k,v)的v的参数,没有意义。
    return map.put(e,PRESENT)==null; 
}

3.执行 put() , 该方法会执行 hash(key) 得到 key 对应的 hash 值
算法:h = key.hashCode()) ^ (h >>> 16) 
算数右移16位。此hash值不是hashcode因为做了右移处理。
public V put(K key, V value) {
    //key = "java" value = PRESENT 共享 
    return putVal(hash(key), key, value, false, true); 
}

4.执行putVal,最难的部分
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    //定义了辅助变量
    Node<K,V>[] tab; Node<K,V> p; int n, i; 
    
    //table 就是 HashMap  的一个数组,类型是 Node[]
    //if 语句表示如果当前 table 是 null, 或者 大小=0
    //就是第一次扩容,到 16 个空间.
    if ((tab = table) == null || (n = tab.length) == 0) 
        n = (tab = resize()).length;


    (1)根据 key,得到 hash  去计算该 key 应该存放到 table 表的哪个索引位置
    并把这个位置的对象,赋给 p 
    
    (2)判断 p 是否为 null
    (2.1) 如果 p 为 null,表示还没有存放元素,就创建一个 Node(key="java",value=PRESENT) 
    (2.2) 就放在该位置 tab[i] = newNode(hash, key, value, null)
    
    //第二次add直接进入此if
    if ((p = tab[i = (n - 1) & hash]) == null)
        //为什么把key对应的hash存放进去呢?
        //再次存入会查看key对应的hash和下一个相等不相等
        //null表示没有后继结点
        tab[i] = newNode(hash, key, value, null);
    else {
    //第三次add,进入else
    
        //一个开发技巧提示:在需要局部变量(辅助变量)时候,在创建
        Node<K,V> e; K k; 

        如果当前索引位置对应的链表的第一个元素和准备添加的 key 的 hash 值一样
        并且满足 下面两个条件之一:
            (1) 准备加入的 key  和 p  指向的 Node结点的 key  是同一个对象
            (2) p指向的 Node结点的 key 的 equals() 和准备加入的 key 比较后相同
        就不能加入

        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;

        再判断 p 是不是一颗红黑树,
        如果是一颗红黑树,就调用 putTreeVal ,来进行添加
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            如果 table 对应索引位置,已经是一个链表,  就使用 for 循环比较
            (1) 依次和该链表的每一个元素比较后,都不相同, 则加入到该链表的最后
            注意在把元素添加到链表后,立即判断:该链表是否已经达到 8 个结点就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树)
            
            注意,在转成红黑树时,要进行判断, 判断条件
            if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
                resize();
            如果上面条件成立,先 table 扩容.
 
            只有上面条件不成立时,才进行转成红黑树
 
            (2) 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接 break

            for (int binCount = 0; ; ++binCount) {
                //如果添加的元素,Set中没有的话,就走这,然后break
                if ((e = p.next) == null) {

                    p.next = newNode(hash, key, value, null);

                    if (binCount >= TREEIFY_THRESHOLD(8) - 1) // -1 for 1st

                        treeifyBin(tab, hash);

                    break;
                }
                //如果已经存在的话,for循环到这直接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;
    //size 就是我们每加入一个结点 Node(k,v,h,next), size++

    if (++size > threshold)
        resize();//扩容
        //空方法,留给子类使用。
    afterNodeInsertion(evict); 
    return null;
}



3.LinkedHashSet的应用




3.1图示:

Set set = new LinkedHashSet();
set.add("AA");
set.add(456);
set.add(new Customer("刘",1001));
set.add(123);
set.add("Java")''




3.2使用

LinkedHashSet:有序的(跟你添加的顺序相同,有序使用链表去实现的)
public static void main(String[] args) {
	LinkedHashSet<String> set=new LinkedHashSet<>();
	set.add("jack");
	set.add("apple");
	set.add("china");
	set.add("jack");
	set.add("apple");
	set.add("china");
	set.add("rouse");
	//1.不仅去重,而且会维持原先的顺序
	System.out.println(set);
	
	set.remove("jack");
	System.out.println(set.contains("rouse")?"存在":"不存在");
	System.out.println("集合的长度:"+set.size());
	System.out.println("删除之后查看数据:"+set);
	
	//2.集合的遍历方式: 增强for循环  迭代器
	//不能使用普通for遍历
	System.out.println("-----------------增强for循环------------------");
	for(String str:set) {
		System.out.println(str);
	}
	System.out.println("---------------迭代器---------------");
	Iterator<String> it=set.iterator();
	while(it.hasNext()) {
		System.out.println(it.next());
	}
	System.out.println("------------------------------");
		
}



4.TreeSet的应用



4.1TreeSet基本使用

TreeSet:底层是由TreeMap实现(TreeMap中的key) 根据数据的大小进行排序(对于自定义类型,默认无法比较大小的,需要借助比较器进行使用TreeSet)
public static void main(String[] args) {
	//1.去重,而且会自动按照数据的大小进行排序
	TreeSet<Integer> ts=new TreeSet<>();
	ts.add(900);
	ts.add(1000);
	ts.add(300);
	ts.add(700);
	ts.add(500);
	ts.add(100);
	ts.add(100);
	ts.add(1000);
	ts.add(900);
	System.out.println(ts);
	ts.remove(700);
	System.out.println("删除之后:");
	System.out.println(ts);
	System.out.println("判断元素是否存在:"+ts.contains(500));
	System.out.println("元素的个数:"+ts.size());
	//1.遍历方式:增强for循环以及迭代器
	for(Integer num:ts) {
		System.out.println(num);
	}
	Iterator<Integer> it=ts.iterator();
	while(it.hasNext()) {
		System.out.println(it.next());
	}
	System.out.println("----------------------------------------");
	//1.如果TreeSet放的是自定义类型 ,那么 自定义类型要么实现Comparable 接口,设置自比较,定义比较规则
	//或者,在创建TreeSet 定义比较器传递Comparator接口的实现类对象,进行定义比较规则
	TreeSet<Student> stuSet=new TreeSet<>(new Comparator<Student>() {
		@Override
		public int compare(Student o1, Student o2) {
			if(o1.equals(o2)) {//如果2个对象相同则不放进去
				return 0;//等于0说明 2个对象 相等
			}else {//如果2个对象不相同 放进去
				return 1;
			}
		}
	});
	
	stuSet.add(new Student("jack1",18));
	stuSet.add(new Student("jack2",19));
	stuSet.add(new Student("jack3",20));
	stuSet.add(new Student("jack4",18));
	stuSet.add(new Student("jack5",21));
	System.out.println(stuSet);
}    



4.2源码解读

-- 1. 当我们使用无参构造器,创建 TreeSet 时,仍然是无序的 
-- 2. 添加的元素,按照字符串大小来排序  
-- 3. 使用 TreeSet 提供的一个构造器,可以传入一个比较器(匿名内部类) 
-- 4.并指定排序规则

请看源码:
1. 构造器把传入的比较器对象,赋给了 TreeSet 的底层的 TreeMap 的属 this.comparator

public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator; 
}

2.调用treeSet.add("tom"),在底层执行
  //cpr 就是我们的匿名内部类(对象)
  if (cpr != null) {
    do {
      parent = t;
      //动态绑定到我们的匿名内部类(对象)
      compare cmp = cpr.compare(key, t.key);
      if (cmp < 0)
        t = t.left;
      else if (cmp > 0)
        t = t.right;
      else //如果相等,即返回 0,这个 Key 就没有加入
        return t.setValue(value);
    } while (t != null);
  }

@SuppressWarnings({"all"})
public class Test {
    public static void main(String[] args) {
        TreeSet treeSet = new TreeSet(new Comparator() { 
            @Override public int compare(Object o1, Object o2) {
            //下面 调用 String 的 compareTo 方法进行字符串大小比较 
            return ((String) o2).compareTo((String) o1);
        }});
        //添加数据.
        treeSet.add("jack");
        treeSet.add("tom");
        treeSet.add("sp");
        treeSet.add("a");
        treeSet.add("abc");
        System.out.println("treeSet=" + treeSet);

    }
}

TreeSet treeSet = new TreeSet(new Comparator() { 
	@Override public int compare(Object o1, Object o2) {
    //下面 调用 String 的 compareTo 方法进行字符串大小比较 
    return ((String) o1).length() - ((String) o2).length();
  }});
此时比较强写成长度大小,那么
  添加数据
  treeSet.add("abd");
	treeSet.add("qwe");
添加是不成功的、因为
   else //如果相等,即返回 0,这个 Key 就没有加入
        return t.setValue(value);



版权声明:本文为jwsl999原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。