Java—Set集合详解(HashSet/LinkedHashSet/TreeSet/EnumSet)

Set集合介绍

Set集合的概念

  Set集合类似于一个容器,程序把很多对象保存到Set集合中,Set集合对添加顺序不记录,当有重复的对象保存到Set集合时,不会新增后加的重复对象。

Set集合的特点

  1. Set集合无重复元素,add()方法添加相同元素时,返回false;
  2. Set集合add()方法不记录顺序;

HashSet类

HashSet介绍

  HashSet是按照哈希算法进行存储元素的,具有良好的查询和存取性能。

HashSet特点

  1. 集合元素值可以为null;
  2. 不保证元素的排列顺序,有可能排列顺序与添加顺序不同;
  3. 非同步集合,多线程访问HashSet时,是不安全的,需要通过同步代码保证同步。
  4. 元素不可重复相同,通过equals()hashCode()方法一起判断是否相同。

HashSet添加元素过程

add()方法:

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

  通过add()方法向HashSet存入元素时,HashSet会调用该对象的hashCode()方法获取hashCode值,然后根据hashCode值决定这个元素在HashSet的存储位置。如果有两个元素通过equals()方法返回true,但hashCode()方法返回值不同,则HashSet会存储到集合的不同位置,依旧可以成功添加该元素。

示例1:HashSet中元素对象的相同性依据

1)创建ClassA类,重写equals()方法

public class ClassA {
    @Override
    public boolean equals(Object obj) {
        return true;
    }
}

2)创建ClassB类,重写hashCode()方法

public class ClassB {
    @Override
    public int hashCode() {
        return 0;
    }
}

3)创建ClassC类,重写equals()hashCode()方法

public class ClassC {
    @Override
    public int hashCode() {
        return 1;
    }

    @Override
    public boolean equals(Object obj) {
        return true;
    }
}

4)测试主类:

public class DemoApplication {
    public static void main(String[] args) {
        // 创建集合
        HashSet hashSet = new HashSet();

        // 添加元素
        hashSet.add(new ClassA());
        hashSet.add(new ClassA());
        hashSet.add(new ClassB());
        hashSet.add(new ClassB());
        hashSet.add(new ClassC());
        hashSet.add(new ClassC());

        System.out.println("hashSet: ");
        hashSet.forEach(obj -> System.out.println(obj));
        }
}

5)运行结果:

hashSet: 
com.example.andya.demo.bean.ClassB@0
com.example.andya.demo.bean.ClassB@0
com.example.andya.demo.bean.ClassC@1
com.example.andya.demo.bean.ClassA@1edf1c96
com.example.andya.demo.bean.ClassA@368102c8

  从上述运行结果可以看出:
1)HashSet集合添加顺序和集合内部元素顺序不一定相同;
2)HashSet添加元素时:

  • ClassA类重写了equals()方法,但是两个对象的hashCode()返回了不同的hashCode值,所以HashSet会将这两个对象保存在哈希表中的不同位置。
  • ClassB类重写了hashCode()方法,但是两个对象的equals()方法返回的是不同的对象地址,所以HashSet会将这两个对象保存到同一个位置,并通过链表链接。这种方式不建议有,因为集合中出现链式结构来存储相同hash值的元素时,查询速度会变慢,性能下降。
  • 只有ClassC是当作一个对象来添加到集合中,只有当equals()hashCode()方法都重写的时候,才可以作为判断对象是否相同的依据。

3)总结:若需要将某个类的对象保存到HashSet集合时,我们需要重写这个类的equals()hashCode()方法,只有保证了两个对象通过equals()方法返回true,hashCode()方法返回值相同时,才是相同对象。

重写hashCode()方法的准则

  1. 可重复性:程序运行时,同一个对象多次调用hashCode()方法返回的是相同值;
  2. 当两个对象通过equals()方法比较后返回的是true,则两个对象的hashCode()方法应返回相同的hash值;
  3. 对象中用作equals()方法比较标准的实例变量,都应该用于计算hashCode值;

HashSet集合为何查询速度快?

  我们回答这个问题前,先看一下HashSet添加元素的流程,使用add()方法添加元素时,HashSet会根据这个元素的hashCode值计算存储位置进行存储。当我们查询元素时,也是通过该元素的hashCode值快速定位到该元素在集合中的位置,其实HashSet底层是通过HashMap实现的。可以观察到源码:

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {

    private transient HashMap<E,Object> map;

    //构造方法
    public HashSet() {
        map = new HashMap<>();
    }

    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }
    
    //add方法
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    
}

LinkedHashSet

