JavaSE学习笔记(二十一)—— Set集合

一、Set集合概述

  Set和List一样,也继承于Collection,是集合的一种。和List不同的是,Set内部实现是基于Map的,所以Set取值时不保证数据和存入的时候顺序一致,并且不允许空值不允许重复值

  Set主要有2个实现方式:HashSet和TreeSet,其中HashSet还有一个子类LinkedHashSet

  这个Set的特点,主要由其内部的Map决定的,可以负责任的说一句,Set就是Map的一个马甲 

二、HashSet

  HashSet:它不保证 set 的迭代顺序;特别是它不保证该顺序恒久不变。

public class SetDemo {
    public static void main(String[] args) {
        // 创建集合对象
        Set<String> set = new HashSet<String>();

        // 创建并添加元素
        set.add("hello");
        set.add("java");
        set.add("world");
        set.add("java");
        set.add("world");

        // 增强for
        for (String s : set) {
            System.out.println(s);
        }
    }
}

   注意:虽然Set集合的元素无序,但是,作为集合来说,它肯定有它自己的存储顺序,而你的顺序恰好和它的存储顺序一致,这代表不了有序,你可以多存储一些数据,就能看到效果。

2.1 存储字符串并遍历

public class HashSetDemo {
    public static void main(String[] args) {
        // 创建集合对象
        HashSet<String> hs = new HashSet<String>();

        // 创建并添加元素
        hs.add("hello");
        hs.add("world");
        hs.add("java");
        hs.add("world");

        // 遍历集合
        for (String s : hs) {
            System.out.println(s);
        }
    }
}

  为什么存储字符串的时候,字符串内容相同的只存储了一个呢?

  查看add()的源码:

interface Collection {
    ...
}

interface Set extends Collection {
    ...
}

class HashSet implements Set {
    private static final Object PRESENT = new Object();
    private transient HashMap<E,Object> map;
    
    public HashSet() {
        map = new HashMap<>();
    }
    
    public boolean add(E e) { //e=hello,world
        return map.put(e, PRESENT)==null;
    }
}

class HashMap implements Map {
    public V put(K key, V value) { //key=e=hello,world
    
        //看哈希表是否为空,如果空,就开辟空间
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        
        //判断对象是否为null
        if (key == null)
            return putForNullKey(value);
        
        int hash = hash(key); //和对象的hashCode()方法相关
        
        //在哈希表中查找hash值
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            //这次的e其实是第一次的world
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
                //走这里其实是没有添加元素
            }
        }

        modCount++;
        addEntry(hash, key, value, i); //把元素添加
        return null;
    }
    
    transient int hashSeed = 0;
    
    final int hash(Object k) { //k=key=e=hello,
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode(); //这里调用的是对象的hashCode()方法

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
}


hs.add("hello");
hs.add("world");
hs.add("java");
hs.add("world");

  通过查看add方法的源码,我们知道这个方法底层依赖 两个方法:hashCode()和equals()。

  步骤:
    首先比较哈希值
    如果相同,继续走,比较地址值或者走equals()
    如果不同,就直接添加到集合中
  按照方法的步骤来说:
    先看hashCode()值是否相同
      相同:继续走equals()方法
        返回true: 说明元素重复,就不添加
        返回false:说明元素不重复,就添加到集合
      不同:就直接把元素添加到集合
  如果类没有重写这两个方法,默认使用的Object()。一般来说不会相同。而String类重写了hashCode()和equals()方法,所以,它就可以把内容相同的字符串去掉。只留下一个。

2.2 存储自定义对象并遍历

  需求:存储自定义对象,并保证元素的唯一性

  要求:如果两个对象的成员变量值都相同,则为同一个元素。

