JAVA-集合专题3-Set


一、set接口和常用方法

1.Set接口基本介绍

  • 无序(添加和取出的顺序不一致)、没有索引。
  • 不允许重复元素,所以最多包含一个null

2.Set接口常用方法

  1. add:添加单个元素
  2. remove:删除指定元素
  3. contains:查找元素是否存在
  4. size:获取元素个数
  5. isEmpty:判断是否为空
  6. clear:清空
  7. addAll:添加多个元素
  8. containsAll:查找多个元素是否都存在
  9. removeAll:删除多个元素

3.Set接口遍历方法

  1. 迭代器
  2. 增强for
  3. 不能使用索引方式

二、HashSet

1.说明

  • HashSet实现了Set接口
  • HashSet实际上是HashMap
  • 可以存放null值,但只能存放一个
  • HashSet不保证元素是有序的,取决于hash后,在确定索引的结果。
  • 不能有重复的元素/对象,
    在这里插入图片描述
    "lucy"字符串放在常量池,第二个显然放不进去了,因为已经存在了。而在new Dog()的时候,每次调用new Dog()方法都是在内存中开辟一块新内存,所以都是不同的对象,即使名字一样。

但是:(原因看add()方法的源码)
在这里插入图片描述

2.HashSet底层机制说明

HashSet的底层是HashMap,而HashMap的底层是(数组+链表+红黑树)
在这里插入图片描述
这里是数组加链表的形式,当该表的规模超过一定大小后,链表会变为红黑树形式。
不用数组的原因是数组效率太低。

3.HashSet扩容机制

添加元素——hash()+equals()
补充:可以通过在自己写的类中重写hashCode()方法和equals()方法来实现一些东西: 可以通过重写hashCode()让该类的对象都返回一眼的哈希值,这样该类的所有结点都会存放在同一链表中。 也可以通过重写equals()方法,指定比较类中某个属性来确定是不是一样的结点。(源码解读中可以看到,调用了传入的值key的hashCode()方法和equals()方法)

要注意的是,虽然在Object类中的equals()方法是用‘==’来比较内存地址。但是在String类中,重写了equals()方法来比较两个字符串的内容是否一致。所以 set.add(“lisi1”); 和 set.add(new String(“lisi”)); 其实在equals方法中相等了,也就不能放进去了

  1. HashSet底层是HashMap
  2. 添加一个元素时,使用hashCode()方法先得到hash值——运算后会转成->索引值(数组下标)
  3. 找到存储数据表table,看这个索引位置是否有已经存放的元素
  4. 如果没有,直接加入(挂在后面)
  5. 如果有,调用equals比较,如果相同,就放弃添加,如果不相同,则添加到最后。注意这里equals()方法不局限于单纯比较内容,可以由程序员重写。
  6. 在JDK8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN TREEIFY_CAPACITY(默认64),就会进行树化(红黑树);如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小<MIN TREEIFY_CAPACITY(默认64),那么会先对数组进行扩容(扩容的方法是数组大小变为原来二倍)

扩容详细过程:
1.HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16加载因子(LoaFactor)是0.75=12
2.如果table表大小达到了临界值12,就会扩容到16
2 = 32,新的临界值就是32*0.75=24,依次类推(补充:这里是指表中所有元素个数,不仅指在数组中的,还要算上链表和数中的元素个数)
3.在JDK8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN TREEIFY_CAPACITY(默认64),就会进行树化(红黑树);如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小<MIN TREEIFY_CAPACITY(默认64),那么会先对数组进行扩容(扩容的方法是数组大小变为原来二倍),如果在继续在该链表加上一个元素,并且table的大小<MIN TREEIFY_CAPACITY(默认64),那么数组继续扩容依次(扩容的方法是数组大小变为原来二倍)。

4.HashSet源码解读

在这里插入图片描述
在这里插入图片描述

