Java系列学习笔记 --- 集合(2) Set接口

目录

一、Set集合概述

二、HastSet类

二、LinkedHashSet类

三、TreeSet类

       3.1 自然排序

       3.2 定制排序

四、总结


一、Set集合概述

       Set集合记不住元素的添加顺序,它是通过hashCode值来寻找元素的存储位置的值,所以Set集合不允许包含相同的元素,如果试图将两个相同的元素加入同一个Set集合中,虽然不会报错,但是插入执行会失败,而且add()方法返回false,新的元素也不会被加入。例如下面范例所示。

Collection c = new HashSet();
System.out.println(c.add("张三"));  //true
System.out.println(c.add("张三"));  //flase
System.out.println("c集合的元素 = "+c);  //[张三]

二、HastSet类

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

  •        不能保证元素的排列顺序,顺序可能与添加顺序不同。
  •        HashSet不是同步的,如果多个线程同时访问,必须使用同步代码。
Set s = Collections.synchronizedSet(new HashSet(...));
  •        集合元素值可以是null

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

       总结:HashSet集合判断两个元素是否相等,除了通过equals()方法比较是否相等之外,还要通过hashCode()方法返回的结果,只要两个都为true那就是同一个元素;反之,只要任意一个不成立,HashSet就会为其分配存储空间。

       例如下面的代码范例中的三个方法,分别重写equals()、hashCode()两个方法的一个或者全部,通过此示例可以看出HashSet判断集合元素相同的标准。

public class S5_HashTest {
	public static void main(String[] args){
		HashSet hs = new HashSet();
		hs.add(new A());
		hs.add(new A());
		hs.add(new B());
		hs.add(new B());
		hs.add(new C());
		hs.add(new C());
		System.out.println(hs);
	}
}
class A{
	public boolean equals(Object obj){
		return true;
	}
}
class B{
	public int hashCode(){
		return 1;
	}
}
class C{
	public int hashCode(){
		return 2;
	}
	public boolean equals(Object obj){
		return true;
	}
}

       上面程序中向hs集合添加了两个A对象、两个B对象和两个C对象,其中C类重写了A类和B类的方法,这就导致HashSet将两个C对象当成同一个对象了。运行上面的程序,看到如下运行结果。

[ListMapAnd.B@1,ListMapAnd.B@1,ListMapAnd.C@2,ListMapAnd.A@15db9742, ListMapAnd.A@6d06d69c]

       从上面的程序可以看出,即使两个A对象通过equals()方法比较返回true,,两个B对象的hashCode()返回相同值,但HashSet依然把他们当成两个对象。唯独只有像C类对象那样hashCode()返回值和equals()值都相同时,HashSet才会判断这两个对象是相等的。

       总结:需要注意的是,如果两个对象的hashCode()方法返回的hashCode值相同,而equals()方法返回的值为false时将会是一个很麻烦的事情。因为两个对象的hashCode值相同的话,HashSet会将它们保存在同一个位置,但如果这样的,后来保存的将会替换掉前面所保存的对象,所以实际上这个回值会采用链式结构来保存多个对象。而且HashSet访问集合元素时也是根据元素的hashCode值来快速定位的,如果HashSet中有两个以上的元素具有相同的hashCode值,将会导致性能的下降,而HashSet相对数组以外来说最大的价值就是查询速度块,所以如果HashSet中存储了多个相同hashCode的元素时,就没有必要使用HashSet来存储元素,而是改用其他的集合。

       另外补充以下两点:

  1. 如果需要把某个类的对象保存到HashSet集合中,重写这个类的equals()方法和hashCode()方法时,应该尽量保证这两个方法返回的值都相等。
  2. hash也被称为哈希、散列,hash算法能够保证快速查找被检索的对象,它的算法夹之在于速度。如果需要查询几何中的某个元素时,HashSet通过调用该对象的hashCoode()方法获取该对象的hashCode值,通过hash算法可以直接根据该元素的hashCode值计算出该元素的存储位置,从而快速定位该元素的位置并取出该元素。它和数组很想,只不过数组是根据索引计算该元素在内存里的存储位置,虽然是所有能存储元素里最快的数据结构,但是它的长度是固定的,无法像集合那样灵活。

       需要注意的是,当程序把可变对象添加到HashSet中之后,尽量不要去修改该集合元素中参与计算hashCode()、equanls()的实例变量,否则将会导致HashSet无法正确操作这些集合元素。

二、LinkedHashSet类

       HashSet还有一个子类LinkedHashSet,所以LinkedHashSet集合也是更具元素的hashCode值来决定元素的存储位置,但它同时也使用链表维护元素的次序。也就是说,当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。

Collection hs = new HashSet();
hs.add("张三");
hs.add("李四");
hs.add("王五");
hs.add("周六");
hs.forEach(elemt -> System.out.print(elemt+" "));
System.out.println();
Collection lhs = new LinkedHashSet();
lhs.add("张三");
lhs.add("李四");
lhs.add("王五");
lhs.add("周六");
lhs.forEach(elemt -> System.out.print(elemt+" "));

       最后的输出结果如下图所示