LinkedHashSet介绍

  LinkedHashSet是HashSet子类,同样是根据元素的hashCode值判断元素的存储位置,且同时使用链表维护元素的顺序,给人的直观效果就是集合的顺序就是元素插入的顺序保存的。
  下面我们通过一组示例来看一下LinkedHashSet的保存元素次序效果。

LinkedHashSet示例

1)主类

public class DemoApplication {
    public static void main(String[] args) {
        LinkedHashSet linkedHashSet = new LinkedHashSet();
        linkedHashSet.add("1");
        linkedHashSet.add("2");
        linkedHashSet.add(3);
        System.out.println("旧的集合:" + linkedHashSet);

        linkedHashSet.remove("1");
        linkedHashSet.add(1);
        System.out.println("新的集合:" + linkedHashSet);
        }
}

2)运行结果

旧的集合:[1, 2, 3]
新的集合:[2, 3, 1]

  从上述运行结果中,我们可以看到LinkedHashSet的元素保存顺序即为添加元素的顺序。

TreeSet

TreeSet介绍

  TreeSet是SortedSet接口实现类,TreeSet是一种排序集合,该对象中只能添加一种类型元素,否则,会抛出java.lang.ClassCastException异常;
  与HashSet集合采用hash算法来决定元素的存储位置有些不一样,TreeSet是采用红黑树这种数据结构存储集合元素。

TreeSet方法

除了Collection集合的常用方法外,TreeSet集合还有如下常用方法:

  • Comparator<? super E> comparator():如果TreeSet采用定制排序,该方法返回定制排序所使用的Comparator,如果TreeSet采用自然排序,则返回null;
  • E first():返回集合中的第一个元素;
  • E last():返回集合中的最后一个元素;
  • E lower(E e):返回集合找找那个微语指定元素之前的元素(小于指定元素的最大元素)
  • E higher(E e):返回集合中位于指定元素之后元素(大于指定元素的最小元素)
  • SortedSet<E> subSet(E fromElement, E toElement):返回Set的子集合,从fromElement(包含)到toElement(不包含)
  • SortedSet<E> headSet(E toElement):返回此Set的子集,该子集是由小于toElement的元素组成;
  • SortedSet<E> tailSet(E toElement):返回此Set的子集,该子集是由大于toElement的元素组成;

示例

1)运行主类:

public class DemoApplication {
    public static void main(String[] args) {
        TreeSet treeSet = new TreeSet();
        treeSet.add(2);
        treeSet.add(1);
        treeSet.add(6);
        treeSet.add(3);
        treeSet.add(8);
        System.out.println(treeSet);

        //查看排序性
        treeSet.remove(3);
        treeSet.add(5);
        System.out.println(treeSet);

        System.out.println("first()方法: " + treeSet.first());
        System.out.println("last()方法: " + treeSet.last());
        System.out.println("lower()方法: " + treeSet.lower(5));
        System.out.println("higher()方法: " + treeSet.higher(5));
        System.out.println("subSet()方法: " + treeSet.subSet(2,6));
        System.out.println("headSet()方法: " + treeSet.headSet(5));
        System.out.println("tailSet()方法: " + treeSet.tailSet(5));
        }
}

2)运行结果:

[1, 2, 3, 6, 8]
[1, 2, 5, 6, 8]
first()方法: 1
last()方法: 8
lower()方法: 2
higher()方法: 6
subSet()方法: [2, 5]
headSet()方法: [1, 2]
tailSet()方法: [5, 6, 8]

  从上述运行结果可以看出,TreeSet不是根据添加元素的顺序进行排序,而是通过元素实际大小进行排序。

TreeSet的compareTo()方法详解

  TreeSet支持两种排序方式:自然排序自定义排序(定制排序)

自然排序

介绍:
  TreeSet是通过Comparable接口的compareTo(Object obj)方法比较元素之间的大小关系,然后将元素按照升序排列,即为自然排序。
实现原理:
  实现Comparable接口,并实现compareTo(Object obj)方法,返回一个整数值。当一个对象调用该方法与另一个对象进行比较时,如obj1.compareTo(obj2),该方法若返回0,则表明obj1和obj2这两个对象相等;若返回一个正整数,则表明obj1大于obj2;若返回一个负整数,则表明obj1小于obj2。然后根据红黑树结构寻找存储位置,当元素相同,若此时使用集合add()方法时,无法将新对象添加到集合内。
  TreeSet通过元素对应的类重写的compareTo(Object obj)方法来比较对象大小,就要求重写元素对象对应类的equals()方法时,需要保证该方法与compareTo(Object obj)方法保持一致的结果,当两个对象通过equals()方法返回true时,两个对象通过compareTo(Object obj)方法应该返回0。