重点: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[]
        //第一次进入(开始时)table应该为空或长度为0
        //如果当前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)如果为null,表示在表中这一行还没存放元素,就创建一个Node(key="要添加的值",value=PRESENT)
        //     把这个Node放在tab[i]
        //     
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //如果当前索引位置对应的第一个元素(数组里的元素)的hash值和准备加入的key的hash一样
            //且满足以下两个条件之一:
            //(1)准备加入的key和p指向的Node结点的key指向同一个对象
            //(2)p指向的Node结点的key与准备加入的key 执行equals()后相等(不知道有没有重写成比较值的方法???????)
            //就不能加入
            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);
            //如果table对应索引位置已经是一个链表,就使用for循环来比较
            //(1)依次和链表中每个元素比较后,都不相同,则加入到链表的最后
            //   注意这里加入后,立即判断当前链表是否超过8个结点
            //   超过则调用treeifyBin()方法,进一步判断table数组大小
            //   如果table表大小小于64,则进行扩容操作
            //   否则,转该链表为红黑树。
            //(2)依次比较过程中发现有相同的情况,直接break;
            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;
        //如果所存东西的数量超过了临界值(容量的0.75),就进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
//HashMap中有更详细简介

三、LinkedHashSet

LinkedHashSet 继承了 HashSe类, 实现了Set接口

1.LinkedHashSet的全面说明

  1. LinkedHashSet是 HashSet的子类
  2. LinkedHashSet底层是一个 LinkedHashMap,底层维护了一个数组+双向链表
  3. LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的.
  4. LinkedHashSet 不允许添重复元素

2.LinkedHashSet底层机制示意图

在这里插入图片描述

3.LinkedHashSet底层代码解读

  1. LinkedHashSet 加入顺序和取出元素/数据的顺序一致
  2. LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类)
  3. LinkedHashSet 底层结构(数组table+双向链表),每个节点还是有next,可以形成自己的单链。
  4. 添加第一次时,直接将数组table扩容到16,存放的结点类型是 LinkedHashMap的Entry子类
  5. 数组是 HashMap的Node[] 。 存放的元素/数据是LinkedHashMap的Entry子类类型。 其实就是子类和父类(多态)。
//HashMap的静态内部类Node
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
//LinkedHashSet的静态内部类Entry
static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

四、TreeSet

代码讲解:

public static void main(String[] args) {
		TreeSet treeSet = new TreeSet(new Comparator() {
			@Override
			public int compare(Object o1, Object o2) {
				//调用String的compareTo方法进行字符串比较
				return ((String)o1).compareTo((String)o2);
			}
		});
		
		treeSet.add("jack");
		treeSet.add("tom");
		treeSet.add("a");
		treeSet.add("rose");
	}
  1. 当我们使用无参构造器创建 TreeSet时,仍然是无序的

  2. 如果希望添加的元素有序,可以使用TreeSet 提供的一个构造器,可以传入一个比较器(匿名内部类),并指定排序规则(重写compare方法)

    public TreeSet(Comparator<? super E> comparator) {
            //TreeSet底层实现就是TreeMap
            this(new TreeMap<>(comparator));
        }
    //继续step Into
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
    
  3. 添加元素时,先调用 add()方法, 然后调用put()方法

    	public V put(K key, V value) {
            Entry<K,V> t = root;
            if (t == null) {
                compare(key, key); // type (and possibly null) check
    
                root = new Entry<>(key, value, null);
                size = 1;
                modCount++;
                return null;
            }
            int cmp;
            Entry<K,V> parent;
            // 这里会使用传入的比较器,调用比较器的compare方法
            Comparator<? super K> cpr = comparator;
            if (cpr != null) {
                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)
                    throw new NullPointerException();
                @SuppressWarnings("unchecked")
                    Comparable<? super K> k = (Comparable<? super K>) key;
                do {
                    parent = t;
                    cmp = k.compareTo(t.key);
                    //cmp为比较结果
                    if (cmp < 0)
                        t = t.left;
                    else if (cmp > 0)
                        t = t.right;
                    else //如果比较结果为0,则不加入
                        return t.setValue(value);
                } while (t != null);
            }
            Entry<K,V> e = new Entry<>(key, value, parent);
            if (cmp < 0)
                parent.left = e;
            else
                parent.right = e;
            fixAfterInsertion(e);
            size++;
            modCount++;
            return null;
        }
    
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值