Java基础——Collection集合-Set接口
提示:文章写Collection集合中的Set接口
一、Set接口
1、概述
Set 集合就像是一个罐子,将数据丢进来就行,因为是丢进来的,Set集合通常不会记忆元素的添加顺序(无序)。Set接口的方法和Collection基本相同,但Set不允许包含重复(无序)的元素。 Set接口的实现类常用的有:HashSet、TreeSet和LinkedHashSet。特点有:元素不可重复
2、Set的常用方法
| add(E e) | 如果 set 中尚未存在指定的元素,则添加此元素 |
| clear() | 移除此 set 中的所有元素 |
| contains(Object o) | 如果 set 包含指定的元素,则返回 true |
| isEmpty() | 如果 set 不包含元素,则返回 true |
| size() | 返回 set 中的元素数(其容量) |
3、案例
import java.util.HashSet;
public class Set_Test {
public static void main(String[] args) {
HashSet<String> stringHashSet = new HashSet<>();
stringHashSet.add("java");
stringHashSet.add("hello");
stringHashSet.add("world");
stringHashSet.add("java");
System.out.println(stringHashSet);//直接遍历stringHashSet中的元素
stringHashSet.contains("hello");//判断集合是否为空
System.out.println(stringHashSet.size());//显示集合中元素的数量
stringHashSet.clear();
System.out.println(stringHashSet.size());//判断clear后集合中元素的数量
}
}
二、HashSet类
1、HashSet概述
HashSet是Set接口的典型实现,大多时候使用Set集合的时候都是使用这个实现类。因为使用Hash算法来储存元素,因此有很好的存储于查找性能。
2、数据结构
数据结构:底层采用了哈希表,哈希表的本质就是“数组+链表”
| ① 根据对象的哈希值计算(求余)存储位置,如果当前位置没有元素则直接存入 |
| ② 如果当前位置有,则拿当前的元素和已经存在的元素比较哈希值,如果哈希值不同,则将当前元素进行存储 |
| ③ 如果哈希值相同则通过equals()方法比较两个元素的内容,如果内容不相同则将当前元素进行存储,如果内容相同则不存储当前元素 |

3、案例
public class HashSetTest {
public static void main(String[] args) {
Set<Student> hs = new HashSet<Student>();
//创建学生对象
Student s1 = new Student("林青霞", 30);
Student s2 = new Student("张曼玉", 35);
Student s3 = new Student("王祖贤", 33);
Student s4 = new Student("王祖贤", 33);
//把学生添加到集合
hs.add(s1);
hs.add(s2);
hs.add(s3);
hs.add(s4);
//遍历集合(增强for)
for (Student s : hs) {
System.out.println(s.toString());
}
}
}
class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
//重写equal方法
@Override
public boolean equals(Object o) {
System.out.println("equals()被调用了......");
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
if (age != student.age) return false;
return name != null ? name.equals(student.name) : student.name == null;
}
/**重写hashCode方法
@Override
public int hashCode() {
System.out.println("hashCode()被调用了......");
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
*/
}
三、LinkedHashSet类
1、概述
LinkedHashSet是Set接口基于双向链表的实现,也是 HashSet 的子类。元素的存储和取出顺序是一致的。特点:有序,元素唯一。
2、数据结构
LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的,如图:

LinkedHashSet 的有序性本质是 LinkedHashMap 的特性,其核心逻辑简化如下:
// LinkedHashMap 核心结构(简化)
class LinkedHashMap<K,V> extends HashMap<K,V> {
// 双向链表的头尾节点
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
// 将新节点链接到双向链表的尾部
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null) {
// 【核心】如果是第一个元素,head 指向该节点
head = p;
} else {
// 非第一个元素:维护双向链表的引用关系
p.before = last;
last.after = p;
}
}
}
3、案例
import java.util.Iterator;
import java.util.LinkedHashSet;
public class LinkedHashSetTest {
public static void main(String[] args) {
LinkedHashSet<Object> set = new LinkedHashSet<>();//创建LinkedHashSet集合
set.add(456);
set.add(123);
set.add(123);
set.add("AA");
set.add("CC");
set.add(new Student("Tom",12));
set.add(new Student("Tom",12));
set.add(129);
System.out.print("[ ");
Iterator<Object> iterator = set.iterator();//使用迭代器遍历
while (iterator.hasNext()){
Object next = iterator.next();
System.out.print(next + " ");//根据遍历后的结果观察特点
}
System.out.println("]");
}
}
四、TreeSet类
1、概述
TreeSet是Set接口基于红黑树的实现类;默认对元素进行自然排序;
2、数据结构
public class TreeNode{
int val;
TreeNode left;
TreeNode right;
TreeNode(int x){
val = x;
}
}

我们可以观察到它的根节点为18,左侧节点为小于根节点的元素,右侧是大于根节点的元素。左侧元素14,它的分支左侧为小于14的元素,右侧为大于14的元素,依次排序。根节点右侧同左侧一样。当我们插入数据时可以提高效率。它可以根据红黑二叉树进行排序。
3、案例
public class TreeSetTest {
public static void main(String[] args) {
Set<Integer> set = new TreeSet<Integer>();
set.add(10);
set.add(40);
set.add(30);
set.add(50);
set.add(20);
set.add(30);
for (Integer i : set) {
System.out.println(i);
}
}
}
四、Comparator比较器
如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。
import java.util.Arrays;
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
public class TreeSetTest2 {
public static void main(String[] args) {
TreeSet<Integer> intSet = new TreeSet<>();
intSet.add(5);
intSet.add(2);
intSet.add(8);
intSet.add(2);
intSet.add(9);
intSet.add(1);//添加数据
System.out.println(intSet);
System.out.println(intSet.first());//打印第一个数据
System.out.println(intSet.last());//打印第二个数据
//使用内部类实现Comparator接口
Set<String> strSet = new TreeSet<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
if (o1.length() != o2.length()){
return o1.length() - o2.length();//根据比较集合长度进行升序排序
}
return o1.compareTo(o2);//根据比较集合内容进行升序排序
}
});
strSet.addAll(Arrays.asList("张三","李四","王五","赵六儿","孙七","周八","张三"));
System.out.println(strSet);
}
}
五、Iterator迭代器
1、Iterator接口概述
在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口java.util.Iterator。Iterator接口也是Java集合中的一员,但它与Collection、Map接口有所不同,Collection接口与Map接口主要用于存储元素,而Iterator主要用于迭代访问(即遍历)Collection中的元素,因此Iterator对象也被称为迭代器。
2、方法
| public Iterator iterator() | 获取集合对应的迭代器,用来遍历集合中的元素的 |
| public E next() | 返回迭代的下一个元素。 |
| public boolean hasNext() | 如果仍有元素可以迭代,则返回 true。 |
3、案例
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class TestIterator {
public void test01(){
Collection coll = new ArrayList();
coll.add("张三");
coll.add("李四");
coll.add("王五");
Iterator iterator = coll.iterator();
System.out.println(iterator.next());
System.out.println(iterator.next());
System.out.println(iterator.next());
System.out.println(iterator.next());
}
public void test02(){
Collection coll = new ArrayList();
coll.add("张三");
coll.add("李四");
coll.add("王五");
Iterator iterator = coll.iterator();//获取迭代器对象
while(iterator.hasNext()) {//判断是否还有元素可迭代
System.out.println(iterator.next());//取出下一个元素
}
}
}
4、 使用Iterator迭代器删除元素
方法:void remove() ;
5、Iterator迭代器的快速失败(fail-fast)机制
如果在Iterator、ListIterator迭代器创建后的任意时间从结构上修改了集合(通过迭代器自身的 remove 或 add 方法之外的任何其他方式),则迭代器将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就完全失败,而不是冒着在将来不确定的时间任意发生不确定行为的风险。
这样设计是因为,迭代器代表集合中某个元素的位置,内部会存储某些能够代表该位置的信息。当集合发生改变时,该信息的含义可能会发生变化,这时操作迭代器就可能会造成不可预料的事情。因此,果断抛异常阻止,是最好的方法。这就是Iterator迭代器的快速失败(fail-fast)机制。
5.1、ConcurrentModificationException异常
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class TestConcurrentModificationException {
public static void main(String[] args) {
Collection coll = new ArrayList();
coll.add("hello");
coll.add("world");
coll.add("java");
coll.add("haha");
coll.add("mysql");
Iterator iterator = coll.iterator();
while(iterator.hasNext()){
String str = (String)iterator.next();
if(str.contains("a")){
coll.remove(str);//foreach遍历集合过程中,调用集合的remove方法
}
}
/*for (Object o : coll) {
String str = (String) o;
if(str.contains("a")){
coll.remove(o);//foreach遍历集合过程中,调用集合的remove方法
}
}*/
}
}
5.2、modCount变量
迭代器如何实现快速失败(fail-fast)机制的呢?
-
在ArrayList等集合类中都有一个modCount变量。它用来记录集合的结构被修改的次数。
-
当我们给集合添加和删除操作时,会导致modCount++。
-
然后当我们用Iterator迭代器遍历集合时,创建集合迭代器的对象时,用一个变量记录当前集合的modCount。例如:
int expectedModCount = modCount;,并且在迭代器每次next()迭代元素时,都要检查expectedModCount != modCount,如果不相等了,那么说明你调用了Iterator迭代器以外的Collection的add,remove等方法,修改了集合的结构,使得modCount++,值变了,就会抛出ConcurrentModificationException。
更多推荐

所有评论(0)