第14章 集合——(3)Set

一、Set接口和常用方法

1.Set接口基本介绍

(1)Set集合中元素无序(即添加顺序和取出顺序不一致),不允许重复,最多包含一个null。
         注意:取出顺序虽然与添加顺序不一致,但无论取出多少次,取出顺序是固定不变的
(2)Set集合中元素没有索引
(3)JDK API中Set接口的实现类有很多,常用的有:HashSet,LinkedHashSet,TreeSet

2.Set接口常用方法 

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

  • add:添加单个元素。
  • addAll:添加多个元素(多个元素放在一个集合里)。
  • remove:删除指定元素。
  • removeAll:删除多个元素(多个元素放在一个集合里)。
  • clear:清空。
  • contains:查找元素是否存在。
  • containsAll:查找多个元素是否都存在(多个元素放在一个集合里)。
  • size:获取元素个数。
  • isEmpty:判断是否为空。

3.Set接口的遍历方式

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

  • 使用迭代器
  • 使用增强for(增强for循环,底层仍然是迭代器
  • 因为Set集合中元素没有索引,所以不能使用普通for循环。

二、HashSet

1.HashSet全面说明

(1)HashSet实现了Set接口。
(2)HashSet实际上是HashMap。

        /*HashSet的无参构造器里调用的是HashMap
            public HashSet() {
                map = new HashMap<>();
            }
         */
        Set hashSet = new HashSet();

(3)HashSet中不能有重复元素/对象。
(4)HashSet可以存放null,但只能存放一个。
(5)HashSet中元素的存放顺序与取出顺序可能不一致,取决于hash后再确定索引的结果。
注意:取出顺序虽然与添加顺序不一致,但无论取出多少次,取出顺序是固定不变的

@SuppressWarnings({"all"})
public class HashSet01 {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();

        //add:添加单个元素,返回一个boolean值。添加成功,返回true;添加失败,返回false。
        System.out.println(hashSet.add("john"));//t
        System.out.println(hashSet.add("lucy"));//t
        System.out.println(hashSet.add("john"));//f
        System.out.println(hashSet.add("jack"));//t
        System.out.println(hashSet.add("Rose"));//t
        //remove:删除指定元素。
        hashSet.remove("john");
        System.out.println(hashSet);//[Rose, lucy, jack]

        //HashMap不能添加重复的元素/对象
        hashSet = new HashSet();
        hashSet.add("lucy");//t
        hashSet.add("lucy");//f
        hashSet.add(new Dog("tom"));//t
        hashSet.add(new Dog("tom"));//t,两只名字都叫tom的狗
        System.out.println(hashSet);//[Dog{name='tom'}, lucy, Dog{name='tom'}]

        //TODO add的底层机制是什么?
        hashSet.add(new String("hsp"));//t
        hashSet.add(new String("hsp"));//f,添加失败!!
        System.out.println(hashSet);//[hsp, Dog{name='tom'}, lucy, Dog{name='tom'}]

    }
}

class Dog {
    private String name;

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

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                '}';
    }
}
  • add:添加单个元素,返回一个boolean值。添加成功,返回true;添加失败,返回false。
  • remove:删除指定元素。

2.HashSet底层机制

(1)模拟简单的数组+链表结构

public class HashSetStructrue {
    public static void main(String[] args) {
        //创建一个数组,数组类型是Node[],有些人称Node[]为表
        Node[] table = new Node[16];
        Node john = new Node("john", null);
        table[2] = john;//将john结点添加到table中索引为2的位置
        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的位置

    }
}

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

    public Node(Object item, Node next) {
        this.item = item;
        this.next = next;
    }
}

 (2)HashSet的添加元素底层的实现

  • HashSet的底层是HashMap,HashMap的底层是数组+链表+红黑树
  • 添加一个元素时,先得到 key 的 hash 值,然后 hash 值转化为在 table 的索引。
  • 找到存储数据表table,看这个索引位置是否已经存放元素。
  • 如果没有,直接加入;如果有,调用equals比较,相同就放弃添加,不同则添加到最后(next)。
  • 在Java8中,如果一条链表的元素个数 >= TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树)。
import java.util.HashSet;

@SuppressWarnings({"all"})
public class HashSetSource {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add("java");
        hashSet.add("php");
        hashSet.add("java");
        System.out.println(hashSet);

