集合04 Collection (Set) - Java

Set

  • Set 基本介绍
  • Set 常用方法
    • Set 遍历方式
  • HashSet 的全面说明
    • 练习
  • HashSet 的底层机制说明
  • HashSet 的扩容机制&转成红黑树机制
    • 练习1
    • 练习2
  • LinkedHashSet
    • LinkedHashSet底层源码
    • 练习

Set 基本介绍

  1. 无序(添加和取出的顺序不一致),没有索引 [后面演示]
  2. 不允许重复元素,所以最多包含一个null
  3. JDK API中Set接口的实现类有很多。最常用的是HashSet、TreeSet

Set 常用方法

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

Set 遍历方式

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

  1. 可以使用迭代器
  2. 增强for
  3. 不能使用索引的方式来获取

以 Set 接口的实现类 HashSet 来讲解 Set 接口的方法 。

public static void main(String[] args) {
        //解读
        //1. 以Set 接口的实现类 HashSet 来讲解Set 接口的方法
        //2. set 接口的实现类的对象(Set接口对象), 不能存放重复的元素, 可以添加一个null
        //3. set 接口对象存放数据是无序(即添加的顺序和取出的顺序不一致)
        //4. 注意:取出的顺序的顺序虽然不是添加的顺序,但是他是固定的
        Set set = new HashSet();
        set.add("john");
        set.add("lucy");
        set.add("john");//重复
        set.add("jack");
        set.add("hsp");
        set.add("mary");
        set.add(null);//
        set.add(null);//再次添加null,但最后set中只有一个null

		//遍历十次set 发现输出顺序是固定的
        for(int i = 0; i <10;i ++) {
            System.out.println("set=" + set);
        }

        //------------遍历------------------------------
        //方式1: 使用迭代器
        System.out.println("=====使用迭代器====");
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object obj =  iterator.next();
            System.out.println("obj=" + obj);

        }

        set.remove(null);

        //方式2: 增强for
        System.out.println("=====增强for====");

        for (Object o : set) {
            System.out.println("o=" + o);
        }
        //set 接口对象,不能通过索引来获取
}

HashSet 的全面说明

见 HashSet.java

  1. HashSet实现了Set接口
  2. HashSet实际上是HashMap,看下源码
//源码
    public HashSet() {
        map = new HashMap<>();
    }
  1. HashSet 可以存放null值,但是只能有一个null,元素不能重复
  2. HashSet 不保证元素是有序的,取决于hash后,再确定索引的结果。即不保证存放元素的顺序与取出顺序一致(有可能一样也有可能不同)
  3. 不能有重复元素/对象。在前面Set 接口使用已经讲过

练习

HashSet01.java

 HashSet set  = new HashSet();//set引用指向一个新的对象
 System.out.println("set=" + set);//0
 //4 Hashset 不能添加相同的元素/数据?
 set.add("lucy");//添加成功
 set.add("lucy");//加入不了
 set.add(new Dog("tom"));
 set.add(new Dog("tom"));
 System.out.println("set=" + set);

分析:
两个lucy是常量池的,同一个
两个 new Dog(“tom”) 不是同一个元素!!

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

分析:
这后面要通过看源码才能知道。
去看他的源码,即 add 到底发生了什么?=> 底层机制

HashSet 的底层机制说明

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

HashSetStructure.java 模拟简单的数组+链表结构。

HashSetSource.java 模拟简单的数组+链表结构。

  1. HashSet底层是HashMap
  2. 添加一个元素时,先得到hash值-会转成->索引值
  3. 找到存储数据表table,看这个索引位置是否已经存放的有元素
  4. 如果没有,直接加入
  5. 如果有,调用equals(请注意equals是按照内容还是地址比较,是程序员可以控制的。比如String类就重写了方法equals,比较的是字符串的内容。)比较,如果相同,就放弃添加,如果不相同,则添加到最后
  6. 在Java8中,如果一条链表的元素个数到达了TREEIFY_THRESHOLD(默认是8),并且table的大小 >=
    MINTREEIFY_CAPACITY(默认64)。就会进行树化(红黑树)
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);

     /*
     韩老师对HashSet 的源码解读
     1. 执行 HashSet()
         public HashSet() {
             map = new HashMap<>();
         }
     2. 执行 add()
        public boolean add(E e) {//e = "java"
         这里的E是泛型后面会讲,debug过程中看到e是字符串常量
             return map.put(e, PRESENT)==null;//(static) PRESENT = new Object();
        }
      3.执行 put() ,
          public V put(K key, V value) {
              //key = "java" value = PRESENT(static) 共享
             return putVal(hash(key), key, value, false, true);
         }
         该方法会执行 hash(key) 得到key对应的hash值 算法h = key.hashCode()) ^ (h >>> 16)
      4.执行 putVal
      final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                boolean evict) {
             Node[] tab; Node p; int n, i; //定义了辅助变量
             //table 就是 HashMap 的一个数组,类型是 Node[]

             //if 语句表示如果当前table 是null, 或者 大小=0
             //就是第一次扩容,到16个空间.
             if ((tab = table) == null || (n = tab.length) == 0)
                 n = (tab = resize()).length; //resize就是开辟空间的

             //(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 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)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,就会扩容到162=32,新的临界值就是320.75 = 24,依次类推。
  3. 在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8)并且table的大小 >=
    MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制。

HashSetIncrement.java
通过重写hashcode满足底层代码条件

练习1

定义一个Employee类,该类包含:private成员属性name,age 要求:
创建3个Employee 对象放入 HashSet中
当 name和age的值相同时,认为是相同员工, 不能添加HashSet集合中

ADD添加
① 先获取元素的哈希值(hashCode方法)
② 对哈希值进行运算,得出一个索引值。即为要存放在哈希表中的位置号
③ 如果该位置上没有其他元素,则查接存放。
如果该位置上已经有其他元素,则需要进行equals判断。如果相等,则不再添加,如果不相等,则以链表的方式添加。

不同的对象具有不同的哈希值(hashCode),相同的对象不能反复添加到hashset中。

题目要求name和age的值相同时,认为是相同员工,不能添加(hashset应该把这种情况视作相同的对象),也就是new的时候,如果是相同的name和age,其hashcode应该相同,所以重写hashCode。(用快捷键 alt+insert)

如果不重写hashCode
如果不重写equals

public class HashSetExercise {
    public static void main(String[] args) {


        /**
         定义一个Employee类,该类包含:private成员属性name,age 要求:
         创建3个Employee 对象放入 HashSet中
         当 name和age的值相同时,认为是相同员工, 不能添加到HashSet集合中

         */
        HashSet hashSet = new HashSet();
        hashSet.add(new Employee("milan", 18));//ok
        hashSet.add(new Employee("smith", 28));//ok
        hashSet.add(new Employee("milan", 18));//加入不成功.

        //回答,加入了几个? 3个
        System.out.println("hashSet=" + hashSet);
    }
}

