集合--Set集合--HashSet类、LinkedHashSet类、TreeSet类及其自然排序

Set集合

HashSet类

import java.util.HashSet;

/*
        Set集合:元素唯一且元素无序(存储和取出顺序不一致)的集合
        
        HashSet类概述
             不保证 set 的迭代顺序
             特别是它不保证该顺序恒久不变。
        HashSet如何保证元素唯一性
             底层数据结构是哈希表(元素是链表的数组)
             哈希表依赖于哈希值存储
        添加功能底层依赖两个方法:
             int hashCode()
             boolean equals(Object obj)

        Set集合中的元素为什么不会重复?看添加add()方法的源码
*/
public class SetDemo1 {
    public static void main(String[] args) {
      //用Set的子类去实例化
        HashSet<String> arr = new HashSet<>();

        //添加元素到集合
        arr.add("hello");
        arr.add("world");
        arr.add("java");
        arr.add("bigdata");
        arr.add("hadoop");
        arr.add("hello");
        arr.add("hello");
        arr.add("java");
        arr.add("spark");
        arr.add("flink");
        arr.add("world");
        arr.add("hadoop");

      //增强for循环遍历
        for(String s : arr){
            System.out.println(s);
        }
    }
}

//----------HashSet<>()中add()的源码分析-------

public interface Set<E> extends Collection<E>{

}

public class HashSet<E> extends AbstractSet<E> implements Set<E>{
    private static final Object PRESENT = new Object();

    private transient HashMap<E,Object> map;