示例
1)对象类

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

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

    //重写equals()方法与compareTo()方法保持一致的效果
    @Override
    public boolean equals(Object obj) {
        if(this == obj) {
            return true;
        }
        if(obj != null && obj.getClass() == OnePerson.class) {
            OnePerson onePerson = (OnePerson) obj;
            return onePerson.age == this.age;
        }
        return  false;
    }

    //重写compareTo()方法,以age作为排序比较标准
    @Override
    public int compareTo(OnePerson o) {
        return this.age > o.age
                ? 1 : this.age < o.age
                ? -1 : 0;
    }

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

2)运行类

public class DemoApplication {
    public static void main(String[] args) {
        TreeSet treeSet = new TreeSet();

        OnePerson onePerson1 = new OnePerson(5, "xiaoming");
        OnePerson onePerson2 = new OnePerson(3, "yaoyao");
        OnePerson onePerson3 = new OnePerson(8, "huangming");
        
        treeSet.add(onePerson1);
        treeSet.add(onePerson2);
        treeSet.add(onePerson3);
        
        treeSet.forEach(onePerson -> System.out.println(onePerson));
        }
}

3)运行结果

OnePerson{age=3, name='yaoyao'}
OnePerson{age=5, name='xiaoming'}
OnePerson{age=8, name='huangming'}

  从上述运行结果看出,对象类OnePerson添加到TreeSet时是按照age大小进行排序添加进集合。

自定义排序

介绍:
  自然排序是根据元素大小进行升序排列,如果需要实现降序等排列需要自定义排序,通过Comparator接口,该接口内有int compare(T o1, T o2)方法,使用该方法比较o1和o2的大小:若返回正整数,则表示o1>o2;若返回负整数,则表示o1<o2;若返回0,则表示o1==o2.
实现原理:
  创建TreeSet集合对象时,提供一个Comparator对象与该集合进行关联,在Comparator对象中实现集合元素的排序逻辑。

示例
1)对象类

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

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

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

    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;
    }

2)运行主类:
Lambda方式

public class DemoApplication {
    public static void main(String[] args) {
        TreeSet treeSet = new TreeSet((o1, o2) ->
        {
            OnePerson2 onePerson1 = (OnePerson2)o1;
            OnePerson2 onePerson2 = (OnePerson2)o2;
            //根据OnePerson对象的age属性来判断大小,age越大,OnePerson对象越小,降序
            return  onePerson1.getAge() > onePerson2.getAge() ? -1
                : onePerson1.getAge() < onePerson2.getAge() ? 1 : 0;
        });

        treeSet.add(new OnePerson2(5, "xiaohong"));
        treeSet.add(new OnePerson2(2, "huangming"));
        treeSet.add(new OnePerson2(9, "yaoling"));

        treeSet.forEach(onePerson -> System.out.println(onePerson));
        }
}

compare()方式

public class DemoApplication {
    public static void main(String[] args) {
        TreeSet<OnePerson2> treeSet = new TreeSet(new Comparator<OnePerson2>() {

            @Override
            public int compare(OnePerson2 o1, OnePerson2 o2) {
                return o1.getAge() > o2.getAge() ? -1
                        : o1.getAge() < o2.getAge() ? 1 : 0;
            }
        });

        treeSet.add(new OnePerson2(5, "xiaohong"));
        treeSet.add(new OnePerson2(2, "huangming"));
        treeSet.add(new OnePerson2(9, "yaoling"));

        treeSet.forEach(onePerson -> System.out.println(onePerson));
    }
}

3)运行结果

OnePerson2{age=9, name='yaoling'}
OnePerson2{age=5, name='xiaohong'}
OnePerson2{age=2, name='huangming'}

  从上述运行结果看出,通过Comparator接口的lambda表达式实现了自定义降序。

EnumSet

EnumSet介绍

  EnumSet是一个枚举类的集合类,集合内的所有元素都必须是指定枚举类型的枚举值,枚举类型在创建EnumSet时显式或者隐式地指定;EnumSet也是有顺序的,该顺序是由枚举值在Enum类中的定义顺序决定。
  EnumSet不允许加入null元素,若尝试添加null元素,会抛出NullPointerException异常。

