【Java基础】分析Set接口常用实现类

Java集合架构图

点击放大查看
在这里插入图片描述
List , Set, Map都是接口,List , Set继承至Collection接口(Collection继承至Iterable),Map为独立接口

Set接口简介

  • Set是一种不包括重复元素的Collection。
  • 与List一样,允许添加null的元素

Set接口直接或者间接实现类

Set集合 说明
HashSet 无序并且性能比TreeSet高效,底层数据结构是哈希表(底层由 HashMap 实现)。(无序,唯一) ,依赖两个方法: hashCode()和equals()
TreeSet 底层数据结构是红黑树(平衡二叉树)。(唯一,有序),需要采用红黑树算法维护数据的排序,对Set的数据类型有要求(添加元素需要实现Comparable接口或者是在TreeSet构造方法上自定义排序Comparator),性能较HashSet低效,比较适用于需要排序的场景。
LinkedHashSet 底层数据结构是链表和哈希表。(FIFO插入有序,唯一) 由链表保证元素有序 ,由哈希表保证元素唯一,其他与HashSet无太大差异,性能较HashSet略低(链表开销)
EnumSet 这四种常见Set实现类中最高效的,采用的是位向量的数据结构存储元素(存储高效、紧凑),要求存储元素必须是一个枚举类Enum(约束大),适用于枚举值执行批量操作的场景

上面4种Set都是非线程安全的,即需要使用代码同步机制保证多线程访问的安全性。想使用线程安全的Set,可采用Collections.synchronizedSortedSet()

HashSet
  • 实现原理,基于哈希表(HashMap)实现
  • 不允许重复,最多可以有一个null元素
  • 不保证顺序恒久不变
  • 添加元素时把元素作为HashMap的key保存,HashMap的value使用一个固定的Object对象
  • 排除重复是通过hashCode和equals方法来检查对象是否相等
  • 判断两个对象是否相同,先判断==内存地址是否相同, 内存地址不同判断两个对象的hashCode是否相同(两个对象的hashCode相等不一定是同一个对象,但如果不同,一定不是同一个对象),若不同,则两个对象不是同一个对象;若相同,还要进行equals判断。equals方法返回true则为同一个对象,返回false则不是同一个对象。
  • 自定义类存入HashSet时,建议重写类的hashCode和equals方法

哈希表的存储结构:

  • 1.7 数组+链表 1.8数组+链表+红黑树,数组里的每个元素以链表的形式存储

如何把对象存储到哈希表中?

  • 先计算对象的hashCode值再对数组的长度求余数,来决定对象要存储在数组中的哪个位置。
示例代码
public class TestSet {
    @Test
    public void testHashSet(){
        //Demo1类没有重写equals方法和HashCode方法, 内存地址相同的对象才能认定为同一个对象(equals比较的是内存地址,hashCode比较的也是内存地址(10位整数))
        Set<Demo1>  demo1Set = new HashSet<>();
        demo1Set.add(new Demo1(1));
        demo1Set.add(new Demo1(1));
        demo1Set.add(new Demo1(1));
        System.out.println("demo1Set=>"+demo1Set.size());// 3

        //Demo2类重写了equals方法和HashCode方法,内存地址不同但是num值相同的Demo2可以判断为同一对象
        Set<Demo2>  demo2Set = new HashSet<>();
        demo2Set.add(new Demo2(1));
        demo2Set.add(new Demo2(1));
        demo2Set.add(new Demo2(1));
        System.out.println("demo2Set=>"+demo2Set.size());// 1
        
        //String类重写equals方法和HashCode方法,值相同的对象HashCode和equals返回相同
        Set<String>  demo3Set = new HashSet<>();
        demo3Set.add("111");
        demo3Set.add("111");
        demo3Set.add("111");
        demo3Set.add("222");
        demo3Set.add("333");
        demo3Set.add(null);
        demo3Set.add(null);
        System.out.println("demo3Set=>"+demo3Set.size());// 4
    }
    
    class Demo1 {
        private Integer num;
        public Demo1(Integer num) {
            this.num = num;
        }
    }

    class Demo2 {
        private Integer num;
        public Demo2(Integer num) {
            this.num = num;
        }
        
        /**
         * 重写了equals方法,只要值相同就可以认为是同一个对象
         *
         * @param obj
         * @return
         */
        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (obj instanceof Demo2) {
                Demo2 demo = (Demo2) obj;
                if (num.equals(demo.num)) {
                    return true;
                }
            }
            return false;
        }
        /**
         * 重写hashCode方法,返回当前num值
         *
         * @return
         */
        @Override
        public int hashCode() {
            return num;
        }
    }
}