        /*源码:
        1.类HashSet的无参构造器-HashSet()
        public HashSet() {
            map = new HashMap<>();
        }

        2.类HashSet—add方法
        public boolean add(E e) {//e:"java"
            return map.put(e, PRESENT)==null;//PRESENT(静态):private static final Object PRESENT = new Object();
        }

        3.类HashMap-put方法:该方法会执行hash(key)得到key对应的hash值
          涉及算法(h = key.hashCode()) ^ (h >>> 16) ^按位异或 >>>无符号右移16位
          为了让不同的key尽量得到不同的hash值
        public V put(K key, V value) {//key:"java" value:PRESENT
            return putVal(hash(key), key, value, false, true);
        }

        4.类HashMap-hash方法
        static final int hash(Object key) {//key:"java"
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }

        5.类HashMap-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;//定义辅助变量,p:指向将要存储Node的位置的现有Node

            //table:HashMap的一个数组,类型是 Node[]
            //if语句:如果当前table是null,或者大小为0,就第一次扩容到16个位置
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;

            //if语句:根据key得到的hash值,去计算该key应该存放到table表的哪个索引位置i
            //       将这个位置的对象赋给p,判断p是否为null
            //如果p为null,表示还没有存放元素,就创建一个Node(key="java",value=PRESENT);并将该Node放置在该位置tab[i]
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            else {
                //开发tip:在需要局部变量(辅助变量)时再定义
                //如果p不为空,表示该位置处已存放元素
                //第1种情况:p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))
                //如果当前索引位置对应的链表的第一个元素和准备添加的元素(key)的hash值一样
                //并且满足下面两个条件之一:
                //(1)p指向的Node结点的key和准备添加的key是同一个对象
                //(2)准备添加的key的equals()方法和p指向的Node结点的key比较后相同(非String类对象,equals的具体实现程序员自己决定)
                //就不能加入
                Node<K,V> e; K k;
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;

                //第2种情况:如果p是一颗红黑树,就调用putTreeVal来进行添加
                else if (p instanceof TreeNode)
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

                //第3种情况:如果要添加元素的索引位置已经是一个链表,就使用for循环依次和该链表的每一个元素进行比较
                //(1)依次比较后都不相同,则加入到该链表的最后
                //    把元素添加到链表后,立即判断该链表是否已经达到8个结点
                //    如果达到8个就调用treeifyBin()对当前这个链表进行树化(转成红黑树)
                //    注意:在转成红黑树时,要进行判断,判断条件(table数组的大小<MIN_TREEIFY_CAPACITY(64))
                //    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                //            resize();
                //    如果条件成立,先对table进行扩容
                //    如果条件不成立,将table转成红黑树
                //(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;//对应(1)
                        }
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;//对应(2)
                        p = e;
                    }
                }
                //HashMap.add(key,value):原本的table中已存在要添加的key,就更新value
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount;
            //如果当前table的大小 > 12(临界值),就再次扩容
            //size:每加入一个结点Node(k,v,h,next),不管是加在链表后还是加在table的索引位置,size++
            if (++size > threshold)
                resize();//扩容
            //空方法,为了让HashMap的子类实现该方法
            afterNodeInsertion(evict);
            return null;
        }
         */

    }
}

(3)HashSet的扩容和转成红黑树机制

  • HashSet的底层是HashMap
  • 第一次添加时,table数组扩容到16,临界值(threshold)是16*0.75=12(0.75是加载因子(loadFactor))。
  • 如果table数组使用到了临界值12,就会继续扩容到16*2=32,新的临界值是32*0.75=24。依次类推... 16(12)→32(24)→64(48)→128(96)...
  • 不管是在链表后缀接结点,还是在table新的索引位置上增加结点,都会增加table使用,size++。
  • 在Java8中,如果一条链表的元素个数 >= TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树),否则仍然采用数组扩容机制。
import java.util.HashSet;

@SuppressWarnings({"all"})
public class HashSetIncrement {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
//        for (int i = 0; i < 100; i++) {
//            hashSet.add(i);
//        }
        //HashSet底层是HashMap
        //第一次添加时,table数组扩容到16,临界值是12
        //table数组使用达到12,table数组再次扩容到32,临界值是24
        //table数组使用达到24,table数组再次扩容到64,临界值是48
        //table数组使用达到48,table数组再次扩容到128,临界值是96
        //table数组使用达到96,table数组再次扩容到256,临界值是192
        //...

