java-集合老大哥Collection-小儿子Set-详解

Set

同样继承Collection接口,小儿子Set与大儿子List完全相反(一个不包含重复元素的Collection)

  1. 无序(HashSet)
  2. 无索引
  3. 不存在重复元素

常见的实现类:HashSetLinkedHashSet

HashSet实现Set接口,是由基于数组的哈希表(实际上是一个HashMap实例)支持,不保证set的迭代顺序,特别是它不保证该顺序恒久不变,此类允许使用null元素,注意此实现不是同步的(线程不安全)

HashSet()方法源码:默认初始容量16的数组(每个位置存放一个链表),负载因子0.75(16*0.75=12,存储12个位置后,开始一倍扩容,新建容量为32的数组,将原数组内容拷贝到新数组中),初始容量与负载因子影响集合迭代(遍历)的性能

    /**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * default initial capacity (16) and load factor (0.75).
     */
    public HashSet() {
        map = new HashMap<>();
    }

HashSet的两种遍历方式(体现了集合无序的特点):

package 集合;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
/**
 * Set接口,特点:不重复元素,没索引
 *
 * Set接口的实现类HashSet
 * 特点:无序集合,存储和取出的顺序不同,没有索引,不存储重复元素
 *  	底层数据结构,哈希表,存储,取出都比较快,线程不安全,运行速度快
 * 代码的编写上和ArrayList完全一致
 */
public class HashSetDemo {
    public static void main(String[] args) {
        Set<String> set = new HashSet<String>();
        set.add("one");
        set.add("two");
        set.add("three");
        set.add("four");
        System.out.println("Iterator遍历Set集合");
        Iterator<String> it = set.iterator();
        while (it.hasNext()){
            System.out.println(it.next());
        }
        System.out.println("增强for遍历Set集合:");
        for(String str:set){
            System.out.println(str);
        }
    }
}
//输出结果
Iterator遍历Set集合
four
one
two
three
增强for遍历Set集合:
four
one
two
three

List集合有索引,遍历有三种方式(+普通的for循环),而Set集合是没有索引的,遍历方式也就只有两种

再看一个例子,看看HashSet是如何实现集合中元素不重复存储的:

Set<String> set = new HashSet<String>();
set.add("one");
set.add("two");
set.add("one");
set.add("two");
System.out.println(set);
//输出
[one, two]

这里之所以只存进去一次one,two是因为一个方法:public native int hashCode(),这个方法是Object类的本地方法,返回一个int类型的整数,可以看做对象的相关信息(比如存储地址,对象的字段等)的映射,这个映射的整数被称之为哈希值hash值

下面看一下哈希值在向集合中添加对象时是如何起作用的:
当向集合中添加对象时,如何判断集合中是否存在该对象,大多数人都会想到调用方法equals()将要插入的对象与集合中的对象逐个比较,方法确实可行,但如果集合中已存在一万条或者更多的数据,采用equals()方法来比较的话,肯定是十分低效的,这里hashCode()方法就要体现作用了:我们已经知道HashSet集合的本质是一个HashMap实例,在HashMap的具体实现中会使用一个table来保存集合中已存在对象的hash值,当集合要添加新的对象时,会先调用这个对象的hashCode()方法来得到该对象的hash值,如果table中没有该hash值,那么就可以直接存进去,如果table中存在该hash值,就再调用equals()方法将相同hash值的两个对象比较,若相同就不存了,不相同就散列到其他地址存进去,这样的话,实际调用equals()方法的次数就大大降低了

下面看一下HashMap中put方法的源码:

public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
	int h;
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
	Node<K,V>[] tab; Node<K,V> p; int n, i;
	if ((tab = table) == null || (n = tab.length) == 0)
		n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
			e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
            	if ((e = p.next) == null) {
                	p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    	treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
					break;
                p = e;
            }
		}
        if (e != null) { // existing mapping for key
        	V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
            	e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
	}
    ++modCount;
    if (++size > threshold)
    	resize();
    afterNodeInsertion(evict);
    return null;
}