执行结果:
在这里插入图片描述

  • Integer和String类重写equals()和hashCode(),值相同的对象, hashCode和equals返回值都相同,因此使用HashSet可以自动去重
  • 使用HashSet保存自定义类Demo1时,由于Demo1没有重写equasl和hashCode方法,因此默认使用的是父类Object的方法, equals判断内存地址, hashCode返回整型内存地址, 因此会值相同的对象都由于内存地址不同,添加进HashSet

Java1.8中Set不允许元素重复的原理

  • 重写了hashCode方法和equals方法,set加入一个对象,首先查看该对象的哈希值,然后看哈希表对应的哈希值是否有对象存储了,如果还没有那么就直接插入哈希表中;如果已经存储着相同哈希值的对象,先判断内存地址 或者 equals方法比较是不是相同,如果不相同则插入。如果相同则覆盖旧值。 注意哈希表在解决冲突问题的时候采用的是拉链法,在同一个哈希值存储的的元素个数小于8时使用链表,大于等于8就改成了红黑树(红黑树是一棵自平衡的二叉树)
LinkedHashSet
  • LinkedHashSet底层是由哈希表和链表(链接列表)实现的一个唯一有序的Set。
  • 链表保证元素有序
  • 哈希表保证元素唯一
  • 继承于 HashSet,并且其内部是通过LinkedHashMap 来实现的。
示例代码
public class TestSet {
    @Test
    public void testLinkedHashSet(){
        //Demo1类没有重写equals方法和HashCode方法, 内存地址相同的对象才能认定为同一个对象(equals比较的是内存地址,hashCode比较的也是内存地址(10位整数))
        Set<Demo1>  demo1Set = new LinkedHashSet<>();
        demo1Set.add(new Demo1(1));
        demo1Set.add(new Demo1(1));
        demo1Set.add(new Demo1(1));
        System.out.println("demo1Set=>"+demo1Set.toString());// 3

        //Demo2类没有重写equals方法和HashCode方法,内存地址不同但是num值相同的Demo2可以判断为同一对象
        Set<Demo2>  demo2Set = new LinkedHashSet<>();
        demo2Set.add(new Demo2(1));
        demo2Set.add(new Demo2(1));
        demo2Set.add(new Demo2(1));
        System.out.println("demo2Set=>"+demo2Set);// 1

        Set<String>  demo3Set = new LinkedHashSet<>();
        demo3Set.add(null);
        demo3Set.add(null);
        demo3Set.add("111");
        demo3Set.add("111");
        demo3Set.add("111");
        demo3Set.add("222");
        demo3Set.add("333");
        System.out.println("demo3Set=>"+demo3Set);// 4
    }


    @Test
    public void testHashSet(){
        //Demo1类没有重写equals方法和HashCode方法, 内存地址相同的对象才能认定为同一个对象(equals比较的是内存地址,hashCode比较的也是内存地址(10位整数))
        Set<Demo1>  demo1Set = new HashSet<>();
        demo1Set.add(new Demo1(1));
        demo1Set.add(new Demo1(1));
        demo1Set.add(new Demo1(1));
        System.out.println("demo1Set=>"+demo1Set.size());// 3

        //Demo2类重写了equals方法和HashCode方法,内存地址不同但是num值相同的Demo2可以判断为同一对象
        Set<Demo2>  demo2Set = new HashSet<>();
        demo2Set.add(new Demo2(1));
        demo2Set.add(new Demo2(1));
        demo2Set.add(new Demo2(1));
        System.out.println("demo2Set=>"+demo2Set.size());// 1

        Set<String>  demo3Set = new HashSet<>();
        demo3Set.add("333");
        demo3Set.add(null);
        demo3Set.add("222");
        demo3Set.add(null);
        demo3Set.add("111");
        demo3Set.add("111");
        demo3Set.add("111");
        System.out.println("demo3Set=>"+demo3Set.size());// 3
    }

    class Demo1 {
        private Integer num;
        public Demo1(Integer num) {
            this.num = num;
        }
        @Override
        public String toString() {
            return num.toString();
        }
    }

    class Demo2 {
        private Integer num;
        public Demo2(Integer num) {
            this.num = num;
        }