        //当向hashSet中增加一个Node,不管是添加到链表后,还是加在table的索引位置,都算是增加了一个table数组使用
        for (int i = 0; i < 7; i++) {//在table的某一个索引位置处添加了一个7结点(A对象)链表
            hashSet.add(new A(i));
        }
        for (int i = 0; i < 7; i++) {//在table的另一个索引位置处添加了一个7结点(B对象)链表
            hashSet.add(new B(i));
        }
        //第 13 次添加,table数组使用已达到12,因此table表扩容至32

//        for (int i = 0; i < 12; i++) {
//            hashSet.add(new A(i));
//        }
        //在Java8中,如果一条链表的元素个数 >= TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树),否则仍然采用数组扩容机制
        //第 1 次添加,在table的索引为 4 的位置上添加一个结点
        //第 2-8 次添加,在table的索引为 4 的位置的最后缀接一个结点,第 8 次添加后,table的索引为 4 的位置上:一个8结点的链表
        //第 9 次添加,table表扩容至 32,table的索引为 4 的位置上:一个9结点的链表
        //第 10 次添加,table表扩容至 64,table的索引为 36 的位置上:一个10结点的链表(位置由4变至36)
        //第 11 次添加,树化,table表容量不变,table的索引为 36 的位置上:链表->红黑树
    }
}

class A {
    private int n;

    public A(int n) {
        this.n = n;
    }

    @Override
    public int hashCode() {
        return 100;//不管 n 为多少,hash 值都为100,保证添加在table的同一个索引位置
    }
}

class B {
    private int n;

    public B(int n) {
        this.n = n;
    }

    @Override
    public int hashCode() {
        return 200;//不管 n 为多少,hash 值都为100,保证添加在table的同一个索引位置
    }
}

3.HashSet课后练习

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

import java.util.HashSet;
import java.util.Objects;

@SuppressWarnings({"all"})
public class HashSetExercise {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add(new Employee("张三", 25));//第1个对象
        hashSet.add(new Employee("李四", 30));//第2个对象
        hashSet.add(new Employee("张三", 25));//第3个对象

//        System.out.println(hashSet);//[Employee{name='李四', age=30}, Employee{name='张三', age=25}, Employee{name='张三', age=25}]
        //没有重写equals和hashCode:hashSet中添加了3个元素。因为第1和3个对象是不同的对象实例,所以hash值也不同,所在的索引位置也不同。

        System.out.println(hashSet);//[Employee{name='李四', age=30}, Employee{name='张三', age=25}]
        //重写equals和hashCode:hashSet中添加了2个元素。因为第1和3个对象虽然是不同的对象实例,但名字与年龄相同,hash值也就相同,equals判断两个对象相等。
    }
}

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

    public void setAge(int age) {
        this.age = age;
    }

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

    //如果name和age值相同,则返回相同的hash值
    @Override
    public boolean equals(Object obj) {
        //如果比较的两个对象是同一个对象,则直接返回true
        if(this == obj) {
            return true;
        }

        //判断obj的运行类型是否为Employee,如果是才比较属性是否相同
        if(obj instanceof Employee) {
            Employee employee = (Employee) obj;//向下转型,以访问到Employee类所特有的属性
            return this.name.equals(employee.name) && this.age == employee.age;
        }

        //如果obj的运行类型不是Employee,直接返回false
        return false;
    }

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


}

(2)定义一个Employee类,该类包含:private成员属性name、sal、birthday(MyDate类),其中birthday为MyDate类型(属性包括:year,month,day)。
         要求:a:创建3个Employee对象放入HashSet中。
                    b:当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中。

@SuppressWarnings({"all"})
public class HashSetExercise02 {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add(new Employee("张三", 2000, new MyDate(2000, 11, 9)));
        hashSet.add(new Employee("李四", 2000, new MyDate(2000, 12, 4)));
        hashSet.add(new Employee("张三", 2000, new MyDate(2000, 11, 9)));

        System.out.println(hashSet);
        /*
        [Employee{name='李四', sal=2000, birthday=MyDate{year=2000, month=12, day=4}}, 
         Employee{name='张三', sal=2000, birthday=MyDate{year=2000, month=11, day=9}}]
         */
    }
}

class Employee {
    private String name;
    private int sal;
    private MyDate birthday;

    public Employee(String name, int sal, MyDate birthday) {
        this.name = name;
        this.sal = sal;
        this.birthday = birthday;
    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return name.equals(employee.name) &&
                birthday.equals(employee.birthday);
    }

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

class MyDate {
    private int year;
    private int month;
    private int day;

