Set是Collection接口的子接口,模仿数学中的集合并对其进行抽象。我们先了解一下Set集合有什么样的特点:

  • 最多包含一个null元素
  • 没有索引
  • 无序(存和读的顺序可能不同)
  • Set.add() 不允许重复,因此可能返回false(List.add()永远返回true,因为List允许有重复)

一、对Set集合的使用

Set与List一样,知道List怎么创建对象之后,我们也可以轻松了解到Set是如何创建对象的,来看代码:

	private static void method() {
		// 创建集合对象
		// HashSet<String> hs = new HashSet<String>();
		Set<String> set = new HashSet<String>();

		// 添加元素
		set.add("Hello");
		set.add("Java");
		set.add("Hadoop");

		// 遍历集合
		Object[] objs = set.toArray();
		for (int i = 0; i < objs.length; i++)
			System.out.println(objs[i]);
		
		for (String s : set)
			System.out.println(s);

		Iterator<String> it = set.iterator();
		while (it.hasNext())
			System.out.println(it.next());
	}

HashSet是Set接口的一个实现类,可以用它来创建Set对象,同时Set的遍历与List相同,此处列举了遍历Set的三种方法。

 

二、Set存储自定义类型去重

先来看一个Set存储自定义类型的实例,先是一个Student类

class Student{
	String name;
	int age;
	public Student(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
	@Override
	public String toString() {
		return "Student [name=" + name + ", age=" + age + "]";
	}
}

使用Set添加自定义类型Student的对象,并遍历set

public class HashSetDemo {

	public static void main(String[] args) {

		HashSet<Student> hs = new HashSet<Student>();
		Student s1 =new Student("asv",12);
		Student s2 = new Student ("ASD",15);
		Student s3 = new Student ("ASD",15);
		
		hs.add(s1);
		hs.add(s2);
		hs.add(s3);

		for(Student s:hs)
			System.out.println(s);
	}

}

当循环输出时,我们发现set集合中存储了两个name=ASD,age=15的Student对象,这是为什么呢,我们不是说Set集合存储的元素不能重复吗?

接下来,我们从源码逐层来分析一下原因。首先是HashSet的add方法:

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

HashSet的add方法其实就是HashMap的put方法,传入的参数为put方法的Key。那我们再来看看put方法:

 //K key:要添加的新元素
 public V put(K key, V value) {
        //根据新添加元素的hashCode()返回值计算出hash值
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        
        //获取当前集合中的每一个元素
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        //直接添加元素
        addEntry(hash, key, value, i);
        return null;
    }

put方法中有一个for循环,for循环中有一个判断语句,如果判断条件为真不添加新元素,否则添加新元素。因此,我们的重点也就是在这个if判断语句上,我们来看它的判断条件:e.hash == hash && ((k = e.key) == key || key.equals(k))。首先比较hash值,并且使用短路&,当hash值不一样时if语句结束添加新元素;如果hash值一样,再用==比较地址在或用equals方法进行比较,比较结果如果为true,则重复不在添加,否则添加新元素。
           

知道这些之后,我们就知道为什么会添加俩个属性值一样的Student对象了,新创建的Student对象虽然它们的属性相同,但地址不同,put方法中的判断语句会返回false,因此直接添加元素。那么怎么做才能让它去重呢?

很明显,我们需要重写Student的equals方法和hashCode方法,让hash值相等或者成员相等的元素不能添加到集合中去

public class Student {
	String name;
	int age;
	public Student() {
		super();
	}
	public Student(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
	@Override
	public String toString() {
		return "Person [name=" + name + ", age=" + age + "]";
	}
	
	@Override
	public int hashCode() {
		return name.hashCode() + age ;
	}
	@Override
	public boolean equals(Object obj) {
		//对象一样,直接返回true ,提高效率
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		
		//使用getClass()比较,可以不用向下转型了   ,提高健壮性
		if (getClass() != obj.getClass())
			return false;
		
		
		Student other = (Student) obj;
            //比较年龄,不相等返回false
		if (age != other.age)
			return false;

            //比较name,不相等返回false
		if (!name.equals(other.name))
			return false;

            //都相等,返回true
		return true;
	}
	
	
}

在重写hashCode方法时,我们可以让所有对象的hash值都返回一个固定的值,比如说 return 1; 这样在判断时,就可以用equals比较对象的成员变量是否一致。这样的话,有些对象成员变量完全不同,但还需要进行hash值和equals方法的比较,使得程序的效率较低。因此,如果能让成员变量不同的值,hash值也不同,就可以减少一部分equals方法的比较,从而提高程序效率。

让hashCode方法的返回值与对象的成员变量有关。上示代码中,我们让hashCode方法返回所有成员变量之和,让基本数据类型直接相加,让引用数据类型获取其hashCode方法返回值后再相加。

注意:boolean类型不能参加运算,因此boolean类型的成员变量我们就不用管了。

 

三、自动生成hashCode方法和equals方法

使用工具Eclipse时,Eclipse为我们提供了hashCode和equals方法的重写,右键找到Source

 

@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + age;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Person other = (Person) obj;
		if (age != other.age)
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		return true;
	}

在自动生成的hashCode方法中,为什么使用数字31,主要有一下原因:

1. 使用质数计算hash值,质数与其他数相乘后,重复的概率小,结果唯一的概率更大。

2. 使用质数越大,冲突虽然减小,但计算速度变慢,31是哈希冲突和性能的折中。

3. JVM会自动对31进行优化,即31*i = (i<<5)-1.

 

 

 

posted on 2018-07-31 13:28  七宝嘤嘤怪  阅读(318)  评论(0编辑  收藏  举报