Java学习笔记一Set集合

Set集合与Collection基本相同,没有提供任何额外的方法,实际上Set就是Collection,只是不允许包含相同的元素。

HashSet类

HashSet是Set接口的典型实现,按Hash算法来存储集合中的元素,具有很好的存取和查找性能。
HashSet具有以下特点:不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。HashSet不是同步的,如果多个线程同时访问一个HashSet,假如有两个或两个以上线程同时修改了HashSet集合时,则必须通过代码来保证其同步。集合元素值可以是null
当向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方法比较返回fasle时将更麻烦,因为两个对象的hashCode值相同,HashSet将试图把它们保存在同一位置,但又不行(否则将只剩下一个对象),所以实际上会在这个位置用链式结构来保存多个对象;而HashSet访问集合元素时也是根据元素的hashCode值来快速定位的,如果HashSet中两个以上的元素具有相同的hashCode值,将会导致性能下降。
HashSet中每个能存储元素的槽位slot通常称为桶bucket,如果有多个元素的hashCode值相同,但它们通过equals()方法比较返回false,就需要在一个桶里放多个元素,这样会导致性能下降。
重写hashCode方法的一般步骤:

  1. 把对象内每个有意义的实例变量(即每个参与equals方法比较标准的实例变量)计算出一个int类型的hashCode值。
    | 实例变量 | 计算方式 |
    | boolean | hashCode=(f?0:1); |
    | 整数类型(byte、short、char、int) | hashCode=(int)f; |
    | long | hashCode=(int)(f^(f>>32)); |
    | float | hashCode=Float.floatToIntBits(f); |
    | double | long l = Double.doubleToLongBits(f); hashCode=(int)(l^(l>>32)); |
    | 引用类型 | hashCode=f.hashCode(); |
  2. 用第1步计算出来的多个hashCode值组合计算出一个hashCode值返回。
    例如return f1.hashCode() + (int)f2; 为了避免直接相加产生偶然相等(两个对象的f1、f2实例变量并不相等,但它们的hashCode的和恰好相等),可通过为各实例变量的hashCode乘以任意一个质数后再相加 return f1.hashCode()*19 + (int)f2*31;
class R
{
	int count;
	public R(int count) { this.count = count; }
	public String toString() { return "R[count:"+count+"]"; }
	public boolean equals(Object obj)
	{
		if(this == obj)	return true;
		if(obj != null && obj.getClass() == R.class){
			R r = (R)obj;
			return this.count == r.count;
		}
		return false;
	}
	public int hashCode() { return this.count; }
}
public class HashSetTest2
{
	public static void main(String[] args)
	{
		HashSet hs = new HashSet();
		hs.add(new R(5));
		hs.add(new R(-3));
		hs.add(new R(9));
		hs.add(new R(-2));
		// 打印HashSet集合,集合元素没有重复
		System.out.println(hs);
		// 取出第一个元素
		Iterator it = hs.iterator();
		R first = (R)it.next();
		// 为第一个元素的count实例变量赋值
		first.count = -3;
		// 再次输出HashSet集合,集合元素有重复元素
		System.out.println(hs);
		// 删除count为-3的R对象
		hs.remove(new R(-3));
		// 可以看到被删除一个R元素
		System.out.println(hs);
		System.out.println("hs是否包含count为-3的R对象?"+hs.contains(new R(-3)));  // 输出false
		System.out.println("hs是否包含count为-2的R对象?"+hs.contains(new R(-2)));  // 输出false
	}
}

在这里插入图片描述

LinkedHahSet类

LinkedHashSet集合根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。也就是说,当遍历LinkedHasdSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。

public class LinkedHashSetTest
{
	public static void main(String[] args){
		LinkedHashSet books = new LinkedHashSet();
		books.add("疯狂JAVA讲义");
		books.add("轻量级Java EE企业应用实战");
		System.out.println(books);
		// 删除 疯狂Java讲义
		books.remove("疯狂JAVA讲义");
		// 添加 疯狂Java讲义
		books.add("疯狂JAVA讲义");
		System.out.println(books);
	}
}

在这里插入图片描述
虽然LinkedHashSet使用了链表记录集合元素的添加顺序,但LinkedHashSet依然是HashSet,因此它依然不允许集合元素重复。

TreeSet类

TreeSet是SortedSet接口的实现类,可以确保集合元素处于排序状态,根据元素实际值的大小来进行排序的。TreeSet采用红黑树的数据结构来存储集合元素,并且支持两种排序方法:自然排序和定制排序。

