Java--集合--HashSet
- HashSet的全面
- HashSet的底层是HashSet,
- 执行add方法之后,或返回一个boolean值,如果添加成功则返回true,如果添加失败,则返回false
-
package com.model.collection.set.hashset; import java.util.HashSet; /** * @Description:测试类 * @Author: 张紫韩 * @Crete 2021/6/12 15:56 */ public class HashSetDemo01 { public static void main(String[] args) { HashSet<Object> set = new HashSet<>(); /** * * HashSet:无顺序,不可以添加重复的值,可以有null,但是只能由一个null * 1.当执行add添加后会返回一个 boolean,如过插入成功则放回true反正则为false * 2.可以使用remove指定删除对象 * */ System.out.println(set.add("a")); //T System.out.println(set.add("b"));//T System.out.println(set.add("c"));//T System.out.println(set.add("d"));//T System.out.println(set.add("a"));//F System.out.println(set.remove("d"));//T System.out.println(set); //在加深一下,非常经典的面试题 //查看源码,做分析, set=new HashSet<>(); System.out.println(set); set.add("lose");//T set.add("lose");//F set.add(new Dog("tom"));//T set.add(new Dog("tom"));//T set.add(new String("abc"));//T set.add(new String("abc"));//F System.out.println(set); } } class Dog{ private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Dog{" + "name='" + name + '\'' + '}'; } public Dog(String name) { this.name = name; } }
- 底层机制数组链表模拟
-
package com.model.collection.set.hashset; import java.util.HashSet; /** * @Description:测试类 * @Author: 张紫韩 * @Crete 2021/6/12 16:25 */ public class HashSetDemo02 { //模拟一个HashSet底层(HashMop底层结构)(数组+链表+红黑树) public static void main(String[] args) { // HashSet<Object> hashSet = new HashSet<>(); Node[] table=new Node[16]; Node jack=new Node("jack", null); table[2]=jack; Node rose=new Node("rose", null); jack.setNext(rose); Node tom=new Node("tom", null); rose.setNext(tom); Node join=new Node("join", null); tom.setNext(join); System.out.println(table); } } class Node{ private Object item; private Node next; public Node(Object item, Node next) { this.item = item; this.next = next; } public Object getItem() { return item; } @Override public String toString() { return "Node{" + "item=" + item + ", next=" + next + '}'; } public void setItem(Object item) { this.item = item; } public Node getNext() { return next; } public void setNext(Node next) { this.next = next; } }
-
- 源码解析
- 结论:
-
-
解读案例
-
执行 new HashSet()
-
public HashSet() { map = new HashMap<>(); }
-
执行add()方法
-
public boolean add(E e) { return map.put(e, PRESENT)==null; }
-
-
调用执行map.put() 方法(key是传入的e,value是present,是一个常量):
-
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
-
执行hash(key)计算key的hash值
-
执行 putVal(hash(key), key, value, false, true)方法
-
//第一次添加元素时: hashSet.add("java");
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
//创建辅助变量 Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断当前的 表 是否为空(第一次添加),如果时第一次添加元素,则进行扩容,扩容的默认长度是16 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
//通过哈希值获取索引,找到这个元素应该插入的位置节点,并赋值给p,同时判断这个节点上是否为null,如果我这个节点为null,则表示这个节点上没有存放元素,直接将这个元素放在这个节点上,否则放在链表的最后面 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 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;
//判断节点数加一后,是否查过需要扩容的数量 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
- 第二次添加hashSet.add("php");
-
//第二次添加元素时:hashSet.add("php") final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //创建辅助变量 Node<K,V>[] tab; Node<K,V> p; int n, i; //判断当前的 表 是否为空(第一次添加),如果时第一次添加元素,则进行扩容,扩容的默认长度是16(第二次添加表不为空,则不进行扩容) if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //通过哈希值获取索引,找到这个元素应该插入的位置节点,并赋值给p,同时判断这个节点上是否为null,如果我这个节点为null,则表示这个节点上没有存放元素,直接将这个元素放在这个节点上,否则放在链表的最后面 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 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; //判断节点数加一后,是否查过需要扩容的数量 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
- 第三次添加元素 hashSet.add("java");
-
//第一次添加元素时: hashSet.add("java"); final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //创建辅助变量 Node<K,V>[] tab; Node<K,V> p; int n, i; //判断当前的 表 是否为空(第一次添加),如果时第一次添加元素,则进行扩容,扩容的默认长度是16,这是第三次所以不会进入 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //通过哈希值获取索引,找到这个元素应该插入的位置节点,并赋值给p,同时判断这个节点上是否为null,如果我这个节点为null,则表示这个节点上没有存放元素,直接将这个元素放在这个节点上,否则放在链表的最后面
//已经由一个java 放在了索引为3 的地方,所以现在的 p执行的是 第一个java节点,不为空,不会进入,则会进入到else中 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else {
//一个开发技巧:在需要局部变量的时候在定义 Node<K,V> e; K k;
//判断p(链表第一个节点)要插入的元素的hash值是否一样,且(他俩的值也一样或者 自定义的 equals方法一样),如果都一样就不能加入到链表中
//注意:equals():并不是单纯的值相等,这个equals方法我们可以自定义,(String类自定义的equals判断的是值是否相同),我们对其他的类的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);
//如果都不是,则应该是一个链表,进入到else else {
//循环判断链表中所有的节点是否和要添加的节点的值一样且hash值也一样,如果都一样就break不能添加,如果到和链表得元素都不一样就添加到链表的最后 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) {//循环判断,e始终指向p的next,p逐渐向 后移动,e也向后移动,达到遍历整个链表 p.next = newNode(hash, key, value, null);
//添加节点之后,判断节点数量是否到了需要转化成红黑树的数量了 (节点数量为8个的时候) if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//判断当前链表是否可以进行链表转化为红黑树(树化),条件是:节点数到达8个即以上,且 table的数组的个数为64个
//如果节点数量到达了8个但table并没有到达64,会对table进行扩容,不会进化为红黑树
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就是我们加入的节点的数量,当size的数量超过临界值就会进行扩容,不是占用了table表的12个位置才会进行扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } -
树化条件判断:(树化条件判断,如果表为空或者长度未达到64则先进行table扩容,不会进行树化)
-
- 结论:
-
总结:
-
HashSet的底层是HashMap,第一次添加时table数组扩容到16,临界值(threshold)是16*加载因子(loadFactor)是0.75*16=12
-
如果table数组到达了临界值12 ,就会扩容到 16*2=32,新的临界值就是32*0.75=24,以此类推
-
在java8中,如果一条链表的元素个数到达了TREEIFY_THRESHOLD(默认是8),并且table>=MIN_TRERIFY_CAPACITY(默认是64),就会进行树化(红黑树),否则仍然采取数组扩容机制 、
-
数组扩容机制:
-
默认容量为16,临界值为12,在加入13个时就会进行扩容
-
扩容到16的二倍32,此时临界值也就变成了24,当加载第25个元素时HashSet就继续进行扩容到64
-
-
链表树化为红黑树
-
当一个链表数量超过八个时就会进行扩容判断,默认情况下HashSet的容量时16,当加入第9个元素后,由于表的容量为达到64所以,会先扩容数组,不会进行树化,会对数组进行扩容(树化的条件时表的容量到64且链表的长度超过8),
-
此时会变成,链表长度为9个,数组长度为32,
-
再次向链表中加入元素,由于数组长度为达到 64选择再次扩容数组,变成了 链表长度10,数组长度64,
-
当再次向这个链表接入节点时,判断 数组长度到了64个且链表长度大于八个,进行了树化
-
-
-
HashSet课堂练习
-
-
-
-
-
-
package com.model.collection.set.hashset; import java.util.HashSet; import java.util.Objects; /** * @Description:测试类 * @Author: 张紫韩 * @Crete 2021/6/13 15:26 */ public class HashSetDemo05 { public static void main(String[] args) { HashSet<Employee> hashSet = new HashSet<>(); hashSet.add(new Employee("a", 18)); hashSet.add(new Employee("b", 18)); hashSet.add(new Employee("c", 18)); hashSet.add(new Employee("c", 18)); //只加入了一个 c System.out.println(hashSet); } } class Employee{ private String name; private int age; public Employee(String name, int age) { this.name = name; this.age = age; } //设置 :两个Employee对象,的name和age相同就返回相同的hash值,且使用equals方法返回true @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); } @Override public String toString() { return "Employee{" + "name='" + name + '\'' + ", age=" + age + '}'; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
-
-
package com.model.collection.set.hashset; import java.util.HashSet; import java.util.Objects; /** * @Description:测试类 * @Author: 张紫韩 * @Crete 2021/6/13 15:52 */ public class HashSetDemo06 { public static void main(String[] args) { HashSet<Object> hashSet = new HashSet<>(); hashSet.add(new Person("a", 19, new MyDate("1", "1", "1")));//T hashSet.add(new Person("b", 19, new MyDate("1", "1", "1")));//T hashSet.add(new Person("a", 18, new MyDate("1", "1", "1")));//F hashSet.add(new Person("a", 19, new MyDate("2", "1", "1")));//T hashSet.add(new Person("c", 18, new MyDate("2", "1", "1")));//T System.out.println(hashSet); } } class Person{ //当姓名了日期相同时就认为是相同的员工就不能加入到table中 private String name; private int sal; private MyDate myDate; public Person(String name, int sal, MyDate myDate) { this.name = name; this.sal = sal; this.myDate = myDate; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return Objects.equals(name, person.name) && Objects.equals(myDate, person.myDate); } @Override public int hashCode() { return Objects.hash(name, myDate); } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", sal=" + sal + ", myDate=" + myDate + '}'; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getSal() { return sal; } public void setSal(int sal) { this.sal = sal; } public MyDate getMyDate() { return myDate; } public void setMyDate(MyDate myDate) { this.myDate = myDate; } } class MyDate{ private String year; private String month; private String day; public MyDate(String year, String month, String day) { this.year = year; this.month = month; this.day = day; } public String getYear() { return year; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MyDate myDate = (MyDate) o; return Objects.equals(year, myDate.year) && Objects.equals(month, myDate.month) && Objects.equals(day, myDate.day); } @Override public int hashCode() { return Objects.hash(year, month, day); } @Override public String toString() { return "MyDate{" + "year='" + year + '\'' + ", month='" + month + '\'' + ", day='" + day + '\'' + '}'; } public void setYear(String year) { this.year = year; } public String getMonth() { return month; } public void setMonth(String month) { this.month = month; } public String getDay() { return day; } public void setDay(String day) { this.day = day; } }
-