Set接口和常用方法

Set接口和常用方法

set借口基本介绍

  1. 以Set 接口的实现类 HashSet 来讲解Set 接口的方法
  2. set 接口的实现类的对象(Set接口对象), 不能存放重复的元素, 可以添加一个null
  3. set接口对象存放数据是无序(即添加的顺序和取出的顺序不一致)
  4. 注意:从set中取元素没有类似set.get()的方法,只能用set.iterator()for(Object o : set){...},第二种方式本质还是set.iterator()
  5. set.iterator()从set中取元素不是从set中随机取,具体按什么方法从set中取元素取决于set.iterator()方法返回的Iterator(迭代器)
  6. set无序性:元素放入set的顺序和取出的顺序不一定一样(例如:将1 2 3依次放入set中,set.iterator()取出的第一个元素不一定是1)

set接口常用方法

添加元素

HashSet 类提供了很多有用的方法,添加元素可以使用 add() 方法:

// 引入 HashSet 类*    
**import** java.util.HashSet;

**public** **class** RunoobTest {
  **public** **static** **void** main(String[] args) {
  HashSet<String> sites = **new** HashSet<String>();
    sites.add("Google");
    sites.add("Runoob");
    sites.add("Taobao");
    sites.add("Zhihu");
    sites.add("Runoob"); *// 重复的元素不会被添加*
    System.out.println(sites);
  }
}


执行以上代码,输出结果如下:

[Google, Runoob, Zhihu, Taobao]

在上面的实例中,Runoob 被添加了两次,它在集合中也只会出现一次,因为集合中的每个元素都必须是唯一的。

判断元素是否存在

我们可以使用 contains() 方法来判断元素是否存在于集合当中:

// 引入 HashSet 类*    
**import** java.util.HashSet;

**public** **class** RunoobTest {
  **public** **static** **void** main(String[] args) {
  HashSet<String> sites = **new** HashSet<String>();
    sites.add("Google");
    sites.add("Runoob");
    sites.add("Taobao");
    sites.add("Zhihu");
    sites.add("Runoob"); *// 重复的元素不会被添加*
    System.out.println(sites.contains("Taobao"));
  }
}

执行以上代码,输出结果如下:

true

删除元素

我们可以使用 remove() 方法来删除集合中的元素:

// 引入 HashSet 类*    
**import** java.util.HashSet;

**public** **class** RunoobTest {
  **public** **static** **void** main(String[] args) {
  HashSet<String> sites = **new** HashSet<String>();
    sites.add("Google");
    sites.add("Runoob");
    sites.add("Taobao");
    sites.add("Zhihu");
    sites.add("Runoob");   *// 重复的元素不会被添加*
    sites.remove("Taobao"); *// 删除元素,删除成功返回 true,否则为 false*
    System.out.println(sites);
  }
}


执行以上代码,输出结果如下:

[Google, Runoob, Zhihu]

删除集合中所有元素可以使用 clear 方法:

// 引入 HashSet 类*    
**import** java.util.HashSet;

**public** **class** RunoobTest {
  **public** **static** **void** main(String[] args) {
  HashSet<String> sites = **new** HashSet<String>();
    sites.add("Google");
    sites.add("Runoob");
    sites.add("Taobao");
    sites.add("Zhihu");
    sites.add("Runoob");   *// 重复的元素不会被添加*
    sites.clear(); 
    System.out.println(sites);
  }
}

执行以上代码,输出结果如下:

[]

计算大小

如果要计算 HashSet 中的元素数量可以使用 size() 方法:

// 引入 HashSet 类*    
**import** java.util.HashSet;

**public** **class** RunoobTest {
  **public** **static** **void** main(String[] args) {
  HashSet<String> sites = **new** HashSet<String>();
    sites.add("Google");
    sites.add("Runoob");
    sites.add("Taobao");
    sites.add("Zhihu");
    sites.add("Runoob");   *// 重复的元素不会被添加*
    System.out.println(sites.size()); 
  }
}


执行以上代码,输出结果如下:

4

迭代 HashSet

  • Set接口不能使用普通for循环进行迭代

增强for

可以使用 for-each 来迭代 HashSet 中的元素。



*// 引入 HashSet 类*    
**import** java.util.HashSet;

**public** **class** RunoobTest {
  **public** **static** **void** main(String[] args) {
  HashSet<String> sites = **new** HashSet<String>();
    sites.add("Google");
    sites.add("Runoob");
    sites.add("Taobao");
    sites.add("Zhihu");
    sites.add("Runoob");   *// 重复的元素不会被添加*
    **for** (String i : sites) {
      System.out.println(i);
    }
  }
}

执行以上代码,输出结果如下:

Google
Runoob
Zhihu
Taobao

迭代器

*// 引入 HashSet 类*    
**import** java.util.HashSet;

**public** **class** RunoobTest {
  **public** **static** **void** main(String[] args) {
  HashSet<String> sites = **new** HashSet<String>();
    sites.add("Google");
    sites.add("Runoob");
    sites.add("Taobao");
    sites.add("Zhihu");
    sites.add("Runoob");   *// 重复的元素不会被添加*
   Iterator iterator = sites.iterator();
while (iterator.hasNext()) {
    Object obj =  iterator.next();
    System.out.println("obj=" + obj);
    }
  }
}

执行以上代码,输出结果如下:

obj=Google
obj=Runoob
obj=Zhihu
obj=Taobao

HashSet

