Set接口
HashSet
-
HashSet简介
HashSet是Set接口的典型实现,大多数时候使用Set集合时就是使用这个实现类。HashSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找功能。
HashSet具有以下特点:
-
HashSet 是一个没有重复元素的集合。
-
不能保证元素的排列顺序,顺序可能和添加顺序不同,顺序也有可能发生变化。
-
HashSet不是同步的,如果多个线程同时访问一个HashSet,假设有两个或两个以上线程同时修改了HashSet集合时,则必须通过代码来保证其同步。
-
集合元素值可以是null。
-
HashSet通过iterator()返回的迭代器是fail-fast的。
当向HashSet集合中存入一个元素时, HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet中的存储位置。如果有两个元素通过equals()方法比较返回true,但它们的hashCode()方法返回值不相等,HashSet将会把它们存储在不同的位置,依然可以添加成功。
也就是说,HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相等。
注意:当把一个对象放入 HashSet中时,如果需要重写该对象对应类的equals()方法,则也应该重写其hashCode()方法。规则是,如果两个对象通过equals()方法比较返回true,这两个对象的hashCode值也应该相同。
如果两个对象通过equals()方法比较返回true,但这两个对象的hashCode()方法返回不同的hashCode值时,这将导致 HashSet会把两个对象保存在Hash表的不同位置,从而使这两个对象都可以添加成功,这就与Set集合的规则冲突了。
如果两个对象的hashCode()方法返回的hashCode值相同,但它们通过equals()方法比较返回false时将更麻烦:因为两个对象的hashCode值相同, HashSet将试图把它们保存在同一个位置,但又不行(否则将只剩下一个对象),所以实际上会在这个位置用链式结构来保存多个对象;而 HashSet访问集合元素时也是根据元素的hashCode值来快速定位的,如果 HashSet中两个以上的元素具有相同的hashCode值,将会导致性能下降。
HashSet中每个能存储元素的“槽位”(slot)通常称为“桶”(bucket),如果有多个元素的hashCode值相同,但它们通过equals()方法比较返回false,就需要在一个“桶”里放多个元素,这样会导致性能下降严重。
hashCode()方法对于 HashSet的重要性非常大(实际上,对象的hashCode值对于后面的HashMap同样重要),下面给出重写hashCode()方法的基本规则:
-
在程序运行过程中,同一个对象多次调用hashCode()方法应该返回相同的值。
-
当两个对象通过equals()方法比较返回true时,这两个对象的hashCode()方法应返回相等的值。
-
对象中用作equals()方法比较标准的实例变量,都应该用于计算hashCode值。
注意:
当向 HashSet中添加可变对象时,必须十分小心。如果修改 HashSet集合中的对象,有可能导致该对象与集合中的其他对象相等,从而导致 HashSet无法准确访问该对象。
TreeSet
-
TreeSet简介
TreeSet是SortedSet接口的实现类,正如SortedSet名字所暗示的,TreeSet可以确保集合元素处于排序状态。
如果希望TreeSet能正常运行,TreeSet只能添加同一种类型的对象。
TreeSet 是一个有序的集合,它的作用是提供有序的Set集合。它继承于AbstractSet抽象类,实现了NavigableSet<E>, Cloneable, java.io.Serializable接口。
TreeSet 继承于AbstractSet,所以它是一个Set集合,具有Set的属性和方法。
TreeSet 实现了NavigableSet接口,意味着它支持一系列的导航方法。比如查找与指定目标最匹配项。
TreeSet 实现了Cloneable接口,意味着它能被克隆。
TreeSet 实现了java.io.Serializable接口,意味着它支持序列化。
TreeSet是基于TreeMap实现的。TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序。这取决于使用的构造方法。默认情况下,TreeSet采用自然排序。
TreeSet为基本操作(add、remove 和 contains)提供受保证的 log(n) 时间开销。
另外,TreeSet是非同步的。 它的iterator 方法返回的迭代器是fail-fast的。
1.自然排序
TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排序,这种方式就是自然排序。在TreeSet中,对于String、Integer等类型就按照该类型自身提供的compareTo()方法进行排序,如果是自定义对象,则必须让自定义对象实现Comparable接口并重写compareTo()方法。
Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。当一个对象调用该方法与另一个对象进行比较时,例如obj
.compareTo(Object obj2),如果该方法返回0,则表明这两个对象相等;如果该方法返回一个正整数,则表明obj1大于obj2;如果该方法返回一个负整数,则表明obj1小于obj2。
Java的一些常用类已经实现了Comparable接口,并提供了比较大小的标准。下面是实现了Comparable接口的常用类:
-
BigDecimal、BigInteger以及所有的数值型对应的包装类:按它们对应的数值大小进行比较。
-
Character:按字符的UNICODE值进行比较。
-
Boolean:true对应的包装类实例大于false对应的包装类实例。
-
String:按字符串中字符的UNICODE值进行比较。
-
Date、Time:后面的时间、日期比前面的时间、日期大。
如果试图把一个对象添加到TreeSet时,则该对象的类必须实现Comparable接口,否则程序将会抛出异常。在自然排序时,集合元素必须实现Comparable接口,否则将会引发运行时异常:ClassCastException——因此,TreeSet要求自然排序的集合元素都必须实现该接口。
为了让程序更加健壮,推荐不要修改放入HashSet和TreeSet集合中元素的关键字实例变量。
2.定制排序
如果需要实现定制排序,则可以通过Comparator接口的帮助。该接口里包含一个int compare(T o1,T o2)方法,该方法用于比较o1和o2的大小:如果该方法返回正整数,则表明o1大于o2;如果该方法返回0,则表明o1等于o2;如果该方法返回负整数,则表明o1小于o2。
如果需要实现定制排序,则需要在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联,由该Comparator对象负责集合元素的排序逻辑。由于Comparator是一个函数式接口,因此可使用Lambda表达式来代替Comparator对象。
import java.util.TreeSet;
class M{
int age;
public M(int age) {
this.age = age;
}
@Override
public String toString() {
return super.toString();
}
}
public class ComparatorTest {
public static void main(String[] args) {
TreeSet<M> treeSet = new TreeSet(((o1, o2) -> {
M m1 = (M) o1;
M m2 = (M) o2;
//根据M对象的age属性来决定大小,age越大,M对象反而越小
return m1.age > m2.age ? -1 : m1.age < m2.age ? 1 : 0;
}));
}
}
注意:
当通过Comparator对象(或Lambda表达式)来实现TreeSet的定制排序时,依然不可以向TreeSet中添加类型不同的对象,否则会引发ClassCastException异常。使用定制排序时,TreeSet对集合元素排序不管集合元素本身的大小,而是由Comparator对象(或Lambda表达式)负责集合元素的排序规则。TreeSet判断两个集合元素相等的标准是:通过Comparator(或Lambda表达式)比较两个元素返回了0,这样TreeSet不会把第二个元素添加到集合中。
总结:comparable和comparator的区别