    public boolean add(E e) {  
        //E -- String  
        //e -- "hello"
        return map.put(e, PRESENT)==null;
    }
}

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>{

    public V put(K key, V value) {
        //key -- "hello"
        //value -- new Object()
        return putVal(hash(key), key, value, false, true);
    }

    //简单理解为调用元素类的hashCode()方法计算哈希值
    static final int hash(Object key) { //"hello"
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        //理解为哈希表存储的是一个一个的结点数组
        Node<K,V>[] tab; 
        Node<K,V> p; 
        int n, i;

        //判断哈希表是否已经初始化完毕,如果没有初始化,就在这里初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        //根据元素对象计算好的哈希值再进行一次与运算,计算出的是该元素存储在哈希表中的位置
        //如果该元素的位置是null,说明该位置没有元素,可以进行存储
        //就创建新的结点,存储元素
        //分析到这一步我们得出第一个结论,存储的位置与元素类中的hashCode()有关。
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {

            //如果该元素的位置不是null,说明这个位置上已经有元素了,可以确定是哈希值是一样的
            //但是呢,我们并不能确定两个值是不是一样的
            Node<K,V> e; K k;
            
            //先将存入元素的哈希值与该位置中元素的哈希值做比较
            //如果哈希值都不一样,继续走判断instanceof()
            //如果哈希值都一样,会调用元素的equals(k)方法进行比较
            //如果equals(k)方法进行比较结果是false,继续向下执行,最终会将元素插入到集合中或者不插入
            //如果equals(k)方法进行比较结果是true,表示元素的哈希值和内容都一样,表示元素重复了
            //就覆盖,从现象上来看,其实就是不赋值
            //说到这里我们已经知道了add()方法和hashCode()以及equals()方法有关
            //会不会去重取决于元素类型有没有重写hashCode()以及equals()方法
            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;
    }
}
import java.util.Objects;

public class Student2 {
    private String name;
    private int age;

    public Student2() {
    }

    public Student2(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;
    }

  //重写equals()和hashCode()方法
    @Override
    public String toString() {
        return "Student2{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student2 student2 = (Student2) o;
        return age == student2.age &&
                Objects.equals(name, student2.name);
    }

    @Override
    public int hashCode() {

        return Objects.hash(name, age);
    }
}

import java.util.HashSet;

/*
        存储自定义对象并遍历
*/
public class HashSetDemo2 {
    public static void main(String[] args) {
        //创建集合对象
        HashSet<Student2> hashSet = new HashSet<>();

        //创建学生对象
        Student2 s1 = new Student2("xiaowang", 18);
        Student2 s2 = new Student2("xiaowang", 18);
        Student2 s3 = new Student2("xiaoli", 19);
        Student2 s4 = new Student2("xiaoliu", 20);

        hashSet.add(s1);
        hashSet.add(s2);
        hashSet.add(s3);
        hashSet.add(s4);

        for(Student2 s:hashSet){
            System.out.println(s);
        }
      //输出结果发现没有去重,这是为什么呢?
      //根据刚才add()的源码分析,如果没有在Student2类中重写equals()和hashCode()
      //比较的就是对象的地址值,而s1到s4的地址值都不一样,所以都添加。没有达到去重的目的。
      //想要去重,就需要在Student2类中重写equals()和hashCode()方法
    }
}

LinkedHashSet类

import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.TreeSet;

/*
        public class HashSet<E> implements Set<E>{}
        
        public class LinkedHashSet<E> extends HashSet<E>{}

        LinkedHashSet:底层数据结构是哈希表和双向链表
           哈希表保证了元素唯一
           链表保证了元素的有序(存储和取出顺序一致)
*/
public class LinkedHashSetDemo {
    public static void main(String[] args) {
      //创建LinkedHashSet集合对象
        LinkedHashSet<String> arr = new LinkedHashSet<>();

        //添加元素到集合
        arr.add("hello");
        arr.add("world");
        arr.add("java");
        arr.add("bigdata");
        arr.add("hadoop");
        arr.add("hello");
        arr.add("hello");
        arr.add("java");
        arr.add("spark");
        arr.add("flink");
        arr.add("world");
        arr.add("hadoop");

        for (String s : arr){
            System.out.println(s);
        }
    }
}

TreeSet类及其自然排序

import java.util.TreeSet;

/*
    TreeSet:元素唯一,元素的顺序可以按照某种规则进行排序
    两种排序方式:
        自然排序 :无参构造--实现comparable接口
        比较器排序 :有参构造--实现Comparator接口

    A NavigableSet实现基于TreeMap 。
    的元件使用其有序natural ordering ,或由Comparator集合创建时提供,这取决于所使用的构造方法。

    要想知道如何去重以及排序,就去看源码。
    因为Integer类实现了comparable接口,所以它可以做自然排序

*/

public class TreeSetDemo1 {
    public static void main(String[] args) {
        //创建一个集合对象
        TreeSet<Integer> ts = new TreeSet<>();

        //添加元素到集合中
        ts.add(20);
        ts.add(18);
        ts.add(23);
        ts.add(24);
        ts.add(66);
        ts.add(12);
        ts.add(18);
        ts.add(20);
        ts.add(23);
        ts.add(2);

        //遍历
        for(Integer i : ts){
            System.out.println(i);
        }
    }
}
//---------------TreeSet集合的add()方法的源码--------------
    
---------------------------------------
interface Collection {
    ...
}

interface Set extends Collection {
    ...
}
---------------------------------------
class TreeSet implements Set {
    ...
    private static final Object PRESENT = new Object();
    private transient NavigableMap<E,Object> m;
    
    public TreeSet() {
         this(new TreeMap<E,Object>());
    }

    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
    ...
}
---------------------------------------
class TreeMap implements NavigableMap {
    ...
    public V put(K key, V value) {
       Entry<K,V> t = root; // 先造根,TreeSet集合底层数据结构是红黑树(是一个自平衡的二叉树)
       if (t == null) {
           compare(key, key); // type (and possibly null) check

           root = new Entry<>(key, value, null);
           size = 1;
           modCount++;
           return null;
       }
       
       int cmp;
       Entry<K,V> parent;
       // split comparator and comparable paths
       Comparator<? super K> cpr = comparator; // 因为用的是TreeSet的无参构造方法,是自然排序,没有用到comparator比较器
       if (cpr != null) {                      // 所以此时的comparator = null,则程序执行else里面的代码
           do {
               parent = t;
               cmp = cpr.compare(key, t.key);
               if (cmp < 0)
                   t = t.left;
               else if (cmp > 0)
                   t = t.right;
               else
                   return t.setValue(value);
           } while (t != null);
       } else {
           if (key == null)
               throw new NullPointerException();
           Comparable<? super K> k = (Comparable<? super K>) key; // 此接口Comparable强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的 compareTo 方法被称为它的自然比较方法。。
           do {                                                   // 举例中我们使用的是包装类Intrger,而Integer类实现了Comparable接口。此例子是向上转型。
               parent = t;
               cmp = k.compareTo(t.key); // 类的 compareTo 方法被称为它的自然比较方法。
               if (cmp < 0)              // int compareTo(T o) 比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。 
                   t = t.left;
               else if (cmp > 0)
                   t = t.right;
               else
                   return t.setValue(value);
           } while (t != null);
       }
       
       Entry<K,V> e = new Entry<>(key, value, parent);
       if (cmp < 0)
           parent.left = e;
       else
           parent.right = e;
       fixAfterInsertion(e);
       size++;
       modCount++;
       return null;
   }
   ...
}
---------------------------------------
    由上可知:真正的比较是依赖于元素的compareTo()方法,而这个方法compareTo()是定义在 Comparable接口里面的(抽象方法)。
    所以,你要想重写该方法,就必须是先实现 Comparable接口。这个接口表示的就是自然排序。
public class Student3 implements Comparable<Student3> {//为了解决这个问题,让Student3实现Comparable接口
    private String name;           //尖括号中放当前元素类型--Student3,并且重写Comparable接口中的抽象方法
    private int age;

    public Student3() {
    }

    public Student3(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 "Student2{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(Student3 o) {
//        return 0;--只插入第一个元素
//        return 1;--永远都可以插入,没有去重
//        return -1;--也不行,也没去重 

        //这里返回什么,其实应该根据我们的规则来排序
        //比如我想在去重的前提下,按照年龄进行排序
//        return this.age - o.age;

        //年龄一样,姓名不一定一样
        //主要条件(题目要求的条件)
        int i = this.age - o.age;
        //隐含条件(需要自己挖掘)
        int i2 = i == 0 ? this.name.compareTo(o.name) : i;
        return i2;
    }
}

import java.util.TreeSet;

/*
        TreeSet存储学生对象并遍历

        按照正常的写法,我们一运行就报错了
        java.lang.ClassCastException:类型转换异常
        由于我这里创建TreeSet对象调用的是无参构造方法,所以走的是自然排序
        而底层源码有一步向下转型
        Comparable<? super K> k = (Comparable<? super K>) key;
        报错了
        原因是我们Student3类没有实现Comparable接口,无法向下转型,所以报错了。

*/
public class TreeSetDemo2 {
    public static void main(String[] args) {
        //创建集合对象
        TreeSet<Student3> ts = new TreeSet<>();

        //创建学生对象
        Student3 s1 = new Student3("周姐",24);
        Student3 s2 = new Student3("李元浩",25);
        Student3 s3 = new Student3("李湘赫",22);
        Student3 s4 = new Student3("汉子哥",26);
        Student3 s5 = new Student3("硬币哥",21);
        Student3 s6 = new Student3("乌兹",20);
        Student3 s7 = new Student3("李元浩",25);
        Student3 s8 = new Student3("厂长",25);

        //将学生对象插入到集合中
        ts.add(s1);
        ts.add(s2);
        ts.add(s3);
        ts.add(s4);
        ts.add(s5);
        ts.add(s6);
        ts.add(s7);
        ts.add(s8);

        for (Student3 s:ts){
            System.out.println(s);
        }
    }
}
posted @ 2021-12-22 00:08  赤兔胭脂小吕布  阅读(38)  评论(0编辑  收藏  举报