概述

  1. HashSet 实现了 Set 接口
  2. HashSet 底层是 HashMap 来实现的,是一个不允许有重复元素的集合。
  public HashSet() {
        map = new HashMap<>();
    }
  1. HashSet 允许有 null 值,但只能有一个null值。

  2. HashSet 是无序的,即不会记录插入的顺序。但是在取出元素后,他的元素顺序是被固定的。

  3. HashSet 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的。 您必须在多线程访问时显式同步对 HashSet 的并发访问。

  4. 案例:

public class HashSet01 {
    public static void main(String[] args) {
        HashSet set = new HashSet();

        //说明
        //1. 在执行add方法后,会返回一个boolean值
        //2. 如果添加成功,返回 true, 否则返回false
        //3. 可以通过 remove 指定删除哪个对象
        System.out.println(set.add("john"));//T
        System.out.println(set.add("lucy"));//T
        System.out.println(set.add("john"));//F
        System.out.println(set.add("jack"));//T
        System.out.println(set.add("Rose"));//T


        set.remove("john");
        System.out.println("set=" + set);//3个

        //
        set  = new HashSet();
        System.out.println("set=" + set);//0
        //4 Hashset 不能添加相同的元素/数据?
        set.add("lucy");//添加成功
        set.add("lucy");//加入不了
        set.add(new Dog("tom"));//OK
        set.add(new Dog("tom"));//Ok
        System.out.println("set=" + set);

        //在加深一下. 非常经典的面试题.
        //看源码,做分析, 先给小伙伴留一个坑,以后讲完源码,你就了然
        //去看他的源码,即 add 到底发生了什么?=> 底层机制.
        set.add(new String("h"));//ok
        set.add(new String("h"));//加入不了.
        System.out.println("set=" + set);


    }
}
class Dog { //定义了Dog类
    private String name;

    public Dog(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                '}';
    }
}

为什么?

 set.add(new String("h"));//ok
        set.add(new String("h"));//加入不了.
        System.out.println("set=" + set);

HashSet底层机制说明

  1. HashSet底层实际是HashMap,HashMap底层是(数组+链表+红黑树)
简单的数组+链表结构
截屏2022-09-14 17.12.56
public class HashSetStructure {
    public static void main(String[] args) {
        //模拟一个HashSet的底层 (HashMap 的底层结构)

        //1. 创建一个数组,数组的类型是 Node[]
        //2. 有些人,直接把 Node[] 数组称为 表
        Node[] table = new Node[16];

        //3. 创建结点
        Node john = new Node("john", null);

        table[2] = john;
        Node jack = new Node("jack", null);
        john.next = jack;// 将jack 结点挂载到john
        Node rose = new Node("Rose", null);
        jack.next = rose;// 将rose 结点挂载到jack

        Node lucy = new Node("lucy", null);
        table[3] = lucy; // 把lucy 放到 table表的索引为3的位置.
        System.out.println("table=" + table);


    }
}
class Node { //结点, 存储数据, 可以指向下一个结点,从而形成链表
    Object item; //存放数据
    Node next; // 指向下一个结点

    public Node(Object item, Node next) {
        this.item = item;
        this.next = next;
    }
}
HashSet 源码解读

步骤:

  1. 获取元素的哈希值(hashCode方法)

  2. 对哈希值进行运算,得到一个索引值即为要存放在哈希表中的位置号

  3. 如果该位置上没有其他元素,则直接存放,如果该位置上有其他元素需要进行equals判断,如相等则不添加,如不相等,则已链表的形式添加。

    具体实现
       1.  执行 *HashSet()*;
    
     public HashSet() {
                map = new HashMap<>();
            }
    
    1. 执行 add()

       public boolean add(E e) {//e = "java"
         return map.put(e, PRESENT)==null;//(static) PRESENT = new Object(); 
      
    2. 执行 put() , 该方法会执行 hash(key) 得到key对应的hash值 算法

      h = key.hashCode()) ^ (h >>> 16)

      注意:

      hash值不完全等价于hashcode,算法的想法是使不同的key尽量得到不同的hash

      public V put(K key, V value) {//key = "java" value = PRESENT 共享
                      return putVal(hash(key), key, value, false, true);
                  }
      
    3. 执行 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节点的,类型是 Node<K,V>[]
                    //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)
    
                    if ((p = tab[i = (n - 1) & hash]) == null)
                        tab[i] = newNode(hash, key, value, null);
                    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) {
                                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;
                                }
                                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;
                }
             */
    
        }
    }
    
HashSet扩容机制
  1. HashSet底层是HashMap, 第一次添加时,table 数组扩容到 16,临界值(threshold)是 16*加载因子(loadFactor)是0.75 = 12。

  2. 如果table 数组使用到了临界值 12,就会扩容到 16 * 2 = 32,新的临界值就是 32*0.75 = 24, 依次类推。

  3. 如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是 8 ),并且table>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制。

  4. 当我们向hashset增加一个元素,-> Node -> 加入table , 就算是增加了一个size++。。

  5. 所以并不是table表中存放数组元素到12才触发扩容机制,而是size到达12是触发扩容机制。

LinkedHashSet (底层数组+双向链表)、有序存放数组

概述
底层机制
  1. LinkedHashSet 加入顺序和取出元素/数据的顺序一致

  2. LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类)

  3. LinkedHashSet 底层结构 (数组table+双向链表)

  4. 添加第一次时,直接将 数组table 扩容到 16 ,存放的结点类型是 LinkedHashMap$Entry

  5. 数组是 HashMap N o d e [ ] 存放的元素 / 数据是 L i n k e d H a s h M a p Node[] 存放的元素/数据是 LinkedHashMap Node[]存放的元素/数据是LinkedHashMapEntry类型

    //继承关系是在内部类完成.
    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);
        }
    }
    
  • 24
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值