TreeSet nums = new TreeSet();
// 向TreeSet中添加四个Integer对象
nums.add(5);
nums.add(2);
nums.add(10);
nums.add(-9);
// 输出集合元素,看到集合元素已经处于排序状态
System.out.println(nums);
// 输出集合中第一个元素
System.out.println(nums.first());
// 输出集合中最后一个元素
System.out.println(nums.last());
// 返回小于4的子集,不包括4
System.out.println(nums.headSet(4));
// 返回大于5的子集,如果Set中包含5,子集中还包含5
System.out.println(nums.tailSet(5));
// 返回大于等于-3、小于4的子集
System.out.println(nums.subSet(-3,4));

在这里插入图片描述

自然排序

TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列。Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类对象就可以比较大小。当一个对象调用该方法与另一个对象进行比较时,例如obj1.compareTo(obj2),如果该方法返回0,则表明这两个对象相等;如果该方法返回一个正整数,则表明obj1大于obj2;如果该方法返回一个负整数,则表明obj1小于obj2。
Java 9改进了TreeSet实现,如果采用自然排序的Set集合的元素没有实现Comparable接口,程序就会立即引发ClassCastException异常。大部分类在实现compareTo(Object obj)方法时,都需要将被比较对象obj强制类型转换成相同类型,因此只有相同类的两个实例才会比较大小。当试图把一个对象添加到TreeSet集合时,TreeSet会调用该对象的compareTo(Object obj)方法与集合中的其他元素进行比较-这就要求集合中的其他元素与该元素是同一个类的实例。也就是说,向TreeSet中添加的应该是同一个类的对象,否则会引发ClassCastException异常。如果向TreeSet中添加的对象是程序员自定义类的对象,则可以向TreeSet中添加多种类型的对象,前提是用户自定义类实现了Comparable接口,且实现compareTo(Object obj)方法没有进行强制类型转换。但当试图取出TreeSet里的集合元素时,不同类型的元素依然会发生ClassCastException异常。
当需要把一个对象放入TreeSet中,重写该对象对应类的equals()方法时,应保证该方法与compareTo(Object obj)方法有一致的结果,其规则是:如果两个对象通过equals方法比较返回true时,这两个对象通过compareTo(Object obj)方法比较应返回0。

class Z implements Comparable
{
    int age;
    public Z(int age) { this.age = age; }
    //重写equals方法,总是返回true
    public boolean equals(Object obj) { return true; }
    //重写compareTo方法,总是返回1
    public int compareTo(Object obj) { return 1; }
}
TreeSet set = new TreeSet();
Z z1 = new Z(6);
set.add(z1);
//第二次添加同一个对象,输出true,表示添加成功
//TreeSet依靠对象的compareTo与容器中其他对象比较大小,而自定义的Z一直返回正数,表明不相等,可以添加
System.out.println(set.add(z1));
// 可见有两个对象,其实是两个引用,指向同一个对象
System.out.println(set);
((Z)(set.first())).age = 9;
System.out.println(((Z)(set.last())).age);

在这里插入图片描述
向TreeSet中添加一个可变对象后,通过修改该可变对象的实例变量,将导致它与其他对象的大小顺序发生改变,但TreeSet不会再次调整它们的顺序。一旦改变了TreeSet集合里可变元素的实例变量,当再试图删除该对象时,TreeSet会删除失败(甚至集合中原有的、实例变量没被修改但与修改后元素相等的元素也无法删除),但是可以删除没有被修改实例变量、且不与其他被修改实例变量的对象重复的对象,删除后TreeSet会对集合中的元素重新索引(不是重新排序),接下来就可以删除TreeSet中的所有元素了,包括那些被修改过实例变量的元素。与HashSet类似,如果TreeSet中包含了可变对象,当可变对象的实例变量被修改时,TreeSet在处理这些对象时将非常复杂,而且容易出错。