public class HashSetDemo2 {
    public static void main(String[] args) {
        // 创建集合对象
        HashSet<Student> hs = new HashSet<Student>();

        // 创建学生对象
        Student s1 = new Student("林青霞", 27);
        Student s2 = new Student("柳岩", 22);
        Student s3 = new Student("王祖贤", 30);
        Student s4 = new Student("林青霞", 27);
        Student s5 = new Student("林青霞", 20);
        Student s6 = new Student("范冰冰", 22);

        // 添加元素
        hs.add(s1);
        hs.add(s2);
        hs.add(s3);
        hs.add(s4);
        hs.add(s5);
        hs.add(s6);

        // 遍历集合
        for (Student s : hs) {
            System.out.println(s.getName() + "---" + s.getAge());
        }
    }
}

  目前是不符合我的要求的:因为我们知道HashSet底层依赖的是hashCode()和equals()方法。而这两个方法我们在学生类中没有重写,所以,默认使用的是Object类。这个时候,他们的哈希值是不会一样的,根本就不会继续判断,就直接执行了添加操作。

  所以我们要在Student类中重写这两个方法——自动生成即可

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

    public Student() {
        super();
    }

    public Student(String name, int age) {
        super();
        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 int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + age;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Student other = (Student) obj;
        if (age != other.age)
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }

    // @Override
    // public int hashCode() {
    // // return 0;
    // // 因为成员变量值影响了哈希值,所以我们把成员变量值相加即可
    // // return this.name.hashCode() + this.age;
    // // 看下面
    // // s1:name.hashCode()=40,age=30
    // // s2:name.hashCode()=20,age=50
    // // 尽可能的区分,我们可以把它们乘以一些整数
    // return this.name.hashCode() + this.age * 15;
    // }
    //
    // @Override
    // public boolean equals(Object obj) {
    // // System.out.println(this + "---" + obj);
    // if (this == obj) {
    // return true;
    // }
    //
    // if (!(obj instanceof Student)) {
    // return false;
    // }
    //
    // Student s = (Student) obj;
    // return this.name.equals(s.name) && this.age == s.age;
    // }
    //
    // @Override
    // public String toString() {
    // return "Student [name=" + name + ", age=" + age + "]";
    // }

}

2.3  HashSet元素保证唯一性的原理

2.4 练习

  编写一个程序,获取10个1至20的随机数,要求随机数不能重复。

/**
 * 分析:
 *         A:创建随机数对象
 *         B:创建一个HashSet集合
 *         C:判断集合的长度是不是小于10
 *             是:就创建一个随机数添加
 *             否:不搭理它
 *         D:遍历HashSet集合
 */
public class HashSetDemo {
    public static void main(String[] args) {
        // 创建随机数对象
        Random r = new Random();

        // 创建一个Set集合
        HashSet<Integer> set = new HashSet<>();

        // 判断集合的长度是不是小于10
        while (set.size() < 10) {
            int num = r.nextInt(20) + 1;
            set.add(num);
        }

        // 遍历Set集合
        for (Integer i : set) {
            System.out.println(i);
        }
    }
}

2.5 LinkedHashSet

  LinkedHashSet是HashSet的子类,它的底层数据结构是由哈希表和链表组成的。
  哈希表保证了元素的唯一性。
  链表保证了元素有序。(存储和取出是一致)

public class LinkedHashSetDemo {
    public static void main(String[] args) {
        // 创建集合对象
        LinkedHashSet<String> hs = new LinkedHashSet<String>();

        // 创建并添加元素
        hs.add("hello");
        hs.add("world");
        hs.add("java");
        hs.add("world");
        hs.add("java");

        // 遍历
        for (String s : hs) {
            System.out.println(s);
        }
    }
}

三、TreeSet

  TreeSet的底层数据结构是红黑树(一种自平衡的二叉树),能够对元素按照某种规则进行排序。其中排序有两种方式:自然排序和比较器排序。

  TreeSet集合的特点:排序唯一

public class TreeSetDemo {
    public static void main(String[] args) {
        // 创建集合对象
        // 自然顺序进行排序
        TreeSet<Integer> ts = new TreeSet<Integer>();

        // 创建元素并添加
        // 20,18,23,22,17,24,19,18,24
        ts.add(20);
        ts.add(18);
        ts.add(23);
        ts.add(22);
        ts.add(17);
        ts.add(24);
        ts.add(19);
        ts.add(18);
        ts.add(24);

        // 遍历
        for (Integer i : ts) {
            System.out.println(i);//17 18 19 20 22 23 24
        }
    }
}

3.1 TreeSet如何保证元素的排序和唯一

  我们查看TreeSet的add()方法的源码,其实最终要看TreeMap的put()方法

interface Collection {...}

interface Set extends Collection {...}

interface NavigableMap {

}

class TreeMap implements NavigableMap {
     public V put(K key, V value) {
        Entry<K,V> t = root;
        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;
        if (cpr != null) {
            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;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    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;
    }
}

class TreeSet implements Set {
    private transient NavigableMap<E,Object> m;
    
    public TreeSet() {
         this(new TreeMap<E,Object>());
    }

    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
}

  通过查看add()的源码,我们发现:真正的比较是依赖于元素的compareTo()方法,而这个方法是定义在 Comparable里面的。所以,你要想重写该方法,就必须是先实现Comparable接口,这个接口表示的就是自然排序。

  因为在TreeSetDemo中,Integer类本身就已经实现了Comparable接口,所以在就能按自然排序的顺序输出。

【总结】

  唯一性:是根据比较的返回是否是0来决定。

  排序:

  • 自然排序(元素具备比较性):让元素所属的类实现自然排序接口 Comparable
  • 比较器排序(集合具备比较性):让集合的构造方法接收一个比较器接口的子类对象 Comparator

3.2 TreeSet存储自定义对象

  需求:保证排序和唯一

【方式一:使用自然排序】

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

        // 创建元素
        Student s1 = new Student("linqingxia", 27);
        Student s2 = new Student("zhangguorong", 29);
        Student s3 = new Student("wanglihong", 23);
        Student s4 = new Student("linqingxia", 27);
        Student s5 = new Student("liushishi", 22);
        Student s6 = new Student("wuqilong", 40);
        Student s7 = new Student("fengqingy", 22);

        // 添加元素
        ts.add(s1);
        ts.add(s2);
        ts.add(s3);
        ts.add(s4);
        ts.add(s5);
        ts.add(s6);
        ts.add(s7);

        // 遍历
        for (Student s : ts) {
            System.out.println(s.getName() + "---" + s.getAge());
        }
    }
}

  因为上面TreeSet采用的是无参构造,无参构造默认使用的是自然排序。而如果一个类的元素要想能够进行自然排序,就必须实现自然排序接口

public class Student implements Comparable<Student> {
    private String name;
    private int age;

    public Student() {
        super();
    }

    public Student(String name, int age) {
        super();
        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 int compareTo(Student s) {
        // return 0;
        // return 1;
        // return -1;

        // 这里返回什么,其实应该根据我的排序规则来做
        // 按照年龄排序,主要条件
        int num = this.age - s.age;
        // 次要条件
        // 年龄相同的时候,还得去看姓名是否也相同
        // 如果年龄和姓名都相同,才是同一个元素
        int num2 = num == 0 ? this.name.compareTo(s.name) : num;
        return num2;
    }
}

【方式二:使用比较器排序】

  需求:请按照姓名的长度排序

  学生类对象:

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

    public Student() {
        super();
    }

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

    get/set...
}

  实现比较器:

public class MyComparator implements Comparator<Student> {

    @Override
    public int compare(Student s1, Student s2) {
        // int num = this.name.length() - s.name.length();
        // this -- s1
        // s -- s2
        // 姓名长度
        int num = s1.getName().length() - s2.getName().length();
        // 姓名内容
        int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
        // 年龄
        int num3 = num2 == 0 ? s1.getAge() - s2.getAge() : num2;
        return num3;
    }

}

  测试:

public class TreeSetDemo {
    public static void main(String[] args) {
        // 创建集合对象
        // public TreeSet(Comparator comparator) //比较器排序
        TreeSet<Student> ts = new TreeSet<Student>(new MyComparator());// 创建元素
        Student s1 = new Student("linqingxia", 27);
        Student s2 = new Student("zhangguorong", 29);
        Student s3 = new Student("wanglihong", 23);
        Student s4 = new Student("linqingxia", 27);
        Student s5 = new Student("liushishi", 22);
        Student s6 = new Student("wuqilong", 40);
        Student s7 = new Student("fengqingy", 22);
        Student s8 = new Student("linqingxia", 29);

        // 添加元素
        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 (Student s : ts) {
            System.out.println(s.getName() + "---" + s.getAge());
        }
    }
}