    public MyDate(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }

    @Override
    public String toString() {
        return "MyDate{" +
                "year=" + year +
                ", month=" + month +
                ", day=" + day +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        //如果比较的两个对象是同一个对象,则直接返回true
        if (this == o) return true;
        //如果比较的对象是null 或 比较的两个对象类型不同,则直接返回false
        if (o == null || getClass() != o.getClass()) return false;

        //比较的两个对象类型相同
        MyDate myDate = (MyDate) o;//向下转型,以比较MyDate类特有的属性
        return year == myDate.year &&
                month == myDate.month &&
                day == myDate.day;
    }

    @Override
    public int hashCode() {
        return Objects.hash(year, month, day);
    }
}

三、LinkedHashSet

1.LinkedHashSet全面说明

(1)LinkedHashSet是HashSet的子类。

(2)LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双向链表

(3)LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的。

(4)LinkedHashSet不允许添加重复元素。

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Set;

@SuppressWarnings({"all"})
public class LinkedHashSetSource {
    public static void main(String[] args) {
        //LinkedHashSet的底层机制
        Set set = new LinkedHashSet();
        set.add(new String("AA"));
        set.add(456);//自动装箱
        set.add(456);
        set.add(new Customer("刘", 1001));
        set.add(123);
        set.add("HSP");

        System.out.println("set = " + set);//set = [AA, 456, com.hspedu.set_.Customer@1540e19d, 123, HSP]
        /*
        1.LinkedHashSet 元素的加入顺序和取出顺序一致
            知识点1:Set集合中元素不允许重复,最多包含一个null。
            知识点2:toString方法默认返回 全类名(包名+类名)+@+哈希值的十六进制,子类往往重写toString方法,用于返回对象的属性信息
        2.LinkedHashSet 底层维护的是一个 LinkedHashMap(是HashMap的子类),底层结构是 数组(table)+双向链表
        3.第一次添加元素,将table扩容至16
        4.数组(table)的类型是 HashMap$Node,存放的元素类型是 LinkedHashMap$Entry(多态数组)
            LinkedHashMap$Entry继承HashMap$Node:
            LinkedHashMap的静态内部类Entry 继承 HashMap的静态内部类Node(Node只给HashMap这条线使用,因此定义在内部)
            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);
                }
            }
         5.在LinkedHashSet中添加元素:HashSet.add -> HashMap.put -> HashMap.putVal
         */



    }
}

class Customer {
    private String name;
    private int no;

    public Customer(String name, int no) {
        this.name = name;
        this.no = no;
    }
}

2.LinkedHashSet课后练习

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

import java.util.LinkedHashSet;
import java.util.Objects;

@SuppressWarnings({"all"})
public class LinkedHashSetExercise {
    public static void main(String[] args) {
        LinkedHashSet linkedHashSet = new LinkedHashSet();
        linkedHashSet.add(new Car("奥拓", 1000));
        linkedHashSet.add(new Car("奥迪", 300000));
        linkedHashSet.add(new Car("法拉利", 10000000));
        linkedHashSet.add(new Car("奥迪", 300000));
        linkedHashSet.add(new Car("保时捷", 70000000));
        linkedHashSet.add(new Car("奥迪", 300000));

        System.out.println("linkedHashSet = " + linkedHashSet);
        /*未重写equals和hashCode方法:三辆奥迪是三个不同的对象,hash值不同,因此都可以添加进LinkedHashSet
            linkedHashSet = [
            Car{name='奥拓', price=1000.0},
            Car{name='奥迪', price=300000.0},
            Car{name='法拉利', price=1.0E7},
            Car{name='奥迪', price=300000.0},
            Car{name='保时捷', price=7.0E7},
            Car{name='奥迪', price=300000.0}]
          重写equals和hashCode方法:三辆奥迪name和price相同,hash值相同且equals返回true,因此只能添加一个进LinkedHashSet
          (只重写一个不起作用)
            linkedHashSet = [
            Car{name='奥拓', price=1000.0},
            Car{name='奥迪', price=300000.0},
            Car{name='法拉利', price=1.0E7},
            Car{name='保时捷', price=7.0E7}]
         */

    }
}

class Car {
    private String name;
    private double price;

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

    public String getName() {
        return name;
    }

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

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

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

    //重写equals和hashCode方法:当name和price相同时,返回相同的hash值,equals返回true
    @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);
    }
}

  • 19
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值