集合--Set、HashSet、LinkedHashSet

3.7 Set接口、常用方法及遍历方式

Set接口基本介绍:

  1. 无序(添加和取出顺序不一致),没有索引
  2. 不允许重复元素,所以最多一个 null
  3. JDK API 中 Set 接口的实现类

Set接口常用方法:

和List接口一样,Set接口也是Collection的子接口,因此,常用方法和 Collection 接口—样

//以 Set 接口的实现类 HashSet 来讲解 Set 接口的方法
// Set 接口的实现类的对象(Set接口对象),不能存放重复的元素可以添加一个null
// Set 接口对象存放数据是无序的(即添加的顺序和取出的顺序不一致)
//注意:取出的顺序虽然不是添加的顺序,但取出顺序是固定的

Set set = new HashSet();
set.add("john");
set.add("luck");
set.add("john");//重复
set.add("jack");
set.add(null);
set.add(null);//再次添加null

Set接口的遍历方式

同Collection的遍历方式一样,因为Set接口是Collection接口的子接口。

  1. 可以使用选代器

    Set set = new HashSet();
    
    Iterator iterator = set.iterator();
    
    while(iterator.hasNext()){
        Object obj = iterator.next();
        System.out.println(obj);
    }
    
  2. 增强for

    Set set = new HashSet();
    
    for(Object o : set){
        System.out.println(o);
    }
    
  3. 不能使用索引的方式来获取

    注意: set 接口对象,不能通过索引来获取,所以不能用普通 for 循环

3.8 HashSet底层结构和源码分析

HashSet 全面说明:

  1. HashSet 实现了 Set接口

  2. HashSet 实际上是 HashMap

    public HashSet(){
        map = new HashMap<>();
    }
    
  3. 可以存放null 值,但只能有一个 null ,即元素不能重复

  4. HashSet 不保证元素是有序的,取决于 hash 后,在确定索引的结果(即,不保证存放元素顺序和取出顺序不一致)

  5. 不能有重复元素/对象

案例说明:

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);
HashSet set = new HashSet();
//4. HashSet 不能存放相同的元素/数据
set.add("lucy"); //添加成功
set.add("lucy"); //加入不了
set.add(new Dog("tom")); //ok
set.add(new Dog("tom")); //ok,两个Dog只是同名,但是两个元素
System.out.println("set="+ set);


//再加深一下,经典面试题
//看 add 的源码,做分析 ==> 底层机制
set.add(new String("hsp")); //ok
set.add(new String("hsp")); //加入不了


class Dog{//定义了一个Dog类
    private String name;
    private Dog(String name){
        this.name = name;
    }
}

HashSet 底层机制说明:

HashSet 底层是 HashMap ,HashMap 底层是(数组+链表+红黑树)

//模拟一个 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 luck = new Node("luck",null);
table[3] = luck;//将luck 放到 table 表 索引为3的位置


class Node{//结点,用来存储数据,可以指向下一个结点,从而形成链表
    Object item; //存放数据
    Node next; //指向下一个结点
    
    public Node(Object item,Node next){
        this.item = item;
        this.next = next;
    }
}

HashSet 扩容机制:(hash() + equals())

(一)结论

  1. HashSet底层是HashMap,第一次添加时,table 数组扩容到16,临界值(threshold)是16 * 加载因子(loadFactor,是0.75)= 12

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

  2. 添加一个元素时,先得到hash值 -会转成->索引值

  3. 找到存储数据表table,看这个索引位置是否已经存放的有元素

  4. 如果没有,直接加入

  5. 如果有,调用equals 比较,如果相同,就放弃添加,如果不相同,则添加到最后

  6. 在 Java8 中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),并且 table 的大小 >= MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树),否则仍采用数组扩容机制

(二)源码

HashSet hashSet = new HashSet();
hashSet.add("java"); //到此,第1次add分析完毕
hashSet.add("php");//第2次add分析完毕
hashSet.add("java");
System.out.println("set=" + hashSet);

/*
对HashSet的源码解读
1.执行 HashSet()
    public HashSet(){
        map = new HashMap<>();
    }
2.执行 add()    
    public boolean add(E e){ //e = "java"
        return map.put(e,PRESENT)==null; //(static) PRESENT = new Object()
    }
3.执行 put(),该方法会执行 hash(key) 得到key对应的hash值 算法h = key.hashCode()) ^ (h >>> 16)
    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)
        
        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;
    }


*/

练习:

定义一个Employee类,该类包含:private成员属性name,age。要求:

1.创建3个Employee 对象放入HashSet中

2.当 name和age的值相同时,认为是相同员工,不能添加到HashSet集合中

public class HashSetExcrise{
    public static void main(String[] args){
        HashSet hashSet = new HashSet();
        
        hashSet.add(new EmpLoyee("milan",18));
        hashSet.add(new Employee("smith",28));
        hashSet.add(new Employee("milan",18));
        //若没有重写方法,此时加入了几个对象?
        //3个,只是内容同名,但是属于不同的Employee 对象
        
    }
}

//创建 Employee 对象
class Employee{
    private String name;
    private int age;
    
    ...//构造器和get/set方法省略
        
    //如果name 和 age 值相同,则返回相同的hash值
    //alt + insert 快捷键
    @Override
    public boolean equals(Object o){
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        EmpLoyee employee = (EmpLoyee) o;
        return age == employee.age &&
        	Objects.equals(name, employee.name); 
    }
    
    @Override
    public int hashCode(){
        return Objects.hash(name,age);
    }
}

作业:

定义一个Employee类,该类包含:private成员属性name,sal,birthday(MyDate类型),其中 birthday 为 MyDate类型(属性包括:year,,month,day),要求:

1.创建3个Employee 放入HashSet中

2.当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中

3.9 LinkedHashSet底层结构和源码分析

LinkedHashSet全面说明:

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

LinkedHastSet 底层机制说明:

  1. 在 LinkedHastSet 中维护了一个hash表和双向链表(LinkedHashSet 有 head 和 tail )
  2. 每一个节点有 pre 和 next 属性,这样可以形成双向链表在添加一个元素时,先求hash值,再求索引。确定该元素在 hashtable 的位置,然后将添加的元素加入到双向链表(如果已经存在,就不添加【原则和 hashset 一样】)
  3. tail.next = newElement // 简单指定
  4. newElement.pre = tail
  5. tail= newEelment;
  6. 这样的话,我们遍历LinkedHashSet 也能确保插入顺序和遍历顺序一致
posted @ 2025-08-28 21:40  lu璐  阅读(7)  评论(0)    收藏  举报