import java.util.TreeSet;
class R implements Comparable
{
    int count;
    public R(int count) { this.count = count; }
    public String toString() { return "R[count:"+count+"]"; }
    // 重写equals方法,根据count来判断是否相等
    public boolean equals(Object obj)
    {
        if(this == obj){
            return true;
        }
        if(obj != null && obj.getClass() == R.class)
        {
            R r = (R)obj;
            return r.count == this.count;
        }
        return false;
    }
    // 重写compareTo方法,根据count来比较大小
    public int compareTo(Object obj){
        R r = (R)obj;
        return count > r.count ? 1 : count < r.count ? -1 : 0;
    }
}
public class TreeSetTest {
    public static void main(String[] args) {
        TreeSet ts = new TreeSet();
        ts.add(new R(5));
        ts.add(new R(-3));
        ts.add(new R(9));
        ts.add(new R(-2));
        // 打印TreeSet集合,集合元素是有序排列的
        System.out.println(ts);
        // 取出第一个元素
        R first = (R)ts.first();
        // 对第一个元素的count赋值
        first.count = 20;
        // 取出最后一个元素
        R last = (R)ts.last();
        // 对最后一个元素的count赋值,与第二个元素的count相同
        last.count = -2;
        // 再次输出将看到TreeSet里的元素处于无序状态,且有重复元素
        System.out.println(ts);
        // 删除实例变量被改变的元素,删除失败
        System.out.println(ts.remove(new R(-2)));
        System.out.println(ts);
        // 删除实例变量没有被改变的元素,删除成功
        System.out.println(ts.remove(new R(5)));
        System.out.println(ts);
    }
}

在这里插入图片描述

定制排序

如果需要实现定制排序,则需要在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联,由该Comparator对象负责集合元素的排序逻辑。由于Comparator是一个函数式接口,因此可使用Lambda表达式来代替Comparator对象。
当通过Comparator对象(或Lambda表达式)来实现TreeSet的定制排序时,依然不可以向TreeSet中添加类型不同的对象,否则会引发ClassCastException异常。使用定制排序时,TreeSet对集合元素排序不管集合元素本身大小,而是由Comparator对象(或Lambda表达式)负责集合元素的排序规则。TreeSet判断两个集合元素相等的标准是:通过Comparator(或Lambda表达式)比较两个元素返回了0,这样TreeSet不会把第二个元素添加到集合中。

import java.util.TreeSet;
class M
{
    int age;
    public M(int age) { this.age = age; }
    public String toString() { return "M[age:"+age+"]"; }
}
public class TreeSetTest1 {
    public static void main(String[] args) {
        // Lambda表达式的目标类型是Comparator
        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(9));
        ts.add(new M(-2));
        System.out.println(ts);
    }
}

在这里插入图片描述

EnumSet类

EnumSet是一个专为枚举类设计的集合类,EnumSet中的所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显示或隐式地指定。EnumSet的集合元素也是有序的,EnumSet以枚举值在Enum类内的定义顺序来决定集合元素的顺序。EnumSet在内部以位向量的形式存储,这种存储形式非常紧凑、高效,因此EnumSet对象占用内存很小,而且运行效率很好。
EnumSet集合不允许加入null元素,如果试图插入null元素,EnumSet将抛出NullPointerException异常。如果只是想判断EnumSet是否包含null元素或试图删除null元素都不会抛出异常,只是删除操作将返回false,因为没有任何null元素被删除。EnumSet类没有暴露任何构造器来创建该类的实例,程序应该通过它提供的类方法来创建EnumSet对象。当复制一个Collection集合里的元素来创建EnumSet集合时,必须保证Collection集合里的所有元素都是同一枚举类的枚举值。

import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
enum Season
{
    SPRING,SUMMER,FALL,WINTER
}
public class EnumSetTest {
    public static void main(String[] args) {
        // 创建一个EnumSet集合,集合元素就是Season枚举类的全部枚举值
        EnumSet es1 = EnumSet.allOf(Season.class);
        System.out.println(es1);
        // 创建一个EnumSet空集合,指定其集合元素是Season类的枚举值
        EnumSet es2 = EnumSet.noneOf(Season.class);
        System.out.println(es2);
        // 手动添加两个元素
        es2.add(Season.WINTER);
        es2.add(Season.SPRING);
        System.out.println(es2);
        // 以指定枚举值创建EnumSet集合
        EnumSet es3 = EnumSet.of(Season.SUMMER, Season.WINTER);
        System.out.println(es3);
        EnumSet es4 = EnumSet.range(Season.SUMMER, Season.WINTER);
        System.out.println(es4);
        // 新创建的EnumSet集合元素和es4集合元素有相同的类型
        // es5集合元素 + es4集合元素 = Season枚举类的全部枚举值
        EnumSet es5 = EnumSet.complementOf(es4);
        System.out.println(es5);

        Collection c = new HashSet();
        c.clear();
        c.add(Season.FALL);
        c.add(Season.SPRING);
        // 复制Collection集合中的所有元素来创建EnumSet集合
        EnumSet enumSet = EnumSet.copyOf(c);
        System.out.println(enumSet);
    }
}

在这里插入图片描述

发布了135 篇原创文章 · 获赞 142 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/asmartkiller/article/details/105049688
今日推荐