//创建Employee
class Employee {
    private String name;
    private int age;

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

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

    public void setAge(int age) {
        this.age = age;
    }
    //如果name 和 age 值相同,则返回相同的hash值

    @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);
    }
}


练习2

集合04 Collection (Set) - Java_第1张图片

LinkedHashSet

set接口的另外一个实现子类。

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

说明:

  1. 在LinkedHastSet 中维护了一hash表和双向链表(LinkedHashSet有head和 tail)
  2. 每一个节点有pre和next属性,这样可以形成双向链表
  3. 在添加一个元素时,先求hash值,在求索引。确定该元素在hashtable的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加[原则和hashset一样])
tail.next = newElement //简单指定
newElement.pre = tail
tail = newEelment;
  1. 这样的话,我们遍历LinkedHashSet 也能确保插入顺序和遍
    历顺序一致

LinkedHashSet底层源码

LinkedHashSetSource.java

练习

Car类(属性:name.price),如果name和price一样。则认为是相同元素,就不能添加。

@SuppressWarnings({"all"})
public class LinkedHashSetExercise {
    public static void main(String[] args) {

        LinkedHashSet linkedHashSet = new LinkedHashSet();
        linkedHashSet.add(new Car("奥拓", 1000));//OK
        linkedHashSet.add(new Car("奥迪", 300000));//OK
        linkedHashSet.add(new Car("法拉利", 10000000));//OK
        linkedHashSet.add(new Car("奥迪", 300000));//加入不了
        linkedHashSet.add(new Car("保时捷", 70000000));//OK
        linkedHashSet.add(new Car("奥迪", 300000));//加入不了

        System.out.println("linkedHashSet=" + linkedHashSet);

    }
}

/**
 * Car 类(属性:name,price),  如果 name 和 price 一样,
 * 则认为是相同元素,就不能添加。 5min
 */

class Car {
    private String name;
    private double price;

    public Car(String name, double price) {
        this.name = name;
        this.price = price;
    }

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

    //重写equals 方法 和 hashCode
    //当 name 和 price 相同时, 就返回相同的 hashCode 值, equals返回t

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Car car = (Car) o;
        return Double.compare(car.price, price) == 0 &&
                Objects.equals(name, car.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, price);
    }
}

new 对象,会有对应的 hashcode。但是现在不想
重写hashcode,new出来一个对象对应一个hashcode,我们题目要求 name和price相同就不能添加。LinkedHashSet规定相同的hashcode不能添加。但是如果按照下面的方式写,因为new出来一个对象对应一个hashcode,所以会运行正确。但是不符合题干。所以我们要【设置:当name和price一样的时候,返回相同的hashcode】

linkedHashSet.add(new Car("奥迪", 300000));
linkedHashSet.add(new Car("奥迪", 300000));

当执行第二句话的时候,得到了和第一句话相同的hashcode,就去tables索引位置,此时 equals 比较,如果是相同的name和price就不准添加。

重写hashcode:保证两句存放的tables索引位置一样(因为不同的hashcode对应的位置有可能不一样)。重写equals保证不准添加。

如果只保留了hashcode没有equals ,就会存放在同一个链表上。没有重写equals 就会调用Object的equals方法,比较的是地址。

本笔记是对韩顺平老师的Java课程做出的梳理。方便本人和观看者进行复习。
课程请见: https://www.bilibili.com/video/BV1fh411y7R8/?spm_id_from=333.999.0.0&vd_source=ceab44fb5c1365a19cb488ab650bab03

你可能感兴趣的:(Java,SE,java)