Set集合
Set集合类似于一个罐子,不记录添加元素的添加顺序,只是不允许包含重复元素(重复的判定在不同的实现类中可能有些区别。
HashSet类
HashSet具有很好的存取和查找性能。
HashSet有以下特点:
- 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也可能发生变化
- HashSet 不是同步的,如果多个线程访问同一个HashSet ,并且有两个或两个以上的线程同时修改HashSet,则必须通过代码来保证同步
- 集合元素值可以是null
Set像是一个罐子,记不住添加元素的顺序,所以查找的时候只能根据元素本身的属性去查找,因此Set不允许包含重复元素(这个重复的判定在不同的实现类中可能有细微的差别)
import java.util.HashSet;
class A{
@Override
public boolean equals(Object obj) {
return true;
}
@Override
public int hashCode() {
return 1;
}
}
class B{
@Override
public boolean equals(Object obj) {
return true;
}
}
class C{
@Override
public int hashCode() {
return 2;
}
}
public class HashSetTest {
public static void main(String[] args) {
HashSet s = new HashSet();
System.out.println(s.add(null));
System.out.println(s.add(null));
s.forEach(ele-> System.out.println(ele));
HashSet h = new HashSet();
System.out.println(h.add(new A()));
System.out.println(h.add(new A()));
System.out.println(h.add(new B()));
System.out.println(h.add(new B()));
System.out.println(h.add(new C()));
System.out.println(h.add(new C()));
System.out.println(h);
}
}
输出结果
true
false
null
true //可以看出 HashSet集合判断两个元素相等的标准是:两个对象通过equals()方法比较得到true,并且hashCode()方法的返回值也相等
false
true //equals()返回true,但hashCode()返回值不相等时,会把它们保存在不同位置,但这违反了Set集合的“不包含重复元素”的规则
true
true //equals()返回false,但hashCode()返回值相等时,会把它们用链式结构保存在一个位置,这样会导致在访问时性能下降
true //(两个以上的元素具有相同的hashCode值)
[A@1, C@2, C@2, B@54a097cc, B@36f6e879]
基于hashCode()和equals()方法对于HashSet的重要性,对于HashSet中的对象,要遵循以下重写这两个方法的基本规则:
- hashCode()每次运行的返回值应该一样
- 当两个对象通过equals()方法返回true时,这两个对象的hashCode()方法应该返回相同的值
- 对象中用作equals()方法比较标准的实例变量,都应该用于计算hashCode值
重写案例
import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
class R{
Double ooo;
public R(double d){
this.ooo = d;
}
@Override
public String toString() {
return "R{" +
"ooo=" + ooo +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
R r = (R) o;
return Objects.equals(ooo, r.ooo);
}
@Override
public int hashCode() {
long l = Double.doubleToLongBits(this.ooo);
return (int)(1^(l>>>32));
}
}
public class HashSetTest2 {
public static void main(String[] args) {
HashSet hs = new HashSet();
hs.add(new R(1.1));
// 整一个迭代器,把第一个元素取出来,没有进入迭代过程所以可以修改元素
// (这也算不上修改吧,迭代过程中也能修改元素,
// 但除了迭代器的remove()方法外不能用其他方法添加或删减元素)
Iterator it = hs.iterator();
R first = (R)it.next();
first.ooo = 2.2;
System.out.println(hs);
// contains(Object o)方法:HashSet集合会先得到o的hashCode,然后到自己集合中对应的地方去找,
// 找到后在通过equals()方法去比较,得到true后返回
System.out.println(hs.contains(new R(1.1)));//因为equals返回了false
System.out.println(hs.contains(new R(2.2)));//压根就没找到R(2.2)对应的hashCode
/*
由此可见,对于已经添加进去的对象,尽量不要修改关于该元素参与计算hashCode()和equals()的实例变量,
否则,像上面的hs中的那一个对象,既无法通过contains(1.1)访问到,也无法通过contains(2.2)访问到
这样会给集合操作这些元素带来很大麻烦
*/
}
}
输出结果
[R{ooo=2.2}]
false
false
LinkedHashSet类
相对于HashSet的区别:
LinkedHashSet 是HashSet的子类,LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时维护元素存储的次序。
当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合中的元素。
import java.util.LinkedHashSet;
public class LinkedHashSetTest {
public static void main(String[] args) {
LinkedHashSet ls = new LinkedHashSet();
ls.add("a");
ls.add("b");
ls.add("c");
ls.add("d");
System.out.println(ls);
ls.remove("c");
System.out.println(ls);
}
}
[a, b, c, d]
[a, b, d]
此实现使客户摆脱了HashSet提供的未指定的,通常混乱的排序,而不会导致与TreeSet相关的增加的成本。无论原始集的实现如何,都可以使用它来产生与原始集具有相同顺序的集合副本:
void foo(Set s) {
Set copy = new LinkedHashSet(s);
...
}
性能
由于需要维护元素的插入顺序(其实是维护链式列表),LinkedHashSet相对于HashSet会增加开销,因此执行添加、删除等操作时性能会轻微下降,但当迭代的时候LinkedHashSet集合的迭代时间与集合的大小成正比,而HashSet集合的迭代时间与集合的容量成正比,因此LinkedHashSet的迭代性能要好一些。
当构造HashSet集合时,有两个参数,一个是初始化容量(默认是16),一个是负载因子(默认值是0.75),因为HashSet的迭代性能和容量成正比,所以我们通常不会设置太大的容量,默认或者更低。
但当构造LinkedHashSet时,选择初始化容量过高的值的副作用不会那么严重,因为其迭代性能与集合大小成正比。
TreeSet类
public class TreeSetTest {
public static void main(String[] args) {
TreeSet s = new TreeSet();
s.add(1);
s.add(2);
s.add(3);
s.add(4);
System.out.println(s);
System.out.println(s.first()); //1
System.out.println(s.last()); //4
System.out.println(s.headSet(3)); //1,2
System.out.println(s.tailSet(2)); //2,3,4
System.out.println(s.subSet(1,3)); //1,2,3
}
}
可以看出(好像也看不太出。。没事),TreeSet并不是根据元素的插入顺序进行排序的,而是根据元素的实际值大小来排序的
与HashSet集合采用hash算法来决定元素的存储位置不同,TreeSet采用红黑树的数据结构来存储集合元素。
TreeSet支持两种排序规则:自然排序和定制排序。在默认情况下,TreeSet采用自然排序。
TreeSet自然排序
自然排序:TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列。
a.compareTo(b)方法来自Comparable接口,该方法返回一个整数:
返回0:两个对象相等
返回1:a大于b
返回-1:a小于b
Java的一些类已经实现了Comparable接口,并提供了比较大小的标准:
- BigDemical、BigInteger以及所有的数值型对应的包装类:按他们的大小进行比较
- Character:按字符的Unicode值进行比较
- Boolean:true对应的包装类实例大于false对应的包装类实例
- String:按字符串中字符的Unicode值进行比较
- Data、Time:(后面的时间、日期) 比 (前面的时间、日期大)
//下面程序试图向TreeSet集合中添加两个Err对象,添加第二个对象时,TreeSet会调用
//该对象的compareTo(Object o)方法与集合中的其他元素进行比较——
//如果其对应类没有实现Comparable接口,则会引发ClassCastException异常
TreeSet ts = new TreeSet();
ts.add(new Error());
ts.add(new Error());
/**
*而且,大部分类在实现compareTo(Object o)方法的时候,都需要将被比较的对象o强转为相同类型,因为只有两个相同类型才能比较大小。
*因此,向TreeSet里添加的应该是同一个类的对象,不然容易引发ClassCastException异常
**/
总结:如果希望TreeSet能够正常工作,TreeSet集合中只能存同一类对象。
HashSet和TreeSet中可变元素的实例变量尽量不要修改(参与相等判断的)
对于TreeSet而言,判断两个元素是否相等的唯一标准是:两个对象通过compareTo(Object o)返回的整数是不是0。
import java.util.TreeSet;
class P implements Comparable{
int num;
public P(int num) {
this.num = num;
}
@Override
public String toString() {
return "P{" +
"num=" + num +
'}';
}
@Override
public int compareTo(Object o) {
P p = (P)o;
return this.num>p.num? 1: this.num==p.num?0:-1;
}
}
public class TreeSet2 {
public static void main(String[] args) {
TreeSet ts = new TreeSet();
ts.add(new P(1));
ts.add(new P(5));
ts.add(new P(7));
ts.add(new P(9));
System.out.println(ts);
P first = (P)ts.first();
first.num = 100;
P last = (P)ts.last();
last.num = 7;
System.out.println(ts);
System.out.println(ts.remove(new P(1)));
System.out.println(ts.remove(new P(100)));
System.out.println(ts);
System.out.println(ts.remove(new P(5)));//①
System.out.println(ts);
/**
* 添加后的元素如果实例变量被修改(而且这个实例变量能够影响compareTo()方法的判断,TreeSet也不会重新排序
* 反而可能会造成出现重复的元素,导致两个都无法被删除,给操作带来困难
*
* 一旦修改了TreeSet集合里可变元素的实例对象,当再试图删除该对象时,TreeSet也会删除失败
* 甚至连集合中原有的、实例变量没有被修改但与修改后的元素相等的元素也无法被删除
*
* 说明TreeSet可以删除没有被修改实例变量、且不与其他修改实例变量的对象重复的对象
*
* 当执行上面①行代码时,TreeSet会对集合中的元素重新索引(不是重新排序)。接下来
* 就可以删除TreeSet中的其他元素了。包括那些被修改过的实例变量的元素。
*/
}
}
[P{num=1}, P{num=5}, P{num=7}, P{num=9}]
[P{num=100}, P{num=5}, P{num=7}, P{num=7}]
false
false
[P{num=100}, P{num=5}, P{num=7}, P{num=7}]
true
[P{num=100}, P{num=7}, P{num=7}]
与HashSet类似的是,如果TreeSet中包含了可变对象,当可变对象的实例变量被修改时,TreeSet在处理这些对象将非常复杂,而且容易出错.为了让程序更加健壮,不要修改放入HashSet和TreeSet集合中的关键实例变量(与CompareTo()方法相关的).
定制排序
TreeSet如果想要实现定制排序,则可以通过Comparator接口的帮助。该接口里包含一个int compare(Object o1,Object o2)方法,o1 == o2 返回0;o1>o2 返回1;o1<o2 返回-1;
通过提供一个Comparator对象与TreeSet关联,由该Comparator对象负责集合的排序逻辑。
由于Comparator是一个函数式接口,因此可使用Lambda表达式来代替Comparator对象。
import java.util.TreeSet;
class N{
int age;
public N(int age) {
this.age = age;
}
}
class M{
int age;
public M(int age) {
this.age = age;
}
@Override
public String toString() {
return "M{" +
"age=" + age +
'}';
}
}
public class TreeSetTest {
public static void main(String[] args) {
TreeSet ts = 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;
});
ts.add(new M(5));
ts.add(new M(9));
ts.add(new M(10));
ts.add(new M(1));
// ts.add(new N(90)); ClassCastException异常 所以还是不要添加不同类的对象
System.out.println(ts);
}
}
[M{age=10}, M{age=9}, M{age=5}, M{age=1}]