李四 张三 王五 周六
张三 李四 王五 周六

三、TreeSet类

       TreeSet是SortedSet接口的实现类,正如名字所示,它能够确保集合元素的排序状态。例如下面的程序范例。

//【HashSet】
HashSet hashSets = new HashSet();
for(int i=1;i<=10;i++){
	hashSets.add((int)(Math.random()*1000+1));
}
System.out.println(hashSets);
//【TreeSet】
TreeSet treeSets = new TreeSet();
treeSets.addAll(hashSets);
System.out.println(treeSets);

       输出结果如下图所示

-- 集合hashSets存储的元素为 --
[593,129,654,758,903,329,507,108,348]
-- 集合treeSets存储的元素为 --
[108,129,329,348,507,593,645,758,903]

       根据上面程序的运行结果可以看出,TreeSet并不是更具元素的插入顺序进行排序的,而是根据元素值的大小进行排序。另外与HashSet集合相比,ThreeSet还提供了如下额外方法。

  • Object first():返回集合中的第一个元素。
  • Object last():返回集合中的最后一个元素。
  • Object lower(Object e):返回集合中小于指定元素的最大元素,即上一个元素。
  • Object higher(Object e):返回集合中大于指定元素的最小元素,即下一个元素。
  • SorteSet subSet(Object fromElemt,Object toElemt):返回此Set的子集合,范围从 [ fromElemt ,toElemt )
  • SortedSet haeaSet(Object toElemt):返回集合中所有小于toElemt的元素所组成的子集合。
  • SortedSet tailSet(Object fromElemt):返回集合中所有小于或等于Elemt的元素所组成的子集合。
  • Comparator comparator():如果TreeSet采用了定制排序,该方法返回定制排序所使用的Comparator ,如果采用自然排序,则返回null。

       这些方法看起来很多,其实也很简单,因为TreeSet中的元素是有序的,所以增加了访问第一个、上一个、下一个、最后一个元素的方法,并提供了三个截取子集合的方法。下面我们通过具体的范例测试上面的方法。

HashSet hashSets = new HashSet();
for(int i=1;i<=10;i++){
    hashSets.add((int)(Math.random()*1000+1));
}
//【TreeSet】
TreeSet treeSets = new TreeSet();
treeSets.addAll(hashSets);
System.out.println("————集合treeSets存储的元素为————\n"+treeSets);
//返回第一个元素
System.out.println(treeSets.first());
//返回最后一个元素
System.out.println(treeSets.last());
//返回小于300的子集
System.out.println(treeSets.lower(500));
//返回大于或等于300的子集
System.out.println(treeSets.higher(500));
//返回小于300的子集
System.out.println(treeSets.headSet(300));
//返回大于或等于300的子集
System.out.println(treeSets.tailSet(300));
//返回大于或等于300、小于700的子集
System.out.println(treeSets.subSet(300,700));

       输出结果如下图所示

       TreeSet采用红黑树的数据结构来存储集合元素,它支持两种排序方法:自然排序和定制排序。默认情况下的升序排序就是自然排序。

3.1 自然排序

       TreeSet通过调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序进行排列。

       Java提供了Comparable接口里定义了一个compareTo(Object obj)方法,当一个对象调用该方法和另一个对象进行比较时,如果该方法返回0,则表明这两个对象相等;如果该方法返回一个正整数,则表明调用对象大于被比较对象;如果该方法返回一个负整数,则表明调用对象小于被比较对象。

Integer x = 100;
System.out.println(x.compareTo(50));   //1
System.out.println(x.compareTo(100));  //0
System.out.println(x.compareTo(150));  //-1

       Java的一些常用类已经实现了Comparable接口,并提供了比较大小的标准,例如下面这些是实现了Comparable接口的常用类。

  •        BigDecimal、BigInteger以及所有的数值型所对应的包装类:按照它们对应的数值大小进行比较。
  •        Character:按照UNICODE值进行比较。
  •        String:按字符串中字符的UNICODE值进行比较
  •        Date、Time:比较日期或时间

       通过上面讲述,我们要知道的一点就是,只有实现了Comparable接口的类的实例对象才能添加到TreeSet中,否则程序会抛出异常。例如下面的程序范例所示。

       上面程序中的ts集合添加test()对象时,插入的对象没有实现Comparable接口,所以程序抛出了ClassCastException异常。

       注意:因为集合可以存储不同类型的对象,所以在调用Comparable()方法的时候,会将被比较对象强制转换成相同类型,不然没法比较。所以,应该向TreeSet添加同类对象,否则也将会引发ClassCastException异常。

       程序向TreeSet添加第一个字符对象的时候,由于没有对象进行比较,所以这个操作完全正常。当向TreeSet添加第二个对象的时候,TreeSet就会调用该对象的compareTo(Object obj)方法进行比较,由于两个对象的数据类型不相同,所以比较失败从而抛出异常。

       将对象加入TreeSet集合中时,TreeSet会调用该对象的compareTo(Object obj)方法与容器中的其他对象比较大小,然后根据红黑树结构找到它的存储位置,如果两个对象通过比较相等的话,新的对象将无法添加到TreeSet集合中。也就是说对TreeSet集合而言,判断是否相等的标准就是通过compareTo(Object obj)方法比较是否返回0。例如下面的例子所示:

