您的位置:首页 > 编程语言 > Java开发

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

2016-05-24 16:21 309 查看
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]

}
}


!!可以看到完全按照插入顺序来的;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: