Java基础知识-第17章-Set接口以及常用实现类介绍

157 阅读20分钟

1、Set接口概述

  • Set接口是Collection的子接口,set接口没有提供额外的方法,使用的都是Collection中声明过的方法。
  • Set:存储无序的、不可重复的数据
  • Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败。
  • Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法,因此向Set(主要指:HashSet、LinkedHashSet)中添加的数据,其所在的类一定要重写hashCode()equals()
    • 重写的hashCode()equals()尽可能保持一致性:相等的对象必须具有相等的散列码
    • 即两个对象equals()时的时候为true,则hashCode()算出来的值也要一样
    • 重写两个方法的小技巧:对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。

1.1、Set接口继承体系

image.png

  • 实现了 Serializable 接口,表明它支持序列化。
  • 实现了 Cloneable 接口,表明它支持克隆,可以调用超类的 clone()方法进行浅拷贝。
  • 继承了 AbstractSet 抽象类,和 ArrayList 和 LinkedList 一样,在他们的抽象父类中,都提供了 equals() 方法和 hashCode() 方法。它们自身并不实现这两个方法
  • 实现类实现了 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持,不能保证元素的顺序。

HashSet 特性

  • 不能保证元素的顺序,元素是无序的
  • HashSet 不是同步的,需要外部保持线程之间的同步问题
  • 集合元素值允许为 null

1.2、Set:存储无序的、不可重复的数据

无序性

不等于随机性,向集合里面添加数据,存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的hashCode哈希值决定数据在数组的哪个位置,所以我们向HashSetLinkedHashSet里面添加数据,最后的打印结果就是和添加数据的顺序不一样,至于为什么两者不一样,原因就是因为两者底层的数据结构不同而已。

public class RunoobTest {
    //以HashSet为例说明:
    @Test
    public void test1(){
        Set set = new HashSet(); //new LinkedHashSet
        set.add(456); //自动装箱
        set.add(123);
        set.add("AA");
        set.add("CC");
        set.add(129);

        //set接口继承了collection接口,自然也有迭代器方法
        Iterator iterator = set.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}

结果:

//以HashSet为例说明:打印结果不是按照添加顺序实现的
AA
CC
129
456
123

//以LinkedHashSet为例说明:打印结果按照添加顺序实现的
456
123
AA
CC
129

不可重复性

保证添加的元素按照equals()判断时,不能返回true,即相同的元素只能添加一个,但是还要加上hashcode()才行,具体原因参考下面内容

Set set = new HashSet(); //new LinkedHashSet
set.add(456);
set.add(123);
set.add(123);//会被认为是重复的,添加不进去,因为integer类重写了equals方法和hashcode()方法
set.add("AA");
set.add("CC");
set.add(new user("tom",12));
set.add(new user("tom",12));//两个tom不是重复的,主要原因就是我们的user类中没有重写equals方法和hashcode()方法
set.add(129);

2、HashSet实现类

2.1、HashSet 概述

HashSet 是 Set 接口的典型实现,由哈希表(实际上是一个 HashMap 实例)支持,大多数时候使用 Set 集合时都使用这个实现类。

  • HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。
  • HashSet 具有以下特点:
    • 它不保证 set 中元素的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用 null 元素。
    • HashSet 不是线程安全的
    • 集合元素可以是 null
  • 往集合中添加数据时,HashSet 集合判断两个元素相等的标准:
    • 两个对象通过 hashCode() 方法比较相等
    • 并且两个对象的 equals() 方法返回值也相等。
  • 对于存放在Set容器中的对象,对应的类一定要重写equals()hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。

2.2、HashSet方法

查看源码即可,在Collection接口中已经说过,只不过在调用每一个重写的方法的时候,记住要进行hashcode哈希值的判断。

//HashSet的遍历操作
//通过这个方法可以发现,HashSet调用了HashMap存放,因为HashSet并不是键值对存储,所以它只是把它的值做了Map中的键,在遍历HashSet的集合元素时,实际上是遍历的Map中Key的集合。
public Iterator<E> iterator() {
    return map.keySet().iterator();
}

//返回集合中元素的容量
public int size() {
    return map.size();
}

//判断是否为空
public boolean isEmpty() {
    return map.isEmpty();
}

//是否包含指定的元素
public boolean contains(Object o) {
    return map.containsKey(o);
}

//添加元素,添加的元素作为了Map中的key,value使用了一个常量表示
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

//删除元素
public boolean remove(Object o) {
    return map.remove(o)==PRESENT;
}

//清空集合
public void clear() {
    map.clear();
}

//克隆方法
public Object clone() {
    try {
        HashSet<E> newSet = (HashSet<E>) super.clone();
        newSet.map = (HashMap<E, Object>) map.clone();
        return newSet;
    } catch (CloneNotSupportedException e) {
        throw new InternalError();
    }
}

//写入输出流操作。
private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException {
    // Write out any hidden serialization magic
    s.defaultWriteObject();

    // Write out HashMap capacity and load factor
    s.writeInt(map.capacity());
    s.writeFloat(map.loadFactor());

    // Write out size
    s.writeInt(map.size());

    // Write out all elements in the proper order.
    for (E e : map.keySet())
        s.writeObject(e);
}

//从输入流中读取对象
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    // Read in any hidden serialization magic
    s.defaultReadObject();

    // Read in HashMap capacity and load factor and create backing HashMap
    int capacity = s.readInt();
    float loadFactor = s.readFloat();
    map = (((HashSet)this) instanceof LinkedHashSet ?
           new LinkedHashMap<E,Object>(capacity, loadFactor) :
           new HashMap<E,Object>(capacity, loadFactor));

    // Read in size
    int size = s.readInt();

    // Read in all elements in the proper order.
    for (int i=0; i<size; i++) {
        E e = (E) s.readObject();
        map.put(e, PRESENT);
    }
}
}

2.3、HashSet源码简析

对于 HashSet 而言,它是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存所有元素,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成

参考blog.csdn.net/m0_38101105…

从下面程序出发

HashSet set = new HashSet(); 
set.add(456);

HashSet部分源码

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable{
    static final long serialVersionUID = -5024744406713321676L;
	//HashSet底层使用HashMap来保存HashSet中所有元素。
    private transient HashMap<E,Object> map;
    // 定义一个虚拟的Object对象作为HashMap的value,将此对象定义为static final。
    private static final Object PRESENT = new Object();
 
    /** 
     * 默认的无参构造器,构造一个空的HashSet。 
     * 实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。 
     */
    public HashSet() {
        map = new HashMap<>();
    }
    /** 
     * 构造一个包含指定collection中的元素的新set。 
     * 
     * 实际底层使用默认的加载因子0.75和足以包含指定 
     * collection中所有元素的初始容量来创建一个HashMap。 
     * @param c 其中的元素将存放在此set中的collection。 
     */ 
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }
   /** 
     * 以指定的initialCapacity和loadFactor构造一个空的HashSet。 
     * 实际底层以相应的参数构造一个空的HashMap。 
     * @param initialCapacity 初始容量。 
     * @param loadFactor 加载因子。 
     */ 
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }

    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }

    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

add方法源码

/** 
     * 添加一个元素,如果该元素已经存在,则返回true,如果不存在,则返回false 
     * @param e 将添加到此set中的元素。 
     * @return 如果此set尚未包含指定元素,则返回true。 
     */
public boolean add(E e) {
    //往map中添加元素,返回null,说明是第一个往map中添加该key。
    return map.put(e, PRESENT)==null;
}
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

总结:

底层结构

由上面源程序可以看出,HashSet的实现其实非常简单, 它只是封装了一个HashMap对象来存储所有的集合元素。所有放入HashSet中的集合元素实际上由HashMap的key来保存,而HashMap的value则存储了一个PRESENT,它是一个静态的Object对象。

HashMap底层在jdk1.7的数据结构就是哈希表,即HashSet的底层结构就是哈希表,而哈希表可以由数组+链表实现,HashSet底层就是这样的一个结构,故存在HashMap中的key,应该重写equals()hashCode() 方法。

add方法

一个哈希表有三列,第一列是hash码,第二列是key,第三列是value,往哈希表中存进一个元素(即调用add()方法),会调用 key的hashCode()方法算出哈希值,根据哈希值得出该key在哈希表中的索引,接着就会遍历该索引中的所有的元素,如果通过hashCode()方法计算出没有相同的key值,则成功存入该元素,如果有相同的哈希值则调用key的equals() 方法,如果equals()方法返回true,则会覆盖哈希表中的元素,返回覆盖之前的值,否则就返回null。你可能会问,为什么有相同的哈希值的话,还要去比较equals,因为即使两个完全不相同的对象它的hashCode值也有可能相同,但是不相同的两个对象hashCode一定不相同

所以当第一次往 HashSet 中添加元素时,add方法会返回 false;由于 HashMap 中的 key 不能重复,所以 HashSet 不能存储重复元素。HashSet的绝大部分方法都是通过调用HashMap的方法来实现的, 因此HashSet和HashMap两个集合在实现本质上是相同的

HashSet的add()方法添加集合元素时实际上是转变为调用HashMap的put()方法来添加key-value对,当新放入HashMap的Entry中的key与集合中原有Entrykey相同(hashCode()返回值相等,通过equals比较也返回true)时,新添加的Entry的value将覆盖原来Entry的value,但key不会有任何改变。因此,如果向HashSet中添加一个已经存在的元素,新添加的集合元素(底层由HashMap的key保存)不会覆盖已有的集合元素。这也就满足了Set中元素不重复的特性。

自动扩容机制

底层也是数组,初始容量为16,当如果使用率超过0.75,(16*0.75=12) 就会扩大容量为原来的2倍。(16扩容为32,依次为64,128....等)这是和HashMap是一样的。

2.4、向HashSet中添加元素的过程

2.4.1、添加步骤

我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法计算元素a的哈希值,然后根据此哈希值,通过某种算法【散列函数】计算出该元素在HashSet底层数组中的存放位置(即为:索引位置),然后判断数组此位置上是否已经有元素:

  • 如果此位置上没有其他元素,则元素a添加成功 --->情况1
  • 如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值
    • 如果hash值不相同,则元素a添加成功--->情况2
    • 如果hash值相同,进而需要调用元素a所在类的equals()方法
      • equals()返回true,元素a添加失败,这个失败指的是覆盖Key。
      • equals()返回false,则元素a添加成功,并且会通过链表的方式继续链接--->情况3
  • 对于添加成功的情况2和情况3而言:元素a 与已经存在指定索引位置上数据以链表的方式存储。
  • 元素在数组中的位置
    • jdk 7 :元素a放到数组中,指向原来的元素。
    • jdk 8 :原来的元素在数组中,指向元素a

图示:

image.png

这个散列函数会与HashSet底层数组的长度相计算得到在数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布, 该散列函数设计的越好

2.4.2、添加原则

因此从上面步骤出发,Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法,因此向Set(主要指:HashSet、LinkedHashSet)中添加的数据,其所在的类一定要重写hashCode()equals()

重写 hashCode() 方法的基本原则

在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。 即重写的hashCode()equals()尽可能保持一致性:相等的对象必须具有相等的散列码

  • 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值也应相等。
  • 对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。

重写 equals() 方法的基本原则

以自定义的Customer类为例,何时需要重写equals()

  • 当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是要改写hashCode(),根据一个类的equals方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是,根据Object.hashCode()方法, 它们仅仅是两个对象。
  • 因此,违反了“相等的对象必须具有相等的散列码”。
  • 结论:复写equals方法的时候一般都需要同时复写hashCode方法。通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。

2.4.3、代码演示

package com.lemon.java;

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

/**
 * @Author Lemons
 * @create 2022-02-25-10:39
 */
class Person {
    private String name;
    private int age;

    public Person(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 boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

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

public class HashSetTest {
    public static void main(String[] args) {
        HashSet<Person> set = new HashSet<Person>();
        Person p1 = new Person("zhangsan", 22);
        Person p2 = new Person("zhangsan", 22);

        set.add(p1);
        set.add(p2);

        System.out.println(set.size()); //1

    }
}

:以Eclipse/IDEA为例,在自定义类中可以调用工具自动重写equals和hashCode。 问题:为什么用Eclipse/IDEA复写hashCode方法,该方法内部源码实现有31这个数字?

//hash方法内部的hashcode方法源码
public static int hashCode(Object a[]) {
    if (a == null)
        return 0;

    int result = 1;

    for (Object element : a)
        result = 31 * result + (element == null ? 0 : element.hashCode());

    return result;
}
  • 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的 “冲突”就越少,查找起来效率也会提高。(减少冲突)
  • 并且31只占用5bits,相乘造成数据溢出的概率较小。31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效 率)
  • 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结 果只能被素数本身和被乘数还有1来整除!(减少冲突)

3、LinkedHashSet 实现类

3.1、LinkedHashSet概述

LinkedHashSet作为HashSet的子类,遍历其内部数据时,可以按照添加的顺序遍历,但是这并不是说LinkedHashSet就是有序的。对于频繁的遍历操作,LinkedHashSet 效事高于HashSet

  • LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置, 但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
  • LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。
  • LinkedHashSet 不允许集合元素重复。
  • LinkedHashSet作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据。

注意,此实现不是同步的。如果多个线程同时访问链接的哈希 Set,而其中至少一个线程 修改了该 Set,则它必须保持外部同步。

图解

image.png

3.2、源码解析

对于 LinkedHashSet 而言,它继承与 HashSet、又基于 LinkedHashMap 来实现的。 LinkedHashSet 底层使用 LinkedHashMap 来保存所有元素,它继承与 HashSet,其所有 的方法操作上又与 HashSet 相同,因此 LinkedHashSet 的实现上非常简单,只提供了四个 构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操作上与父类 HashSet 的操作相同,直接调用父类 HashSet 的方法即可。 LinkedHashSet 的源代码如下:

4、TreeSet实现类

4.1、TreeSet实现类概述

TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。TreeSet底层使用红黑树结构存储数据

  • 有序,查询速度比List快
  • 向TreeSet中添加的数据,可以按照添加对象的指定属性进行排序,因此必须要求是相同类的对象,所以会涉及到我们的对象排序比较
@Test  
public void test1(){  
  TreeSet set = new TreeSet();    
  //报错:不能添加不同类的对象  
  set.add(123);  
  set.add(456);  
  set.add("AA");  
  set.add(new User("Tom",12)); 
} 

新增的方法如下:

Comparator comparator()
Object first()
Object last()
Object lower(Object e)
Object higher(Object e)
SortedSet subSet(fromElement, toElement)
SortedSet headSet(toElement)
SortedSet tailSet(fromElement)

红黑树结构

image.png

4.2、TreeSet 两种排序方法

4.2.1、自然排序(实现Comparable接口)

TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序。如下

@Test  
public void test1(){  
    TreeSet set = new TreeSet();    
    set.add(123);  //自动装箱,Integer类实现了compareTo方法
    set.add(456);  
    set.add(12);  
    set.add(345); 

    Iterator iterator = set.iterator();
    while(iterator.hasNext()){
        System.out.println(iterator.next());
    }
} 

结果:

//从小到大顺序排列
12
123
345
456

自然排序:

  • TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列
  • 如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable 接口。
  • 实现 Comparable 接口的类实现必须实现 compareTo(Object obj) 方法,两个对象即通过 compareTo(Object obj) 方法的返回值来比较大小。
  • Comparable接口的典型实现类:
    • BigDecimal、BigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小进行比较
    • Character:按字符的 unicode 值来进行比较
    • Boolean:true 对应的包装类实例大于 false 对应的包装类实例
    • String:按字符串中字符的 unicode 值进行比较
    • Date、Time:后边的时间、日期比前面的时间、日期大
  • 这些实现类都实现了Comparable接口,重写了compareTo()方法,重写compareTo()的规则:
    • 如果当前对象大于形参对象,返回正整数,若小于,则返回负数,若相等,返回零
    • 对于自定义类来说,如果需要排序,可以让自定义类实现Comparable接口,重写CompareTo()方法,在CompareTo()方法中指明如何排序

自定义类代码演示自然排序

自定义类对象的自然排序,要想比较大小,得实现comparable接口

Comparable接口使用举例:

  • String、包装类等实现了Comparable接口,重写了compareTo()方法
  • 重写compareTo()的规则:如果当前对象大于形参对象,返回正整数,若小于,则返回负数,若相等,返回零。对于自定义类来说,如果需要排序,可以让自定义类实现Comparable接口,重写CompareTo()方法,在CompareTo()方法中指明如何排序

Goods类

//按照商品名字从小到大排列
public class Goods implements Comparable{
    private double price;
    private String name;

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


    @Override
    public int compareTo(Object o) {
        if(o instanceof Goods){
            Goods good = (Goods)o;
            return this.name.compareTo(good.name);//升序
            //-return this.name.compareTo(good.name);//降序
        }else{
            throw new RuntimeException("ERROR"); 
        }
    }

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

测试:

@Test  
public void test1(){  
    
    TreeSet set = new TreeSet();    

    set.add(new Goods(300, "A商品"));  
    set.add(new Goods(543, "E商品"));  
    set.add(new Goods(154, "C商品"));  
    set.add(new Goods(300, "D商品"));  

    Iterator iterator = set.iterator();
    while(iterator.hasNext()){
        System.out.println(iterator.next());
    }
}

结果:

//实现了按照商品名字从小到大排列
Goods{price=300.0, name='A商品'}
Goods{price=154.0, name='C商品'}
Goods{price=300.0, name='D商品'}
Goods{price=543.0, name='E商品'}

如果要先按照价格从小到大排列,再按照名字从小到大排列,则

@Override
public int compareTo(Object o) {
    if(o instanceof Goods){
        Goods good = (Goods)o;
        int number = Double.compare(this.price, good.price);

        if(number != 0){
            return number;
        }else{
            return this.name.compareTo(good.name);
        }
    }

    throw new RuntimeException("ERROR");
}

向 TreeSet 中添加元素

向 TreeSet 中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较。

  • 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类的对象。
  • 对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(Object obj) 方法**比较返回值,**不是equals方法
  • 当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 方法时,应保证该方法与 compareTo(Object obj) 方法有一致的结果:如果两个对象通过 equals() 方法比较返回 true,则通过 compareTo(Object obj) 方法比较应返回 0。 否则,让人难以理解。
@Test  
public void test1(){  

    TreeSet set = new TreeSet();    

    set.add(new Goods(300, "A商品"));  
    set.add(new Goods(543, "E商品"));  
    set.add(new Goods(154, "C商品"));  
    set.add(new Goods(300, "D商品"));
    set.add(new Goods(700, "D商品"));//这个不会加进去,因为比较对象相同的是compareTo()方法,返回0,想要将该数字加进去,可以通过二级排序。

    Iterator iterator = set.iterator();
    while(iterator.hasNext()){
        System.out.println(iterator.next());
    }
}

4.2.2、定制排序(实现Comparator接口)

TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素,或希望按照其它属性大小进行排序,则可以考虑使用定制排序。

定制排序,通过实现Comparator接口来实现,需要重写 compare(T o1,T o2)方法。

  • 利用 int compare(T o1,T o2) 方法,比较o1和o2的大小:
    • 如果方法返回正整数,则表示o1大于o2;
    • 如果返回0,表示相等;
    • 返回负整数,表示o1小于o2。
  • 要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
    • 此时,仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异常。
    • 使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0,不再是equals()

代码实现:

package com.lemon.java;

import org.junit.Test;
import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;

public class JUnitTest {

    @Test
    public void test1() {
        Comparator com = new Comparator() {
            //按照年龄从小到大排列
            @override
            public int compare(object o1, object o2) {
                if(o1 instanceof User && o2 instanceof User){
                    User u1 = (User)o1;
                    User u2 = (User)o2;
                    return Integer.compare(u1.getAge(),u2.getAge());
                }else{
                    throw new RuntineException("输入的数据类型不匹配");
                }
            }
        };
        TreeSet set = new TreeSet(com);//会按照我们加的参数,不加就是自然排序
        set.add(new User("Tom",12));
        set.add(new User("Jerry",32));
        set.add(new User("Jin",2));
        set.add(new User("Mike",65));
        set.add(nen User("Mary",33));
        set.add(nen User("Jack",33));
        set.add(new User("Jack",56));

        Iterator iterator = set.iterator();
        while(iterator.hasNext()){
            Systen.out.printIn(iterator.next());
        }
    }
}

5、练习

练习:输出打印结果

@Test //其中Person类中重写了hashCode()和equal()方法
public void test1() {
    
    HashSet set = new HashSet();
    
    Person p1 = new Person(1001,"AA");
    Person p2 = new Person(1002,"BB");
    
    set.add(p1);
    set.add(p2);
    System.out.println(set);//[Person(1001,"AA"),Person(1002,"BB")]
    
    p1.name = "CC";
    set.remove(p1);//这个移除是按照哈希值计算的,而现在是按照1001,CC的哈希值对应的元素位置。计算出的哈希值找到对应的元素,就很有可能删除的不是原来的1001,AA的哈希值对应的元素位置。
    System.out.println(set);//[Person(1001,"CC"),Person(1002,"BB")]
    
    set.add(new Person(1001,"CC"));//此时1001,"CC"这个对应的位置已经被上面删除了,为空
    System.out.println(set);//[Person(1001,"CC"),Person(1001,"CC"),Person(1002,"BB")]
    
    set.add(new Person(1001,"AA"));//equals不同
    System.out.println(set);//[Person(1001,"CC"),Person(1001,"CC"),Person(1002,"BB"),Person(1001,"AA")]
}

练习:在List内去除重复数字值,要求尽量简单

public static List duplicateList(List list) {
    HashSet set = new HashSet();//利用Hashset去重
    set.addAll(list);
    return new ArrayList(set);
    
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(new Integer(1)); 
        list.add(new Integer(2));
        list.add(new Integer(2));
        list.add(new Integer(4));
        list.add(new Integer(4));
        List list2 = duplicateList(list);
        for (Object integer : list2) {
            System.out.println(integer); //1.2.4
        }
    }
}

注意:自定义的类要想实现去重,则必须实现equals方法和hashcode方法