[疯狂Java]集合:Set、HashSet、LinkedHashSet

1. Set:

    1) Set其实就是Collection,它几乎没有扩展Collection的任何功能(没有在Collection的基础上添加任何额外的方法);

    2) 只不过Set的add方法要求添加的元素不能重复,如果重复则添加失败返回false;


2. HashSet:

    1) 是Set的最常用的一种实现,实现方法是哈希表;

    2) 按照哈希算法存储元素:

         i. 当一个元素add进来的时候,或先调用元素的hashCode方法得到一个哈希值;

         ii. 然后根据哈希值找到对应的哈希链(得到链头),链头称为“槽位”,链本身称谓“槽位”上的“桶”;

         iii. 由于多个元素可能哈希值冲突,因此桶里可能包含多个元素,因此需要拿插入元素跟桶里的所有元素进行比较(equals方法),如果桶里还没有该元素则将该元素加入桶中(哈希链的链尾插入),否则就代表有元素跟插入元素重复,则拒绝插入!返回false;

    3) 从算法中了解到,HashSet中元素的两个关键方法hashCode和equals方法,这两个方法非常重要,必须要合理实现:

         i. hashCode决定槽位,equals决定同理是否有重复元素;

         ii. 两者合并后得出的结论是:hashCode和equals共同决定集合中元素是否重复;

         iii. 如果两个元素hashCode相同但equals不同,那么就位于同一个桶中(槽位相同),如果两个元素hashCode不同但equals相同则位于两个不同的桶中(槽位不同),这两种情况都属于不同的元素;

         iv. 只有hashCode和equals同时相同才属于真正的重复!

    4) 规范:HashSet保存的元素必须要实现hashCode和equals方法,并且两者最好应该保持一致(即hashCode相同则equals一定相同),这样可以保证每个槽位只有一个元素,不会形成桶,因为桶使用链表维护的,会降低效率;

!!并且两者不一致可能也会导致很多意外的错误和混乱!

    5) 实验:

class A { // equals恒等,但hashCode恒不等(使用默认的地址作为hashCode)

	@Override
	public boolean equals(Object obj) {
		// TODO Auto-generated method stub
		return true;
	}
	
}

class B { // hashCode恒等,但equals恒不等(默认使用地址进行比较)

	@Override
	public int hashCode() {
		// TODO Auto-generated method stub
		return 1;
	}
	
}

class C { // 两者都恒等

	@Override
	public boolean equals(Object obj) {
		// TODO Auto-generated method stub
		return true;
	}

	@Override
	public int hashCode() {
		// TODO Auto-generated method stub
		return 2;
	}
	
}

public class Test {
	public static void main(String[] args) {
		
		HashSet set = new HashSet();
		
		set.add(new A());
		set.add(new A());
		set.add(new B());
		set.add(new B());
		set.add(new C());
		set.add(new C());
		
		System.out.println(set);
		
	}
}
!打印结果看到:[com.lirx.B@1, com.lirx.B@1, com.lirx.C@2, com.lirx.A@1db9742, com.lirx.A@106d69c]

!!A有两个(两个不同的随机hashCode),B也有两个(hashCode相同),C只有一个,说明A和B都没有重复,只不过B位于同一个桶中而已;


3. HashSet中最好不要保存可变元素!

    1) 设想本来HashSet中添加了若干元素,一切都是好好的,里面都没有元素重复,但是如果后来修改了其中的元素,由于修改后可能导致hashCode、equals计算得到的值完全相同,但此时这两个元素还位于集合中,这不就和不重复的规矩相悖了吗?这完全有可能!而且很容易导致错误和混乱!

    2) 一个最简单的案例:

class A { int val; } -> hashCode: val  -> equals:val,即hashCode直接用val表示,equals直接用val比较

然后HashSet set -> add: 1, 2, 3, 4(类型都是A,值都是val的值)

!!由于只有在add时才会检查是否重复,而修改时不会,那么现在我们修改第一个元素,把其修改成3,那么集合就编程了3, 2, 3, 4,其中第一个和第三个元素的hashCode和equals都相同,因此两者重复了,但是它们的槽位都是按照原来(1, 2, 3, 4)计算的,也就是说位置图其实是这样的:

hash地址   元素                                   hash地址    元素

1                  1                                         1                   3

2                  2             ---变成了-->       2                   2

3                  3                                         3                   3

4                  4                                         4                   4

!明显可以看到第一个元素3,按照其hashCode方法计算得到的hash值应该是3,和现在其位于的槽位(hash值)1不相符,出现了错误,那么这样的错误会导致什么严重的后果呢??

!!接着我们set.remove(3);试图把3这个元素删除,那么根据哈希算法,会先计算3的哈希值(3),然后就定位到第三个元素,然后在槽位3的桶中用equals寻找3这个元素,找到了然后删除,然后结果就变成了

hash地址   元素

1                  3

2                  2

4                  4
!它并没有删除第一个3,因为第一个元素的槽位是1,1这个地址似乎无法根据3这个值计算出来的!

!!接着我们调用set.contains(3);查看3是否位于集合中,此时还是根据hash算法来定位这个元素,发现集合中根本没有3这个槽位,因此返回false,即值3不在集合中!这明显错误了!

    3) 因此HashSet最好不要存放可变元素,因为当你改变元素的时候可能会导致元素重复!而这种重复可能会导致未知错误!


4. LinkedHashSet:

    1) HashSet不维护元素的插入顺序,哈希地址越小顺序越靠前,但是LinkedHashSet维护元素插入顺序;

    2) LinkedHashSet结构完全和HashSet一样,方法也完全一样,只不过就是比HashSet多维护了一张链表,用来记录元素的插入顺序,而遍历元素的时候就是顺着这张链表遍历的;

    3) 优点和缺点:

         i. 优点很明显,可以维护插入顺序,并且在遍历迭代的时候效率高(直接遍历这张链表即可);

         ii. 缺点:在插入删除元素时需要额外维护这张链表,因此需要消耗一定的时间,同时链表也需要额外的存储空间;

    4) 测试:

public class Test {
	public static void main(String[] args) {
		
		LinkedHashSet set = new LinkedHashSet();
		
		set.add(1);
		set.add(2);
		System.out.println(set); // [1, 2]
		
		set.remove(1);
		set.add(1);
		System.out.println(set); // [2, 1]
		
	}
}

!!可以看到完全按照插入顺序来的;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值