Java集合笔记(二):Set(集)

Set的一般用法

Set是最简单的一种集合,集合的对象不按特定方式排序,并且没有重复对象.Set接口主要有两个实现类:HashSet和TreeSet.HashSet按照哈希算法来存取集合中的对象,存取速度比较快.HashSe类还有一个子类LinkedHashSet类,它不仅实现了哈希算法,而且实现了链表数据结构,链表数据结构能提高插入和删除元素的性能.

HashSet类

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

  • 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也可能发生变化;
  • HashSet不是同步的,如果多个线程同时访问同一个HashSet,必须通过代码来保证其同步;
  • 集合元素可以是null值;

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

//类A的equals()方法总是返回true,但没有重写其hashCode()方法
public class A {
    public boolean equals(Object obj){
        return true;
    }
}
//类B的hashCode()方法总是返回1,但没有重写其equals()方法
public class B {
    public int hashCode(){
        return 1;
    }
}
//类C的hashCode()方法总是返回2,且重写其equals()方法总是返回true
public class C {
    public int hashCode(){
        return 2;
    }
    public boolean equals(Object obj){
        return true;
    }
}
public class HashSetTest {
    public static void main(String[] args) {
        HashSet books = new HashSet<>();
        books.add(new A());
        books.add(new A());
        books.add(new B());
        books.add(new B());
        books.add(new C());
        books.add(new C());
        System.out.println(books);
    }
}

输出结果:

[com.company.set.B@1, com.company.set.B@1, com.company.set.C@2, com.company.set.A@7f31245a, com.company.set.A@14ae5a5]
即使两个A对象通过equals()方法比较返回true,但hashSet依然把它们当成两个对象;
即使两个B对象的hashCode相同值(1),但HashSet依然把它们当成两个对象;
如果两个对象的hashCode值相等,但通过equals()方法比较返回false时:因为两个对象的hashCode值相同,HashSet试图把它们保存在同一个位置,但又不行(否则将只剩一个对象).所以实际上会在这个位置用链式结构来保存多个对象;而HashSet访问集合元素也是根据元素的hashCode值来快速定位的,如果HashSet中两个以上的元素具有相同的hashCode值,将会导致性能下降;

总结:

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

注意:

如果需要把某个对象保存到HashSet集合中,重写这个类的equals方法和hashCode方法时,应该尽可能保证两个对象通过equals方法比较返回true时,它们的hashCode方法返回值也相等

TreeSet类

TreeSet类实现了SortedSet接口,能够对集合中的对象进行排序.
Java集合笔记(二):Set(集)
以下程序创建了TreeSet对象,然后向集合中加入了4个Integer对象:

public class TreeSetTest01 {
    public static void main(String[] args) {
        Set<Integer> set = new TreeSet<>();
        set.add(8);
        set.add(7);
        set.add(6);
        set.add(9);
        for (Integer i : set) {
            System.out.println(i.toString());
        }
    }
}

以上程序的打印结果为:

6 7 8 9 

TressSet支持两种排序方式:自然排序和客户化排序.默认情况下TreeSet采用自然排序方式.

自然排序

在JDK类库中,有一部分实现了Comparable接口,如Integer,Double和String等.Comparable接口有一个CompareTo(Object o)方法,它返回整数类型.对于表达式x.compareTo(y),如果返回值为0,表示x和y相等;如果返回值大于0,表示x大于y;如果返回值小于0.表示x小于y.
TreeSet调用对象的compareTo()方法比较集合中对象的大小,然后进行升序排列,这种排序方式称为自然排序;

下面简单总结一下 JDK类库中实现了Comparable接口的一些类的排序方式 :

排序
BigDecimal,BigInteger,Byte,Double, Float ,Integer,Long,Short 按数字大小排序
Charactor 按字符的Unicode值的数字大小排序
String 按字符串字符的Unicode值的数字大小排序

注意: 使用自然排序时,只能向TreeSet集合中加入相同类型的对象,并且这些对象的类必须**实现了Compareable接口

Comparable 是一个对象本身就已经支持自比较所需要实现的接口(如 String、Integer 自己就可以完成比较大小操作,已经实现了Comparable接口), 此接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的 compareTo 方法被称为它的自然比较方法。

比如你有一个Customer类 想让这个类的实例加入集合后自动就具有某种排序功能只要这些实例加入集合后 就会按照你给Customer对象设定的方式排序 ,代码:
Customer实体类:


public class Customer implements Comparable {
    private String name;
    private Integer age;

    /*
    判断两个Customer对象相等的条件为name属性和age属性都相等
     */
    @Override
    public int compareTo(Object o) {
        Customer other = (Customer) o;
        //先按照name属性排序
        if (this.name.compareTo(other.getName()) > 0) {
            return 1;
        }
        if (this.name.compareTo(other.getName()) < 0) {
            return -1;
        }

        //再按照age属性排序
        if (this.age.compareTo(other.getAge()) > 0) {
            return 1;
        }
        if (this.age.compareTo(other.getAge()) < 0) {
            return -1;
        }
        return 0;
    }