大致思路:先调用要添加对象的hashCode()方法得到该对象的hash值,查看table中是否存在该hash值,若存在则调用equals()方法进行比较,若为true则更新value值,否则将新的对象添加到HashMap中,从这里可以看出hashCode()方法的存在就是为了减少equals()方法调用的次数,进而提高存取效率(取出元素要依赖该方法)

Tips:两个对象的hash值相同不代表两个对象相等,用equals()方法进行比较可能会返回false,但是hash值不同,则对象一定不相等equals()方法一定会返回false,同样的,equals()方法比较返回true,那么调用hashCode()方法得到的hash值一定相同,这是java中所规定的hashCode方法的常规协定,因此在重写equals()方法时,必须要重写hashCode()方法来维护hashCode方法的常规协定,该协定声明相等对象必须具有相等的哈希值

更进一步了解hashCode()方法,看下面的例子

package 集合;
/**
 * 对象的哈希值是普通的十进制整数
 * 父类Object,方法 public int hashCode(),计算结果为int整数
 */

public class HashDemo {
    public static void main(String[] args) {
        Person person = new Person();
        int hash = person.hashCode();
        System.out.println(hash);

        // java 中new出来的对象都是不一样的,在内存中存储地址不相同
        String str1 = new String("one");
        String str2 = new String("one"); //仅值相同,存储地址不同
        System.out.println(str1.hashCode()); //String类继承了Object类,重写了hashCode()方法,有自己的计算方式
        System.out.println(str2.hashCode()); //String的hashCode()方法计算的方式和字符串的长度,字符的顺序及每个字符的ASCII码值有关
        System.out.println(str1==str2); // == 比较的是存储地址
        System.out.println(str1.equals(str2)); // equals()方法比较的是值
    }
}
//输出
1
110182
110182
false
true

看一下person类与String类各自重写的hashCode()方法

/**
* Person类与String类都继承父类Object,可以实现自己的hashCode()方法
*/
public class Person {
    // Person类继承父类Object,可以重写hashCode()方法,给定Person类的hash值
    public int hashCode(){
        return 1;
    }
    public Person(){}
}

//String类的hashCode方法的重写
/**
* Returns a hash code for this string. The hash code for a
* {@code String} object is computed as
* <blockquote><pre>
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* </pre></blockquote>
* using {@code int} arithmetic, where {@code s[i]} is the
* <i>i</i>th character of the string, {@code n} is the length of
* the string, and {@code ^} indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return  a hash code value for this object.
*/
public int hashCode() {
	int h = hash;
    if (h == 0 && value.length > 0) {
    	char val[] = value;

        for (int i = 0; i < value.length; i++) {
        	h = 31 * h + val[i];
        }
        hash = h;
	}
	return h;
}

Person类重写hashCode()方法,返回固定的hash值,而String类的hashCode()方法,如果字符串相等,那么一定会有相同的hash值,但是扔存在字符串不相等,hash值同样相等的情况(概率很小),所以判断对象是否相等还需要equals()方法

深入理解HashSet(Person)不重复存储对象

//Person类
package 集合;
public class Person {
    private String name;
    private int age;
    public Person(){}
	public Person(String name,int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
    }
}
package 集合;
import java.util.HashSet;
import java.util.Iterator;
public class HashSetDemo {
    public static void main(String[] args) {
        HashSet<Person> set = new HashSet<Person>();
        set.add(new Person("张三",20));
        set.add(new Person("李四",22));
        set.add(new Person("王五",27));
        set.add(new Person("张三",20));
        System.out.println("Iterator遍历set集合");
        Iterator<String> it = set.iterator();
        while (it.hasNext()){
            System.out.println(it.next());
        }
    }
}
//输出
Iterator遍历set集合
Person{name='张三', age=20}
Person{name='李四', age=22}
Person{name='王五', age=27}
Person{name='张三', age=20}

观察可知,对象Person{name='张三', age=20}重复存放到集合中,这是因为new操作得到的是一个新的对象,会在内存中开辟新的存储空间,因为Person类未重写equals()方法

System.out.println(new Person("张三",20).equals(new Person("张三",20))); // false