EnumSet常用方法

  • EnumSet<E> allOf(Class<E> elementType):创建一个包含指定枚举类全部枚举值的EnumSet集合。
  • EnumSet<E> noneOf(Class<E> elementType):创建一个元素类型为指定枚举类型的空EnumSet集合。
  • EnumSet<E> of(E first, E... rest):创建同一类型枚举值的一个或多个枚举值的EnumSet集合。
  • EnumSet<E> range(E from, E to):创建一个包含从from到to枚举值范围内所有枚举值的EnumSet集合。
  • EnumSet<E> complementOf(EnumSet<E> s):创建一个元素类型和指定的EnumSet集合相同的EnumSet集合,新EnumSet集合包含原EnumSet集合不包含的,新的EnumSet集合和原集合所有元素加起来是枚举类的所有枚举值。
  • EnumSet<E> copyOf(EnumSet<E> s):创建一个与指定EnumSet集合具有相同元素类型、相同元素值的EnumSet集合。
  • EnumSet<E> copyOf(Collection<E> c):使用一个普通集合创建EnumSet集合。但是该普通集合中的元素必须是枚举值,否则会抛出ClassCastException异常。

示例

1)枚举类

public enum ColourEnum {
    RED,
    ORANGE,
    YELLOW,
    GREEN,
    BLUE,
    INDIGO,
    PURPLE
}

2)运行主类

public class DemoApplication {

    public static void main(String[] args) {

        //allOf()获取枚举类的全部枚举值
        EnumSet<ColourEnum> colourEnumEnumSet = EnumSet.allOf(ColourEnum.class);
        System.out.println("allOf()方法:" + colourEnumEnumSet);

        //创建空集合,指定集合元素是ColourEnum类的枚举值
        EnumSet<ColourEnum> colourEnumEnumSet1 = EnumSet.noneOf(ColourEnum.class);
        colourEnumEnumSet1.add(ColourEnum.GREEN);
        colourEnumEnumSet1.add(ColourEnum.RED);
        System.out.println("noneOf()方法:" + colourEnumEnumSet1);

        //指定枚举值创建枚举集合
        EnumSet<ColourEnum> colourEnumEnumSet2 = EnumSet.of(ColourEnum.BLUE, ColourEnum.ORANGE);
        System.out.println("of()方法:" + colourEnumEnumSet2);

        //指定范围创建枚举集合
        EnumSet<ColourEnum> colourEnumEnumSet3 = EnumSet.range(ColourEnum.ORANGE, ColourEnum.INDIGO);
        System.out.println("range()方法:" + colourEnumEnumSet3);

        //补充枚举值,合集是整个ColourEnum
        EnumSet<ColourEnum> colourEnumEnumSet4 = EnumSet.complementOf(colourEnumEnumSet3);
        System.out.println("complementOf()方法:" + colourEnumEnumSet4);

        //复制枚举集合
        EnumSet<ColourEnum> colourEnumEnumSet5 = EnumSet.copyOf(colourEnumEnumSet4);
        System.out.println("copyOf()方法:" + colourEnumEnumSet5);
    
    }
}

3)运行结果

allOf()方法:[RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, PURPLE]
noneOf()方法:[RED, GREEN]
of()方法:[ORANGE, BLUE]
range()方法:[ORANGE, YELLOW, GREEN, BLUE, INDIGO]
complementOf()方法:[RED, PURPLE]
copyOf()方法:[RED, PURPLE]

各种Set集合的比较

性能比较

1)HashSet性能比TreeSet好(添加、查询),只有当需要保持排序的Set时,才使用TreeSet,否则使用HashSet。
2)HashSet比LinkedHashSet性能好(添加、删除)。
3)LinkedHashSet比HashSet性能好(遍历查询)。
4)EnumSet是所有Set性能最好的,但缺陷是只能保存同一个枚举类的枚举值作为集合元素。
5)HashSet、TreeSet和EnumSet都是线程不安全的,需要通过Collections工具类的synchronizedSet(Set<T> s)synchronizedSortedSet(SortedSet<T> s)方法来包装Set集合。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容

  • 上一篇文章介绍了Set集合的通用知识。Set集合中包含了三个比较重要的实现类:HashSet、TreeSet和En...
    Ruheng阅读 15,635评论 3 57
  • Java集合类可用于存储数量不等的对象,并可以实现常用的数据结构如栈,队列等,Java集合还可以用于保存具有映射关...
    小徐andorid阅读 1,936评论 0 13
  • 一:基本概念 Java的所有集合都放在java.util包下,集合跟数组不同之处在于前者只能保存对象(对象的引...
    Febers阅读 397评论 0 0
  • 10.1 Set集合 Set接口继承Collection接口,没有提供额外的方法。Set集合不允许包含相同...
    王毅巽阅读 346评论 0 0
  • 四、集合框架 1:String类:字符串(重点) (1)多个字符组成的一个序列,叫字符串。生活中很多数据的描述都采...
    佘大将军阅读 747评论 0 2