    public Customer(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public Customer setName(String name) {
        this.name = name;
        return this;
    }

    public Integer getAge() {
        return age;
    }

    public Customer setAge(Integer age) {
        this.age = age;
        return this;
    }
    /*
    equals方法应采用和CompareTo方法中采用相同的比较规则
     */

    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Customer)) {
            return true;
        }
        final Customer other = (Customer) o;
        if (this.name.equals(other.getName()) && this.age == other.getAge()) {
            return true;
        } else {
            return false;
        }
    }

    /*
    如果一个类重新实现了equals()方法,那么也应该重新实现HashCode()方法,并且保证当两个对象相等时,他们的哈希码值相同
     */
    public int hashCode() {
        int result;
        result = (name == null ? 0 : name.hashCode());
        result = 29 * result + age;
        return result;
    }

}

测试类:

public class TreeSetTest02 {
    public static void main(String[] args) {
        TreeSet<Customer> set = new TreeSet<>();
        set.add(new Customer("Tom",20));  
        set.add(new Customer("Tom",15));  
        set.add(new Customer("Mike",15));  
        for (Customer customer : set) {
            System.out.println(customer.getName() + " " + customer.getAge());
        }
    }
}

输出结果:

Mike 15
Tom 15
Tom 20

值得注意的是,对于TreeSet中已经修改了它们的name属性和age属性,TreeSet不会对集合进行重新排序,
Java代码:

import java.util.TreeSet;
public class TreeSetTest02 {
   public static void main(String[] args) {
       TreeSet<Customer> set = new TreeSet<>();
       Customer customer1 = new Customer("Tom", 15);
       Customer customer2 = new Customer("Tom", 16);
       set.add(customer1);
       set.add(customer2);
       //customer1.setAge(20);
       for (Customer customer : set) {
           System.out.println(customer.getName() + " " + customer.getAge());
       }
   }
}

上边我先把

customer1.setAge(20);  

这句注释掉 ,输出结果:

Tom 15
Tom 16

然后我们把那句取消注释之后的打印结果是:

Tom 20
Tom 16

Tom 20,如果按照升序应该在下边 但是却在上边 说明TreeSet没有给它重新排序哦在实际应用中Customer对象的name属性和age属性肯定应该是可以被修改的,因此不适合用TreeSet来排序。那大家也应该能想到最适合用TreeSet排序的就是不可变类,比如Integer,Double,String等 所谓不可变类,是指当创建了这个类的实例后,就不允许修改它的属性值。大家以后用还是小心点好儿!

客户化排序

除了自然排序,TreeSet还支持客户化排序, java.util.Comparator接口提供具体的排序方式,指定被比较的对象的类型,Comparator有个compare(Type x,Type y)方法,用于比较两个对象的大小,当compare(x,y)大于0时表示x大于y,小于0表示x小于y; 来个例子如果希望TreeSet按照Customer对象的name属性进行降序排列,可以先创建一个实现Comparator接口的类,
代码:

import java.util.Comparator;

public class CustomerComparator implements Comparator<Customer> {
    @Override
    public int compare(Customer customer1, Customer customer2) {
        if(customer1.getName().compareTo(customer2.getName())>0){return -1;}
        if(customer1.getName().compareTo(customer2.getName())<0){return 11;}
        return 0;
    }
}

public class TreeSetTest03 {
    public static void main(String[] args) {
        TreeSet<Customer> set = new TreeSet<>(new CustomerComparator());
        Customer customer1 = new Customer("Tom", 15);
        Customer customer2 = new Customer("Jack", 16);
        Customer customer3 = new Customer("Mike", 26);
        set.add(customer1);
        set.add(customer2);
        set.add(customer3);
        for (Customer customer : set) {
            System.out.println(customer.getName() + " " + customer.getAge());
        }
    }
}

输出结果:是倒序 …

Tom 15
Mike 26
Jack 16

那你现在是不知道了comparable接口和Comparator接口的区别了并且也能更好的使用TreeSet集合了
总结一下吧

  • 用自定义类实现Comparable接口,那么这个类就具有排序功能,Comparable和具体你要进行排序的类的实例邦定。而Comparator比较灵活,只需要通过构造方法指定一个比较器就行了,这种方式比较灵活。我们的要排序的类可以分别和多个实现Comparator接口的类绑定,从而达到可以按自己的意愿实现按多种方式排序的目的。Comparable——“静态绑定排序”,Comparator——“动态绑定排序”。

说说编写java类时应该养成一些好习惯吧

  • 如果java类重新定义了equals方法,那么这个类也必须重新定义hashCode()方法,并且保证当两个对象用equals方法比较结果为true时,这两个对象的hashCode()方法的返回值相等.

  • 如果java类实现了Comparable接口,那么这个类应该从新定义compareTo() equals() 和hashCode()方法,保证compareTo()和equals()方法采用相同的比较规则来比较两个对象是否相等,并且保证当两个对象用equals()方法比较的结果为true时,这两个对象的hashCode()方法的返回值相等.

  • HashSet和HashMap具有较好的性能,是Set和Map首选实现类,只有在需要排序的场合,才考虑使用TreeSet和TreeMap. LinkedList 和 ArrayList各有优缺点,如果经常对元素执行插入和删除操作,那么可以用LinkedList,如果经常随机访问元素,那么可以用ArrayList.