public class S9_ConparaTo {
	public static void main(String[] args) {
		TreeSet ts = new TreeSet();
		A9 a9 = new A9(5);
		System.out.println(ts.add(a9));
		System.out.println(ts.add(a9));
		System.out.println(ts);
		//修改集合里面第一个对象的属性值
		((A9)ts.first()).num = 100;
		System.out.println("第一个值:"+((A9)ts.first()).num);
		System.out.println("第二个值:"+((A9)ts.last()).num);
	}
}
class A9 implements Comparable{
	int num;
	public A9(int num){
		this.num = num;
    }
	public boolean equals(Object object){
		    return true;
    }
    public int compareTo(Object object){
	    return 1;
    }
}

       最终的输出结果如下图所示

       从上面的程序中我们可以看出,虽然两个对象的equals方法总是返回true,但因为compareTo()方法所返回的值永远时1,所以TreeSet依旧会认为这两个Z1对象不相等。然而实际上,这两个对象是完全相等,所以TreeSet所存储的两个对象在对内存中存储的位置完全相同,所以当修改了TreeSet集合中的其中一个相等对象时,另外一个对象的值自然也就会发生改变。

       另外需要注意一点的时,TreeSet集合只有在对象插入的时候才会对对这个对象进行排序,也就是说如果TreeSet集合插入的可变对象的话,那么TreeSet集合不会在它改变之后再次进行排序。例如下面例子所示。

public class S9_ComparaTo2 {
	public static void main(String[] args) {
		TreeSet ts = new TreeSet();
		ts.add(new A91(300));
		ts.add(new A91(100));
		ts.add(new A91(200));
		System.out.println("-- 第一次遍历 --");
		ts.forEach(obj -> System.out.print(((A91)obj).num+" "));
		//修改集合里面第一个对象的属性值
		((A91)ts.first()).num = 300;
		System.out.println("\n-- 第二次遍历 --");
		ts.forEach(obj -> System.out.print(((A91)obj).num+" "));
		//1、删除被改变的元素
		System.out.print(ts.remove(new A91(100))+" ");
		System.out.print(ts.remove(new A91(300)));
		System.out.println("\n-- 第三次遍历 --");
		ts.forEach(obj -> System.out.print(((A91)obj).num+" "));
		//2、删除未改变的元素
		System.out.print(ts.remove(new A91(200)));
		System.out.println("\n-- 第四次遍历 --");
		ts.forEach(obj -> System.out.print(((A91)obj).num+" "));
	}
}
class A91 implements Comparable{
	int num;
	public A91(int num){
		this.num = num;
	}
	public int compareTo(Object obj){
		A91 a = (A91)obj;
		return num > a.num ? 1 : num  < a.num ? -1 : 0;
	}
}

       输出结果如下所示

       从上面的程序和结果中可以看到,想要删除已经被替换掉了的,将会删除失败,更有可能是删除错了。而删除没有改变的对象时,并不会删除替换之后和被删除的对象一样的对象。

3.2 定制排序

       TreeSet的自然排序是根据集合元素的大小,按照升序进行排列。如果需要实现定制排序,例如以降序排列,则可以通过Comparator接口中的compara(T o1,T o2)方法。如果该方法返回正整数,则表明o1大于o2;如果该方法返回0,则证明o1等于o2;如果该方法返回负整数,则表明o1小于o2。

       如果需要实现定制排序,则需要在创建TreeSet集合对象时,提供Comparator对象与该TreeSet集合关联,由该Comparator对象负责元素的排序逻辑。由于Comparator时一个函数式接口,因此可以使用Lambda表达式来代替Comparator对象。

public class S10_Comparator {
	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(-3));
		ts.add(new M(8));
		System.out.println(ts);
	}
}
class M{
	int age;
	public M(int age){
		this.age = age;
	}
	public String toString(){
		return "M[age:"+age+"]";
	}
}

       上面程序中,粗体字部分使用了目标类型未Comparator的Lambda表达式,它负责ts集合的排序。所以当把M对象添加到ts集合中时,实现Comparable接口,运行程序可以看到如下运行结果和插入一个数值30的排序算法大致过程。

四、总结

       1、HashSet的整体性能比TreeSet要好,因为TreeSet需要红黑树算法来维护集合元素的次序。只有当需要一个褒词排序的Set时,才应该使用TreeSet,否则都应该使用HashSet。

       2、HashSet还有一个子类:LinkedHashSet,它的插入、删除操作要比HashSet略慢一点,这时由于维护链表所带来的额外开销造成的,但有了链表,遍历LinkedHashSet的效率会更快。

       3、需要注意的是,HashSet、TreeSet、LinkedHashSet都是线程不安全的,所以,如果由多个线程同时访问一个Set集合,并且修改了该集合,则必须手动保证该Set集合的同步性。

 

 

 

 

 

 

 

 

 

 

猜你喜欢

转载自blog.csdn.net/Rao_Limon/article/details/89242303