Set
Set继承于Collection接口,没有新增方法,不允许出现重复元素且无序,主要有HashSet与TreeSet两大实现类。
在判断重复元素时,Set集合会调用hashCode()方法与equals()方法。
散列码由对象的实例域产生的一个整数,如果是自定义类,则必须实现hashCode()方法,且实现的hashCode()方法必须与equals()方法兼容,即如果a.equals(b)为true,a与b必须具有相同的散列码。
HashSet
HashSet底层是用HashMap实现的,往HashSet中添加元素,实际上就是把这个元素作为key加入到内部的map中,由于map中的key是不可重复的,因此HashSet天然具有“不可重复”的特性。
//map集合,作为HashSet存放元素的容器
private transient HashMap<E,Object> map;
//PRESENT作为一个静态常量,是map中每个key对应的一个相同的value,并没有实际意义。
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
/**
* Constructs a new, empty set; the backing {@code HashMap} instance has
* default initial capacity (16) and load factor (0.75).
*/
//无参构造方法,完成内部map的创建
public HashSet() {
map = new HashMap<>();
}
equals()与hashCode()
HashSet集合判断两个元素相等的标准是:两个对象通过equals()方法比较相等,并且两个对象的hasCode()方法返回值也相等。
equals()定义在JDK中的Object.java中,因此Java中所有的类都有equals()方法,都可以通过equals()去比较两个对象是否相等。但是默认的equals()方法等价于"==",即是通过判断两个对象的地址是否相等(是否是同一个对象)来区分它们是否相等。
一般需要在Object的子类中重写equals()方法,使得两个对象的数据域属性相等时,则返回true。
hashCode()的作用是获取哈希码,也称为散列码,返回一个int整数,这个哈希码的作用的确定该对象在哈希表中索引位置。hashCode()定义在JDK的Object.java中,因此java中所有类都有hashCode()方法。但是,只有当创建某类对象需要创建散列表时,该类的hashCode()方法才有用,如HashMap,HashTable、HashSet。
默认hashCode()方法,只有两个对象的地址相同,两个对象的hashCode()返回的结果相等。
HashSet中判断集合中元素相等
- 通过equals()比较返回false,两个元素hashCode()返回值不相等,HashSet将把两个元素存在不同位置。
- 通过equals()比较返回true,两个元素hashCode()返回值不相等,HashSet将把两个元素存在不同位置。
- 通过equals()比较返回false,两个元素hashCode()返回值相等,HashSet将把两个元素存在相同位置,在这个位置以链式结构保存多个元素。
- 通过equals()比较返回true,两个元素hashCode()返回值相等,HashSet将不添加该新元素。
HashSet根据元素的hashCode值快速定位,如果两个元素具有相同的hashCode值,会导致性能下降,因此重写equals()与hashCode()方法时应尽量保证两个对象通过hashCode()方法返回值相等时,equals()返回true。
LinkedHashSet
LinkedHashSet是HashSet的子类,也是根据元素的hashCode值来决定元素的存储位置,也同样不允许元素重复,但是同时使用链表维护了元素的次序,按照插入次序。遍历LinkedHashSet时,会根据元素添加的顺序来访问集合里的元素。由于要维护元素的插入顺序,在性能上略低于HashSet。
HashSet<String> hashSet = new HashSet<>();
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
for (int i = 1; i <= 9; i++)
{
hashSet.add("I" + i);
linkedHashSet.add("I" + i);
}
for(String h1 : hashSet)
System.out.print(h1 + " ");
System.out.println();
for(String h2 : linkedHashSet)
System.out.print(h2 + " ");
//输出结果为:
//I9 I1 I2 I3 I4 I5 I6 I7 I8
//I1 I2 I3 I4 I5 I6 I7 I8 I9
注意:HashSet遍历输出元素其实有一定的顺序,只是不能保证是元素添加时的顺序,集合中元素与hashCode()方法没变时,遍历输出的元素顺序其实是不变。而LinkedHashSet遍历输出元素总是按照元素添加时的顺序。
TreeSet
TreeSet继承于AbstractSet,实现了NavigableSet接口,其是SortedSet子接口,TreeSet集合可以保证元素处于排序状态。TreeSet底层是用TreeMap实现的,内部维持了一个简化版的TreeMap,通过key来存储Set元素。TreeSet内部需要对存储的元素进行排序,因此对应的类必须实现Comparable接口,这样才能根据compareTo()方法比较对象之间的大小。
TreeSet排序方式
TreeSet的有序,不同于LinkedHashSet的添加顺序,而是根据元素属性进行排序的方式来实现。
TreeSet支持自然排序(默认)和定制排序。
注意:
- 采用自然排序时,必须对要添加到TreeSet中的类实现Comparable接口,否则会抛出异常: java.lang.ClassCastException。
- TreeSet中不能放入null元素。
自然排序
TreeSet会调用集合中元素所属类的compareTo(Object obj)方法来比较元素之间的大小关系,然后将元素按照升序排列,此为自然排序。
如果两个对象通过compareTo(Object obj)方法比较返回0,则TreeSet则认为两者相等,不将新元素添加到集合内。
TreeSet根据红黑树结构找到集合元素的存储位置。
定值排序
定值排序通过Comparator接口,重写compare(T o1, T o2)方法。实现定值排序时,需要在创建TreeSet时,调用一个带参构造器,传入Comparator对象,由该对象负责集合元素的排序逻辑,集合元素所属类不必实现Comparable接口。
Comparator<Person> comparator = new Comparator<Person>(){
@Override
public int compare(Person o1, Person o2) {
...
}
};
TreeSet<Person> set = new TreeSet<Person>(comparator);
注意:如果向TreeSet中添加了一个可变对象后,并且后面程序修改了该可变对象的实例变量,这将导致它与其他对象的大小顺序发生改变,但是TreeSet不会再次调整它们的顺序,即可能顺序会乱。