Java 集合类 2-Set一家人

1. Set接口

 Set与List同为Collection的继承子接口最大不同就是Set接口中的内容是不允许重复的。当添加重复元素时,不会报错,但是这个添加操作会被忽略。

 而且Set并没有对Collection接口进行方法的扩充。

 Set有两个实现子类,分别是HashSet和TreeSet,继承关系如下:

Set

1.1 HashSet

 HashSet定义如下:

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

 HashSet子类可以添加元素null,添加重复元素时会自动忽略添加操作,存储入HashSet的元素的存储顺序是无序的。

 由于Set接口没有扩充Collection接口,所以Set的实现子类拥有的方法和Collection一致,主要方法如下:

// 返回集合长度
public int size();

// 集合是否为空,空返回true,反之false
public boolean isEmpty();

// 集合中是否包含指定元素
boolean contains(Object o);

// 返回一个Iterator接口对象,用于集合的输出,后面会讲
Iterator<E> iterator();

// 将集合编程对象数组返回
Object[] toArray();

// 将指定元素添加进集合中
boolean add(E e);

// 在集合中删除指定元素
boolean remove(Object o);

 使用:

import java.util.HashSet;
import java.util.Set;

public class Test {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        set.add("hello");
        // 添加重复元素
        set.add("hello");
        // 添加null
        set.add(null);
        set.add(null);
        set.add("world");
        set.add("java");
        System.out.println(set);
    }
}

运行结果:

运行结果

1.2 TreeSet

 TreeSet子类定义如下:

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable

 TreeSet子类添加重复元素时和HashSet处理方式一致,不允许添加null,会报错。另外,存储在TreeSet中的元素会自动进行升序排序。

 TreeSet也是Set接口的实现类,其方法与HashSet一致。

import java.util.Set;
import java.util.TreeSet;

public class Test {
    public static void main(String[] args) {
        Set<Character> set = new TreeSet<>();
        set.add('c');
        // 重复存储
        set.add('c');
        set.add('d');
        set.add('a');
        set.add('b');
        System.out.println(set);
    }
}

运行结果:

运行结果

2. TreeSet排序分析

 这里,我们分析一下TreeSet将存储的对象数组自动排序这个特性。

 虽然TreeSet可以完成对象数组的排序处理,但并不是所有的对象都能进行排序。

 只有对象所在的类实现了Comparable接口并且覆写接口中的compareTo方法,TreeSet底层才能通过该方法知道对象的大小关系然后进行排序。

 另外,对象类覆写compareTo()方法时,类中所有属性都要进行比较操作,实例如下:

import java.util.Set;
import java.util.TreeSet;

class Person implements Comparable<Person> {
    private int age;
    private String name;
    public Person(String name, int age) {
        super();
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return "[name: "+this.name+", age: "+this.age+"]\n";
    }

    // 比较方法中所有的属性都要参与比较操作
    // 比较策略:优先比较age属性
    @Override
    public int compareTo(Person obj) {
        if(this.age > obj.age) {
            return 1;
        } else if(this.age < obj.age) {
            return -1;
        } else {
            // String类覆写过compareTo方法,所以这里直接调用
            return this.name.compareTo(obj.name);
        }
    }


}

public class Test {
    public static void main(String[] args) {
        Set<Person> set = new TreeSet<>();
        set.add(new Person("xucc", 14));
        set.add(new Person("licc", 13));
        set.add(new Person("dabb", 12));
        set.add(new Person("bbgir", 11));
        System.out.println(set);
    }
}

运行结果:

运行结果

 因为我们的比较策略是优先比较年龄,可以看出TreeSet按照年龄将Person进行了升序排序。

 在实际使用TreeSet中,如果一个类的属性很多,那么覆写它的compareTo()方法将会变得非常麻烦,所以我们使用的还是HashSet子类比较多一点。

3. HashSet对重复元素的判断

 Set接口的子类不能存储重复元素。TreeSet是根据覆写的compareTo()方法进行判断的。而HashSet是根据Object类的两个方法进行判断的:

// Hash码
public native int hashCode();

// 对象比较
public boolean equals(Object obj);

 HashMap底层是通过哈希表实现的,所以在判断重复的时候,首先根据对象的Hash码快速找到对象所位于的桶,然后使用equals方法对该桶上所有的对象进行比较,如果找到了一个相同的对象,证明当前判断的元素重复,否则就不重复。

 由此可以看出来,hashCode方法的作用是提高查找的效率,equals方法才算是真正的对象判断方法。

 看下面的例子:

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

class Person {
    public int age;
    public String name;
    public Person(String name, int age) {
        super();
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return "[name: "+this.name+", age: "+this.age+"]\n";
    }

    @Override
    public boolean equals(Object obj) {
        if(this == obj) {
            return true;
        }
        if(obj==null || this.getClass()!=obj.getClass()) {
            return false;
        }
        return Objects.equals(this.name, ((Person)obj).name) &&
                Objects.equals(this.age, ((Person)obj).age);
    }

    // hash方法最后调用了Objects的hash方法
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

public class Test {
    public static void main(String[] args) {
        Set<Person> set = new HashSet<>();
        set.add(new Person("xucc", 14));
        set.add(new Person("licc", 13));
        set.add(new Person("dabb", 12));
        set.add(new Person("bbgir", 11));
        System.out.println(set);
    }
}

运行结果:

运行结果

 如果两个对象hashCode()相同,equals()不同,不能证明两个对象相同,因为对象经过hash函数转换之后可能在同一个桶中,但是值不一定相同。

 如果两个对象hashCode()不同,equals()相同,也不能证明对象相同。第一个原因是因为传入的对象可能不属于同一个Map,当不属于同一个Map时,就算同一个对象,它们的hash码也是不同的。第二个原因是我们覆写的equals()方法可能存在编写漏洞,所以再加上hashCode()的判定可以起到双重保险的作用。

 所以,只有hashCode()和equals()方法一起使用才能标识出对象的唯一性。

 到这里,Collection接口下的List子接口和Map子接口都介绍完了,关于它俩,再提出最后一点小建议: 保存自定义对象的时候使用List接口的子类,保存系统类信息的时候使用Set接口的子类。

猜你喜欢

转载自blog.csdn.net/weixin_40739833/article/details/80531566