最新Java基础系列课程--Day11-范型与集合(二)https://developer.aliyun.com/article/1423517
五、Set系列集合
5.1 认识Set集合的特点
Set集合是属于Collection体系下的另一个分支,它的特点如下图所示
下面我们用代码简单演示一下,每一种Set集合的特点。
//Set<Integer> set = new HashSet<>(); //无序、无索引、不重复 //Set<Integer> set = new LinkedHashSet<>(); //有序、无索引、不重复 Set<Integer> set = new TreeSet<>(); //可排序(升序)、无索引、不重复 set.add(666); set.add(555); set.add(555); set.add(888); set.add(888); set.add(777); set.add(777); System.out.println(set); //[555, 666, 777, 888]
5.2 HashSet集合底层原理
接下来,为了让同学们更加透彻的理解HashSet为什么可以去重,我们来看一下它的底层原理。
HashSet集合底层是基于哈希表实现的,哈希表根据JDK版本的不同,也是有点区别的
- JDK8以前:哈希表 = 数组+链表
- JDK8以后:哈希表 = 数组+链表+红黑树
我们发现往HashSet集合中存储元素时,底层调用了元素的两个方法:一个是hashCode方法获取元素的hashCode值(哈希值);另一个是调用了元素的equals方法,用来比较新添加的元素和集合中已有的元素是否相同。
- 只有新添加元素的hashCode值和集合中以后元素的hashCode值相同、新添加的元素调用equals方法和集合中已有元素比较结果为true, 才认为元素重复。
- 如果hashCode值相同,equals比较不同,则以链表的形式连接在数组的同一个索引为位置(如上图所示)
在JDK8开始后,为了提高性能,当链表的长度超过8时,就会把链表转换为红黑树,如下图所示:
5.3 HashSet去重原理
前面我们学习了HashSet存储元素的原理,依赖于两个方法:一个是hashCode方法用来确定在底层数组中存储的位置,另一个是用equals方法判断新添加的元素是否和集合中已有的元素相同。
要想保证在HashSet集合中没有重复元素,我们需要重写元素类的hashCode和equals方法。比如以下面的Student类为例,假设把Student类的对象作为HashSet集合的元素,想要让学生的姓名和年龄相同,就认为元素重复。
public class Student{ private String name; //姓名 private int age; //年龄 private double height; //身高 //无参数构造方法 public Student(){} //全参数构造方法 public Student(String name, int age, double height){ this.name=name; this.age=age; this.height=height; } //...get、set、toString()方法自己补上.. //按快捷键生成hashCode和equals方法 //alt+insert 选择 hashCode and equals @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; if (age != student.age) return false; if (Double.compare(student.height, height) != 0) return false; return name != null ? name.equals(student.name) : student.name == null; } @Override public int hashCode() { int result; long temp; result = name != null ? name.hashCode() : 0; result = 31 * result + age; temp = Double.doubleToLongBits(height); result = 31 * result + (int) (temp ^ (temp >>> 32)); return result; } }
接着,写一个测试类,往HashSet集合中存储Student对象。
public class Test{ public static void main(String[] args){ Set<Student> students = new HashSet<>(); Student s1 = new Student("至尊宝",20, 169.6); Student s2 = new Student("蜘蛛精",23, 169.6); Student s3 = new Student("蜘蛛精",23, 169.6); Student s4 = new Student("牛魔王",48, 169.6); students.add(s1); students.add(s2); students.add(s3); students.add(s4); for(Student s : students){ System.out.println(s); } } }
打印结果如下,我们发现存了两个蜘蛛精,当时实际打印出来只有一个,而且是无序的。
Student{name='牛魔王', age=48, height=169.6} Student{name='至尊宝', age=20, height=169.6} Student{name='蜘蛛精', age=23, height=169.6}
5.4 LinkedHashSet底层原理
接下来,我们再学习一个HashSet的子类LinkedHashSet类。LinkedHashSet它底层采用的是也是哈希表结构,只不过额外新增了一个双向链表来维护元素的存取顺序。如下下图所示:
每次添加元素,就和上一个元素用双向链表连接一下。第一个添加的元素是双向链表的头节点,最后一个添加的元素是双向链表的尾节点。
把上个案例中的集合改成LinkedList集合,我们观察效果怎样
public class Test{ public static void main(String[] args){ Set<Student> students = new LinkedHashSet<>(); Student s1 = new Student("至尊宝",20, 169.6); Student s2 = new Student("蜘蛛精",23, 169.6); Student s3 = new Student("蜘蛛精",23, 169.6); Student s4 = new Student("牛魔王",48, 169.6); students.add(s1); students.add(s2); students.add(s3); students.add(s4); for(Student s : students){ System.out.println(s); } } }
打印结果如下
Student{name='至尊宝', age=20, height=169.6} Student{name='蜘蛛精', age=23, height=169.6} Student{name='牛魔王', age=48, height=169.6}
5.5 TreeSet集合
最后,我们学习一下TreeSet集合。TreeSet集合的特点是可以对元素进行排序,但是必须指定元素的排序规则。
如果往集合中存储String类型的元素,或者Integer类型的元素,它们本身就具备排序规则,所以直接就可以排序。
Set<Integer> set1= new TreeSet<>(); set1.add(8); set1.add(6); set1.add(4); set1.add(3); set1.add(7); set1.add(1); set1.add(5); set1.add(2); System.out.println(set1); //[1,2,3,4,5,6,7,8] Set<Integer> set2= new TreeSet<>(); set2.add("a"); set2.add("c"); set2.add("e"); set2.add("b"); set2.add("d"); set2.add("f"); set2.add("g"); System.out.println(set1); //[a,b,c,d,e,f,g]
如果往TreeSet集合中存储自定义类型的元素,比如说Student类型,则需要我们自己指定排序规则,否则会出现异常。
//创建TreeSet集合,元素为Student类型 Set<Student> students = new TreeSet<>(); //创建4个Student对象 Student s1 = new Student("至尊宝",20, 169.6); Student s2 = new Student("紫霞",23, 169.8); Student s3 = new Student("蜘蛛精",23, 169.6); Student s4 = new Student("牛魔王",48, 169.6); //添加Studnet对象到集合 students.add(s1); students.add(s2); students.add(s3); students.add(s4); System.out.println(students);
此时运行代码,会直接报错。原因是TreeSet不知道按照什么条件对Student对象来排序。
我们想要告诉TreeSet集合按照指定的规则排序,有两种办法:
第一种:让元素的类实现Comparable接口,重写compareTo方法
第二种:在创建TreeSet集合时,通过构造方法传递Compartor比较器对象
- **排序方式1:**我们先来演示第一种排序方式
//第一步:先让Student类,实现Comparable接口 //注意:Student类的对象是作为TreeSet集合的元素的 public class Student implements Comparable<Student>{ private String name; private int age; private double height; //无参数构造方法 public Student(){} //全参数构造方法 public Student(String name, int age, double height){ this.name=name; this.age=age; this.height=height; } //...get、set、toString()方法自己补上.. //第二步:重写compareTo方法 //按照年龄进行比较,只需要在方法中让this.age和o.age相减就可以。 /* 原理: 在往TreeSet集合中添加元素时,add方法底层会调用compareTo方法,根据该方法的 结果是正数、负数、还是零,决定元素放在后面、前面还是不存。 */ @Override public int compareTo(Student o) { //this:表示将要添加进去的Student对象 //o: 表示集合中已有的Student对象 return this.age-o.age; } }
此时,再运行测试类,结果如下
Student{name='至尊宝', age=20, height=169.6} Student{name='紫霞', age=20, height=169.8} Student{name='蜘蛛精', age=23, height=169.6} Student{name='牛魔王', age=48, height=169.6}
- **排序方式2:**接下来演示第二种排序方式
//创建TreeSet集合时,传递比较器对象排序 /* 原理:当调用add方法时,底层会先用比较器,根据Comparator的compare方是正数、负数、还是零,决定谁在后,谁在前,谁不存。 */ //下面代码中是按照学生的年龄升序排序 Set<Student> students = new TreeSet<>(new Comparator<Student>{ @Override public int compare(Student o1, Student o2){ //需求:按照学生的身高排序 return Double.compare(o1,o2); } }); //创建4个Student对象 Student s1 = new Student("至尊宝",20, 169.6); Student s2 = new Student("紫霞",23, 169.8); Student s3 = new Student("蜘蛛精",23, 169.6); Student s4 = new Student("牛魔王",48, 169.6); //添加Studnet对象到集合 students.add(s1); students.add(s2); students.add(s3); students.add(s4); System.out.println(students);
5.6 总结Collection集合
最后,将所有的Collection集合总结一下,要求大家掌握每一种集合的特点,以及他们的体系结构。
好了,关于Collection集合,到这里就学习完了。
5.7 并发修改异常
学完Collection集合后,还有一个小问题需要给同学们补充说明一下,那就是在使用迭代器遍历集合时,可能存在并发修改异常。
我们先把这个异常用代码演示出来,再解释一下为什么会有这个异常产生
List<String> list = new ArrayList<>(); list.add("王麻子"); list.add("小李子"); list.add("李爱花"); list.add("张全蛋"); list.add("晓李"); list.add("李玉刚"); System.out.println(list); // [王麻子, 小李子, 李爱花, 张全蛋, 晓李, 李玉刚] //需求:找出集合中带"李"字的姓名,并从集合中删除 Iterator<String> it = list.iterator(); while(it.hasNext()){ String name = it.next(); if(name.contains("李")){ list.remove(name); } } System.out.println(list);
运行上面的代码,会出现下面的异常。这就是并发修改异常
为什么会出现这个异常呢?那是因为迭代器遍历机制,规定迭代器遍历集合的同时,不允许集合自己去增删元素,否则就会出现这个异常。
怎么解决这个问题呢?不使用集合的删除方法,而是使用迭代器的删除方法,代码如下:
List<String> list = new ArrayList<>(); list.add("王麻子"); list.add("小李子"); list.add("李爱花"); list.add("张全蛋"); list.add("晓李"); list.add("李玉刚"); System.out.println(list); // [王麻子, 小李子, 李爱花, 张全蛋, 晓李, 李玉刚] //需求:找出集合中带"李"字的姓名,并从集合中删除 Iterator<String> it = list.iterator(); while(it.hasNext()){ String name = it.next(); if(name.contains("李")){ //list.remove(name); it.remove(); //当前迭代器指向谁,就删除谁 } } System.out.println(list);