【方式三:使用匿名内部类的比较器排序】——推荐

public class TreeSetDemo {
    public static void main(String[] args) {
        // 创建集合对象
        // TreeSet<Student> ts = new TreeSet<Student>(); //自然排序
        // public TreeSet(Comparator comparator) //比较器排序
        // TreeSet<Student> ts = new TreeSet<Student>(new MyComparator());

        // 如果一个方法的参数是接口,那么真正要的是接口的实现类的对象
        // 而匿名内部类就可以实现这个东西
        TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() {
            @Override
            public int compare(Student s1, Student s2) {
                // 姓名长度
                int num = s1.getName().length() - s2.getName().length();
                // 姓名内容
                int num2 = num == 0 ? s1.getName().compareTo(s2.getName())
                        : num;
                // 年龄
                int num3 = num2 == 0 ? s1.getAge() - s2.getAge() : num2;
                return num3;
            }
        });

        // 创建元素
        Student s1 = new Student("linqingxia", 27);
        Student s2 = new Student("zhangguorong", 29);
        Student s3 = new Student("wanglihong", 23);
        Student s4 = new Student("linqingxia", 27);
        Student s5 = new Student("liushishi", 22);
        Student s6 = new Student("wuqilong", 40);
        Student s7 = new Student("fengqingy", 22);
        Student s8 = new Student("linqingxia", 29);

        // 添加元素
        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 (Student s : ts) {
            System.out.println(s.getName() + "---" + s.getAge());
        }
    }
}

3.3 练习

  键盘录入5个学生信息(姓名,语文成绩,数学成绩,英语成绩),按照总分从高到低输出到控制台

/**
 * 分析:
 *         A:定义学生类
 *         B:创建一个TreeSet集合
 *         C:总分从高到底如何实现呢?
 *         D:键盘录入5个学生信息
 *         E:遍历TreeSet集合
 */
public class TreeSetDemo {
    public static void main(String[] args) {
        // 创建一个TreeSet集合
        TreeSet<Student> set = new TreeSet<>(new Comparator<Student>() {
            @Override
            public int compare(Student s1, Student s2) {
                // 总分从高到低
                int num = s2.getSum() - s1.getSum();
                // 总分相同的不一定语文相同
                int num2 = num == 0 ? s1.getChinese() - s2.getChinese() : num;
                // 总分相同的不一定数序相同
                int num3 = num2 == 0 ? s1.getMath() - s2.getMath() : num2;
                // 总分相同的不一定英语相同
                int num4 = num3 == 0 ? s1.getEnglish() - s2.getEnglish() : num3;
                // 姓名还不一定相同呢
                int num5 = num4 == 0 ? s1.getName().compareTo(s2.getName()) : num4;
                return num5;
            }
        });

        System.out.println("学生信息录入开始");
        // 键盘录入5个学生信息
        for (int x = 1; x <= 5; x++) {
            Scanner sc = new Scanner(System.in);
            System.out.println("请输入第" + x + "个学生的姓名:");
            String name = sc.nextLine();
            System.out.println("请输入第" + x + "个学生的语文成绩:");
            String chineseString = sc.nextLine();
            System.out.println("请输入第" + x + "个学生的数学成绩:");
            String mathString = sc.nextLine();
            System.out.println("请输入第" + x + "个学生的英语成绩:");
            String englishString = sc.nextLine();

            // 把数据封装到学生对象中
            Student s = new Student();
            s.setName(name);
            s.setChinese(Integer.parseInt(chineseString));
            s.setMath(Integer.parseInt(mathString));
            s.setEnglish(Integer.parseInt(englishString));

            // 把学生对象添加到集合
            set.add(s);
        }

        System.out.println("学生信息录入完毕");
        System.out.println("学习信息从高到低排序如下:");
        System.out.println("姓名\t语文成绩\t数学成绩\t英语成绩");

        // 遍历集合
        for (Student s : set) {
            System.out.println(s.getName() + "\t" + s.getChinese() + "\t"
                    + s.getMath() + "\t" + s.getEnglish());
        }

    }
}

 

posted @ 2019-05-15 14:56  yi0123  阅读(413)  评论(0编辑  收藏  举报