美文网首页JavaJava 核心技术
Java—Set集合详解(HashSet/LinkedHashS

Java—Set集合详解(HashSet/LinkedHashS

作者: Hughman | 来源:发表于2020-03-31 11:00 被阅读0次

    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集合。

    相关文章

      网友评论

        本文标题:Java—Set集合详解(HashSet/LinkedHashS

        本文链接:https://www.haomeiwen.com/subject/msxwuhtx.html