所以这里调用的是如下Object类中的equals()方法,返回值是false,会将对象Person{name='张三', age=20}重复存放到集合中

public boolean equals(Object obj) {
	return (this == obj);
}

要将Person对象中的姓名,年龄相同的看做是一个对象,需要依赖Person对象自己的hashCode()equals()方法,在Person类中重写这两个方法:

	/**
     * 对象存储哈希表,去掉重复元素,依据对象自己的hashCode,equals重写目标
     * 使对象中属性值name,age如果属性值相同,得到相同的哈希值
     * 哈希值的计算参考String,字符串的每个字符都参与计算 31*原来的计算结果 + 字符ASCII码值
     * name属性本身就是字符串,简单 name.hashCode() + age
     */
    @Override
    public int hashCode(){
        //return name.hashCode()+age; // b,10 和 a,11 哈希值相同都是108,所以需要重写equals方法
        //降低出现如上相同哈希值的概率,改进如下
        return name.hashCode()+age*2;
    }
    @Override
    public boolean equals(Object object){
        if(this == object){
            return true;
        }
        if(object == null){
            return false;
        }
        if(object instanceof Person) { //判断一个对象是否是该类的实例
            Person person = (Person) object; //是的话就可以强制转换
            return name.equals(person.name) && age == person.age;
        }
        return false;
    }

再次运行,观察是否会重复存放姓名,年龄相同的对象,观察结果如下:

Iterator遍历set集合
Person{name='张三', age=20}
Person{name='王五', age=27}
Person{name='李四', age=22}

对象Person{name='张三', age=20}未重复存放

Tips:重写equals()方法时,必须重写hashCode()方法,下面是摘自Effective Java的一段话

  • 在程序执行期间,只要equals()方法的比较操作用到的信息没有被修改,那么对这同一个对象调用多次,hashCode()方法必须始终如一的返回同一个整数
  • 如果两个对象根据equals()方法比较是相等的,那么调用两个对象的hashCode()方法必须返回相同的整数结果
  • 如果两个对象根据equals()方法比较是不等的,则hashCode()方法不一定要返回不同的整数
  • 设计hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都应该产生同样的值,如果在将一个对象用put()添加进HashMap时产生一个hash值,而用get()取出时却产生了另一个hash值,那么就无法获取该对象了。所以如果你的,hashCode()方法依赖于对象中易变的数据,用户就要当心了,因为数据发生变化时,hashCode()方法会产生另一个不同的hash值,从而无法获取到原有的对象,get()得到的为null

get方法在HashMap中的源码:

public V get(Object key) {
	Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

hashCode设计实例(数据变化)

Person person1 = new Person("Lin",21);
System.out.println(person1.hashCode());
HashMap<Person,Integer> hashMap = new HashMap<Person,Integer>();
hashMap.put(person1,1);
person1.setAge(13);
System.out.println(person1.hashCode());
System.out.println(hashMap.get(person1));

运行如下:

76443
76427
null

这就是因为Person类中的hashCode()方法在重写时依赖了属性age,如下

@Override
public int hashCode(){
	return name.hashCode()+age*2;
}

所以当age发生变化时,get()得到就是null

LinkedHashSet

继承自HashSet,是基于链表的哈希表实现,LinkedHashSet自身特性:具有顺序,存储和取出的顺序是相同的,同样的,实现是不同步的(线程不安全),运行速度快
一句话:有序的Set集合

package 集合;
import java.util.LinkedHashSet;
public class LinkedHashSetDemo {
    public static void main(String[] args) {
        LinkedHashSet<Integer> linkedHashSet = new LinkedHashSet<Integer>();
        linkedHashSet.add(1);
        linkedHashSet.add(2);
        linkedHashSet.add(3);
        linkedHashSet.add(4);
        linkedHashSet.add(5);
        linkedHashSet.add(1);//仍然不允许重复
        System.out.println(linkedHashSet);
    }
}
//输出
[1, 2, 3, 4, 5]
发布了22 篇原创文章 · 获赞 21 · 访问量 7009

猜你喜欢

转载自blog.csdn.net/weixin_43108122/article/details/100985304
今日推荐