        /**
         * 重写了equals方法,只要值相同就可以认为是同一个对象
         * @param obj
         * @return
         */
        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (obj instanceof Demo2) {
                Demo2 demo = (Demo2) obj;
                if (num.equals(demo.num)) {
                    return true;
                }
            }
            return false;
        }
        /**
         * 重写hashCode方法,返回当前num值
         * @return
         */
        @Override
        public int hashCode() {
            return num;
        }
        @Override
        public String toString() {
            return num.toString();
        }
    }
}

执行结果:
在这里插入图片描述

  • LinkedHashSet是带有有去重, 带有插入顺序的Set, 插入自定义对象是必须重写equals和hashCode方法, 否则因为默认的equals和hashCode判断内存地址相同而插入相同对象
TreeSet
  • TreeSet基于 TreeMapNavigableSet(继承自SortedSet接口) 实现。默认使用元素的自然顺序对元素进行排序(自定义对象需要实现Comparable接口),或者创建 set 时在构造方法中指定Comparator进行自定义排序
  • TreeSet保证元素的排序唯一性
  • 底层数据结构是红黑树(自平衡二叉树)

如何保证元素排序的呢?
添加元素实现自然排序Comparable接口/构造方法中创建自定义比较器排序Comparator
如何保证元素唯一性的呢
在比较器中根据比较的返回值是否是0来决定,两个元素相比如果为0则表示对象相等

示例代码
public class TestSet {
 	@Test
    public void testTreeSet() {

        //Demo类实现了Comparable接口,并重写了compareTo方法,去重的方式与equals和hashCode无关,通过比较器返回0来判断元素相等
        Set<Demo> demo1Set = new TreeSet<>();
        demo1Set.add(new Demo(1));
        demo1Set.add(new Demo(2));
        demo1Set.add(new Demo(3));
        System.out.println("demo1Set=>" + demo1Set);// 1

        //因为String已经实现了Comparable接口并重写了comparTo方法,默认只能按自然顺序升序  可以在类外部使用Comparator自定义排序规则
        Set<String> demo2Set = new TreeSet<>(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                if (o1.compareTo(o2) > 0) {
                    return -1;
                } else if (o1.compareTo(o2) < 0) {
                    return 1;
                } else {
                    return 0;
                }
            }
        });
        demo2Set.add("CCC");
        demo2Set.add("EEE");
        demo2Set.add("FFF");
        demo2Set.add("DDD");
        demo2Set.add("AAA");
        System.out.println("demo3Set=>" + demo2Set);// 3
    }


    class Demo implements Comparable<Demo> {
        private Integer num;

        public Demo(Integer num) {
            this.num = num;
        }

        /**
         * 重写了equals方法,只要值相同就可以认为是同一个对象
         *
         * @param obj
         * @return
         */
        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (obj instanceof Demo) {
                Demo demo = (Demo) obj;
                if (num.equals(demo.num)) {
                    return true;
                }
            }
            return false;
        }

        /**
         * 重写hashCode方法,返回当前num值
         *
         * @return
         */
        @Override
        public int hashCode() {
            return num;
        }

        @Override
        public String toString() {
            return num.toString();
        }


        @Override
        public int compareTo(Demo obj) {
            if ((this.num - obj.num) > 0) {
                return -1;
            } else if ((this.num - obj.num) < 0) {
                return 1;
            } else {
                return 0;
            }
        }
    }
}    

在这里插入图片描述

  • 可以看出有两种排序方式,一种是在类内部实现Comparable接口,重写CompareTo方法, 一种是在构造方法或者Collections.sort(set,comparator)自定义构造器Comparator实现排序
  • 不需要重写equals和hashCode方法,因此是通过在比较器中返回 "0",来判断来两个元素是否相同,从而实现去重的.

TreeSet接口间接的实现了SortedSet接口,具有以下方法

