zoukankan      html  css  js  c++  java
  • java集合(4)-Set集合

      Set集合,类似于一个罐子,程序可以把多个对象"丢进"Set集合,而Set集合通常不能记住每个元素的添加顺序.Set集合与Collection基本相同,没有提供任何额外的方法.实际上Set就是Collection,只是行为有所不同(Set不允许有重复元素)

      Set集合不允许包含相同的元素,如果试图把两个相同的元素添加入同一个Set集合中,则添加操作失败,add()返回false,且新元素不会被加入.

      上面介绍Set的通用知识,因此完全适合后面介绍的HashSet,TreeSet和EnumSet三个实现类,只是这三个实现类各有特色.

    一  HashSet

      HashSet是Set接口的典型实现,大多数时候使用的Set集合时就是使用这个实现类.HashSet按Hash算法来存储集合中的元素,因此具有良好的存取和查找性能。

      HashSet具有以下特点:

        1.不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。

        2.HashSet不是同步的,如果多个线程同时访问一个HashSet。假设有两个或者以上的线程同时修改了HashSet集合时,则必须通过代码来保证其同步。

        3.集合元素可以是null.

      当使用HashSet集合来存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode()值,然后根据该hashCode值决定该对象在HashSet中的存储位置。如果有两个元素通过equals()方法比较返回true,但是它们的hashCode()方法返回值不相等,HashSet将会把它们存入不同的位置,依然可以添加成功。

      也就是说,HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相等。

    示例代码:类A,B,C,分别重写了equals()方法,hashCode()方法这两个中的一个或者两个。

    package com.j1803.collectionOfIterator;
    
    import java.util.HashSet;
    import java.util.Set;
    
    //类A重写了equals()方法,总是返回true,但没有重写hashCode()方法.
    class A{
        @Override
        public boolean equals(Object obj){
            return true;
        }
    }
    
    //类B重写了hashCode()方法,总是返回2,但没有重写equals()方法
    class B{
        @Override
        public int hashCode(){
            return 2;
        }
    }
    //类C重写了equals()方法,总是返回true,重写了hashCode()方法,总是返回2
    class C{
        @Override
        public boolean equals(Object obj){
            return true;
        }
        @Override
        public int hashCode(){
            return 2;
        }
    }
    public class HashSetTest {
        public static void main(String[] args) {
            Set book=new HashSet();
            HashSet books=new HashSet();
            A a=new A();
            B b=new B();
            C c=new C();
            books.add(a);
            books.add(a);
            books.add(b);
            books.add(b);
            books.add(c);
            books.add(c);
            System.out.println(books);
    
        }
    }
    [com.j1803.collectionOfIterator.B@2, com.j1803.collectionOfIterator.A@4554617c]
    
    Process finished with exit code 0
    package com.j1803.collectionOfIterator;
    
    import java.util.HashSet;
    import java.util.Set;
    
    //类A重写了equals()方法,总是返回true,但没有重写hashCode()方法.
    class A{
        @Override
        public boolean equals(Object obj){
            return true;
        }
    }
    
    //类B重写了hashCode()方法,总是返回2,但没有重写equals()方法
    class B{
        @Override
        public int hashCode(){
            return 2;
        }
    }
    //类C重写了equals()方法,总是返回true,重写了hashCode()方法,总是返回2
    class C{
       /* @Override
        public boolean equals(Object obj){
            return true;
        }*/
       @Override
        public int hashCode(){
            return 2;
        }
    }
    public class HashSetTest {
        public static void main(String[] args) {
            Set book=new HashSet();
            HashSet books=new HashSet();
            A a=new A();
            B b=new B();
            C c=new C();
            books.add(a);
            books.add(a);
            books.add(b);
            books.add(b);
            books.add(c);
            books.add(c);
            book.add(new A());
            book.add(new A());
            book.add(new B());
            book.add(new B());
            Boolean flag1=book.add(new C());
            System.out.println(flag1);
            Boolean flag2=book.add(new C());
            System.out.println(flag2);
            System.out.println(book);
            System.out.println(books);
    
        }
    }
    true
    true
    [com.j1803.collectionOfIterator.B@2, com.j1803.collectionOfIterator.B@2, com.j1803.collectionOfIterator.C@2, com.j1803.collectionOfIterator.C@2, com.j1803.collectionOfIterator.A@74a14482, com.j1803.collectionOfIterator.A@1540e19d]
    [com.j1803.collectionOfIterator.B@2, com.j1803.collectionOfIterator.C@2, com.j1803.collectionOfIterator.A@4554617c]

    Process finished with exit code 0


       

    package com.j1803.setTest;
    import java.util.HashSet;
    class A{
    	@Override
    	public boolean equals(Object arg0) {
    		// TODO Auto-generated method stub
    		return true;
    	}
    }
    class B{
    
    	@Override
    	public int hashCode() {
    		// TODO Auto-generated method stub
    		return 2;
    	}
    	
    }
    class C{
    
    	@Override
    	public boolean equals(Object obj) {
    		// TODO Auto-generated method stub
    		return true;
    	}
    
    	@Override
    	public int hashCode() {
    		// TODO Auto-generated method stub
    		//注意hashCode()返回为1不是与B类中一样返回为2
    		return 1;
    	}
    }
    public class HashSetTest {
    	
    	public static void main(String[] args) {
    		HashSet books1=new HashSet();
    		books1.add(new A());
    		books1.add(new A());
    
    		books1.add(new B());
    		books1.add(new B());
    		
    		books1.add(new C());
    		books1.add(new C());
    		System.out.println(books1);
    
    
    
    		
    	}
    [com.j1803.setTest.A@7852e922, com.j1803.setTest.C@1, com.j1803.setTest.B@2, com.j1803.setTest.B@2, com.j1803.setTest.A@4e25154f]
    

         注意点:当把一个对象放入HashSet中时,如果需要重写该对象对应类的equals()方法,则也应该重写其hashCode()方法。规则是:如果两个对象通过equals()方法比较返回true,则这两个对象的hashCode()值也应该相同。

        如果两个对象通过equals()方法比较返回true,但这两个对象的hashCode()方法返回值不同,这将导致HashSet会把这两个对象保存在Hash表中不同的位置,从而使两个对象都可以添加成功这就与Set集合的规则相冲突了,

        如果两个对象的hashCode()方法返回的hashCode()值相同,但它们通过equals()方法比较返回false时将更麻烦:因为两个对象的hashCode()值相同,HashSet将试图将它们保存在同一个位置,但又不行(否则只剩下一个对象)所以在实际上会在这个位置用链式结构来保存多个对象;而HashSet访问集合元素时也是根据元素的hashCode值来快速定位的,如果HashSet中两个以上的元素具有相同的hashCode值,将导致性能下降。

      hashCode()方法对于HashSet的重要性(实际上,对象的hashCode值对于后面的HashMap同样重要),下面给出重写hashCode()方法的基本原则。

      在程序运行过程中,同一个对象多次调用hashCode()方法应该返回相同的值。

      当两个对象通过equals()方法比较返回true时,这两个对象的hashCode()方法也应该返回相等的值。

      对象中用作equals()方法比较标准的实例变量,都应该用于计算hashCode值。

    下面给出重写hashCode()方法的一般几个步骤。

    二  LinkedHashSet类

          HashSet还有一个子类LinkedHashSet,LinkHashSet集合也是根据元素的hashCode的值来决定元素的存储位置的,但它使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。也就是说,当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按照元素的添加顺序来访问集合里的元素。

      LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时将有很好的性能,因为它是以链表来维护内部顺序的。

    package com.j1803.setTest;
    import java.util.LinkedHashSet;
    public class LinkedHashSetTest {
        public static void main(String[] args) {
            LinkedHashSet book=new LinkedHashSet();
            book.add("AAAAA");
            book.add("BBBBB");
            book.add("CCCCC");
            book.add("DDDDD");
            System.out.println(book);
        }
    
    }
    [AAAAA, BBBBB, CCCCC, DDDDD]

      输出LinkedHashSet集合的元素时,元素的顺序总是与添加顺序一致。

      虽然LinkedHashSet使用了链表记录集合元素的添加顺序,但LinkedHashSet依然是HashSet,因此它依然不允许集合元素重复。

    三  TreeSet类

      TresSet是SortedSet接口的实现类,正如SortedSet名字所暗示的,TreeSet可以确保集合元素处于排序状态。与HashSet集合相比,TreeSet还提供了如下几个额外的方法

      Comparator comparator():如果TreeSet采用了定制排序,则该方法返回定制排序所使用的Comparator;如果TreeSet采用了自然排序,则返回null.

      Object first():返回集合的第一个元素。

      Object last():返回集合中的最后一个元素。

      Object lower(Object e):返回集合中位于指定元素之前的元素(也就是小于指定元素的最大元素,参考元素不需要是TreeSet集合里的元素)。

      Object higher(Object e):返回集合中位于指定元素之后的元素(也就是大于指定元素的最小元素,参考元素不需要是TreeSet集合里的元素)。

      Object subSet(Object fromElement,Object toElement):返回此Set的子集合,范围从fromElement(包含)到toElement(不包含)。

      SortedSet headSet(Object toElement):返回此Set集合的子集,由小于toElement的元素组成。

      SortedSet tailSet(Object fromElement):返回此Set集合的子集,由大于或等于fromElement的元素组成。

    package com.j1803.setTest;
    import java.util.TreeSet;
    public class TreeSetTest {
        public static void main(String[] args) {
            TreeSet num=new TreeSet();
            num.add(-5);
            num.add(45);
            num.add(78);
            num.add(-13);
            num.add(40);
            num.add(99);
            num.add(0);
            System.out.println(num);
            //输出集合的第一个元素
            System.out.println("输出集合的第一个元素"+num.first());
            //输出集合的最后一个元素
            System.out.println("输出集合的最后一个元素"+num.last());
            //输出小于50最大的元素
            System.out.println("输出小于50的最大的元素"+num.lower(50));
            //输出大于50的最小元素
            System.out.println("输出大于50的最小元素"+num.higher(50));
            //输出50到80之间的元素
            System.out.println("输出10到80之间的元素"+num.subSet(10, 80));
            //输出小于50的元素treeSet集合
            System.out.println("输出小于50的元素"+num.headSet(50));
            //输出大于50的元素的treeSet集合
            System.out.println("输出大于50的元素的treeSet集合"+num.tailSet(50));
        }
    }
    [-13, -5, 0, 40, 45, 78, 99]
    输出集合的第一个元素-13
    输出集合的最后一个元素99
    输出小于50的最大的元素45
    输出大于50的最小元素78
    输出10到80之间的元素[40, 45, 78]
    输出小于50的元素[-13, -5, 0, 40, 45]
    输出大于50的元素的treeSet集合[78, 99]

    TreeSet并不是根据元素的插入顺序来排序的,而是根据元素实际值的大小来进行排序的。

    与HashSet集合采用hash算法来决定元素的存储位置不同,TreeSet采用红黑树的数据结构来存储集合数据。TreeSet支持两种排序方法:自然排序和定制排序,默认为自然排序。

      1.自然排序

      TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系。然后将集合元素按照升序排列,这种方式就是自然排序。

      Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。当一个对象调用该方法与另一个对象进行比较是。例如obj1.compareTo(obj2),如果改方法返回0,则表明这两个对象相等,如果该方法返回一个正整数,则表明obj1大于obj2;如果该方法返回一个负整数,则表明obj1小于obj2.

      Java的一些常用类已经实现了Comparable接口,并提供了比较大小的标准。下面是实现了Comparable接口的常用类。

      BigDecimal,Character,Boolean,String,Date,Time.

      如果把一个自定义的类的对象添加到treeSet中,则该对象对应的类必须实现Comparable接口,否则程序将会抛出异常ClassCastException.

      大部分类在实现compareTo(Object obj)方法时,都需要将被比较对象obj强制类型转换成相同类型,因为只有相同类的两个实例才会比较大小。

      如果向TreeSet中添加的对象是程序员自定义的类的对象,则可以向TreeSet中添加多种类型的对象,前提是用户自定义的类实现了Comparable接口,且实现compareTo(Object obj)方法没有进行强制类型转换。但是当试图取出TreeSet集合元素时,不同类型的元素依然会发生ClassCastException异常。

      总结:如果希望TreeSet能正常运作,TreeSet只能添加同一种类型的对象。

      当一个对象加入TreeSet集合中时,TreeSet调用该对象的compareTo(Object obj)方法与容器中的其他对象比较,然后根据红黑树结构找到它的存储位置。如果两个对象通过compareTo(Object obj)方法比较相等,新对象将无法添加到TreeSet集合中。

      对于TreeSet集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过compareTo(Object obj)方法比较是否返回0,如果返回0,则认为相等,否则就认为它们不相等。

    package com.j1803.setTest;
    
    import java.util.TreeSet;
    
    /**
     * @author zhou_oyster
     *
     */
    class Person implements Comparable{
        private int age;
        @Override
        public boolean equals(Object obj) {
            return true;
        }
        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            this.age = age;
        }
        @Override
        public int compareTo(Object arg0) {
            return 1;
        }
        public Person(int age) {
            super();
            this.age = age;
        }
    }
    public class TreeSetTest1 {
        public static void main(String[] args) {
        TreeSet book=new TreeSet();
        Person person=new Person(45);
        book.add(person);
        System.out.println(book.add(person));//输出true,添加成功
        System.out.println(book);//显示所有的元素
        //取出book集合中的第一个元素并修改年龄
        ((Person)book.first()).setAge(12);
        //查看第一个元素的年龄和最后一个元素的年龄
        System.out.println(((Person)book.first()).getAge()+"===================="+((Person)book.last()).getAge());    
        }
    
    }
    true
    [com.j1803.setTest.Person@7852e922, com.j1803.setTest.Person@7852e922]
    12====================12

      可以看到虽然修改Comparable的compareTo()方法,误让程序以为person和他本身不相等,从而可以添加成功,集合中保存对象的引用指的是同一个对象,所以修改了第一个age,后面的age也修改了。

      故:当需要把一个对象放入TreeSet中,重写该对象对应类的equals()方法时,应保证该方法与compareTo(Object obj)方法有一致结果,其规则是:如果两个对象通过equals()方法比较返回true,这两个对象通过compareTo()方法应该返回0.

      反之如果compareTo(Object obj)返回0而equals()返回false,则会与Set规则产生冲突。

      如果向TreeSet中添加了可变对象,并且后面的程序修改了该可变对象的实例变量,将导致它与其他对象的大小顺序发生了改变,但TreeSet不会再次调整它们的顺序,甚至可能导致TreeSet中保存的这两个对象通过compareTo(Object obj)方法比较返回0.

    package com.j1803.setTest;
    import java.util.TreeSet;
    public class Book implements Comparable{
        private int price;
        public Book(int price) {
            super();
            this.price = price;
        }
        @Override
        public String toString() {
            return "Book [price=" + price + "]";
        }
        public int getPrice() {
            return price;
        }
        public void setPrice(int price) {
            this.price = price;
        }
            @Override
            public boolean equals(Object obj) {
                if (this == obj)
                    return true;
                if (obj == null)
                    return false;
                if (getClass() != obj.getClass())
                    return false;
                Book other = (Book) obj;
                if (price != other.price)
                    return false;
                return true;
            }
            @Override
            public int compareTo(Object obj) {
                Book book=(Book)obj;
                return this.price>book.getPrice()?1:this.price<book.getPrice()?-1:0;
            }
        public static void main(String[] args) {
            TreeSet set=new TreeSet();
            set.add(new Book(12));
            set.add(new Book(10));
            set.add(new Book(-10));
            set.add(new Book(-5));
            set.add(new Book(5));
            set.add(new Book(8));
            //打印set集合
            System.out.println(set);
            //修改第一个元素
            Book book1=(Book)set.first();
            book1.setPrice(13);
            //修改最后一个元素,使其与第二个元素的price相同
            Book book2=(Book)set.last();
            book2.setPrice(-5);
            //打印,可以看到无序且有重复元素
            System.out.println(set);
            //删除实例变量被改变的元素,删除失败。
            System.out.println(set.remove(new Book(13)));
            //打印
            System.out.println(set);
            //删除实例变量没有被改变的元素,删除成功。
            System.out.println(set.remove(new Book(10)));
            //打印
            System.out.println(set);    
        }
    
    }
    [Book [price=-10], Book [price=-5], Book [price=5], Book [price=8], Book [price=10], Book [price=12]]
    [Book [price=13], Book [price=-5], Book [price=5], Book [price=8], Book [price=10], Book [price=-5]]
    false
    [Book [price=13], Book [price=-5], Book [price=5], Book [price=8], Book [price=10], Book [price=-5]]
    true
    [Book [price=13], Book [price=-5], Book [price=5], Book [price=8], Book [price=-5]]

      可以删除没有被修改实例变量,且不与其他修改实例变量的对象重复的对象。

      当执行了红色代码后TreeSet会对集合中的元素重新索引(不是重新排序),接下来可以删除TreeSet所有元素,推荐不要修改放入HashSet和TreeSet集合中元素的关键实例变量。

        //删除元素
    System.out.println(set.remove(new Book(-5)));
    System.out.println(set);
    [Book [price=13], Book [price=-5], Book [price=5], Book [price=8], Book [price=-5]]
    true
    [Book [price=13], Book [price=5], Book [price=8], Book [price=-5]]

      2.定制排序

      TreeSet的自然排序是根据元素的大小,TreeSet将它们以升序排列。如果需要实现定制排序,例如以降序排列。则可以通过Comparator接口的帮助。,该接口里包含了一个int compare(T o1,T o2)方法。该方法用于比较o1和o2的大小:如果该方法中返回正整数,则表明o1大于o2;如果该方法返回0,则表明o1等于o2;如果该方法返回负整数,则表明o1大于o2.

      如果需要实现定制排序,则需要在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联,由该Comparator对象负责集合元素的排序逻辑。由于Comparator是一个函数式接口,因此可用Lambda表达式来代替Comparator对象。

    四  EnumSet类

      EnumSet是一个专为枚举类设计的集合类,EnumSet中的所有元素都必须是指定枚举类型的枚举值。该枚举类型在创建EnumSet时显示或隐式地指定。EnumSet的集合元素也是有序的,EnumSet以枚举值在Enum类内的定义顺序来决定集合元素的顺序。

      EnumSet在内部以位向量的形式存储,这种存储方式非常紧凑,高效,因此EnumSet对象占用内存很小,而且运行效率很好。尤其是进行批量操作(如调用containsAll()和remainAll()方法)时,如果其参数也是EnumSet集合,则该批量操作的执行速度也非常快。

      EnumSet集合不允许加入null元素,如果试图插入null元素,EnumSet将抛出NullPointException异常。如果只是想判断EnumSet是否包含null元素或者试图删除null元素都不会抛出异常,只是删除操作将返回false,因为没有任何null元素被删除。

      EnumSet类没有暴露任何构造器来创建该类的实例,程序应该通过它提供的类方法来创建EnumSet对象。EnumSet类它提供了如下常用的类方法来创建EnumSet对象。

      EnumSet allOf(Class elementType):创建一个包含指定枚举类里所有枚举值的EnumSet集合.

      EnumSet complementOf(Enumset s):创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合,新EnumSet集合包含原EnumSet集合所不包含的,此枚举类剩下的枚举值(也就是新EnumSet集           合和原来EnumSet集合的集合元素加起来都是该枚举类的所有枚举值)。

      EnumSet copyOf(Collection c):使用一个普通集合来创建EnumSet集合。

      EnumSet copyOf(EnumSet s):创建一个与指定EnumSet具有相同元素类型,相同集合元素的EnumSet集合。

      EnumSet noneOf(Class elementType):创建一个元素类型为指定枚举类型的空EnumSet.

      EnumSet of(E first,E...rest):创建一个包含一个或多个枚举值的EnumSet集合,传入的多个枚举值必须属于同一个枚举类。

      EnumSet range(E from,E to):创建一个包含从from枚举值到to枚举值范围内所有枚举值的EnumSet集合。

    package com.j1803.EnumSetTest1;
    import java.util.EnumSet;
    enum Season{
        SPRING,SUMMER,FALL,WINTER
    }
    public class EnumSetTest {
        public static void main(String[] args) {
            //创建一个EnumSet集合,集合元素就是Season枚举类的全部枚举值。
            EnumSet esl= EnumSet.allOf(Season.class);
            //输出[SPRING,SUMMER,FALL,WINTER]
            System.out.println(esl);
            //创建一个EnumSet空集合,指定集合元素是Season类的枚举值。
            EnumSet es2=EnumSet.noneOf(Season.class);
            //输出[]
            System.out.println(es2);
            //手动添加元素。
            es2.add(Season.FALL);
            es2.add(Season.SPRING);
            //输出[FALL,SPRING]
            System.out.println(es2);
            //以指定枚举值创建EnumSet集合
            EnumSet es3=EnumSet.of(Season.SUMMER,Season.FALL,Season.WINTER);
            //输出[SUMMER,FALL,WINTER]
            System.out.println(es3);
            EnumSet es4=EnumSet.of(Season.SUMMER,Season.WINTER);
            //新创建的EnumSet集合元素和es4集合元素有相同的类型
            //es5集合元素+es4集合元素=Season枚举类的全部枚举值
            EnumSet es5= EnumSet.complementOf(es4);
            System.out.println(es5);
        }
    }
    [SPRING, SUMMER, FALL, WINTER]
    []
    [SPRING, FALL]
    [SUMMER, FALL, WINTER]
    [SPRING, FALL]

    要求复制另一个Collection集合中的所有元素到新创建的

    package com.j1803.EnumSetTest1;
    import java.util.Collection;
    import java.util.EnumSet;
    import java.util.HashSet;
    enum Season{
        SPRING,SUMMER,FALL,WINTER
    }
    public class EnumSetTest {
        public static void main(String[] args) {
            Collection c1=new HashSet();
            c1.add(Season.SUMMER);
            c1.add(Season.SPRING);
            c1.add(Season.FALL);
            //复制Collection集合中的所有元素来创建EnumSet集合
            EnumSet enumSet= EnumSet.copyOf(c1);
            //输出[SUMMER,SPRING,FALL]
            System.out.println(enumSet);
            //运行报ClassCastException错误
            //enumSet.add("PHP");
            //enumSet.add("C++");
            enumSet.add(Season.WINTER);
            System.out.println(enumSet);
        }
    }
    [SPRING, SUMMER, FALL]
    [SPRING, SUMMER, FALL, WINTER]
    

     五  各Set实现类的性能分析

      HashSet与TreeSet:HashSet的性能是比TreeSet要好,因为TreeSet需要额外的红黑树算法来维护集合元素的次序,当需要一个注重保持排序的Set时,才使用TreeSet。

      EnumSet是所有Set实现类中性能最好的,但它只能保持同一个枚举类的枚举值作为集合元素。

      HashSet,TreeSet和EnumSet都是线程不安全的。如果有多个线程同时访问一个Set集合,并且有超过一个线程修改了该Set集合,则必须手动保证该Set集合的同步性。通常可以通过Collection工具类的synchronizedSortedSet方法来"包装"该Set集合。在创建时进行,以防止对Set集合的意外非同步访问。

    SortedSet sortedSet= Collections.synchronizedSortedSet(new TreeSet());

      

  • 相关阅读:
    分布式计算框架——MapReduce
    Hadoop总结
    HBase原理总结
    LeetCode牛客148题
    LeetCode刷题[Python版]
    【Linux】netstat命令详解
    基于docker搭建Prometheus+Grafana监控(一)
    Zabbix笔记一:zabbix安装
    【Linux】vi/vim中如果替换
    【redis】Redis学习:redis测试注意点
  • 原文地址:https://www.cnblogs.com/shadow-shine/p/9710892.html
Copyright © 2011-2022 走看看