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
    extends AbstractSet
    implements Set, Cloneable, java.io.Serializable {

    private transient HashMap map;

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

    public HashSet(Collection 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 comparator():如果TreeSet采用定制排序,该方法返回定制排序所使用的Comparator,如果TreeSet采用自然排序,则返回null;
  • E first():返回集合中的第一个元素;
  • E last():返回集合中的最后一个元素;
  • E lower(E e):返回集合找找那个微语指定元素之前的元素(小于指定元素的最大元素)
  • E higher(E e):返回集合中位于指定元素之后元素(大于指定元素的最小元素)
  • SortedSet subSet(E fromElement, E toElement):返回Set的子集合,从fromElement(包含)到toElement(不包含)
  • SortedSet headSet(E toElement):返回此Set的子集,该子集是由小于toElement的元素组成;
  • SortedSet 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{
    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 实现原理:
  创建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 treeSet = new TreeSet(new Comparator() {

            @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 allOf(Class elementType):创建一个包含指定枚举类全部枚举值的EnumSet集合。
  • EnumSet noneOf(Class elementType):创建一个元素类型为指定枚举类型的空EnumSet集合。
  • EnumSet of(E first, E... rest):创建同一类型枚举值的一个或多个枚举值的EnumSet集合。
  • EnumSet range(E from, E to):创建一个包含从from到to枚举值范围内所有枚举值的EnumSet集合。
  • EnumSet complementOf(EnumSet s):创建一个元素类型和指定的EnumSet集合相同的EnumSet集合,新EnumSet集合包含原EnumSet集合不包含的,新的EnumSet集合和原集合所有元素加起来是枚举类的所有枚举值。
  • EnumSet copyOf(EnumSet s):创建一个与指定EnumSet集合具有相同元素类型、相同元素值的EnumSet集合。
  • EnumSet copyOf(Collection 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 colourEnumEnumSet = EnumSet.allOf(ColourEnum.class);
        System.out.println("allOf()方法:" + colourEnumEnumSet);

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

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

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

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

        //复制枚举集合
        EnumSet 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 s)synchronizedSortedSet(SortedSet s)方法来包装Set集合。

你可能感兴趣的:(Java—Set集合详解(HashSet/LinkedHashSet/TreeSet/EnumSet))