方法 说明
public Comparator<? super E> 返回排序有关联的比较器
public E first() 返回集合中的第一个元素
public SortedSet<E> headSet(E toElement) 返回从开始到指定元素的集合
public E last() 返回最后一个元素
public SortedSet<E> subSet(E fromElement,E toElement) 返回指定对象间的元素
public SortedSet<E> tailSet( 从指定元素到最后) 返回排序有关联的比较器
EnumSet
  • EnumSet 是一个专为枚举设计的集合类,EnumSet中的所有元素都必须是指定枚举类型的枚举值,该枚举类型创建EnumSet时显式或隐式地指定
  • EnumSet以在Enum类内枚举值的定义顺序来决定
  • EnumSet在内部以位向量的形式存储,这种存储形式非常紧凑、高效,因此EnumSet对象占用内存很小,而且运行效率很好。尤其是进行批量操作(如调用containsAll()和retainAll()方法)时,如果其参数也是EnumSet集合,则该批量操作的执行速度也非常快。
  • EnumSet集合不允许添加null元素,否则将抛出NullPointerException异常。
  • EnumSet类没有暴露任何构造器来创建该类的实例,程序应该通过它提供的类方法来创建EnumSet对象。
  • 如果只是想判断EnumSet是否包含null元素或试图删除null元素都不会抛出异常,只是删除操作将返回false,因为没有任何null元素被删除。

常用方法

方法 说明
EnumSet allOf(Class elementType): 创建一个包含指定枚举类里所有枚举值的EnumSet集合。
EnumSet complementOf(EnumSet e): 创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合,新EnumSet集合包含原EnumSet集合所不包含的、此类枚举类剩下的枚举值(即新EnumSet集合和原EnumSet集合的集合元素加起来是该枚举类的所有枚举值)。
EnumSet copyOf(Collection c): 使用一个普通集合来创建EnumSet集合。
EnumSet copyOf(EnumSet e): 创建一个指定EnumSet具有相同元素类型、相同集合元素的EnumSet集合。
EnumSet noneOf(Class elementType): 创建一个元素类型为指定枚举类型的空EnumSet。
EnumSet of(E first,E…rest): 创建一个包含一个或多个枚举值的EnumSet集合,传入的多个枚举值必须属于同一个枚举类。
EnumSet range(E from,E to): 创建一个包含从from枚举值到to枚举值范围内所有枚举值的EnumSet集合。
示例代码
public class TestEnumSet {
    //创建一个枚举
    enum WeatherEnum{
        SPRING("春天"),
        SUMMER("夏天"),
        FALL("秋天"),
        WINTER("冬天");

        private  String value;
        WeatherEnum(String value) {
            this.value = value;
        }
    }

    @Test
    public void testEnumSet(){
        //1.创建一个包含Session(枚举类)里所有枚举值的EnumSet集合
        EnumSet e1 = EnumSet.allOf(WeatherEnum.class);
        System.out.println("EnumSet.allOf=>"+e1);//[SPRING, SUMMER, FAIL, WINTER]

        Iterator<WeatherEnum> iterator = e1.iterator();
        while (iterator.hasNext()){
            WeatherEnum weatherEnum = iterator.next();
            System.out.println("-----iterator=>"+weatherEnum.value);
        }

        //2.创建一个空EnumSet
        EnumSet e2 = EnumSet.noneOf(WeatherEnum.class);
        System.out.println("e2.noneOf=>"+e2);//[]

        //3. add()空EnumSet集合中添加枚举元素
        e2.add(WeatherEnum.SPRING);
        e2.add(WeatherEnum.SUMMER);
        System.out.println("e2.add=>"+e2);//[SPRING, SUMMER]

        //4. 以指定枚举值创建EnumSet集合
        EnumSet e3 = EnumSet.of(WeatherEnum.SPRING,WeatherEnum.FALL);
        System.out.println("EnumSet.of=>"+e3);//[SPRING, FAIL]

        //5.创建一个包含从from枚举值到to枚举值范围内所有枚举值的EnumSet集合。
        EnumSet e4 = EnumSet.range(WeatherEnum.SPRING,WeatherEnum.FALL);
        System.out.println("EnumSet.range=>"+e4);//[SPRING, SUMMER, FAIL]

        //6.创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合,
        //  新EnumSet集合包含原EnumSet集合所不包含的枚举值
        EnumSet e5 = EnumSet.complementOf(e4);
        System.out.println("EnumSet.complementOf(e4)=>"+e5);//[WINTER]

        //7.创建一个集合
        Set set = new HashSet();
        set.clear();
        set.add(WeatherEnum.SPRING);
        set.add(WeatherEnum.WINTER);
        //复制Collection集合中的所有元素来创建EnumSet集合
        EnumSet es = EnumSet.copyOf(set);
        System.out.println("EnumSet.copyOf=>"+es);
    }
}

执行结果:
在这里插入图片描述

发布了62 篇原创文章 · 获赞 109 · 访问量 5294

猜你喜欢

转载自blog.csdn.net/qq877728715/article/details/103064567
今日推荐